diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivity.kt index 7721f0114..1ff53d481 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivity.kt @@ -83,12 +83,31 @@ class NestActivity : AppCompatActivity() { } ACTION_PIP_LEAVE -> { + // Tear down the speaker session + listener BEFORE + // finish() so AudioRecord (and the system mic + // indicator) releases promptly. onCleared() alone + // runs late in the destroy lifecycle. + pipCleanupAction?.invoke() finish() } } } } + /** + * Wired by [NestActivityBody] via a DisposableEffect. Invokes + * `viewModel.leave()` so the broadcast handle + listener close + * route through `cleanupScope`/GlobalScope and survive Activity + * destruction. Called from PIP close paths (the overlay's Leave + * action and a swipe-dismiss while in PIP). + */ + @Volatile + private var pipCleanupAction: (() -> Unit)? = null + + fun setPipCleanupAction(action: (() -> Unit)?) { + pipCleanupAction = action + } + private val _toggleMuteSignal = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) @@ -180,6 +199,22 @@ class NestActivity : AppCompatActivity() { enterPip() } + /** + * Detects a PIP swipe-dismiss. When the user dismisses the floating + * window the system finishes the activity, so [isFinishing] is true + * by the time onStop runs. Tearing down here (rather than waiting + * for onCleared) keeps AudioRecord + the system mic indicator from + * lingering through the destruction queue. Non-PIP onStop (Home / + * Recents while not in PIP) is a no-op so backgrounding doesn't + * teardown. + */ + override fun onStop() { + if (isInPipMode.value && isFinishing) { + pipCleanupAction?.invoke() + } + super.onStop() + } + /** * Enter Picture-in-Picture if the audio session is live and the * system supports PIP. Called both by [onUserLeaveHint] (Home / diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt index 11d37caf6..065e38e25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -39,6 +40,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -274,6 +276,19 @@ private fun NestActivityBody( PipBridge(ui, pipMuteSignal, viewModel, onMuteState, onConnectedChange) NestForegroundServiceLifecycle(ui) + // Tell NestActivity how to tear the session down when the user + // closes PIP. Both close paths in PIP — the overlay's Leave + // action and a swipe-dismiss while in PIP — invoke this so the + // speaker handle close (releases AudioRecord and clears the + // system mic indicator) and the listener close run on + // cleanupScope/GlobalScope before the activity destruction + // queue eats viewModelScope. + val activity = LocalContext.current as? NestActivity + DisposableEffect(activity, viewModel) { + activity?.setPipCleanupAction { viewModel.leave() } + onDispose { activity?.setPipCleanupAction(null) } + } + // Hand-raise is screen-local UI state; presence-publish picks it // up via the heartbeat and emits onstage / muted / publishing // alongside. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt index 4e01702aa..77c241ec8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/screen/NestFullScreen.kt @@ -394,6 +394,15 @@ internal fun NestFullScreen( TextButton( onClick = { showHostLeaveConfirm = false + // Tear down the speaker session + listener before + // finishing so the AudioRecord (and the system mic + // indicator) releases promptly. Same reasoning as + // the Close the Room path — onCleared() runs late + // in the destroy lifecycle and can leave the mic + // held while the activity is queued for destruction. + // Unlike Close the Room, this path leaves the + // kind-30312 meeting space open for other users. + viewModel.leave() onLeave() }, ) {