diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index 5f5b29a..eb362de 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -153,6 +153,18 @@ class MainActivity : FlutterActivity() { } result.success(true) } + "getCallState" -> { + val stateStr = when (MyInCallService.currentCall?.state) { + android.telecom.Call.STATE_ACTIVE -> "active" + android.telecom.Call.STATE_RINGING -> "ringing" + android.telecom.Call.STATE_DIALING -> "dialing" + android.telecom.Call.STATE_DISCONNECTED -> "disconnected" + android.telecom.Call.STATE_DISCONNECTING -> "disconnecting" + else -> "unknown" + } + Log.d(TAG, "getCallState called, returning: $stateStr") + result.success(stateStr) + } else -> result.notImplemented() } } diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index 368ffde..dbbea12 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,72 @@ 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(); + _checkInitialCallState(); + _listenToCallState(); + } + + @override + void dispose() { + _callTimer?.cancel(); + _callStateSubscription?.cancel(); + super.dispose(); + } + + void _checkInitialCallState() async { + try { + final state = await _callService.getCallState(); + print('CallPage: Initial call state: $state'); + if (mounted && state == "active") { + setState(() { + _callStatus = "00:00"; + _startCallTimer(); + }); + } + } catch (e) { + print('CallPage: Error checking initial state: $e'); + } + } + + void _listenToCallState() { + _callStateSubscription = _callService.callStateStream.listen((state) { + print('CallPage: 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'; + }); + } + }); + } void _addDigit(String digit) { setState(() { @@ -61,16 +128,16 @@ class _CallPageState extends State { void _hangUp() async { try { + print('CallPage: 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); - } } catch (e) { - print("CallPage: Error hanging up: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error hanging up: $e")), - ); + print('CallPage: Error hanging up: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error hanging up: $e")), + ); + } } } @@ -80,6 +147,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 +159,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 +202,7 @@ class _CallPageState extends State { fontSize: statusFontSize, color: Colors.white70), ), Text( - 'Calling...', + _callStatus, style: TextStyle( fontSize: statusFontSize, color: Colors.white70), ), @@ -167,8 +235,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 +314,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 +360,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 +378,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 +425,4 @@ class _CallPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/dialer/lib/features/contacts/widgets/contact_modal.dart b/dialer/lib/features/contacts/widgets/contact_modal.dart index 198f614..a73f7e6 100644 --- a/dialer/lib/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/features/contacts/widgets/contact_modal.dart @@ -267,8 +267,12 @@ class _ContactModalState extends State { ), onTap: () async { if (widget.contact.phones.isNotEmpty) { - await _callService.makeGsmCall(context, - phoneNumber: phoneNumber); + await _callService.makeGsmCall( + context, + phoneNumber: phoneNumber, + displayName: widget.contact.displayName, + thumbnail: widget.contact.thumbnail, + ); } }, ), diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/features/history/history_page.dart index 3c5b1f1..9906908 100644 --- a/dialer/lib/features/history/history_page.dart +++ b/dialer/lib/features/history/history_page.dart @@ -425,7 +425,12 @@ class _HistoryPageState extends State icon: const Icon(Icons.phone, color: Colors.green), onPressed: () async { if (contact.phones.isNotEmpty) { - _callService.makeGsmCall(context, phoneNumber: contact.phones.first.number); + await _callService.makeGsmCall( + context, + phoneNumber: contact.phones.first.number, + displayName: contact.displayName, + thumbnail: contact.thumbnail, + ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index e03c652..821608f 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,46 +1,98 @@ +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 { - print('CallService: Handling method call: ${call.method}'); + 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; - currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); - print('CallService: Call added, number: $currentPhoneNumber, state: $state'); + 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(phoneNumber); + _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; + 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") { _closeCallPage(); if (wasPhoneLocked) { - _channel.invokeMethod("callEndedFromFlutter"); + 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: Skipping fetch, active call: $_activeCallNumber, current: $currentPhoneNumber, displayName: $currentDisplayName'); + } _navigateToCallPage(); } else if (state == "ringing") { - final phoneNumber = call.arguments["callId"] as String; - _handleIncomingCall(phoneNumber.replaceFirst('tel:', '')); + 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": @@ -49,22 +101,93 @@ class CallService { print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); _closeCallPage(); if (wasPhoneLocked) { - _channel.invokeMethod("callEndedFromFlutter"); + await _channel.invokeMethod("callEndedFromFlutter"); } currentPhoneNumber = null; + currentDisplayName = null; + currentThumbnail = 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; - print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); - _handleIncomingCall(phoneNumber); + 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?; + print('CallService: Audio state changed, route: $route'); 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; + } + } + + 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'); @@ -75,67 +198,119 @@ 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 _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; } - if (_isCallPageVisible) { - print('CallService: CallPage already visible, skipping navigation'); + 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; } - print('CallService: Navigating to CallPage'); - Navigator.push( + 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: currentPhoneNumber!, + displayName: currentDisplayName ?? currentPhoneNumber!, phoneNumber: currentPhoneNumber!, - thumbnail: null, + thumbnail: currentThumbnail, ), ), ).then((_) { _isCallPageVisible = false; + _isNavigating = false; print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _navigateToIncomingCallPage(BuildContext context) { - if (_isCallPageVisible) { - 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; } - print('CallService: Navigating to IncomingCallPage'); Navigator.push( context, MaterialPageRoute( settings: const RouteSettings(name: '/incoming_call'), builder: (context) => IncomingCallPage( - displayName: currentPhoneNumber!, + displayName: currentDisplayName ?? currentPhoneNumber!, phoneNumber: currentPhoneNumber!, - thumbnail: null, + thumbnail: currentThumbnail, ), ), ).then((_) { _isCallPageVisible = false; + _isNavigating = false; print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; @@ -155,6 +330,7 @@ class CallService { } else { print('CallService: No page to pop'); } + _activeCallNumber = null; } Future> makeGsmCall( @@ -164,8 +340,17 @@ class CallService { 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; - print('CallService: Making GSM call to $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);