diff --git a/app/src/main/java/com/cashpilot/android/ui/MainActivity.kt b/app/src/main/java/com/cashpilot/android/ui/MainActivity.kt index 3574e63..7c47878 100644 --- a/app/src/main/java/com/cashpilot/android/ui/MainActivity.kt +++ b/app/src/main/java/com/cashpilot/android/ui/MainActivity.kt @@ -69,11 +69,12 @@ class MainActivity : ComponentActivity() { val settings by viewModel.settings.collectAsState() val hasNotif by viewModel.hasNotificationAccess.collectAsState() val hasUsage by viewModel.hasUsageAccess.collectAsState() + val hasBattery by viewModel.hasBatteryOptOut.collectAsState() var showSettings by rememberSaveable { mutableStateOf(false) } var setupDismissed by rememberSaveable { mutableStateOf(false) } val needsSetup = !setupDismissed && - (settings.serverUrl.isBlank() || settings.apiKey.isBlank() || !hasNotif || !hasUsage) + (settings.serverUrl.isBlank() || settings.apiKey.isBlank() || !hasNotif || !hasUsage || !hasBattery) // Handle system Back from Settings → return to Dashboard BackHandler(enabled = showSettings && !needsSetup) { diff --git a/app/src/main/java/com/cashpilot/android/ui/MainViewModel.kt b/app/src/main/java/com/cashpilot/android/ui/MainViewModel.kt index 52a13a2..59cb37e 100644 --- a/app/src/main/java/com/cashpilot/android/ui/MainViewModel.kt +++ b/app/src/main/java/com/cashpilot/android/ui/MainViewModel.kt @@ -2,6 +2,7 @@ package com.cashpilot.android.ui import android.app.AppOpsManager import android.app.Application +import android.os.PowerManager import android.content.ComponentName import android.content.Context import android.os.Build @@ -74,6 +75,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { private val _hasUsageAccess = MutableStateFlow(false) val hasUsageAccess: StateFlow = _hasUsageAccess.asStateFlow() + private val _hasBatteryOptOut = MutableStateFlow(false) + val hasBatteryOptOut: StateFlow = _hasBatteryOptOut.asStateFlow() + val lastHeartbeat: StateFlow = HeartbeatService.lastHeartbeat val lastHeartbeatFailed: StateFlow = HeartbeatService.lastHeartbeatFailed @@ -225,5 +229,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } else { false } + + val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager + _hasBatteryOptOut.value = pm?.isIgnoringBatteryOptimizations(ctx.packageName) ?: false } } diff --git a/app/src/main/java/com/cashpilot/android/ui/screen/SetupScreen.kt b/app/src/main/java/com/cashpilot/android/ui/screen/SetupScreen.kt index 7d8d307..5ac0b70 100644 --- a/app/src/main/java/com/cashpilot/android/ui/screen/SetupScreen.kt +++ b/app/src/main/java/com/cashpilot/android/ui/screen/SetupScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.BatteryAlert import androidx.compose.material.icons.filled.QueryStats import androidx.compose.material3.Button import androidx.compose.material3.Card @@ -52,6 +53,7 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) { val settings by viewModel.settings.collectAsState() val hasNotif by viewModel.hasNotificationAccess.collectAsState() val hasUsage by viewModel.hasUsageAccess.collectAsState() + val hasBattery by viewModel.hasBatteryOptOut.collectAsState() val context = LocalContext.current var localUrl by rememberSaveable { mutableStateOf("") } @@ -164,6 +166,23 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) { } } + // Step 4: Battery optimization + SetupCard( + step = 4, + icon = Icons.Default.BatteryAlert, + title = stringResource(R.string.setup_battery_title), + description = stringResource(R.string.setup_battery_desc), + done = hasBattery, + ) { + TextButton( + onClick = { openBatteryOptimizationSettings(context) }, + ) { + Text(stringResource(R.string.setup_grant_access)) + Spacer(Modifier.width(4.dp)) + Icon(Icons.Default.ChevronRight, null, Modifier.size(18.dp)) + } + } + Spacer(Modifier.height(8.dp)) Button( @@ -173,7 +192,7 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) { onComplete() }, modifier = Modifier.fillMaxWidth(), - enabled = serverDone && hasNotif && hasUsage, + enabled = serverDone && hasNotif && hasUsage && hasBattery, ) { Text(stringResource(R.string.setup_continue)) } @@ -263,3 +282,10 @@ private fun openUsageAccessSettings(context: Context) { .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), ) } + +private fun openBatteryOptimizationSettings(context: Context) { + context.startActivity( + Intent(AndroidSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34eab1f..1e0d5e2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,8 @@ CashPilot reads foreground notifications to detect when passive income apps are running. No notification content is collected — only presence is checked. Usage Access Allows CashPilot to check when apps were last active and measure per-app network usage. This data stays on your device and is only sent to your own server. + Battery Optimization + Disable battery optimization so Android doesn\'t kill CashPilot\'s background service. Without this, heartbeats may stop when the screen is off. Open Settings Continue to Dashboard Skip for now