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"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
versionCode = 3
|
||||
versionName = "0.1.2"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,12 +109,15 @@ class DeflockClient(context: Context) {
|
||||
val out = ArrayList<AlprPoint>(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 }
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user