feat: speaker option in call
All checks were successful
/ build-stealth (push) Successful in 10m27s
/ build (push) Successful in 10m30s
/ mirror (push) Successful in 5s

This commit is contained in:
Florian Griffon 2025-04-27 17:43:52 +03:00
parent 6c5ce1beab
commit 936250829b
4 changed files with 88 additions and 14 deletions

View File

@ -178,6 +178,19 @@ class MainActivity : FlutterActivity() {
result.error("MUTE_FAILED", "No active call or failed to mute", null) result.error("MUTE_FAILED", "No active call or failed to mute", null)
} }
} }
"speakerCall" -> {
val speaker = call.argument<Boolean>("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() else -> result.notImplemented()
} }
} }

View File

@ -10,6 +10,7 @@ import android.media.AudioManager
import android.os.Build import android.os.Build
import android.telecom.Call import android.telecom.Call
import android.telecom.InCallService import android.telecom.InCallService
import android.telecom.CallAudioState
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.icing.dialer.activities.MainActivity import com.icing.dialer.activities.MainActivity
@ -32,7 +33,8 @@ class MyInCallService : InCallService() {
audioManager.isMicrophoneMute = mute audioManager.isMicrophoneMute = mute
Log.d(TAG, "Set microphone mute state to $mute") Log.d(TAG, "Set microphone mute state to $mute")
channel?.invokeMethod("audioStateChanged", mapOf( channel?.invokeMethod("audioStateChanged", mapOf(
"muted" to mute "muted" to mute,
"speaker" to audioManager.isSpeakerphoneOn
)) ))
true true
} catch (e: Exception) { } catch (e: Exception) {
@ -41,6 +43,24 @@ class MyInCallService : InCallService() {
} }
} ?: false } ?: 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() { 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}") Log.d(TAG, "Audio state changed: route=${state.route}, muted=${state.isMuted}")
channel?.invokeMethod("audioStateChanged", mapOf( channel?.invokeMethod("audioStateChanged", mapOf(
"route" to state.route, "route" to state.route,
"muted" to state.isMuted "muted" to state.isMuted,
"speaker" to (state.route == CallAudioState.ROUTE_SPEAKER)
)) ))
} }

View File

@ -25,7 +25,7 @@ class _CallPageState extends State<CallPage> {
final ObfuscateService _obfuscateService = ObfuscateService(); final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService(); final CallService _callService = CallService();
bool isMuted = false; bool isMuted = false;
bool isSpeakerOn = false; bool isSpeaker = false;
bool isKeypadVisible = false; bool isKeypadVisible = false;
bool icingProtocolOk = true; bool icingProtocolOk = true;
String _typedDigits = ""; String _typedDigits = "";
@ -115,7 +115,8 @@ class _CallPageState extends State<CallPage> {
print('CallPage: Failed to toggle mute: ${result['message']}'); print('CallPage: Failed to toggle mute: ${result['message']}');
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( 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<CallPage> {
} }
} }
void _toggleSpeaker() { Future<void> _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(() { setState(() {
isSpeakerOn = !isSpeakerOn; 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() { void _toggleKeypad() {
@ -168,7 +189,8 @@ class _CallPageState extends State<CallPage> {
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; final double nameFontSize = isKeypadVisible ? 24.0 : 24.0;
final double statusFontSize = isKeypadVisible ? 16.0 : 16.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( return Scaffold(
body: Container( body: Container(
color: Colors.black, color: Colors.black,
@ -256,7 +278,8 @@ class _CallPageState extends State<CallPage> {
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: _toggleKeypad, 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<CallPage> {
onPressed: _toggleMute, onPressed: _toggleMute,
icon: Icon( icon: Icon(
isMuted ? Icons.mic_off : Icons.mic, isMuted ? Icons.mic_off : Icons.mic,
color: isMuted ? Colors.amber : Colors.white, color: isMuted
? Colors.amber
: Colors.white,
size: 32, size: 32,
), ),
), ),
@ -354,10 +379,10 @@ class _CallPageState extends State<CallPage> {
IconButton( IconButton(
onPressed: _toggleSpeaker, onPressed: _toggleSpeaker,
icon: Icon( icon: Icon(
isSpeakerOn isSpeaker
? Icons.volume_up ? Icons.volume_up
: Icons.volume_off, : Icons.volume_off,
color: isSpeakerOn color: isSpeaker
? Colors.amber ? Colors.amber
: Colors.white, : Colors.white,
size: 32, size: 32,

View File

@ -165,6 +165,21 @@ class CallService {
} }
} }
Future<Map<String, dynamic>> 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<String, dynamic>.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() { void dispose() {
_callStateController.close(); _callStateController.close();
} }