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
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+14
-9
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user