From 6c57297f582f3e9a963d66d8b3ad98bdfa11a03c Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Tue, 28 Apr 2026 21:22:20 -0400 Subject: [PATCH] =?UTF-8?q?v0.1.2=20=E2=80=94=20audit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - DetectionService.startInForeground now passes FOREGROUND_SERVICE_TYPE_LOCATION OR'd with TYPE_CONNECTED_DEVICE on Android 14+. Without this, the system silently revoked location access once the screen locked, breaking DeFlock + Waze for foreground-service use (the whole point of the foreground service). - DeflockClient and WazeClient now skip JSON entries whose lat/lon parse to NaN. Previously NaN flowed into Location.distanceBetween, the NaN > limit check returned false (IEEE 754), and we submitted a full-confidence detection labeled "@0m" — instant false-positive RED from a single malformed map entry. UX: - First-run permission flow auto-starts scanning after the user grants everything; no second tap on START required. - Settings shows a "Restart scan to apply" button when toggling sources while scanning. Source toggle changes used to silently no-op until the next manual stop+start. versionCode 1 → 3, versionName 0.1.0 → 0.1.2. --- app/build.gradle.kts | 4 +-- .../org/soulstone/overwatch/MainActivity.kt | 14 +++++++- .../soulstone/overwatch/scan/DeflockClient.kt | 7 ++-- .../soulstone/overwatch/scan/WazeClient.kt | 7 ++-- .../overwatch/service/DetectionService.kt | 12 ++++--- .../soulstone/overwatch/ui/SettingsScreen.kt | 33 ++++++++++++++++--- 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 382dff1..dbb7e10 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "org.soulstone.overwatch" minSdk = 26 targetSdk = 35 - versionCode = 1 - versionName = "0.1.0" + versionCode = 3 + versionName = "0.1.2" } buildTypes { diff --git a/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt b/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt index d1a8287..cad6f42 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/MainActivity.kt @@ -42,7 +42,13 @@ class MainActivity : ComponentActivity() { private val permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { result -> - permissionsGranted.value = result.all { it.value } + val allGranted = result.all { it.value } + permissionsGranted.value = allGranted + if (allGranted) { + // First-run path: user just granted everything, kick off scanning + // immediately so they don't have to tap START a second time. + DetectionService.start(this) + } } private val permissionsGranted = androidx.compose.runtime.mutableStateOf(false) @@ -87,8 +93,14 @@ class MainActivity : ComponentActivity() { ) } Screen.SETTINGS -> { + val running by DetectionService.running.collectAsState() SettingsScreen( settings = settings, + isRunning = running, + onRestart = { + DetectionService.stop(this) + DetectionService.start(this) + }, onBack = { screen = Screen.MAIN } ) } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt index 11e7cee..db9936d 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/scan/DeflockClient.kt @@ -109,12 +109,15 @@ class DeflockClient(context: Context) { val out = ArrayList(arr.length()) for (i in 0 until arr.length()) { val o = arr.getJSONObject(i) + val lat = o.optDouble("lat") + val lon = o.optDouble("lon") + if (lat.isNaN() || lon.isNaN()) continue val tags = o.optJSONObject("tags") out.add( AlprPoint( id = o.optLong("id", 0L), - lat = o.optDouble("lat"), - lon = o.optDouble("lon"), + lat = lat, + lon = lon, operator = tags?.optString("operator")?.ifBlank { null } ?: tags?.optString("surveillance:operator")?.ifBlank { null }, manufacturer = tags?.optString("manufacturer")?.ifBlank { null } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeClient.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeClient.kt index bd6343e..a2ebe8a 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeClient.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeClient.kt @@ -93,12 +93,15 @@ class WazeClient { val loc = a.optJSONObject("location") ?: continue val uuid = a.optString("uuid") if (uuid.isBlank()) continue + val lat = loc.optDouble("y") + val lon = loc.optDouble("x") + if (lat.isNaN() || lon.isNaN()) continue out.add( Alert( uuid = uuid, subtype = a.optString("subtype").ifBlank { null }, - lat = loc.optDouble("y"), - lon = loc.optDouble("x"), + lat = lat, + lon = lon, pubMillis = a.optLong("pubMillis", System.currentTimeMillis()), confidence = a.optInt("confidence", 0), reliability = a.optInt("reliability", 0), diff --git a/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt b/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt index da193ed..58a092a 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt @@ -181,11 +181,13 @@ class DetectionService : LifecycleService() { private fun startInForeground() { val notification = buildNotification() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground( - NOTIFICATION_ID, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE - ) + // Android 14+ requires the runtime type to cover every capability + // the service uses. We declare both in the manifest; pass both here + // so location-using sources (DeFlock, Waze) keep working with the + // screen off. + val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE or + ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION + startForeground(NOTIFICATION_ID, notification, type) } else { startForeground(NOTIFICATION_ID, notification) } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt index 44bc1df..033c08e 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt @@ -13,6 +13,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -35,6 +38,8 @@ import org.soulstone.overwatch.data.settings.Settings @Composable fun SettingsScreen( settings: Settings, + isRunning: Boolean, + onRestart: () -> Unit, onBack: () -> Unit ) { val ble by settings.bleEnabled.collectAsState() @@ -74,11 +79,29 @@ fun SettingsScreen( SourceToggle("DEFLOCK • ALPR map (cdn.deflock.me)", deflock) { settings.setDeflockEnabled(it) } SourceToggle("WAZE • Live police reports", waze) { settings.setWazeEnabled(it) } Spacer(Modifier.height(8.dp)) - Text( - "Source toggles take effect on next Start.", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + if (isRunning) { + Button( + onClick = onRestart, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurface + ), + shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Restart scan to apply", + fontSize = 13.sp, + fontFamily = FontFamily.Monospace + ) + } + } else { + Text( + "Source toggles take effect on next Start.", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } Spacer(Modifier.height(16.dp)) SectionLabel("Proximity thresholds")