Two reliability bugs the user-walkthrough audit caught:
1. Foreground service flickered on every transport blip. The
LaunchedEffect keyed on `isConnected = ui.connection is
Connected`, so a Reconnecting state stopped the service
mid-blip and restarted it on recovery. Beyond the audible
dropout from losing the wake-lock, Android 14+ doesn't
reliably let us re-promote to FOREGROUND_SERVICE_TYPE_MICROPHONE
after losing it — risking a permanent broadcast-side regression
after a single relay hiccup. Treat Reconnecting as "still
live" for the service decision.
2. TalkRow disappeared mid-broadcast on transient listener
disconnect. The speaker session is INDEPENDENT of the
listener — the user could be broadcasting cleanly while the
listener is Reconnecting, but our `if (ui.connection !is
Connected) return` early-exited the entire row, so the user
could neither mute their mic nor stop their broadcast until
the listener recovered. Switch the gate to "user can act on
broadcast state" — Connected listener OR an in-flight
broadcast handle (Connecting/Broadcasting).
Same logic applied to the PIP-entry gate so onUserLeaveHint
during a transient blip doesn't lock the user out of PIP.
This commit is contained in:
+20
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user