From fe57c5684e111dac2b125bcd66f43eae65d077e4 Mon Sep 17 00:00:00 2001 From: Faded Date: Mon, 14 Jul 2025 14:23:59 +0500 Subject: [PATCH 1/2] 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 2/2] 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