-
v0.1.6 — audit fixes (critical, moderate, minor) + UX polish
released this
2026-04-29 02:11:56 +00:00 Critical
- DetectionService: subscribe to threatLevel + top event flows; rebuild the
foreground notification on every change so a locked-screen user sees
escalations. Vibrate on upward tier transitions (escalating waveforms for
YELLOW/ORANGE/RED), gated by Settings.vibrateOnAlert (default on). - DetectionService: only mark _running=true if at least one scanner started;
stopSelf() if everything was disabled or denied. Switch START_STICKY →
START_NOT_STICKY so a system-killed service doesn't re-create into a
stuck "running but not scanning" state. - DeflockClient: detect Overpass timeout-in-body (
{"remark": "...timed out..."}) and treat as failure — previously these 200-with-empty-elements
responses got cached for 24 h, hiding ALPRs in that 5×5 km cell for the
next day. - DeflockScanner: record lastFetch coords + timestamp on BOTH success and
failure, with a 60 s backoff window after a failed attempt. Previously
lastFetchLatwas only set on Success, so every subsequent location
update would re-trigger a 30 s POST that collectLatest then cancelled —
we'd never finish a fetch under sustained Overpass slowness. - LocationProvider: stale-lastLocation race fix. The async
lastLocation
callback now only seeds_locationif it's still null and we're still
running — previously it could overwrite a fresher fix from
requestLocationUpdates, or fire after stop() and resurrect _location with
stale data.
Moderate
- CitizenScanner: wait for the first non-null location with .first { } before
starting the poll/delay loop. First Citizen poll now fires within seconds
of the location fix, not up to 60 s after. - MainScreen: when not running, show a muted gray circle with "IDLE" text
instead of the same solid green look as "scanning, all clear" — the
pulse animation was the only differentiator before. - Compose state: rememberSaveable for the screen enum + bottom-sheet open
state, so SETTINGS survives rotation. - MainActivity: detect permanently-denied permissions (the user picked
"don't ask again") via shouldShowRequestPermissionRationale. UI swaps the
call-to-action to "Open app settings" which fires
Settings.ACTION_APPLICATION_DETAILS_SETTINGS. onResume re-checks so a
user returning from app settings is reflected immediately.
Improvements
- BLE/WiFi scanners record SourceHealth.OK on a successful start (and
FAILED with a specific reason on every short-circuit — disabled adapter,
missing permission, etc.) so the drill-down sheet is honest about radio
state, not just network state. - DetectionEvent gains optional lat/lon (populated by DEFLOCK and CITIZEN);
SourceRow shows a tap-to-open-Maps icon next to events with coordinates,
firing ageo:lat,lon?q=lat,lon(label)Intent. - SettingsScreen sliders use onValueChangeFinished — only commit to
SharedPreferences on drag-release, not on every pixel of movement. - New Settings.vibrateOnAlert toggle (default on) wired to a SettingsScreen
row under a new "Alerts" section.
Minor
- BleScanner iterates ALL manufacturer-data entries to find XUNTONG; only
falls back to the first entry if no XUNTONG match is present. Previously
we only inspected the first entry. - Drop dead
?.on JSONArray.optString in CitizenClient (returns String,
never null). - Remove unused rememberCoroutineScope in MainScreen.
- Update stale Phase/Waze references in DetectionService comments.
- Add VIBRATE permission to manifest.
versionCode 6 → 7, versionName 0.1.5 → 0.1.6.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Downloads
- DetectionService: subscribe to threatLevel + top event flows; rebuild the