From a6d39e5d5df215d5f249e7c5acd58f7d8ffb4d5f Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 21:39:39 +0300 Subject: [PATCH 01/21] feat: fetch contact in makeGsmCall to show it in call --- dialer/lib/services/call_service.dart | 152 ++++++++++++-------------- 1 file changed, 70 insertions(+), 82 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index e03c652..c7e41a6 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -2,124 +2,115 @@ 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'; // Import your ContactService class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; + static String? currentDisplayName; // Store display name + static Uint8List? currentThumbnail; // Store thumbnail static bool _isCallPageVisible = false; - static Map? _pendingCall; - static bool wasPhoneLocked = false; + final ContactService _contactService = ContactService(); // Instantiate ContactService static final GlobalKey navigatorKey = GlobalKey(); CallService() { _channel.setMethodCallHandler((call) async { - print('CallService: Handling method call: ${call.method}'); + final context = navigatorKey.currentContext; + print('CallService: Received method ${call.method} with args ${call.arguments}'); + if (context == null) { + print('CallService: Navigator context is null, cannot navigate'); + return; + } + switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String; final state = call.arguments["state"] as String; currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); + // Fetch contact info using ContactService if not already set + await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); if (state == "ringing") { - _handleIncomingCall(phoneNumber); + _navigateToIncomingCallPage(context); } else { - _navigateToCallPage(); + _navigateToCallPage(context); } break; case "callStateChanged": final state = call.arguments["state"] as String; - wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; - print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); + print('CallService: State changed to $state'); if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(); - if (wasPhoneLocked) { - _channel.invokeMethod("callEndedFromFlutter"); - } + _closeCallPage(context); } else if (state == "active" || state == "dialing") { - _navigateToCallPage(); + _navigateToCallPage(context); } else if (state == "ringing") { - final phoneNumber = call.arguments["callId"] as String; - _handleIncomingCall(phoneNumber.replaceFirst('tel:', '')); + _navigateToIncomingCallPage(context); } break; case "callEnded": case "callRemoved": - wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; - print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); - _closeCallPage(); - if (wasPhoneLocked) { - _channel.invokeMethod("callEndedFromFlutter"); - } + print('CallService: Call ended/removed'); + _closeCallPage(context); currentPhoneNumber = null; - break; - case "incomingCallFromNotification": - 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); + currentDisplayName = null; + currentThumbnail = null; break; } }); } - void _handleIncomingCall(String 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); - } - } - - 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 _fetchContactInfo(String phoneNumber) async { + if (currentDisplayName != null && currentThumbnail != null) return; // Already set + try { + final contacts = await _contactService.fetchContacts(); // Use ContactService + for (var contact in contacts) { + for (var phone in contact.phones) { + if (_normalizePhoneNumber(phone.number) == _normalizePhoneNumber(phoneNumber)) { + currentDisplayName = contact.displayName; + currentThumbnail = contact.thumbnail; + return; + } + } } + // If no match found, use phone number as fallback + currentDisplayName ??= phoneNumber; + currentThumbnail ??= null; + } catch (e) { + print('CallService: Error fetching contact info: $e'); + currentDisplayName = phoneNumber; + currentThumbnail = null; } } - void _navigateToCallPage() { - final context = navigatorKey.currentContext; - if (context == null) { - print('CallService: Cannot navigate to CallPage, context is null'); - return; - } - if (_isCallPageVisible) { + String _normalizePhoneNumber(String number) { + return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); + } + + void _navigateToCallPage(BuildContext context) { + if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') { print('CallService: CallPage already visible, skipping navigation'); return; } print('CallService: Navigating to CallPage'); - Navigator.push( + 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; - print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _navigateToIncomingCallPage(BuildContext context) { - if (_isCallPageVisible) { + if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') { print('CallService: IncomingCallPage already visible, skipping navigation'); return; } @@ -129,35 +120,30 @@ class CallService { MaterialPageRoute( settings: const RouteSettings(name: '/incoming_call'), builder: (context) => IncomingCallPage( - displayName: currentPhoneNumber!, + displayName: currentDisplayName ?? currentPhoneNumber!, phoneNumber: currentPhoneNumber!, - thumbnail: null, + thumbnail: currentThumbnail, ), ), ).then((_) { _isCallPageVisible = 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'); + void _closeCallPage(BuildContext context) { + if (!_isCallPageVisible) { + print('CallService: CallPage not visible, skipping pop'); return; } - print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { - print('CallService: Popping call page'); + print('CallService: Popping CallPage'); Navigator.pop(context); _isCallPageVisible = false; - } else { - print('CallService: No page to pop'); } } - Future> makeGsmCall( + Future makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, @@ -165,43 +151,45 @@ class CallService { }) async { try { currentPhoneNumber = phoneNumber; + currentDisplayName = displayName ?? phoneNumber; // Use provided or fetch later + currentThumbnail = thumbnail; // Use provided or fetch later print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); - final resultMap = Map.from(result as Map); - if (resultMap["status"] != "calling") { + if (result["status"] != "calling") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to initiate call")), ); + return; } - return resultMap; + // Fetch contact info if not provided + await _fetchContactInfo(phoneNumber); + _navigateToCallPage(context); // Navigate immediately after call initiation } 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()}; + rethrow; } } - 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'); - final resultMap = Map.from(result as Map); - if (resultMap["status"] != "ended") { + if (result["status"] != "ended") { 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")), ); - return {"status": "error", "message": e.toString()}; + rethrow; } } } \ No newline at end of file -- 2.45.2 From 6fb7fefef46d5841a15fd032fe121c644974e56f Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 21:55:26 +0300 Subject: [PATCH 02/21] feat: give parameters to callPage to avoid fetching when possible --- .../contacts/widgets/contact_modal.dart | 8 +++-- dialer/lib/features/history/history_page.dart | 7 +++- dialer/lib/services/call_service.dart | 32 ++++++++++--------- 3 files changed, 29 insertions(+), 18 deletions(-) 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 c7e41a6..c7e8ad2 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,16 +1,17 @@ +import 'dart:typed_data'; 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'; // Import your ContactService +import '../services/contact_service.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; - static String? currentDisplayName; // Store display name - static Uint8List? currentThumbnail; // Store thumbnail + static String? currentDisplayName; + static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; - final ContactService _contactService = ContactService(); // Instantiate ContactService + final ContactService _contactService = ContactService(); static final GlobalKey navigatorKey = GlobalKey(); @@ -28,7 +29,6 @@ class CallService { final phoneNumber = call.arguments["callId"] as String; final state = call.arguments["state"] as String; currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); - // Fetch contact info using ContactService if not already set await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); if (state == "ringing") { @@ -61,12 +61,12 @@ class CallService { } Future _fetchContactInfo(String phoneNumber) async { - if (currentDisplayName != null && currentThumbnail != null) return; // Already set try { - final contacts = await _contactService.fetchContacts(); // Use ContactService + final contacts = await _contactService.fetchContacts(); + final normalizedPhoneNumber = _normalizePhoneNumber(phoneNumber); for (var contact in contacts) { for (var phone in contact.phones) { - if (_normalizePhoneNumber(phone.number) == _normalizePhoneNumber(phoneNumber)) { + if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) { currentDisplayName = contact.displayName; currentThumbnail = contact.thumbnail; return; @@ -74,8 +74,8 @@ class CallService { } } // If no match found, use phone number as fallback - currentDisplayName ??= phoneNumber; - currentThumbnail ??= null; + currentDisplayName = phoneNumber; + currentThumbnail = null; } catch (e) { print('CallService: Error fetching contact info: $e'); currentDisplayName = phoneNumber; @@ -151,8 +151,12 @@ class CallService { }) async { try { currentPhoneNumber = phoneNumber; - currentDisplayName = displayName ?? phoneNumber; // Use provided or fetch later - currentThumbnail = thumbnail; // Use provided or fetch later + // Use provided displayName and thumbnail if available, otherwise fetch + currentDisplayName = displayName ?? phoneNumber; + currentThumbnail = thumbnail; + if (displayName == null || thumbnail == null) { + await _fetchContactInfo(phoneNumber); + } print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); @@ -162,9 +166,7 @@ class CallService { ); return; } - // Fetch contact info if not provided - await _fetchContactInfo(phoneNumber); - _navigateToCallPage(context); // Navigate immediately after call initiation + _navigateToCallPage(context); } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( -- 2.45.2 From a0331adf224116840d0fd1f0fde80ae7ae1468b2 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 22:06:43 +0300 Subject: [PATCH 03/21] fix: no screen stacking when making calls --- dialer/lib/services/call_service.dart | 64 ++++++++++++++++++--------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index c7e8ad2..88e7811 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,4 +1,3 @@ -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; @@ -11,6 +10,7 @@ class CallService { static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; + static String? _currentCallState; final ContactService _contactService = ContactService(); static final GlobalKey navigatorKey = GlobalKey(); @@ -31,22 +31,12 @@ class CallService { currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); - if (state == "ringing") { - _navigateToIncomingCallPage(context); - } else { - _navigateToCallPage(context); - } + _handleCallState(context, state); break; case "callStateChanged": final state = call.arguments["state"] as String; print('CallService: State changed to $state'); - if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(context); - } else if (state == "active" || state == "dialing") { - _navigateToCallPage(context); - } else if (state == "ringing") { - _navigateToIncomingCallPage(context); - } + _handleCallState(context, state); break; case "callEnded": case "callRemoved": @@ -55,6 +45,7 @@ class CallService { currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; + _currentCallState = null; break; } }); @@ -73,7 +64,6 @@ class CallService { } } } - // If no match found, use phone number as fallback currentDisplayName = phoneNumber; currentThumbnail = null; } catch (e) { @@ -87,12 +77,33 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } + void _handleCallState(BuildContext context, String state) { + if (_currentCallState == state) { + print('CallService: State $state already handled, skipping'); + return; + } + _currentCallState = state; + + if (state == "disconnected" || state == "disconnecting") { + _closeCallPage(context); + } else if (state == "active" || state == "dialing") { + _navigateToCallPage(context); + } else if (state == "ringing") { + _navigateToIncomingCallPage(context); + } + } + void _navigateToCallPage(BuildContext context) { - if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') { + 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; } - print('CallService: Navigating to CallPage'); + if (_isCallPageVisible && currentRoute == '/incoming_call') { + print('CallService: Replacing IncomingCallPage with CallPage'); + Navigator.pop(context); + } Navigator.pushReplacement( context, MaterialPageRoute( @@ -104,17 +115,23 @@ class CallService { ), ), ).then((_) { + print('CallService: CallPage popped'); _isCallPageVisible = false; }); _isCallPageVisible = true; } void _navigateToIncomingCallPage(BuildContext context) { - if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') { + 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'); return; } - print('CallService: Navigating to IncomingCallPage'); + if (_isCallPageVisible && currentRoute == '/call') { + print('CallService: CallPage visible, not showing IncomingCallPage'); + return; + } Navigator.push( context, MaterialPageRoute( @@ -126,20 +143,24 @@ class CallService { ), ), ).then((_) { + print('CallService: IncomingCallPage popped'); _isCallPageVisible = false; }); _isCallPageVisible = true; } void _closeCallPage(BuildContext context) { + 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'); + print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); Navigator.pop(context); _isCallPageVisible = false; + } else { + print('CallService: Cannot pop, no routes to pop'); } } @@ -151,7 +172,6 @@ class CallService { }) async { try { currentPhoneNumber = phoneNumber; - // Use provided displayName and thumbnail if available, otherwise fetch currentDisplayName = displayName ?? phoneNumber; currentThumbnail = thumbnail; if (displayName == null || thumbnail == null) { @@ -166,7 +186,7 @@ class CallService { ); return; } - _navigateToCallPage(context); + _handleCallState(context, "dialing"); } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( @@ -185,6 +205,8 @@ class CallService { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); + } else { + _closeCallPage(context); } } catch (e) { print("CallService: Error hanging up call: $e"); -- 2.45.2 From 59e813974c445bb5338732a2e8e5ed31408eacd7 Mon Sep 17 00:00:00 2001 From: florian Date: Thu, 17 Apr 2025 12:26:32 +0000 Subject: [PATCH 04/21] callNotifications and various fix related to calls (#49) Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/49 Co-authored-by: florian Co-committed-by: florian --- dialer/lib/services/call_service.dart | 34 +++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 88e7811..7057ad2 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -17,13 +17,7 @@ class CallService { CallService() { _channel.setMethodCallHandler((call) async { - final context = navigatorKey.currentContext; - print('CallService: Received method ${call.method} with args ${call.arguments}'); - if (context == null) { - print('CallService: Navigator context is null, cannot navigate'); - return; - } - + print('CallService: Handling method call: ${call.method}'); switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String; @@ -40,8 +34,12 @@ class CallService { break; case "callEnded": case "callRemoved": - print('CallService: Call ended/removed'); - _closeCallPage(context); + wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; + print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); + _closeCallPage(); + if (wasPhoneLocked) { + _channel.invokeMethod("callEndedFromFlutter"); + } currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; @@ -117,6 +115,7 @@ class CallService { ).then((_) { print('CallService: CallPage popped'); _isCallPageVisible = false; + print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } @@ -145,6 +144,7 @@ class CallService { ).then((_) { print('CallService: IncomingCallPage popped'); _isCallPageVisible = false; + print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } @@ -155,6 +155,7 @@ class CallService { print('CallService: CallPage not visible, skipping pop'); return; } + print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); Navigator.pop(context); @@ -164,7 +165,7 @@ class CallService { } } - Future makeGsmCall( + Future> makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, @@ -180,7 +181,8 @@ class CallService { print('CallService: Making GSM call to $phoneNumber'); 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")), ); @@ -192,28 +194,30 @@ class CallService { 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") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); } else { _closeCallPage(context); } + return resultMap; } catch (e) { print("CallService: Error hanging up call: $e"); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error hanging up call: $e")), ); - rethrow; + return {"status": "error", "message": e.toString()}; } } } \ No newline at end of file -- 2.45.2 From bf9bb3ed9df82c5c6239cb35e0b8313f98cb64b7 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 21:55:26 +0300 Subject: [PATCH 05/21] feat: give parameters to callPage to avoid fetching when possible --- dialer/lib/services/call_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 7057ad2..d828416 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,3 +1,4 @@ +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; -- 2.45.2 From 24fef24c6da03ce02264ce9773bf1b8bb08927d0 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 22:06:43 +0300 Subject: [PATCH 06/21] fix: no screen stacking when making calls --- dialer/lib/services/call_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index d828416..7057ad2 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,4 +1,3 @@ -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; -- 2.45.2 From 540b330f0d32cc1f26935ceb5dec70cab3f1b587 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Thu, 17 Apr 2025 16:07:08 +0300 Subject: [PATCH 07/21] feat: merge call_service with callNotifications changes --- dialer/lib/services/call_service.dart | 83 ++++++++++++++++++++------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 7057ad2..26307be 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -11,13 +11,15 @@ class CallService { static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; static String? _currentCallState; + static Map? _pendingCall; + static bool wasPhoneLocked = false; final ContactService _contactService = ContactService(); static final GlobalKey navigatorKey = GlobalKey(); CallService() { _channel.setMethodCallHandler((call) async { - print('CallService: Handling method call: ${call.method}'); + print('CallService: Received method ${call.method} with args ${call.arguments}'); switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String; @@ -25,12 +27,13 @@ class CallService { currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); - _handleCallState(context, state); + _handleCallState(state); break; case "callStateChanged": final state = call.arguments["state"] as String; - print('CallService: State changed to $state'); - _handleCallState(context, state); + wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; + print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); + _handleCallState(state); break; case "callEnded": case "callRemoved": @@ -45,6 +48,14 @@ class CallService { currentThumbnail = null; _currentCallState = null; break; + case "incomingCallFromNotification": + 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); + break; } }); } @@ -75,7 +86,38 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } - void _handleCallState(BuildContext context, String state) { + void _handleIncomingCall(String 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); + } + } + + 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()); + } + } + } + + void _handleCallState(String state) { + final context = navigatorKey.currentContext; + if (context == null) { + print('CallService: Navigator context is null, cannot navigate'); + return; + } if (_currentCallState == state) { print('CallService: State $state already handled, skipping'); return; @@ -83,7 +125,7 @@ class CallService { _currentCallState = state; if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(context); + _closeCallPage(); } else if (state == "active" || state == "dialing") { _navigateToCallPage(context); } else if (state == "ringing") { @@ -115,7 +157,6 @@ class CallService { ).then((_) { print('CallService: CallPage popped'); _isCallPageVisible = false; - print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } @@ -144,18 +185,21 @@ class CallService { ).then((_) { print('CallService: IncomingCallPage popped'); _isCallPageVisible = false; - print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } - void _closeCallPage(BuildContext context) { + void _closeCallPage() { + final context = navigatorKey.currentContext; + if (context == null) { + 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; } - print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); Navigator.pop(context); @@ -165,7 +209,7 @@ class CallService { } } - Future> makeGsmCall( + Future makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, @@ -181,43 +225,40 @@ class CallService { print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); - final resultMap = Map.from(result as Map); - if (resultMap["status"] != "calling") { + if (result["status"] != "calling") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to initiate call")), ); return; } - _handleCallState(context, "dialing"); + _handleCallState("dialing"); } 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()}; + rethrow; } } - 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'); - final resultMap = Map.from(result as Map); - if (resultMap["status"] != "ended") { + if (result["status"] != "ended") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); } else { - _closeCallPage(context); + _closeCallPage(); } - 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()}; + rethrow; } } } \ No newline at end of file -- 2.45.2 From c76826ba62b72c87788d90fcbcdd91974ba78962 Mon Sep 17 00:00:00 2001 From: florian Date: Thu, 17 Apr 2025 12:26:32 +0000 Subject: [PATCH 08/21] callNotifications and various fix related to calls (#49) Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/49 Co-authored-by: florian Co-committed-by: florian --- dialer/lib/services/call_service.dart | 96 +++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 26307be..43d34e4 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -2,7 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; import '../features/call/incoming_call_page.dart'; +<<<<<<< HEAD import '../services/contact_service.dart'; +======= +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) class CallService { static const MethodChannel _channel = MethodChannel('call_service'); @@ -10,16 +13,25 @@ class CallService { static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; +<<<<<<< HEAD static String? _currentCallState; static Map? _pendingCall; static bool wasPhoneLocked = false; final ContactService _contactService = ContactService(); +======= + static Map? _pendingCall; + static bool wasPhoneLocked = false; +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) static final GlobalKey navigatorKey = GlobalKey(); CallService() { _channel.setMethodCallHandler((call) async { +<<<<<<< HEAD print('CallService: Received method ${call.method} with args ${call.arguments}'); +======= + print('CallService: Handling method call: ${call.method}'); +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String; @@ -27,13 +39,35 @@ class CallService { currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); +<<<<<<< HEAD _handleCallState(state); +======= + if (state == "ringing") { + _handleIncomingCall(phoneNumber); + } else { + _navigateToCallPage(); + } +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) break; case "callStateChanged": final state = call.arguments["state"] as String; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); +<<<<<<< HEAD _handleCallState(state); +======= + if (state == "disconnected" || state == "disconnecting") { + _closeCallPage(); + if (wasPhoneLocked) { + _channel.invokeMethod("callEndedFromFlutter"); + } + } else if (state == "active" || state == "dialing") { + _navigateToCallPage(); + } else if (state == "ringing") { + final phoneNumber = call.arguments["callId"] as String; + _handleIncomingCall(phoneNumber.replaceFirst('tel:', '')); + } +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) break; case "callEnded": case "callRemoved": @@ -56,10 +90,18 @@ class CallService { print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); _handleIncomingCall(phoneNumber); break; + case "incomingCallFromNotification": + 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); + break; } }); } +<<<<<<< HEAD Future _fetchContactInfo(String phoneNumber) async { try { final contacts = await _contactService.fetchContacts(); @@ -86,6 +128,8 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } +======= +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) void _handleIncomingCall(String phoneNumber) { final context = navigatorKey.currentContext; if (context == null) { @@ -112,6 +156,7 @@ class CallService { } } +<<<<<<< HEAD void _handleCallState(String state) { final context = navigatorKey.currentContext; if (context == null) { @@ -145,6 +190,20 @@ class CallService { Navigator.pop(context); } Navigator.pushReplacement( +======= + void _navigateToCallPage() { + final context = navigatorKey.currentContext; + if (context == null) { + print('CallService: Cannot navigate to CallPage, context is null'); + return; + } + if (_isCallPageVisible) { + print('CallService: CallPage already visible, skipping navigation'); + return; + } + print('CallService: Navigating to CallPage'); + Navigator.push( +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) context, MaterialPageRoute( settings: const RouteSettings(name: '/call'), @@ -157,14 +216,19 @@ class CallService { ).then((_) { print('CallService: CallPage popped'); _isCallPageVisible = false; + print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } void _navigateToIncomingCallPage(BuildContext context) { +<<<<<<< HEAD final currentRoute = ModalRoute.of(context)?.settings.name; print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute'); if (_isCallPageVisible && currentRoute == '/incoming_call') { +======= + if (_isCallPageVisible) { +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) print('CallService: IncomingCallPage already visible, skipping navigation'); return; } @@ -185,6 +249,7 @@ class CallService { ).then((_) { print('CallService: IncomingCallPage popped'); _isCallPageVisible = false; + print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } @@ -193,23 +258,35 @@ class CallService { final context = navigatorKey.currentContext; if (context == null) { print('CallService: Cannot close page, context is null'); +<<<<<<< HEAD return; } print('CallService: Attempting to close call page. Visible: $_isCallPageVisible'); if (!_isCallPageVisible) { print('CallService: CallPage not visible, skipping pop'); +======= +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) return; } + print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { +<<<<<<< HEAD print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); Navigator.pop(context); _isCallPageVisible = false; } else { print('CallService: Cannot pop, no routes to pop'); +======= + print('CallService: Popping call page'); + Navigator.pop(context); + _isCallPageVisible = false; + } else { + print('CallService: No page to pop'); +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) } } - Future makeGsmCall( + Future> makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, @@ -225,40 +302,47 @@ class CallService { print('CallService: Making GSM call to $phoneNumber'); 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; } +<<<<<<< HEAD _handleCallState("dialing"); +======= + return resultMap; +>>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) } 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") { 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; + return {"status": "error", "message": e.toString()}; } } } \ No newline at end of file -- 2.45.2 From c6a88e237b920547239fe595fb9e95ad5418c5b1 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 21:39:39 +0300 Subject: [PATCH 09/21] feat: fetch contact in makeGsmCall to show it in call --- dialer/lib/services/call_service.dart | 78 +++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 43d34e4..9233a85 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -3,18 +3,29 @@ import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; import '../features/call/incoming_call_page.dart'; <<<<<<< HEAD +<<<<<<< HEAD import '../services/contact_service.dart'; ======= >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= +import '../services/contact_service.dart'; // Import your ContactService +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; +<<<<<<< HEAD static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; <<<<<<< HEAD static String? _currentCallState; +======= + static String? currentDisplayName; // Store display name + static Uint8List? currentThumbnail; // Store thumbnail + static bool _isCallPageVisible = false; + final ContactService _contactService = ContactService(); // Instantiate ContactService +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) static Map? _pendingCall; static bool wasPhoneLocked = false; final ContactService _contactService = ContactService(); @@ -37,6 +48,10 @@ class CallService { final phoneNumber = call.arguments["callId"] as String; final state = call.arguments["state"] as String; currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); +<<<<<<< HEAD +======= + // Fetch contact info using ContactService if not already set +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); <<<<<<< HEAD @@ -80,13 +95,19 @@ class CallService { currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; +<<<<<<< HEAD _currentCallState = null; +======= +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) break; case "incomingCallFromNotification": final phoneNumber = call.arguments["phoneNumber"] as String; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; currentPhoneNumber = phoneNumber; +<<<<<<< HEAD await _fetchContactInfo(currentPhoneNumber!); +======= +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); _handleIncomingCall(phoneNumber); break; @@ -101,6 +122,7 @@ class CallService { }); } +<<<<<<< HEAD <<<<<<< HEAD Future _fetchContactInfo(String phoneNumber) async { try { @@ -109,14 +131,28 @@ class CallService { for (var contact in contacts) { for (var phone in contact.phones) { if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) { +======= + Future _fetchContactInfo(String phoneNumber) async { + if (currentDisplayName != null && currentThumbnail != null) return; // Already set + try { + final contacts = await _contactService.fetchContacts(); // Use ContactService + for (var contact in contacts) { + for (var phone in contact.phones) { + if (_normalizePhoneNumber(phone.number) == _normalizePhoneNumber(phoneNumber)) { +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) currentDisplayName = contact.displayName; currentThumbnail = contact.thumbnail; return; } } } +<<<<<<< HEAD currentDisplayName = phoneNumber; currentThumbnail = null; +======= + // If no match found, use phone number as fallback + currentDisplayName ??= phoneNumber; + currentThumbnail ??= null; } catch (e) { print('CallService: Error fetching contact info: $e'); currentDisplayName = phoneNumber; @@ -128,8 +164,39 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } + Future _fetchContactInfo(String phoneNumber) async { + if (currentDisplayName != null && currentThumbnail != null) return; // Already set + try { + final contacts = await _contactService.fetchContacts(); // Use ContactService + for (var contact in contacts) { + for (var phone in contact.phones) { + if (_normalizePhoneNumber(phone.number) == _normalizePhoneNumber(phoneNumber)) { + currentDisplayName = contact.displayName; + currentThumbnail = contact.thumbnail; + return; + } + } + } + // If no match found, use phone number as fallback + currentDisplayName ??= phoneNumber; + currentThumbnail ??= null; +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) + } catch (e) { + print('CallService: Error fetching contact info: $e'); + currentDisplayName = phoneNumber; + currentThumbnail = null; + } + } + + String _normalizePhoneNumber(String number) { + return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); + } + +<<<<<<< HEAD ======= >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) void _handleIncomingCall(String phoneNumber) { final context = navigatorKey.currentContext; if (context == null) { @@ -294,11 +361,16 @@ class CallService { }) async { try { currentPhoneNumber = phoneNumber; +<<<<<<< HEAD currentDisplayName = displayName ?? phoneNumber; currentThumbnail = thumbnail; if (displayName == null || thumbnail == null) { await _fetchContactInfo(phoneNumber); } +======= + currentDisplayName = displayName ?? phoneNumber; // Use provided or fetch later + currentThumbnail = thumbnail; // Use provided or fetch later +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); @@ -309,11 +381,17 @@ class CallService { ); return; } +<<<<<<< HEAD <<<<<<< HEAD _handleCallState("dialing"); ======= return resultMap; >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= + // Fetch contact info if not provided + await _fetchContactInfo(phoneNumber); + _navigateToCallPage(context); // Navigate immediately after call initiation +>>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( -- 2.45.2 From 7cb5993382d232afa3d481a3aec8490f6c353a0b Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 21:55:26 +0300 Subject: [PATCH 10/21] feat: give parameters to callPage to avoid fetching when possible --- dialer/lib/services/call_service.dart | 43 ++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 9233a85..26c5096 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,19 +1,25 @@ +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; import '../features/call/incoming_call_page.dart'; <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD import '../services/contact_service.dart'; ======= >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) ======= import '../services/contact_service.dart'; // Import your ContactService >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= +import '../services/contact_service.dart'; +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; +<<<<<<< HEAD <<<<<<< HEAD static String? currentDisplayName; static Uint8List? currentThumbnail; @@ -23,16 +29,27 @@ class CallService { ======= static String? currentDisplayName; // Store display name static Uint8List? currentThumbnail; // Store thumbnail +======= + static String? currentDisplayName; + static Uint8List? currentThumbnail; +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) static bool _isCallPageVisible = false; +<<<<<<< HEAD final ContactService _contactService = ContactService(); // Instantiate ContactService >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) static Map? _pendingCall; static bool wasPhoneLocked = false; +<<<<<<< HEAD final ContactService _contactService = ContactService(); ======= static Map? _pendingCall; static bool wasPhoneLocked = false; >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= +======= + final ContactService _contactService = ContactService(); +>>>>>>> acbccaa (feat: give parameters to callPage to avoid fetching when possible) +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) static final GlobalKey navigatorKey = GlobalKey(); @@ -49,9 +66,12 @@ class CallService { final state = call.arguments["state"] as String; currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); <<<<<<< HEAD +<<<<<<< HEAD ======= // Fetch contact info using ContactService if not already set >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); <<<<<<< HEAD @@ -133,13 +153,17 @@ class CallService { if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) { ======= Future _fetchContactInfo(String phoneNumber) async { - if (currentDisplayName != null && currentThumbnail != null) return; // Already set try { - final contacts = await _contactService.fetchContacts(); // Use ContactService + final contacts = await _contactService.fetchContacts(); + final normalizedPhoneNumber = _normalizePhoneNumber(phoneNumber); for (var contact in contacts) { for (var phone in contact.phones) { +<<<<<<< HEAD if (_normalizePhoneNumber(phone.number) == _normalizePhoneNumber(phoneNumber)) { >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= + if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) { +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) currentDisplayName = contact.displayName; currentThumbnail = contact.thumbnail; return; @@ -151,8 +175,8 @@ class CallService { currentThumbnail = null; ======= // If no match found, use phone number as fallback - currentDisplayName ??= phoneNumber; - currentThumbnail ??= null; + currentDisplayName = phoneNumber; + currentThumbnail = null; } catch (e) { print('CallService: Error fetching contact info: $e'); currentDisplayName = phoneNumber; @@ -362,15 +386,22 @@ class CallService { try { currentPhoneNumber = phoneNumber; <<<<<<< HEAD +<<<<<<< HEAD +======= + // Use provided displayName and thumbnail if available, otherwise fetch +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) currentDisplayName = displayName ?? phoneNumber; currentThumbnail = thumbnail; if (displayName == null || thumbnail == null) { await _fetchContactInfo(phoneNumber); } +<<<<<<< HEAD ======= currentDisplayName = displayName ?? phoneNumber; // Use provided or fetch later currentThumbnail = thumbnail; // Use provided or fetch later >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); @@ -382,6 +413,7 @@ class CallService { return; } <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD _handleCallState("dialing"); ======= @@ -392,6 +424,9 @@ class CallService { await _fetchContactInfo(phoneNumber); _navigateToCallPage(context); // Navigate immediately after call initiation >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= + _navigateToCallPage(context); +>>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( -- 2.45.2 From fa6e538c24cade30bf1665490b0d79808c02ae37 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 8 Apr 2025 22:06:43 +0300 Subject: [PATCH 11/21] fix: no screen stacking when making calls --- dialer/lib/services/call_service.dart | 104 +++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 26c5096..2b113b9 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -1,4 +1,3 @@ -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; @@ -24,6 +23,7 @@ class CallService { static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; +<<<<<<< HEAD <<<<<<< HEAD static String? _currentCallState; ======= @@ -34,6 +34,8 @@ class CallService { static Uint8List? currentThumbnail; >>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) static bool _isCallPageVisible = false; +======= +>>>>>>> 9e76148 (fix: no screen stacking when making calls) <<<<<<< HEAD final ContactService _contactService = ContactService(); // Instantiate ContactService >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) @@ -47,6 +49,9 @@ class CallService { >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) ======= ======= +======= + static String? _currentCallState; +>>>>>>> f491fb6 (fix: no screen stacking when making calls) final ContactService _contactService = ContactService(); >>>>>>> acbccaa (feat: give parameters to callPage to avoid fetching when possible) >>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) @@ -74,9 +79,12 @@ class CallService { >>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); +<<<<<<< HEAD <<<<<<< HEAD _handleCallState(state); ======= +======= +>>>>>>> 9e76148 (fix: no screen stacking when making calls) if (state == "ringing") { _handleIncomingCall(phoneNumber); } else { @@ -102,7 +110,18 @@ class CallService { final phoneNumber = call.arguments["callId"] as String; _handleIncomingCall(phoneNumber.replaceFirst('tel:', '')); } +<<<<<<< HEAD >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= +======= + _handleCallState(context, state); + break; + case "callStateChanged": + final state = call.arguments["state"] as String; + print('CallService: State changed to $state'); + _handleCallState(context, state); +>>>>>>> f491fb6 (fix: no screen stacking when making calls) +>>>>>>> 9e76148 (fix: no screen stacking when making calls) break; case "callEnded": case "callRemoved": @@ -115,10 +134,14 @@ class CallService { currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; +<<<<<<< HEAD <<<<<<< HEAD _currentCallState = null; ======= >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= + _currentCallState = null; +>>>>>>> 9e76148 (fix: no screen stacking when making calls) break; case "incomingCallFromNotification": final phoneNumber = call.arguments["phoneNumber"] as String; @@ -170,11 +193,14 @@ class CallService { } } } +<<<<<<< HEAD <<<<<<< HEAD currentDisplayName = phoneNumber; currentThumbnail = null; ======= // If no match found, use phone number as fallback +======= +>>>>>>> 9e76148 (fix: no screen stacking when making calls) currentDisplayName = phoneNumber; currentThumbnail = null; } catch (e) { @@ -188,6 +214,7 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } +<<<<<<< HEAD Future _fetchContactInfo(String phoneNumber) async { if (currentDisplayName != null && currentThumbnail != null) return; // Already set try { @@ -228,10 +255,25 @@ class CallService { _pendingCall = {"phoneNumber": phoneNumber}; Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); } else { +======= + void _handleCallState(BuildContext context, String state) { + if (_currentCallState == state) { + print('CallService: State $state already handled, skipping'); + return; + } + _currentCallState = state; + + if (state == "disconnected" || state == "disconnecting") { + _closeCallPage(context); + } else if (state == "active" || state == "dialing") { + _navigateToCallPage(context); + } else if (state == "ringing") { +>>>>>>> f491fb6 (fix: no screen stacking when making calls) _navigateToIncomingCallPage(context); } } +<<<<<<< HEAD void _checkPendingCall() { if (_pendingCall != null) { final context = navigatorKey.currentContext; @@ -294,7 +336,24 @@ class CallService { } print('CallService: Navigating to CallPage'); Navigator.push( +<<<<<<< HEAD >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= +======= + 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'); + Navigator.pop(context); + } + Navigator.pushReplacement( +>>>>>>> f491fb6 (fix: no screen stacking when making calls) +>>>>>>> 9e76148 (fix: no screen stacking when making calls) context, MaterialPageRoute( settings: const RouteSettings(name: '/call'), @@ -313,6 +372,7 @@ class CallService { } void _navigateToIncomingCallPage(BuildContext context) { +<<<<<<< HEAD <<<<<<< HEAD final currentRoute = ModalRoute.of(context)?.settings.name; print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute'); @@ -320,6 +380,14 @@ class CallService { ======= if (_isCallPageVisible) { >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= + if (_isCallPageVisible) { +======= + final currentRoute = ModalRoute.of(context)?.settings.name; + print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute'); + if (_isCallPageVisible && currentRoute == '/incoming_call') { +>>>>>>> f491fb6 (fix: no screen stacking when making calls) +>>>>>>> 9e76148 (fix: no screen stacking when making calls) print('CallService: IncomingCallPage already visible, skipping navigation'); return; } @@ -345,10 +413,12 @@ class CallService { _isCallPageVisible = true; } +<<<<<<< HEAD void _closeCallPage() { final context = navigatorKey.currentContext; if (context == null) { print('CallService: Cannot close page, context is null'); +<<<<<<< HEAD <<<<<<< HEAD return; } @@ -357,10 +427,19 @@ class CallService { print('CallService: CallPage not visible, skipping pop'); ======= >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= +======= + void _closeCallPage(BuildContext context) { + print('CallService: Attempting to close call page. Visible: $_isCallPageVisible'); + if (!_isCallPageVisible) { + print('CallService: CallPage not visible, skipping pop'); +>>>>>>> f491fb6 (fix: no screen stacking when making calls) +>>>>>>> 9e76148 (fix: no screen stacking when making calls) return; } print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { +<<<<<<< HEAD <<<<<<< HEAD print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); Navigator.pop(context); @@ -368,12 +447,24 @@ class CallService { } else { print('CallService: Cannot pop, no routes to pop'); ======= +======= +>>>>>>> 9e76148 (fix: no screen stacking when making calls) print('CallService: Popping call page'); Navigator.pop(context); _isCallPageVisible = false; } else { print('CallService: No page to pop'); +<<<<<<< HEAD >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= +======= + print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); + Navigator.pop(context); + _isCallPageVisible = false; + } else { + print('CallService: Cannot pop, no routes to pop'); +>>>>>>> f491fb6 (fix: no screen stacking when making calls) +>>>>>>> 9e76148 (fix: no screen stacking when making calls) } } @@ -387,9 +478,12 @@ class CallService { currentPhoneNumber = phoneNumber; <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD ======= // Use provided displayName and thumbnail if available, otherwise fetch >>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) +======= +>>>>>>> 9e76148 (fix: no screen stacking when making calls) currentDisplayName = displayName ?? phoneNumber; currentThumbnail = thumbnail; if (displayName == null || thumbnail == null) { @@ -414,6 +508,7 @@ class CallService { } <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD _handleCallState("dialing"); ======= @@ -427,6 +522,9 @@ class CallService { ======= _navigateToCallPage(context); >>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) +======= + _handleCallState(context, "dialing"); +>>>>>>> 9e76148 (fix: no screen stacking when making calls) } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( @@ -447,7 +545,11 @@ class CallService { SnackBar(content: Text("Failed to end call")), ); } else { +<<<<<<< HEAD _closeCallPage(); +======= + _closeCallPage(context); +>>>>>>> 9e76148 (fix: no screen stacking when making calls) } return resultMap; } catch (e) { -- 2.45.2 From 4a078c139a67dafabb2312064933d50ac2da340a Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Thu, 17 Apr 2025 16:07:08 +0300 Subject: [PATCH 12/21] feat: merge call_service with callNotifications changes --- dialer/lib/services/call_service.dart | 102 ++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 13 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 2b113b9..21e93cb 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -51,7 +51,12 @@ class CallService { ======= ======= static String? _currentCallState; +<<<<<<< HEAD >>>>>>> f491fb6 (fix: no screen stacking when making calls) +======= + static Map? _pendingCall; + static bool wasPhoneLocked = false; +>>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) final ContactService _contactService = ContactService(); >>>>>>> acbccaa (feat: give parameters to callPage to avoid fetching when possible) >>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) @@ -60,11 +65,15 @@ class CallService { CallService() { _channel.setMethodCallHandler((call) async { +<<<<<<< HEAD <<<<<<< HEAD print('CallService: Received method ${call.method} with args ${call.arguments}'); ======= print('CallService: Handling method call: ${call.method}'); >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) +======= + print('CallService: Received method ${call.method} with args ${call.arguments}'); +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String; @@ -80,11 +89,14 @@ class CallService { await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD _handleCallState(state); ======= ======= >>>>>>> 9e76148 (fix: no screen stacking when making calls) +======= +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) if (state == "ringing") { _handleIncomingCall(phoneNumber); } else { @@ -121,7 +133,19 @@ class CallService { print('CallService: State changed to $state'); _handleCallState(context, state); >>>>>>> f491fb6 (fix: no screen stacking when making calls) +<<<<<<< HEAD >>>>>>> 9e76148 (fix: no screen stacking when making calls) +======= +======= + _handleCallState(state); + break; + case "callStateChanged": + final state = call.arguments["state"] as String; + wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; + print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); + _handleCallState(state); +>>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) break; case "callEnded": case "callRemoved": @@ -147,10 +171,13 @@ class CallService { final phoneNumber = call.arguments["phoneNumber"] as String; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; currentPhoneNumber = phoneNumber; +<<<<<<< HEAD <<<<<<< HEAD await _fetchContactInfo(currentPhoneNumber!); ======= >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); _handleIncomingCall(phoneNumber); break; @@ -158,6 +185,9 @@ class CallService { final phoneNumber = call.arguments["phoneNumber"] as String; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; currentPhoneNumber = phoneNumber; +======= + await _fetchContactInfo(currentPhoneNumber!); +>>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); _handleIncomingCall(phoneNumber); break; @@ -214,6 +244,7 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } +<<<<<<< HEAD <<<<<<< HEAD Future _fetchContactInfo(String phoneNumber) async { if (currentDisplayName != null && currentThumbnail != null) return; // Already set @@ -243,11 +274,16 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } +<<<<<<< HEAD <<<<<<< HEAD ======= >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) ======= >>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) +======= +======= +>>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) void _handleIncomingCall(String phoneNumber) { final context = navigatorKey.currentContext; if (context == null) { @@ -255,8 +291,36 @@ class CallService { _pendingCall = {"phoneNumber": phoneNumber}; Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); } else { +<<<<<<< HEAD ======= void _handleCallState(BuildContext context, String state) { +======= + _navigateToIncomingCallPage(context); + } + } + + 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()); + } + } + } + + void _handleCallState(String state) { + final context = navigatorKey.currentContext; + if (context == null) { + print('CallService: Navigator context is null, cannot navigate'); + return; + } +>>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) if (_currentCallState == state) { print('CallService: State $state already handled, skipping'); return; @@ -264,7 +328,7 @@ class CallService { _currentCallState = state; if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(context); + _closeCallPage(); } else if (state == "active" || state == "dialing") { _navigateToCallPage(context); } else if (state == "ringing") { @@ -366,7 +430,6 @@ class CallService { ).then((_) { print('CallService: CallPage popped'); _isCallPageVisible = false; - print('CallService: CallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } @@ -408,17 +471,20 @@ class CallService { ).then((_) { print('CallService: IncomingCallPage popped'); _isCallPageVisible = false; - print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) void _closeCallPage() { final context = navigatorKey.currentContext; if (context == null) { print('CallService: Cannot close page, context is null'); <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD return; } @@ -428,8 +494,14 @@ class CallService { ======= >>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) ======= +======= +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) ======= void _closeCallPage(BuildContext context) { +======= + return; + } +>>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) print('CallService: Attempting to close call page. Visible: $_isCallPageVisible'); if (!_isCallPageVisible) { print('CallService: CallPage not visible, skipping pop'); @@ -437,7 +509,6 @@ class CallService { >>>>>>> 9e76148 (fix: no screen stacking when making calls) return; } - print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { <<<<<<< HEAD <<<<<<< HEAD @@ -468,7 +539,7 @@ class CallService { } } - Future> makeGsmCall( + Future makeGsmCall( BuildContext context, { required String phoneNumber, String? displayName, @@ -499,8 +570,7 @@ class CallService { print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); - final resultMap = Map.from(result as Map); - if (resultMap["status"] != "calling") { + if (result["status"] != "calling") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to initiate call")), ); @@ -509,6 +579,7 @@ class CallService { <<<<<<< HEAD <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD _handleCallState("dialing"); ======= @@ -525,39 +596,44 @@ class CallService { ======= _handleCallState(context, "dialing"); >>>>>>> 9e76148 (fix: no screen stacking when making calls) +======= + _handleCallState("dialing"); +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) } 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()}; + rethrow; } } - 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'); - final resultMap = Map.from(result as Map); - if (resultMap["status"] != "ended") { + if (result["status"] != "ended") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); } else { +<<<<<<< HEAD <<<<<<< HEAD _closeCallPage(); ======= _closeCallPage(context); >>>>>>> 9e76148 (fix: no screen stacking when making calls) +======= + _closeCallPage(); +>>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) } - 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()}; + rethrow; } } } \ No newline at end of file -- 2.45.2 From dac6b53b2a9f0e9650fba84551586bfc1eb03242 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Thu, 17 Apr 2025 16:47:11 +0300 Subject: [PATCH 13/21] feat: merged --- dialer/lib/services/call_service.dart | 375 -------------------------- 1 file changed, 375 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 21e93cb..26307be 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -2,141 +2,31 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../features/call/call_page.dart'; import '../features/call/incoming_call_page.dart'; -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD import '../services/contact_service.dart'; -======= ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= -import '../services/contact_service.dart'; // Import your ContactService ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= -import '../services/contact_service.dart'; ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; -<<<<<<< HEAD -<<<<<<< HEAD static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; -<<<<<<< HEAD -<<<<<<< HEAD static String? _currentCallState; -======= - static String? currentDisplayName; // Store display name - static Uint8List? currentThumbnail; // Store thumbnail -======= - static String? currentDisplayName; - static Uint8List? currentThumbnail; ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) - static bool _isCallPageVisible = false; -======= ->>>>>>> 9e76148 (fix: no screen stacking when making calls) -<<<<<<< HEAD - final ContactService _contactService = ContactService(); // Instantiate ContactService ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) static Map? _pendingCall; static bool wasPhoneLocked = false; -<<<<<<< HEAD final ContactService _contactService = ContactService(); -======= - static Map? _pendingCall; - static bool wasPhoneLocked = false; ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= -======= -======= - static String? _currentCallState; -<<<<<<< HEAD ->>>>>>> f491fb6 (fix: no screen stacking when making calls) -======= - static Map? _pendingCall; - static bool wasPhoneLocked = false; ->>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) - final ContactService _contactService = ContactService(); ->>>>>>> acbccaa (feat: give parameters to callPage to avoid fetching when possible) ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) static final GlobalKey navigatorKey = GlobalKey(); CallService() { _channel.setMethodCallHandler((call) async { -<<<<<<< HEAD -<<<<<<< HEAD print('CallService: Received method ${call.method} with args ${call.arguments}'); -======= - print('CallService: Handling method call: ${call.method}'); ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= - print('CallService: Received method ${call.method} with args ${call.arguments}'); ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String; final state = call.arguments["state"] as String; currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); -<<<<<<< HEAD -<<<<<<< HEAD -======= - // Fetch contact info using ContactService if not already set ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) await _fetchContactInfo(currentPhoneNumber!); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD - _handleCallState(state); -======= -======= ->>>>>>> 9e76148 (fix: no screen stacking when making calls) -======= ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) - if (state == "ringing") { - _handleIncomingCall(phoneNumber); - } else { - _navigateToCallPage(); - } ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) - break; - case "callStateChanged": - final state = call.arguments["state"] as String; - wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; - print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); -<<<<<<< HEAD - _handleCallState(state); -======= - if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(); - if (wasPhoneLocked) { - _channel.invokeMethod("callEndedFromFlutter"); - } - } else if (state == "active" || state == "dialing") { - _navigateToCallPage(); - } else if (state == "ringing") { - final phoneNumber = call.arguments["callId"] as String; - _handleIncomingCall(phoneNumber.replaceFirst('tel:', '')); - } -<<<<<<< HEAD ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= -======= - _handleCallState(context, state); - break; - case "callStateChanged": - final state = call.arguments["state"] as String; - print('CallService: State changed to $state'); - _handleCallState(context, state); ->>>>>>> f491fb6 (fix: no screen stacking when making calls) -<<<<<<< HEAD ->>>>>>> 9e76148 (fix: no screen stacking when making calls) -======= -======= _handleCallState(state); break; case "callStateChanged": @@ -144,8 +34,6 @@ class CallService { wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); _handleCallState(state); ->>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) break; case "callEnded": case "callRemoved": @@ -158,36 +46,13 @@ class CallService { currentPhoneNumber = null; currentDisplayName = null; currentThumbnail = null; -<<<<<<< HEAD -<<<<<<< HEAD _currentCallState = null; -======= ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= - _currentCallState = null; ->>>>>>> 9e76148 (fix: no screen stacking when making calls) break; case "incomingCallFromNotification": final phoneNumber = call.arguments["phoneNumber"] as String; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; currentPhoneNumber = phoneNumber; -<<<<<<< HEAD -<<<<<<< HEAD await _fetchContactInfo(currentPhoneNumber!); -======= ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) - print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); - _handleIncomingCall(phoneNumber); - break; - case "incomingCallFromNotification": - final phoneNumber = call.arguments["phoneNumber"] as String; - wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; - currentPhoneNumber = phoneNumber; -======= - await _fetchContactInfo(currentPhoneNumber!); ->>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); _handleIncomingCall(phoneNumber); break; @@ -195,8 +60,6 @@ class CallService { }); } -<<<<<<< HEAD -<<<<<<< HEAD Future _fetchContactInfo(String phoneNumber) async { try { final contacts = await _contactService.fetchContacts(); @@ -204,33 +67,12 @@ class CallService { for (var contact in contacts) { for (var phone in contact.phones) { if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) { -======= - Future _fetchContactInfo(String phoneNumber) async { - try { - final contacts = await _contactService.fetchContacts(); - final normalizedPhoneNumber = _normalizePhoneNumber(phoneNumber); - for (var contact in contacts) { - for (var phone in contact.phones) { -<<<<<<< HEAD - if (_normalizePhoneNumber(phone.number) == _normalizePhoneNumber(phoneNumber)) { ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= - if (_normalizePhoneNumber(phone.number) == normalizedPhoneNumber) { ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) currentDisplayName = contact.displayName; currentThumbnail = contact.thumbnail; return; } } } -<<<<<<< HEAD -<<<<<<< HEAD - currentDisplayName = phoneNumber; - currentThumbnail = null; -======= - // If no match found, use phone number as fallback -======= ->>>>>>> 9e76148 (fix: no screen stacking when making calls) currentDisplayName = phoneNumber; currentThumbnail = null; } catch (e) { @@ -244,46 +86,6 @@ class CallService { return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); } -<<<<<<< HEAD -<<<<<<< HEAD - Future _fetchContactInfo(String phoneNumber) async { - if (currentDisplayName != null && currentThumbnail != null) return; // Already set - try { - final contacts = await _contactService.fetchContacts(); // Use ContactService - for (var contact in contacts) { - for (var phone in contact.phones) { - if (_normalizePhoneNumber(phone.number) == _normalizePhoneNumber(phoneNumber)) { - currentDisplayName = contact.displayName; - currentThumbnail = contact.thumbnail; - return; - } - } - } - // If no match found, use phone number as fallback - currentDisplayName ??= phoneNumber; - currentThumbnail ??= null; ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) - } catch (e) { - print('CallService: Error fetching contact info: $e'); - currentDisplayName = phoneNumber; - currentThumbnail = null; - } - } - - String _normalizePhoneNumber(String number) { - return number.replaceAll(RegExp(r'[\s\-\(\)]'), ''); - } - -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= -======= ->>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) void _handleIncomingCall(String phoneNumber) { final context = navigatorKey.currentContext; if (context == null) { @@ -291,10 +93,6 @@ class CallService { _pendingCall = {"phoneNumber": phoneNumber}; Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); } else { -<<<<<<< HEAD -======= - void _handleCallState(BuildContext context, String state) { -======= _navigateToIncomingCallPage(context); } } @@ -314,46 +112,6 @@ class CallService { } } - void _handleCallState(String state) { - final context = navigatorKey.currentContext; - if (context == null) { - print('CallService: Navigator context is null, cannot navigate'); - return; - } ->>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) - if (_currentCallState == state) { - print('CallService: State $state already handled, skipping'); - return; - } - _currentCallState = state; - - if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(); - } else if (state == "active" || state == "dialing") { - _navigateToCallPage(context); - } else if (state == "ringing") { ->>>>>>> f491fb6 (fix: no screen stacking when making calls) - _navigateToIncomingCallPage(context); - } - } - -<<<<<<< HEAD - 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()); - } - } - } - -<<<<<<< HEAD void _handleCallState(String state) { final context = navigatorKey.currentContext; if (context == null) { @@ -387,37 +145,6 @@ class CallService { Navigator.pop(context); } Navigator.pushReplacement( -======= - void _navigateToCallPage() { - final context = navigatorKey.currentContext; - if (context == null) { - print('CallService: Cannot navigate to CallPage, context is null'); - return; - } - if (_isCallPageVisible) { - print('CallService: CallPage already visible, skipping navigation'); - return; - } - print('CallService: Navigating to CallPage'); - Navigator.push( -<<<<<<< HEAD ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= -======= - 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'); - Navigator.pop(context); - } - Navigator.pushReplacement( ->>>>>>> f491fb6 (fix: no screen stacking when making calls) ->>>>>>> 9e76148 (fix: no screen stacking when making calls) context, MaterialPageRoute( settings: const RouteSettings(name: '/call'), @@ -435,22 +162,9 @@ class CallService { } void _navigateToIncomingCallPage(BuildContext context) { -<<<<<<< HEAD -<<<<<<< HEAD final currentRoute = ModalRoute.of(context)?.settings.name; print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute'); if (_isCallPageVisible && currentRoute == '/incoming_call') { -======= - if (_isCallPageVisible) { ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= - if (_isCallPageVisible) { -======= - final currentRoute = ModalRoute.of(context)?.settings.name; - print('CallService: Navigating to IncomingCallPage. Visible: $_isCallPageVisible, Current Route: $currentRoute'); - if (_isCallPageVisible && currentRoute == '/incoming_call') { ->>>>>>> f491fb6 (fix: no screen stacking when making calls) ->>>>>>> 9e76148 (fix: no screen stacking when making calls) print('CallService: IncomingCallPage already visible, skipping navigation'); return; } @@ -475,67 +189,23 @@ class CallService { _isCallPageVisible = true; } -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) void _closeCallPage() { final context = navigatorKey.currentContext; if (context == null) { print('CallService: Cannot close page, context is null'); -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD return; } print('CallService: Attempting to close call page. Visible: $_isCallPageVisible'); if (!_isCallPageVisible) { print('CallService: CallPage not visible, skipping pop'); -======= ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= -======= ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) -======= - void _closeCallPage(BuildContext context) { -======= - return; - } ->>>>>>> 6628dd6 (feat: merge call_service with callNotifications changes) - print('CallService: Attempting to close call page. Visible: $_isCallPageVisible'); - if (!_isCallPageVisible) { - print('CallService: CallPage not visible, skipping pop'); ->>>>>>> f491fb6 (fix: no screen stacking when making calls) ->>>>>>> 9e76148 (fix: no screen stacking when making calls) return; } if (Navigator.canPop(context)) { -<<<<<<< HEAD -<<<<<<< HEAD print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); Navigator.pop(context); _isCallPageVisible = false; } else { print('CallService: Cannot pop, no routes to pop'); -======= -======= ->>>>>>> 9e76148 (fix: no screen stacking when making calls) - print('CallService: Popping call page'); - Navigator.pop(context); - _isCallPageVisible = false; - } else { - print('CallService: No page to pop'); -<<<<<<< HEAD ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= -======= - print('CallService: Popping CallPage. Current Route: ${ModalRoute.of(context)?.settings.name}'); - Navigator.pop(context); - _isCallPageVisible = false; - } else { - print('CallService: Cannot pop, no routes to pop'); ->>>>>>> f491fb6 (fix: no screen stacking when making calls) ->>>>>>> 9e76148 (fix: no screen stacking when making calls) } } @@ -547,26 +217,11 @@ class CallService { }) async { try { currentPhoneNumber = phoneNumber; -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -======= - // Use provided displayName and thumbnail if available, otherwise fetch ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) -======= ->>>>>>> 9e76148 (fix: no screen stacking when making calls) currentDisplayName = displayName ?? phoneNumber; currentThumbnail = thumbnail; if (displayName == null || thumbnail == null) { await _fetchContactInfo(phoneNumber); } -<<<<<<< HEAD -======= - currentDisplayName = displayName ?? phoneNumber; // Use provided or fetch later - currentThumbnail = thumbnail; // Use provided or fetch later ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) print('CallService: Making GSM call to $phoneNumber'); final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); print('CallService: makeGsmCall result: $result'); @@ -576,29 +231,7 @@ class CallService { ); return; } -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD _handleCallState("dialing"); -======= - return resultMap; ->>>>>>> ec1779b (callNotifications and various fix related to calls (#49)) -======= - // Fetch contact info if not provided - await _fetchContactInfo(phoneNumber); - _navigateToCallPage(context); // Navigate immediately after call initiation ->>>>>>> 3e2be8a (feat: fetch contact in makeGsmCall to show it in call) -======= - _navigateToCallPage(context); ->>>>>>> c2c646a (feat: give parameters to callPage to avoid fetching when possible) -======= - _handleCallState(context, "dialing"); ->>>>>>> 9e76148 (fix: no screen stacking when making calls) -======= - _handleCallState("dialing"); ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) } catch (e) { print("CallService: Error making call: $e"); ScaffoldMessenger.of(context).showSnackBar( @@ -618,15 +251,7 @@ class CallService { SnackBar(content: Text("Failed to end call")), ); } else { -<<<<<<< HEAD -<<<<<<< HEAD _closeCallPage(); -======= - _closeCallPage(context); ->>>>>>> 9e76148 (fix: no screen stacking when making calls) -======= - _closeCallPage(); ->>>>>>> b3c8bf7 (feat: merge call_service with callNotifications changes) } } catch (e) { print("CallService: Error hanging up call: $e"); -- 2.45.2 From f6559f8a938ee3b98048490612c2fbd4a43557c8 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Fri, 18 Apr 2025 22:14:38 +0300 Subject: [PATCH 14/21] feat: timer in call page --- dialer/lib/features/call/call_page.dart | 117 +++++++-- dialer/lib/services/call_service.dart | 300 +++++++++++++++++------- 2 files changed, 309 insertions(+), 108 deletions(-) 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 -- 2.45.2 From 5bb64d98b8edf3ed42439f6fcc0d8a5b4c509cd8 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Fri, 18 Apr 2025 23:05:55 +0300 Subject: [PATCH 15/21] feat: contact info in ongoing and incoming call --- dialer/lib/services/call_service.dart | 58 ++++++++++----------------- 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index b20b048..234d850 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -8,7 +8,7 @@ import '../services/contact_service.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; - static String? currentDisplayName; + static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; static Map? _pendingCall; @@ -19,31 +19,29 @@ class CallService { 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'); + 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: [${eventTime}ms] Invalid callAdded args: $call.arguments'); + print('CallService: Invalid callAdded args: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); - print('CallService: [${eventTime}ms] Decoded phone number: $decodedPhoneNumber'); + print('CallService: 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'); + print('CallService: Call added, number: $currentPhoneNumber, displayName: $currentDisplayName, state: $state'); _callStateController.add(state); if (state == "ringing") { _handleIncomingCall(decodedPhoneNumber); @@ -55,16 +53,13 @@ class CallService { 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'); + print('CallService: Invalid callStateChanged args: $call.arguments'); return; } - print('CallService: [${eventTime}ms] State changed to $state, wasPhoneLocked: $wasPhoneLocked, disconnectCause: $disconnectCause'); + print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); _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"); } @@ -81,13 +76,13 @@ class CallService { await _fetchContactInfo(currentPhoneNumber!); } } else { - print('CallService: [${eventTime}ms] Skipping fetch, active call: $_activeCallNumber, current: $currentPhoneNumber, displayName: $currentDisplayName'); + 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: [${eventTime}ms] Invalid ringing callId: $call.arguments'); + print('CallService: Invalid ringing callId: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); @@ -103,12 +98,8 @@ class CallService { 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'); + print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); _closeCallPage(); - print('CallService: [${DateTime.now().millisecondsSinceEpoch}ms] closeCallPage completed'); if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); } @@ -121,7 +112,7 @@ class CallService { 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'); + print('CallService: Invalid incomingCallFromNotification args: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber); @@ -131,12 +122,12 @@ class CallService { await _fetchContactInfo(decodedPhoneNumber); } } - print('CallService: [${eventTime}ms] Incoming call from notification: $decodedPhoneNumber, displayName: $currentDisplayName, wasPhoneLocked: $wasPhoneLocked'); + 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: [${eventTime}ms] Audio state changed, route: $route'); + print('CallService: Audio state changed, route: $route'); break; } }); @@ -320,18 +311,18 @@ class CallService { 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: Closing call page, _isCallPageVisible: $_isCallPageVisible'); + if (Navigator.canPop(context)) { print('CallService: Popping call page'); Navigator.pop(context); + _isCallPageVisible = false; } else { - print('CallService: No call page to pop, _isCallPageVisible: $_isCallPageVisible, Current Route: $currentRoute'); + print('CallService: No page to pop'); } - _isCallPageVisible = false; _activeCallNumber = null; } + Future> makeGsmCall( BuildContext context, { required String phoneNumber, @@ -368,19 +359,13 @@ class CallService { } } - 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'); 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"]}'); + if (resultMap["status"] != "ended") { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Failed to end call")), ); @@ -391,7 +376,6 @@ class CallService { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error hanging up call: $e")), ); - _closeCallPage(); return {"status": "error", "message": e.toString()}; } } -- 2.45.2 From 7339d5982ef2b39a9b17cdcc3b7529ec140fd5cc Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Sun, 20 Apr 2025 00:53:28 +0300 Subject: [PATCH 16/21] fix: timer in call page after incoming call --- .../icing/dialer/activities/MainActivity.kt | 12 +++++++++ dialer/lib/features/call/call_page.dart | 27 ++++++++++++++----- dialer/lib/services/call_service.dart | 16 ++++++++--- 3 files changed, 45 insertions(+), 10 deletions(-) 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 b6dbce5..dbbea12 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -37,6 +37,7 @@ class _CallPageState extends State { @override void initState() { super.initState(); + _checkInitialCallState(); _listenToCallState(); } @@ -47,10 +48,24 @@ class _CallPageState extends State { 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) { - final eventTime = DateTime.now().millisecondsSinceEpoch; - print('CallPage: [${eventTime}ms] Call state changed to $state'); + print('CallPage: Call state changed to $state'); if (mounted) { setState(() { if (state == "active") { @@ -76,7 +91,6 @@ class _CallPageState extends State { 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'); }); } }); @@ -114,12 +128,11 @@ class _CallPageState extends State { void _hangUp() async { try { - final hangUpStart = DateTime.now().millisecondsSinceEpoch; - print('CallPage: [${hangUpStart}ms] Initiating hangUp'); + print('CallPage: Initiating hangUp'); final result = await _callService.hangUpCall(context); - print('CallPage: [${DateTime.now().millisecondsSinceEpoch}ms] Hang up result: $result'); + print('CallPage: Hang up result: $result'); } catch (e) { - print('CallPage: [${DateTime.now().millisecondsSinceEpoch}ms] Error hanging up: $e'); + print('CallPage: Error hanging up: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error hanging up: $e")), diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 234d850..821608f 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -8,7 +8,7 @@ import '../services/contact_service.dart'; class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; - static String? currentDisplayName; + static String? currentDisplayName; static Uint8List? currentThumbnail; static bool _isCallPageVisible = false; static Map? _pendingCall; @@ -133,6 +133,17 @@ class CallService { }); } + 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(); } @@ -322,7 +333,6 @@ class CallService { _activeCallNumber = null; } - Future> makeGsmCall( BuildContext context, { required String phoneNumber, @@ -359,7 +369,7 @@ class CallService { } } - Future> hangUpCall(BuildContext context) async { + Future> hangUpCall(BuildContext context) async { try { print('CallService: Hanging up call'); final result = await _channel.invokeMethod('hangUpCall'); -- 2.45.2 From 6c5ce1beab9e097d4d0cbf0ef932cdb8310df69f Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Sun, 27 Apr 2025 17:27:27 +0300 Subject: [PATCH 17/21] feat: can mute mic in call --- .../icing/dialer/activities/MainActivity.kt | 13 ++++++++ .../icing/dialer/services/MyInCallService.kt | 28 +++++++++++++++-- dialer/lib/features/call/call_page.dart | 31 ++++++++++++++++--- dialer/lib/services/call_service.dart | 21 +++++++++++++ 4 files changed, 86 insertions(+), 7 deletions(-) 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 eb362de..0c492b5 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 @@ -165,6 +165,19 @@ class MainActivity : FlutterActivity() { Log.d(TAG, "getCallState called, returning: $stateStr") result.success(stateStr) } + "muteCall" -> { + val mute = call.argument("mute") ?: false + val success = MyInCallService.currentCall?.let { + MyInCallService.toggleMute(mute) + } ?: false + if (success) { + Log.d(TAG, "Mute call set to $mute") + result.success(mapOf("status" to "success")) + } else { + Log.w(TAG, "No active call or failed to mute") + result.error("MUTE_FAILED", "No active call or failed to mute", null) + } + } else -> result.notImplemented() } } diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt index b5a4c8a..d79abd1 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt @@ -6,6 +6,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.media.AudioManager import android.os.Build import android.telecom.Call import android.telecom.InCallService @@ -22,6 +23,24 @@ class MyInCallService : InCallService() { private const val NOTIFICATION_CHANNEL_ID = "incoming_call_channel" private const val NOTIFICATION_ID = 1 var wasPhoneLocked: Boolean = false + private var instance: MyInCallService? = null + + fun toggleMute(mute: Boolean): Boolean { + return instance?.let { service -> + try { + val audioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager + audioManager.isMicrophoneMute = mute + Log.d(TAG, "Set microphone mute state to $mute") + channel?.invokeMethod("audioStateChanged", mapOf( + "muted" to mute + )) + true + } catch (e: Exception) { + Log.e(TAG, "Failed to set mute state: $e") + false + } + } ?: false + } } private val callCallback = object : Call.Callback() { @@ -60,6 +79,7 @@ class MyInCallService : InCallService() { override fun onCallAdded(call: Call) { super.onCallAdded(call) + instance = this currentCall = call val stateStr = when (call.state) { Call.STATE_DIALING -> "dialing" @@ -90,13 +110,17 @@ class MyInCallService : InCallService() { "wasPhoneLocked" to wasPhoneLocked )) currentCall = null + instance = null cancelNotification() } override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) { super.onCallAudioStateChanged(state) - Log.d(TAG, "Audio state changed: route=${state.route}") - channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route)) + Log.d(TAG, "Audio state changed: route=${state.route}, muted=${state.isMuted}") + channel?.invokeMethod("audioStateChanged", mapOf( + "route" to state.route, + "muted" to state.isMuted + )) } private fun showIncomingCallScreen(phoneNumber: String) { diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index dbbea12..ab42055 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -102,10 +102,31 @@ class _CallPageState extends State { }); } - void _toggleMute() { - setState(() { - isMuted = !isMuted; - }); + void _toggleMute() async { + try { + print('CallPage: Toggling mute, current state: $isMuted'); + final result = await _callService.muteCall(context, mute: !isMuted); + print('CallPage: Mute call result: $result'); + if (mounted && result['status'] == 'success') { + setState(() { + isMuted = !isMuted; + }); + } else { + print('CallPage: Failed to toggle mute: ${result['message']}'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to toggle mute: ${result['message']}')), + ); + } + } + } catch (e) { + print('CallPage: Error toggling mute: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error toggling mute: $e')), + ); + } + } } void _toggleSpeaker() { @@ -298,7 +319,7 @@ class _CallPageState extends State { onPressed: _toggleMute, icon: Icon( isMuted ? Icons.mic_off : Icons.mic, - color: Colors.white, + color: isMuted ? Colors.amber : Colors.white, size: 32, ), ), diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index 821608f..33e9310 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -144,6 +144,27 @@ class CallService { } } + 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()}; + } + } + void dispose() { _callStateController.close(); } -- 2.45.2 From 936250829be1aa5d6d84ee6836fddbb2e5fd24af Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Sun, 27 Apr 2025 17:43:52 +0300 Subject: [PATCH 18/21] feat: speaker option in call --- .../icing/dialer/activities/MainActivity.kt | 13 +++++ .../icing/dialer/services/MyInCallService.kt | 25 +++++++++- dialer/lib/features/call/call_page.dart | 49 ++++++++++++++----- dialer/lib/services/call_service.dart | 15 ++++++ 4 files changed, 88 insertions(+), 14 deletions(-) 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 0c492b5..ee194a2 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 @@ -178,6 +178,19 @@ class MainActivity : FlutterActivity() { result.error("MUTE_FAILED", "No active call or failed to mute", null) } } + "speakerCall" -> { + val speaker = call.argument("speaker") ?: false + val success = MyInCallService.currentCall?.let { + MyInCallService.toggleSpeaker(speaker) + } ?: false + if (success) { + Log.d(TAG, "Speaker call set to $speaker") + result.success(mapOf("status" to "success")) + } else { + Log.w(TAG, "No active call or failed to set speaker") + result.error("SPEAKER_FAILED", "No active call or failed to set speaker", null) + } + } else -> result.notImplemented() } } diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt index d79abd1..f9f71ad 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt @@ -10,6 +10,7 @@ import android.media.AudioManager import android.os.Build import android.telecom.Call import android.telecom.InCallService +import android.telecom.CallAudioState import android.util.Log import androidx.core.app.NotificationCompat import com.icing.dialer.activities.MainActivity @@ -32,7 +33,8 @@ class MyInCallService : InCallService() { audioManager.isMicrophoneMute = mute Log.d(TAG, "Set microphone mute state to $mute") channel?.invokeMethod("audioStateChanged", mapOf( - "muted" to mute + "muted" to mute, + "speaker" to audioManager.isSpeakerphoneOn )) true } catch (e: Exception) { @@ -41,6 +43,24 @@ class MyInCallService : InCallService() { } } ?: false } + + fun toggleSpeaker(speaker: Boolean): Boolean { + return instance?.let { service -> + try { + val audioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager + audioManager.isSpeakerphoneOn = speaker + Log.d(TAG, "Set speakerphone state to $speaker") + channel?.invokeMethod("audioStateChanged", mapOf( + "muted" to audioManager.isMicrophoneMute, + "speaker" to speaker + )) + true + } catch (e: Exception) { + Log.e(TAG, "Failed to set speaker state: $e") + false + } + } ?: false + } } private val callCallback = object : Call.Callback() { @@ -119,7 +139,8 @@ class MyInCallService : InCallService() { Log.d(TAG, "Audio state changed: route=${state.route}, muted=${state.isMuted}") channel?.invokeMethod("audioStateChanged", mapOf( "route" to state.route, - "muted" to state.isMuted + "muted" to state.isMuted, + "speaker" to (state.route == CallAudioState.ROUTE_SPEAKER) )) } diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index ab42055..54333c7 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -25,7 +25,7 @@ class _CallPageState extends State { final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); bool isMuted = false; - bool isSpeakerOn = false; + bool isSpeaker = false; bool isKeypadVisible = false; bool icingProtocolOk = true; String _typedDigits = ""; @@ -115,7 +115,8 @@ class _CallPageState extends State { print('CallPage: Failed to toggle mute: ${result['message']}'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to toggle mute: ${result['message']}')), + SnackBar( + content: Text('Failed to toggle mute: ${result['message']}')), ); } } @@ -129,10 +130,30 @@ class _CallPageState extends State { } } - void _toggleSpeaker() { - setState(() { - isSpeakerOn = !isSpeakerOn; - }); + Future _toggleSpeaker() async { + try { + print('CallPage: Toggling speaker, current state: $isSpeaker'); + final result = + await _callService.speakerCall(context, speaker: !isSpeaker); + print('CallPage: Speaker call result: $result'); + if (result['status'] == 'success') { + setState(() { + isSpeaker = !isSpeaker; + }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to toggle speaker: ${result['message']}')), + ); + } + } catch (e) { + print('CallPage: Error toggling speaker: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error toggling speaker: $e')), + ); + } + } } void _toggleKeypad() { @@ -168,7 +189,8 @@ 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"}'); + print( + 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); return Scaffold( body: Container( color: Colors.black, @@ -256,7 +278,8 @@ 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), ), ], ), @@ -319,7 +342,9 @@ class _CallPageState extends State { onPressed: _toggleMute, icon: Icon( isMuted ? Icons.mic_off : Icons.mic, - color: isMuted ? Colors.amber : Colors.white, + color: isMuted + ? Colors.amber + : Colors.white, size: 32, ), ), @@ -354,10 +379,10 @@ class _CallPageState extends State { IconButton( onPressed: _toggleSpeaker, icon: Icon( - isSpeakerOn + isSpeaker ? Icons.volume_up : Icons.volume_off, - color: isSpeakerOn + color: isSpeaker ? Colors.amber : Colors.white, size: 32, @@ -446,4 +471,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 33e9310..ec52df9 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -165,6 +165,21 @@ class CallService { } } + 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(); } -- 2.45.2 From 81187f438442ceecfa757195a1e30c5d2ed529f0 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Mon, 28 Apr 2025 18:51:35 +0300 Subject: [PATCH 19/21] fix: mute and speaker options working in callPage --- .../icing/dialer/services/MyInCallService.kt | 33 ++++++------- dialer/lib/features/call/call_page.dart | 47 ++++++++++++------- dialer/lib/services/call_service.dart | 16 ++++++- 3 files changed, 63 insertions(+), 33 deletions(-) diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt index f9f71ad..5469c6d 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt @@ -29,13 +29,8 @@ class MyInCallService : InCallService() { fun toggleMute(mute: Boolean): Boolean { return instance?.let { service -> try { - val audioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager - audioManager.isMicrophoneMute = mute - Log.d(TAG, "Set microphone mute state to $mute") - channel?.invokeMethod("audioStateChanged", mapOf( - "muted" to mute, - "speaker" to audioManager.isSpeakerphoneOn - )) + service.setMuted(mute) + Log.d(TAG, "Requested to set call mute state to $mute") true } catch (e: Exception) { Log.e(TAG, "Failed to set mute state: $e") @@ -47,16 +42,12 @@ class MyInCallService : InCallService() { fun toggleSpeaker(speaker: Boolean): Boolean { return instance?.let { service -> try { - val audioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager - audioManager.isSpeakerphoneOn = speaker - Log.d(TAG, "Set speakerphone state to $speaker") - channel?.invokeMethod("audioStateChanged", mapOf( - "muted" to audioManager.isMicrophoneMute, - "speaker" to speaker - )) + val route = if (speaker) CallAudioState.ROUTE_SPEAKER else CallAudioState.ROUTE_EARPIECE + service.setAudioRoute(route) + Log.d(TAG, "Requested to set audio route to $route") true } catch (e: Exception) { - Log.e(TAG, "Failed to set speaker state: $e") + Log.e(TAG, "Failed to set audio route: $e") false } } ?: false @@ -119,6 +110,16 @@ class MyInCallService : InCallService() { showIncomingCallScreen(call.details.handle.toString().replace("tel:", "")) } call.registerCallback(callCallback) + if (callAudioState != null) { + val audioState = callAudioState + channel?.invokeMethod("audioStateChanged", mapOf( + "route" to audioState.route, + "muted" to audioState.isMuted, + "speaker" to (audioState.route == CallAudioState.ROUTE_SPEAKER) + )) + } else { + Log.w("MyInCallService", "callAudioState is null in onCallAdded") + } } override fun onCallRemoved(call: Call) { @@ -134,7 +135,7 @@ class MyInCallService : InCallService() { cancelNotification() } - override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) { + override fun onCallAudioStateChanged(state: CallAudioState) { super.onCallAudioStateChanged(state) Log.d(TAG, "Audio state changed: route=${state.route}, muted=${state.isMuted}") channel?.invokeMethod("audioStateChanged", mapOf( diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index 54333c7..0b3bdc3 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -33,21 +33,35 @@ class _CallPageState extends State { int _callSeconds = 0; String _callStatus = "Calling..."; StreamSubscription? _callStateSubscription; + StreamSubscription>? _audioStateSubscription; @override void initState() { super.initState(); _checkInitialCallState(); _listenToCallState(); + _listenToAudioState(); + _setInitialAudioState(); } @override void dispose() { _callTimer?.cancel(); _callStateSubscription?.cancel(); + _audioStateSubscription?.cancel(); super.dispose(); } + void _setInitialAudioState() { + final initialAudioState = _callService.currentAudioState; + if (initialAudioState != null) { + setState(() { + isMuted = initialAudioState['muted'] ?? false; + isSpeaker = initialAudioState['speaker'] ?? false; + }); + } + } + void _checkInitialCallState() async { try { final state = await _callService.getCallState(); @@ -82,6 +96,17 @@ class _CallPageState extends State { }); } + void _listenToAudioState() { + _audioStateSubscription = _callService.audioStateStream.listen((state) { + if (mounted) { + setState(() { + isMuted = state['muted'] ?? isMuted; + isSpeaker = state['speaker'] ?? isSpeaker; + }); + } + }); + } + void _startCallTimer() { _callTimer?.cancel(); _callTimer = Timer.periodic(const Duration(seconds: 1), (timer) { @@ -107,18 +132,12 @@ class _CallPageState extends State { print('CallPage: Toggling mute, current state: $isMuted'); final result = await _callService.muteCall(context, mute: !isMuted); print('CallPage: Mute call result: $result'); - if (mounted && result['status'] == 'success') { - setState(() { - isMuted = !isMuted; - }); - } else { + if (mounted && result['status'] != 'success') { print('CallPage: Failed to toggle mute: ${result['message']}'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to toggle mute: ${result['message']}')), - ); - } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to toggle mute: ${result['message']}')), + ); } } catch (e) { print('CallPage: Error toggling mute: $e'); @@ -136,11 +155,7 @@ class _CallPageState extends State { final result = await _callService.speakerCall(context, speaker: !isSpeaker); print('CallPage: Speaker call result: $result'); - if (result['status'] == 'success') { - setState(() { - isSpeaker = !isSpeaker; - }); - } else { + if (result['status'] != 'success') { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to toggle speaker: ${result['message']}')), diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index ec52df9..d14073e 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -17,10 +17,14 @@ class CallService { static bool _isNavigating = false; final ContactService _contactService = ContactService(); final _callStateController = StreamController.broadcast(); + final _audioStateController = StreamController>.broadcast(); + Map? _currentAudioState; static final GlobalKey navigatorKey = GlobalKey(); Stream get callStateStream => _callStateController.stream; + Stream> get audioStateStream => _audioStateController.stream; + Map? get currentAudioState => _currentAudioState; CallService() { _channel.setMethodCallHandler((call) async { @@ -127,7 +131,16 @@ class CallService { break; case "audioStateChanged": final route = call.arguments["route"] as int?; - print('CallService: Audio state changed, route: $route'); + 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; } }); @@ -182,6 +195,7 @@ class CallService { void dispose() { _callStateController.close(); + _audioStateController.close(); } Future _fetchContactInfo(String phoneNumber) async { -- 2.45.2 From ce7a5b10b09769dd9d874368d6088b5b8071a286 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Tue, 29 Apr 2025 23:26:28 +0300 Subject: [PATCH 20/21] feat: add contact during call if contact is unknown --- dialer/lib/features/call/call_page.dart | 55 +++++++++++++++++-------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index 0b3bdc3..42f1bb3 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:dialer/services/call_service.dart'; import 'package:dialer/services/obfuscate_service.dart'; import 'package:dialer/widgets/username_color_generator.dart'; @@ -35,6 +36,8 @@ class _CallPageState extends State { StreamSubscription? _callStateSubscription; StreamSubscription>? _audioStateSubscription; + bool get isNumberUnknown => widget.displayName == widget.phoneNumber; + @override void initState() { super.initState(); @@ -198,6 +201,23 @@ class _CallPageState extends State { } } + void _addContact() async { + if (await FlutterContacts.requestPermission()) { + final newContact = Contact()..phones = [Phone(widget.phoneNumber)]; + final updatedContact = + await FlutterContacts.openExternalInsert(newContact); + if (updatedContact != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Contact added successfully!')), + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Permission denied for contacts')), + ); + } + } + @override Widget build(BuildContext context) { final double avatarRadius = isKeypadVisible ? 45.0 : 45.0; @@ -416,24 +436,25 @@ class _CallPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.person_add, - color: Colors.white, - size: 32, + if (isNumberUnknown) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _addContact, + 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( mainAxisSize: MainAxisSize.min, children: [ -- 2.45.2 From a80b6da4264e09af5befd1b928eb11a98374ffce Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Wed, 30 Apr 2025 00:16:31 +0300 Subject: [PATCH 21/21] feat: can't use return button and exit callpage during call --- dialer/lib/features/call/call_page.dart | 518 ++++++++++++------------ 1 file changed, 266 insertions(+), 252 deletions(-) diff --git a/dialer/lib/features/call/call_page.dart b/dialer/lib/features/call/call_page.dart index 42f1bb3..0d7cf48 100644 --- a/dialer/lib/features/call/call_page.dart +++ b/dialer/lib/features/call/call_page.dart @@ -226,285 +226,299 @@ class _CallPageState extends State { print( 'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}'); - return Scaffold( - body: Container( - color: Colors.black, - child: SafeArea( - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 35), - ObfuscatedAvatar( - imageBytes: widget.thumbnail, - radius: avatarRadius, - backgroundColor: - generateColorFromName(widget.displayName), - fallbackInitial: widget.displayName, - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.center, + return PopScope( + canPop: + _callStatus == "Call Ended", // Allow navigation only if call ended + child: Scaffold( + body: Container( + color: Colors.black, + child: SafeArea( + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Icon( - icingProtocolOk ? Icons.lock : Icons.lock_open, - color: icingProtocolOk ? Colors.green : Colors.red, - size: 16, + const SizedBox(height: 35), + ObfuscatedAvatar( + imageBytes: widget.thumbnail, + radius: avatarRadius, + backgroundColor: + generateColorFromName(widget.displayName), + fallbackInitial: widget.displayName, ), - const SizedBox(width: 4), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icingProtocolOk ? Icons.lock : Icons.lock_open, + color: + icingProtocolOk ? Colors.green : Colors.red, + size: 16, + ), + const SizedBox(width: 4), + Text( + 'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}', + style: TextStyle( + color: + icingProtocolOk ? Colors.green : Colors.red, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 4), Text( - 'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}', + _obfuscateService.obfuscateData(widget.displayName), style: TextStyle( - color: icingProtocolOk ? Colors.green : Colors.red, - fontSize: 12, + fontSize: nameFontSize, + color: Colors.white, fontWeight: FontWeight.bold, ), ), + Text( + widget.phoneNumber, + style: TextStyle( + fontSize: statusFontSize, color: Colors.white70), + ), + Text( + _callStatus, + style: TextStyle( + fontSize: statusFontSize, color: Colors.white70), + ), ], ), - const SizedBox(height: 4), - Text( - _obfuscateService.obfuscateData(widget.displayName), - style: TextStyle( - fontSize: nameFontSize, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - Text( - widget.phoneNumber, - style: TextStyle( - fontSize: statusFontSize, color: Colors.white70), - ), - Text( - _callStatus, - style: TextStyle( - fontSize: statusFontSize, color: Colors.white70), - ), - ], - ), - ), - Expanded( - child: Column( - children: [ - if (isKeypadVisible) ...[ - const Spacer(flex: 2), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - _typedDigits, - maxLines: 1, - textAlign: TextAlign.right, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 24, - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - padding: EdgeInsets.zero, - onPressed: _toggleKeypad, - icon: - const Icon(Icons.close, color: Colors.white), - ), - ], - ), - ), - Container( - height: MediaQuery.of(context).size.height * 0.35, - margin: const EdgeInsets.symmetric(horizontal: 20), - child: GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 3, - childAspectRatio: 1.3, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - children: List.generate(12, (index) { - String label; - if (index < 9) { - label = '${index + 1}'; - } else if (index == 9) { - label = '*'; - } else if (index == 10) { - label = '0'; - } else { - label = '#'; - } - return GestureDetector( - onTap: () => _addDigit(label), - child: Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.transparent, - ), - child: Center( + ), + Expanded( + child: Column( + children: [ + if (isKeypadVisible) ...[ + const Spacer(flex: 2), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( child: Text( - label, + _typedDigits, + maxLines: 1, + textAlign: TextAlign.right, + overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 32, color: Colors.white), + fontSize: 24, + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), - ), - ); - }), - ), - ), - const Spacer(flex: 1), - ] else ...[ - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _toggleMute, - icon: Icon( - isMuted ? Icons.mic_off : Icons.mic, - color: isMuted - ? Colors.amber - : Colors.white, - size: 32, - ), - ), - Text( - isMuted ? 'Unmute' : 'Mute', - style: const TextStyle( - color: Colors.white, fontSize: 14), - ), - ], - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _toggleKeypad, - icon: const Icon( - Icons.dialpad, - color: Colors.white, - size: 32, - ), - ), - const Text( - 'Keypad', - style: TextStyle( - color: Colors.white, fontSize: 14), - ), - ], - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _toggleSpeaker, - icon: Icon( - isSpeaker - ? Icons.volume_up - : Icons.volume_off, - color: isSpeaker - ? Colors.amber - : Colors.white, - size: 32, - ), - ), - const Text( - 'Speaker', - style: TextStyle( - color: Colors.white, fontSize: 14), - ), - ], + IconButton( + padding: EdgeInsets.zero, + onPressed: _toggleKeypad, + icon: const Icon(Icons.close, + color: Colors.white), ), ], ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + ), + Container( + height: MediaQuery.of(context).size.height * 0.35, + margin: const EdgeInsets.symmetric(horizontal: 20), + child: GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + childAspectRatio: 1.3, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + children: List.generate(12, (index) { + String label; + if (index < 9) { + label = '${index + 1}'; + } else if (index == 9) { + label = '*'; + } else if (index == 10) { + label = '0'; + } else { + label = '#'; + } + return GestureDetector( + onTap: () => _addDigit(label), + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + ), + child: Center( + child: Text( + label, + style: const TextStyle( + fontSize: 32, color: Colors.white), + ), + ), + ), + ); + }), + ), + ), + const Spacer(flex: 1), + ] else ...[ + const Spacer(), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - if (isNumberUnknown) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: _addContact, - icon: const Icon( - Icons.person_add, - color: Colors.white, - size: 32, + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _toggleMute, + icon: Icon( + isMuted ? Icons.mic_off : Icons.mic, + color: isMuted + ? Colors.amber + : Colors.white, + size: 32, + ), ), - ), - const Text( - 'Add Contact', - style: TextStyle( - color: Colors.white, fontSize: 14), - ), - ], - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.sim_card, - color: Colors.white, - size: 32, - ), + Text( + isMuted ? 'Unmute' : 'Mute', + style: const TextStyle( + color: Colors.white, + fontSize: 14), + ), + ], ), - const Text( - 'Change SIM', - style: TextStyle( - color: Colors.white, fontSize: 14), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _toggleKeypad, + icon: const Icon( + Icons.dialpad, + color: Colors.white, + size: 32, + ), + ), + const Text( + 'Keypad', + style: TextStyle( + color: Colors.white, + fontSize: 14), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _toggleSpeaker, + icon: Icon( + isSpeaker + ? Icons.volume_up + : Icons.volume_off, + color: isSpeaker + ? Colors.amber + : Colors.white, + size: 32, + ), + ), + const Text( + 'Speaker', + style: TextStyle( + color: Colors.white, + fontSize: 14), + ), + ], + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + if (isNumberUnknown) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: _addContact, + icon: const Icon( + Icons.person_add, + color: Colors.white, + size: 32, + ), + ), + const Text( + 'Add Contact', + style: TextStyle( + color: Colors.white, + fontSize: 14), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.sim_card, + color: Colors.white, + size: 32, + ), + ), + const Text( + 'Change SIM', + style: TextStyle( + color: Colors.white, + fontSize: 14), + ), + ], ), ], ), ], ), - ], - ), - ), - const Spacer(flex: 3), - ], - ], - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: GestureDetector( - onTap: _hangUp, - child: Container( - padding: const EdgeInsets.all(12), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.call_end, - color: Colors.white, - size: 32, + ), + const Spacer(flex: 3), + ], + ], ), ), - ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: GestureDetector( + onTap: _hangUp, + child: Container( + padding: const EdgeInsets.all(12), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.call_end, + color: Colors.white, + size: 32, + ), + ), + ), + ), + ], ), - ], + ), ), - ), - ), - ); + )); } } -- 2.45.2