v0.1.2 — audit fixes
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.
This commit is contained in:
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "org.soulstone.overwatch"
|
applicationId = "org.soulstone.overwatch"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 3
|
||||||
versionName = "0.1.0"
|
versionName = "0.1.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -42,7 +42,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
private val permissionLauncher = registerForActivityResult(
|
private val permissionLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
) { result ->
|
) { 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)
|
private val permissionsGranted = androidx.compose.runtime.mutableStateOf(false)
|
||||||
@@ -87,8 +93,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Screen.SETTINGS -> {
|
Screen.SETTINGS -> {
|
||||||
|
val running by DetectionService.running.collectAsState()
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
settings = settings,
|
settings = settings,
|
||||||
|
isRunning = running,
|
||||||
|
onRestart = {
|
||||||
|
DetectionService.stop(this)
|
||||||
|
DetectionService.start(this)
|
||||||
|
},
|
||||||
onBack = { screen = Screen.MAIN }
|
onBack = { screen = Screen.MAIN }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,12 +109,15 @@ class DeflockClient(context: Context) {
|
|||||||
val out = ArrayList<AlprPoint>(arr.length())
|
val out = ArrayList<AlprPoint>(arr.length())
|
||||||
for (i in 0 until arr.length()) {
|
for (i in 0 until arr.length()) {
|
||||||
val o = arr.getJSONObject(i)
|
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")
|
val tags = o.optJSONObject("tags")
|
||||||
out.add(
|
out.add(
|
||||||
AlprPoint(
|
AlprPoint(
|
||||||
id = o.optLong("id", 0L),
|
id = o.optLong("id", 0L),
|
||||||
lat = o.optDouble("lat"),
|
lat = lat,
|
||||||
lon = o.optDouble("lon"),
|
lon = lon,
|
||||||
operator = tags?.optString("operator")?.ifBlank { null }
|
operator = tags?.optString("operator")?.ifBlank { null }
|
||||||
?: tags?.optString("surveillance:operator")?.ifBlank { null },
|
?: tags?.optString("surveillance:operator")?.ifBlank { null },
|
||||||
manufacturer = tags?.optString("manufacturer")?.ifBlank { null }
|
manufacturer = tags?.optString("manufacturer")?.ifBlank { null }
|
||||||
|
|||||||
@@ -93,12 +93,15 @@ class WazeClient {
|
|||||||
val loc = a.optJSONObject("location") ?: continue
|
val loc = a.optJSONObject("location") ?: continue
|
||||||
val uuid = a.optString("uuid")
|
val uuid = a.optString("uuid")
|
||||||
if (uuid.isBlank()) continue
|
if (uuid.isBlank()) continue
|
||||||
|
val lat = loc.optDouble("y")
|
||||||
|
val lon = loc.optDouble("x")
|
||||||
|
if (lat.isNaN() || lon.isNaN()) continue
|
||||||
out.add(
|
out.add(
|
||||||
Alert(
|
Alert(
|
||||||
uuid = uuid,
|
uuid = uuid,
|
||||||
subtype = a.optString("subtype").ifBlank { null },
|
subtype = a.optString("subtype").ifBlank { null },
|
||||||
lat = loc.optDouble("y"),
|
lat = lat,
|
||||||
lon = loc.optDouble("x"),
|
lon = lon,
|
||||||
pubMillis = a.optLong("pubMillis", System.currentTimeMillis()),
|
pubMillis = a.optLong("pubMillis", System.currentTimeMillis()),
|
||||||
confidence = a.optInt("confidence", 0),
|
confidence = a.optInt("confidence", 0),
|
||||||
reliability = a.optInt("reliability", 0),
|
reliability = a.optInt("reliability", 0),
|
||||||
|
|||||||
@@ -181,11 +181,13 @@ class DetectionService : LifecycleService() {
|
|||||||
private fun startInForeground() {
|
private fun startInForeground() {
|
||||||
val notification = buildNotification()
|
val notification = buildNotification()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
startForeground(
|
// Android 14+ requires the runtime type to cover every capability
|
||||||
NOTIFICATION_ID,
|
// the service uses. We declare both in the manifest; pass both here
|
||||||
notification,
|
// so location-using sources (DeFlock, Waze) keep working with the
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
// screen off.
|
||||||
)
|
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE or
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||||
|
startForeground(NOTIFICATION_ID, notification, type)
|
||||||
} else {
|
} else {
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -35,6 +38,8 @@ import org.soulstone.overwatch.data.settings.Settings
|
|||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
isRunning: Boolean,
|
||||||
|
onRestart: () -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val ble by settings.bleEnabled.collectAsState()
|
val ble by settings.bleEnabled.collectAsState()
|
||||||
@@ -74,11 +79,29 @@ fun SettingsScreen(
|
|||||||
SourceToggle("DEFLOCK • ALPR map (cdn.deflock.me)", deflock) { settings.setDeflockEnabled(it) }
|
SourceToggle("DEFLOCK • ALPR map (cdn.deflock.me)", deflock) { settings.setDeflockEnabled(it) }
|
||||||
SourceToggle("WAZE • Live police reports", waze) { settings.setWazeEnabled(it) }
|
SourceToggle("WAZE • Live police reports", waze) { settings.setWazeEnabled(it) }
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
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(
|
Text(
|
||||||
"Source toggles take effect on next Start.",
|
"Source toggles take effect on next Start.",
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
SectionLabel("Proximity thresholds")
|
SectionLabel("Proximity thresholds")
|
||||||
|
|||||||
Reference in New Issue
Block a user