• v0.1.6 2ff63da9b5

    v0.1.6 — audit fixes (critical, moderate, minor) + UX polish

    .leviathan 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
      lastFetchLat was 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 _location if 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 a geo: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