refactor: streamline voicemail retrieval and playback handling with improved error messaging
All checks were successful
/ mirror (push) Successful in 5s
/ build-stealth (push) Successful in 9m41s
/ build (push) Successful in 9m46s

This commit is contained in:
AlexisDanlos 2025-04-24 10:34:37 +02:00
parent ec3f13a34b
commit ea132dd348
2 changed files with 68 additions and 110 deletions

View File

@ -3,9 +3,7 @@ package com.icing.dialer.services
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.VoicemailContract import android.provider.VoicemailContract
import android.telecom.TelecomManager import android.telecom.TelecomManager
import android.util.Log import android.util.Log
@ -23,29 +21,16 @@ class VoicemailService(private val context: Context) : MethodChannel.MethodCallH
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getVoicemails" -> { "getVoicemails" -> {
if (!isDefaultDialer() || !hasVoicemailPermissions()) {
result.error("NO_ACCESS", "Need defaultdialer & voicemail perms", null)
return
}
try { try {
Log.d(TAG, "Getting voicemails...") val list = getVoicemails()
result.success(list)
// 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)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error getting voicemails", e) Log.e(TAG, "getVoicemails failed", e)
result.error("VOICEMAIL_ERROR", e.message, null) result.error("ERROR", e.message, null)
} }
} }
"markVoicemailAsRead" -> { "markVoicemailAsRead" -> {
@ -82,92 +67,74 @@ class VoicemailService(private val context: Context) : MethodChannel.MethodCallH
} }
private fun hasVoicemailPermissions(): Boolean { private fun hasVoicemailPermissions(): Boolean {
val permissions = arrayOf( return listOf(
Manifest.permission.READ_VOICEMAIL, Manifest.permission.READ_VOICEMAIL,
Manifest.permission.WRITE_VOICEMAIL, Manifest.permission.WRITE_VOICEMAIL,
Manifest.permission.ADD_VOICEMAIL // Added this permission Manifest.permission.ADD_VOICEMAIL
) ).all {
return permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
} }
} }
private fun isDefaultDialer(): Boolean { private fun isDefaultDialer(): Boolean {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager val tm = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
return telecomManager.defaultDialerPackage == context.packageName return tm.defaultDialerPackage == context.packageName
} }
private fun getVoicemails(): List<Map<String, Any?>> { // Changed return type to Any? private fun getVoicemails(): List<Map<String, Any?>> {
val voicemails = mutableListOf<Map<String, Any?>>() // Changed type to Any? val out = mutableListOf<Map<String, Any?>>()
val uri = VoicemailContract.Voicemails.CONTENT_URI val uri = VoicemailContract.Voicemails.CONTENT_URI
val proj = arrayOf(
val projection = arrayOf(
VoicemailContract.Voicemails._ID, VoicemailContract.Voicemails._ID,
VoicemailContract.Voicemails.NUMBER, VoicemailContract.Voicemails.NUMBER,
VoicemailContract.Voicemails.DATE, VoicemailContract.Voicemails.DATE,
VoicemailContract.Voicemails.DURATION, VoicemailContract.Voicemails.DURATION,
VoicemailContract.Voicemails.HAS_CONTENT, VoicemailContract.Voicemails.HAS_CONTENT,
VoicemailContract.Voicemails.IS_READ, VoicemailContract.Voicemails.IS_READ
VoicemailContract.Voicemails.SOURCE_PACKAGE
) )
context.contentResolver.query( context.contentResolver.query(uri, proj, null, null,
uri, "${VoicemailContract.Voicemails.DATE} DESC")?.use { c ->
projection, while (c.moveToNext()) {
null, val id = c.getString(c.getColumnIndexOrThrow(VoicemailContract.Voicemails._ID))
null, val number = c.getString(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.NUMBER))
"${VoicemailContract.Voicemails.DATE} DESC" val date = c.getLong(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.DATE))
)?.use { cursor -> val dur = c.getLong(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.DURATION))
while (cursor.moveToNext()) { val hasBlob = c.getInt(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.HAS_CONTENT)) == 1
val id = cursor.getString(cursor.getColumnIndexOrThrow(VoicemailContract.Voicemails._ID)) val isRead = c.getInt(c.getColumnIndexOrThrow(VoicemailContract.Voicemails.IS_READ)) == 1
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
var filePath: String? = null val filePath = if (hasBlob) saveVoicemailToFile(id) else null
if (hasContent) {
filePath = saveVoicemailToFile(id)
}
val voicemail: Map<String, Any?> = mapOf( // Added explicit type out += mapOf(
"id" to id, "id" to id,
"phoneNumber" to (number ?: "Unknown"), "phoneNumber" to (number ?: ""),
"timestamp" to date, "timestamp" to date,
"duration" to (duration / 1000).toInt(), // Convert to seconds "duration" to dur.toInt(),
"filePath" to filePath, "filePath" to filePath,
"isRead" to isRead "isRead" to isRead
) )
voicemails.add(voicemail)
} }
} }
return out
return voicemails
} }
private fun saveVoicemailToFile(voicemailId: String): String? { private fun saveVoicemailToFile(voicemailId: String): String? {
val voicemailUri = Uri.withAppendedPath(VoicemailContract.Voicemails.CONTENT_URI, voicemailId) val vUri = Uri.withAppendedPath(VoicemailContract.Voicemails.CONTENT_URI, voicemailId)
return try {
try { context.contentResolver.openInputStream(vUri)?.use { inp ->
val inputStream = context.contentResolver.openInputStream(voicemailUri) ?: return null val cache = File(context.cacheDir, "vm_$voicemailId.3gp")
val cacheDir = context.cacheDir FileOutputStream(cache).use { out ->
val voicemailFile = File(cacheDir, "voicemail_$voicemailId.3gp") val buf = ByteArray(4096)
var r: Int
FileOutputStream(voicemailFile).use { outputStream -> while (inp.read(buf).also { r = it } > 0) {
val buffer = ByteArray(4096) out.write(buf, 0, r)
var bytesRead: Int }
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
} }
cache.absolutePath
} }
inputStream.close()
return voicemailFile.absolutePath
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error saving voicemail to file", e) Log.e(TAG, "saveVoicemailToFile failed", e)
return null null
} }
} }

