feat: implement pending SIM switch handling and improve call state management
Some checks failed
/ mirror (push) Failing after 5s
/ build (push) Successful in 10m29s
/ build-stealth (push) Successful in 10m31s

This commit is contained in:
AlexisDanlos 2025-06-18 17:33:31 +02:00
parent babe733dff
commit 4eff7b503c
2 changed files with 161 additions and 69 deletions

View File

@ -73,11 +73,16 @@ class CallService {
'CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); 'CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked');
_callStateController.add(state); _callStateController.add(state);
if (state == "disconnected" || state == "disconnecting") { if (state == "disconnected" || state == "disconnecting") {
_closeCallPage(); // Only close call page if there's no pending SIM switch
if (_pendingSimSwitch == null) {
_closeCallPage();
}
if (wasPhoneLocked) { if (wasPhoneLocked) {
await _channel.invokeMethod("callEndedFromFlutter"); await _channel.invokeMethod("callEndedFromFlutter");
} }
_activeCallNumber = null; _activeCallNumber = null;
// Handle pending SIM switch after call is disconnected
_handlePendingSimSwitch();
} else if (state == "active" || state == "dialing") { } else if (state == "active" || state == "dialing") {
final phoneNumber = call.arguments["callId"] as String?; final phoneNumber = call.arguments["callId"] as String?;
if (phoneNumber != null && if (phoneNumber != null &&
@ -123,7 +128,10 @@ class CallService {
wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
print( print(
'CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); 'CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked');
_closeCallPage(); // Only close call page if there's no pending SIM switch
if (_pendingSimSwitch == null) {
_closeCallPage();
}
if (wasPhoneLocked) { if (wasPhoneLocked) {
await _channel.invokeMethod("callEndedFromFlutter"); await _channel.invokeMethod("callEndedFromFlutter");
} }
@ -421,6 +429,13 @@ class CallService {
print('CallService: Cannot close page, context is null'); print('CallService: Cannot close page, context is null');
return; return;
} }
// Only attempt to close if a call page is actually visible
if (!_isCallPageVisible) {
print('CallService: Call page already closed');
return;
}
print( print(
'CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); 'CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible');
if (Navigator.canPop(context)) { if (Navigator.canPop(context)) {
@ -428,7 +443,8 @@ class CallService {
Navigator.pop(context); Navigator.pop(context);
_isCallPageVisible = false; _isCallPageVisible = false;
} else { } else {
print('CallService: No page to pop'); print('CallService: No page to pop, setting _isCallPageVisible to false');
_isCallPageVisible = false;
} }
_activeCallNumber = null; _activeCallNumber = null;
} }
@ -501,6 +517,12 @@ class CallService {
} }
} }
// Pending SIM switch data
static Map<String, dynamic>? _pendingSimSwitch;
// Getter to check if there's a pending SIM switch
static bool get hasPendingSimSwitch => _pendingSimSwitch != null;
Future<Map<String, dynamic>> hangUpCall(BuildContext context) async { Future<Map<String, dynamic>> hangUpCall(BuildContext context) async {
try { try {
print('CallService: Hanging up call'); print('CallService: Hanging up call');
@ -521,4 +543,86 @@ class CallService {
return {"status": "error", "message": e.toString()}; return {"status": "error", "message": e.toString()};
} }
} }
Future<void> switchSimAndRedial({
required String phoneNumber,
required String displayName,
required int simSlot,
Uint8List? thumbnail,
}) async {
try {
print(
'CallService: Starting SIM switch to slot $simSlot for $phoneNumber');
// Store the redial information for after hangup
_pendingSimSwitch = {
'phoneNumber': phoneNumber,
'displayName': displayName,
'simSlot': simSlot,
'thumbnail': thumbnail,
};
// Hang up the current call - this will trigger the disconnected state
await _channel.invokeMethod('hangUpCall');
print(
'CallService: Hangup initiated, waiting for disconnection to complete redial');
} catch (e) {
print('CallService: Error during SIM switch: $e');
_pendingSimSwitch = null;
rethrow;
}
}
void _handlePendingSimSwitch() async {
if (_pendingSimSwitch == null) return;
final switchData = _pendingSimSwitch!;
_pendingSimSwitch = null;
try {
print('CallService: Executing pending SIM switch redial');
// Wait a moment to ensure the previous call is fully disconnected
await Future.delayed(const Duration(milliseconds: 1000));
// Store the new call info for the redial
currentPhoneNumber = switchData['phoneNumber'];
currentDisplayName = switchData['displayName'];
currentThumbnail =
switchData['thumbnail']; // Make the new call with the selected SIM
final result = await _channel.invokeMethod('makeGsmCall', {
'phoneNumber': switchData['phoneNumber'],
'simSlot': switchData['simSlot'],
});
print('CallService: SIM switch redial result: $result');
// Show success feedback
final context = navigatorKey.currentContext;
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Switched to SIM ${switchData['simSlot'] + 1} and redialing...'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
print('CallService: Error during SIM switch redial: $e');
// Show error feedback and close the call page
final context = navigatorKey.currentContext;
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to redial with new SIM: $e'),
backgroundColor: Colors.red,
),
);
// Close the call page since redial failed
_closeCallPage();
}
}
}
} }

View File

@ -36,6 +36,7 @@ class _CallPageState extends State<CallPage> {
String _callStatus = "Calling..."; String _callStatus = "Calling...";
StreamSubscription<String>? _callStateSubscription; StreamSubscription<String>? _callStateSubscription;
StreamSubscription<Map<String, dynamic>>? _audioStateSubscription; StreamSubscription<Map<String, dynamic>>? _audioStateSubscription;
bool _isCallActive = true; // Track if call is still active
bool get isNumberUnknown => widget.displayName == widget.phoneNumber; bool get isNumberUnknown => widget.displayName == widget.phoneNumber;
@ -70,10 +71,19 @@ class _CallPageState extends State<CallPage> {
try { try {
final state = await _callService.getCallState(); final state = await _callService.getCallState();
print('CallPage: Initial call state: $state'); print('CallPage: Initial call state: $state');
if (mounted && state == "active") { if (mounted) {
setState(() { setState(() {
_callStatus = "00:00"; if (state == "active") {
_startCallTimer(); _callStatus = "00:00";
_isCallActive = true;
_startCallTimer();
} else if (state == "disconnected" || state == "disconnecting") {
_callStatus = "Call Ended";
_isCallActive = false;
} else {
_callStatus = "Calling...";
_isCallActive = true;
}
}); });
} }
} catch (e) { } catch (e) {
@ -88,12 +98,23 @@ class _CallPageState extends State<CallPage> {
setState(() { setState(() {
if (state == "active") { if (state == "active") {
_callStatus = "00:00"; _callStatus = "00:00";
_isCallActive = true;
_startCallTimer(); _startCallTimer();
} else if (state == "disconnected" || state == "disconnecting") { } else if (state == "disconnected" || state == "disconnecting") {
_callTimer?.cancel(); _callTimer?.cancel();
_callStatus = "Call Ended"; _callStatus = "Call Ended";
_isCallActive =
false; // Only navigate back if there's no pending SIM switch
if (!CallService.hasPendingSimSwitch) {
Future.delayed(const Duration(seconds: 2), () {
if (mounted && Navigator.canPop(context)) {
Navigator.of(context).pop();
}
});
}
} else { } else {
_callStatus = "Calling..."; _callStatus = "Calling...";
_isCallActive = true;
} }
}); });
} }
@ -219,76 +240,39 @@ class _CallPageState extends State<CallPage> {
void _switchToNewSim(int simSlot) async { void _switchToNewSim(int simSlot) async {
try { try {
print( print(
'CallPage: Switching to SIM slot $simSlot for ${widget.phoneNumber}'); 'CallPage: Initiating SIM switch to slot $simSlot for ${widget.phoneNumber}');
// Check if widget is still mounted before starting // Use the CallService to handle the SIM switch logic
if (!mounted) { await _callService.switchSimAndRedial(
print('CallPage: Widget unmounted, canceling SIM switch operation');
return;
}
// First hang up the current call
await _callService.hangUpCall(context);
// Wait a brief moment for the call to end
await Future.delayed(const Duration(milliseconds: 500));
// Check if widget is still mounted before proceeding
if (!mounted) {
print('CallPage: Widget unmounted, canceling SIM switch operation');
return;
}
// Make a new call with the selected SIM
final result = await _callService.makeGsmCallWithSim(
context,
phoneNumber: widget.phoneNumber, phoneNumber: widget.phoneNumber,
displayName: widget.displayName, displayName: widget.displayName,
thumbnail: widget.thumbnail,
simSlot: simSlot, simSlot: simSlot,
thumbnail: widget.thumbnail,
); );
// Check if widget is still mounted before showing snackbar print('CallPage: SIM switch initiated successfully');
if (!mounted) { } catch (e) {
print('CallPage: Widget unmounted, skipping result notification'); print('CallPage: Error initiating SIM switch: $e');
return;
}
if (result['status'] == 'calling') { // Show error feedback if widget is still mounted
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Switched to SIM ${simSlot + 1} and redialing...'), content: Text('Error switching SIM: $e'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Failed to switch SIM and redial: ${result['message'] ?? 'Unknown error'}'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
} }
} catch (e) {
print('CallPage: Error switching SIM: $e');
// Check if widget is still mounted before showing error snackbar
if (!mounted) {
print('CallPage: Widget unmounted, skipping error notification');
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error switching SIM: $e'),
backgroundColor: Colors.red,
),
);
} }
} }
void _hangUp() async { void _hangUp() async {
// Don't try to hang up if call is already ended
if (!_isCallActive) {
print('CallPage: Ignoring hangup - call already ended');
return;
}
try { try {
print('CallPage: Initiating hangUp'); print('CallPage: Initiating hangUp');
final result = await _callService.hangUpCall(context); final result = await _callService.hangUpCall(context);
@ -331,9 +315,9 @@ class _CallPageState extends State<CallPage> {
print( print(
'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}');
return PopScope( return PopScope(
canPop: _callStatus == "Call Ended", canPop: !_isCallActive,
onPopInvoked: (didPop) { onPopInvoked: (didPop) {
if (!didPop) { if (!didPop && _isCallActive) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Cannot leave during an active call')), SnackBar(content: Text('Cannot leave during an active call')),
); );
@ -592,10 +576,14 @@ class _CallPageState extends State<CallPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
onPressed: _showSimSelectionDialog, onPressed: _isCallActive
icon: const Icon( ? _showSimSelectionDialog
: null,
icon: Icon(
Icons.sim_card, Icons.sim_card,
color: Colors.white, color: _isCallActive
? Colors.white
: Colors.grey,
size: 32, size: 32,
), ),
), ),
@ -621,15 +609,15 @@ class _CallPageState extends State<CallPage> {
Padding( Padding(
padding: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.only(bottom: 16.0),
child: GestureDetector( child: GestureDetector(
onTap: _hangUp, onTap: _isCallActive ? _hangUp : null,
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: Colors.red, color: _isCallActive ? Colors.red : Colors.grey,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: Icon(
Icons.call_end, _isCallActive ? Icons.call_end : Icons.call_end,
color: Colors.white, color: Colors.white,
size: 32, size: 32,
), ),