diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index a3ba5d74..99981280 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -28,14 +28,18 @@ class MainActivity : ComponentActivity() { private lateinit var permissionManager: PermissionManager private lateinit var onboardingCoordinator: OnboardingCoordinator + private lateinit var bluetoothStatusManager: BluetoothStatusManager private val chatViewModel: ChatViewModel by viewModels() // UI state for onboarding flow private var onboardingState by mutableStateOf(OnboardingState.CHECKING) + private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) private var errorMessage by mutableStateOf("") + private var isBluetoothLoading by mutableStateOf(false) enum class OnboardingState { CHECKING, + BLUETOOTH_CHECK, PERMISSION_EXPLANATION, PERMISSION_REQUESTING, INITIALIZING, @@ -48,6 +52,12 @@ class MainActivity : ComponentActivity() { // Initialize permission management permissionManager = PermissionManager(this) + bluetoothStatusManager = BluetoothStatusManager( + activity = this, + context = this, + onBluetoothEnabled = ::handleBluetoothEnabled, + onBluetoothDisabled = ::handleBluetoothDisabled + ) onboardingCoordinator = OnboardingCoordinator( activity = this, permissionManager = permissionManager, @@ -77,6 +87,20 @@ class MainActivity : ComponentActivity() { InitializingScreen() } + OnboardingState.BLUETOOTH_CHECK -> { + BluetoothCheckScreen( + status = bluetoothStatus, + onEnableBluetooth = { + isBluetoothLoading = true + bluetoothStatusManager.requestEnableBluetooth() + }, + onRetry = { + checkBluetoothAndProceed() + }, + isLoading = isBluetoothLoading + ) + } + OnboardingState.PERMISSION_EXPLANATION -> { PermissionExplanationScreen( permissionCategories = permissionManager.getCategorizedPermissions(), @@ -124,6 +148,49 @@ class MainActivity : ComponentActivity() { // Small delay to show the checking state 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()) { android.util.Log.d("MainActivity", "First time launch, showing 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() { android.util.Log.d("MainActivity", "Onboarding completed, initializing app") onboardingState = OnboardingState.INITIALIZING @@ -198,9 +293,18 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() - // Only set background state if app is fully initialized + // Check Bluetooth status on resume and handle accordingly if (onboardingState == OnboardingState.COMPLETE) { 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 + } } } diff --git a/app/src/main/java/com/bitchat/android/onboarding/BluetoothCheckScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/BluetoothCheckScreen.kt new file mode 100644 index 00000000..47892df0 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/BluetoothCheckScreen.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/com/bitchat/android/onboarding/BluetoothStatusManager.kt b/app/src/main/java/com/bitchat/android/onboarding/BluetoothStatusManager.kt new file mode 100644 index 00000000..32fc2193 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/BluetoothStatusManager.kt @@ -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? = 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 +}