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)
|
||||
}
|
||||
"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 {
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
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/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,
|
||||
|
Loading…
Reference in New Issue
Block a user