Add Real-Time Bluetooth State Monitoring to OnboardingFlowScreen (#55)

* Add bluetooth monitor function to onboarding flow screen

* Adds function that checks bluetooth status in real time

* Add bluetooth monitor function to onboarding flow screen

* Refactor

* Comment button to manually check BT state

* Original commits

* feat(ui): Remove auto-trigger for Bluetooth enable prompt

* feat(ui): Remove auto-trigger for Bluetooth enable prompt

* feat(ui): Remove BluetoothStatusBanner. Pending approval on feature, will add in a new PR

* Refactor and fix conflicts

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
Eben Justice
2025-08-05 23:49:04 +02:00
committed by GitHub
co-authored by callebtc
parent 335f914f32
commit a0d1c6f77c
3 changed files with 104 additions and 24 deletions
@@ -11,11 +11,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.Lifecycle
import com.bitchat.android.mesh.BluetoothMeshService
@@ -60,16 +62,13 @@ class MainActivity : ComponentActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize core mesh service first
meshService = BluetoothMeshService(this)
// Initialize permission management
permissionManager = PermissionManager(this)
// Initialize core mesh service first
meshService = BluetoothMeshService(this)
bluetoothStatusManager = BluetoothStatusManager(
activity = this,
context = this,
@@ -124,6 +123,7 @@ class MainActivity : ComponentActivity() {
@Composable
private fun OnboardingFlowScreen() {
val context = LocalContext.current
val onboardingState by mainViewModel.onboardingState.collectAsState()
val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState()
val locationStatus by mainViewModel.locationStatus.collectAsState()
@@ -132,7 +132,29 @@ class MainActivity : ComponentActivity() {
val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState()
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)
Log.d("BluetoothStatusUI", "BroadcastReceiver unregistered")
} catch (e: IllegalStateException) {
Log.w("BluetoothStatusUI", "Receiver was not registered")
}
}
}
when (onboardingState) {
OnboardingState.CHECKING -> {
InitializingScreen()
@@ -209,7 +231,7 @@ class MainActivity : ComponentActivity() {
// Let ChatViewModel handle navigation state
val handled = chatViewModel.handleBackPressed()
if (!handled) {
// If ChatViewModel doesn't handle it, disable this callback
// If ChatViewModel doesn't handle it, disable this callback
// and let the system handle it (which will exit the app)
this.isEnabled = false
onBackPressedDispatcher.onBackPressed()
@@ -217,10 +239,9 @@ class MainActivity : ComponentActivity() {
}
}
}
// Add the callback - this will be automatically removed when the activity is destroyed
onBackPressedDispatcher.addCallback(this, backCallback)
ChatScreen(viewModel = chatViewModel)
}
@@ -568,7 +589,7 @@ class MainActivity : ComponentActivity() {
handleOnboardingFailed("Some permissions were revoked. Please grant all permissions to continue.")
return@launch
}
// Set up mesh service delegate and start services
meshService.delegate = chatViewModel
meshService.startServices()
@@ -626,7 +647,7 @@ class MainActivity : ComponentActivity() {
}
}
override fun onPause() {
override fun onPause() {
super.onPause()
// Only set background state if app is fully initialized
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
@@ -661,6 +682,7 @@ class MainActivity : ComponentActivity() {
}
}
override fun onDestroy() {
super.onDestroy()
@@ -144,18 +144,20 @@ private fun BluetoothDisabledContent(
)
}
OutlinedButton(
onClick = onRetry,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Check Again",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
),
modifier = Modifier.padding(vertical = 4.dp)
)
}
//Since we are automatically checking bluetooth state -- commented
// OutlinedButton(
// onClick = onRetry,
// modifier = Modifier.fillMaxWidth()
// ) {
// Text(
// text = "Check Again",
// style = MaterialTheme.typography.bodyMedium.copy(
// fontFamily = FontFamily.Monospace
// ),
// modifier = Modifier.padding(vertical = 4.dp)
// )
// }
}
}
}
@@ -2,12 +2,15 @@ package com.bitchat.android.onboarding
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
/**
* Manages Bluetooth enable/disable state and user prompts
@@ -203,6 +206,59 @@ class BluetoothStatusManager(
fun logBluetoothStatus() {
Log.d(TAG, getDiagnostics())
}
/**
* Monitors Bluetooth state changes in real-time and invokes the provided callback.
* Returns the BroadcastReceiver for manual un-registration.
*/
fun monitorBluetoothState(
context: Context,
bluetoothStatusManager: BluetoothStatusManager,
onBluetoothStateChanged: (BluetoothStatus) -> Unit
): BroadcastReceiver {
Log.d(TAG, "Starting Bluetooth State Monitoring")
if (!bluetoothStatusManager.isBluetoothSupported()) {
Log.e(TAG, "Bluetooth is not supported")
onBluetoothStateChanged(BluetoothStatus.NOT_SUPPORTED)
bluetoothStatusManager.handleBluetoothStatus(BluetoothStatus.NOT_SUPPORTED)
return object : BroadcastReceiver() { override fun onReceive(p0: Context?, p1: Intent?) {}
}
}
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_ON -> {
Log.d(TAG, "Bluetooth turned ON")
onBluetoothStateChanged(BluetoothStatus.ENABLED)
bluetoothStatusManager.handleBluetoothStatus(BluetoothStatus.ENABLED)
}
BluetoothAdapter.STATE_OFF -> {
Log.d(TAG, "Bluetooth turned OFF")
onBluetoothStateChanged(BluetoothStatus.DISABLED)
bluetoothStatusManager.onBluetoothDisabled("User has turned off their Blue")
}
BluetoothAdapter.STATE_TURNING_ON, BluetoothAdapter.STATE_OFF -> {
Log.d(TAG, "Bluetooth state transitioning: ${bluetoothStatusManager.getAdapterStateName(intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR))}")
}
}
}
}
//Register the receiver
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
//Check initial Bluetooth state
val initialStatus = bluetoothStatusManager.checkBluetoothStatus()
onBluetoothStateChanged(initialStatus)
return receiver
}
}
/**