fix(nav): host the bluetooth receiver above the destinations

The ACTION_STATE_CHANGED receiver was registered from a DisposableEffect
inside OnboardingFlowScreen. That was harmless while the when branch kept
that composable on screen for chat too, but chat is its own destination
now, so reaching COMPLETE took the onboarding entry out of composition and
unregistered the only receiver watching the adapter. Turning Bluetooth off
from Quick Settings while sitting in chat then went unnoticed, and the
onResume fallback does not fire without a lifecycle transition.

Reproduced on a Pixel 9a. Disabling Bluetooth on the chat route logged
nothing on this branch, against main:

  D BluetoothStatusManager: Bluetooth turned OFF
  W MainActivity: Bluetooth disabled or failed: User has turned off their Blue

Hoisted next to NavDisplay, which lives for as long as the Activity's
composition, so it no longer depends on which destination is showing. The
callback also reads onboardingState from the ViewModel rather than the
value captured when the effect ran, which the effect's keys never
invalidated.

Same two lines now log on the chat route, and 591 unit tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26
This commit is contained in:
Moe Hamade 2026-08-26 01:38:07 +03:00
parent c0a8a3c6df
commit 1835281fc9

View File

@ -179,6 +179,32 @@ class MainActivity : OrientationAwareActivity() {
val onboardingState by mainViewModel.onboardingState.collectAsState()
val root = rootRouteFor(onboardingState)
// Hosted here rather than in the onboarding destination: turning
// Bluetooth off has to be noticed on the chat route too, and a
// destination-scoped receiver is unregistered when its entry leaves
// the back stack.
val context = LocalContext.current
DisposableEffect(context, bluetoothStatusManager) {
val receiver = bluetoothStatusManager.monitorBluetoothState(
context = context,
bluetoothStatusManager = bluetoothStatusManager,
onBluetoothStateChanged = { status ->
if (status == BluetoothStatus.ENABLED &&
mainViewModel.onboardingState.value == OnboardingState.BLUETOOTH_CHECK
) {
checkBluetoothAndProceed()
}
}
)
onDispose {
try {
context.unregisterReceiver(receiver)
} catch (e: IllegalStateException) {
Log.w("BluetoothStatusUI", "Receiver was not registered")
}
}
}
// Keyed on root, so this fires only when the app crosses between
// onboarding and chat — not on every step within onboarding.
// resetTo rather than goTo: onboarding must not be reachable with
@ -246,7 +272,6 @@ class MainActivity : OrientationAwareActivity() {
@Composable
private fun OnboardingFlowScreen(modifier: Modifier = Modifier) {
val context = LocalContext.current
val onboardingState by mainViewModel.onboardingState.collectAsState()
val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState()
val locationStatus by mainViewModel.locationStatus.collectAsState()
@ -256,27 +281,6 @@ class MainActivity : OrientationAwareActivity() {
val isLocationLoading by mainViewModel.isLocationLoading.collectAsState()
val isBatteryOptimizationLoading by mainViewModel.isBatteryOptimizationLoading.collectAsState()
DisposableEffect(context, bluetoothStatusManager) {
val receiver = bluetoothStatusManager.monitorBluetoothState(
context = context,
bluetoothStatusManager = bluetoothStatusManager,
onBluetoothStateChanged = { status ->
if (status == BluetoothStatus.ENABLED && onboardingState == OnboardingState.BLUETOOTH_CHECK) {
checkBluetoothAndProceed()
}
}
)
onDispose {
try {
context.unregisterReceiver(receiver)
} catch (e: IllegalStateException) {
Log.w("BluetoothStatusUI", "Receiver was not registered")
}
}
}
when (onboardingState) {
OnboardingState.PERMISSION_REQUESTING -> {
InitializingScreen(modifier)