From fe57c5684e111dac2b125bcd66f43eae65d077e4 Mon Sep 17 00:00:00 2001 From: Faded Date: Mon, 14 Jul 2025 14:23:59 +0500 Subject: [PATCH 01/17] feat(battery): add battery optimization management for background reliability Add battery optimization permission and management system to ensure app runs reliably in background: - Add REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission - Create BatteryOptimizationManager to handle battery optimization checks and requests - Add battery optimization UI screens and flow in onboarding - Integrate battery optimization checks into MainActivity flow --- app/src/main/AndroidManifest.xml | 3 + .../java/com/bitchat/android/MainActivity.kt | 116 ++++++- .../onboarding/BatteryOptimizationManager.kt | 177 +++++++++++ .../onboarding/BatteryOptimizationScreen.kt | 296 ++++++++++++++++++ .../onboarding/PermissionExplanationScreen.kt | 2 + .../android/onboarding/PermissionManager.kt | 40 +++ 6 files changed, 628 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationManager.kt create mode 100644 app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ea01524..3cc22c28 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,9 @@ + + + diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 1ed97a92..2d601eae 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -34,6 +34,7 @@ class MainActivity : ComponentActivity() { private lateinit var onboardingCoordinator: OnboardingCoordinator private lateinit var bluetoothStatusManager: BluetoothStatusManager private lateinit var locationStatusManager: LocationStatusManager + private lateinit var batteryOptimizationManager: BatteryOptimizationManager // Core mesh service - managed at app level private lateinit var meshService: BluetoothMeshService @@ -50,14 +51,17 @@ class MainActivity : ComponentActivity() { private var onboardingState by mutableStateOf(OnboardingState.CHECKING) private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) private var locationStatus by mutableStateOf(LocationStatus.ENABLED) + private var batteryOptimizationStatus by mutableStateOf(BatteryOptimizationStatus.DISABLED) private var errorMessage by mutableStateOf("") private var isBluetoothLoading by mutableStateOf(false) private var isLocationLoading by mutableStateOf(false) + private var isBatteryOptimizationLoading by mutableStateOf(false) enum class OnboardingState { CHECKING, BLUETOOTH_CHECK, LOCATION_CHECK, + BATTERY_OPTIMIZATION_CHECK, PERMISSION_EXPLANATION, PERMISSION_REQUESTING, INITIALIZING, @@ -65,6 +69,8 @@ class MainActivity : ComponentActivity() { ERROR } + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -85,6 +91,12 @@ class MainActivity : ComponentActivity() { onLocationEnabled = ::handleLocationEnabled, onLocationDisabled = ::handleLocationDisabled ) + batteryOptimizationManager = BatteryOptimizationManager( + activity = this, + context = this, + onBatteryOptimizationDisabled = ::handleBatteryOptimizationDisabled, + onBatteryOptimizationFailed = ::handleBatteryOptimizationFailed + ) onboardingCoordinator = OnboardingCoordinator( activity = this, permissionManager = permissionManager, @@ -142,6 +154,24 @@ class MainActivity : ComponentActivity() { ) } + OnboardingState.BATTERY_OPTIMIZATION_CHECK -> { + BatteryOptimizationScreen( + status = batteryOptimizationStatus, + onDisableBatteryOptimization = { + isBatteryOptimizationLoading = true + batteryOptimizationManager.requestDisableBatteryOptimization() + }, + onRetry = { + checkBatteryOptimizationAndProceed() + }, + onSkip = { + // Skip battery optimization and proceed + proceedWithPermissionCheck() + }, + isLoading = isBatteryOptimizationLoading + ) + } + OnboardingState.PERMISSION_EXPLANATION -> { PermissionExplanationScreen( permissionCategories = permissionManager.getCategorizedPermissions(), @@ -300,8 +330,8 @@ class MainActivity : ComponentActivity() { when (locationStatus) { LocationStatus.ENABLED -> { - // Location services enabled, proceed with permission/onboarding check - proceedWithPermissionCheck() + // Location services enabled, check battery optimization next + checkBatteryOptimizationAndProceed() } LocationStatus.DISABLED -> { // Show location enable screen (should have permissions as existing user) @@ -325,6 +355,65 @@ class MainActivity : ComponentActivity() { android.util.Log.d("MainActivity", "Location services enabled by user") isLocationLoading = false locationStatus = LocationStatus.ENABLED + checkBatteryOptimizationAndProceed() + } + + /** + * Check Battery Optimization status and proceed with onboarding flow + */ + private fun checkBatteryOptimizationAndProceed() { + android.util.Log.d("MainActivity", "Checking battery optimization status") + + // For first-time users, skip battery optimization check and go straight to permissions + // We'll check battery optimization after permissions are granted + if (permissionManager.isFirstTimeLaunch()) { + android.util.Log.d("MainActivity", "First-time launch, skipping battery optimization check - will check after permissions") + proceedWithPermissionCheck() + return + } + + // For existing users, check battery optimization status + batteryOptimizationManager.logBatteryOptimizationStatus() + + batteryOptimizationStatus = when { + !batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED + batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED + else -> BatteryOptimizationStatus.ENABLED + } + + when (batteryOptimizationStatus) { + BatteryOptimizationStatus.DISABLED, BatteryOptimizationStatus.NOT_SUPPORTED -> { + // Battery optimization is disabled or not supported, proceed with permission check + proceedWithPermissionCheck() + } + BatteryOptimizationStatus.ENABLED -> { + // Show battery optimization disable screen + android.util.Log.d("MainActivity", "Battery optimization enabled, showing disable screen") + onboardingState = OnboardingState.BATTERY_OPTIMIZATION_CHECK + isBatteryOptimizationLoading = false + } + } + } + + /** + * Handle Battery Optimization disabled callback + */ + private fun handleBatteryOptimizationDisabled() { + android.util.Log.d("MainActivity", "Battery optimization disabled by user") + isBatteryOptimizationLoading = false + batteryOptimizationStatus = BatteryOptimizationStatus.DISABLED + proceedWithPermissionCheck() + } + + /** + * Handle Battery Optimization failed callback + */ + private fun handleBatteryOptimizationFailed(message: String) { + android.util.Log.w("MainActivity", "Battery optimization disable failed: $message") + isBatteryOptimizationLoading = false + + // Don't treat this as a fatal error - proceed with onboarding + // User can always change this setting later proceedWithPermissionCheck() } @@ -382,11 +471,16 @@ class MainActivity : ComponentActivity() { } private fun handleOnboardingComplete() { - android.util.Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app") + android.util.Log.d("MainActivity", "Onboarding completed, checking Bluetooth, Location, and Battery Optimization before initializing app") - // After permissions are granted, re-check both Bluetooth and Location status + // After permissions are granted, re-check Bluetooth, Location, and Battery Optimization status val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() val currentLocationStatus = locationStatusManager.checkLocationStatus() + val currentBatteryOptimizationStatus = when { + !batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED + batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED + else -> BatteryOptimizationStatus.ENABLED + } when { currentBluetoothStatus != BluetoothStatus.ENABLED -> { @@ -403,9 +497,19 @@ class MainActivity : ComponentActivity() { onboardingState = OnboardingState.LOCATION_CHECK isLocationLoading = false } + currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> { + // Battery optimization is still enabled, show disable screen + android.util.Log.d("MainActivity", "Permissions granted, but Battery optimization still enabled. Showing Battery optimization disable screen.") + batteryOptimizationStatus = currentBatteryOptimizationStatus + onboardingState = OnboardingState.BATTERY_OPTIMIZATION_CHECK + isBatteryOptimizationLoading = false + } else -> { - // Both are enabled, proceed to app initialization - android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") + // Bluetooth, Location, and Battery Optimization are all properly configured, proceed to app initialization + android.util.Log.d("MainActivity", "Bluetooth, Location, and Battery Optimization all properly configured, proceeding to initialization") + bluetoothStatus = currentBluetoothStatus + locationStatus = currentLocationStatus + batteryOptimizationStatus = currentBatteryOptimizationStatus onboardingState = OnboardingState.INITIALIZING initializeApp() } diff --git a/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationManager.kt b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationManager.kt new file mode 100644 index 00000000..77ede503 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationManager.kt @@ -0,0 +1,177 @@ +package com.bitchat.android.onboarding + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts + +/** + * Manages battery optimization settings for the app + * Handles checking if the app is whitelisted from battery optimization + * and requesting the user to disable battery optimization + */ +class BatteryOptimizationManager( + private val activity: ComponentActivity, + private val context: Context, + private val onBatteryOptimizationDisabled: () -> Unit, + private val onBatteryOptimizationFailed: (String) -> Unit +) { + + companion object { + private const val TAG = "BatteryOptimizationManager" + } + + private var batteryOptimizationLauncher: ActivityResultLauncher? = null + + init { + setupBatteryOptimizationLauncher() + } + + /** + * Setup the battery optimization request launcher + */ + private fun setupBatteryOptimizationLauncher() { + batteryOptimizationLauncher = activity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + Log.d(TAG, "Battery optimization request result: ${result.resultCode}") + + // Check if battery optimization is now disabled + if (isBatteryOptimizationDisabled()) { + Log.d(TAG, "Battery optimization successfully disabled") + onBatteryOptimizationDisabled() + } else { + Log.w(TAG, "Battery optimization still enabled after user interaction") + // Don't treat this as a failure - user might have chosen not to disable it + // We'll proceed anyway but log the status + onBatteryOptimizationDisabled() + } + } + } + + /** + * Check if battery optimization is disabled for this app + */ + fun isBatteryOptimizationDisabled(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + val isIgnoring = powerManager.isIgnoringBatteryOptimizations(context.packageName) + Log.d(TAG, "Battery optimization disabled: $isIgnoring") + isIgnoring + } catch (e: Exception) { + Log.e(TAG, "Error checking battery optimization status", e) + // If we can't check, assume it's enabled (more conservative) + false + } + } else { + // Battery optimization doesn't exist on Android < 6.0 + Log.d(TAG, "Battery optimization not applicable for Android < 6.0") + true + } + } + + /** + * Request to disable battery optimization for this app + */ + fun requestDisableBatteryOptimization() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + Log.d(TAG, "Requesting to disable battery optimization") + + val intent = Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${context.packageName}") + } + + // Check if the intent can be resolved + if (intent.resolveActivity(context.packageManager) != null) { + batteryOptimizationLauncher?.launch(intent) + } else { + Log.w(TAG, "Battery optimization settings not available, opening general settings") + openBatteryOptimizationSettings() + } + } catch (e: Exception) { + Log.e(TAG, "Error requesting battery optimization disable", e) + onBatteryOptimizationFailed("Unable to open battery optimization settings: ${e.message}") + } + } else { + Log.d(TAG, "Battery optimization not applicable for Android < 6.0") + onBatteryOptimizationDisabled() + } + } + + /** + * Open general battery optimization settings if direct request fails + */ + private fun openBatteryOptimizationSettings() { + try { + val intent = Intent().apply { + action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS + } + + if (intent.resolveActivity(context.packageManager) != null) { + batteryOptimizationLauncher?.launch(intent) + } else { + // Fallback to general application settings + openAppSettings() + } + } catch (e: Exception) { + Log.e(TAG, "Error opening battery optimization settings", e) + onBatteryOptimizationFailed("Unable to open settings: ${e.message}") + } + } + + /** + * Open app settings as a last resort + */ + private fun openAppSettings() { + try { + val intent = Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.fromParts("package", context.packageName, null) + } + batteryOptimizationLauncher?.launch(intent) + } catch (e: Exception) { + Log.e(TAG, "Error opening app settings", e) + onBatteryOptimizationFailed("Unable to open app settings: ${e.message}") + } + } + + /** + * Check if battery optimization is supported on this device + */ + fun isBatteryOptimizationSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + } + + /** + * Get battery optimization status for logging + */ + fun getBatteryOptimizationStatus(): String { + return when { + !isBatteryOptimizationSupported() -> "Not supported (Android < 6.0)" + isBatteryOptimizationDisabled() -> "Disabled (app is whitelisted)" + else -> "Enabled (app is being optimized)" + } + } + + /** + * Log battery optimization status for debugging + */ + fun logBatteryOptimizationStatus() { + Log.d(TAG, "Battery optimization status: ${getBatteryOptimizationStatus()}") + } +} + +enum class BatteryOptimizationStatus { + ENABLED, + DISABLED, + NOT_SUPPORTED +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt new file mode 100644 index 00000000..5213768b --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt @@ -0,0 +1,296 @@ +package com.bitchat.android.onboarding + +import androidx.compose.animation.core.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.* +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 battery optimization status or requesting battery optimization disable + */ + + + +@Composable +fun BatteryOptimizationScreen( + status: BatteryOptimizationStatus, + onDisableBatteryOptimization: () -> Unit, + onRetry: () -> Unit, + onSkip: () -> Unit, + isLoading: Boolean = false +) { + val colorScheme = MaterialTheme.colorScheme + + Box( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + when (status) { + BatteryOptimizationStatus.ENABLED -> { + BatteryOptimizationEnabledContent( + onDisableBatteryOptimization = onDisableBatteryOptimization, + onRetry = onRetry, + onSkip = onSkip, + colorScheme = colorScheme, + isLoading = isLoading + ) + } + + BatteryOptimizationStatus.DISABLED -> { + BatteryOptimizationCheckingContent( + colorScheme = colorScheme + ) + } + + BatteryOptimizationStatus.NOT_SUPPORTED -> { + BatteryOptimizationNotSupportedContent( + onRetry = onRetry, + colorScheme = colorScheme + ) + } + } + } +} + +@Composable +private fun BatteryOptimizationEnabledContent( + onDisableBatteryOptimization: () -> Unit, + onRetry: () -> Unit, + onSkip: () -> Unit, + colorScheme: ColorScheme, + isLoading: Boolean +) { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "bitchat*", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Icon( + imageVector = Icons.Outlined.BatteryAlert, + contentDescription = "Battery Optimization", + modifier = Modifier.size(64.dp), + tint = colorScheme.error + ) + + Text( + text = "Battery Optimization Detected", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface + ), + textAlign = TextAlign.Center + ) + + Text( + text = "bitchat needs to run in the background to maintain mesh network connections and relay messages for other users.", + style = MaterialTheme.typography.bodyLarge.copy( + color = colorScheme.onSurfaceVariant + ), + textAlign = TextAlign.Center + ) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Why disable battery optimization?", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface + ) + ) + + Text( + text = "• Ensures reliable message delivery\n• Maintains mesh network connectivity\n• Allows background message relay\n• Prevents connection drops", + style = MaterialTheme.typography.bodyMedium.copy( + color = colorScheme.onSurfaceVariant + ) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = onDisableBatteryOptimization, + modifier = Modifier.fillMaxWidth(), + enabled = !isLoading + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text("Disable Battery Optimization") + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = onRetry, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) { + Text("Check Again") + } + + TextButton( + onClick = onSkip, + modifier = Modifier.weight(1f), + enabled = !isLoading + ) { + Text("Skip for Now") + } + } + + Text( + text = "Note: You can change this setting later in Android Settings > Apps > bitchat > Battery", + style = MaterialTheme.typography.bodySmall.copy( + color = colorScheme.onSurfaceVariant + ), + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun BatteryOptimizationCheckingContent( + 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 + ) + ) + + val infiniteTransition = rememberInfiniteTransition(label = "rotation") + val rotation by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(2000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "rotation" + ) + + Icon( + imageVector = Icons.Filled.BatteryStd, + contentDescription = "Checking Battery Optimization", + modifier = Modifier + .size(64.dp) + .rotate(rotation), + tint = colorScheme.primary + ) + + Text( + text = "Battery Optimization Disabled", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface + ), + textAlign = TextAlign.Center + ) + + Text( + text = "bitchat can run reliably in the background", + style = MaterialTheme.typography.bodyLarge.copy( + color = colorScheme.onSurfaceVariant + ), + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun BatteryOptimizationNotSupportedContent( + onRetry: () -> Unit, + colorScheme: ColorScheme +) { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "bitchat*", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ) + ) + + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Battery Optimization Not Supported", + modifier = Modifier.size(64.dp), + tint = colorScheme.primary + ) + + Text( + text = "Battery Optimization Not Required", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + color = colorScheme.onSurface + ), + textAlign = TextAlign.Center + ) + + Text( + text = "Your device doesn't require battery optimization settings. bitchat will run normally.", + style = MaterialTheme.typography.bodyLarge.copy( + color = colorScheme.onSurfaceVariant + ), + textAlign = TextAlign.Center + ) + + Button( + onClick = onRetry, + modifier = Modifier.fillMaxWidth() + ) { + Text("Continue") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt index 4f066096..7c24cadc 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt @@ -237,6 +237,7 @@ private fun getPermissionEmoji(permissionType: PermissionType): String { PermissionType.NEARBY_DEVICES -> "šŸ“±" PermissionType.PRECISE_LOCATION -> "šŸ“" PermissionType.NOTIFICATIONS -> "šŸ””" + PermissionType.BATTERY_OPTIMIZATION -> "šŸ”‹" PermissionType.OTHER -> "šŸ”§" } } @@ -246,6 +247,7 @@ private fun getPermissionIconColor(permissionType: PermissionType): Color { PermissionType.NEARBY_DEVICES -> Color(0xFF2196F3) // Blue PermissionType.PRECISE_LOCATION -> Color(0xFFFF9800) // Orange PermissionType.NOTIFICATIONS -> Color(0xFF4CAF50) // Green + PermissionType.BATTERY_OPTIMIZATION -> Color(0xFFF44336) // Red PermissionType.OTHER -> Color(0xFF9C27B0) // Purple } } diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt index 7d293324..cfec35e7 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt @@ -4,6 +4,7 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.Build +import android.os.PowerManager import android.util.Log import androidx.core.content.ContextCompat @@ -86,6 +87,31 @@ class PermissionManager(private val context: Context) { return getRequiredPermissions().all { isPermissionGranted(it) } } + /** + * Check if battery optimization is disabled for this app + */ + fun isBatteryOptimizationDisabled(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + powerManager.isIgnoringBatteryOptimizations(context.packageName) + } catch (e: Exception) { + Log.e(TAG, "Error checking battery optimization status", e) + false + } + } else { + // Battery optimization doesn't exist on Android < 6.0 + true + } + } + + /** + * Check if battery optimization is supported on this device + */ + fun isBatteryOptimizationSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + } + /** * Get the list of permissions that are missing */ @@ -152,6 +178,19 @@ class PermissionManager(private val context: Context) { ) } + // Battery optimization category (if applicable) + if (isBatteryOptimizationSupported()) { + categories.add( + PermissionCategory( + type = PermissionType.BATTERY_OPTIMIZATION, + description = "Disable battery optimization to ensure bitchat runs reliably in the background and maintains mesh network connections", + permissions = listOf("BATTERY_OPTIMIZATION"), // Custom identifier + isGranted = isBatteryOptimizationDisabled(), + systemDescription = "Allow bitchat to run without battery restrictions" + ) + ) + } + return categories } @@ -208,5 +247,6 @@ enum class PermissionType(val nameValue: String) { NEARBY_DEVICES("Nearby Devices"), PRECISE_LOCATION("Precise Location"), NOTIFICATIONS("Notifications"), + BATTERY_OPTIMIZATION("Battery Optimization"), OTHER("Other") } From 21dc1a5b5e435ef1e30220eeb23e5a04ef854e06 Mon Sep 17 00:00:00 2001 From: Faded Date: Mon, 14 Jul 2025 15:51:50 +0500 Subject: [PATCH 02/17] fix(chat): cursor location and enhance message input with slash command styling - Change message input from String to TextFieldValue for better cursor control - Add SlashCommandVisualTransformation to style slash commands with colored backgrounds - Maintain cursor position when selecting command suggestions --- .../java/com/bitchat/android/ui/ChatScreen.kt | 24 ++++--- .../com/bitchat/android/ui/InputComponents.kt | 70 +++++++++++++++++-- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 4c9e30dc..7f8ce190 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.Dp @@ -65,7 +67,7 @@ fun ChatScreen(viewModel: ChatViewModel) { val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList()) val showAppInfo by viewModel.showAppInfo.observeAsState(false) - var messageText by remember { mutableStateOf("") } + var messageText by remember { mutableStateOf(TextFieldValue("")) } var showPasswordPrompt by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) } var passwordInput by remember { mutableStateOf("") } @@ -112,20 +114,24 @@ fun ChatScreen(viewModel: ChatViewModel) { // Input area - stays at bottom ChatInputSection( messageText = messageText, - onMessageTextChange = { newText: String -> + onMessageTextChange = { newText: TextFieldValue -> messageText = newText - viewModel.updateCommandSuggestions(newText) + viewModel.updateCommandSuggestions(newText.text) }, onSend = { - if (messageText.trim().isNotEmpty()) { - viewModel.sendMessage(messageText.trim()) - messageText = "" + if (messageText.text.trim().isNotEmpty()) { + viewModel.sendMessage(messageText.text.trim()) + messageText = TextFieldValue("") } }, showCommandSuggestions = showCommandSuggestions, commandSuggestions = commandSuggestions, onSuggestionClick = { suggestion: CommandSuggestion -> - messageText = viewModel.selectCommandSuggestion(suggestion) + val commandText = viewModel.selectCommandSuggestion(suggestion) + messageText = TextFieldValue( + text = commandText, + selection = TextRange(commandText.length) + ) }, selectedPrivatePeer = selectedPrivatePeer, currentChannel = currentChannel, @@ -195,8 +201,8 @@ fun ChatScreen(viewModel: ChatViewModel) { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChatInputSection( - messageText: String, - onMessageTextChange: (String) -> Unit, + messageText: TextFieldValue, + onMessageTextChange: (TextFieldValue) -> Unit, onSend: () -> Unit, showCommandSuggestions: Boolean, commandSuggestions: List, diff --git a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt index 761b502b..d04d84aa 100644 --- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt @@ -15,23 +15,79 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.text.withStyle /** * Input components for ChatScreen * Extracted from ChatScreen.kt for better organization */ +/** + * VisualTransformation that styles slash commands with background and color + * while preserving cursor positioning and click handling + */ +class SlashCommandVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val slashCommandRegex = Regex("(/\\w+)(?=\\s|$)") + val annotatedString = buildAnnotatedString { + var lastIndex = 0 + + slashCommandRegex.findAll(text.text).forEach { match -> + // Add text before the match + if (match.range.first > lastIndex) { + append(text.text.substring(lastIndex, match.range.first)) + } + + // Add the styled slash command + withStyle( + style = SpanStyle( + color = Color(0xFF00FF7F), // Bright green + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Medium, + background = Color(0xFF2D2D2D) // Dark gray background + ) + ) { + append(match.value) + } + + lastIndex = match.range.last + 1 + } + + // Add remaining text + if (lastIndex < text.text.length) { + append(text.text.substring(lastIndex)) + } + } + + return TransformedText( + text = annotatedString, + offsetMapping = OffsetMapping.Identity + ) + } +} + + + + + @Composable fun MessageInput( - value: String, - onValueChange: (String) -> Unit, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, onSend: () -> Unit, selectedPrivatePeer: String?, currentChannel: String?, @@ -39,6 +95,7 @@ fun MessageInput( modifier: Modifier = Modifier ) { val colorScheme = MaterialTheme.colorScheme + val isFocused = remember { mutableStateOf(false) } Row( modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding @@ -70,7 +127,12 @@ fun MessageInput( cursorBrush = SolidColor(colorScheme.primary), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardActions = KeyboardActions(onSend = { onSend() }), - modifier = Modifier.weight(1f) + visualTransformation = SlashCommandVisualTransformation(), + modifier = Modifier + .weight(1f) + .onFocusChanged { focusState -> + isFocused.value = focusState.isFocused + } ) Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing From 0a33bd734d45d7f0ea64078ee28bd91c38947718 Mon Sep 17 00:00:00 2001 From: Mohamad Hamade Date: Mon, 14 Jul 2025 22:16:19 +0300 Subject: [PATCH 03/17] Bugfix: Handled single or triple clicked at once instead of executing them as separate events This commit introduces a new `singleOrTripleClickable` Modifier extension function. This function allows distinguishing between single and triple clicks on a Composable element. The `ChatHeader` was updated to utilize this new Modifier for handling clicks on the "bitchat*" title, where a single click shows app info and a triple click triggers a separate action. --- .../android/core/ui/utils/ModifierExt.kt | 57 +++++++++++++++++++ .../java/com/bitchat/android/ui/ChatHeader.kt | 18 +++--- 2 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/core/ui/utils/ModifierExt.kt diff --git a/app/src/main/java/com/bitchat/android/core/ui/utils/ModifierExt.kt b/app/src/main/java/com/bitchat/android/core/ui/utils/ModifierExt.kt new file mode 100644 index 00000000..19dabb57 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/core/ui/utils/ModifierExt.kt @@ -0,0 +1,57 @@ +package com.bitchat.android.core.ui.utils + +import androidx.compose.foundation.clickable +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +fun Modifier.singleOrTripleClickable( + onSingleClick: () -> Unit, + onTripleClick: () -> Unit, + clickTimeThreshold: Long = 300L +): Modifier = composed { + var tapCount by remember { mutableIntStateOf(0) } + var lastTapTime by remember { mutableLongStateOf(0L) } + var singleClickJob by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + + this.clickable { + val currentTime = System.currentTimeMillis() + + if (currentTime - lastTapTime < clickTimeThreshold) { + tapCount++ + } else { + tapCount = 1 + } + + lastTapTime = currentTime + + // Cancel any pending single click action + singleClickJob?.cancel() + singleClickJob = null + + when (tapCount) { + 1 -> { + // Wait to see if more taps come + singleClickJob = coroutineScope.launch { + delay(clickTimeThreshold) + if (tapCount == 1) { + onSingleClick() + } + } + } + 3 -> { + // Triple click detected - execute immediately + onTripleClick() + tapCount = 0 + } + } + + // Reset after threshold if no triple click + if (tapCount > 3) { + tapCount = 0 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 65e2ee8e..1b9fd098 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.bitchat.android.core.ui.utils.singleOrTripleClickable /** * Header components for ChatScreen @@ -181,15 +182,8 @@ fun ChatHeaderContent( MainHeader( nickname = nickname, onNicknameChange = viewModel::setNickname, - onTitleClick = { - tripleClickCount++ - if (tripleClickCount >= 3) { - tripleClickCount = 0 - onTripleClick() - } else { - onShowAppInfo() - } - }, + onTitleClick = onShowAppInfo, + onTripleTitleClick = onTripleClick, onSidebarClick = onSidebarClick, viewModel = viewModel ) @@ -345,6 +339,7 @@ private fun MainHeader( nickname: String, onNicknameChange: (String) -> Unit, onTitleClick: () -> Unit, + onTripleTitleClick: () -> Unit, onSidebarClick: () -> Unit, viewModel: ChatViewModel ) { @@ -365,7 +360,10 @@ private fun MainHeader( text = "bitchat*", style = MaterialTheme.typography.headlineSmall, color = colorScheme.primary, - modifier = Modifier.clickable { onTitleClick() } + modifier = Modifier.singleOrTripleClickable( + onSingleClick = onTitleClick, + onTripleClick = onTripleTitleClick + ) ) Spacer(modifier = Modifier.width(8.dp)) From 245181d73671c748ffdf3a5325aa62d2ed614a25 Mon Sep 17 00:00:00 2001 From: Mohamad Hamade Date: Tue, 15 Jul 2025 02:24:41 +0300 Subject: [PATCH 04/17] Refactor: Introduce MainViewModel for onboarding state management This commit introduces a `MainViewModel` to manage the UI state for the onboarding flow. This change centralizes the onboarding state (including Bluetooth status, location status, error messages, and loading indicators) within the ViewModel, allowing it to survive configuration changes and simplifying state management within `MainActivity`. Key changes: - Created `MainViewModel.kt` to hold and manage onboarding-related UI state. - Moved onboarding state variables (e.g., `onboardingState`, `bluetoothStatus`, `locationStatus`) from `MainActivity` to `MainViewModel`. - Updated `MainActivity` to observe and update onboarding state through the `MainViewModel`. - Created `OnboardingState.kt` to define the possible states of the onboarding process. - Ensured that the onboarding process is not restarted on configuration changes by checking `mainViewModel.onboardingState` before initiating. --- .../java/com/bitchat/android/MainActivity.kt | 190 ++++++++---------- .../java/com/bitchat/android/MainViewModel.kt | 52 +++++ .../com/bitchat/android/OnboardingState.kt | 12 ++ 3 files changed, 153 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/MainViewModel.kt create mode 100644 app/src/main/java/com/bitchat/android/OnboardingState.kt diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 1ed97a92..232fca28 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -1,27 +1,30 @@ package com.bitchat.android -import android.Manifest import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.viewModels -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.ViewModelProvider import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.bitchat.android.mesh.BluetoothMeshService -import com.bitchat.android.onboarding.* +import com.bitchat.android.onboarding.BluetoothCheckScreen +import com.bitchat.android.onboarding.BluetoothStatus +import com.bitchat.android.onboarding.BluetoothStatusManager +import com.bitchat.android.onboarding.InitializationErrorScreen +import com.bitchat.android.onboarding.InitializingScreen +import com.bitchat.android.onboarding.LocationCheckScreen +import com.bitchat.android.onboarding.LocationStatus +import com.bitchat.android.onboarding.LocationStatusManager +import com.bitchat.android.onboarding.OnboardingCoordinator +import com.bitchat.android.onboarding.PermissionExplanationScreen +import com.bitchat.android.onboarding.PermissionManager import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.theme.BitchatTheme @@ -37,6 +40,7 @@ class MainActivity : ComponentActivity() { // Core mesh service - managed at app level private lateinit var meshService: BluetoothMeshService + private val mainViewModel: MainViewModel by viewModels() private val chatViewModel: ChatViewModel by viewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -46,25 +50,6 @@ class MainActivity : ComponentActivity() { } } - // UI state for onboarding flow - private var onboardingState by mutableStateOf(OnboardingState.CHECKING) - private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) - private var locationStatus by mutableStateOf(LocationStatus.ENABLED) - private var errorMessage by mutableStateOf("") - private var isBluetoothLoading by mutableStateOf(false) - private var isLocationLoading by mutableStateOf(false) - - enum class OnboardingState { - CHECKING, - BLUETOOTH_CHECK, - LOCATION_CHECK, - PERMISSION_EXPLANATION, - PERMISSION_REQUESTING, - INITIALIZING, - COMPLETE, - ERROR - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -103,42 +88,45 @@ class MainActivity : ComponentActivity() { } } - // Start the onboarding process - checkOnboardingStatus() + // Only start onboarding process if we're in the initial CHECKING state + // This prevents restarting onboarding on configuration changes + if (mainViewModel.onboardingState == OnboardingState.CHECKING) { + checkOnboardingStatus() + } } @Composable private fun OnboardingFlowScreen() { - when (onboardingState) { + when (mainViewModel.onboardingState) { OnboardingState.CHECKING -> { InitializingScreen() } OnboardingState.BLUETOOTH_CHECK -> { BluetoothCheckScreen( - status = bluetoothStatus, + status = mainViewModel.bluetoothStatus, onEnableBluetooth = { - isBluetoothLoading = true + mainViewModel.updateBluetoothLoading(true) bluetoothStatusManager.requestEnableBluetooth() }, onRetry = { checkBluetoothAndProceed() }, - isLoading = isBluetoothLoading + isLoading = mainViewModel.isBluetoothLoading ) } OnboardingState.LOCATION_CHECK -> { LocationCheckScreen( - status = locationStatus, + status = mainViewModel.locationStatus, onEnableLocation = { - isLocationLoading = true + mainViewModel.updateLocationLoading(true) locationStatusManager.requestEnableLocation() }, onRetry = { checkLocationAndProceed() }, - isLoading = isLocationLoading + isLoading = mainViewModel.isLocationLoading ) } @@ -146,7 +134,7 @@ class MainActivity : ComponentActivity() { PermissionExplanationScreen( permissionCategories = permissionManager.getCategorizedPermissions(), onContinue = { - onboardingState = OnboardingState.PERMISSION_REQUESTING + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_REQUESTING) onboardingCoordinator.requestPermissions() } ) @@ -184,9 +172,9 @@ class MainActivity : ComponentActivity() { OnboardingState.ERROR -> { InitializationErrorScreen( - errorMessage = errorMessage, + errorMessage = mainViewModel.errorMessage, onRetry = { - onboardingState = OnboardingState.CHECKING + mainViewModel.updateOnboardingState(OnboardingState.CHECKING) checkOnboardingStatus() }, onOpenSettings = { @@ -225,9 +213,9 @@ class MainActivity : ComponentActivity() { // For existing users, check Bluetooth status first bluetoothStatusManager.logBluetoothStatus() - bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() + mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) - when (bluetoothStatus) { + when (mainViewModel.bluetoothStatus) { BluetoothStatus.ENABLED -> { // Bluetooth is enabled, check location services next checkLocationAndProceed() @@ -235,14 +223,14 @@ class MainActivity : ComponentActivity() { BluetoothStatus.DISABLED -> { // Show Bluetooth enable screen (should have permissions as existing user) android.util.Log.d("MainActivity", "Bluetooth disabled, showing enable screen") - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) + mainViewModel.updateBluetoothLoading(false) } BluetoothStatus.NOT_SUPPORTED -> { // Device doesn't support Bluetooth android.util.Log.e("MainActivity", "Bluetooth not supported") - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) + mainViewModel.updateBluetoothLoading(false) } } } @@ -258,14 +246,14 @@ class MainActivity : ComponentActivity() { if (permissionManager.isFirstTimeLaunch()) { android.util.Log.d("MainActivity", "First time launch, showing permission explanation") - onboardingState = OnboardingState.PERMISSION_EXPLANATION + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } else if (permissionManager.areAllPermissionsGranted()) { android.util.Log.d("MainActivity", "Existing user with permissions, initializing app") - onboardingState = OnboardingState.INITIALIZING + mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING) initializeApp() } else { android.util.Log.d("MainActivity", "Existing user missing permissions, showing explanation") - onboardingState = OnboardingState.PERMISSION_EXPLANATION + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } } } @@ -275,8 +263,8 @@ class MainActivity : ComponentActivity() { */ private fun handleBluetoothEnabled() { android.util.Log.d("MainActivity", "Bluetooth enabled by user") - isBluetoothLoading = false - bluetoothStatus = BluetoothStatus.ENABLED + mainViewModel.updateBluetoothLoading(false) + mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED) checkLocationAndProceed() } @@ -296,9 +284,9 @@ class MainActivity : ComponentActivity() { // For existing users, check location status locationStatusManager.logLocationStatus() - locationStatus = locationStatusManager.checkLocationStatus() + mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) - when (locationStatus) { + when (mainViewModel.locationStatus) { LocationStatus.ENABLED -> { // Location services enabled, proceed with permission/onboarding check proceedWithPermissionCheck() @@ -306,14 +294,14 @@ class MainActivity : ComponentActivity() { LocationStatus.DISABLED -> { // Show location enable screen (should have permissions as existing user) android.util.Log.d("MainActivity", "Location services disabled, showing enable screen") - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } LocationStatus.NOT_AVAILABLE -> { // Device doesn't support location services (very unusual) android.util.Log.e("MainActivity", "Location services not available") - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } } } @@ -323,8 +311,8 @@ class MainActivity : ComponentActivity() { */ private fun handleLocationEnabled() { android.util.Log.d("MainActivity", "Location services enabled by user") - isLocationLoading = false - locationStatus = LocationStatus.ENABLED + mainViewModel.updateLocationLoading(false) + mainViewModel.updateLocationStatus(LocationStatus.ENABLED) proceedWithPermissionCheck() } @@ -333,18 +321,18 @@ class MainActivity : ComponentActivity() { */ private fun handleLocationDisabled(message: String) { android.util.Log.w("MainActivity", "Location services disabled or failed: $message") - isLocationLoading = false - locationStatus = locationStatusManager.checkLocationStatus() + mainViewModel.updateLocationLoading(false) + mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) when { - locationStatus == LocationStatus.NOT_AVAILABLE -> { + mainViewModel.locationStatus == LocationStatus.NOT_AVAILABLE -> { // Show permanent error for devices without location services - errorMessage = message - onboardingState = OnboardingState.ERROR + mainViewModel.updateErrorMessage(message) + mainViewModel.updateOnboardingState(OnboardingState.ERROR) } else -> { // Stay on location check screen for retry - onboardingState = OnboardingState.LOCATION_CHECK + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) } } } @@ -354,14 +342,14 @@ class MainActivity : ComponentActivity() { */ private fun handleBluetoothDisabled(message: String) { android.util.Log.w("MainActivity", "Bluetooth disabled or failed: $message") - isBluetoothLoading = false - bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() + mainViewModel.updateBluetoothLoading(false) + mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) when { - bluetoothStatus == BluetoothStatus.NOT_SUPPORTED -> { + mainViewModel.bluetoothStatus == BluetoothStatus.NOT_SUPPORTED -> { // Show permanent error for unsupported devices - errorMessage = message - onboardingState = OnboardingState.ERROR + mainViewModel.updateErrorMessage(message) + mainViewModel.updateOnboardingState(OnboardingState.ERROR) } message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> { // During first-time onboarding, if Bluetooth enable fails due to permissions, @@ -372,11 +360,11 @@ class MainActivity : ComponentActivity() { message.contains("Permission") -> { // For existing users, redirect to permission explanation to grant missing permissions android.util.Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation") - onboardingState = OnboardingState.PERMISSION_EXPLANATION + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } else -> { // Stay on Bluetooth check screen for retry - onboardingState = OnboardingState.BLUETOOTH_CHECK + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) } } } @@ -392,21 +380,21 @@ class MainActivity : ComponentActivity() { currentBluetoothStatus != BluetoothStatus.ENABLED -> { // Bluetooth still disabled, but now we have permissions to enable it android.util.Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") - bluetoothStatus = currentBluetoothStatus - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + mainViewModel.updateBluetoothStatus(currentBluetoothStatus) + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) + mainViewModel.updateBluetoothLoading(false) } currentLocationStatus != LocationStatus.ENABLED -> { // Location services still disabled, but now we have permissions to enable it android.util.Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.") - locationStatus = currentLocationStatus - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateLocationStatus(currentLocationStatus) + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } else -> { // Both are enabled, proceed to app initialization android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") - onboardingState = OnboardingState.INITIALIZING + mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING) initializeApp() } } @@ -414,8 +402,8 @@ class MainActivity : ComponentActivity() { private fun handleOnboardingFailed(message: String) { android.util.Log.e("MainActivity", "Onboarding failed: $message") - errorMessage = message - onboardingState = OnboardingState.ERROR + mainViewModel.updateErrorMessage(message) + mainViewModel.updateOnboardingState(OnboardingState.ERROR) } private fun initializeApp() { @@ -450,7 +438,7 @@ class MainActivity : ComponentActivity() { delay(500) android.util.Log.d("MainActivity", "App initialization complete") - onboardingState = OnboardingState.COMPLETE + mainViewModel.updateOnboardingState(OnboardingState.COMPLETE) } catch (e: Exception) { android.util.Log.e("MainActivity", "Failed to initialize app", e) @@ -462,7 +450,7 @@ class MainActivity : ComponentActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Handle notification intents when app is already running - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { handleNotificationIntent(intent) } } @@ -470,7 +458,7 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() // Check Bluetooth and Location status on resume and handle accordingly - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { // Set app foreground state meshService.connectionManager.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false) @@ -479,9 +467,9 @@ class MainActivity : ComponentActivity() { 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 + mainViewModel.updateBluetoothStatus(currentBluetoothStatus) + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) + mainViewModel.updateBluetoothLoading(false) return } @@ -489,9 +477,9 @@ class MainActivity : ComponentActivity() { val currentLocationStatus = locationStatusManager.checkLocationStatus() if (currentLocationStatus != LocationStatus.ENABLED) { android.util.Log.w("MainActivity", "Location services disabled while app was backgrounded") - locationStatus = currentLocationStatus - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateLocationStatus(currentLocationStatus) + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } } } @@ -499,7 +487,7 @@ class MainActivity : ComponentActivity() { override fun onPause() { super.onPause() // Only set background state if app is fully initialized - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { // Set app background state meshService.connectionManager.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true) @@ -535,7 +523,7 @@ class MainActivity : ComponentActivity() { * Restart mesh services (for debugging/troubleshooting) */ fun restartMeshServices() { - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { lifecycleScope.launch { try { android.util.Log.d("MainActivity", "Restarting mesh services") @@ -562,7 +550,7 @@ class MainActivity : ComponentActivity() { } // Stop mesh services if app was fully initialized - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { try { meshService.stopServices() android.util.Log.d("MainActivity", "Mesh services stopped successfully") diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt new file mode 100644 index 00000000..61e13d63 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt @@ -0,0 +1,52 @@ +package com.bitchat.android + +import androidx.compose.runtime.* +import androidx.lifecycle.ViewModel +import com.bitchat.android.onboarding.BluetoothStatus +import com.bitchat.android.onboarding.LocationStatus + +class MainViewModel : ViewModel() { + + private var _onboardingState by mutableStateOf(OnboardingState.CHECKING) + val onboardingState: OnboardingState get() = _onboardingState + + private var _bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) + val bluetoothStatus: BluetoothStatus get() = _bluetoothStatus + + private var _locationStatus by mutableStateOf(LocationStatus.ENABLED) + val locationStatus: LocationStatus get() = _locationStatus + + private var _errorMessage by mutableStateOf("") + val errorMessage: String get() = _errorMessage + + private var _isBluetoothLoading by mutableStateOf(false) + val isBluetoothLoading: Boolean get() = _isBluetoothLoading + + private var _isLocationLoading by mutableStateOf(false) + val isLocationLoading: Boolean get() = _isLocationLoading + + // Public update functions for MainActivity + fun updateOnboardingState(state: OnboardingState) { + _onboardingState = state + } + + fun updateBluetoothStatus(status: BluetoothStatus) { + _bluetoothStatus = status + } + + fun updateLocationStatus(status: LocationStatus) { + _locationStatus = status + } + + fun updateErrorMessage(message: String) { + _errorMessage = message + } + + fun updateBluetoothLoading(loading: Boolean) { + _isBluetoothLoading = loading + } + + fun updateLocationLoading(loading: Boolean) { + _isLocationLoading = loading + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/OnboardingState.kt b/app/src/main/java/com/bitchat/android/OnboardingState.kt new file mode 100644 index 00000000..a5a1e83b --- /dev/null +++ b/app/src/main/java/com/bitchat/android/OnboardingState.kt @@ -0,0 +1,12 @@ +package com.bitchat.android + +enum class OnboardingState { + CHECKING, + BLUETOOTH_CHECK, + LOCATION_CHECK, + PERMISSION_EXPLANATION, + PERMISSION_REQUESTING, + INITIALIZING, + COMPLETE, + ERROR +} \ No newline at end of file From 17472a18541e4ffcdc844458132cbb2a429c26c1 Mon Sep 17 00:00:00 2001 From: Mohamad Hamade Date: Tue, 15 Jul 2025 13:18:29 +0300 Subject: [PATCH 05/17] Refactor: Remove unused tripleClickCount state variable The `tripleClickCount` mutable state variable in the `ChatHeader` composable was not being used and has been removed to simplify the code --- app/src/main/java/com/bitchat/android/ui/ChatHeader.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 1b9fd098..ee9c699d 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -149,8 +149,7 @@ fun ChatHeaderContent( onShowAppInfo: () -> Unit ) { val colorScheme = MaterialTheme.colorScheme - var tripleClickCount by remember { mutableStateOf(0) } - + when { selectedPrivatePeer != null -> { // Private chat header - ensure state synchronization From b14eb9006b09a8b65225997cf676575e5115d198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Demat=C3=A9=20Jr?= Date: Tue, 15 Jul 2025 18:47:32 -0300 Subject: [PATCH 06/17] fix: update sidebar text to use string resources --- .../com/bitchat/android/ui/SidebarComponents.kt | 14 +++++++------- app/src/main/res/values/strings.xml | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 27e6927f..f34ed992 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -1,8 +1,7 @@ package com.bitchat.android.ui +import com.bitchat.android.R import android.util.Log -import androidx.compose.animation.* -import androidx.compose.animation.core.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -16,11 +15,12 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex + /** * Sidebar components for ChatScreen @@ -140,7 +140,7 @@ private fun SidebarHeader() { verticalAlignment = Alignment.CenterVertically ) { Text( - text = "YOUR NETWORK", + text = stringResource(id = R.string.your_network).uppercase(), style = MaterialTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold, fontFamily = FontFamily.Monospace @@ -175,7 +175,7 @@ fun ChannelsSection( ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = "CHANNELS", + text = stringResource(id = R.string.channels).uppercase(), style = MaterialTheme.typography.labelSmall, color = colorScheme.onSurface.copy(alpha = 0.6f), fontWeight = FontWeight.Bold @@ -255,7 +255,7 @@ fun PeopleSection( ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = "PEOPLE", + text = stringResource(id = R.string.people).uppercase(), style = MaterialTheme.typography.labelSmall, color = colorScheme.onSurface.copy(alpha = 0.6f), fontWeight = FontWeight.Bold @@ -264,7 +264,7 @@ fun PeopleSection( if (connectedPeers.isEmpty()) { Text( - text = "No one connected", + text = stringResource(id = R.string.no_one_connected), style = MaterialTheme.typography.bodyMedium, color = colorScheme.onSurface.copy(alpha = 0.5f), modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51c0a712..82b84804 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,4 +16,5 @@ Online Users No one connected Triple tap to clear all data + Your Network From d730acbb79f76790fede54aed862de48dec7251b Mon Sep 17 00:00:00 2001 From: Mohamad Hamade Date: Wed, 16 Jul 2025 01:13:42 +0300 Subject: [PATCH 07/17] Refactor: Migrate MainViewModel to use StateFlow This commit refactors the `MainViewModel` to use `StateFlow` instead of `mutableStateOf` for managing its observable properties. This change improves the way UI state is handled and observed, aligning with modern Android development best practices. Additionally, `OnboardingState.kt` has been moved to the `onboarding` package for better organization. --- .../java/com/bitchat/android/MainActivity.kt | 67 ++++++++++++++----- .../java/com/bitchat/android/MainViewModel.kt | 65 +++++++++--------- .../{ => onboarding}/OnboardingState.kt | 2 +- 3 files changed, 86 insertions(+), 48 deletions(-) rename app/src/main/java/com/bitchat/android/{ => onboarding}/OnboardingState.kt (82%) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 232fca28..44318781 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -10,9 +10,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.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.Lifecycle import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.onboarding.BluetoothCheckScreen import com.bitchat.android.onboarding.BluetoothStatus @@ -23,6 +27,7 @@ import com.bitchat.android.onboarding.LocationCheckScreen import com.bitchat.android.onboarding.LocationStatus import com.bitchat.android.onboarding.LocationStatusManager import com.bitchat.android.onboarding.OnboardingCoordinator +import com.bitchat.android.onboarding.OnboardingState import com.bitchat.android.onboarding.PermissionExplanationScreen import com.bitchat.android.onboarding.PermissionManager import com.bitchat.android.ui.ChatScreen @@ -88,23 +93,39 @@ class MainActivity : ComponentActivity() { } } + // Collect state changes in a lifecycle-aware manner + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + mainViewModel.onboardingState.collect { state -> + handleOnboardingStateChange(state) + } + } + } + // Only start onboarding process if we're in the initial CHECKING state // This prevents restarting onboarding on configuration changes - if (mainViewModel.onboardingState == OnboardingState.CHECKING) { + if (mainViewModel.onboardingState.value == OnboardingState.CHECKING) { checkOnboardingStatus() } } @Composable private fun OnboardingFlowScreen() { - when (mainViewModel.onboardingState) { + val onboardingState by mainViewModel.onboardingState.collectAsState() + val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState() + val locationStatus by mainViewModel.locationStatus.collectAsState() + val errorMessage by mainViewModel.errorMessage.collectAsState() + val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState() + val isLocationLoading by mainViewModel.isLocationLoading.collectAsState() + + when (onboardingState) { OnboardingState.CHECKING -> { InitializingScreen() } OnboardingState.BLUETOOTH_CHECK -> { BluetoothCheckScreen( - status = mainViewModel.bluetoothStatus, + status = bluetoothStatus, onEnableBluetooth = { mainViewModel.updateBluetoothLoading(true) bluetoothStatusManager.requestEnableBluetooth() @@ -112,13 +133,13 @@ class MainActivity : ComponentActivity() { onRetry = { checkBluetoothAndProceed() }, - isLoading = mainViewModel.isBluetoothLoading + isLoading = isBluetoothLoading ) } OnboardingState.LOCATION_CHECK -> { LocationCheckScreen( - status = mainViewModel.locationStatus, + status = locationStatus, onEnableLocation = { mainViewModel.updateLocationLoading(true) locationStatusManager.requestEnableLocation() @@ -126,7 +147,7 @@ class MainActivity : ComponentActivity() { onRetry = { checkLocationAndProceed() }, - isLoading = mainViewModel.isLocationLoading + isLoading = isLocationLoading ) } @@ -172,7 +193,7 @@ class MainActivity : ComponentActivity() { OnboardingState.ERROR -> { InitializationErrorScreen( - errorMessage = mainViewModel.errorMessage, + errorMessage = errorMessage, onRetry = { mainViewModel.updateOnboardingState(OnboardingState.CHECKING) checkOnboardingStatus() @@ -185,6 +206,20 @@ class MainActivity : ComponentActivity() { } } + private fun handleOnboardingStateChange(state: OnboardingState) { + + when (state) { + OnboardingState.COMPLETE -> { + // App is fully initialized, mesh service is running + android.util.Log.d("MainActivity", "Onboarding completed - app ready") + } + OnboardingState.ERROR -> { + android.util.Log.e("MainActivity", "Onboarding error state reached") + } + else -> {} + } + } + private fun checkOnboardingStatus() { android.util.Log.d("MainActivity", "Checking onboarding status") @@ -215,7 +250,7 @@ class MainActivity : ComponentActivity() { bluetoothStatusManager.logBluetoothStatus() mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) - when (mainViewModel.bluetoothStatus) { + when (mainViewModel.bluetoothStatus.value) { BluetoothStatus.ENABLED -> { // Bluetooth is enabled, check location services next checkLocationAndProceed() @@ -286,7 +321,7 @@ class MainActivity : ComponentActivity() { locationStatusManager.logLocationStatus() mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) - when (mainViewModel.locationStatus) { + when (mainViewModel.locationStatus.value) { LocationStatus.ENABLED -> { // Location services enabled, proceed with permission/onboarding check proceedWithPermissionCheck() @@ -325,7 +360,7 @@ class MainActivity : ComponentActivity() { mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) when { - mainViewModel.locationStatus == LocationStatus.NOT_AVAILABLE -> { + mainViewModel.locationStatus.value == LocationStatus.NOT_AVAILABLE -> { // Show permanent error for devices without location services mainViewModel.updateErrorMessage(message) mainViewModel.updateOnboardingState(OnboardingState.ERROR) @@ -346,7 +381,7 @@ class MainActivity : ComponentActivity() { mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) when { - mainViewModel.bluetoothStatus == BluetoothStatus.NOT_SUPPORTED -> { + mainViewModel.bluetoothStatus.value == BluetoothStatus.NOT_SUPPORTED -> { // Show permanent error for unsupported devices mainViewModel.updateErrorMessage(message) mainViewModel.updateOnboardingState(OnboardingState.ERROR) @@ -450,7 +485,7 @@ class MainActivity : ComponentActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Handle notification intents when app is already running - if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { handleNotificationIntent(intent) } } @@ -458,7 +493,7 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() // Check Bluetooth and Location status on resume and handle accordingly - if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { // Set app foreground state meshService.connectionManager.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false) @@ -487,7 +522,7 @@ class MainActivity : ComponentActivity() { override fun onPause() { super.onPause() // Only set background state if app is fully initialized - if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { // Set app background state meshService.connectionManager.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true) @@ -523,7 +558,7 @@ class MainActivity : ComponentActivity() { * Restart mesh services (for debugging/troubleshooting) */ fun restartMeshServices() { - if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { lifecycleScope.launch { try { android.util.Log.d("MainActivity", "Restarting mesh services") @@ -550,7 +585,7 @@ class MainActivity : ComponentActivity() { } // Stop mesh services if app was fully initialized - if (mainViewModel.onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { try { meshService.stopServices() android.util.Log.d("MainActivity", "Mesh services stopped successfully") diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt index 61e13d63..62177c18 100644 --- a/app/src/main/java/com/bitchat/android/MainViewModel.kt +++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt @@ -1,52 +1,55 @@ package com.bitchat.android -import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.LocationStatus +import com.bitchat.android.onboarding.OnboardingState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow class MainViewModel : ViewModel() { - - private var _onboardingState by mutableStateOf(OnboardingState.CHECKING) - val onboardingState: OnboardingState get() = _onboardingState - - private var _bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) - val bluetoothStatus: BluetoothStatus get() = _bluetoothStatus - - private var _locationStatus by mutableStateOf(LocationStatus.ENABLED) - val locationStatus: LocationStatus get() = _locationStatus - - private var _errorMessage by mutableStateOf("") - val errorMessage: String get() = _errorMessage - - private var _isBluetoothLoading by mutableStateOf(false) - val isBluetoothLoading: Boolean get() = _isBluetoothLoading - - private var _isLocationLoading by mutableStateOf(false) - val isLocationLoading: Boolean get() = _isLocationLoading - + + private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING) + val onboardingState: StateFlow = _onboardingState.asStateFlow() + + private val _bluetoothStatus = MutableStateFlow(BluetoothStatus.ENABLED) + val bluetoothStatus: StateFlow = _bluetoothStatus.asStateFlow() + + private val _locationStatus = MutableStateFlow(LocationStatus.ENABLED) + val locationStatus: StateFlow = _locationStatus.asStateFlow() + + private val _errorMessage = MutableStateFlow("") + val errorMessage: StateFlow = _errorMessage.asStateFlow() + + private val _isBluetoothLoading = MutableStateFlow(false) + val isBluetoothLoading: StateFlow = _isBluetoothLoading.asStateFlow() + + private val _isLocationLoading = MutableStateFlow(false) + val isLocationLoading: StateFlow = _isLocationLoading.asStateFlow() + // Public update functions for MainActivity fun updateOnboardingState(state: OnboardingState) { - _onboardingState = state + _onboardingState.value = state } - + fun updateBluetoothStatus(status: BluetoothStatus) { - _bluetoothStatus = status + _bluetoothStatus.value = status } - + fun updateLocationStatus(status: LocationStatus) { - _locationStatus = status + _locationStatus.value = status } - + fun updateErrorMessage(message: String) { - _errorMessage = message + _errorMessage.value = message } - + fun updateBluetoothLoading(loading: Boolean) { - _isBluetoothLoading = loading + _isBluetoothLoading.value = loading } - + fun updateLocationLoading(loading: Boolean) { - _isLocationLoading = loading + _isLocationLoading.value = loading } } \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/OnboardingState.kt b/app/src/main/java/com/bitchat/android/onboarding/OnboardingState.kt similarity index 82% rename from app/src/main/java/com/bitchat/android/OnboardingState.kt rename to app/src/main/java/com/bitchat/android/onboarding/OnboardingState.kt index a5a1e83b..cb8e73b5 100644 --- a/app/src/main/java/com/bitchat/android/OnboardingState.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/OnboardingState.kt @@ -1,4 +1,4 @@ -package com.bitchat.android +package com.bitchat.android.onboarding enum class OnboardingState { CHECKING, From eb575182c613091e42a5f95af72f09e17caccc12 Mon Sep 17 00:00:00 2001 From: alpermelkeli <108495629+alpermelkeli@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:30:26 +0300 Subject: [PATCH 08/17] refactor: remove context attribute at ChatViewModel.kt --- app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 190f696d..d8fc1634 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -24,14 +24,12 @@ class ChatViewModel( application: Application, val meshService: BluetoothMeshService ) : AndroidViewModel(application), BluetoothMeshDelegate { - - private val context: Context = application.applicationContext - + // State management private val state = ChatState() // Specialized managers - private val dataManager = DataManager(context) + private val dataManager = DataManager(application.applicationContext) private val messageManager = MessageManager(state) private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope) val privateChatManager = PrivateChatManager(state, messageManager, dataManager) @@ -46,7 +44,7 @@ class ChatViewModel( privateChatManager = privateChatManager, notificationManager = notificationManager, coroutineScope = viewModelScope, - onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(context) }, + onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(application.applicationContext) }, getMyPeerID = { meshService.myPeerID } ) From d0dac19119857f3fb6db475188e38f31c8ed0aa9 Mon Sep 17 00:00:00 2001 From: Hector de Isidro <5445152+hector6872@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:07:04 +0200 Subject: [PATCH 09/17] Replace BoxWithConstraints with Box since constraints are unused --- app/src/main/java/com/bitchat/android/ui/ChatScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 4c9e30dc..cad1de71 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -86,7 +86,7 @@ fun ChatScreen(viewModel: ChatViewModel) { } // Use WindowInsets to handle keyboard properly - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize()) { val headerHeight = 36.dp // Main content area that responds to keyboard/window insets From 15a989c5cdfba6f4aeb3b0195987a817dc17e297 Mon Sep 17 00:00:00 2001 From: Hector de Isidro <5445152+hector6872@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:08:20 +0200 Subject: [PATCH 10/17] Remove unnecessary Box wrapper --- .../main/java/com/bitchat/android/ui/ChatScreen.kt | 14 ++++++-------- .../com/bitchat/android/ui/MessageComponents.kt | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index cad1de71..99d2d909 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -100,14 +100,12 @@ fun ChatScreen(viewModel: ChatViewModel) { Spacer(modifier = Modifier.height(headerHeight)) // Messages area - takes up available space, will compress when keyboard appears - Box(modifier = Modifier.weight(1f)) { - MessagesList( - messages = displayMessages, - currentUserNickname = nickname, - meshService = viewModel.meshService, - modifier = Modifier.fillMaxSize() - ) - } + MessagesList( + messages = displayMessages, + currentUserNickname = nickname, + meshService = viewModel.meshService, + modifier = Modifier.weight(1f) + ) // Input area - stays at bottom ChatInputSection( diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index ba97657f..7dd07898 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -42,10 +42,10 @@ fun MessagesList( } } - SelectionContainer { + SelectionContainer(modifier = modifier) { LazyColumn( state = listState, - modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(2.dp) ) { items(messages) { message -> From 37e3852f4088c8d6bda7d5cacdb4dd818bde16c3 Mon Sep 17 00:00:00 2001 From: Hector de Isidro <5445152+hector6872@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:11:26 +0200 Subject: [PATCH 11/17] Use contentPadding instead of padding so items remain fully visible --- app/src/main/java/com/bitchat/android/ui/MessageComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index 7dd07898..0cb9e977 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -45,7 +45,7 @@ fun MessagesList( SelectionContainer(modifier = modifier) { LazyColumn( state = listState, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(2.dp) ) { items(messages) { message -> From f2a8f86f17ffb182c1848e61a37372f3120da44b Mon Sep 17 00:00:00 2001 From: Hector de Isidro <5445152+hector6872@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:17:56 +0200 Subject: [PATCH 12/17] Use HorizontalDivider instead of deprecated Divider --- .../main/java/com/bitchat/android/ui/ChatScreen.kt | 12 ++++++------ .../java/com/bitchat/android/ui/SidebarComponents.kt | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 99d2d909..33793f4c 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -210,7 +210,7 @@ private fun ChatInputSection( shadowElevation = 8.dp ) { Column { - Divider(color = colorScheme.outline.copy(alpha = 0.3f)) + HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.3f)) // Command suggestions box if (showCommandSuggestions && commandSuggestions.isNotEmpty()) { @@ -219,8 +219,8 @@ private fun ChatInputSection( onSuggestionClick = onSuggestionClick, modifier = Modifier.fillMaxWidth() ) - - Divider(color = colorScheme.outline.copy(alpha = 0.2f)) + + HorizontalDivider(color = colorScheme.outline.copy(alpha = 0.2f)) } MessageInput( @@ -283,12 +283,12 @@ private fun ChatFloatingHeader( } // Divider under header - Divider( - color = colorScheme.outline.copy(alpha = 0.3f), + HorizontalDivider( modifier = Modifier .fillMaxWidth() .offset(y = headerHeight) - .zIndex(1f) + .zIndex(1f), + color = colorScheme.outline.copy(alpha = 0.3f) ) } diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 27e6927f..d0a3afc7 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -73,8 +73,8 @@ fun SidebarOverlay( .windowInsetsPadding(WindowInsets.statusBars) // Add status bar padding ) { SidebarHeader() - - Divider() + + HorizontalDivider() // Scrollable content LazyColumn( @@ -101,7 +101,7 @@ fun SidebarOverlay( } item { - Divider(modifier = Modifier.padding(vertical = 4.dp)) + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) } } From 305b334244672c270b4fc805cc5e106db18da1f6 Mon Sep 17 00:00:00 2001 From: Hector de Isidro <5445152+hector6872@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:20:09 +0200 Subject: [PATCH 13/17] Remove redundant @OptIn annotation --- app/src/main/java/com/bitchat/android/ui/ChatScreen.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 33793f4c..e05e15b1 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -46,7 +46,6 @@ import java.util.* * - DialogComponents: Password prompts and modals * - ChatUIUtils: Utility functions for formatting and colors */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ChatScreen(viewModel: ChatViewModel) { val colorScheme = MaterialTheme.colorScheme @@ -190,7 +189,6 @@ fun ChatScreen(viewModel: ChatViewModel) { ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChatInputSection( messageText: String, From 0611baa681a53aea3af6bdf4431e6191ec447bc4 Mon Sep 17 00:00:00 2001 From: Mohamad Hamade Date: Thu, 17 Jul 2025 13:49:31 +0300 Subject: [PATCH 14/17] fix: center align toolbar item in ChatHeader - passed modifier.fillmaxHeight so the content inside the row can actually be centered --- app/src/main/java/com/bitchat/android/ui/ChatHeader.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 65e2ee8e..895e3fc6 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -360,7 +360,10 @@ private fun MainHeader( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = Modifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically + ) { Text( text = "bitchat*", style = MaterialTheme.typography.headlineSmall, From 3fddb3f594359e1efc125095a0ec417c72b7e148 Mon Sep 17 00:00:00 2001 From: ganpatsingh <2019uee1399@mnit.ac.in> Date: Fri, 18 Jul 2025 00:50:57 +0530 Subject: [PATCH 15/17] 1. remove unused methods in MainActivity.kt 2. replace android.util.log with log --- .../java/com/bitchat/android/MainActivity.kt | 96 ++++++++----------- 1 file changed, 39 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 1ed97a92..66084427 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -3,6 +3,7 @@ package com.bitchat.android import android.Manifest import android.content.Intent import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts @@ -198,7 +199,7 @@ class MainActivity : ComponentActivity() { } private fun checkOnboardingStatus() { - android.util.Log.d("MainActivity", "Checking onboarding status") + Log.d("MainActivity", "Checking onboarding status") lifecycleScope.launch { // Small delay to show the checking state @@ -213,12 +214,12 @@ class MainActivity : ComponentActivity() { * Check Bluetooth status and proceed with onboarding flow */ private fun checkBluetoothAndProceed() { - // android.util.Log.d("MainActivity", "Checking Bluetooth status") + // Log.d("MainActivity", "Checking Bluetooth status") // For first-time users, skip Bluetooth check and go straight to permissions // We'll check Bluetooth after permissions are granted if (permissionManager.isFirstTimeLaunch()) { - android.util.Log.d("MainActivity", "First-time launch, skipping Bluetooth check - will check after permissions") + Log.d("MainActivity", "First-time launch, skipping Bluetooth check - will check after permissions") proceedWithPermissionCheck() return } @@ -234,13 +235,13 @@ class MainActivity : ComponentActivity() { } BluetoothStatus.DISABLED -> { // Show Bluetooth enable screen (should have permissions as existing user) - android.util.Log.d("MainActivity", "Bluetooth disabled, showing enable screen") + 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") + Log.e("MainActivity", "Bluetooth not supported") onboardingState = OnboardingState.BLUETOOTH_CHECK isBluetoothLoading = false } @@ -251,20 +252,20 @@ class MainActivity : ComponentActivity() { * Proceed with permission checking */ private fun proceedWithPermissionCheck() { - android.util.Log.d("MainActivity", "Proceeding with permission check") + Log.d("MainActivity", "Proceeding with permission check") lifecycleScope.launch { delay(200) // Small delay for smooth transition if (permissionManager.isFirstTimeLaunch()) { - android.util.Log.d("MainActivity", "First time launch, showing permission explanation") + Log.d("MainActivity", "First time launch, showing permission explanation") onboardingState = OnboardingState.PERMISSION_EXPLANATION } else if (permissionManager.areAllPermissionsGranted()) { - android.util.Log.d("MainActivity", "Existing user with permissions, initializing app") + Log.d("MainActivity", "Existing user with permissions, initializing app") onboardingState = OnboardingState.INITIALIZING initializeApp() } else { - android.util.Log.d("MainActivity", "Existing user missing permissions, showing explanation") + Log.d("MainActivity", "Existing user missing permissions, showing explanation") onboardingState = OnboardingState.PERMISSION_EXPLANATION } } @@ -274,7 +275,7 @@ class MainActivity : ComponentActivity() { * Handle Bluetooth enabled callback */ private fun handleBluetoothEnabled() { - android.util.Log.d("MainActivity", "Bluetooth enabled by user") + Log.d("MainActivity", "Bluetooth enabled by user") isBluetoothLoading = false bluetoothStatus = BluetoothStatus.ENABLED checkLocationAndProceed() @@ -284,12 +285,12 @@ class MainActivity : ComponentActivity() { * Check Location services status and proceed with onboarding flow */ private fun checkLocationAndProceed() { - android.util.Log.d("MainActivity", "Checking location services status") + Log.d("MainActivity", "Checking location services status") // For first-time users, skip location check and go straight to permissions // We'll check location after permissions are granted if (permissionManager.isFirstTimeLaunch()) { - android.util.Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions") + Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions") proceedWithPermissionCheck() return } @@ -305,13 +306,13 @@ class MainActivity : ComponentActivity() { } LocationStatus.DISABLED -> { // Show location enable screen (should have permissions as existing user) - android.util.Log.d("MainActivity", "Location services disabled, showing enable screen") + Log.d("MainActivity", "Location services disabled, showing enable screen") onboardingState = OnboardingState.LOCATION_CHECK isLocationLoading = false } LocationStatus.NOT_AVAILABLE -> { // Device doesn't support location services (very unusual) - android.util.Log.e("MainActivity", "Location services not available") + Log.e("MainActivity", "Location services not available") onboardingState = OnboardingState.LOCATION_CHECK isLocationLoading = false } @@ -322,7 +323,7 @@ class MainActivity : ComponentActivity() { * Handle Location enabled callback */ private fun handleLocationEnabled() { - android.util.Log.d("MainActivity", "Location services enabled by user") + Log.d("MainActivity", "Location services enabled by user") isLocationLoading = false locationStatus = LocationStatus.ENABLED proceedWithPermissionCheck() @@ -332,7 +333,7 @@ class MainActivity : ComponentActivity() { * Handle Location disabled callback */ private fun handleLocationDisabled(message: String) { - android.util.Log.w("MainActivity", "Location services disabled or failed: $message") + Log.w("MainActivity", "Location services disabled or failed: $message") isLocationLoading = false locationStatus = locationStatusManager.checkLocationStatus() @@ -353,7 +354,7 @@ class MainActivity : ComponentActivity() { * Handle Bluetooth disabled callback */ private fun handleBluetoothDisabled(message: String) { - android.util.Log.w("MainActivity", "Bluetooth disabled or failed: $message") + Log.w("MainActivity", "Bluetooth disabled or failed: $message") isBluetoothLoading = false bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() @@ -366,12 +367,12 @@ class MainActivity : ComponentActivity() { message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> { // During first-time onboarding, if Bluetooth enable fails due to permissions, // proceed to permission explanation screen where user will grant permissions first - android.util.Log.d("MainActivity", "Bluetooth enable requires permissions, proceeding to permission explanation") + Log.d("MainActivity", "Bluetooth enable requires permissions, proceeding to permission explanation") proceedWithPermissionCheck() } message.contains("Permission") -> { // For existing users, redirect to permission explanation to grant missing permissions - android.util.Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation") + Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation") onboardingState = OnboardingState.PERMISSION_EXPLANATION } else -> { @@ -382,7 +383,7 @@ class MainActivity : ComponentActivity() { } private fun handleOnboardingComplete() { - android.util.Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app") + Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app") // After permissions are granted, re-check both Bluetooth and Location status val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() @@ -391,21 +392,21 @@ class MainActivity : ComponentActivity() { when { currentBluetoothStatus != BluetoothStatus.ENABLED -> { // Bluetooth still disabled, but now we have permissions to enable it - android.util.Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") + Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") bluetoothStatus = currentBluetoothStatus onboardingState = OnboardingState.BLUETOOTH_CHECK isBluetoothLoading = false } currentLocationStatus != LocationStatus.ENABLED -> { // Location services still disabled, but now we have permissions to enable it - android.util.Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.") + Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.") locationStatus = currentLocationStatus onboardingState = OnboardingState.LOCATION_CHECK isLocationLoading = false } else -> { // Both are enabled, proceed to app initialization - android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") + Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") onboardingState = OnboardingState.INITIALIZING initializeApp() } @@ -413,13 +414,13 @@ class MainActivity : ComponentActivity() { } private fun handleOnboardingFailed(message: String) { - android.util.Log.e("MainActivity", "Onboarding failed: $message") + Log.e("MainActivity", "Onboarding failed: $message") errorMessage = message onboardingState = OnboardingState.ERROR } private fun initializeApp() { - android.util.Log.d("MainActivity", "Starting app initialization") + Log.d("MainActivity", "Starting app initialization") lifecycleScope.launch { try { @@ -427,12 +428,12 @@ class MainActivity : ComponentActivity() { // This solves the issue where app needs restart to work on first install delay(1000) // Give the system time to process permission grants - android.util.Log.d("MainActivity", "Permissions verified, initializing chat system") + Log.d("MainActivity", "Permissions verified, initializing chat system") // Ensure all permissions are still granted (user might have revoked in settings) if (!permissionManager.areAllPermissionsGranted()) { val missing = permissionManager.getMissingPermissions() - android.util.Log.w("MainActivity", "Permissions revoked during initialization: $missing") + Log.w("MainActivity", "Permissions revoked during initialization: $missing") handleOnboardingFailed("Some permissions were revoked. Please grant all permissions to continue.") return@launch } @@ -441,7 +442,7 @@ class MainActivity : ComponentActivity() { meshService.delegate = chatViewModel meshService.startServices() - android.util.Log.d("MainActivity", "Mesh service started successfully") + Log.d("MainActivity", "Mesh service started successfully") // Handle any notification intent handleNotificationIntent(intent) @@ -449,11 +450,11 @@ class MainActivity : ComponentActivity() { // Small delay to ensure mesh service is fully initialized delay(500) - android.util.Log.d("MainActivity", "App initialization complete") + Log.d("MainActivity", "App initialization complete") onboardingState = OnboardingState.COMPLETE } catch (e: Exception) { - android.util.Log.e("MainActivity", "Failed to initialize app", e) + Log.e("MainActivity", "Failed to initialize app", e) handleOnboardingFailed("Failed to initialize the app: ${e.message}") } } @@ -474,11 +475,11 @@ class MainActivity : ComponentActivity() { // Set app foreground state meshService.connectionManager.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") + Log.w("MainActivity", "Bluetooth disabled while app was backgrounded") bluetoothStatus = currentBluetoothStatus onboardingState = OnboardingState.BLUETOOTH_CHECK isBluetoothLoading = false @@ -488,7 +489,7 @@ class MainActivity : ComponentActivity() { // Check if location services were disabled while app was backgrounded val currentLocationStatus = locationStatusManager.checkLocationStatus() if (currentLocationStatus != LocationStatus.ENABLED) { - android.util.Log.w("MainActivity", "Location services disabled while app was backgrounded") + Log.w("MainActivity", "Location services disabled while app was backgrounded") locationStatus = currentLocationStatus onboardingState = OnboardingState.LOCATION_CHECK isLocationLoading = false @@ -520,7 +521,7 @@ class MainActivity : ComponentActivity() { val senderNickname = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_SENDER_NICKNAME) if (peerID != null) { - android.util.Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification") + Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification") // Open the private chat with this peer chatViewModel.startPrivateChat(peerID) @@ -530,25 +531,6 @@ class MainActivity : ComponentActivity() { } } } - - /** - * Restart mesh services (for debugging/troubleshooting) - */ - fun restartMeshServices() { - if (onboardingState == OnboardingState.COMPLETE) { - lifecycleScope.launch { - try { - android.util.Log.d("MainActivity", "Restarting mesh services") - meshService.stopServices() - delay(1000) - meshService.startServices() - android.util.Log.d("MainActivity", "Mesh services restarted successfully") - } catch (e: Exception) { - android.util.Log.e("MainActivity", "Error restarting mesh services: ${e.message}") - } - } - } - } override fun onDestroy() { super.onDestroy() @@ -556,18 +538,18 @@ class MainActivity : ComponentActivity() { // Cleanup location status manager try { locationStatusManager.cleanup() - android.util.Log.d("MainActivity", "Location status manager cleaned up successfully") + Log.d("MainActivity", "Location status manager cleaned up successfully") } catch (e: Exception) { - android.util.Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}") + Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}") } // Stop mesh services if app was fully initialized if (onboardingState == OnboardingState.COMPLETE) { try { meshService.stopServices() - android.util.Log.d("MainActivity", "Mesh services stopped successfully") + Log.d("MainActivity", "Mesh services stopped successfully") } catch (e: Exception) { - android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}") + Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}") } } } From d8d834b713843a574c32555d92eac0d2d2a70bdb Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Jul 2025 09:20:57 +0200 Subject: [PATCH 16/17] bump to 0.7.1 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f0e3945..e75b92ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.bitchat.android" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 3 - versionName = "0.7" + versionCode = 4 + versionName = "0.7.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 1775ecfe44ac13b72588c782c4d36af44274da2d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 19 Jul 2025 09:53:28 +0200 Subject: [PATCH 17/17] changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa5fd491..c8f60c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [0.7.1] - 2025-07-19 + +### Added +-feat(battery): add battery optimization management for background reliability + +### Fixed +- fix: center align toolbar item in ChatHeader - passed modifier.fillmaxHeight so the content inside the row can actually be centered +- fix: update sidebar text to use string resources +- fix(chat): cursor location and enhance message input with slash command styling + +### Changed +- refactor: remove context attribute at ChatViewModel.kt +- Refactor: Migrate MainViewModel to use StateFlow + +### Improved +- Use HorizontalDivider instead of deprecated Divider +- Use contentPadding instead of padding so items remain fully visible + + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.7]