From 936250829be1aa5d6d84ee6836fddbb2e5fd24af Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Sun, 27 Apr 2025 17:43:52 +0300 Subject: [PATCH] feat: speaker option in call --- .../icing/dialer/activities/MainActivity.kt | 13 +++++ .../icing/dialer/services/MyInCallService.kt | 25 +++++++++- dialer/lib/features/call/call_page.dart | 49 ++++++++++++++----- dialer/lib/services/call_service.dart | 15 ++++++ 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index 0c492b5..ee194a2 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -178,6 +178,19 @@ class MainActivity : FlutterActivity() { result.error("MUTE_FAILED", "No active call or failed to mute", null) } } + "speakerCall" -> { + val speaker = call.argument("speaker") ?: false + val success = MyInCallService.currentCall?.let { + MyInCallService.toggleSpeaker(speaker) + } ?: false + if (success) { + Log.d(TAG, "Speaker call set to $speaker") + result.success(mapOf("status" to "success")) + } else { + Log.w(TAG, "No active call or failed to set speaker") + result.error("SPEAKER_FAILED", "No active call or failed to set speaker", null) + } + } else -> result.notImplemented() } } diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt index d79abd1..f9f71ad 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt @@ -10,6 +10,7 @@ import android.media.AudioManager import android.os.Build import android.telecom.Call import android.telecom.InCallService +import android.telecom.CallAudioState import android.util.Log import androidx.core.app.NotificationCompat import com.icing.dialer.activities.MainActivity @@ -32,7 +33,8 @@ class MyInCallService : InCallService() { audioManager.isMicrophoneMute = mute Log.d(TAG, "Set microphone mute state to $mute") channel?.invokeMethod("audioStateChanged", mapOf( - "muted" to mute + "muted" to mute, + "speaker" to audioManager.isSpeakerphoneOn )) true } catch (e: Exception) { @@ -41,6 +43,24 @@ class MyInCallService : InCallService() { } } ?: false } + + fun toggleSpeaker(speaker: Boolean): Boolean { + return instance?.let { service -> + try { + val audioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager + audioManager.isSpeakerphoneOn = speaker + Log.d(TAG, "Set speakerphone state to $speaker") + channel?.invokeMethod("audioStateChanged", mapOf( + "muted" to audioManager.isMicrophoneMute, + "speaker" to speaker + )) + true + } catch (e: Exception) { + Log.e(TAG, "Failed to set speaker state: $e") + false + } + } ?: false + } } private val callCallback = object : Call.Callback() { @@ -119,7 +139,8 @@ class MyInCallService : InCallService() { Log.d(TAG, "Audio state changed: route=${state.route}, muted=${state.isMuted}") channel?.invokeMethod("audioStateChanged", mapOf( "route" to state.route, - "muted" to state.isMuted + "muted" to state.isMuted, + "speaker" to (state.route == CallAudioState.ROUTE_SPEAKER) )) } diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index ab42055..54333c7 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -25,7 +25,7 @@ class _CallPageState extends State { final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); bool isMuted = false; - bool isSpeakerOn = false; + bool isSpeaker = false; bool isKeypadVisible = false; bool icingProtocolOk = true; String _typedDigits = ""; @@ -115,7 +115,8 @@ class _CallPageState extends State { print('CallPage: Failed to toggle mute: ${result['message']}'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to toggle mute: ${result['message']}')), + SnackBar( + content: Text('Failed to toggle mute: ${result['message']}')), ); } } @@ -129,10 +130,30 @@ class _CallPageState extends State { } } - void _toggleSpeaker() { - setState(() { - isSpeakerOn = !isSpeakerOn; - }); + Future _toggleSpeaker() async { + try { + print('CallPage: Toggling speaker, current state: $isSpeaker'); + final result = + await _callService.speakerCall(context, speaker: !isSpeaker); + print('CallPage: Speaker call result: $result'); + if (result['status'] == 'success') { + setState(() { + isSpeaker = !isSpeaker; + }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to toggle speaker: ${result['message']}')), + ); + } + } catch (e) { + print('CallPage: Error toggling speaker: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error toggling speaker: $e')), + ); + } + } } void _toggleKeypad() { @@ -168,7 +189,8 @@ class _CallPageState extends State { final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; final double statusFontSize = isKeypadVisible ? 16.0 : 16.0; - print('CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); + print( + 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); return Scaffold( body: Container( color: Colors.black, @@ -256,7 +278,8 @@ class _CallPageState extends State { IconButton( padding: EdgeInsets.zero, onPressed: _toggleKeypad, - icon: const Icon(Icons.close, color: Colors.white), + icon: + const Icon(Icons.close, color: Colors.white), ), ], ), @@ -319,7 +342,9 @@ class _CallPageState extends State { onPressed: _toggleMute, icon: Icon( isMuted ? Icons.mic_off : Icons.mic, - color: isMuted ? Colors.amber : Colors.white, + color: isMuted + ? Colors.amber + : Colors.white, size: 32, ), ), @@ -354,10 +379,10 @@ class _CallPageState extends State { IconButton( onPressed: _toggleSpeaker, icon: Icon( - isSpeakerOn + isSpeaker ? Icons.volume_up : Icons.volume_off, - color: isSpeakerOn + color: isSpeaker ? Colors.amber : Colors.white, size: 32, @@ -446,4 +471,4 @@ class _CallPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 33e9310..ec52df9 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -165,6 +165,21 @@ class CallService { } } + Future> speakerCall(BuildContext context, {required bool speaker}) async { + try { + print('CallService: Toggling speaker to $speaker'); + final result = await _channel.invokeMethod('speakerCall', {'speaker': speaker}); + print('CallService: speakerCall result: $result'); + return Map.from(result); + } catch (e) { + print('CallService: Error toggling speaker: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to toggle speaker: $e')), + ); + return {'status': 'error', 'message': e.toString()}; + } + } + void dispose() { _callStateController.close(); }