diff --git a/app/src/main/java/com/cashpilot/android/model/Settings.kt b/app/src/main/java/com/cashpilot/android/model/Settings.kt index f1173ba..50c5ecf 100644 --- a/app/src/main/java/com/cashpilot/android/model/Settings.kt +++ b/app/src/main/java/com/cashpilot/android/model/Settings.kt @@ -6,4 +6,5 @@ data class Settings( val apiKey: String = "", val heartbeatIntervalSeconds: Int = 30, val enabledSlugs: Set = KnownApps.all.map { it.slug }.toSet(), + val setupCompleted: Boolean = false, ) 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..8389a00 100644 --- a/app/src/main/java/com/cashpilot/android/ui/MainActivity.kt +++ b/app/src/main/java/com/cashpilot/android/ui/MainActivity.kt @@ -67,23 +67,17 @@ class MainActivity : ComponentActivity() { CashPilotTheme { Surface(modifier = Modifier.fillMaxSize()) { val settings by viewModel.settings.collectAsState() - val hasNotif by viewModel.hasNotificationAccess.collectAsState() - val hasUsage by viewModel.hasUsageAccess.collectAsState() var showSettings by rememberSaveable { mutableStateOf(false) } - var setupDismissed by rememberSaveable { mutableStateOf(false) } - - val needsSetup = !setupDismissed && - (settings.serverUrl.isBlank() || settings.apiKey.isBlank() || !hasNotif || !hasUsage) // Handle system Back from Settings → return to Dashboard - BackHandler(enabled = showSettings && !needsSetup) { + BackHandler(enabled = showSettings && settings.setupCompleted) { showSettings = false } when { - needsSetup -> SetupScreen( + !settings.setupCompleted -> SetupScreen( viewModel = viewModel, - onComplete = { setupDismissed = true }, + onComplete = {}, ) showSettings -> SettingsScreen( viewModel = viewModel, diff --git a/app/src/main/java/com/cashpilot/android/ui/screen/SettingsScreen.kt b/app/src/main/java/com/cashpilot/android/ui/screen/SettingsScreen.kt index 9cfcad6..51427aa 100644 --- a/app/src/main/java/com/cashpilot/android/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/com/cashpilot/android/ui/screen/SettingsScreen.kt @@ -12,10 +12,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -38,6 +41,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.cashpilot.android.R import com.cashpilot.android.model.KnownApps @@ -47,6 +51,7 @@ import com.cashpilot.android.ui.MainViewModel @Composable fun SettingsScreen(viewModel: MainViewModel, onBack: () -> Unit) { val settings by viewModel.settings.collectAsState() + val hasBattery by viewModel.hasBatteryOptOut.collectAsState() val context = LocalContext.current // Local state for text fields — avoids per-keystroke DataStore writes @@ -122,35 +127,34 @@ fun SettingsScreen(viewModel: MainViewModel, onBack: () -> Unit) { ) } - // Permissions section + // Battery optimization item { Spacer(modifier = Modifier.height(8.dp)) - Text("Permissions", style = MaterialTheme.typography.titleMedium) - Text( - "Grant these permissions for full app detection.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - item { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedButton( - onClick = { openNotificationListenerSettings(context) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text("Grant Notification Access") - } - OutlinedButton( - onClick = { openUsageAccessSettings(context) }, - modifier = Modifier.fillMaxWidth(), + if (hasBattery) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Text("Grant Usage Access") + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + Spacer(Modifier.width(8.dp)) + Text( + stringResource(R.string.battery_unrestricted), + style = MaterialTheme.typography.bodyMedium, + ) } + } else { OutlinedButton( onClick = { openBatteryOptimizationSettings(context) }, modifier = Modifier.fillMaxWidth(), ) { - Text("Disable Battery Optimization") + Text(stringResource(R.string.battery_disable_optimization)) } } } @@ -197,12 +201,24 @@ fun SettingsScreen(viewModel: MainViewModel, onBack: () -> Unit) { onClick = { openUrl(context, "https://github.com/GeiserX/CashPilot-android") }, modifier = Modifier.fillMaxWidth(), ) { + Icon( + painter = painterResource(R.drawable.ic_github), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) Text(stringResource(R.string.about_github_android)) } OutlinedButton( onClick = { openUrl(context, "https://github.com/GeiserX/CashPilot") }, modifier = Modifier.fillMaxWidth(), ) { + Icon( + painter = painterResource(R.drawable.ic_github), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) Text(stringResource(R.string.about_github_server)) } Button( @@ -226,23 +242,18 @@ private fun openUrl(context: Context, url: String) { ) } -private fun openNotificationListenerSettings(context: Context) { - context.startActivity( - Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) -} - -private fun openUsageAccessSettings(context: Context) { - context.startActivity( - Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) -} - private fun openBatteryOptimizationSettings(context: Context) { - context.startActivity( - Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) + try { + context.startActivity( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + android.net.Uri.parse("package:${context.packageName}"), + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } catch (_: Exception) { + context.startActivity( + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } } 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 1b18cc3..401d0c7 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 @@ -77,6 +77,11 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) { val serverDone = localUrl.isNotBlank() && localKey.isNotBlank() + val finishSetup = { + viewModel.updateSettings { it.copy(serverUrl = localUrl, apiKey = localKey, setupCompleted = true) } + onComplete() + } + Scaffold { padding -> Column( modifier = Modifier @@ -186,11 +191,7 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) { Spacer(Modifier.height(8.dp)) Button( - onClick = { - // Persist any pending text field values - viewModel.updateSettings { it.copy(serverUrl = localUrl, apiKey = localKey) } - onComplete() - }, + onClick = finishSetup, modifier = Modifier.fillMaxWidth(), enabled = serverDone && hasNotif && hasUsage, ) { @@ -198,10 +199,7 @@ fun SetupScreen(viewModel: MainViewModel, onComplete: () -> Unit) { } TextButton( - onClick = { - viewModel.updateSettings { it.copy(serverUrl = localUrl, apiKey = localKey) } - onComplete() - }, + onClick = finishSetup, modifier = Modifier.fillMaxWidth(), ) { Text(stringResource(R.string.setup_skip)) diff --git a/app/src/main/java/com/cashpilot/android/util/SettingsStore.kt b/app/src/main/java/com/cashpilot/android/util/SettingsStore.kt index 53275d4..ca2e217 100644 --- a/app/src/main/java/com/cashpilot/android/util/SettingsStore.kt +++ b/app/src/main/java/com/cashpilot/android/util/SettingsStore.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey @@ -20,6 +21,7 @@ object SettingsStore { private val API_KEY = stringPreferencesKey("api_key") private val HEARTBEAT_INTERVAL = intPreferencesKey("heartbeat_interval") private val ENABLED_SLUGS = stringSetPreferencesKey("enabled_slugs") + private val SETUP_COMPLETED = booleanPreferencesKey("setup_completed") fun settings(context: Context): Flow = context.dataStore.data.map { prefs -> @@ -28,6 +30,9 @@ object SettingsStore { apiKey = prefs[API_KEY] ?: "", heartbeatIntervalSeconds = prefs[HEARTBEAT_INTERVAL] ?: 30, enabledSlugs = prefs[ENABLED_SLUGS] ?: KnownApps.all.map { it.slug }.toSet(), + // Migration: mark as completed only if both URL and key were configured before this field existed + setupCompleted = prefs[SETUP_COMPLETED] + ?: (prefs[SERVER_URL]?.isNotEmpty() == true && prefs[API_KEY]?.isNotEmpty() == true), ) } @@ -38,12 +43,15 @@ object SettingsStore { apiKey = prefs[API_KEY] ?: "", heartbeatIntervalSeconds = prefs[HEARTBEAT_INTERVAL] ?: 30, enabledSlugs = prefs[ENABLED_SLUGS] ?: KnownApps.all.map { it.slug }.toSet(), + setupCompleted = prefs[SETUP_COMPLETED] + ?: (prefs[SERVER_URL]?.isNotEmpty() == true && prefs[API_KEY]?.isNotEmpty() == true), ) val updated = transform(current) prefs[SERVER_URL] = updated.serverUrl prefs[API_KEY] = updated.apiKey prefs[HEARTBEAT_INTERVAL] = updated.heartbeatIntervalSeconds prefs[ENABLED_SLUGS] = updated.enabledSlugs + prefs[SETUP_COMPLETED] = updated.setupCompleted } } } diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 0000000..82248e8 --- /dev/null +++ b/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e0d5e2..3dd63f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,12 +46,16 @@ Last: %s Notification active + + Battery optimization disabled + Disable Battery Optimization + Insecure: API key and app data will be sent unencrypted. Use https:// if possible. About - GitHub — CashPilot Android - GitHub — CashPilot Server + CashPilot Android + CashPilot Server Sponsor / Donate