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
This commit is contained in:
Claude
2026-04-02 21:01:31 +00:00
parent 80151965c7
commit 989f10c6e2
3 changed files with 35 additions and 19 deletions
@@ -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,
@@ -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
@@ -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"
}
}