Skip to content

Commit f20aa0e

Browse files
authored
Add battery optimization to setup wizard (#7)
* Add battery optimization as Step 4 in setup wizard Detects whether battery optimization is disabled via PowerManager and shows it as a required step before continuing to dashboard. * Enforce battery optimization in setup gating MainActivity needsSetup now includes hasBatteryOptOut check, matching SetupScreen's Continue button requirement.
1 parent db6aa1e commit f20aa0e

4 files changed

Lines changed: 38 additions & 2 deletions

File tree

app/src/main/java/com/cashpilot/android/ui/MainActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ class MainActivity : ComponentActivity() {
6969
val settings by viewModel.settings.collectAsState()
7070
val hasNotif by viewModel.hasNotificationAccess.collectAsState()
7171
val hasUsage by viewModel.hasUsageAccess.collectAsState()
72+
val hasBattery by viewModel.hasBatteryOptOut.collectAsState()
7273
var showSettings by rememberSaveable { mutableStateOf(false) }
7374
var setupDismissed by rememberSaveable { mutableStateOf(false) }
7475

7576
val needsSetup = !setupDismissed &&
76-
(settings.serverUrl.isBlank() || settings.apiKey.isBlank() || !hasNotif || !hasUsage)
77+
(settings.serverUrl.isBlank() || settings.apiKey.isBlank() || !hasNotif || !hasUsage || !hasBattery)
7778

7879
// Handle system Back from Settings → return to Dashboard
7980
BackHandler(enabled = showSettings && !needsSetup) {

app/src/main/java/com/cashpilot/android/ui/MainViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.cashpilot.android.ui
22

33
import android.app.AppOpsManager
44
import android.app.Application
5+
import android.os.PowerManager
56
import android.content.ComponentName
67
import android.content.Context
78
import android.os.Build
@@ -74,6 +75,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
7475
private val _hasUsageAccess = MutableStateFlow(false)
7576
val hasUsageAccess: StateFlow<Boolean> = _hasUsageAccess.asStateFlow()
7677

78+
private val _hasBatteryOptOut = MutableStateFlow(false)
79+
val hasBatteryOptOut: StateFlow<Boolean> = _hasBatteryOptOut.asStateFlow()
80+
7781
val lastHeartbeat: StateFlow<Long> = HeartbeatService.lastHeartbeat
7882
val lastHeartbeatFailed: StateFlow<Boolean> = HeartbeatService.lastHeartbeatFailed
7983

@@ -225,5 +229,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
225229
} else {
226230
false
227231
}
232+
233+
val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager
234+
_hasBatteryOptOut.value = pm?.isIgnoringBatteryOptimizations(ctx.packageName) ?: false
228235
}
229236
}

app/src/main/java/com/cashpilot/android/ui/screen/SetupScreen.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.compose.material.icons.filled.CheckCircle
2020
import androidx.compose.material.icons.filled.ChevronRight
2121
import androidx.compose.material.icons.filled.Cloud
2222
import androidx.compose.material.icons.filled.Notifications
23+
import androidx.compose.material.icons.filled.BatteryAlert
2324
import androidx.compose.material.icons.filled.QueryStats
2425
import androidx.compose.material3.Button
2526
import androidx.compose.material3.Card
@@ -52,6 +53,7 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) {
5253
val settings by viewModel.settings.collectAsState()
5354
val hasNotif by viewModel.hasNotificationAccess.collectAsState()
5455
val hasUsage by viewModel.hasUsageAccess.collectAsState()
56+
val hasBattery by viewModel.hasBatteryOptOut.collectAsState()
5557
val context = LocalContext.current
5658

5759
var localUrl by rememberSaveable { mutableStateOf("") }
@@ -164,6 +166,23 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) {
164166
}
165167
}
166168

169+
// Step 4: Battery optimization
170+
SetupCard(
171+
step = 4,
172+
icon = Icons.Default.BatteryAlert,
173+
title = stringResource(R.string.setup_battery_title),
174+
description = stringResource(R.string.setup_battery_desc),
175+
done = hasBattery,
176+
) {
177+
TextButton(
178+
onClick = { openBatteryOptimizationSettings(context) },
179+
) {
180+
Text(stringResource(R.string.setup_grant_access))
181+
Spacer(Modifier.width(4.dp))
182+
Icon(Icons.Default.ChevronRight, null, Modifier.size(18.dp))
183+
}
184+
}
185+
167186
Spacer(Modifier.height(8.dp))
168187

169188
Button(
@@ -173,7 +192,7 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) {
173192
onComplete()
174193
},
175194
modifier = Modifier.fillMaxWidth(),
176-
enabled = serverDone && hasNotif && hasUsage,
195+
enabled = serverDone && hasNotif && hasUsage && hasBattery,
177196
) {
178197
Text(stringResource(R.string.setup_continue))
179198
}
@@ -263,3 +282,10 @@ private fun openUsageAccessSettings(context: Context) {
263282
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
264283
)
265284
}
285+
286+
private fun openBatteryOptimizationSettings(context: Context) {
287+
context.startActivity(
288+
Intent(AndroidSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
289+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
290+
)
291+
}

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
<string name="setup_notif_desc">CashPilot reads foreground notifications to detect when passive income apps are running. No notification content is collected — only presence is checked.</string>
3232
<string name="setup_usage_title">Usage Access</string>
3333
<string name="setup_usage_desc">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.</string>
34+
<string name="setup_battery_title">Battery Optimization</string>
35+
<string name="setup_battery_desc">Disable battery optimization so Android doesn\'t kill CashPilot\'s background service. Without this, heartbeats may stop when the screen is off.</string>
3436
<string name="setup_grant_access">Open Settings</string>
3537
<string name="setup_continue">Continue to Dashboard</string>
3638
<string name="setup_skip">Skip for now</string>

0 commit comments

Comments
 (0)