change-default-sim (#60)
Co-authored-by: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Co-authored-by: stcb <21@stcb.cc> Reviewed-on: #60 Co-authored-by: AlexisDanlos <alexis.danlos@epitech.eu> Co-committed-by: AlexisDanlos <alexis.danlos@epitech.eu>
This commit is contained in:
parent
35558c6a43
commit
aa8d32f28c
@ -24,7 +24,7 @@ android {
|
||||
applicationId = "com.icing.dialer"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdk = 23
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
@ -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<PhoneAccountHandle> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -10,6 +10,8 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.CallLog
|
||||
import android.telecom.TelecomManager
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.telephony.SubscriptionInfo
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.icing.dialer.KeystoreHelper
|
||||
@ -96,11 +98,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 {
|
||||
@ -228,16 +230,25 @@ class MainActivity : FlutterActivity() {
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
if (call.method == "getCallLogs") {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
|
||||
val callLogs = getCallLogs()
|
||||
result.success(callLogs)
|
||||
} else {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_CALL_LOG), REQUEST_CODE_CALL_LOG_PERMISSION)
|
||||
result.error("PERMISSION_DENIED", "Call log permission not granted", null)
|
||||
when (call.method) {
|
||||
"getCallLogs" -> {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
|
||||
val callLogs = getCallLogs()
|
||||
result.success(callLogs)
|
||||
} else {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_CALL_LOG), REQUEST_CODE_CALL_LOG_PERMISSION)
|
||||
result.error("PERMISSION_DENIED", "Call log permission not granted", null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.notImplemented()
|
||||
"getLatestCallLog" -> {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
|
||||
val latestCallLog = getLatestCallLog()
|
||||
result.success(latestCallLog)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Call log permission not granted", null)
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -322,11 +333,29 @@ class MainActivity : FlutterActivity() {
|
||||
val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
|
||||
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
|
||||
|
||||
// Extract subscription ID (SIM card info) if available
|
||||
var subscriptionId: Int? = null
|
||||
var simName: String? = null
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
val subIdColumnIndex = it.getColumnIndex("subscription_id")
|
||||
if (subIdColumnIndex >= 0) {
|
||||
subscriptionId = it.getInt(subIdColumnIndex)
|
||||
// Get the actual SIM name
|
||||
simName = getSimNameFromSubscriptionId(subscriptionId)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to get subscription_id: ${e.message}")
|
||||
}
|
||||
|
||||
val map = mutableMapOf<String, Any?>(
|
||||
"number" to number,
|
||||
"type" to type,
|
||||
"date" to date,
|
||||
"duration" to duration
|
||||
"duration" to duration,
|
||||
"subscription_id" to subscriptionId,
|
||||
"sim_name" to simName
|
||||
)
|
||||
logsList.add(map)
|
||||
}
|
||||
@ -334,6 +363,79 @@ class MainActivity : FlutterActivity() {
|
||||
return logsList
|
||||
}
|
||||
|
||||
private fun getLatestCallLog(): Map<String, Any?>? {
|
||||
val cursor: Cursor? = contentResolver.query(
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CallLog.Calls.DATE + " DESC"
|
||||
)
|
||||
cursor?.use {
|
||||
if (it.moveToNext()) {
|
||||
val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
|
||||
val type = it.getInt(it.getColumnIndexOrThrow(CallLog.Calls.TYPE))
|
||||
val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
|
||||
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
|
||||
|
||||
// Extract subscription ID (SIM card info) if available
|
||||
var subscriptionId: Int? = null
|
||||
var simName: String? = null
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
val subIdColumnIndex = it.getColumnIndex("subscription_id")
|
||||
if (subIdColumnIndex >= 0) {
|
||||
subscriptionId = it.getInt(subIdColumnIndex)
|
||||
// Get the actual SIM name
|
||||
simName = getSimNameFromSubscriptionId(subscriptionId)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to get subscription_id: ${e.message}")
|
||||
}
|
||||
|
||||
return mapOf(
|
||||
"number" to number,
|
||||
"type" to type,
|
||||
"date" to date,
|
||||
"duration" to duration,
|
||||
"subscription_id" to subscriptionId,
|
||||
"sim_name" to simName
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getSimNameFromSubscriptionId(subscriptionId: Int?): String? {
|
||||
if (subscriptionId == null) return null
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
val subscriptionManager = getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager
|
||||
val subscriptionInfo: SubscriptionInfo? = subscriptionManager.getActiveSubscriptionInfo(subscriptionId)
|
||||
|
||||
return subscriptionInfo?.let { info ->
|
||||
// Try to get display name first, fallback to carrier name, then generic name
|
||||
when {
|
||||
!info.displayName.isNullOrBlank() && info.displayName.toString() != info.subscriptionId.toString() -> {
|
||||
info.displayName.toString()
|
||||
}
|
||||
!info.carrierName.isNullOrBlank() -> {
|
||||
info.carrierName.toString()
|
||||
}
|
||||
else -> "SIM ${info.simSlotIndex + 1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to get SIM name for subscription $subscriptionId: ${e.message}")
|
||||
}
|
||||
|
||||
// Fallback to generic name
|
||||
return "SIM ${subscriptionId + 1}"
|
||||
}
|
||||
|
||||
private fun handleIncomingCallIntent(intent: Intent?) {
|
||||
intent?.let {
|
||||
if (it.getBooleanExtra("isIncomingCall", false)) {
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -1,15 +1,19 @@
|
||||
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';
|
||||
// Import for history update callback
|
||||
import '../../presentation/features/history/history_page.dart';
|
||||
|
||||
class CallService {
|
||||
static const MethodChannel _channel = MethodChannel('call_service');
|
||||
static String? currentPhoneNumber;
|
||||
static String? currentDisplayName;
|
||||
static Uint8List? currentThumbnail;
|
||||
static int? currentSimSlot; // Track which SIM slot is being used
|
||||
static bool _isCallPageVisible = false;
|
||||
static Map<String, dynamic>? _pendingCall;
|
||||
static bool wasPhoneLocked = false;
|
||||
@ -17,18 +21,43 @@ class CallService {
|
||||
static bool _isNavigating = false;
|
||||
final ContactService _contactService = ContactService();
|
||||
final _callStateController = StreamController<String>.broadcast();
|
||||
final _audioStateController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _audioStateController =
|
||||
StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _simStateController = StreamController<int?>.broadcast();
|
||||
Map<String, dynamic>? _currentAudioState;
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
Stream<String> get callStateStream => _callStateController.stream;
|
||||
Stream<Map<String, dynamic>> get audioStateStream => _audioStateController.stream;
|
||||
Stream<Map<String, dynamic>> get audioStateStream =>
|
||||
_audioStateController.stream;
|
||||
Stream<int?> get simStateStream => _simStateController.stream;
|
||||
Map<String, dynamic>? get currentAudioState => _currentAudioState;
|
||||
// Getter for current SIM slot
|
||||
static int? get getCurrentSimSlot => currentSimSlot;
|
||||
// Get SIM display name for the current call
|
||||
static String? getCurrentSimDisplayName() {
|
||||
if (currentSimSlot == null) return null;
|
||||
return "SIM ${currentSimSlot! + 1}";
|
||||
}
|
||||
|
||||
// Cancel pending SIM switch (used when user manually hangs up)
|
||||
void cancelPendingSimSwitch() {
|
||||
if (_pendingSimSwitch != null) {
|
||||
print('CallService: Canceling pending SIM switch due to manual hangup');
|
||||
_pendingSimSwitch = null;
|
||||
_manualHangupFlag = true; // Mark that hangup was manual
|
||||
print('CallService: Manual hangup flag set to $_manualHangupFlag');
|
||||
} else {
|
||||
print('CallService: No pending SIM switch to cancel');
|
||||
// Don't set manual hangup flag if there's no SIM switch to cancel
|
||||
}
|
||||
}
|
||||
|
||||
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 +66,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,30 +89,63 @@ 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") {
|
||||
print('CallService: ========== CALL DISCONNECTED ==========');
|
||||
print(
|
||||
'CallService: _pendingSimSwitch: ${_pendingSimSwitch != null}');
|
||||
print('CallService: _manualHangupFlag: $_manualHangupFlag');
|
||||
print('CallService: _isCallPageVisible: $_isCallPageVisible');
|
||||
|
||||
// Always close call page on disconnection - SIM switching should not prevent this
|
||||
print('CallService: Calling _closeCallPage() on call disconnection');
|
||||
_closeCallPage();
|
||||
|
||||
// Reset manual hangup flag after successful page close
|
||||
if (_manualHangupFlag) {
|
||||
print(
|
||||
'CallService: Resetting manual hangup flag after page close');
|
||||
_manualHangupFlag = false;
|
||||
}
|
||||
if (wasPhoneLocked) {
|
||||
await _channel.invokeMethod("callEndedFromFlutter");
|
||||
}
|
||||
|
||||
// Notify history page to add the latest call
|
||||
// Add a small delay to ensure call log is updated by the system
|
||||
Timer(const Duration(milliseconds: 500), () {
|
||||
HistoryPageState.addNewCallToHistory();
|
||||
});
|
||||
|
||||
_activeCallNumber = null;
|
||||
// Handle pending SIM switch after call is disconnected
|
||||
_handlePendingSimSwitch();
|
||||
} 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 +154,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,38 +169,65 @@ 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 ==========');
|
||||
print('CallService: wasPhoneLocked: $wasPhoneLocked');
|
||||
print('CallService: _pendingSimSwitch: ${_pendingSimSwitch != null}');
|
||||
print('CallService: _manualHangupFlag: $_manualHangupFlag');
|
||||
print('CallService: _isCallPageVisible: $_isCallPageVisible');
|
||||
|
||||
// Always close call page when call ends - SIM switching should not prevent this
|
||||
print('CallService: Calling _closeCallPage() on call ended/removed');
|
||||
_closeCallPage();
|
||||
|
||||
// Reset manual hangup flag after closing page
|
||||
if (_manualHangupFlag) {
|
||||
print(
|
||||
'CallService: Resetting manual hangup flag after callEnded');
|
||||
_manualHangupFlag = false;
|
||||
}
|
||||
if (wasPhoneLocked) {
|
||||
await _channel.invokeMethod("callEndedFromFlutter");
|
||||
}
|
||||
|
||||
// Notify history page to add the latest call
|
||||
// Add a small delay to ensure call log is updated by the system
|
||||
Timer(const Duration(milliseconds: 500), () {
|
||||
HistoryPageState.addNewCallToHistory();
|
||||
});
|
||||
|
||||
currentPhoneNumber = null;
|
||||
currentDisplayName = null;
|
||||
currentThumbnail = null;
|
||||
currentSimSlot = null; // Reset SIM slot when call ends
|
||||
_simStateController.add(null); // Notify UI that SIM is cleared
|
||||
_activeCallNumber = null;
|
||||
break;
|
||||
case "incomingCallFromNotification":
|
||||
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 +251,8 @@ class CallService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> muteCall(BuildContext context, {required bool mute}) async {
|
||||
Future<Map<String, dynamic>> muteCall(BuildContext context,
|
||||
{required bool mute}) async {
|
||||
try {
|
||||
print('CallService: Toggling mute to $mute');
|
||||
final result = await _channel.invokeMethod('muteCall', {'mute': mute});
|
||||
@ -178,10 +273,12 @@ class CallService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> speakerCall(BuildContext context, {required bool speaker}) async {
|
||||
Future<Map<String, dynamic>> 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<String, dynamic>.from(result);
|
||||
} catch (e) {
|
||||
@ -208,18 +305,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 +328,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 +360,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,24 +394,32 @@ 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;
|
||||
}
|
||||
_activeCallNumber = currentPhoneNumber;
|
||||
Navigator.pushReplacement(
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: '/call'),
|
||||
@ -332,9 +445,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 +461,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 +479,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,13 +491,31 @@ class CallService {
|
||||
print('CallService: Cannot close page, context is null');
|
||||
return;
|
||||
}
|
||||
print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible');
|
||||
if (Navigator.canPop(context)) {
|
||||
print('CallService: Popping call page');
|
||||
Navigator.pop(context);
|
||||
|
||||
// Only attempt to close if a call page is actually visible
|
||||
if (!_isCallPageVisible) {
|
||||
print('CallService: Call page already closed');
|
||||
return;
|
||||
}
|
||||
|
||||
print(
|
||||
'CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible, _pendingSimSwitch: ${_pendingSimSwitch != null}, _manualHangupFlag: $_manualHangupFlag');
|
||||
|
||||
// Use popUntil to ensure we go back to the home page
|
||||
try {
|
||||
Navigator.popUntil(context, (route) => route.isFirst);
|
||||
_isCallPageVisible = false;
|
||||
} else {
|
||||
print('CallService: No page to pop');
|
||||
print('CallService: Used popUntil to return to home page');
|
||||
} catch (e) {
|
||||
print('CallService: Error with popUntil, trying regular pop: $e');
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
_isCallPageVisible = false;
|
||||
print('CallService: Used regular pop as fallback');
|
||||
} else {
|
||||
print('CallService: No page to pop, setting _isCallPageVisible to false');
|
||||
_isCallPageVisible = false;
|
||||
}
|
||||
}
|
||||
_activeCallNumber = null;
|
||||
}
|
||||
@ -388,20 +525,54 @@ class CallService {
|
||||
required String phoneNumber,
|
||||
String? displayName,
|
||||
Uint8List? thumbnail,
|
||||
}) async {
|
||||
try {
|
||||
// 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 {"status": "already_active", "message": "Call already in progress"};
|
||||
return {
|
||||
"status": "already_active",
|
||||
"message": "Call already in progress"
|
||||
};
|
||||
}
|
||||
currentPhoneNumber = phoneNumber;
|
||||
currentDisplayName = displayName ?? phoneNumber;
|
||||
currentThumbnail = thumbnail;
|
||||
currentSimSlot = simSlot; // Track the SIM slot being used
|
||||
_simStateController.add(simSlot); // Notify UI of SIM change
|
||||
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<String, dynamic>.from(result as Map);
|
||||
if (resultMap["status"] != "calling") {
|
||||
@ -419,17 +590,40 @@ class CallService {
|
||||
}
|
||||
}
|
||||
|
||||
// Pending SIM switch data
|
||||
static Map<String, dynamic>? _pendingSimSwitch;
|
||||
static bool _manualHangupFlag = false; // Track if hangup was manual
|
||||
|
||||
// Getter to check if there's a pending SIM switch
|
||||
static bool get hasPendingSimSwitch => _pendingSimSwitch != null;
|
||||
Future<Map<String, dynamic>> hangUpCall(BuildContext context) async {
|
||||
try {
|
||||
print('CallService: Hanging up call');
|
||||
print('CallService: ========== HANGUP INITIATED ==========');
|
||||
print('CallService: _pendingSimSwitch: ${_pendingSimSwitch != null}');
|
||||
print('CallService: _manualHangupFlag: $_manualHangupFlag');
|
||||
print('CallService: _isCallPageVisible: $_isCallPageVisible');
|
||||
|
||||
final result = await _channel.invokeMethod('hangUpCall');
|
||||
print('CallService: hangUpCall result: $result');
|
||||
final resultMap = Map<String, dynamic>.from(result as Map);
|
||||
|
||||
if (resultMap["status"] != "ended") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to end call")),
|
||||
);
|
||||
} else {
|
||||
// If hangup was successful, ensure call page closes after a short delay
|
||||
// This is a fallback in case the native call state events don't fire properly
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (_isCallPageVisible) {
|
||||
print(
|
||||
'CallService: FALLBACK - Force closing call page after hangup');
|
||||
_closeCallPage();
|
||||
_manualHangupFlag = false; // Reset flag
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
} catch (e) {
|
||||
print("CallService: Error hanging up call: $e");
|
||||
@ -439,4 +633,88 @@ class CallService {
|
||||
return {"status": "error", "message": e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> switchSimAndRedial({
|
||||
required String phoneNumber,
|
||||
required String displayName,
|
||||
required int simSlot,
|
||||
Uint8List? thumbnail,
|
||||
}) async {
|
||||
try {
|
||||
print(
|
||||
'CallService: Starting SIM switch to slot $simSlot for $phoneNumber');
|
||||
|
||||
// Store the redial information for after hangup
|
||||
_pendingSimSwitch = {
|
||||
'phoneNumber': phoneNumber,
|
||||
'displayName': displayName,
|
||||
'simSlot': simSlot,
|
||||
'thumbnail': thumbnail,
|
||||
};
|
||||
|
||||
// Hang up the current call - this will trigger the disconnected state
|
||||
await _channel.invokeMethod('hangUpCall');
|
||||
print(
|
||||
'CallService: Hangup initiated, waiting for disconnection to complete redial');
|
||||
} catch (e) {
|
||||
print('CallService: Error during SIM switch: $e');
|
||||
_pendingSimSwitch = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePendingSimSwitch() async {
|
||||
if (_pendingSimSwitch == null) return;
|
||||
|
||||
final switchData = _pendingSimSwitch!;
|
||||
_pendingSimSwitch = null;
|
||||
|
||||
try {
|
||||
print('CallService: Executing pending SIM switch redial');
|
||||
|
||||
// Wait a moment to ensure the previous call is fully disconnected
|
||||
await Future.delayed(const Duration(
|
||||
milliseconds: 1000)); // Store the new call info for the redial
|
||||
currentPhoneNumber = switchData['phoneNumber'];
|
||||
currentDisplayName = switchData['displayName'];
|
||||
currentThumbnail = switchData['thumbnail'];
|
||||
currentSimSlot = switchData['simSlot']; // Track the new SIM slot
|
||||
_simStateController.add(switchData['simSlot']); // Notify UI of SIM change
|
||||
|
||||
// Make the new call with the selected SIM
|
||||
final result = await _channel.invokeMethod('makeGsmCall', {
|
||||
'phoneNumber': switchData['phoneNumber'],
|
||||
'simSlot': switchData['simSlot'],
|
||||
});
|
||||
|
||||
print('CallService: SIM switch redial result: $result');
|
||||
|
||||
// Show success feedback
|
||||
final context = navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Switched to SIM ${switchData['simSlot'] + 1} and redialing...'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('CallService: Error during SIM switch redial: $e');
|
||||
|
||||
// Show error feedback and close the call page
|
||||
final context = navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to redial with new SIM: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
// Close the call page since redial failed
|
||||
_closeCallPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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,7 +4,9 @@ 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';
|
||||
import 'package:sim_data_new/sim_data.dart';
|
||||
|
||||
class CallPage extends StatefulWidget {
|
||||
final String displayName;
|
||||
@ -35,15 +37,57 @@ class _CallPageState extends State<CallPage> {
|
||||
String _callStatus = "Calling...";
|
||||
StreamSubscription<String>? _callStateSubscription;
|
||||
StreamSubscription<Map<String, dynamic>>? _audioStateSubscription;
|
||||
StreamSubscription<int?>? _simStateSubscription;
|
||||
bool _isCallActive = true; // Track if call is still active
|
||||
String? _simName; // Human-readable SIM card name
|
||||
|
||||
bool get isNumberUnknown => widget.displayName == widget.phoneNumber;
|
||||
|
||||
// Fetch and update human-readable SIM name based on slot
|
||||
Future<void> _updateSimName(int? simSlot) async {
|
||||
if (!mounted) return;
|
||||
if (simSlot != null) {
|
||||
try {
|
||||
final simData = await SimDataPlugin.getSimData();
|
||||
// Find the SIM card matching the slot index, if any
|
||||
dynamic card;
|
||||
for (var c in simData.cards) {
|
||||
if (c.slotIndex == simSlot) {
|
||||
card = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
String name;
|
||||
if (card != null && card.displayName.isNotEmpty) {
|
||||
name = card.displayName;
|
||||
} else if (card != null && card.carrierName.isNotEmpty) {
|
||||
name = card.carrierName;
|
||||
} else {
|
||||
name = 'SIM ${simSlot + 1}';
|
||||
}
|
||||
setState(() {
|
||||
_simName = name;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_simName = 'SIM ${simSlot + 1}';
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_simName = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkInitialCallState();
|
||||
_listenToCallState();
|
||||
_listenToAudioState();
|
||||
_listenToSimState();
|
||||
_updateSimName(CallService.getCurrentSimSlot); // Initial SIM name
|
||||
_setInitialAudioState();
|
||||
}
|
||||
|
||||
@ -52,6 +96,7 @@ class _CallPageState extends State<CallPage> {
|
||||
_callTimer?.cancel();
|
||||
_callStateSubscription?.cancel();
|
||||
_audioStateSubscription?.cancel();
|
||||
_simStateSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -69,10 +114,19 @@ class _CallPageState extends State<CallPage> {
|
||||
try {
|
||||
final state = await _callService.getCallState();
|
||||
print('CallPage: Initial call state: $state');
|
||||
if (mounted && state == "active") {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_callStatus = "00:00";
|
||||
_startCallTimer();
|
||||
if (state == "active") {
|
||||
_callStatus = "00:00";
|
||||
_isCallActive = true;
|
||||
_startCallTimer();
|
||||
} else if (state == "disconnected" || state == "disconnecting") {
|
||||
_callStatus = "Call Ended";
|
||||
_isCallActive = false;
|
||||
} else {
|
||||
_callStatus = "Calling...";
|
||||
_isCallActive = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@ -87,12 +141,16 @@ class _CallPageState extends State<CallPage> {
|
||||
setState(() {
|
||||
if (state == "active") {
|
||||
_callStatus = "00:00";
|
||||
_isCallActive = true;
|
||||
_startCallTimer();
|
||||
} else if (state == "disconnected" || state == "disconnecting") {
|
||||
_callTimer?.cancel();
|
||||
_callStatus = "Call Ended";
|
||||
_isCallActive = false;
|
||||
// Let CallService handle navigation - don't navigate from here
|
||||
} else {
|
||||
_callStatus = "Calling...";
|
||||
_isCallActive = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -110,6 +168,12 @@ class _CallPageState extends State<CallPage> {
|
||||
});
|
||||
}
|
||||
|
||||
void _listenToSimState() {
|
||||
_simStateSubscription = _callService.simStateStream.listen((simSlot) {
|
||||
_updateSimName(simSlot);
|
||||
});
|
||||
}
|
||||
|
||||
void _startCallTimer() {
|
||||
_callTimer?.cancel();
|
||||
_callTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
@ -180,7 +244,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,17 +266,76 @@ 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: Initiating SIM switch to slot $simSlot for ${widget.phoneNumber}');
|
||||
|
||||
// Use the CallService to handle the SIM switch logic
|
||||
await _callService.switchSimAndRedial(
|
||||
phoneNumber: widget.phoneNumber,
|
||||
displayName: widget.displayName,
|
||||
simSlot: simSlot,
|
||||
thumbnail: widget.thumbnail,
|
||||
);
|
||||
|
||||
print('CallPage: SIM switch initiated successfully');
|
||||
} catch (e) {
|
||||
print('CallPage: Error initiating SIM switch: $e');
|
||||
|
||||
// Show error feedback if widget is still mounted
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error switching SIM: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _hangUp() async {
|
||||
// Don't try to hang up if call is already ended
|
||||
if (!_isCallActive) {
|
||||
print('CallPage: Ignoring hangup - call already ended');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
print('CallPage: Initiating hangUp');
|
||||
print(
|
||||
'CallPage: Initiating manual hangUp - canceling any pending SIM switch');
|
||||
|
||||
// Immediately mark call as inactive to prevent further interactions
|
||||
setState(() {
|
||||
_isCallActive = false;
|
||||
_callStatus = "Ending Call...";
|
||||
});
|
||||
|
||||
// Cancel any pending SIM switch since user is manually hanging up
|
||||
_callService.cancelPendingSimSwitch();
|
||||
|
||||
final result = await _callService.hangUpCall(context);
|
||||
print('CallPage: Hang up result: $result');
|
||||
|
||||
// If the page is still visible after hangup, try to close it
|
||||
if (mounted && ModalRoute.of(context)?.isCurrent == true) {
|
||||
print('CallPage: Still visible after hangup, navigating back');
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} catch (e) {
|
||||
print('CallPage: Error hanging up: $e');
|
||||
if (mounted) {
|
||||
@ -228,15 +351,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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -248,14 +373,24 @@ class _CallPageState extends State<CallPage> {
|
||||
|
||||
print(
|
||||
'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}');
|
||||
return PopScope(
|
||||
canPop: _callStatus == "Call Ended",
|
||||
onPopInvoked: (didPop) {
|
||||
if (!didPop) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Cannot leave during an active call')),
|
||||
);
|
||||
|
||||
// If call is disconnected and we're not actively navigating, force navigation
|
||||
if ((_callStatus == "Call Ended" || !_isCallActive) && mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && ModalRoute.of(context)?.isCurrent == true) {
|
||||
print('CallPage: Call ended, forcing navigation back to home');
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop:
|
||||
true, // Always allow popping - CallService manages when it's appropriate
|
||||
onPopInvoked: (didPop) {
|
||||
print(
|
||||
'CallPage: PopScope onPopInvoked - didPop: $didPop, _isCallActive: $_isCallActive, _callStatus: $_callStatus');
|
||||
// No longer prevent popping during active calls - CallService handles this
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Container(
|
||||
@ -320,6 +455,30 @@ class _CallPageState extends State<CallPage> {
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
// Show SIM information if a SIM slot has been set
|
||||
if (_simName != null)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 4.0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.blue.withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
// Show human-readable SIM name plus slot number
|
||||
'$_simName (SIM ${CallService.getCurrentSimSlot! + 1})',
|
||||
style: TextStyle(
|
||||
fontSize: statusFontSize - 2,
|
||||
color: Colors.lightBlueAccent,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -510,10 +669,14 @@ class _CallPageState extends State<CallPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
onPressed: _isCallActive
|
||||
? _showSimSelectionDialog
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.sim_card,
|
||||
color: Colors.white,
|
||||
color: _isCallActive
|
||||
? Colors.white
|
||||
: Colors.grey,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
@ -539,15 +702,15 @@ class _CallPageState extends State<CallPage> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: GestureDetector(
|
||||
onTap: _hangUp,
|
||||
onTap: _isCallActive ? _hangUp : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
decoration: BoxDecoration(
|
||||
color: _isCallActive ? Colors.red : Colors.grey,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.call_end,
|
||||
child: Icon(
|
||||
_isCallActive ? Icons.call_end : Icons.call_end,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
|
@ -19,6 +19,7 @@ class History {
|
||||
final String callType; // 'incoming' or 'outgoing'
|
||||
final String callStatus; // 'missed' or 'answered'
|
||||
final int attempts;
|
||||
final String? simName; // Name of the SIM used for the call
|
||||
|
||||
History(
|
||||
this.contact,
|
||||
@ -26,6 +27,7 @@ class History {
|
||||
this.callType,
|
||||
this.callStatus,
|
||||
this.attempts,
|
||||
this.simName,
|
||||
);
|
||||
}
|
||||
|
||||
@ -33,29 +35,90 @@ class HistoryPage extends StatefulWidget {
|
||||
const HistoryPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HistoryPageState createState() => _HistoryPageState();
|
||||
HistoryPageState createState() => HistoryPageState();
|
||||
}
|
||||
|
||||
class _HistoryPageState extends State<HistoryPage>
|
||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||
List<History> histories = [];
|
||||
bool loading = true;
|
||||
class HistoryPageState extends State<HistoryPage>
|
||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||
// Static histories list shared across all instances
|
||||
static List<History> _globalHistories = [];
|
||||
|
||||
// Getter to access the global histories list
|
||||
List<History> get histories => _globalHistories;
|
||||
|
||||
bool _isInitialLoad = true;
|
||||
int? _expandedIndex;
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
final CallService _callService = CallService();
|
||||
Timer? _debounceTimer;
|
||||
|
||||
// Create a MethodChannel instance.
|
||||
static const MethodChannel _channel = MethodChannel('com.example.calllog');
|
||||
|
||||
// Static reference to the current instance for call-end notifications
|
||||
static HistoryPageState? _currentInstance;
|
||||
|
||||
// Global flag to track if history has been loaded once across all instances
|
||||
static bool _hasLoadedInitialHistory = false;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true; // Preserve state when switching pages
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_currentInstance = this; // Register this instance
|
||||
|
||||
// Only load initial data if it hasn't been loaded before
|
||||
if (!_hasLoadedInitialHistory) {
|
||||
_buildHistories();
|
||||
} else {
|
||||
// If history was already loaded, just mark this instance as not doing initial load
|
||||
_isInitialLoad = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Public method to trigger reload when page becomes visible
|
||||
void triggerReload() {
|
||||
// Disabled automatic reloading - only load once and add new entries via addNewCallToHistory
|
||||
print("HistoryPage: triggerReload called but disabled to prevent full reload");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
if (_currentInstance == this) {
|
||||
_currentInstance = null; // Unregister this instance
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Static method to add a new call to the history list
|
||||
static void addNewCallToHistory() {
|
||||
_currentInstance?._addLatestCallToHistory();
|
||||
}
|
||||
|
||||
/// Notify all instances to refresh UI when history changes
|
||||
static void _notifyHistoryChanged() {
|
||||
_currentInstance?.setState(() {
|
||||
// Trigger UI rebuild for the current instance
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
// Disabled automatic reloading when app comes to foreground
|
||||
print("HistoryPage: didChangeAppLifecycleState called but disabled to prevent full reload");
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (loading && histories.isEmpty) {
|
||||
_buildHistories();
|
||||
}
|
||||
// didChangeDependencies is not reliable for TabBarView changes
|
||||
// We'll use a different approach with RouteAware or manual detection
|
||||
}
|
||||
|
||||
Future<void> _refreshContacts() async {
|
||||
@ -116,6 +179,22 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Helper: Get SIM name from subscription ID
|
||||
String? _getSimNameFromSubscriptionId(int? subscriptionId) {
|
||||
if (subscriptionId == null) return null;
|
||||
|
||||
// Map subscription IDs to SIM names
|
||||
// These values might need to be adjusted based on your device
|
||||
switch (subscriptionId) {
|
||||
case 0:
|
||||
return "SIM 1";
|
||||
case 1:
|
||||
return "SIM 2";
|
||||
default:
|
||||
return "SIM ${subscriptionId + 1}";
|
||||
}
|
||||
}
|
||||
|
||||
/// Request permission for reading call logs.
|
||||
Future<bool> _requestCallLogPermission() async {
|
||||
var status = await Permission.phone.status;
|
||||
@ -130,10 +209,12 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
// Request permission.
|
||||
bool hasPermission = await _requestCallLogPermission();
|
||||
if (!hasPermission) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Call log permission not granted')));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Call log permission not granted')));
|
||||
}
|
||||
setState(() {
|
||||
loading = false;
|
||||
_isInitialLoad = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -203,8 +284,22 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
);
|
||||
}
|
||||
|
||||
// Extract SIM information if available
|
||||
String? simName;
|
||||
if (entry.containsKey('sim_name') && entry['sim_name'] != null) {
|
||||
simName = entry['sim_name'] as String;
|
||||
print("DEBUG: Found sim_name: $simName for number: $number"); // Debug print
|
||||
} else if (entry.containsKey('subscription_id')) {
|
||||
final subId = entry['subscription_id'];
|
||||
print("DEBUG: Found subscription_id: $subId for number: $number, but no sim_name"); // Debug print
|
||||
simName = _getSimNameFromSubscriptionId(subId);
|
||||
print("DEBUG: Mapped to SIM name: $simName"); // Debug print
|
||||
} else {
|
||||
print("DEBUG: No SIM info found for number: $number"); // Debug print
|
||||
}
|
||||
|
||||
callHistories
|
||||
.add(History(matchedContact, callDate, callType, callStatus, 1));
|
||||
.add(History(matchedContact, callDate, callType, callStatus, 1, simName));
|
||||
// Yield every 10 iterations to avoid blocking the UI.
|
||||
if (i % 10 == 0) await Future.delayed(Duration(milliseconds: 1));
|
||||
}
|
||||
@ -212,10 +307,121 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
// Sort histories by most recent.
|
||||
callHistories.sort((a, b) => b.date.compareTo(a.date));
|
||||
|
||||
setState(() {
|
||||
histories = callHistories;
|
||||
loading = false;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_globalHistories = callHistories;
|
||||
_isInitialLoad = false;
|
||||
_hasLoadedInitialHistory = true; // Mark that history has been loaded once
|
||||
});
|
||||
// Notify other instances about the initial load
|
||||
_notifyHistoryChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the latest call log entry to the history list
|
||||
Future<void> _addLatestCallToHistory() async {
|
||||
try {
|
||||
// Get the latest call log entry
|
||||
final dynamic rawEntry = await _channel.invokeMethod('getLatestCallLog');
|
||||
|
||||
if (rawEntry == null) {
|
||||
print("No latest call log entry found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to proper type - handle the method channel result properly
|
||||
final Map<String, dynamic> latestEntry = Map<String, dynamic>.from(
|
||||
(rawEntry as Map<Object?, Object?>).cast<String, dynamic>()
|
||||
);
|
||||
|
||||
final String number = latestEntry['number'] ?? '';
|
||||
if (number.isEmpty) return;
|
||||
|
||||
// Ensure contacts are loaded
|
||||
final contactState = ContactState.of(context);
|
||||
if (contactState.loading) {
|
||||
await Future.doWhile(() async {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
return contactState.loading;
|
||||
});
|
||||
}
|
||||
List<Contact> contacts = contactState.contacts;
|
||||
|
||||
// Convert timestamp to DateTime
|
||||
DateTime callDate = DateTime.fromMillisecondsSinceEpoch(latestEntry['date'] ?? 0);
|
||||
|
||||
int typeInt = latestEntry['type'] ?? 0;
|
||||
int duration = latestEntry['duration'] ?? 0;
|
||||
String callType;
|
||||
String callStatus;
|
||||
|
||||
// Map integer values to call type/status
|
||||
switch (typeInt) {
|
||||
case 1:
|
||||
callType = "incoming";
|
||||
callStatus = (duration == 0) ? "missed" : "answered";
|
||||
break;
|
||||
case 2:
|
||||
callType = "outgoing";
|
||||
callStatus = "answered";
|
||||
break;
|
||||
case 3:
|
||||
callType = "incoming";
|
||||
callStatus = "missed";
|
||||
break;
|
||||
default:
|
||||
callType = "unknown";
|
||||
callStatus = "unknown";
|
||||
}
|
||||
|
||||
// Try to find a matching contact
|
||||
Contact? matchedContact = findContactForNumber(number, contacts);
|
||||
if (matchedContact == null) {
|
||||
// Create a dummy contact if not found
|
||||
matchedContact = Contact(
|
||||
id: "dummy-$number",
|
||||
displayName: number,
|
||||
phones: [Phone(number)],
|
||||
);
|
||||
}
|
||||
|
||||
// Extract SIM information if available
|
||||
String? simName;
|
||||
if (latestEntry.containsKey('sim_name') && latestEntry['sim_name'] != null) {
|
||||
simName = latestEntry['sim_name'] as String;
|
||||
print("DEBUG: Found sim_name: $simName for number: $number");
|
||||
} else if (latestEntry.containsKey('subscription_id')) {
|
||||
final subId = latestEntry['subscription_id'];
|
||||
print("DEBUG: Found subscription_id: $subId for number: $number, but no sim_name");
|
||||
simName = _getSimNameFromSubscriptionId(subId);
|
||||
print("DEBUG: Mapped to SIM name: $simName");
|
||||
} else {
|
||||
print("DEBUG: No SIM info found for number: $number");
|
||||
}
|
||||
|
||||
// Create new history entry
|
||||
History newHistory = History(matchedContact, callDate, callType, callStatus, 1, simName);
|
||||
|
||||
// Check if this call is already in the list (avoid duplicates)
|
||||
bool alreadyExists = _globalHistories.any((history) =>
|
||||
history.contact.phones.isNotEmpty &&
|
||||
sanitizeNumber(history.contact.phones.first.number) == sanitizeNumber(number) &&
|
||||
history.date.difference(callDate).abs().inSeconds < 5); // Within 5 seconds
|
||||
|
||||
if (!alreadyExists && mounted) {
|
||||
setState(() {
|
||||
// Insert at the beginning since it's the most recent
|
||||
_globalHistories.insert(0, newHistory);
|
||||
});
|
||||
// Notify other instances about the change
|
||||
_notifyHistoryChanged();
|
||||
print("Added new call to history: $number at $callDate");
|
||||
} else {
|
||||
print("Call already exists in history or widget unmounted");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error adding latest call to history: $e");
|
||||
}
|
||||
}
|
||||
|
||||
List _buildGroupedList(List<History> historyList) {
|
||||
@ -283,9 +489,9 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context); // required due to AutomaticKeepAliveClientMixin
|
||||
final contactState = ContactState.of(context);
|
||||
|
||||
if (loading || contactState.loading) {
|
||||
// Show loading only on initial load and if no data is available yet
|
||||
if (_isInitialLoad && histories.isEmpty) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
@ -413,9 +619,22 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
_obfuscateService.obfuscateData(contact.displayName),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
DateFormat('MMM dd, hh:mm a').format(history.date),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('MMM dd, hh:mm a').format(history.date),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
if (history.simName != null)
|
||||
Text(
|
||||
history.simName!,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -625,6 +844,11 @@ class CallDetailsPage extends StatelessWidget {
|
||||
label: 'Attempts:',
|
||||
value: '${history.attempts}',
|
||||
),
|
||||
if (history.simName != null)
|
||||
DetailRow(
|
||||
label: 'SIM Used:',
|
||||
value: history.simName!,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (contact.phones.isNotEmpty)
|
||||
DetailRow(
|
||||
|
@ -19,6 +19,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
late SearchController _searchBarController;
|
||||
String _rawSearchInput = '';
|
||||
final GlobalKey<HistoryPageState> _historyPageKey = GlobalKey<HistoryPageState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -93,6 +94,10 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
|
||||
void _handleTabIndex() {
|
||||
setState(() {});
|
||||
// Trigger history page reload when switching to history tab (index 1)
|
||||
if (_tabController.index == 1) {
|
||||
_historyPageKey.currentState?.triggerReload();
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleFavorite(Contact contact) async {
|
||||
@ -270,11 +275,11 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
children: [
|
||||
TabBarView(
|
||||
controller: _tabController,
|
||||
children: const [
|
||||
FavoritesPage(),
|
||||
HistoryPage(),
|
||||
ContactPage(),
|
||||
VoicemailPage(),
|
||||
children: [
|
||||
const FavoritesPage(),
|
||||
HistoryPage(key: _historyPageKey),
|
||||
const ContactPage(),
|
||||
const VoicemailPage(),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
|
@ -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(
|
||||
|
220
dialer/lib/presentation/features/settings/sim/settings_sim.dart
Normal file
220
dialer/lib/presentation/features/settings/sim/settings_sim.dart
Normal file
@ -0,0 +1,220 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sim_data_new/sim_data.dart';
|
||||
|
||||
class SettingsSimPage extends StatefulWidget {
|
||||
const SettingsSimPage({super.key});
|
||||
|
||||
@override
|
||||
_SettingsSimPageState createState() => _SettingsSimPageState();
|
||||
}
|
||||
|
||||
class _SettingsSimPageState extends State<SettingsSimPage> {
|
||||
int _selectedSim = 0;
|
||||
SimData? _simData;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSimCards();
|
||||
_loadDefaultSim();
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.blue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Error loading SIM cards',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_error!,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
_loadSimCards();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Fallback to default options:',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildFallbackSimList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_simData == null || _simData!.cards.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.sim_card_alert,
|
||||
color: Colors.orange,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'No SIM cards detected',
|
||||
style: TextStyle(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Using default options:',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildFallbackSimList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _simData!.cards.length,
|
||||
itemBuilder: (context, index) {
|
||||
final card = _simData!.cards[index];
|
||||
return RadioListTile<int>(
|
||||
title: Text(
|
||||
_getSimDisplayName(card, index),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
_getSimSubtitle(card),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
value: card.slotIndex,
|
||||
groupValue: _selectedSim,
|
||||
onChanged: _onSimChanged,
|
||||
activeColor: Colors.blue,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFallbackSimList() {
|
||||
return Column(
|
||||
children: [
|
||||
RadioListTile<int>(
|
||||
title: const Text('SIM 1', style: TextStyle(color: Colors.white)),
|
||||
value: 0,
|
||||
groupValue: _selectedSim,
|
||||
onChanged: _onSimChanged,
|
||||
activeColor: Colors.blue,
|
||||
),
|
||||
RadioListTile<int>(
|
||||
title: const Text('SIM 2', style: TextStyle(color: Colors.white)),
|
||||
value: 1,
|
||||
groupValue: _selectedSim,
|
||||
onChanged: _onSimChanged,
|
||||
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) {
|
||||
subtitleParts.add(card.carrierName);
|
||||
}
|
||||
|
||||
if (subtitleParts.isEmpty) {
|
||||
subtitleParts.add('Slot ${card.slotIndex}');
|
||||
}
|
||||
|
||||
return subtitleParts.join(' • ');
|
||||
}
|
||||
}
|
@ -55,6 +55,7 @@ dependencies:
|
||||
encrypt: ^5.0.3
|
||||
uuid: ^4.5.1
|
||||
provider: ^6.1.2
|
||||
sim_data_new: ^1.0.1
|
||||
|
||||
intl: any
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Running Icing Dialer in STEALTH mode..."
|
||||
flutter run --dart-define=STEALTH=true
|
||||
flutter run --release --dart-define=STEALTH=true
|
||||
|
Loading…
Reference in New Issue
Block a user