import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; import '../features/call/incoming_call_page.dart'; import '../services/contact_service.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; static String? currentDisplayName; static Uint8List? currentThumbnail; 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(); static final GlobalKey navigatorKey = GlobalKey(); Stream get callStateStream => _callStateController.stream; CallService() { _channel.setMethodCallHandler((call) async { final eventTime = DateTime.now().millisecondsSinceEpoch; final disconnectCause = call.arguments["disconnectCause"] ?? "none"; print('CallService: [${eventTime}ms] Handling method call: ${call.method} with args: ${call.arguments}, disconnectCause: $disconnectCause'); 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: [${eventTime}ms] Invalid callAdded args: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); print('CallService: [${eventTime}ms] Decoded phone number: $decodedPhoneNumber'); if (_activeCallNumber != decodedPhoneNumber) { currentPhoneNumber = decodedPhoneNumber; if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(decodedPhoneNumber); } } print('CallService: [${eventTime}ms] 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: [${eventTime}ms] Invalid callStateChanged args: $call.arguments'); return; } print('CallService: [${eventTime}ms] State changed to $state, wasPhoneLocked: $wasPhoneLocked, disconnectCause: $disconnectCause'); _callStateController.add(state); if (state == "disconnected" || state == "disconnecting") { final closeStart = DateTime.now().millisecondsSinceEpoch; print('CallService: [${closeStart}ms] Initiating closeCallPage for state: $state'); _closeCallPage(); print('CallService: [${DateTime.now().millisecondsSinceEpoch}ms] closeCallPage completed'); if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); } _activeCallNumber = null; } 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: [${eventTime}ms] 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: [${eventTime}ms] 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: [${eventTime}ms] Call ended/removed, wasPhoneLocked: $wasPhoneLocked, disconnectCause: $disconnectCause'); _callStateController.add("disconnected"); final closeStart = DateTime.now().millisecondsSinceEpoch; print('CallService: [${closeStart}ms] Initiating closeCallPage for callEnded/callRemoved'); _closeCallPage(); print('CallService: [${DateTime.now().millisecondsSinceEpoch}ms] closeCallPage completed'); if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); } currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; _activeCallNumber = null; break; case "incomingCallFromNotification": final phoneNumber = call.arguments["phoneNumber"] as String?; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; if (phoneNumber == null) { print('CallService: [${eventTime}ms] 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: [${eventTime}ms] Incoming call from notification: $decodedPhoneNumber, displayName: $currentDisplayName, wasPhoneLocked: $wasPhoneLocked'); _handleIncomingCall(decodedPhoneNumber); break; case "audioStateChanged": final route = call.arguments["route"] as int?; print('CallService: [${eventTime}ms] Audio state changed, route: $route'); break; } }); } void dispose() { _callStateController.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.pushReplacement( 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; } final currentRoute = ModalRoute.of(context)?.settings.name ?? 'unknown'; print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible, Current Route: $currentRoute'); if (_isCallPageVisible && (currentRoute == '/call' || currentRoute == '/incoming_call')) { print('CallService: Popping call page'); Navigator.pop(context); } else { print('CallService: No call page to pop, _isCallPageVisible: $_isCallPageVisible, Current Route: $currentRoute'); } _isCallPageVisible = false; _activeCallNumber = null; } Future> makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, Uint8List? thumbnail, }) 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; if (displayName == null || thumbnail == null) { await _fetchContactInfo(phoneNumber); } print('CallService: Making GSM call to $phoneNumber, displayName: $currentDisplayName'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); 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()}; } } Future> hangUpCall(BuildContext context) async { try { print('CallService: Hanging up call'); final result = await _channel.invokeMethod('hangUpCall'); print('CallService: hangUpCall result: $result'); final resultMap = Map.from(result as Map); if (resultMap["status"] == "ended") { final closeStart = DateTime.now().millisecondsSinceEpoch; print('CallService: [${closeStart}ms] Initiating closeCallPage for hangUpCall'); _closeCallPage(); print('CallService: [${DateTime.now().millisecondsSinceEpoch}ms] closeCallPage completed'); } else { print('CallService: Hang up failed, status: ${resultMap["status"]}'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); } return resultMap; } catch (e) { print("CallService: Error hanging up call: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error hanging up call: $e")), ); _closeCallPage(); return {"status": "error", "message": e.toString()}; } } }