- Replace the static threat circle with an osmdroid-backed map
centered on the user, with red ALPR pins and a tier-color scrim.
Falls back to the muted gradient when idle or before the first
location fix arrives.
- Add DetectionSource.MIC: BLE/WiFi candidate path for Amazon
Echo/Ring (Lab126 OUIs + AVS service UUID 0xFE03), Google Nest/
Home/Chromecast (Google OUIs + mfg id 0x00E0), and generic
Chinese hidden-cam vendors. Score capped at 84 (ORANGE) so RED
stays reserved for ALPR/Axon-grade evidence. Toggleable in
Settings; piggybacks on the BLE+WiFi scanners — no new radio.
- Drop the "[DЯΣΛMMΛKΣЯ]" stylized branding for a clean OVERWATCH
header (notification channel + app label updated to match).
- Fix DeFlock geo-pin tap doing nothing: resolveActivity returns
null on Android 11+ without a <queries> entry even when Maps is
installed. Drop the pre-check, try/catch ActivityNotFoundException,
fall back to a maps.google.com URL if no geo: handler exists.
Stale items corrected:
- Architecture file list referenced WazeClient.kt and WazeScanner.kt
(deleted) and CDN-tile DeflockClient (now Overpass POST). Added the
missing CitizenClient/CitizenScanner/SourceHealth/ThreatLevel files.
- Permissions table said "DeFlock CDN + Waze API" — now Overpass +
Citizen. Added VIBRATE row.
- Settings section listed Waze instead of Citizen; missing the new
Vibrate-on-escalation toggle and Restart-to-apply button.
- Status said "Phases 1-5 complete as of v0.1.0" — bumped to v0.1.7
with a per-version changelog of what landed.
Added:
- Hero paragraph mentions notification + vibration alerting.
- New "How alerts work" section explaining notification updates,
vibration cadence, drill-down sheet, and Open-in-Maps.
- Idle-visual note in scoring section.
- START_NOT_STICKY note in architecture.
- Open-app-settings recovery note in permissions section.
The screen enum lives entirely inside Compose, so the system back press
went straight to Activity.finish(). Added a BackHandler in the SETTINGS
branch that intercepts and routes back to MAIN.
versionCode 7 → 8, versionName 0.1.6 → 0.1.7.
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.
Waze's reCAPTCHA gating on live-map/api/georss has no clean mobile
workaround, and the Citizen source added in v0.1.4 covers the same
threat model with better data. Keeping a permanently-failed source
visible was UI clutter — drop it.
Removed:
- scan/WazeClient.kt and scan/WazeScanner.kt (deleted)
- WAZE from DetectionSource enum
- waze flow from SourceHealth (+ flowFor/record/reset cases)
- WazeObservation + scoreWaze + W_WAZE_POLICE from ConfidenceEngine
- wazeEnabled from Settings (+ KEY_WAZE)
- WAZE row from SettingsScreen
- wazeScanner from DetectionService
Renamed (Citizen now owns the proximity slider that Waze used to share):
- Settings.wazeProximityM → citizenProximityM
- Settings.setWazeProximityM → setCitizenProximityM
- KEY_WAZE_PROX → KEY_CITIZEN_PROX
- DEFAULT_WAZE_PROX → DEFAULT_CITIZEN_PROX (still 500)
- SettingsScreen "Waze alert distance" → "Citizen alert distance"
Existing users will see the slider reset to 500 m default since the
SharedPreferences key changed.
versionCode 5 → 6, versionName 0.1.4 → 0.1.5.
Waze remains gated behind 2025/2026 reCAPTCHA on live-map; added Citizen
as a working alternative for police-presence signal. Citizen pulls from
911 + scanner traffic, returns rich incident data (lat/lon, timestamp,
severity level, responding precinct, title), and has no auth or
rate-limit gating.
New scan/CitizenClient.kt:
- GET /api/incident/trending (bbox query → list of incident ids)
- GET /api/incident/{id} (full detail per id)
- Sealed TrendingResult so the scanner can surface 4xx via SourceHealth.
New scan/CitizenScanner.kt:
- 60s poll interval, 30-min freshness window
- Per-id detail cache for the lifetime of a start/stop cycle —
incidents are immutable, so each is fetched at most once per session
- Title regex filter: drops pure fire/medical events that don't imply
police presence; retains them when the title also names police action
- Submits to the shared DetectionStore as DetectionSource.CITIZEN
ConfidenceEngine.scoreCitizen:
- Base 55 (matches the old W_WAZE_POLICE weight)
- +5 if level >= 2 (Citizen's own severity)
- +5 if title contains police-action keyword (police/officer/arrest/
swat/tactical/raid/pursuit/stop/search warrant)
Settings: new citizenEnabled toggle (default on); UI row in
SettingsScreen. SourceHealth has a new flow for CITIZEN. DetectionService
starts the scanner alongside the others when location is available.
Continued investigation of Waze / Google Maps police APIs:
- Waze SDK (hewliyang/waze-traffic-api): wraps the same blocked endpoint
- ddd/google_maps reverse-engineering: locations only, no incidents
- Google Maps Platform: no public incidents API (just displays Waze data internally)
- TomTom Traffic Incidents: traffic-only, no police presence
- Waze for Cities partner feed: real but requires being a city/police agency
versionCode 4 → 5, versionName 0.1.3 → 0.1.4.
The cdn.deflock.me CDN is gated behind Cloudflare bot mitigation that
mobile HTTP clients can't pass. The live deflock-app Flutter client
abandoned that path; it POSTs Overpass-QL queries directly to
overpass.deflock.org (with overpass-api.de as a fallback). Verified by
hitting the same endpoint from curl — 22 ALPRs returned for the
Springfield VA bbox, matching the user's screenshot of the working app.
DeflockClient rewrite:
- POST [out:json][timeout:25];(node[surveillance][type=ALPR](bbox););out body;
- 5 km half-width bbox around the user
- 24h on-disk cache keyed by 0.05° grid cell (revisits don't refetch)
- Returns sealed FetchResult: Success(points) | Failed(reason)
DeflockScanner update:
- Replaces 20° tile concept with distance-based refetch (1.5 km threshold)
- Records SourceHealth on each fetch outcome
Waze: reCAPTCHA gating confirmed. WazeClient.fetchPoliceNear now returns
sealed FetchResult; WazeScanner records SourceHealth.FAILED with
"Upstream blocked (HTTP 403)" so the user sees why no Waze data is
flowing instead of silent zeros.
New fusion/SourceHealth.kt — per-source MutableStateFlow registry,
record(source, ok, message) + reset() called on service start/stop.
UI: SourceRow in the bottom-sheet drill-down now shows the health
message in orange when status = FAILED instead of "no detections".
versionCode 3 → 4, versionName 0.1.2 → 0.1.3.
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.
The button was gated on `granted || running`, but the only thing that
triggers the permission request is tapping the button — catch-22 that
left first-time users with no way to grant permissions.
Always enable the button when not running; the onStartStop handler already
routes correctly (start scanning if granted, else launch the permission
request flow). Updated the helper text to point at this directly.