feat: add SIM selection dialog and integrate SIM slot handling for calls

This commit is contained in:
AlexisDanlos 2025-06-13 15:22:11 +02:00 committed by stcb
parent 29dfc0a2ac
commit cdd3a470c0
5 changed files with 347 additions and 16 deletions

View File

@ -96,11 +96,11 @@ class MainActivity : FlutterActivity() {
}
}
result.success(true)
}
"makeGsmCall" -> {
} "makeGsmCall" -> {
val phoneNumber = call.argument<String>("phoneNumber")
val simSlot = call.argument<Int>("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 {

View File

@ -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")

View File

@ -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<Map<String, dynamic>> 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 {

View File

@ -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<SimSelectionDialog> {
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<int>(
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<String> 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('');
}
}

View File

@ -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<CallPage> {
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<CallPage> {
});
}
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<CallPage> {
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<CallPage> {
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {},
onPressed: _showSimSelectionDialog,
icon: const Icon(
Icons.sim_card,
color: Colors.white,