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
This commit is contained in:
Faded
2025-07-14 14:23:59 +05:00
parent 013f0c00cf
commit fe57c5684e
6 changed files with 628 additions and 6 deletions
@@ -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<Intent>? = 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
}
@@ -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")
}
}
}
@@ -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
}
}
@@ -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")
}