diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index 368ffde..b6dbce5 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:dialer/services/call_service.dart'; @@ -28,6 +29,58 @@ class _CallPageState extends State { bool isKeypadVisible = false; bool icingProtocolOk = true; String _typedDigits = ""; + Timer? _callTimer; + int _callSeconds = 0; + String _callStatus = "Calling..."; + StreamSubscription? _callStateSubscription; + + @override + void initState() { + super.initState(); + _listenToCallState(); + } + + @override + void dispose() { + _callTimer?.cancel(); + _callStateSubscription?.cancel(); + super.dispose(); + } + + void _listenToCallState() { + _callStateSubscription = _callService.callStateStream.listen((state) { + final eventTime = DateTime.now().millisecondsSinceEpoch; + print('CallPage: [${eventTime}ms] Call state changed to $state'); + if (mounted) { + setState(() { + if (state == "active") { + _callStatus = "00:00"; + _startCallTimer(); + } else if (state == "disconnected" || state == "disconnecting") { + _callTimer?.cancel(); + _callStatus = "Call Ended"; + } else { + _callStatus = "Calling..."; + } + }); + } + }); + } + + void _startCallTimer() { + _callTimer?.cancel(); + _callTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (mounted) { + setState(() { + _callSeconds++; + final minutes = (_callSeconds ~/ 60).toString().padLeft(2, '0'); + final seconds = (_callSeconds % 60).toString().padLeft(2, '0'); + _callStatus = '$minutes:$seconds'; + print('CallPage: [${DateTime.now().millisecondsSinceEpoch}ms] Timer updated, duration: $_callStatus'); + }); + } + }); + } void _addDigit(String digit) { setState(() { @@ -61,16 +114,17 @@ class _CallPageState extends State { void _hangUp() async { try { + final hangUpStart = DateTime.now().millisecondsSinceEpoch; + print('CallPage: [${hangUpStart}ms] Initiating hangUp'); final result = await _callService.hangUpCall(context); - print('CallPage: Hang up result: $result'); - if (result["status"] == "ended" && mounted && Navigator.canPop(context)) { - Navigator.pop(context); - } + print('CallPage: [${DateTime.now().millisecondsSinceEpoch}ms] Hang up result: $result'); } catch (e) { - print("CallPage: Error hanging up: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error hanging up: $e")), - ); + print('CallPage: [${DateTime.now().millisecondsSinceEpoch}ms] Error hanging up: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error hanging up: $e")), + ); + } } } @@ -80,6 +134,7 @@ class _CallPageState extends State { final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; final double statusFontSize = isKeypadVisible ? 16.0 : 16.0; + print('CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); return Scaffold( body: Container( color: Colors.black, @@ -91,7 +146,7 @@ class _CallPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 35), + const SizedBox(height: 35), ObfuscatedAvatar( imageBytes: widget.thumbnail, radius: avatarRadius, @@ -134,7 +189,7 @@ class _CallPageState extends State { fontSize: statusFontSize, color: Colors.white70), ), Text( - 'Calling...', + _callStatus, style: TextStyle( fontSize: statusFontSize, color: Colors.white70), ), @@ -167,8 +222,7 @@ class _CallPageState extends State { IconButton( padding: EdgeInsets.zero, onPressed: _toggleKeypad, - icon: - const Icon(Icons.close, color: Colors.white), + icon: const Icon(Icons.close, color: Colors.white), ), ], ), @@ -247,8 +301,11 @@ class _CallPageState extends State { children: [ IconButton( onPressed: _toggleKeypad, - icon: const Icon(Icons.dialpad, - color: Colors.white, size: 32), + icon: const Icon( + Icons.dialpad, + color: Colors.white, + size: 32, + ), ), const Text( 'Keypad', @@ -290,12 +347,17 @@ class _CallPageState extends State { children: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.person_add, - color: Colors.white, size: 32), + icon: const Icon( + Icons.person_add, + color: Colors.white, + size: 32, + ), + ), + const Text( + 'Add Contact', + style: TextStyle( + color: Colors.white, fontSize: 14), ), - const Text('Add Contact', - style: TextStyle( - color: Colors.white, fontSize: 14)), ], ), Column( @@ -303,12 +365,17 @@ class _CallPageState extends State { children: [ IconButton( onPressed: () {}, - icon: const Icon(Icons.sim_card, - color: Colors.white, size: 32), + icon: const Icon( + Icons.sim_card, + color: Colors.white, + size: 32, + ), + ), + const Text( + 'Change SIM', + style: TextStyle( + color: Colors.white, fontSize: 14), ), - const Text('Change SIM', - style: TextStyle( - color: Colors.white, fontSize: 14)), ], ), ], @@ -345,4 +412,4 @@ class _CallPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 26307be..b20b048 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; @@ -10,71 +11,163 @@ class CallService { static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; - static String? _currentCallState; 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 { - print('CallService: Received method ${call.method} with args ${call.arguments}'); + 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; - currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); - await _fetchContactInfo(currentPhoneNumber!); - print('CallService: Call added, number: $currentPhoneNumber, state: $state'); - _handleCallState(state); + 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; + final state = call.arguments["state"] as String?; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; - print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); - _handleCallState(state); + 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: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); + 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) { - _channel.invokeMethod("callEndedFromFlutter"); + await _channel.invokeMethod("callEndedFromFlutter"); } currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; - _currentCallState = null; + _activeCallNumber = null; break; case "incomingCallFromNotification": - final phoneNumber = call.arguments["phoneNumber"] as String; + final phoneNumber = call.arguments["phoneNumber"] as String?; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; - currentPhoneNumber = phoneNumber; - await _fetchContactInfo(currentPhoneNumber!); - print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); - _handleIncomingCall(phoneNumber); + 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) { - if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) { + 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; @@ -83,10 +176,16 @@ class CallService { } String _normalizePhoneNumber(String number) { - return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); + 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'); @@ -97,53 +196,64 @@ class CallService { } } - void _checkPendingCall() { - if (_pendingCall != null) { - final context = navigatorKey.currentContext; - if (context != null) { - print('CallService: Processing queued call: ${_pendingCall!["phoneNumber"]}'); - currentPhoneNumber = _pendingCall!["phoneNumber"]; - _navigateToIncomingCallPage(context); - _pendingCall = null; - } else { - print('CallService: Context still null, retrying...'); - Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); - } + 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 _handleCallState(String state) { + void _navigateToCallPage() { + if (_isNavigating) { + print('CallService: Navigation already in progress, skipping'); + return; + } + _isNavigating = true; + final context = navigatorKey.currentContext; if (context == null) { - print('CallService: Navigator context is null, cannot navigate'); + print('CallService: Cannot navigate to CallPage, context is null'); + _isNavigating = false; return; } - if (_currentCallState == state) { - print('CallService: State $state already handled, skipping'); + 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; } - _currentCallState = state; - - if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(); - } else if (state == "active" || state == "dialing") { - _navigateToCallPage(context); - } else if (state == "ringing") { - _navigateToIncomingCallPage(context); - } - } - - void _navigateToCallPage(BuildContext context) { - final currentRoute = ModalRoute.of(context)?.settings.name; - print('CallService: Navigating to CallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute'); - if (_isCallPageVisible && currentRoute == '/call') { - print('CallService: CallPage already visible, skipping navigation'); - return; - } - if (_isCallPageVisible && currentRoute == '/incoming_call') { - print('CallService: Replacing IncomingCallPage with CallPage'); + 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( @@ -155,21 +265,35 @@ class CallService { ), ), ).then((_) { - print('CallService: CallPage popped'); _isCallPageVisible = false; + _isNavigating = false; + print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _navigateToIncomingCallPage(BuildContext context) { - final currentRoute = ModalRoute.of(context)?.settings.name; - print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute'); - if (_isCallPageVisible && currentRoute == '/incoming_call') { - print('CallService: IncomingCallPage already visible, skipping navigation'); + 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( @@ -183,8 +307,9 @@ class CallService { ), ), ).then((_) { - print('CallService: IncomingCallPage popped'); _isCallPageVisible = false; + _isNavigating = false; + print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } @@ -195,70 +320,79 @@ class CallService { print('CallService: Cannot close page, context is null'); return; } - print('CallService: Attempting to close call page. Visible: $_isCallPageVisible'); - if (!_isCallPageVisible) { - print('CallService: CallPage not visible, skipping pop'); - return; - } - if (Navigator.canPop(context)) { - print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); + 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); - _isCallPageVisible = false; } else { - print('CallService: Cannot pop, no routes to pop'); + print('CallService: No call page to pop, _isCallPageVisible: $_isCallPageVisible, Current Route: $currentRoute'); } + _isCallPageVisible = false; + _activeCallNumber = null; } - Future makeGsmCall( + 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'); + print('CallService: Making GSM call to $phoneNumber, displayName: $currentDisplayName'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); - if (result["status"] != "calling") { + final resultMap = Map.from(result as Map); + if (resultMap["status"] != "calling") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to initiate call")), ); - return; } - _handleCallState("dialing"); + return resultMap; } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error making call: $e")), ); - rethrow; + return {"status": "error", "message": e.toString()}; } } - Future hangUpCall(BuildContext context) async { + Future> hangUpCall(BuildContext context) async { try { print('CallService: Hanging up call'); final result = await _channel.invokeMethod('hangUpCall'); print('CallService: hangUpCall result: $result'); - if (result["status"] != "ended") { + 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")), ); - } else { - _closeCallPage(); } + return resultMap; } catch (e) { print("CallService: Error hanging up call: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error hanging up call: $e")), ); - rethrow; + _closeCallPage(); + return {"status": "error", "message": e.toString()}; } } } \ No newline at end of file