View File

@ -92,42 +92,33 @@ class _VoicemailPageState extends State<VoicemailPage> {
} }
} }
Future<void> _togglePlayPause(Voicemail voicemail) async { Future<void> _togglePlayPause(Voicemail vm) async {
if (_currentPlayingId == voicemail.id && _isPlaying) { if (vm.filePath == null) {
return _showSnack('Voicemail file not available');
}
final src = DeviceFileSource(vm.filePath!);
if (_currentPlayingId == vm.id && _isPlaying) {
await _audioPlayer.pause(); await _audioPlayer.pause();
setState(() => _isPlaying = false); setState(() => _isPlaying = false);
} else { } else {
// If we're playing a different voicemail, stop current one if (_isPlaying) await _audioPlayer.stop();
if (_isPlaying && _currentPlayingId != voicemail.id) { try {
await _audioPlayer.stop(); await _audioPlayer.play(src);
} setState(() {
_isPlaying = true;
// Start playing the new voicemail _currentPlayingId = vm.id;
if (voicemail.filePath != null) { });
try { if (!vm.isRead) await _voicemailService.markAsRead(vm.id);
await _audioPlayer.play(DeviceFileSource(voicemail.filePath!)); } catch (e) {
setState(() { _showSnack('Error playing voicemail: $e');
_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')),
);
} }
} }
} }
void _showSnack(String msg) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
void _makeCall(String phoneNumber) { void _makeCall(String phoneNumber) {
_callService.makeGsmCall(context, phoneNumber: phoneNumber); _callService.makeGsmCall(context, phoneNumber: phoneNumber);
} }