Reported from a real device: with "show within" at 1100 m the circle was
YELLOW on a police report 312 m away scoring 52; dragging to 300 m turned it
GREEN and said "All clear". Same spot, same police car.
v0.5.10 fixed the widening direction by calibrating the curves, but the
narrowing direction was still broken and is worse — a view setting that can
hide a live alert, and silently turn the circle green while it does, is a trap
rather than a feature. The earlier verification swept the slider at a location
where the nearest thing scored 35, so it could not have caught this.
Root cause was structural, not calibration: the scanners used the user's
setting as their emit cutoff, so it decided what entered the store, and the
store decides the tier. Scanners now evaluate at their own fixed radii
(DeFlock 1200 m, Waze 2000 m; aircraft already had its own), DetectionEvent
carries the distance it was observed at, and the UI filters only what it
*draws and lists*. Anything at YELLOW or above is shown regardless of range.
Removes the now-dead refresh() paths and the jobs that drove them, since a
range change no longer needs to touch a scanner.
Verified on device: a camera 285 m away scoring 48 holds the tier at YELLOW
from a 200 m view range through 4900 m, both out-of-range alerts stay listed
rather than vanishing, and standing 29 m from a camera still reads 89 RED.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
The header and "Current release" lines had silently read v0.5.7 since that
release: three version bumps used an unguarded string replace whose pattern no
longer matched, so they no-op'd without failing. Both now read v0.5.11, the
v0.5.11 changelog entry is present, and the build section records that this is
tested on Android 16 with a punch-hole cutout as well as Android 14.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
Two separate compatibility bugs, both reproduced before fixing.
**Edge-to-edge.** Android 15 draws apps edge-to-edge whether or not they ask
for anything targeting SDK 35, and the statusBarColor/navigationBarColor theme
attributes this app relied on became no-ops at the same time. Reproduced on a
fresh Android 16 emulator with a punch-hole cutout: the shipped v0.5.11-1 drew
"OVERWATCH" inside the status bar next to the clock, buried the settings gear
under the wifi and battery icons where it could not be tapped, and jammed the
permission hint into the gesture bar. MainActivity now calls enableEdgeToEdge()
explicitly — so the behaviour is the same on older releases rather than
shifting under the user on an OS upgrade — and both screens pad themselves with
WindowInsets.safeDrawing, which covers the system bars and the cutout. The
background still runs edge to edge; only content is held clear. The overlay
bubble states LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT rather than relying on it,
since a draggable free-floating window could otherwise park under a camera hole.
**BLE with the screen off.** Since Android 8.1 the Bluetooth stack stops
delivering results for scans started with no ScanFilter once the screen turns
off, and a foreground service does not exempt it — it is a stack rule, not a
process-lifetime one. This app called startScan(null, ...) while promising to
keep watching from a pocket, so BLE detection was silently dead in exactly the
case that matters. A ScanFilter cannot express an OUI prefix, so the primary
MAC-prefix method genuinely cannot run with the screen off; what can be named
precisely still can. The scanner now switches on ACTION_SCREEN_ON/OFF:
unfiltered while the screen is on, and a filtered scan (Raven service UUIDs,
the XUNTONG manufacturer id, mic-target company ids) while it is off, capped at
16 filters because slots are a hardware resource and a silently empty scan is
the worst failure this app has. The drill-down says so rather than hiding it.
Verified on Android 16 (API 36) and Android 14: foreground service starts with
types=0x18, all five scanners run, screen off logs "filtered scan (16 filters)"
and screen on returns to unfiltered, zero scan failures, zero crashes.
SOURCES.md gains a platform-constraints section covering these plus the BLE
5-starts-per-30s limit, WiFi scan throttling, and WifiManager.startScan()'s
deprecation and planned removal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
The scores were already absolute, but the tier is max(score) over everything
*reported* and the range slider decides what is reported — so the setting
still leaked into the alarm. Measured against a real 195-node cache: standing
still and dragging the slider from 300 m to 500 m flipped the app from GREEN
to YELLOW with nothing physical changing.
The fix is calibration, not plumbing. Every curve now crosses below the YELLOW
line at roughly the distance the thing stops being able to act on you. A Flock
camera reads plates at ~30-50 m, so it is RED on top of it, ORANGE at 100 m,
and GREEN by 500 m — still drawn on the map, just no longer an alarm. Waze
keeps a wider band deliberately: a police car covers 700 m in under a minute,
a bollarded camera never moves. Aircraft are unaffected, since that scanner
uses its own ranges rather than the slider.
The main-screen slider is relabelled "show within" to say what it actually is
— a view control, not a sensitivity control.
Verified on device: sweeping 200 m -> 4900 m leaves the tier at GREEN while
the reported count goes 0 -> 133, and standing 29 m from a real camera reads
89 RED, then 50 at 252 m and 41 at 373 m. Scores match the falloff formula
exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
The radius is the one setting a user reaches for while actually moving, so it
no longer lives behind the gear icon: it sits under the map circle, where it
also fills what was dead space in the bottom-pinned layout. Commits on release
rather than per-pixel, so dragging it doesn't restart the location scanners on
every frame. Removed from Settings rather than duplicated.
Added a legend under the circle, because five classes now share it and the
colours were otherwise just decoration: ALPR red, speed camera amber, generic
camera gray, Waze police blue, aircraft violet, plus the user crosshair.
Verified on device: dragging to 2500 m rezooms the map and takes the DeFlock
detection list from 1 entry to 57, with graded scores (49 @ 494 m, 46 @ 567 m,
46 @ 582 m) — so the change reaches the scanners, not just the map. No crashes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
Adds a sixth source: police and surveillance aircraft overhead, via the free
community ADS-B networks (opendata.adsb.fi, api.adsb.lol fallback, no API
key). Contacts are matched by ICAO 24-bit address against a bundled registry
of 1,971 US law-enforcement airframes generated by scripts/gen-le-aircraft.py
from ADSBexchange's basic-ac-db and committed as res/raw/le_aircraft.csv.
Community feeds are used deliberately: FlightAware and Flightradar24 filter
law-enforcement flights at government request, which is the traffic this
source exists to see. Two parse traps handled — the envelope is {"ac":[..]}
on radius queries but {"aircraft":[..]} on wider ones, and alt_baro is the
string "ground" when parked.
Registry coverage is imperfect, so the scanner also detects loiter/orbit
behaviour (3+ samples over 4+ min within 5 km of their centroid, below
12,000 ft and under 200 kt). Surveillance aircraft circle; airliners and
medevac flights going somewhere do not. Behaviour-only hits are capped at 69
and must be within 8 km, so ordinary traffic never alerts.
Rejected with measurements: plane-alert-db matched 0 of 394 live aircraft
over a 250 nm sweep of DC while 15 helicopters were aloft (only 255 of its
entries are US-registered); registry.faa.gov is Akamai-403 and N-number
keyed; OpenSky's CSV is 94 MB for the same data; EFF's Atlas of Surveillance
has no aircraft identities at all, only a 2022 FAA drone lookup.
Overpass query widened from ALPR-only to man_made=surveillance plus
highway=speed_camera, classified into ALPR / speed camera / generic camera
with separate falloff curves so a shop's CCTV cannot alarm like a Flock
install. Cache key bumped to deflock2_ so v1 ALPR-only entries are not served
for another 24 h.
DeFlock, Waze and aircraft are now scored by continuous distance falloff
rather than flat values. The anchors are absolute metres and deliberately
independent of the detection-radius setting: moving a slider must not move
the threat level.
New SOURCES.md documents every endpoint, identifier and scoring table,
including the sources that were tried and rejected and the measurements that
killed them.
Verified against live traffic: detected San Diego PD's AS50 at 1580 m /
1000 ft scoring 83 (ORANGE) and a San Diego County Sheriff B407 at 11742 m
scoring 44, with the score tracking the helicopter from 81 to 83 as it
closed. Both match the falloff formula exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
setup-android defaults to packages "tools platform-tools", but Google has
removed the obsolete `tools` package from the SDK repository, so sdkmanager
exits 1 with "Failed to find package 'tools'". Pass packages: '' — AGP
downloads the platform and build-tools it needs, and this step only has to
provide sdkmanager and accept licenses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
v4 ignores the runner's preinstalled cmdline-tools (12.0), downloads 20.0,
and its license auto-accept then fails with "6 of 7 SDK package licenses not
accepted" followed by an interactive prompt. It passed on 2026-08-29 and
broke by 2026-09-17, so the trigger is a runner-image change rather than the
workflow. v3 is known-good for this repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
Drops the api.blackflagintel.com proxy. Routing everyone through one shared
credential meant handing out a token that wasn't theirs and billing every
install to one account; now each install authenticates with its owner's own
OpenWeb Ninja key, entered in Settings and stored encrypted via SecureStore
(Keystore AES/GCM). Nothing is baked into the APK — verified against the
built dex: the endpoint and X-API-Key header are present, no key, no
blackflagintel host, no X-App-Token.
Requests now send alert_types=POLICE&max_jams=0. Both are honored server-side
as of 2026-09-16 (alert_types was ignored when this was first written), which
cuts a typical response from ~18 KB to ~1.5 KB. max_alerts stays at the 200
ceiling and the client still filters by type, so a silent upstream revert
can't crowd POLICE out or let other types through.
Settings: wazeProxyToken -> wazeApiKey under a new storage key; the stale
proxy token is purged from the secret store on first run rather than left
behind. Auth failures now read "Invalid or missing API key (HTTP 401)".
Verified live with a real key: the exact URL the client builds (including
URLEncoder's %2C bbox commas) returns 200 with POLICE alerts, and on-device
the key persists through SecureStore and reaches the client
(WazeScanner: configured=true). The fetch->parse->emit leg could not be
exercised on the emulator: it delivers no location fix (last location=null,
shell denied MOCK_LOCATION), which blocks every location-driven source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
The Onion-Location header already offers the mirror to anyone browsing in Tor
Browser. This is for everyone else — someone in an ordinary browser, who
otherwise has no way to learn the onion exists.
The address is printed in full rather than hidden behind a "Tor" link, because
a .onion link is a DNS failure outside Tor Browser; the string itself is the
useful artifact. currentColor and opacity so it inherits each footer's existing
colour instead of needing per-site theme variables, user-select:all so one
click selects the whole thing, and word-break so 56 characters wrap rather than
overflow on a phone. No JS, so it survives Tor Browser's Safest level.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRcCTSYyyiYiTzTwMyfQb6
These sites are mirrored as Tor onion services, and a webfont pulled from
Google left the network through an exit node on every page load — telling a
third party about a visitor who chose an onion precisely so that would not
happen.
Variable fonts, latin + latin-ext: one file per subset covers every weight the
site uses, so it is one request instead of five, which matters more over Tor
than on clearnet. latin-ext is only fetched when a page actually contains such
a character. All three families are OFL-1.1, which permits redistribution;
see fonts/OFL.txt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRcCTSYyyiYiTzTwMyfQb6
AGP 9 rejects the standalone Kotlin plugin, so v0.5.5 removed the alias from
app/build.gradle.kts and the version catalog — but the root build file also
declared it with `apply false`, leaving an unresolved catalog reference that
only CI hit. The local build had passed on a stale configuration-cache entry
that never recompiled the root script; verified here with a clean build and
--no-configuration-cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
Citizen ended the police-dispatch partnership behind its public feed in
June 2026 and stubbed the endpoints: /api/incident/trending returns a bare
JSON empty string, /api/incident/{id} returns {} for any id, and
data.sp0n.io/v1/incidents/trending returns a zero-byte body even with no
params. Passing that "" to JSONObject is what surfaced "Value of type
java.lang.String cannot be converted to JSONObject" in the drill-down.
CitizenClient now models the stub as TrendingResult.Retired instead of
throwing, keeping the "results present but empty" case a real Success, and
CitizenScanner reports the shutdown plainly and backs off to a 30-min
heartbeat rather than polling a dead endpoint every 60 s.
Toolchain: AGP 9.3.2, Gradle 9.7.1, Kotlin 2.4.10, Compose BOM 2026.08.00,
core-ktx 1.19.0, lifecycle 2.11.0, activity-compose 1.13.0,
play-services-location 21.4.0, compileSdk 37. targetSdk stays 35 on purpose
— API 36+ tightens foreground-service behavior and screen-off scanning is
the core feature. AGP 9 bundles Kotlin, so the standalone kotlin.android
plugin is dropped. CI actions bumped off deprecated Node-20 versions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
IEEE-verified vendor registrations (ShotSpotter, WatchGuard/Motorola,
Verkada, Avigilon Alta, Axis, FLIR, Hanwha, March Networks, GeoVision,
Mobotix, Sunell) added to both radio target tables via a shared
VendorOuis map with per-vendor drill-down labels. Police-exclusive
vendors (WatchGuard Video, ShotSpotter) score ORANGE on sight — same
rationale as the Axon OUI. README + showcase page updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
Swaps the ◉ glyph for the new radar logo (nav, footer, favicon, OG card),
bumps to v0.5.3, and notes smart-glasses (Meta/Snap/Vuzix) in the Commercial source.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Radar logo — range rings, crosshair, sweep beam, red contact blip on the dark
screen. Adaptive foreground + a monochrome themed-icon layer + a matching
notification small icon (was the generic system ic_menu_view).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
Camera-bearing smart glasses via Bluetooth SIG company IDs (Meta 0x01AB, Meta
Platforms Technologies 0x058E, Luxottica 0x0D53, Snap 0x03C2, Vuzix 0x060C) —
the same manufacturer-id vector used for Echo/Nest and the one Nearby Glasses
relies on. RayNeo/XREAL/Rokid (no dedicated SIG id) matched by BLE-name hint.
New Family.GLASSES; COMMERCIAL toggle label updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
Every CI build previously minted a fresh debug keystore, so each release failed
to install over the last (signature mismatch) — forcing an uninstall and loss of
the on-device proxy token. Commit a fixed debug.keystore (a debug key is
non-secret; password is the well-known "android") and point the debug
signingConfig at it, so CI and local builds sign identically and updates install
in place. No functional app changes.
versionCode 17 -> 18, versionName 0.5.1 -> 0.5.2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
- Larger main-screen map circle (220 -> 300dp) wrapped in a threat-color ring
(green/yellow/orange/red while scanning, gray idle).
- User position renders as a ⌖ crosshair; map geodata color-coded by source
(Flock/DeFlock red, Waze blue, Citizen purple) in both the main circle and
the floating overlay bubble.
- START button moved to the bottom of the screen.
- README brought current: COMMERCIAL source + overlay documented, Waze-via-proxy
and the CI release pipeline described, architecture/permissions/changelog
updated, build reqs corrected to JDK 17 / SDK 35.
versionCode 16 -> 17, versionName 0.5.0 -> 0.5.1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
release.yml builds :app:assembleDebug (JDK 17 + Android SDK) and uploads the
APK as an artifact on every run; on a v* tag push it also creates a GitHub
Release with the APK attached. No build-time secrets — the app carries no
key/token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
Re-adds the WAZE detection source (removed in v0.1.5 when Waze reCAPTCHA-gated
its live-map endpoint). Data comes from OpenWeb Ninja's hosted Waze feed, but
the paid API key never ships in the app: a Caddy reverse proxy at
api.blackflagintel.com injects the key server-side, and the app authenticates
with a scoped X-App-Token entered in Settings and stored encrypted via the
Android Keystore (SecureStore). No credential is baked into the APK.
- scan/WazeClient.kt, scan/WazeScanner.kt: proxy client + 4-min poller,
200-alert page + client-side POLICE filter, 45-min freshness window.
- data/settings/SecureStore.kt: Keystore AES/GCM at-rest store (no dependency).
- Settings: encrypted wazeProxyToken + "Waze police feed" token field.
- ConfidenceEngine.scoreWaze, SourceHealth/DetectionSource WAZE, service wiring.
- Removes the BuildConfig/local.properties key baking entirely.
versionCode 15 -> 16, versionName 0.4.0 -> 0.5.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
v0.3.1 introduced TouchInterceptor (a FrameLayout wrapping the
ComposeView) so we could intercept touches before the inner MapView
saw ACTION_DOWN. That made the wrapper the window-root view (the
View actually attached to WindowManager) but I left the
ViewTreeLifecycleOwner / ViewTreeSavedStateRegistryOwner tags on
the inner ComposeView.
Compose's WindowRecomposer.create looks up findViewTreeLifecycleOwner
on the window-root, not on the inner ComposeView. With the tag
missing on the wrapper, Compose throws IllegalStateException at
composition startup — service crash on overlay enable.
Fix: move setViewTreeLifecycleOwner + setViewTreeSavedStateRegistryOwner
to the wrapper. v0.3.0 worked because the ComposeView itself was the
window-root then; the same pattern (owners on whatever's attached
directly to WindowManager) holds here.
- Bug: dragging the bubble panned the OSM map instead of moving the
bubble. The OnTouchListener was attached to the ComposeView, but
the inner MapView consumed ACTION_DOWN for its own pan handling
before the listener fired. Fix: wrap the ComposeView in a custom
TouchInterceptor FrameLayout whose onInterceptTouchEvent always
returns true. Touches go to the wrapper's OnTouchListener; child
views (including MapView) never see them. Bubble is now purely a
visualization — pan/zoom is impossible.
- Drag-to-dismiss: when the user starts dragging (after passing
TAP_SLOP), a translucent dark circle with a white X appears at
bottom-center via a separate WindowManager view. Highlights red
when the bubble's screen-space center is within hit slop of the X.
Releasing on the X tears down the bubble AND fires onDismissed —
DetectionService flips setOverlayEnabled(false) so the toggle and
the bubble state stay in sync. Releasing elsewhere is a normal
drag (just repositions). The dismiss zone uses FLAG_NOT_TOUCHABLE
so it never steals the gesture; it's purely visual feedback.
A 140dp draggable bubble shows the same map / tier scrim / user dot /
ALPR dots that the in-app circle does, on top of any other app, while
scanning is on. Tap = brings the host app forward; drag = repositions.
- Manifest: add SYSTEM_ALERT_WINDOW (special-access — granted via
system Settings page, not the runtime prompt).
- Settings: add overlayEnabled flag (default off) + a "Display over
other apps" section in SettingsScreen. Flipping the toggle to on
fires Settings.ACTION_MANAGE_OVERLAY_PERMISSION so the user can
grant via the system page; if they deny or revoke, the OverlayMgr
re-checks canDrawOverlays() at every show() call and silently
no-ops, no crash.
- New OverlayManager: owns the WindowManager view at
TYPE_APPLICATION_OVERLAY with FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL
so touches outside the bubble pass through and the bubble never
steals IME focus. Custom OverlayOwner implementing LifecycleOwner +
SavedStateRegistryOwner since LifecycleService doesn't satisfy SSR
(Compose's ComposeView requires both via the view tree).
- Drag/tap handler at the View layer: rawX/rawY math for the drag,
TAP_SLOP_PX guard to discriminate tap from drag, tap launches
MainActivity (FLAG_ACTIVITY_NEW_TASK | SINGLE_TOP).
- New OverlayBubble composable: smaller (140dp) self-contained version
of the in-app threat circle that pulls running/threat/location/
mapPoints/proximity from the same companion StateFlows. Shared
dot-drawable helper extracted into ui/MarkerIcons.kt.
- DetectionService observes settings.overlayEnabled in beginScanning
and toggles the overlay; endScanning hides it.
- Map markers: replace osmdroid's default teardrop pin (which reads as
a 'click me' affordance the map doesn't actually offer) with simple
red dots for ALPRs, matching the blue user-position dot. Drawables
share a single dotDrawable() helper.
- Live re-eval on proximity slider change. Bug: moving the DeFlock or
Citizen distance slider while scanning updated the map's visible
radius but didn't trigger a re-evaluation of which detections fire,
so events outside a tightened radius lingered until restart and
events inside a widened radius wouldn't appear until the next
fix/poll cycle. Fix: add DetectionStore.clearSource(), add
refresh() to DeflockScanner + CitizenScanner that clear the source
and re-emit against cached state, observe the proximity StateFlows
in DetectionService (drop initial replay so we don't redundantly
clear+re-emit on first scan start).
- COMMERCIAL row label was clipping under the Switch. Shorten to
'COMMERCIAL • Nest, Ring, Echo' (drop 'devices') and give every
SourceToggle's Text Modifier.weight(1f) so labels wrap gracefully
on narrow displays instead of getting truncated.
- Center the OVERWATCH header (was left-aligned with the gear pushed
by SpaceBetween — now Box-aligned so the title sits dead-center
regardless of icon width).
- Settings + drill-down: rename the MIC source label to "COMMERCIAL"
("Nest, Ring, Echo devices") for clarity. Internal enum stays MIC.
- Drop the GREEN/YELLOW/ORANGE/RED text inside the threat circle and
bump the tier scrim alpha (0.40-0.65 vs. 0.18-0.50) so the color
reads at a glance over OSM tiles.
- Force the per-event "Open in Maps" pin to use Google Maps instead
of whichever geo: handler the user has set as default (Waze, etc.
could intercept). setPackage("com.google.android.apps.maps") + a
matching <queries> entry in the manifest so it works on Android
11+; web fallback if Maps isn't installed.
- Add a blue user-position dot at the center of the map circle, drawn
on top of any ALPR pins.
- Auto-fit the visible map radius to max(deflockProximityM,
citizenProximityM) via zoomToBoundingBox so the circle's edge
literally represents the alert distance the user has chosen.
- 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.