diff --git a/dialer/android/app/src/main/java/com/example/dialer/MainActivity.java b/dialer/android/app/src/main/java/com/example/dialer/MainActivity.java new file mode 100644 index 0000000..1fbc925 --- /dev/null +++ b/dialer/android/app/src/main/java/com/example/dialer/MainActivity.java @@ -0,0 +1,49 @@ +package com.example.dialer; + +import android.os.Bundle; +import android.net.Uri; +import android.content.Context; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.MethodCall; +import android.telecom.TelecomManager; +import android.telecom.PhoneAccountHandle; +import java.util.List; +import java.util.Collections; + +public class MainActivity extends FlutterActivity { + private static final String CHANNEL = "call_service"; + + @Override + public void configureFlutterEngine(FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL) + .setMethodCallHandler( + new MethodCallHandler() { + @Override + public void onMethodCall(MethodCall call, Result result) { + if (call.method.equals("makeGsmCall")) { + String phoneNumber = call.argument("phoneNumber"); + int simSlot = call.argument("simSlot"); + TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); + List accounts = telecomManager.getCallCapablePhoneAccounts(); + PhoneAccountHandle selectedAccount = accounts.get(simSlot < accounts.size() ? simSlot : 0); + Bundle extras = new Bundle(); + extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, selectedAccount); + Uri uri = Uri.fromParts("tel", phoneNumber, null); + telecomManager.placeCall(uri, extras); + result.success(Collections.singletonMap("status", "calling")); + } else if (call.method.equals("hangUpCall")) { + // TODO: implement hangUpCall if needed + result.success(Collections.singletonMap("status", "ended")); + } else { + result.notImplemented(); + } + } + } + ); + } +} \ No newline at end of file diff --git a/dialer/android/gradle.properties b/dialer/android/gradle.properties index 78b3d37..513ff0d 100644 --- a/dialer/android/gradle.properties +++ b/dialer/android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryErro android.useAndroidX=true android.enableJetifier=true dev.steenbakker.mobile_scanner.useUnbundled=true -org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64 +# org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64 diff --git a/dialer/lib/domain/services/call_service.dart b/dialer/lib/domain/services/call_service.dart index 1c6d242..346f4dc 100644 --- a/dialer/lib/domain/services/call_service.dart +++ b/dialer/lib/domain/services/call_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../presentation/features/call/call_page.dart'; import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page import 'contact_service.dart'; @@ -17,18 +18,22 @@ class CallService { static bool _isNavigating = false; final ContactService _contactService = ContactService(); final _callStateController = StreamController.broadcast(); - final _audioStateController = StreamController>.broadcast(); + final _audioStateController = + StreamController>.broadcast(); Map? _currentAudioState; - static final GlobalKey navigatorKey = GlobalKey(); - + static final GlobalKey navigatorKey = + GlobalKey(); + Stream get callStateStream => _callStateController.stream; - Stream> get audioStateStream => _audioStateController.stream; + Stream> get audioStateStream => + _audioStateController.stream; Map? get currentAudioState => _currentAudioState; CallService() { _channel.setMethodCallHandler((call) async { - print('CallService: Handling method call: ${call.method}, with args: ${call.arguments}'); + print( + 'CallService: Handling method call: ${call.method}, with args: ${call.arguments}'); switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String?; @@ -37,15 +42,18 @@ class CallService { print('CallService: Invalid callAdded args: $call.arguments'); return; } - final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); + final decodedPhoneNumber = + Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); print('CallService: Decoded phone number: $decodedPhoneNumber'); if (_activeCallNumber != decodedPhoneNumber) { currentPhoneNumber = decodedPhoneNumber; - if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { + if (currentDisplayName == null || + currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(decodedPhoneNumber); } } - print('CallService: 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); @@ -57,10 +65,12 @@ class CallService { final state = call.arguments["state"] as String?; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; if (state == null) { - print('CallService: Invalid callStateChanged args: $call.arguments'); + print( + 'CallService: Invalid callStateChanged args: $call.arguments'); return; } - print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); + print( + 'CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); _callStateController.add(state); if (state == "disconnected" || state == "disconnecting") { _closeCallPage(); @@ -70,17 +80,24 @@ class CallService { _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) { + 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) { + } else if (currentPhoneNumber != null && + _activeCallNumber != currentPhoneNumber) { + if (currentDisplayName == null || + currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(currentPhoneNumber!); } } else { - print('CallService: Skipping fetch, active call: $_activeCallNumber, current: $currentPhoneNumber, displayName: $currentDisplayName'); + print( + 'CallService: Skipping fetch, active call: $_activeCallNumber, current: $currentPhoneNumber, displayName: $currentDisplayName'); } _navigateToCallPage(); } else if (state == "ringing") { @@ -89,10 +106,12 @@ class CallService { print('CallService: Invalid ringing callId: $call.arguments'); return; } - final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); + final decodedPhoneNumber = + Uri.decodeComponent(phoneNumber.replaceFirst('tel:', '')); if (_activeCallNumber != decodedPhoneNumber) { currentPhoneNumber = decodedPhoneNumber; - if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { + if (currentDisplayName == null || + currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(decodedPhoneNumber); } } @@ -102,7 +121,8 @@ class CallService { case "callEnded": case "callRemoved": wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; - print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); + print( + 'CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); _closeCallPage(); if (wasPhoneLocked) { await _channel.invokeMethod("callEndedFromFlutter"); @@ -116,24 +136,28 @@ class CallService { final phoneNumber = call.arguments["phoneNumber"] as String?; wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; if (phoneNumber == null) { - print('CallService: Invalid incomingCallFromNotification args: $call.arguments'); + print( + 'CallService: Invalid incomingCallFromNotification args: $call.arguments'); return; } final decodedPhoneNumber = Uri.decodeComponent(phoneNumber); if (_activeCallNumber != decodedPhoneNumber) { currentPhoneNumber = decodedPhoneNumber; - if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) { + if (currentDisplayName == null || + currentDisplayName == currentPhoneNumber) { await _fetchContactInfo(decodedPhoneNumber); } } - print('CallService: 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?; 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'); + print( + 'CallService: Audio state changed, route: $route, muted: $muted, speaker: $speaker'); final audioState = { "route": route, "muted": muted, @@ -157,7 +181,8 @@ class CallService { } } - Future> muteCall(BuildContext context, {required bool mute}) async { + Future> muteCall(BuildContext context, + {required bool mute}) async { try { print('CallService: Toggling mute to $mute'); final result = await _channel.invokeMethod('muteCall', {'mute': mute}); @@ -178,10 +203,12 @@ class CallService { } } - Future> speakerCall(BuildContext context, {required bool speaker}) async { + Future> speakerCall(BuildContext context, + {required bool speaker}) async { try { print('CallService: Toggling speaker to $speaker'); - final result = await _channel.invokeMethod('speakerCall', {'speaker': speaker}); + final result = + await _channel.invokeMethod('speakerCall', {'speaker': speaker}); print('CallService: speakerCall result: $result'); return Map.from(result); } catch (e) { @@ -208,18 +235,21 @@ class CallService { for (var contact in contacts) { for (var phone in contact.phones) { final normalizedContactNumber = _normalizePhoneNumber(phone.number); - print('CallService: Comparing $normalizedPhoneNumber with contact number $normalizedContactNumber'); + 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"}'); + 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'); + print( + 'CallService: No contact match, using phoneNumber as displayName: $currentDisplayName'); } catch (e) { print('CallService: Error fetching contact info: $e'); currentDisplayName = phoneNumber; @@ -228,19 +258,23 @@ class CallService { } String _normalizePhoneNumber(String number) { - return number.replaceAll(RegExp(r'[\s\-\(\)]'), '').replaceFirst(RegExp(r'^\+'), ''); + 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'); + 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'); + print( + 'CallService: Context is null, queuing incoming call: $phoneNumber'); _pendingCall = {"phoneNumber": phoneNumber}; Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); } else { @@ -256,7 +290,8 @@ class CallService { final phoneNumber = _pendingCall!["phoneNumber"]; if (_activeCallNumber == phoneNumber && _isCallPageVisible) { - print('CallService: Pending call for $phoneNumber already active, clearing'); + print( + 'CallService: Pending call for $phoneNumber already active, clearing'); _pendingCall = null; return; } @@ -289,19 +324,27 @@ class CallService { return; } final currentRoute = ModalRoute.of(context)?.settings.name ?? 'unknown'; - print('CallService: Navigating to CallPage, Visible: $_isCallPageVisible, Current Route: $currentRoute, Active Call: $_activeCallNumber, DisplayName: $currentDisplayName'); - if (_isCallPageVisible && currentRoute == '/call' && _activeCallNumber == currentPhoneNumber) { - print('CallService: CallPage already visible for $_activeCallNumber, skipping navigation'); + print( + 'CallService: Navigating to CallPage, Visible: $_isCallPageVisible, Current Route: $currentRoute, Active Call: $_activeCallNumber, DisplayName: $currentDisplayName'); + if (_isCallPageVisible && + currentRoute == '/call' && + _activeCallNumber == currentPhoneNumber) { + print( + 'CallService: CallPage already visible for $_activeCallNumber, skipping navigation'); _isNavigating = false; return; } - if (_isCallPageVisible && currentRoute == '/incoming_call' && _activeCallNumber == currentPhoneNumber) { - print('CallService: Popping IncomingCallPage before navigating to CallPage'); + 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'); + print( + 'CallService: Cannot navigate to CallPage, currentPhoneNumber is null'); _isNavigating = false; return; } @@ -332,9 +375,13 @@ class CallService { _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'); + 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; } @@ -344,7 +391,8 @@ class CallService { return; } if (currentPhoneNumber == null) { - print('CallService: Cannot navigate to IncomingCallPage, currentPhoneNumber is null'); + print( + 'CallService: Cannot navigate to IncomingCallPage, currentPhoneNumber is null'); _isNavigating = false; return; } @@ -361,7 +409,8 @@ class CallService { ).then((_) { _isCallPageVisible = false; _isNavigating = false; - print('CallService: IncomingCallPage popped, _isCallPageVisible set to false'); + print( + 'CallService: IncomingCallPage popped, _isCallPageVisible set to false'); }); _isCallPageVisible = true; } @@ -372,7 +421,8 @@ class CallService { print('CallService: Cannot close page, context is null'); return; } - print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); + print( + 'CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible'); if (Navigator.canPop(context)) { print('CallService: Popping call page'); Navigator.pop(context); @@ -390,9 +440,15 @@ class CallService { Uint8List? thumbnail, }) async { try { + // Load default SIM slot from settings + final prefs = await SharedPreferences.getInstance(); + final simSlot = prefs.getInt('default_sim_slot') ?? 0; if (_activeCallNumber == phoneNumber && _isCallPageVisible) { print('CallService: Call already active for $phoneNumber, skipping'); - return {"status": "already_active", "message": "Call already in progress"}; + return { + "status": "already_active", + "message": "Call already in progress" + }; } currentPhoneNumber = phoneNumber; currentDisplayName = displayName ?? phoneNumber; @@ -400,8 +456,10 @@ class CallService { if (displayName == null || thumbnail == null) { await _fetchContactInfo(phoneNumber); } - print('CallService: Making GSM call to $phoneNumber, displayName: $currentDisplayName'); - final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber}); + print( + 'CallService: Making GSM call to $phoneNumber, displayName: $currentDisplayName, simSlot: $simSlot'); + final result = await _channel.invokeMethod( + 'makeGsmCall', {"phoneNumber": phoneNumber, "simSlot": simSlot}); print('CallService: makeGsmCall result: $result'); final resultMap = Map.from(result as Map); if (resultMap["status"] != "calling") { @@ -439,4 +497,4 @@ class CallService { return {"status": "error", "message": e.toString()}; } } -} \ No newline at end of file +} diff --git a/dialer/lib/presentation/features/settings/settings.dart b/dialer/lib/presentation/features/settings/settings.dart index d6f1808..9d2cfe2 100644 --- a/dialer/lib/presentation/features/settings/settings.dart +++ b/dialer/lib/presentation/features/settings/settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:dialer/presentation/features/settings/call/settings_call.dart'; import 'package:dialer/presentation/features/settings/cryptography/key_management.dart'; import 'package:dialer/presentation/features/settings/blocked/settings_blocked.dart'; +import 'package:dialer/presentation/features/settings/sim/settings_sim.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @@ -26,6 +27,12 @@ class SettingsPage extends StatelessWidget { MaterialPageRoute(builder: (context) => const BlockedNumbersPage()), ); break; + case 'Default SIM': + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsSimPage()), + ); + break; // Add more cases for other settings pages default: // Handle default or unknown settings @@ -38,7 +45,8 @@ class SettingsPage extends StatelessWidget { final settingsOptions = [ 'Calling settings', 'Key management', - 'Blocked numbers' + 'Blocked numbers', + 'Default SIM', ]; return Scaffold( diff --git a/dialer/lib/presentation/features/settings/sim/settings_sim.dart b/dialer/lib/presentation/features/settings/sim/settings_sim.dart new file mode 100644 index 0000000..c33938f --- /dev/null +++ b/dialer/lib/presentation/features/settings/sim/settings_sim.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsSimPage extends StatefulWidget { + const SettingsSimPage({super.key}); + + @override + _SettingsSimPageState createState() => _SettingsSimPageState(); +} + +class _SettingsSimPageState extends State { + int _selectedSim = 0; + + @override + void initState() { + super.initState(); + _loadDefaultSim(); + } + + void _loadDefaultSim() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _selectedSim = prefs.getInt('default_sim_slot') ?? 0; + }); + } + + void _onSimChanged(int? value) async { + if (value != null) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt('default_sim_slot', value); + setState(() { + _selectedSim = value; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text('Default SIM'), + ), + body: ListView( + children: [ + RadioListTile( + title: const Text('SIM 1', style: TextStyle(color: Colors.white)), + value: 0, + groupValue: _selectedSim, + onChanged: _onSimChanged, + activeColor: Colors.blue, + ), + RadioListTile( + title: const Text('SIM 2', style: TextStyle(color: Colors.white)), + value: 1, + groupValue: _selectedSim, + onChanged: _onSimChanged, + activeColor: Colors.blue, + ), + ], + ), + ); + } +}