From 1835281fc95fe375655d63f32cf4f5e943cd76d2 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:38:07 +0300 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01MdKgoKZbL5K1wsn1WsES26 --- .../java/com/bitchat/android/MainActivity.kt | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 143d2f3e..d0047923 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -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)