From 80151965c7415925b82ed0598fbbcea3d1087b73 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 20:55:00 +0000 Subject: [PATCH] fix: critical lifecycle bugs found during code review 1. onStop() no longer hangs up call when user switches apps - Added wasInPipMode flag to track if the activity was ever in PiP - onStop only hangs up + finishes when PiP was dismissed (swiped away), not when the user simply presses Home from the full-screen call UI - Pressing Home from full-screen enters PiP via onUserLeaveHint 2. Removed triple-hangup from overlapping lifecycle methods - onPictureInPictureModeChanged now only updates UI state - onStop handles PiP dismissal only - onDestroy only cleans up the PiP receiver (hangup already handled by CallController's state collector on Ended) 3. CallNotificationReceiver guards against stale notifications - Returns early if callManager/callController are null - Only launches CallActivity if the call is still IncomingCall - Cancels notification before any other action to prevent re-taps https://claude.ai/code/session_01Ak5tTkujpjNG1r5ASuPipZ --- .../amethyst/ui/call/CallActivity.kt | 31 +++++++------------ .../ui/call/CallNotificationReceiver.kt | 23 ++++++++------ 2 files changed, 26 insertions(+), 28 deletions(-) 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 8df8478cb..9ac561e48 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 @@ -46,6 +46,10 @@ import kotlinx.coroutines.launch class CallActivity : AppCompatActivity() { val isInPipMode = mutableStateOf(false) + // Tracks whether we entered PiP at least once, so we can distinguish + // "user swiped PiP away" from "user pressed Home from full-screen call". + private var wasInPipMode = false + private val pipActionReceiver = object : BroadcastReceiver() { @OptIn(DelicateCoroutinesApi::class) @@ -112,26 +116,24 @@ class CallActivity : AppCompatActivity() { enterPipIfActive() } - @OptIn(DelicateCoroutinesApi::class) override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { super.onPictureInPictureModeChanged(isInPictureInPictureMode) isInPipMode.value = isInPictureInPictureMode - if (!isInPictureInPictureMode) { - // User pressed the X button on PiP or expanded it. - // If the activity is finishing (user dismissed PiP), hang up. - if (isFinishing) { - GlobalScope.launch { ActiveCallHolder.callManager?.hangup() } - } + if (isInPictureInPictureMode) { + wasInPipMode = true } } @OptIn(DelicateCoroutinesApi::class) override fun onStop() { super.onStop() - // When PiP is dismissed (X button), Android stops the activity. - // If we're not in PiP anymore and the activity is stopping, hang up and finish. - if (!isInPictureInPictureMode) { + // Only hang up if the user dismissed PiP (swiped it away). + // When PiP is dismissed, Android calls onStop with isInPictureInPictureMode == false + // after the activity was previously in PiP mode. + // We must NOT hang up when the user simply presses Home from the full-screen + // call UI (that enters PiP via onUserLeaveHint instead). + if (wasInPipMode && !isInPictureInPictureMode) { val state = ActiveCallHolder.callManager?.state?.value if (state is CallState.Connected || state is CallState.Connecting || state is CallState.Offering) { GlobalScope.launch { ActiveCallHolder.callManager?.hangup() } @@ -140,17 +142,8 @@ class CallActivity : AppCompatActivity() { } } - @OptIn(DelicateCoroutinesApi::class) override fun onDestroy() { unregisterPipReceiver() - - // If the activity is being destroyed while still in an active call - // (e.g. user swiped PiP away), hang up the call. - val state = ActiveCallHolder.callManager?.state?.value - if (state is CallState.Connected || state is CallState.Connecting || state is CallState.Offering) { - GlobalScope.launch { ActiveCallHolder.callManager?.hangup() } - } - super.onDestroy() } 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 1c4f62425..ac6f7e1ca 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,6 +23,7 @@ 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 @@ -36,22 +37,26 @@ class CallNotificationReceiver : BroadcastReceiver() { ) { when (intent.action) { ACTION_ACCEPT_CALL -> { + NotificationUtils.cancelCallNotification(context) + val callController = ActiveCallHolder.callController val callManager = ActiveCallHolder.callManager - val state = callManager?.state?.value - if (state is com.vitorpamplona.amethyst.commons.call.CallState.IncomingCall) { - callController?.acceptIncomingCall(state.sdpOffer) + if (callController == null || callManager == null) return + + val state = callManager.state.value + if (state is CallState.IncomingCall) { + callController.acceptIncomingCall(state.sdpOffer) + CallActivity.launch(context) } - NotificationUtils.cancelCallNotification(context) - CallActivity.launch(context) } ACTION_REJECT_CALL -> { - val callManager = ActiveCallHolder.callManager - GlobalScope.launch { - callManager?.rejectCall() - } NotificationUtils.cancelCallNotification(context) + + val callManager = ActiveCallHolder.callManager ?: return + GlobalScope.launch { + callManager.rejectCall() + } } } }