Skip to content

Commit 197adcf

Browse files
authored
fix: prevent setup auto-dismiss, improve settings UX (#9)
* fix: prevent setup auto-dismiss, improve settings UX - Add setupCompleted flag to DataStore so setup screen stays until user explicitly clicks Continue/Skip (no more auto-redirect after granting the last permission) - Migration: existing installs with server URL set are auto-marked as setup completed - Settings: remove notification/usage access buttons (handled in setup), keep only battery optimization with state-aware text - Settings: fix battery intent to use direct per-app prompt - Settings: add GitHub icon to About section buttons - Update string resources for battery state and about labels * fix: tighten setup migration and extract duplicated lambda - Migration now requires both server URL and API key to be non-empty before marking setupCompleted (prevents skipping wizard for partial configs) - Extract finishSetup lambda in SetupScreen so Continue and Skip stay in sync
1 parent 326c7ad commit 197adcf

7 files changed

Lines changed: 84 additions & 59 deletions

File tree

app/src/main/java/com/cashpilot/android/model/Settings.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ data class Settings(
66
val apiKey: String = "",
77
val heartbeatIntervalSeconds: Int = 30,
88
val enabledSlugs: Set<String> = KnownApps.all.map { it.slug }.toSet(),
9+
val setupCompleted: Boolean = false,
910
)

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,17 @@ class MainActivity : ComponentActivity() {
6767
CashPilotTheme {
6868
Surface(modifier = Modifier.fillMaxSize()) {
6969
val settings by viewModel.settings.collectAsState()
70-
val hasNotif by viewModel.hasNotificationAccess.collectAsState()
71-
val hasUsage by viewModel.hasUsageAccess.collectAsState()
7270
var showSettings by rememberSaveable { mutableStateOf(false) }
73-
var setupDismissed by rememberSaveable { mutableStateOf(false) }
74-
75-
val needsSetup = !setupDismissed &&
76-
(settings.serverUrl.isBlank() || settings.apiKey.isBlank() || !hasNotif || !hasUsage)
7771

7872
// Handle system Back from Settings → return to Dashboard
79-
BackHandler(enabled = showSettings && !needsSetup) {
73+
BackHandler(enabled = showSettings && settings.setupCompleted) {
8074
showSettings = false
8175
}
8276

8377
when {
84-
needsSetup -> SetupScreen(
78+
!settings.setupCompleted -> SetupScreen(
8579
viewModel = viewModel,
86-
onComplete = { setupDismissed = true },
80+
onComplete = {},
8781
)
8882
showSettings -> SettingsScreen(
8983
viewModel = viewModel,

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

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import androidx.compose.foundation.layout.fillMaxSize
1212
import androidx.compose.foundation.layout.fillMaxWidth
1313
import androidx.compose.foundation.layout.height
1414
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.foundation.layout.size
16+
import androidx.compose.foundation.layout.width
1517
import androidx.compose.foundation.lazy.LazyColumn
1618
import androidx.compose.foundation.lazy.items
1719
import androidx.compose.material.icons.Icons
1820
import androidx.compose.material.icons.automirrored.filled.ArrowBack
21+
import androidx.compose.material.icons.filled.CheckCircle
1922
import androidx.compose.material3.Button
2023
import androidx.compose.material3.ExperimentalMaterial3Api
2124
import androidx.compose.material3.Icon
@@ -38,6 +41,7 @@ import androidx.compose.ui.Alignment
3841
import androidx.compose.ui.Modifier
3942
import androidx.compose.ui.platform.LocalContext
4043
import androidx.compose.ui.res.stringResource
44+
import androidx.compose.ui.res.painterResource
4145
import androidx.compose.ui.unit.dp
4246
import com.cashpilot.android.R
4347
import com.cashpilot.android.model.KnownApps
@@ -47,6 +51,7 @@ import com.cashpilot.android.ui.MainViewModel
4751
@Composable
4852
fun SettingsScreen(viewModel: MainViewModel, onBack: () -> Unit) {
4953
val settings by viewModel.settings.collectAsState()
54+
val hasBattery by viewModel.hasBatteryOptOut.collectAsState()
5055
val context = LocalContext.current
5156

5257
// Local state for text fields — avoids per-keystroke DataStore writes
@@ -122,35 +127,34 @@ fun SettingsScreen(viewModel: MainViewModel, onBack: () -> Unit) {
122127
)
123128
}
124129

125-
// Permissions section
130+
// Battery optimization
126131
item {
127132
Spacer(modifier = Modifier.height(8.dp))
128-
Text("Permissions", style = MaterialTheme.typography.titleMedium)
129-
Text(
130-
"Grant these permissions for full app detection.",
131-
style = MaterialTheme.typography.bodySmall,
132-
color = MaterialTheme.colorScheme.onSurfaceVariant,
133-
)
134-
}
135-
item {
136-
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
137-
OutlinedButton(
138-
onClick = { openNotificationListenerSettings(context) },
139-
modifier = Modifier.fillMaxWidth(),
140-
) {
141-
Text("Grant Notification Access")
142-
}
143-
OutlinedButton(
144-
onClick = { openUsageAccessSettings(context) },
145-
modifier = Modifier.fillMaxWidth(),
133+
if (hasBattery) {
134+
Row(
135+
modifier = Modifier
136+
.fillMaxWidth()
137+
.padding(vertical = 8.dp),
138+
verticalAlignment = Alignment.CenterVertically,
146139
) {
147-
Text("Grant Usage Access")
140+
Icon(
141+
Icons.Default.CheckCircle,
142+
contentDescription = null,
143+
tint = MaterialTheme.colorScheme.primary,
144+
modifier = Modifier.size(20.dp),
145+
)
146+
Spacer(Modifier.width(8.dp))
147+
Text(
148+
stringResource(R.string.battery_unrestricted),
149+
style = MaterialTheme.typography.bodyMedium,
150+
)
148151
}
152+
} else {
149153
OutlinedButton(
150154
onClick = { openBatteryOptimizationSettings(context) },
151155
modifier = Modifier.fillMaxWidth(),
152156
) {
153-
Text("Disable Battery Optimization")
157+
Text(stringResource(R.string.battery_disable_optimization))
154158
}
155159
}
156160
}
@@ -197,12 +201,24 @@ fun SettingsScreen(viewModel: MainViewModel, onBack: () -> Unit) {
197201
onClick = { openUrl(context, "https://github.com/GeiserX/CashPilot-android") },
198202
modifier = Modifier.fillMaxWidth(),
199203
) {
204+
Icon(
205+
painter = painterResource(R.drawable.ic_github),
206+
contentDescription = null,
207+
modifier = Modifier.size(18.dp),
208+
)
209+
Spacer(Modifier.width(8.dp))
200210
Text(stringResource(R.string.about_github_android))
201211
}
202212
OutlinedButton(
203213
onClick = { openUrl(context, "https://github.com/GeiserX/CashPilot") },
204214
modifier = Modifier.fillMaxWidth(),
205215
) {
216+
Icon(
217+
painter = painterResource(R.drawable.ic_github),
218+
contentDescription = null,
219+
modifier = Modifier.size(18.dp),
220+
)
221+
Spacer(Modifier.width(8.dp))
206222
Text(stringResource(R.string.about_github_server))
207223
}
208224
Button(
@@ -226,23 +242,18 @@ private fun openUrl(context: Context, url: String) {
226242
)
227243
}
228244

229-
private fun openNotificationListenerSettings(context: Context) {
230-
context.startActivity(
231-
Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
232-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
233-
)
234-
}
235-
236-
private fun openUsageAccessSettings(context: Context) {
237-
context.startActivity(
238-
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
239-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
240-
)
241-
}
242-
243245
private fun openBatteryOptimizationSettings(context: Context) {
244-
context.startActivity(
245-
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
246-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
247-
)
246+
try {
247+
context.startActivity(
248+
Intent(
249+
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
250+
android.net.Uri.parse("package:${context.packageName}"),
251+
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
252+
)
253+
} catch (_: Exception) {
254+
context.startActivity(
255+
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
256+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
257+
)
258+
}
248259
}

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) {
7777

7878
val serverDone = localUrl.isNotBlank() && localKey.isNotBlank()
7979

80+
val finishSetup = {
81+
viewModel.updateSettings { it.copy(serverUrl = localUrl, apiKey = localKey, setupCompleted = true) }
82+
onComplete()
83+
}
84+
8085
Scaffold { padding ->
8186
Column(
8287
modifier = Modifier
@@ -186,22 +191,15 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) {
186191
Spacer(Modifier.height(8.dp))
187192

188193
Button(
189-
onClick = {
190-
// Persist any pending text field values
191-
viewModel.updateSettings { it.copy(serverUrl = localUrl, apiKey = localKey) }
192-
onComplete()
193-
},
194+
onClick = finishSetup,
194195
modifier = Modifier.fillMaxWidth(),
195196
enabled = serverDone && hasNotif && hasUsage,
196197
) {
197198
Text(stringResource(R.string.setup_continue))
198199
}
199200

200201
TextButton(
201-
onClick = {
202-
viewModel.updateSettings { it.copy(serverUrl = localUrl, apiKey = localKey) }
203-
onComplete()
204-
},
202+
onClick = finishSetup,
205203
modifier = Modifier.fillMaxWidth(),
206204
) {
207205
Text(stringResource(R.string.setup_skip))

app/src/main/java/com/cashpilot/android/util/SettingsStore.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import androidx.datastore.core.DataStore
55
import androidx.datastore.preferences.core.Preferences
66
import androidx.datastore.preferences.core.edit
7+
import androidx.datastore.preferences.core.booleanPreferencesKey
78
import androidx.datastore.preferences.core.intPreferencesKey
89
import androidx.datastore.preferences.core.stringPreferencesKey
910
import androidx.datastore.preferences.core.stringSetPreferencesKey
@@ -20,6 +21,7 @@ object SettingsStore {
2021
private val API_KEY = stringPreferencesKey("api_key")
2122
private val HEARTBEAT_INTERVAL = intPreferencesKey("heartbeat_interval")
2223
private val ENABLED_SLUGS = stringSetPreferencesKey("enabled_slugs")
24+
private val SETUP_COMPLETED = booleanPreferencesKey("setup_completed")
2325

2426
fun settings(context: Context): Flow<Settings> =
2527
context.dataStore.data.map { prefs ->
@@ -28,6 +30,9 @@ object SettingsStore {
2830
apiKey = prefs[API_KEY] ?: "",
2931
heartbeatIntervalSeconds = prefs[HEARTBEAT_INTERVAL] ?: 30,
3032
enabledSlugs = prefs[ENABLED_SLUGS] ?: KnownApps.all.map { it.slug }.toSet(),
33+
// Migration: mark as completed only if both URL and key were configured before this field existed
34+
setupCompleted = prefs[SETUP_COMPLETED]
35+
?: (prefs[SERVER_URL]?.isNotEmpty() == true && prefs[API_KEY]?.isNotEmpty() == true),
3136
)
3237
}
3338

@@ -38,12 +43,15 @@ object SettingsStore {
3843
apiKey = prefs[API_KEY] ?: "",
3944
heartbeatIntervalSeconds = prefs[HEARTBEAT_INTERVAL] ?: 30,
4045
enabledSlugs = prefs[ENABLED_SLUGS] ?: KnownApps.all.map { it.slug }.toSet(),
46+
setupCompleted = prefs[SETUP_COMPLETED]
47+
?: (prefs[SERVER_URL]?.isNotEmpty() == true && prefs[API_KEY]?.isNotEmpty() == true),
4148
)
4249
val updated = transform(current)
4350
prefs[SERVER_URL] = updated.serverUrl
4451
prefs[API_KEY] = updated.apiKey
4552
prefs[HEARTBEAT_INTERVAL] = updated.heartbeatIntervalSeconds
4653
prefs[ENABLED_SLUGS] = updated.enabledSlugs
54+
prefs[SETUP_COMPLETED] = updated.setupCompleted
4755
}
4856
}
4957
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="#FF000000"
8+
android:pathData="M12,2C6.477,2 2,6.477 2,12c0,4.418 2.865,8.166 6.839,9.489 0.5,0.092 0.682,-0.217 0.682,-0.482 0,-0.237 -0.008,-0.866 -0.013,-1.7 -2.782,0.603 -3.369,-1.342 -3.369,-1.342 -0.454,-1.155 -1.11,-1.462 -1.11,-1.462 -0.908,-0.62 0.069,-0.608 0.069,-0.608 1.003,0.07 1.531,1.03 1.531,1.03 0.892,1.529 2.341,1.087 2.91,0.831 0.091,-0.646 0.349,-1.086 0.635,-1.337 -2.22,-0.253 -4.555,-1.11 -4.555,-4.943 0,-1.091 0.39,-1.984 1.029,-2.683 -0.103,-0.253 -0.446,-1.27 0.098,-2.647 0,0 0.84,-0.269 2.75,1.025A9.564,9.564 0,0 1,12 6.844a9.59,9.59 0,0 1,2.504 0.337c1.909,-1.294 2.747,-1.025 2.747,-1.025 0.546,1.377 0.202,2.394 0.1,2.647 0.64,0.699 1.028,1.592 1.028,2.683 0,3.842 -2.339,4.687 -4.566,4.935 0.359,0.309 0.678,0.919 0.678,1.852 0,1.336 -0.012,2.415 -0.012,2.743 0,0.267 0.18,0.578 0.688,0.48C19.138,20.163 22,16.418 22,12c0,-5.523 -4.477,-10 -10,-10z" />
9+
</vector>

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,16 @@
4646
<string name="state_last_active">Last: %s</string>
4747
<string name="notification_active">Notification active</string>
4848

49+
<!-- Battery -->
50+
<string name="battery_unrestricted">Battery optimization disabled</string>
51+
<string name="battery_disable_optimization">Disable Battery Optimization</string>
52+
4953
<!-- Warnings -->
5054
<string name="cleartext_warning">Insecure: API key and app data will be sent unencrypted. Use https:// if possible.</string>
5155

5256
<!-- About section -->
5357
<string name="about">About</string>
54-
<string name="about_github_android">GitHub — CashPilot Android</string>
55-
<string name="about_github_server">GitHub — CashPilot Server</string>
58+
<string name="about_github_android">CashPilot Android</string>
59+
<string name="about_github_server">CashPilot Server</string>
5660
<string name="about_donate">Sponsor / Donate</string>
5761
</resources>

0 commit comments

Comments
 (0)