feat: incoming call notification works in any scenario
This commit is contained in:
parent
8e603adc3b
commit
0a56dc2f83
@ -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<String?, Boolean>? = null // Store incoming call data
|
||||
private var pendingIncomingCall: Pair<String?, Boolean>? = 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<String>("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")
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -7,17 +7,14 @@ class CallService {
|
||||
static const MethodChannel _channel = MethodChannel('call_service');
|
||||
static String? currentPhoneNumber;
|
||||
static bool _isCallPageVisible = false;
|
||||
static Map<String, dynamic>? _pendingCall;
|
||||
static bool wasPhoneLocked = false;
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
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()};
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user