diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt index 4f7c611..53121bf 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt @@ -6,11 +6,8 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.database.Cursor -import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.provider.CallLog import android.telecom.TelecomManager import android.util.Log @@ -29,18 +26,22 @@ class MainActivity : FlutterActivity() { private val TAG = "MainActivity" private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001 private val REQUEST_CODE_CALL_LOG_PERMISSION = 1002 - private var pendingIncomingCall: Pair? = null // Store incoming call data + private var pendingIncomingCall: Pair? = null + private var wasPhoneLocked: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "onCreate started") - Log.d(TAG, "Waiting for Flutter to signal permissions") + wasPhoneLocked = intent.getBooleanExtra("wasPhoneLocked", false) + Log.d(TAG, "Was phone locked at start: $wasPhoneLocked") handleIncomingCallIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) + wasPhoneLocked = intent.getBooleanExtra("wasPhoneLocked", false) + Log.d(TAG, "onNewIntent, wasPhoneLocked: $wasPhoneLocked") handleIncomingCallIntent(intent) } @@ -54,13 +55,22 @@ class MainActivity : FlutterActivity() { when (call.method) { "permissionsGranted" -> { Log.d(TAG, "Received permissionsGranted from Flutter") + pendingIncomingCall?.let { (phoneNumber, showScreen) -> + if (showScreen) { + MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf( + "phoneNumber" to phoneNumber, + "wasPhoneLocked" to wasPhoneLocked + )) + pendingIncomingCall = null + } + } checkAndRequestDefaultDialer() result.success(true) } "makeGsmCall" -> { val phoneNumber = call.argument("phoneNumber") if (phoneNumber != null) { - val success = CallService.makeGsmCall(this, phoneNumber) // Use CallService + val success = CallService.makeGsmCall(this, phoneNumber) if (success) { result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber)) } else { @@ -75,12 +85,17 @@ class MainActivity : FlutterActivity() { it.disconnect() Log.d(TAG, "Call disconnected") MyInCallService.channel?.invokeMethod("callEnded", mapOf( - "callId" to it.details.handle.toString() + "callId" to it.details.handle.toString(), + "wasPhoneLocked" to wasPhoneLocked )) true } ?: false if (success) { result.success(mapOf("status" to "ended")) + if (wasPhoneLocked) { + Log.d(TAG, "Finishing and removing task after hangup, phone was locked") + finishAndRemoveTask() + } } else { Log.w(TAG, "No active call to hang up") result.error("HANGUP_FAILED", "No active call to hang up", null) @@ -99,15 +114,11 @@ class MainActivity : FlutterActivity() { result.error("ANSWER_FAILED", "No active call to answer", null) } } - "flutterReady" -> { // New method to signal Flutter is initialized - Log.d(TAG, "Flutter is ready") - pendingIncomingCall?.let { (phoneNumber, showScreen) -> - if (showScreen) { - MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf( - "phoneNumber" to phoneNumber - )) - pendingIncomingCall = null // Clear after handling - } + "callEndedFromFlutter" -> { + Log.d(TAG, "Call ended from Flutter, wasPhoneLocked: $wasPhoneLocked") + if (wasPhoneLocked) { + finishAndRemoveTask() + Log.d(TAG, "Finishing and removing task after call ended, phone was locked") } result.success(true) } @@ -148,8 +159,6 @@ class MainActivity : FlutterActivity() { val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER) startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER) Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+") - } else { - Log.d(TAG, "RoleManager: Available=${roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)}, Held=${roleManager.isRoleHeld(RoleManager.ROLE_DIALER)}") } } else { val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER) @@ -228,14 +237,14 @@ class MainActivity : FlutterActivity() { if (it.getBooleanExtra("isIncomingCall", false)) { val phoneNumber = it.getStringExtra("phoneNumber") val showScreen = it.getBooleanExtra("showIncomingCallScreen", false) - Log.d(TAG, "Received incoming call intent for $phoneNumber, showScreen=$showScreen") + Log.d(TAG, "Received incoming call intent for $phoneNumber, showScreen=$showScreen, wasPhoneLocked=$wasPhoneLocked") if (showScreen) { if (MyInCallService.channel != null) { MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf( - "phoneNumber" to phoneNumber + "phoneNumber" to phoneNumber, + "wasPhoneLocked" to wasPhoneLocked )) } else { - // Store the intent data if Flutter isn't ready yet pendingIncomingCall = Pair(phoneNumber, true) Log.d(TAG, "Flutter channel not ready, storing pending call: $phoneNumber") } diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt index 580b13e..b5a4c8a 100644 --- a/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt +++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt @@ -1,5 +1,6 @@ package com.icing.dialer.services +import android.app.KeyguardManager import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -20,6 +21,7 @@ class MyInCallService : InCallService() { private const val TAG = "MyInCallService" private const val NOTIFICATION_CHANNEL_ID = "incoming_call_channel" private const val NOTIFICATION_ID = 1 + var wasPhoneLocked: Boolean = false } private val callCallback = object : Call.Callback() { @@ -36,13 +38,20 @@ class MyInCallService : InCallService() { Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}") channel?.invokeMethod("callStateChanged", mapOf( "callId" to call.details.handle.toString(), - "state" to stateStr + "state" to stateStr, + "wasPhoneLocked" to wasPhoneLocked )) if (state == Call.STATE_RINGING) { + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + wasPhoneLocked = keyguardManager.isKeyguardLocked + Log.d(TAG, "Phone locked at ringing: $wasPhoneLocked") showIncomingCallScreen(call.details.handle.toString().replace("tel:", "")) } else if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) { - Log.d(TAG, "Call ended: ${call.details.handle}") - channel?.invokeMethod("callEnded", mapOf("callId" to call.details.handle.toString())) + Log.d(TAG, "Call ended: ${call.details.handle}, wasPhoneLocked: $wasPhoneLocked") + channel?.invokeMethod("callEnded", mapOf( + "callId" to call.details.handle.toString(), + "wasPhoneLocked" to wasPhoneLocked + )) currentCall = null cancelNotification() } @@ -64,6 +73,9 @@ class MyInCallService : InCallService() { "state" to stateStr )) if (stateStr == "ringing") { + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + wasPhoneLocked = keyguardManager.isKeyguardLocked + Log.d(TAG, "Phone locked at call added: $wasPhoneLocked") showIncomingCallScreen(call.details.handle.toString().replace("tel:", "")) } call.registerCallback(callCallback) @@ -71,9 +83,12 @@ class MyInCallService : InCallService() { override fun onCallRemoved(call: Call) { super.onCallRemoved(call) - Log.d(TAG, "Call removed: ${call.details.handle}") + Log.d(TAG, "Call removed: ${call.details.handle}, wasPhoneLocked: $wasPhoneLocked") call.unregisterCallback(callCallback) - channel?.invokeMethod("callRemoved", mapOf("callId" to call.details.handle.toString())) + channel?.invokeMethod("callRemoved", mapOf( + "callId" to call.details.handle.toString(), + "wasPhoneLocked" to wasPhoneLocked + )) currentCall = null cancelNotification() } @@ -90,41 +105,47 @@ class MyInCallService : InCallService() { putExtra("phoneNumber", phoneNumber) putExtra("isIncomingCall", true) putExtra("showIncomingCallScreen", true) + putExtra("wasPhoneLocked", wasPhoneLocked) } - startActivity(intent) - Log.d(TAG, "Launched MainActivity to show incoming call screen for $phoneNumber") - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - "Incoming Calls", - NotificationManager.IMPORTANCE_HIGH - ).apply { - description = "Notifications for incoming calls" - enableVibration(true) - setShowBadge(true) + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + if (keyguardManager.isKeyguardLocked) { + startActivity(intent) + Log.d(TAG, "Launched MainActivity directly for locked screen, phoneNumber: $phoneNumber") + } else { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + "Incoming Calls", + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Notifications for incoming calls" + enableVibration(true) + setShowBadge(true) + } + notificationManager.createNotificationChannel(channel) } - notificationManager.createNotificationChannel(channel) + + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0) + ) + + val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_alert) + .setContentTitle("Incoming Call") + .setContentText("Call from $phoneNumber") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setFullScreenIntent(pendingIntent, true) + .setAutoCancel(true) + .setOngoing(true) + .build() + + startActivity(intent) + notificationManager.notify(NOTIFICATION_ID, notification) + Log.d(TAG, "Launched MainActivity with notification for unlocked screen, phoneNumber: $phoneNumber") } - - val pendingIntent = PendingIntent.getActivity( - this, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0) - ) - - val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(android.R.drawable.ic_dialog_alert) - .setContentTitle("Incoming Call") - .setContentText("Call from $phoneNumber") - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .setOngoing(true) - .build() - - notificationManager.notify(NOTIFICATION_ID, notification) - Log.d(TAG, "Notification shown for incoming call from $phoneNumber") } private fun cancelNotification() { diff --git a/dialer/lib/services/call_service.dart b/dialer/lib/services/call_service.dart index ea8b6bf..e03c652 100644 --- a/dialer/lib/services/call_service.dart +++ b/dialer/lib/services/call_service.dart @@ -7,17 +7,14 @@ class CallService { static const MethodChannel _channel = MethodChannel('call_service'); static String? currentPhoneNumber; static bool _isCallPageVisible = false; + static Map? _pendingCall; + static bool wasPhoneLocked = false; static final GlobalKey navigatorKey = GlobalKey(); CallService() { _channel.setMethodCallHandler((call) async { - final context = navigatorKey.currentContext; - if (context == null) { - print('CallService: Navigator context is null, cannot navigate'); - return; - } - + print('CallService: Handling method call: ${call.method}'); switch (call.method) { case "callAdded": final phoneNumber = call.arguments["callId"] as String; @@ -25,43 +22,84 @@ class CallService { currentPhoneNumber = phoneNumber.replaceFirst('tel:', ''); print('CallService: Call added, number: $currentPhoneNumber, state: $state'); if (state == "ringing") { - _navigateToIncomingCallPage(context); + _handleIncomingCall(phoneNumber); } else { - _navigateToCallPage(context); + _navigateToCallPage(); } break; case "callStateChanged": final state = call.arguments["state"] as String; - print('CallService: State changed to $state'); + wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; + print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked'); if (state == "disconnected" || state == "disconnecting") { - _closeCallPage(context); + _closeCallPage(); + if (wasPhoneLocked) { + _channel.invokeMethod("callEndedFromFlutter"); + } } else if (state == "active" || state == "dialing") { - _navigateToCallPage(context); + _navigateToCallPage(); } else if (state == "ringing") { - _navigateToIncomingCallPage(context); + final phoneNumber = call.arguments["callId"] as String; + _handleIncomingCall(phoneNumber.replaceFirst('tel:', '')); } break; case "callEnded": case "callRemoved": - print('CallService: Call ended/removed'); - _closeCallPage(context); + wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; + print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked'); + _closeCallPage(); + if (wasPhoneLocked) { + _channel.invokeMethod("callEndedFromFlutter"); + } currentPhoneNumber = null; break; case "incomingCallFromNotification": final phoneNumber = call.arguments["phoneNumber"] as String; + wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false; currentPhoneNumber = phoneNumber; - print('CallService: Incoming call from notification: $phoneNumber'); - _navigateToIncomingCallPage(context); + print('CallService: Incoming call from notification: $phoneNumber, wasPhoneLocked: $wasPhoneLocked'); + _handleIncomingCall(phoneNumber); break; } }); - - // Signal Flutter is ready - WidgetsFlutterBinding.ensureInitialized(); - _channel.invokeMethod("flutterReady"); } - void _navigateToCallPage(BuildContext context) { + void _handleIncomingCall(String phoneNumber) { + final context = navigatorKey.currentContext; + if (context == null) { + print('CallService: Context is null, queuing incoming call: $phoneNumber'); + _pendingCall = {"phoneNumber": phoneNumber}; + Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); + } else { + _navigateToIncomingCallPage(context); + } + } + + void _checkPendingCall() { + if (_pendingCall != null) { + final context = navigatorKey.currentContext; + if (context != null) { + print('CallService: Processing queued call: ${_pendingCall!["phoneNumber"]}'); + currentPhoneNumber = _pendingCall!["phoneNumber"]; + _navigateToIncomingCallPage(context); + _pendingCall = null; + } else { + print('CallService: Context still null, retrying...'); + Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall()); + } + } + } + + void _navigateToCallPage() { + final context = navigatorKey.currentContext; + if (context == null) { + print('CallService: Cannot navigate to CallPage, context is null'); + return; + } + if (_isCallPageVisible) { + print('CallService: CallPage already visible, skipping navigation'); + return; + } print('CallService: Navigating to CallPage'); Navigator.push( context, @@ -81,6 +119,10 @@ class CallService { } void _navigateToIncomingCallPage(BuildContext context) { + if (_isCallPageVisible) { + print('CallService: IncomingCallPage already visible, skipping navigation'); + return; + } print('CallService: Navigating to IncomingCallPage'); Navigator.push( context, @@ -99,8 +141,13 @@ class CallService { _isCallPageVisible = true; } - void _closeCallPage(BuildContext context) { - print('CallService: Attempting to close call page, _isCallPageVisible: $_isCallPageVisible'); + void _closeCallPage() { + final context = navigatorKey.currentContext; + if (context == null) { + 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); @@ -133,7 +180,7 @@ class CallService { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error making call: $e")), ); - rethrow; + return {"status": "error", "message": e.toString()}; } } @@ -154,7 +201,7 @@ class CallService { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Error hanging up call: $e")), ); - rethrow; + return {"status": "error", "message": e.toString()}; } } } \ No newline at end of file