feat: add SIM selection dialog and integrate SIM slot handling for calls
This commit is contained in:
parent
b31a5e2b87
commit
babe733dff
@ -96,11 +96,11 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
} "makeGsmCall" -> {
|
||||||
"makeGsmCall" -> {
|
|
||||||
val phoneNumber = call.argument<String>("phoneNumber")
|
val phoneNumber = call.argument<String>("phoneNumber")
|
||||||
|
val simSlot = call.argument<Int>("simSlot") ?: 0
|
||||||
if (phoneNumber != null) {
|
if (phoneNumber != null) {
|
||||||
val success = CallService.makeGsmCall(this, phoneNumber)
|
val success = CallService.makeGsmCall(this, phoneNumber, simSlot)
|
||||||
if (success) {
|
if (success) {
|
||||||
result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber))
|
result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber))
|
||||||
} else {
|
} else {
|
||||||
|
@ -13,14 +13,35 @@ import android.Manifest
|
|||||||
object CallService {
|
object CallService {
|
||||||
private val TAG = "CallService"
|
private val TAG = "CallService"
|
||||||
|
|
||||||
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
|
fun makeGsmCall(context: Context, phoneNumber: String, simSlot: Int = 0): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||||
val uri = Uri.parse("tel:$phoneNumber")
|
val uri = Uri.parse("tel:$phoneNumber")
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
|
||||||
telecomManager.placeCall(uri, Bundle())
|
// Get available phone accounts (SIM cards)
|
||||||
Log.d(TAG, "Initiated call to $phoneNumber")
|
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
|
true
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "CALL_PHONE permission not granted")
|
Log.e(TAG, "CALL_PHONE permission not granted")
|
||||||
|
@ -443,6 +443,30 @@ class CallService {
|
|||||||
// Load default SIM slot from settings
|
// Load default SIM slot from settings
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final simSlot = prefs.getInt('default_sim_slot') ?? 0;
|
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) {
|
if (_activeCallNumber == phoneNumber && _isCallPageVisible) {
|
||||||
print('CallService: Call already active for $phoneNumber, skipping');
|
print('CallService: Call already active for $phoneNumber, skipping');
|
||||||
return {
|
return {
|
||||||
|
204
dialer/lib/presentation/common/widgets/sim_selection_dialog.dart
Normal file
204
dialer/lib/presentation/common/widgets/sim_selection_dialog.dart
Normal 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(' • ');
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import 'package:flutter_contacts/flutter_contacts.dart';
|
|||||||
import 'package:dialer/domain/services/call_service.dart';
|
import 'package:dialer/domain/services/call_service.dart';
|
||||||
import 'package:dialer/domain/services/obfuscate_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/username_color_generator.dart';
|
||||||
|
import 'package:dialer/presentation/common/widgets/sim_selection_dialog.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class CallPage extends StatefulWidget {
|
class CallPage extends StatefulWidget {
|
||||||
@ -180,7 +181,7 @@ class _CallPageState extends State<CallPage> {
|
|||||||
final result =
|
final result =
|
||||||
await _callService.speakerCall(context, speaker: !isSpeaker);
|
await _callService.speakerCall(context, speaker: !isSpeaker);
|
||||||
print('CallPage: Speaker call result: $result');
|
print('CallPage: Speaker call result: $result');
|
||||||
if (result['status'] != 'success') {
|
if (mounted && result['status'] != 'success') {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Failed to toggle speaker: ${result['message']}')),
|
content: Text('Failed to toggle speaker: ${result['message']}')),
|
||||||
@ -202,10 +203,89 @@ class _CallPageState extends State<CallPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleIcingProtocol() {
|
void _showSimSelectionDialog() {
|
||||||
setState(() {
|
showDialog(
|
||||||
icingProtocolOk = !icingProtocolOk;
|
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 {
|
void _hangUp() async {
|
||||||
@ -228,15 +308,17 @@ class _CallPageState extends State<CallPage> {
|
|||||||
final newContact = Contact()..phones = [Phone(widget.phoneNumber)];
|
final newContact = Contact()..phones = [Phone(widget.phoneNumber)];
|
||||||
final updatedContact =
|
final updatedContact =
|
||||||
await FlutterContacts.openExternalInsert(newContact);
|
await FlutterContacts.openExternalInsert(newContact);
|
||||||
if (updatedContact != null) {
|
if (mounted && updatedContact != null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Contact added successfully!')),
|
SnackBar(content: Text('Contact added successfully!')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
SnackBar(content: Text('Permission denied for contacts')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
SnackBar(content: Text('Permission denied for contacts')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -510,7 +592,7 @@ class _CallPageState extends State<CallPage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {},
|
onPressed: _showSimSelectionDialog,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.sim_card,
|
Icons.sim_card,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
Loading…
Reference in New Issue
Block a user