From 989f10c6e2c9d861dd7a41fdce19e5dbed9b0f8e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 21:01:31 +0000 Subject: [PATCH] fix: notification Accept opens CallActivity directly (Android 12+) Android 12+ blocks starting activities from BroadcastReceivers used as notification trampolines. The Accept action now uses PendingIntent.getActivity to launch CallActivity directly with EXTRA_ACCEPT_CALL. CallActivity.onCreate/onNewIntent checks this extra and accepts the incoming call. The Reject action remains as a BroadcastReceiver since it doesn't need to start an activity. https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ --- .../notifications/NotificationUtils.kt | 9 +++++--- .../amethyst/ui/call/CallActivity.kt | 23 +++++++++++++++++++ .../ui/call/CallNotificationReceiver.kt | 22 +++++------------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt index 3f92bd0ca..a669d409b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationUtils.kt @@ -561,13 +561,16 @@ object NotificationUtils { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) + // Accept launches CallActivity directly (not via BroadcastReceiver) + // to comply with Android 12+ notification trampoline restrictions. val acceptIntent = - Intent(applicationContext, com.vitorpamplona.amethyst.ui.call.CallNotificationReceiver::class.java).apply { - action = com.vitorpamplona.amethyst.ui.call.CallNotificationReceiver.ACTION_ACCEPT_CALL + Intent(applicationContext, com.vitorpamplona.amethyst.ui.call.CallActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP + putExtra(com.vitorpamplona.amethyst.ui.call.CallActivity.EXTRA_ACCEPT_CALL, true) } val acceptPendingIntent = - PendingIntent.getBroadcast( + PendingIntent.getActivity( applicationContext, CALL_NOTIFICATION_ID + 1, acceptIntent, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt index 9ac561e48..071eb11e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallActivity.kt @@ -97,6 +97,7 @@ class CallActivity : AppCompatActivity() { registerPipReceiver() observeVideoStateForPip(callController) + handleAcceptCallIntent(intent) setContent { AmethystTheme { @@ -111,6 +112,27 @@ class CallActivity : AppCompatActivity() { } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleAcceptCallIntent(intent) + } + + private fun handleAcceptCallIntent(intent: Intent?) { + if (intent?.getBooleanExtra(EXTRA_ACCEPT_CALL, false) != true) return + // Clear the extra so it doesn't re-trigger on config changes + intent.removeExtra(EXTRA_ACCEPT_CALL) + + com.vitorpamplona.amethyst.service.notifications.NotificationUtils + .cancelCallNotification(this) + + val callController = ActiveCallHolder.callController ?: return + val callManager = ActiveCallHolder.callManager ?: return + val state = callManager.state.value + if (state is CallState.IncomingCall) { + callController.acceptIncomingCall(state.sdpOffer) + } + } + override fun onUserLeaveHint() { super.onUserLeaveHint() enterPipIfActive() @@ -274,6 +296,7 @@ class CallActivity : AppCompatActivity() { } companion object { + const val EXTRA_ACCEPT_CALL = "com.vitorpamplona.amethyst.EXTRA_ACCEPT_CALL" private const val ACTION_PIP_HANGUP = "com.vitorpamplona.amethyst.PIP_HANGUP" private const val ACTION_PIP_TOGGLE_MUTE = "com.vitorpamplona.amethyst.PIP_TOGGLE_MUTE" private const val PIP_HANGUP_REQUEST_CODE = 0x60001 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallNotificationReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallNotificationReceiver.kt index ac6f7e1ca..d9f1594a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallNotificationReceiver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallNotificationReceiver.kt @@ -23,12 +23,17 @@ package com.vitorpamplona.amethyst.ui.call import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.vitorpamplona.amethyst.commons.call.CallState import com.vitorpamplona.amethyst.service.notifications.NotificationUtils import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +/** + * Handles the Reject action from the incoming call notification. + * + * The Accept action launches [CallActivity] directly via PendingIntent.getActivity + * to comply with Android 12+ notification trampoline restrictions. + */ class CallNotificationReceiver : BroadcastReceiver() { @OptIn(DelicateCoroutinesApi::class) override fun onReceive( @@ -36,20 +41,6 @@ class CallNotificationReceiver : BroadcastReceiver() { intent: Intent, ) { when (intent.action) { - ACTION_ACCEPT_CALL -> { - NotificationUtils.cancelCallNotification(context) - - val callController = ActiveCallHolder.callController - val callManager = ActiveCallHolder.callManager - if (callController == null || callManager == null) return - - val state = callManager.state.value - if (state is CallState.IncomingCall) { - callController.acceptIncomingCall(state.sdpOffer) - CallActivity.launch(context) - } - } - ACTION_REJECT_CALL -> { NotificationUtils.cancelCallNotification(context) @@ -62,7 +53,6 @@ class CallNotificationReceiver : BroadcastReceiver() { } companion object { - const val ACTION_ACCEPT_CALL = "com.vitorpamplona.amethyst.ACCEPT_CALL" const val ACTION_REJECT_CALL = "com.vitorpamplona.amethyst.REJECT_CALL" } }