Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/java/com/cashpilot/android/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/com/cashpilot/android/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,6 +75,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private val _hasUsageAccess = MutableStateFlow(false)
val hasUsageAccess: StateFlow<Boolean> = _hasUsageAccess.asStateFlow()

private val _hasBatteryOptOut = MutableStateFlow(false)
val hasBatteryOptOut: StateFlow<Boolean> = _hasBatteryOptOut.asStateFlow()

val lastHeartbeat: StateFlow<Long> = HeartbeatService.lastHeartbeat
val lastHeartbeatFailed: StateFlow<Boolean> = HeartbeatService.lastHeartbeatFailed

Expand Down Expand Up @@ -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
}
}
28 changes: 27 additions & 1 deletion app/src/main/java/com/cashpilot/android/ui/screen/SetupScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("") }
Expand Down Expand Up @@ -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(
Expand All @@ -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))
}
Expand Down Expand Up @@ -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),
)
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
<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>
<string name="setup_usage_title">Usage Access</string>
<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>
<string name="setup_battery_title">Battery Optimization</string>
<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>
<string name="setup_grant_access">Open Settings</string>
<string name="setup_continue">Continue to Dashboard</string>
<string name="setup_skip">Skip for now</string>
Expand Down
Loading