diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/VoicemailService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/VoicemailService.kt index 84ad585..27ee78c 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/VoicemailService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/VoicemailService.kt @@ -3,9 +3,7 @@ package com.icing.dialer.services import android.Manifest import android.content.Context import android.content.pm.PackageManager -import android.database.Cursor import android.net.Uri -import android.os.Build import android.provider.VoicemailContract import android.telecom.TelecomManager import android.util.Log @@ -23,29 +21,16 @@ class VoicemailService(private val context: Context) : MethodChannel.MethodCallH override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "getVoicemails" -> { + if (!isDefaultDialer() || !hasVoicemailPermissions()) { + result.error("NO_ACCESS", "Need default‑dialer & voicemail perms", null) + return + } try { - Log.d(TAG, "Getting voicemails...") - - // Check if app is default dialer - if (!isDefaultDialer()) { - Log.e(TAG, "App is not the default dialer, cannot access voicemails") - result.error("NOT_DEFAULT_DIALER", "App must be set as default dialer to access voicemails", null) - return - } - - // Check permissions - if (!hasVoicemailPermissions()) { - Log.e(TAG, "Missing voicemail permissions") - result.error("MISSING_PERMISSIONS", "App doesn't have required voicemail permissions", null) - return - } - - val voicemails = getVoicemails() - Log.d(TAG, "Found ${voicemails.size} voicemails") - result.success(voicemails) + val list = getVoicemails() + result.success(list) } catch (e: Exception) { - Log.e(TAG, "Error getting voicemails", e) - result.error("VOICEMAIL_ERROR", e.message, null) + Log.e(TAG, "getVoicemails failed", e) + result.error("ERROR", e.message, null) } } "markVoicemailAsRead" -> { @@ -82,92 +67,74 @@ class VoicemailService(private val context: Context) : MethodChannel.MethodCallH } private fun hasVoicemailPermissions(): Boolean { - val permissions = arrayOf( + return listOf( Manifest.permission.READ_VOICEMAIL, Manifest.permission.WRITE_VOICEMAIL, - Manifest.permission.ADD_VOICEMAIL // Added this permission - ) - - return permissions.all { + Manifest.permission.ADD_VOICEMAIL + ).all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } } private fun isDefaultDialer(): Boolean { - val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - return telecomManager.defaultDialerPackage == context.packageName + val tm = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + return tm.defaultDialerPackage == context.packageName } - private fun getVoicemails(): List> { // Changed return type to Any? - val voicemails = mutableListOf>() // Changed type to Any? + private fun getVoicemails(): List> { + val out = mutableListOf>() val uri = VoicemailContract.Voicemails.CONTENT_URI - - val projection = arrayOf( + val proj = arrayOf( VoicemailContract.Voicemails._ID, VoicemailContract.Voicemails.NUMBER, VoicemailContract.Voicemails.DATE, VoicemailContract.Voicemails.DURATION, VoicemailContract.Voicemails.HAS_CONTENT, - VoicemailContract.Voicemails.IS_READ, - VoicemailContract.Voicemails.SOURCE_PACKAGE + VoicemailContract.Voicemails.IS_READ ) - context.contentResolver.query( - uri, - projection, - null, - null, - "${VoicemailContract.Voicemails.DATE} DESC" - )?.use { cursor -> - while (cursor.moveToNext()) { - val id = cursor.getString(cursor.getColumnIndexOrThrow(VoicemailContract.Voicemails._ID)) - val number = cursor.getString(cursor.getColumnIndexOrThrow(VoicemailContract.Voicemails.NUMBER)) - val date = cursor.getLong(cursor.getColumnIndexOrThrow(VoicemailContract.Voicemails.DATE)) - val duration = cursor.getLong(cursor.getColumnIndexOrThrow(VoicemailContract.Voicemails.DURATION)) - val hasContent = cursor.getInt(cursor.getColumnIndexOrThrow(VoicemailContract.Voicemails.HAS_CONTENT)) == 1 - val isRead = cursor.getInt(cursor.getColumnIndexOrThrow(VoicemailContract.Voicemails.IS_READ)) == 1 + context.contentResolver.query(uri, proj, null, null, + "${VoicemailContract.Voicemails.DATE} DESC")?.use { c -> + while (c.moveToNext()) { + val id = c.getString(c.getColumnIndexOrThrow(VoicemailContract.Voicemails._ID)) + val number = c.getString(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.NUMBER)) + val date = c.getLong(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.DATE)) + val dur = c.getLong(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.DURATION)) + val hasBlob = c.getInt(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.HAS_CONTENT)) == 1 + val isRead = c.getInt(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.IS_READ)) == 1 - var filePath: String? = null - if (hasContent) { - filePath = saveVoicemailToFile(id) - } + val filePath = if (hasBlob) saveVoicemailToFile(id) else null - val voicemail: Map = mapOf( // Added explicit type - "id" to id, - "phoneNumber" to (number ?: "Unknown"), - "timestamp" to date, - "duration" to (duration / 1000).toInt(), // Convert to seconds - "filePath" to filePath, - "isRead" to isRead + out += mapOf( + "id" to id, + "phoneNumber" to (number ?: ""), + "timestamp" to date, + "duration" to dur.toInt(), + "filePath" to filePath, + "isRead" to isRead ) - voicemails.add(voicemail) } } - - return voicemails + return out } private fun saveVoicemailToFile(voicemailId: String): String? { - val voicemailUri = Uri.withAppendedPath(VoicemailContract.Voicemails.CONTENT_URI, voicemailId) - - try { - val inputStream = context.contentResolver.openInputStream(voicemailUri) ?: return null - val cacheDir = context.cacheDir - val voicemailFile = File(cacheDir, "voicemail_$voicemailId.3gp") - - FileOutputStream(voicemailFile).use { outputStream -> - val buffer = ByteArray(4096) - var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - outputStream.write(buffer, 0, bytesRead) + val vUri = Uri.withAppendedPath(VoicemailContract.Voicemails.CONTENT_URI, voicemailId) + return try { + context.contentResolver.openInputStream(vUri)?.use { inp -> + val cache = File(context.cacheDir, "vm_$voicemailId.3gp") + FileOutputStream(cache).use { out -> + val buf = ByteArray(4096) + var r: Int + while (inp.read(buf).also { r = it } > 0) { + out.write(buf, 0, r) + } } + cache.absolutePath } - - inputStream.close() - return voicemailFile.absolutePath } catch (e: Exception) { - Log.e(TAG, "Error saving voicemail to file", e) - return null + Log.e(TAG, "saveVoicemailToFile failed", e) + null } } diff --git a/dialer/lib/presentation/features/voicemail/voicemail_page.dart b/dialer/lib/presentation/features/voicemail/voicemail_page.dart index 39e0fbc..87a5411 100644 --- a/dialer/lib/presentation/features/voicemail/voicemail_page.dart +++ b/dialer/lib/presentation/features/voicemail/voicemail_page.dart @@ -92,41 +92,32 @@ class _VoicemailPageState extends State { } } - Future _togglePlayPause(Voicemail voicemail) async { - if (_currentPlayingId == voicemail.id && _isPlaying) { + Future _togglePlayPause(Voicemail vm) async { + if (vm.filePath == null) { + return _showSnack('Voicemail file not available'); + } + final src = DeviceFileSource(vm.filePath!); + if (_currentPlayingId == vm.id && _isPlaying) { await _audioPlayer.pause(); setState(() => _isPlaying = false); } else { - // If we're playing a different voicemail, stop current one - if (_isPlaying && _currentPlayingId != voicemail.id) { - await _audioPlayer.stop(); - } - - // Start playing the new voicemail - if (voicemail.filePath != null) { - try { - await _audioPlayer.play(DeviceFileSource(voicemail.filePath!)); - setState(() { - _isPlaying = true; - _currentPlayingId = voicemail.id; - }); - - // Mark as read if not already - if (!voicemail.isRead) { - await _voicemailService.markAsRead(voicemail.id); - } - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error playing voicemail: $e')), - ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Voicemail file not available')), - ); + if (_isPlaying) await _audioPlayer.stop(); + try { + await _audioPlayer.play(src); + setState(() { + _isPlaying = true; + _currentPlayingId = vm.id; + }); + if (!vm.isRead) await _voicemailService.markAsRead(vm.id); + } catch (e) { + _showSnack('Error playing voicemail: $e'); } } } + + void _showSnack(String msg) { + if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + } void _makeCall(String phoneNumber) { _callService.makeGsmCall(context, phoneNumber: phoneNumber);