diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomActivityContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomActivityContent.kt index 487f20d8f..d54ea0328 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomActivityContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomActivityContent.kt @@ -245,7 +245,13 @@ private fun AudioRoomActivityBody( // Push mute + connected state up so the Activity can rebuild PIP // RemoteActions and gate `enterPictureInPictureMode` on Connected. LaunchedEffect(ui.isMuted) { onMuteState(ui.isMuted) } - val isConnectedNow = ui.connection is ConnectionUiState.Connected + // PIP entry gate — treat Reconnecting as "still in the room" so a + // mid-room network blip doesn't lock the user out of PIP. The + // listener's hot SubscribeHandle pump preserves audio across the + // cutover; PIP would otherwise un-arm and re-arm spuriously. + val isConnectedNow = + ui.connection is ConnectionUiState.Connected || + ui.connection is ConnectionUiState.Reconnecting LaunchedEffect(isConnectedNow) { onConnectedChange(isConnectedNow) } // Forward PIP-overlay mute taps into the VM. Per-Activity SharedFlow @@ -323,13 +329,22 @@ private fun AudioRoomActivityBody( // Foreground service lifecycle — the Activity is the lifecycle anchor; // the service exists for screen-locked / activity-stopped audio. + // Treat Reconnecting as "still live" so a transient transport blip + // doesn't drop the wake-lock + foreground type. On Android 14+ + // re-promoting to FOREGROUND_SERVICE_TYPE_MICROPHONE after losing + // it isn't always allowed; keeping the service running across the + // wrapper's recovery window is both gentler on the user (no audio + // dropout from the OS killing us) and safer against the platform + // permission model. val context = LocalContext.current - val isConnected = ui.connection is ConnectionUiState.Connected + val isLive = + ui.connection is ConnectionUiState.Connected || + ui.connection is ConnectionUiState.Reconnecting val isBroadcasting = ui.broadcast is BroadcastUiState.Broadcasting - LaunchedEffect(isConnected, isBroadcasting) { + LaunchedEffect(isLive, isBroadcasting) { when { - isConnected && isBroadcasting -> AudioRoomForegroundService.promoteToMicrophone(context) - isConnected -> AudioRoomForegroundService.startListening(context) + isLive && isBroadcasting -> AudioRoomForegroundService.promoteToMicrophone(context) + isLive -> AudioRoomForegroundService.startListening(context) else -> AudioRoomForegroundService.stop(context) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt index cd692f477..41a99ca97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/audiorooms/room/AudioRoomFullScreen.kt @@ -368,7 +368,16 @@ private fun TalkRow( ui: AudioRoomUiState, speakerPubkeyHex: String, ) { - if (ui.connection !is ConnectionUiState.Connected) return + // Render whenever the user can act on broadcast state. The + // speaker session is independent of the listener: a transient + // listener Reconnecting must NOT hide the mic-mute / Stop + // controls if the broadcast is in flight, otherwise the user + // can't pause their own mic during a network blip. + val canAct = + ui.connection is ConnectionUiState.Connected || + ui.broadcast is BroadcastUiState.Broadcasting || + ui.broadcast is BroadcastUiState.Connecting + if (!canAct) return val context = LocalContext.current var permissionDenied by rememberSaveable { mutableStateOf(false) } val permissionLauncher =