From cdd3a470c07565b76bee279726327cc48e743d80 Mon Sep 17 00:00:00 2001 From: AlexisDanlos Date: Fri, 13 Jun 2025 15:22:11 +0200 Subject: [PATCH] feat: add SIM selection dialog and integrate SIM slot handling for calls --- .../icing/dialer/activities/MainActivity.kt | 6 +- .../com/icing/dialer/services/CallService.kt | 27 ++- dialer/lib/domain/services/call_service.dart | 24 +++ .../common/widgets/sim_selection_dialog.dart | 204 ++++++++++++++++++ .../presentation/features/call/call_page.dart | 102 ++++++++- 5 files changed, 347 insertions(+), 16 deletions(-) create mode 100644 dialer/lib/presentation/common/widgets/sim_selection_dialog.dart 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 f58cdab..d7b54b5 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 @@ -96,11 +96,11 @@ class MainActivity : FlutterActivity() { } } result.success(true) - } - "makeGsmCall" -> { + } "makeGsmCall" -> { val phoneNumber = call.argument("phoneNumber") + val simSlot = call.argument("simSlot") ?: 0 if (phoneNumber != null) { - val success = CallService.makeGsmCall(this, phoneNumber) + val success = CallService.makeGsmCall(this, phoneNumber, simSlot) if (success) { result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber)) } else { diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/CallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/CallService.kt index 7958799..d3762af 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/CallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/CallService.kt @@ -13,14 +13,35 @@ import android.Manifest object CallService { private val TAG = "CallService" - fun makeGsmCall(context: Context, phoneNumber: String): Boolean { + fun makeGsmCall(context: Context, phoneNumber: String, simSlot: Int = 0): Boolean { return try { val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager val uri = Uri.parse("tel:$phoneNumber") if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) { - telecomManager.placeCall(uri, Bundle()) - Log.d(TAG, "Initiated call to $phoneNumber") + // Get available phone accounts (SIM cards) + val phoneAccounts = telecomManager.callCapablePhoneAccounts + + if (phoneAccounts.isNotEmpty()) { + // Select the appropriate SIM slot + val selectedAccount = if (simSlot < phoneAccounts.size) { + phoneAccounts[simSlot] + } else { + // Fallback to first available SIM if requested slot doesn't exist + phoneAccounts[0] + } + + val extras = Bundle().apply { + putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, selectedAccount) + } + + telecomManager.placeCall(uri, extras) + Log.d(TAG, "Initiated call to $phoneNumber using SIM slot $simSlot") + } else { + // No SIM cards available, make call without specifying SIM + telecomManager.placeCall(uri, Bundle()) + Log.d(TAG, "Initiated call to $phoneNumber without SIM selection (no SIMs available)") + } true } else { Log.e(TAG, "CALL_PHONE permission not granted") diff --git a/dialer/lib/domain/services/call_service.dart b/dialer/lib/domain/services/call_service.dart index 346f4dc..f3d17b1 100644 --- a/dialer/lib/domain/services/call_service.dart +++ b/dialer/lib/domain/services/call_service.dart @@ -443,6 +443,30 @@ class CallService { // Load default SIM slot from settings final prefs = await SharedPreferences.getInstance(); final simSlot = prefs.getInt('default_sim_slot') ?? 0; + return await makeGsmCallWithSim( + context, + phoneNumber: phoneNumber, + displayName: displayName, + thumbnail: thumbnail, + simSlot: simSlot, + ); + } catch (e) { + print("CallService: Error making call: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error making call: $e")), + ); + return {"status": "error", "message": e.toString()}; + } + } + + Future> makeGsmCallWithSim( + BuildContext context, { + required String phoneNumber, + String? displayName, + Uint8List? thumbnail, + required int simSlot, + }) async { + try { if (_activeCallNumber == phoneNumber && _isCallPageVisible) { print('CallService: Call already active for $phoneNumber, skipping'); return { diff --git a/dialer/lib/presentation/common/widgets/sim_selection_dialog.dart b/dialer/lib/presentation/common/widgets/sim_selection_dialog.dart new file mode 100644 index 0000000..cd17034 --- /dev/null +++ b/dialer/lib/presentation/common/widgets/sim_selection_dialog.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:sim_data_new/sim_data.dart'; + +class SimSelectionDialog extends StatefulWidget { + final String phoneNumber; + final String displayName; + final Function(int simSlot) onSimSelected; + + const SimSelectionDialog({ + super.key, + required this.phoneNumber, + required this.displayName, + required this.onSimSelected, + }); + + @override + _SimSelectionDialogState createState() => _SimSelectionDialogState(); +} + +class _SimSelectionDialogState extends State { + SimData? _simData; + bool _isLoading = true; + String? _error; + int? _selectedSimSlot; + + @override + void initState() { + super.initState(); + _loadSimCards(); + } + + void _loadSimCards() async { + try { + final simData = await SimDataPlugin.getSimData(); + setState(() { + _simData = simData; + _isLoading = false; + _error = null; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = e.toString(); + }); + print('Error loading SIM cards: $e'); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[900], + title: const Text( + 'Select SIM for Call', + style: TextStyle(color: Colors.white), + ), + content: _buildContent(), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.grey), + ), + ), + if (_selectedSimSlot != null) + TextButton( + onPressed: () { + widget.onSimSelected(_selectedSimSlot!); + Navigator.of(context).pop(); + }, + child: const Text( + 'Switch SIM', + style: TextStyle(color: Colors.blue), + ), + ), + ], + ); + } + + Widget _buildContent() { + if (_isLoading) { + return const SizedBox( + height: 100, + child: Center( + child: CircularProgressIndicator(color: Colors.blue), + ), + ); + } + + if (_error != null) { + return _buildErrorContent(); + } + + if (_simData?.cards.isEmpty ?? true) { + return _buildFallbackContent(); + } + + return _buildSimList(); + } + + Widget _buildErrorContent() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 48, + ), + const SizedBox(height: 16), + Text( + 'Error loading SIM cards', + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 8), + Text( + _error!, + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadSimCards, + child: const Text('Retry'), + ), + ], + ); + } + + Widget _buildFallbackContent() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildSimTile('SIM 1', 'Slot 0', 0), + _buildSimTile('SIM 2', 'Slot 1', 1), + ], + ); + } + + Widget _buildSimList() { + return Column( + mainAxisSize: MainAxisSize.min, + children: _simData!.cards.map((card) { + final index = _simData!.cards.indexOf(card); + return _buildSimTile( + _getSimDisplayName(card, index), + _getSimSubtitle(card), + card.slotIndex, + ); + }).toList(), + ); + } + + Widget _buildSimTile(String title, String subtitle, int slotIndex) { + return RadioListTile( + title: Text( + title, + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + subtitle, + style: const TextStyle(color: Colors.grey), + ), + value: slotIndex, + groupValue: _selectedSimSlot, + onChanged: (value) { + setState(() { + _selectedSimSlot = value; + }); + }, + activeColor: Colors.blue, + ); + } + + String _getSimDisplayName(dynamic card, int index) { + if (card.displayName != null && card.displayName.isNotEmpty) { + return card.displayName; + } + if (card.carrierName != null && card.carrierName.isNotEmpty) { + return card.carrierName; + } + return 'SIM ${index + 1}'; + } + + String _getSimSubtitle(dynamic card) { + List subtitleParts = []; + + if (card.phoneNumber != null && card.phoneNumber.isNotEmpty) { + subtitleParts.add(card.phoneNumber); + } + + if (card.carrierName != null && + card.carrierName.isNotEmpty && + (card.displayName == null || card.displayName.isEmpty)) { + subtitleParts.add(card.carrierName); + } + + if (subtitleParts.isEmpty) { + subtitleParts.add('Slot ${card.slotIndex}'); + } + + return subtitleParts.join(' • '); + } +} diff --git a/dialer/lib/presentation/features/call/call_page.dart b/dialer/lib/presentation/features/call/call_page.dart index 38c665b..084eae7 100644 --- a/dialer/lib/presentation/features/call/call_page.dart +++ b/dialer/lib/presentation/features/call/call_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:dialer/domain/services/call_service.dart'; import 'package:dialer/domain/services/obfuscate_service.dart'; import 'package:dialer/presentation/common/widgets/username_color_generator.dart'; +import 'package:dialer/presentation/common/widgets/sim_selection_dialog.dart'; import 'package:flutter/services.dart'; class CallPage extends StatefulWidget { @@ -180,7 +181,7 @@ class _CallPageState extends State { final result = await _callService.speakerCall(context, speaker: !isSpeaker); print('CallPage: Speaker call result: $result'); - if (result['status'] != 'success') { + if (mounted && result['status'] != 'success') { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to toggle speaker: ${result['message']}')), @@ -202,10 +203,89 @@ class _CallPageState extends State { }); } - void _toggleIcingProtocol() { - setState(() { - icingProtocolOk = !icingProtocolOk; - }); + void _showSimSelectionDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return SimSelectionDialog( + phoneNumber: widget.phoneNumber, + displayName: widget.displayName, + onSimSelected: _switchToNewSim, + ); + }, + ); + } + + void _switchToNewSim(int simSlot) async { + try { + print( + 'CallPage: Switching to SIM slot $simSlot for ${widget.phoneNumber}'); + + // Check if widget is still mounted before starting + if (!mounted) { + print('CallPage: Widget unmounted, canceling SIM switch operation'); + return; + } + + // First hang up the current call + await _callService.hangUpCall(context); + + // Wait a brief moment for the call to end + await Future.delayed(const Duration(milliseconds: 500)); + + // Check if widget is still mounted before proceeding + if (!mounted) { + print('CallPage: Widget unmounted, canceling SIM switch operation'); + return; + } + + // Make a new call with the selected SIM + final result = await _callService.makeGsmCallWithSim( + context, + phoneNumber: widget.phoneNumber, + displayName: widget.displayName, + thumbnail: widget.thumbnail, + simSlot: simSlot, + ); + + // Check if widget is still mounted before showing snackbar + if (!mounted) { + print('CallPage: Widget unmounted, skipping result notification'); + return; + } + + if (result['status'] == 'calling') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Switched to SIM ${simSlot + 1} and redialing...'), + backgroundColor: Colors.green, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Failed to switch SIM and redial: ${result['message'] ?? 'Unknown error'}'), + backgroundColor: Colors.red, + ), + ); + } + } catch (e) { + print('CallPage: Error switching SIM: $e'); + + // Check if widget is still mounted before showing error snackbar + if (!mounted) { + print('CallPage: Widget unmounted, skipping error notification'); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error switching SIM: $e'), + backgroundColor: Colors.red, + ), + ); + } } void _hangUp() async { @@ -228,15 +308,17 @@ class _CallPageState extends State { final newContact = Contact()..phones = [Phone(widget.phoneNumber)]; final updatedContact = await FlutterContacts.openExternalInsert(newContact); - if (updatedContact != null) { + if (mounted && updatedContact != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Contact added successfully!')), ); } } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Permission denied for contacts')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Permission denied for contacts')), + ); + } } } @@ -510,7 +592,7 @@ class _CallPageState extends State { mainAxisSize: MainAxisSize.min, children: [ IconButton( - onPressed: () {}, + onPressed: _showSimSelectionDialog, icon: const Icon( Icons.sim_card, color: Colors.white,