import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../presentation/features/call/call_page.dart'; import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page import 'contact_service.dart'; // Import for history update callback import '../../presentation/features/history/history_page.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; static String? currentDisplayName; static Uint8List? currentThumbnail; static int? currentSimSlot; // Track which SIM slot is being used static bool _isCallPageVisible = false; static Map? _pendingCall; static bool wasPhoneLocked = false; static String? _activeCallNumber; static bool _isNavigating = false; final ContactService _contactService = ContactService(); final _callStateController = StreamController.broadcast(); final _audioStateController = StreamController>.broadcast(); final _simStateController = StreamController.broadcast(); Map? _currentAudioState; static final GlobalKey navigatorKey = GlobalKey(); Stream get callStateStream => _callStateController.stream; Stream> get audioStateStream => _audioStateController.stream; Stream get simStateStream => _simStateController.stream; Map? get currentAudioState => _currentAudioState; // Getter for current SIM slot static int? get getCurrentSimSlot => currentSimSlot; // Get SIM display name for the current call static String? getCurrentSimDisplayName() { if (currentSimSlot == null) return null; return "SIM ${currentSimSlot! + 1}"; } // Cancel pending SIM switch (used when user manually hangs up) void cancelPendingSimSwitch() { if (_pendingSimSwitch != null) { print('CallService: Canceling pending SIM switch due to manual hangup'); _pendingSimSwitch = null; _manualHangupFlag = true; // Mark that hangup was manual print('CallService: Manual hangup flag set to $_manualHangupFlag'); } else { print('CallService: No pending SIM switch to cancel'); // Don't set manual hangup flag if there's no SIM switch to cancel } } CallService() { _channel.setMethodCallHandler((call) async { print( 'CallService: Handling method call: ${call.method}, with args: ${call.arguments}'); switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String?; final state = call.arguments["state"] as String?; if (phoneNumber == null || state == null) { print('CallService: Invalid callAdded args: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); print('CallService: Decoded phone number: $decodedPhoneNumber'); if (_activeCallNumber != decodedPhoneNumber) { currentPhoneNumber = decodedPhoneNumber; if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(decodedPhoneNumber); } } print( 'CallService: Call added, number: $currentPhoneNumber, displayName: $currentDisplayName, state: $state'); _callStateController.add(state); if (state == "ringing") { _handleIncomingCall(decodedPhoneNumber); } else { _navigateToCallPage(); } break; case "callStateChanged": final state = call.arguments["state"] as String?; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; if (state == null) { print( 'CallService: Invalid callStateChanged args: $call.arguments'); return; } print( 'CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); _callStateController.add(state); if (state == "disconnected" || state == "disconnecting") { print('CallService: ========== CALL DISCONNECTED =========='); print( 'CallService: _pendingSimSwitch: ${_pendingSimSwitch != null}'); print('CallService: _manualHangupFlag: $_manualHangupFlag'); print('CallService: _isCallPageVisible: $_isCallPageVisible'); // Always close call page on disconnection - SIM switching should not prevent this print('CallService: Calling _closeCallPage() on call disconnection'); _closeCallPage(); // Reset manual hangup flag after successful page close if (_manualHangupFlag) { print( 'CallService: Resetting manual hangup flag after page close'); _manualHangupFlag = false; } if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); } // Notify history page to add the latest call // Add a small delay to ensure call log is updated by the system Timer(const Duration(milliseconds: 500), () { HistoryPageState.addNewCallToHistory(); }); _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 && _activeCallNumber != Uri.decodeComponent(phoneNumber.replaceFirst('tel:', ''))) { currentPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(currentPhoneNumber!); } } else if (currentPhoneNumber != null && _activeCallNumber != currentPhoneNumber) { if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(currentPhoneNumber!); } } else { print( 'CallService: Skipping fetch, active call: $_activeCallNumber, current: $currentPhoneNumber, displayName: $currentDisplayName'); } _navigateToCallPage(); } else if (state == "ringing") { final phoneNumber = call.arguments["callId"] as String?; if (phoneNumber == null) { print('CallService: Invalid ringing callId: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); if (_activeCallNumber != decodedPhoneNumber) { currentPhoneNumber = decodedPhoneNumber; if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(decodedPhoneNumber); } } _handleIncomingCall(decodedPhoneNumber); } break; case "callEnded": case "callRemoved": wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; print('CallService: ========== CALL ENDED/REMOVED =========='); print('CallService: wasPhoneLocked: $wasPhoneLocked'); print('CallService: _pendingSimSwitch: ${_pendingSimSwitch != null}'); print('CallService: _manualHangupFlag: $_manualHangupFlag'); print('CallService: _isCallPageVisible: $_isCallPageVisible'); // Always close call page when call ends - SIM switching should not prevent this print('CallService: Calling _closeCallPage() on call ended/removed'); _closeCallPage(); // Reset manual hangup flag after closing page if (_manualHangupFlag) { print( 'CallService: Resetting manual hangup flag after callEnded'); _manualHangupFlag = false; } if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); } // Notify history page to add the latest call // Add a small delay to ensure call log is updated by the system Timer(const Duration(milliseconds: 500), () { HistoryPageState.addNewCallToHistory(); }); currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; currentSimSlot = null; // Reset SIM slot when call ends _simStateController.add(null); // Notify UI that SIM is cleared _activeCallNumber = null; break; case "incomingCallFromNotification": final phoneNumber = call.arguments["phoneNumber"] as String?; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; if (phoneNumber == null) { print( 'CallService: Invalid incomingCallFromNotification args: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber); if (_activeCallNumber != decodedPhoneNumber) { currentPhoneNumber = decodedPhoneNumber; if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(decodedPhoneNumber); } } print( 'CallService: Incoming call from notification: $decodedPhoneNumber, displayName: $currentDisplayName, wasPhoneLocked: $wasPhoneLocked'); _handleIncomingCall(decodedPhoneNumber); break; case "audioStateChanged": final route = call.arguments["route"] as int?; final muted = call.arguments["muted"] as bool?; final speaker = call.arguments["speaker"] as bool?; print( 'CallService: Audio state changed, route: $route, muted: $muted, speaker: $speaker'); final audioState = { "route": route, "muted": muted, "speaker": speaker, }; _currentAudioState = audioState; _audioStateController.add(audioState); break; } }); } Future getCallState() async { try { final state = await _channel.invokeMethod('getCallState'); print('CallService: getCallState returned: $state'); return state as String?; } catch (e) { print('CallService: Error getting call state: $e'); return null; } } Future> muteCall(BuildContext context, {required bool mute}) async { try { print('CallService: Toggling mute to $mute'); final result = await _channel.invokeMethod('muteCall', {'mute': mute}); print('CallService: muteCall result: $result'); final resultMap = Map.from(result as Map); if (resultMap['status'] != 'success') { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to toggle mute')), ); } return resultMap; } catch (e) { print('CallService: Error toggling mute: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error toggling mute: $e')), ); return {'status': 'error', 'message': e.toString()}; } } 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(); _audioStateController.close(); } Future _fetchContactInfo(String phoneNumber) async { try { print('CallService: Fetching contact info for $phoneNumber'); final contacts = await _contactService.fetchContacts(); print('CallService: Retrieved ${contacts.length} contacts'); final normalizedPhoneNumber = _normalizePhoneNumber(phoneNumber); print('CallService: Normalized phone number: $normalizedPhoneNumber'); for (var contact in contacts) { for (var phone in contact.phones) { final normalizedContactNumber = _normalizePhoneNumber(phone.number); print( 'CallService: Comparing $normalizedPhoneNumber with contact number $normalizedContactNumber'); if (normalizedContactNumber == normalizedPhoneNumber) { currentDisplayName = contact.displayName; currentThumbnail = contact.thumbnail; print( 'CallService: Found contact - displayName: $currentDisplayName, thumbnail: ${currentThumbnail != null ? "present" : "null"}'); return; } } } currentDisplayName = phoneNumber; currentThumbnail = null; print( 'CallService: No contact match, using phoneNumber as displayName: $currentDisplayName'); } catch (e) { print('CallService: Error fetching contact info: $e'); currentDisplayName = phoneNumber; currentThumbnail = null; } } String _normalizePhoneNumber(String number) { return number .replaceAll(RegExp(r'[\s\-\(\)]'), '') .replaceFirst(RegExp(r'^\+'), ''); } void _handleIncomingCall(String phoneNumber) { if (_activeCallNumber == phoneNumber && _isCallPageVisible) { print( 'CallService: Incoming call for $phoneNumber already active, skipping'); return; } _activeCallNumber = phoneNumber; final context = navigatorKey.currentContext; if (context == null) { print( 'CallService: Context is null, queuing incoming call: $phoneNumber'); _pendingCall = {"phoneNumber": phoneNumber}; Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); } else { _navigateToIncomingCallPage(context); } } Future _checkPendingCall() async { if (_pendingCall == null) { print('CallService: No pending call to process'); return; } final phoneNumber = _pendingCall!["phoneNumber"]; if (_activeCallNumber == phoneNumber && _isCallPageVisible) { print( 'CallService: Pending call for $phoneNumber already active, clearing'); _pendingCall = null; return; } final context = navigatorKey.currentContext; if (context != null) { print('CallService: Processing queued call: $phoneNumber'); currentPhoneNumber = phoneNumber; _activeCallNumber = phoneNumber; await _fetchContactInfo(phoneNumber); _navigateToIncomingCallPage(context); _pendingCall = null; } else { print('CallService: Context still null, retrying...'); Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); } } void _navigateToCallPage() { if (_isNavigating) { print('CallService: Navigation already in progress, skipping'); return; } _isNavigating = true; final context = navigatorKey.currentContext; if (context == null) { print('CallService: Cannot navigate to CallPage, context is null'); _isNavigating = false; return; } final currentRoute = ModalRoute.of(context)?.settings.name ?? 'unknown'; print( 'CallService: Navigating to CallPage, Visible: $_isCallPageVisible, Current Route: $currentRoute, Active Call: $_activeCallNumber, DisplayName: $currentDisplayName'); if (_isCallPageVisible && currentRoute == '/call' && _activeCallNumber == currentPhoneNumber) { print( 'CallService: CallPage already visible for $_activeCallNumber, skipping navigation'); _isNavigating = false; return; } if (_isCallPageVisible && currentRoute == '/incoming_call' && _activeCallNumber == currentPhoneNumber) { print( 'CallService: Popping IncomingCallPage before navigating to CallPage'); Navigator.pop(context); _isCallPageVisible = false; } if (currentPhoneNumber == null) { print( 'CallService: Cannot navigate to CallPage, currentPhoneNumber is null'); _isNavigating = false; return; } _activeCallNumber = currentPhoneNumber; Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: '/call'), builder: (context) => CallPage( displayName: currentDisplayName ?? currentPhoneNumber!, phoneNumber: currentPhoneNumber!, thumbnail: currentThumbnail, ), ), ).then((_) { _isCallPageVisible = false; _isNavigating = false; print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _navigateToIncomingCallPage(BuildContext context) { if (_isNavigating) { print('CallService: Navigation already in progress, skipping'); return; } _isNavigating = true; final currentRoute = ModalRoute.of(context)?.settings.name ?? 'unknown'; print( 'CallService: Navigating to IncomingCallPage, Visible: $_isCallPageVisible, Current Route: $currentRoute, Active Call: $_activeCallNumber, DisplayName: $currentDisplayName'); if (_isCallPageVisible && currentRoute == '/incoming_call' && _activeCallNumber == currentPhoneNumber) { print( 'CallService: IncomingCallPage already visible for $_activeCallNumber, skipping navigation'); _isNavigating = false; return; } if (_isCallPageVisible && currentRoute == '/call') { print('CallService: CallPage visible, not showing IncomingCallPage'); _isNavigating = false; return; } if (currentPhoneNumber == null) { print( 'CallService: Cannot navigate to IncomingCallPage, currentPhoneNumber is null'); _isNavigating = false; return; } Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: '/incoming_call'), builder: (context) => IncomingCallPage( displayName: currentDisplayName ?? currentPhoneNumber!, phoneNumber: currentPhoneNumber!, thumbnail: currentThumbnail, ), ), ).then((_) { _isCallPageVisible = false; _isNavigating = false; print( 'CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _closeCallPage() { final context = navigatorKey.currentContext; if (context == null) { 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, _pendingSimSwitch: ${_pendingSimSwitch != null}, _manualHangupFlag: $_manualHangupFlag'); // Use popUntil to ensure we go back to the home page try { Navigator.popUntil(context, (route) => route.isFirst); _isCallPageVisible = false; print('CallService: Used popUntil to return to home page'); } catch (e) { print('CallService: Error with popUntil, trying regular pop: $e'); if (Navigator.canPop(context)) { Navigator.pop(context); _isCallPageVisible = false; print('CallService: Used regular pop as fallback'); } else { print('CallService: No page to pop, setting _isCallPageVisible to false'); _isCallPageVisible = false; } } _activeCallNumber = null; } Future> makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, Uint8List? thumbnail, }) async { try { // Load default SIM slot from settings final prefs = await SharedPreferences.getInstance(); final simSlot = prefs.getInt('default_sim_slot') ?? 0; return await makeGsmCallWithSim( context, phoneNumber: phoneNumber, displayName: displayName, thumbnail: thumbnail, simSlot: simSlot, ); } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error making call: $e")), ); return {"status": "error", "message": e.toString()}; } } Future> makeGsmCallWithSim( BuildContext context, { required String phoneNumber, String? displayName, Uint8List? thumbnail, required int simSlot, }) async { try { if (_activeCallNumber == phoneNumber && _isCallPageVisible) { print('CallService: Call already active for $phoneNumber, skipping'); return { "status": "already_active", "message": "Call already in progress" }; } currentPhoneNumber = phoneNumber; currentDisplayName = displayName ?? phoneNumber; currentThumbnail = thumbnail; currentSimSlot = simSlot; // Track the SIM slot being used _simStateController.add(simSlot); // Notify UI of SIM change if (displayName == null || thumbnail == null) { await _fetchContactInfo(phoneNumber); } print( 'CallService: Making GSM call to $phoneNumber, displayName: $currentDisplayName, simSlot: $simSlot'); final result = await _channel.invokeMethod( 'makeGsmCall', {"phoneNumber": phoneNumber, "simSlot": simSlot}); print('CallService: makeGsmCall result: $result'); final resultMap = Map.from(result as Map); if (resultMap["status"] != "calling") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to initiate call")), ); } return resultMap; } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error making call: $e")), ); return {"status": "error", "message": e.toString()}; } } // Pending SIM switch data static Map? _pendingSimSwitch; static bool _manualHangupFlag = false; // Track if hangup was manual // Getter to check if there's a pending SIM switch static bool get hasPendingSimSwitch => _pendingSimSwitch != null; Future> hangUpCall(BuildContext context) async { try { print('CallService: ========== HANGUP INITIATED =========='); print('CallService: _pendingSimSwitch: ${_pendingSimSwitch != null}'); print('CallService: _manualHangupFlag: $_manualHangupFlag'); print('CallService: _isCallPageVisible: $_isCallPageVisible'); final result = await _channel.invokeMethod('hangUpCall'); print('CallService: hangUpCall result: $result'); final resultMap = Map.from(result as Map); if (resultMap["status"] != "ended") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); } else { // If hangup was successful, ensure call page closes after a short delay // This is a fallback in case the native call state events don't fire properly Future.delayed(const Duration(milliseconds: 1500), () { if (_isCallPageVisible) { print( 'CallService: FALLBACK - Force closing call page after hangup'); _closeCallPage(); _manualHangupFlag = false; // Reset flag } }); } return resultMap; } catch (e) { print("CallService: Error hanging up call: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error hanging up call: $e")), ); return {"status": "error", "message": e.toString()}; } } Future 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']; currentSimSlot = switchData['simSlot']; // Track the new SIM slot _simStateController.add(switchData['simSlot']); // Notify UI of SIM change // 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(); } } } }