check bluetooth

This commit is contained in:
callebtc
2025-07-10 13:49:52 +02:00
parent 441496e36a
commit 3cee97ccc7
3 changed files with 573 additions and 1 deletions
@@ -28,14 +28,18 @@ class MainActivity : ComponentActivity() {
private lateinit var permissionManager: PermissionManager private lateinit var permissionManager: PermissionManager
private lateinit var onboardingCoordinator: OnboardingCoordinator private lateinit var onboardingCoordinator: OnboardingCoordinator
private lateinit var bluetoothStatusManager: BluetoothStatusManager
private val chatViewModel: ChatViewModel by viewModels() private val chatViewModel: ChatViewModel by viewModels()
// UI state for onboarding flow // UI state for onboarding flow
private var onboardingState by mutableStateOf(OnboardingState.CHECKING) private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED)
private var errorMessage by mutableStateOf("") private var errorMessage by mutableStateOf("")
private var isBluetoothLoading by mutableStateOf(false)
enum class OnboardingState { enum class OnboardingState {
CHECKING, CHECKING,
BLUETOOTH_CHECK,
PERMISSION_EXPLANATION, PERMISSION_EXPLANATION,
PERMISSION_REQUESTING, PERMISSION_REQUESTING,
INITIALIZING, INITIALIZING,
@@ -48,6 +52,12 @@ class MainActivity : ComponentActivity() {
// Initialize permission management // Initialize permission management
permissionManager = PermissionManager(this) permissionManager = PermissionManager(this)
bluetoothStatusManager = BluetoothStatusManager(
activity = this,
context = this,
onBluetoothEnabled = ::handleBluetoothEnabled,
onBluetoothDisabled = ::handleBluetoothDisabled
)
onboardingCoordinator = OnboardingCoordinator( onboardingCoordinator = OnboardingCoordinator(
activity = this, activity = this,
permissionManager = permissionManager, permissionManager = permissionManager,
@@ -77,6 +87,20 @@ class MainActivity : ComponentActivity() {
InitializingScreen() InitializingScreen()
} }
OnboardingState.BLUETOOTH_CHECK -> {
BluetoothCheckScreen(
status = bluetoothStatus,
onEnableBluetooth = {
isBluetoothLoading = true
bluetoothStatusManager.requestEnableBluetooth()
},
onRetry = {
checkBluetoothAndProceed()
},
isLoading = isBluetoothLoading
)
}
OnboardingState.PERMISSION_EXPLANATION -> { OnboardingState.PERMISSION_EXPLANATION -> {
PermissionExplanationScreen( PermissionExplanationScreen(
permissionCategories = permissionManager.getCategorizedPermissions(), permissionCategories = permissionManager.getCategorizedPermissions(),
@@ -124,6 +148,49 @@ class MainActivity : ComponentActivity() {
// Small delay to show the checking state // Small delay to show the checking state
delay(500) delay(500)
// First check Bluetooth status (always required)
checkBluetoothAndProceed()
}
}
/**
* Check Bluetooth status and proceed with onboarding flow
*/
private fun checkBluetoothAndProceed() {
android.util.Log.d("MainActivity", "Checking Bluetooth status")
bluetoothStatusManager.logBluetoothStatus()
bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
when (bluetoothStatus) {
BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, proceed with permission/onboarding check
proceedWithPermissionCheck()
}
BluetoothStatus.DISABLED -> {
// Show Bluetooth enable screen
android.util.Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
}
BluetoothStatus.NOT_SUPPORTED -> {
// Device doesn't support Bluetooth
android.util.Log.e("MainActivity", "Bluetooth not supported")
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
}
}
}
/**
* Proceed with permission checking after Bluetooth is confirmed enabled
*/
private fun proceedWithPermissionCheck() {
android.util.Log.d("MainActivity", "Bluetooth enabled, checking permissions")
lifecycleScope.launch {
delay(200) // Small delay for smooth transition
if (permissionManager.isFirstTimeLaunch()) { if (permissionManager.isFirstTimeLaunch()) {
android.util.Log.d("MainActivity", "First time launch, showing permission explanation") android.util.Log.d("MainActivity", "First time launch, showing permission explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION onboardingState = OnboardingState.PERMISSION_EXPLANATION
@@ -138,6 +205,34 @@ class MainActivity : ComponentActivity() {
} }
} }
/**
* Handle Bluetooth enabled callback
*/
private fun handleBluetoothEnabled() {
android.util.Log.d("MainActivity", "Bluetooth enabled by user")
isBluetoothLoading = false
bluetoothStatus = BluetoothStatus.ENABLED
proceedWithPermissionCheck()
}
/**
* Handle Bluetooth disabled callback
*/
private fun handleBluetoothDisabled(message: String) {
android.util.Log.w("MainActivity", "Bluetooth disabled or failed: $message")
isBluetoothLoading = false
bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
if (bluetoothStatus == BluetoothStatus.NOT_SUPPORTED) {
// Show permanent error for unsupported devices
errorMessage = message
onboardingState = OnboardingState.ERROR
} else {
// Stay on Bluetooth check screen for retry
onboardingState = OnboardingState.BLUETOOTH_CHECK
}
}
private fun handleOnboardingComplete() { private fun handleOnboardingComplete() {
android.util.Log.d("MainActivity", "Onboarding completed, initializing app") android.util.Log.d("MainActivity", "Onboarding completed, initializing app")
onboardingState = OnboardingState.INITIALIZING onboardingState = OnboardingState.INITIALIZING
@@ -198,9 +293,18 @@ class MainActivity : ComponentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Only set background state if app is fully initialized // Check Bluetooth status on resume and handle accordingly
if (onboardingState == OnboardingState.COMPLETE) { if (onboardingState == OnboardingState.COMPLETE) {
chatViewModel.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false)
// Check if Bluetooth was disabled while app was backgrounded
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
if (currentBluetoothStatus != BluetoothStatus.ENABLED) {
android.util.Log.w("MainActivity", "Bluetooth disabled while app was backgrounded")
bluetoothStatus = currentBluetoothStatus
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
}
} }
} }
@@ -0,0 +1,275 @@
package com.bitchat.android.onboarding
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
/**
* Screen shown when checking Bluetooth status or requesting Bluetooth enable
*/
@Composable
fun BluetoothCheckScreen(
status: BluetoothStatus,
onEnableBluetooth: () -> Unit,
onRetry: () -> Unit,
isLoading: Boolean = false
) {
val colorScheme = MaterialTheme.colorScheme
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
when (status) {
BluetoothStatus.DISABLED -> {
BluetoothDisabledContent(
onEnableBluetooth = onEnableBluetooth,
onRetry = onRetry,
colorScheme = colorScheme,
isLoading = isLoading
)
}
BluetoothStatus.NOT_SUPPORTED -> {
BluetoothNotSupportedContent(
colorScheme = colorScheme
)
}
BluetoothStatus.ENABLED -> {
BluetoothCheckingContent(
colorScheme = colorScheme
)
}
}
}
}
@Composable
private fun BluetoothDisabledContent(
onEnableBluetooth: () -> Unit,
onRetry: () -> Unit,
colorScheme: ColorScheme,
isLoading: Boolean
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Bluetooth icon
Card(
colors = CardDefaults.cardColors(
containerColor = Color(0xFFE3F2FD)
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Text(
text = "📶",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(16.dp)
)
}
Text(
text = "Bluetooth Required",
style = MaterialTheme.typography.headlineSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.primary
),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "bitchat needs Bluetooth to:",
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "• Discover nearby users\n" +
"• Create mesh network connections\n" +
"• Send and receive messages\n" +
"• Work without internet or servers",
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
)
}
}
if (isLoading) {
BluetoothLoadingIndicator()
} else {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = onEnableBluetooth,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF2196F3) // Bluetooth blue
)
) {
Text(
text = "Enable Bluetooth",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
),
modifier = Modifier.padding(vertical = 4.dp)
)
}
OutlinedButton(
onClick = onRetry,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Check Again",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
),
modifier = Modifier.padding(vertical = 4.dp)
)
}
}
}
}
}
@Composable
private fun BluetoothNotSupportedContent(
colorScheme: ColorScheme
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Error icon
Card(
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFEBEE)
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Text(
text = "",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(16.dp)
)
}
Text(
text = "Bluetooth Not Supported",
style = MaterialTheme.typography.headlineSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.error
),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = colorScheme.errorContainer.copy(alpha = 0.1f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Text(
text = "This device doesn't support Bluetooth Low Energy (BLE), which is required for bitchat to function.\n\nbitchat needs BLE to create mesh networks and communicate with nearby devices without internet.",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface
),
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
}
}
@Composable
private fun BluetoothCheckingContent(
colorScheme: ColorScheme
) {
Column(
verticalArrangement = Arrangement.spacedBy(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "bitchat*",
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.primary
),
textAlign = TextAlign.Center
)
BluetoothLoadingIndicator()
Text(
text = "Checking Bluetooth status...",
style = MaterialTheme.typography.bodyLarge.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
)
)
}
}
@Composable
private fun BluetoothLoadingIndicator() {
// Animated rotation for the loading indicator
val infiniteTransition = rememberInfiniteTransition(label = "bluetooth_loading")
val rotationAngle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "rotation"
)
Box(
modifier = Modifier.size(60.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.rotate(rotationAngle),
color = Color(0xFF2196F3), // Bluetooth blue
strokeWidth = 3.dp
)
}
}
@@ -0,0 +1,193 @@
package com.bitchat.android.onboarding
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
/**
* Manages Bluetooth enable/disable state and user prompts
* Checks Bluetooth status on every app startup
*/
class BluetoothStatusManager(
private val activity: ComponentActivity,
private val context: Context,
private val onBluetoothEnabled: () -> Unit,
private val onBluetoothDisabled: (String) -> Unit
) {
companion object {
private const val TAG = "BluetoothStatusManager"
}
private var bluetoothEnableLauncher: ActivityResultLauncher<Intent>? = null
private var bluetoothAdapter: BluetoothAdapter? = null
init {
setupBluetoothAdapter()
setupBluetoothEnableLauncher()
}
/**
* Setup Bluetooth adapter reference
*/
private fun setupBluetoothAdapter() {
try {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
Log.d(TAG, "Bluetooth adapter initialized: ${bluetoothAdapter != null}")
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize Bluetooth adapter", e)
bluetoothAdapter = null
}
}
/**
* Setup launcher for Bluetooth enable request
*/
private fun setupBluetoothEnableLauncher() {
bluetoothEnableLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val isEnabled = bluetoothAdapter?.isEnabled == true
Log.d(TAG, "Bluetooth enable request result: $isEnabled (result code: ${result.resultCode})")
if (isEnabled) {
onBluetoothEnabled()
} else {
onBluetoothDisabled("Bluetooth is required for bitchat to discover and connect to nearby users. Please enable Bluetooth to continue.")
}
}
}
/**
* Check if Bluetooth is supported on this device
*/
fun isBluetoothSupported(): Boolean {
return bluetoothAdapter != null
}
/**
* Check if Bluetooth is currently enabled
*/
fun isBluetoothEnabled(): Boolean {
return bluetoothAdapter?.isEnabled == true
}
/**
* Check Bluetooth status and handle accordingly
* This should be called on every app startup
*/
fun checkBluetoothStatus(): BluetoothStatus {
Log.d(TAG, "Checking Bluetooth status")
return when {
bluetoothAdapter == null -> {
Log.e(TAG, "Bluetooth not supported on this device")
BluetoothStatus.NOT_SUPPORTED
}
!bluetoothAdapter!!.isEnabled -> {
Log.w(TAG, "Bluetooth is disabled")
BluetoothStatus.DISABLED
}
else -> {
Log.d(TAG, "Bluetooth is enabled and ready")
BluetoothStatus.ENABLED
}
}
}
/**
* Request user to enable Bluetooth
*/
fun requestEnableBluetooth() {
Log.d(TAG, "Requesting user to enable Bluetooth")
try {
val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
bluetoothEnableLauncher?.launch(enableBluetoothIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to request Bluetooth enable", e)
onBluetoothDisabled("Failed to request Bluetooth enable: ${e.message}")
}
}
/**
* Handle Bluetooth status check result
*/
fun handleBluetoothStatus(status: BluetoothStatus) {
when (status) {
BluetoothStatus.ENABLED -> {
Log.d(TAG, "Bluetooth is enabled, proceeding")
onBluetoothEnabled()
}
BluetoothStatus.DISABLED -> {
Log.d(TAG, "Bluetooth is disabled, requesting enable")
requestEnableBluetooth()
}
BluetoothStatus.NOT_SUPPORTED -> {
Log.e(TAG, "Bluetooth not supported")
onBluetoothDisabled("This device doesn't support Bluetooth, which is required for bitchat to function.")
}
}
}
/**
* Get user-friendly status message
*/
fun getStatusMessage(status: BluetoothStatus): String {
return when (status) {
BluetoothStatus.ENABLED -> "Bluetooth is enabled and ready"
BluetoothStatus.DISABLED -> "Bluetooth is disabled. Please enable Bluetooth to use bitchat."
BluetoothStatus.NOT_SUPPORTED -> "This device doesn't support Bluetooth."
}
}
/**
* Get detailed diagnostics
*/
fun getDiagnostics(): String {
return buildString {
appendLine("Bluetooth Status Diagnostics:")
appendLine("Adapter available: ${bluetoothAdapter != null}")
appendLine("Bluetooth supported: ${isBluetoothSupported()}")
appendLine("Bluetooth enabled: ${isBluetoothEnabled()}")
appendLine("Current status: ${checkBluetoothStatus()}")
bluetoothAdapter?.let { adapter ->
appendLine("Adapter name: ${adapter.name ?: "Unknown"}")
appendLine("Adapter address: ${adapter.address ?: "Unknown"}")
appendLine("Adapter state: ${getAdapterStateName(adapter.state)}")
}
}
}
private fun getAdapterStateName(state: Int): String {
return when (state) {
BluetoothAdapter.STATE_OFF -> "OFF"
BluetoothAdapter.STATE_TURNING_ON -> "TURNING_ON"
BluetoothAdapter.STATE_ON -> "ON"
BluetoothAdapter.STATE_TURNING_OFF -> "TURNING_OFF"
else -> "UNKNOWN($state)"
}
}
/**
* Log current Bluetooth status for debugging
*/
fun logBluetoothStatus() {
Log.d(TAG, getDiagnostics())
}
}
/**
* Bluetooth status enum
*/
enum class BluetoothStatus {
ENABLED,
DISABLED,
NOT_SUPPORTED
}