feat: implement pending SIM switch handling and improve call state management

This commit is contained in:
AlexisDanlos 2025-06-18 17:33:31 +02:00 committed by stcb
parent cdd3a470c0
commit c8ea9204ff
2 changed files with 161 additions and 69 deletions

View File

@ -73,11 +73,16 @@ class CallService {
'CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked');
_callStateController.add(state);
if (state == "disconnected" || state == "disconnecting") {
_closeCallPage();
// Only close call page if there's no pending SIM switch
if (_pendingSimSwitch == null) {
_closeCallPage();
}
if (wasPhoneLocked) {
await _channel.invokeMethod("callEndedFromFlutter");
}
_activeCallNumber = null;
// Handle pending SIM switch after call is disconnected
_handlePendingSimSwitch();
} else if (state == "active" || state == "dialing") {
final phoneNumber = call.arguments["callId"] as String?;
if (phoneNumber != null &&
@ -123,7 +128,10 @@ class CallService {
wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
print(
'CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked');
_closeCallPage();
// Only close call page if there's no pending SIM switch
if (_pendingSimSwitch == null) {
_closeCallPage();
}
if (wasPhoneLocked) {
await _channel.invokeMethod("callEndedFromFlutter");
}
@ -421,6 +429,13 @@ class CallService {
print('CallService: Cannot close page, context is null');
return;
}
// Only attempt to close if a call page is actually visible
if (!_isCallPageVisible) {
print('CallService: Call page already closed');
return;
}
print(
'CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible');
if (Navigator.canPop(context)) {
@ -428,7 +443,8 @@ class CallService {
Navigator.pop(context);
_isCallPageVisible = false;
} else {
print('CallService: No page to pop');
print('CallService: No page to pop, setting _isCallPageVisible to false');
_isCallPageVisible = false;
}
_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 {
try {
print('CallService: Hanging up call');
@ -521,4 +543,86 @@ class CallService {
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...";
StreamSubscription<String>? _callStateSubscription;
StreamSubscription<Map<String, dynamic>>? _audioStateSubscription;
bool _isCallActive = true; // Track if call is still active
bool get isNumberUnknown => widget.displayName == widget.phoneNumber;
@ -70,10 +71,19 @@ class _CallPageState extends State<CallPage> {
try {
final state = await _callService.getCallState();
print('CallPage: Initial call state: $state');
if (mounted && state == "active") {
if (mounted) {
setState(() {
_callStatus = "00:00";
_startCallTimer();
if (state == "active") {
_callStatus = "00:00";
_isCallActive = true;
_startCallTimer();
} else if (state == "disconnected" || state == "disconnecting") {
_callStatus = "Call Ended";
_isCallActive = false;
} else {
_callStatus = "Calling...";
_isCallActive = true;
}
});
}
} catch (e) {
@ -88,12 +98,23 @@ class _CallPageState extends State<CallPage> {
setState(() {
if (state == "active") {
_callStatus = "00:00";
_isCallActive = true;
_startCallTimer();
} else if (state == "disconnected" || state == "disconnecting") {
_callTimer?.cancel();
_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 {
_callStatus = "Calling...";
_isCallActive = true;
}
});
}
@ -219,76 +240,39 @@ class _CallPageState extends State<CallPage> {
void _switchToNewSim(int simSlot) async {
try {
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
if (!mounted) {
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,
// Use the CallService to handle the SIM switch logic
await _callService.switchSimAndRedial(
phoneNumber: widget.phoneNumber,
displayName: widget.displayName,
thumbnail: widget.thumbnail,
simSlot: simSlot,
thumbnail: widget.thumbnail,
);
// Check if widget is still mounted before showing snackbar
if (!mounted) {
print('CallPage: Widget unmounted, skipping result notification');
return;
}
print('CallPage: SIM switch initiated successfully');
} catch (e) {
print('CallPage: Error initiating SIM switch: $e');
if (result['status'] == 'calling') {
// Show error feedback if widget is still mounted
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Switched to SIM ${simSlot + 1} and redialing...'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Failed to switch SIM and redial: ${result['message'] ?? 'Unknown error'}'),
content: Text('Error switching SIM: $e'),
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 {
// Don't try to hang up if call is already ended
if (!_isCallActive) {
print('CallPage: Ignoring hangup - call already ended');
return;
}
try {
print('CallPage: Initiating hangUp');
final result = await _callService.hangUpCall(context);
@ -331,9 +315,9 @@ class _CallPageState extends State<CallPage> {
print(
'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}');
return PopScope(
canPop: _callStatus == "Call Ended",
canPop: !_isCallActive,
onPopInvoked: (didPop) {
if (!didPop) {
if (!didPop && _isCallActive) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Cannot leave during an active call')),
);
@ -592,10 +576,14 @@ class _CallPageState extends State<CallPage> {
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _showSimSelectionDialog,
icon: const Icon(
onPressed: _isCallActive
? _showSimSelectionDialog
: null,
icon: Icon(
Icons.sim_card,
color: Colors.white,
color: _isCallActive
? Colors.white
: Colors.grey,
size: 32,
),
),
@ -621,15 +609,15 @@ class _CallPageState extends State<CallPage> {
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: GestureDetector(
onTap: _hangUp,
onTap: _isCallActive ? _hangUp : null,
child: Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Colors.red,
decoration: BoxDecoration(
color: _isCallActive ? Colors.red : Colors.grey,
shape: BoxShape.circle,
),
child: const Icon(
Icons.call_end,
child: Icon(
_isCallActive ? Icons.call_end : Icons.call_end,
color: Colors.white,
size: 32,
),