13 Commits

Author SHA1 Message Date
Kara Zajac f958e30653 Drop the DREAMMAKER byline from the footer
OVERWATCH stands on its own — remove the "DREAMMAKER project" attribution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173RhsXkdMViDR8DMNAG3ms
2026-07-15 20:39:33 -04:00
KaraZajac fb4502f79a docs: clean up the README header (drop DREAMMAKER branding)
Replaces the stylized "[DЯΣΛMMΛKΣЯ] // 0VΣЯW4TCH" header block with a plain
"# OVERWATCH" title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
2026-07-15 20:36:33 -04:00
Kara Zajac 869726ce19 Cache-bust style.css so the new .shots gallery rules load
Cloudflare was serving a stale cached style.css without the screenshots
gallery CSS; version the link (?v=2) to fetch the fresh stylesheet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173RhsXkdMViDR8DMNAG3ms
2026-07-15 19:18:13 -04:00
KaraZajac 66b58891a3 docs: add a Screenshots section to the README landing page
Shows the three app screens (main / sources / settings) on the repo's front
page, reusing the existing docs/img assets. Complements the showcase site's
gallery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
2026-07-15 19:13:20 -04:00
Kara Zajac 7d85dec970 Add screenshots gallery to the OVERWATCH showcase page
New "Screens" section (3-up phone gallery: threat ring, source
drill-down, settings) with captions, plus a nav anchor and .shots CSS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173RhsXkdMViDR8DMNAG3ms
2026-07-15 19:09:10 -04:00
Kara Zajac 98b6769256 docs: adopt the radar icon + v0.5.3 (smart-glasses detection)
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>
2026-07-15 18:55:07 -04:00
KaraZajac ae6ea536fc v0.5.3 — smart-glasses detection + radar icon
Build APK / build (push) Has been cancelled
versionCode 18 -> 19, versionName 0.5.2 -> 0.5.3. README: COMMERCIAL source now
lists smart glasses; changelog updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
2026-07-15 18:29:39 -04:00
KaraZajac 7eb3f2e1b3 feat: radar app icon (launcher + themed + notification)
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
2026-07-15 18:28:54 -04:00
KaraZajac c730657605 feat: detect Meta/Snap/Vuzix smart glasses in the COMMERCIAL source
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
2026-07-15 18:28:54 -04:00
KaraZajac 610d6161e8 docs: link the homepage (overwatch.netslum.io) in the README
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR
2026-07-13 22:51:51 -04:00
Kara Zajac 84b1de016b docs: add OVERWATCH showcase page (overwatch.netslum.io)
Static landing page (index.html + style.css + app.js + og) reusing the
SKELETONKEY project-page design system, tailored to the Android app:
hero + threat-circle screenshot, six detection-source cards, the
green/yellow/orange/red tier model, passive-only ethics, and a
sideload guide. Canonical = overwatch.netslum.io.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 18:03:11 -04:00
KaraZajac 9314814ea4 v0.5.2 — commit fixed debug keystore for stable signing
Build APK / build (push) Has been cancelled
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
2026-07-13 17:11:07 -04:00
KaraZajac 7671e8fef8 v0.5.1 — UI: map ring, ⌖ crosshair, source-color dots, bottom START
Build APK / build (push) Has been cancelled
- 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
2026-07-13 17:03:06 -04:00
24 changed files with 2088 additions and 43 deletions
+64 -20
View File
@@ -1,5 +1,4 @@
# [DЯΣΛMMΛKΣЯ]
## &nbsp;&nbsp;&nbsp;. //0VΣЯW4TCH
# OVERWATCH
A native Android (Kotlin) **passive surveillance-detection** app. Open it, hit
**START**, and a circle turns **green / yellow / orange / red** depending on
@@ -13,7 +12,21 @@ on upward escalations — you don't have to be looking at the screen.
> advertise/fuzz code from one of the reference projects is intentionally
> excluded.
Latest release: [v0.5.0](https://github.com/KaraZajac/OVERWATCH/releases) (debug-signed APK, sideload).
Website: **[overwatch.netslum.io](https://overwatch.netslum.io)** · Latest release: [v0.5.3](https://github.com/KaraZajac/OVERWATCH/releases) (debug-signed APK, sideload).
---
## Screenshots
<p align="center">
<img src="docs/img/overwatch-main.png" width="240" alt="Main screen — live threat-map circle" />
&nbsp;&nbsp;
<img src="docs/img/overwatch-sources.png" width="240" alt="Detection-source drill-down" />
&nbsp;&nbsp;
<img src="docs/img/overwatch-settings.png" width="240" alt="Settings" />
</p>
<p align="center"><sub>Live threat-map circle · source drill-down · settings</sub></p>
---
@@ -26,6 +39,7 @@ Latest release: [v0.5.0](https://github.com/KaraZajac/OVERWATCH/releases) (debug
| **DEFLOCK** | Crowdsourced ALPR locations within configurable proximity (default 200 m) | POST to Overpass API (`overpass.deflock.org` → fallback `overpass-api.de`) for `man_made=surveillance + surveillance:type=ALPR` in a 5 km bbox; 24 h on-disk cache by 0.05° grid cell. Refetches when the user moves > 1.5 km from the last fetch center. Backoffs after Overpass failures; treats `{"remark": "...timed out..."}` 200-responses as failure so timeouts don't poison the cache. |
| **CITIZEN** | Real-time public-safety incidents (police-relevant only — fire/medical-only events filtered out) within configurable proximity, < 30 min old | `citizen.com/api/incident/trending` (bbox) polled every 60 s, then per-incident detail via `/api/incident/{id}` with an in-memory cache so each incident is fetched once per session. First poll fires immediately on the first location fix. |
| **WAZE** | User-reported `POLICE` alerts still active in the feed within configurable proximity (default 500 m), up to ~45 min old | `api.blackflagintel.com/waze/alerts-and-jams` — the OVERWATCH proxy (Caddy) that injects the OpenWeb Ninja key server-side and forwards to their hosted Waze scrape, sidestepping the reCAPTCHA gating that 403s direct `live-map/api/georss` calls. The app authenticates with an `X-App-Token` entered in Settings (encrypted on-device); the paid key never ships in the APK. Polled every ~4 min. Upstream ignores type filtering and caps at 200 alerts, so the client pulls the full page and filters to `POLICE` itself. Alerts carry confidence (05) + reliability (010); high values nudge the score up. No token → source shows "not configured" in the drill-down. |
| **COMMERCIAL** | Nearby consumer smart-home / voice gear (Nest, Ring, Echo, hidden cams) and camera-bearing smart glasses (Meta, Snap, Vuzix) as a secondary situational signal | Rides the BLE + WiFi scans — OUI / device-name / service-UUID / SSID matches plus Bluetooth SIG company IDs from `MicTargets`. Score-capped at ORANGE so a cluster of doorbells (or a passing pair of Ray-Bans) never reads as ALPR-grade certainty. |
> **Waze is back (v0.4.0+), via a key-protected proxy.** Waze reCAPTCHA-gated its `live-map/api/georss` endpoint in 2025/2026 — automated calls get HTTP 403 regardless of IP or headless-vs-headful browser (it scores browser *reputation*, verified by direct testing), which is why v0.1.5 removed the original integration and why no free scraper survives. OVERWATCH reads Waze POLICE alerts through [OpenWeb Ninja](https://www.openwebninja.com)'s hosted feed (pay-as-you-go ~$0.005/req, ≈ $13/mo at the 4-min poll). To keep the paid key off every device, the app doesn't hold it: a Caddy reverse proxy at `api.blackflagintel.com` injects the key server-side, and the app authenticates with a scoped, revocable `X-App-Token` entered in Settings (stored encrypted via the Android Keystore). The Waze for Cities partner feed was ruled out — it excludes POLICE and is agency-only. Waze complements Citizen: denser for roadway stops / speed traps.
@@ -45,13 +59,21 @@ match in the same area produce a higher tier than either alone). When idle,
the circle shows muted gray with `IDLE` text so it's distinguishable at a
glance from "scanning, all clear."
While scanning, the circle becomes a live OpenStreetMap centered on you, wrapped
in a **threat-color ring** (the current tier at a glance) and marked with a ⌖
crosshair for your position. Map geodata is color-coded by source — **Flock /
DeFlock cameras red, Waze police blue, Citizen incidents purple** — so each dot
is self-explanatory. The same map renders in a smaller floating overlay bubble
(Settings → Display over other apps) so it works over other apps.
---
## How alerts work
- **In-app**: the threat circle pulses while scanning; tap it to open the
bottom-sheet drill-down with per-source rows. DEFLOCK and CITIZEN events
carry coordinates — each row has a tap-to-open Maps icon (`geo:` intent).
- **In-app**: the threat circle shows a live map with a threat-color ring and
source-color dots while scanning; tap it to open the bottom-sheet drill-down
with per-source rows. DeFlock, Citizen, and Waze events carry coordinates —
each row has a tap-to-open Maps icon.
- **Foreground notification**: rebuilt on every threat-tier change. Title
becomes `OVERWATCH • RED` (or whatever tier); text shows the top
detection's score + label. Notification priority bumps to HIGH on RED so
@@ -67,24 +89,30 @@ glance from "scanning, all clear."
## Architecture
```
ui/MainScreen.kt circle + START/STOP + tap-to-open bottom sheet
ui/SettingsScreen.kt source toggles, distance sliders, vibrate, theme
ui/MainScreen.kt map circle + threat ring + START/STOP + drill-down sheet
ui/OverlayBubble.kt floating "chat-bubble" version of the map circle
ui/MarkerIcons.kt map marker drawables — source dots + ⌖ user crosshair
ui/SettingsScreen.kt source toggles, distance sliders, Waze token, vibrate, theme
ui/theme/Theme.kt Material 3 dark/light + threat colors
service/DetectionService.kt foreground service — owns scanners, notification, vibration
service/OverlayManager.kt WindowManager host for the floating overlay bubble
scan/BleScanner.kt BLE callback scanner
scan/WifiScanner.kt WifiManager poller + SCAN_RESULTS receiver
scan/DeflockClient.kt Overpass POST (deflock.org → overpass-api.de) + 24h cache
scan/DeflockScanner.kt location-driven proximity check + failure backoff
scan/CitizenClient.kt GET /api/incident/trending + /api/incident/{id}
scan/CitizenScanner.kt 60 s poller, fire/medical filter, per-id cache
fusion/ConfidenceEngine.kt scoring (one place — BLE / WiFi / DeFlock / Citizen)
scan/WazeClient.kt GET api.blackflagintel.com proxy (X-App-Token) → OpenWeb Ninja
scan/WazeScanner.kt ~4 min poller, 200-alert page, client-side POLICE filter
fusion/ConfidenceEngine.kt scoring (BLE / WiFi / DeFlock / Citizen / Waze / Commercial)
fusion/RssiTracker.kt rise-peak-fall stationary-signal detector
fusion/DetectionStore.kt in-memory dedup, 5-min retention, max-tier flow
fusion/SourceHealth.kt per-source OK/FAILED registry for the drill-down
fusion/ThreatLevel.kt 4-tier enum + DetectionSource enum
data/location/LocationProvider.kt FusedLocationProviderClient wrapper
data/settings/Settings.kt SharedPreferences-backed StateFlow settings
data/targets/ BleOuis, WifiOuis, RavenUuids, Patterns, Manufacturers
data/settings/SecureStore.kt Keystore AES/GCM store for the Waze proxy token
data/targets/ BleOuis, WifiOuis, RavenUuids, Patterns, Manufacturers, MicTargets
```
No detection-history database. All state is in-memory and clears on stop, by
@@ -96,8 +124,8 @@ into a stuck state.
## Build & install
Requires:
- **JDK 21** (Android Gradle Plugin 8.7.x rejects JDK 26)
- **Android Studio** with SDK Platform 34 + Build-Tools 34.x + Platform-Tools
- **JDK 17** (17 or 21; Android Gradle Plugin 8.7.x rejects JDK 26)
- **Android Studio** with SDK Platform 35 + Build-Tools 35.x + Platform-Tools
```sh
# 1) Copy the example local.properties and point sdk.dir at your install
@@ -115,6 +143,13 @@ export JAVA_HOME=/usr/local/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
Or download the latest debug-signed APK from
[Releases](https://github.com/KaraZajac/OVERWATCH/releases).
Releases are cut by CI: pushing a `v*` tag runs `.github/workflows/release.yml`,
which builds `:app:assembleDebug` and attaches the APK to a GitHub Release. No
build-time secrets — the app ships with no key or token. A fixed `debug.keystore`
is committed (a debug key is non-secret; password is the well-known `android`) so
every build — CI or local — signs identically and updates install in place
without an uninstall.
---
## Permissions
@@ -126,10 +161,11 @@ Or download the latest debug-signed APK from
| `ACCESS_FINE_LOCATION` | Required for BLE pre-S, WiFi pre-T, and DeFlock/Citizen proximity |
| `NEARBY_WIFI_DEVICES` (API 33+) | WiFi scan results without using location |
| `ACCESS_WIFI_STATE`, `CHANGE_WIFI_STATE` | Trigger and read scan results |
| `INTERNET`, `ACCESS_NETWORK_STATE` | DeFlock Overpass + Citizen API |
| `INTERNET`, `ACCESS_NETWORK_STATE` | DeFlock Overpass, Citizen API, Waze proxy |
| `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_CONNECTED_DEVICE`, `FOREGROUND_SERVICE_LOCATION` | Keep scanning with the screen off |
| `POST_NOTIFICATIONS` (API 33+) | Foreground-service notification |
| `VIBRATE` | Haptic alert on threat-tier escalation |
| `SYSTEM_ALERT_WINDOW` | Optional floating threat-circle overlay (special-access; granted via system settings) |
Requested at runtime when you press START for the first time. If you
permanently deny a required permission ("don't ask again"), the START button
@@ -142,7 +178,7 @@ so you can grant manually.
Tap the gear icon in the top-right.
- **Detection sources**: toggle BLE / WiFi / DeFlock / Citizen / Waze independently.
- **Detection sources**: toggle BLE / WiFi / DeFlock / Citizen / Waze / Commercial independently.
Changes take effect on the next Start. While scanning, a **Restart scan to
apply** button appears that does `stop()` + `start()` in one tap.
- **Proximity thresholds** (sliders commit on release, not per-pixel):
@@ -154,6 +190,8 @@ Tap the gear icon in the top-right.
paid OpenWeb Ninja key stays server-side on the proxy. Empty = Waze source off.
- **Alerts**:
- Vibrate on threat escalation (default on)
- **Display over other apps**: floating threat-circle overlay (needs the
special-access permission; the app bounces you to the system page to grant it).
- **Appearance**: System / Dark / Light (default Dark)
---
@@ -166,22 +204,28 @@ These live under `REFERENCES/` (gitignored):
- **flock-detection** — confidence-scoring algorithm (highest reusability), RSSI rise-peak-fall, OUIs + UUIDs + patterns
- **flock-you** — 31-OUI WiFi superset (promiscuous-mode tricks not portable to Android)
- **deflock** + **deflock-app** — Overpass query format + proximity-alert pattern (the Flutter app uses Overpass directly, not the CDN tiles, which the OVERWATCH client mirrors)
- **wazepolice** — live-map/api/georss recipe; informed v0.1.0v0.1.5 Waze integration that has since been removed (endpoint is reCAPTCHA-gated)
- **wazepolice** — original live-map/api/georss recipe; that endpoint is now reCAPTCHA-gated, so v0.5.0 re-added Waze via the OpenWeb Ninja proxy (`api.blackflagintel.com`) instead of hitting Waze directly
---
## Status
Phases 15 (skeleton, BLE, WiFi, DeFlock, Citizen, polish) complete and
field-tested. Current release **v0.1.7** addresses two full audit passes
(see release notes for v0.1.2, v0.1.3, v0.1.6). Notable changes since v0.1.0:
field-tested. Current release **v0.5.1**. Notable changes:
- v0.1.2 — Android 14+ foreground service type fix (location was being silently revoked); NaN-coordinate filter on map data.
- v0.1.2 — Android 14+ foreground service type fix; NaN-coordinate filter on map data.
- v0.1.3 — DeFlock CDN replaced by direct Overpass calls (Cloudflare-blocked).
- v0.1.4 — Citizen.com added as 5th source, per-source health registry.
- v0.1.5 — Waze removed (reCAPTCHA-gated; no clean mobile workaround).
- v0.1.6 — Dynamic notification with tier + label, haptic alerts on escalation, Open-in-Maps for geo events, idle visual differentiated from "scanning, all clear", permanent-deny recovery via Open Settings.
- v0.1.5 — Waze removed (reCAPTCHA-gated; no clean mobile workaround at the time).
- v0.1.6 — Dynamic notification with tier + label, haptic alerts, Open-in-Maps for geo events.
- v0.1.7 — System back from Settings returns to MAIN instead of exiting.
- v0.2.0 — Live map circle + COMMERCIAL source (Nest / Ring / Echo / hidden cams).
- v0.2.1v0.2.2 — Live proximity refresh, map dot markers, map + Settings UI polish.
- v0.3.0v0.3.2 — Floating threat-circle overlay (chat-bubble style) + drag/crash fixes.
- v0.5.0 — Waze re-added via a key-protected Caddy proxy (`api.blackflagintel.com`): the OpenWeb Ninja key stays server-side, the app uses an encrypted per-device token. GitHub Actions release pipeline added.
- v0.5.1 — UI: larger map circle with a threat-color ring, ⌖ user crosshair, source-color dots (Flock red / Waze blue / Citizen purple), START moved to the bottom.
- v0.5.2 — Committed a fixed debug keystore so CI + local builds sign identically; updates now install in place (no functional changes).
- v0.5.3 — Detect Meta / Snap / Vuzix smart glasses in the COMMERCIAL source (BLE company-id + name vectors); new radar app icon (launcher, themed, and notification).
## License
+15 -2
View File
@@ -12,8 +12,21 @@ android {
applicationId = "org.soulstone.overwatch"
minSdk = 26
targetSdk = 35
versionCode = 16
versionName = "0.5.0"
versionCode = 19
versionName = "0.5.3"
}
// Fixed debug keystore committed to the repo (a debug key is non-secret — its
// password is the well-known "android") so CI and local builds sign
// identically. Without it each CI build minted a fresh debug key and updates
// wouldn't install over the previous one.
signingConfigs {
getByName("debug") {
storeFile = rootProject.file("debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
}
buildTypes {
@@ -5,17 +5,19 @@ import java.util.UUID
/**
* Curated targets for "device with a microphone in your space" detection.
*
* Scope is intentionally narrow — only well-known smart-home OEMs whose devices
* stay in fixed locations and continuously listen. Apple manufacturer id 0x004C
* is deliberately excluded because every iPhone, AirPod, and Apple Watch
* advertises it; a coffee shop full of phones must not light up the alarm.
* Scope is intentionally narrow — well-known smart-home OEMs whose devices stay
* in fixed locations and continuously listen, plus body-worn smart glasses
* (Meta Ray-Ban, Snap Spectacles) that record the space around them. Apple
* manufacturer id 0x004C is deliberately excluded because every iPhone, AirPod,
* and Apple Watch advertises it; a coffee shop full of phones must not light up
* the alarm.
*
* Detection vectors collected from public OUI registries (Wireshark/IEEE)
* and device-setup advertisement docs.
*/
object MicTargets {
enum class Family { ECHO, RING, GOOGLE, HIDDEN_CAM }
enum class Family { ECHO, RING, GOOGLE, HIDDEN_CAM, GLASSES }
/** Bluetooth SIG company identifiers for "voice/smart-home" device families. */
private val MFG_GOOGLE = 0x00E0
@@ -23,6 +25,25 @@ object MicTargets {
/** Yingxin / cheap-spy-cam mfg id seen in field reports. */
private val MFG_YINGXIN = 0x05A7
/**
* Bluetooth SIG company identifiers for camera-bearing smart glasses. The
* manufacturer-id field is the reliable vector — device names and service
* UUIDs are inconsistent across advertising frames (per the open-source
* Nearby Glasses project, which this mirrors). Caveat: these ids are
* manufacturer-wide, so Meta's also match Quest VR headsets (still a
* camera/mic device, so an acceptable secondary signal). Luxottica in BLE
* mfg data is essentially only their *smart* eyewear — plain sunglasses
* have no radio. Brands without a dedicated SIG company id (RayNeo — which
* rides under TCL's 0x0BC6, too broad to use — plus XREAL, Rokid) advertise
* under a chipset vendor's id, so those are caught by distinctive BLE-name
* hints below instead.
*/
private val MFG_META = 0x01AB // Meta Platforms, Inc. (ex-Facebook)
private val MFG_META_TECH = 0x058E // Meta Platforms Technologies (Reality Labs; also Quest)
private val MFG_LUXOTTICA = 0x0D53 // Luxottica — Ray-Ban / Oakley Meta frames
private val MFG_SNAP = 0x03C2 // Snap Inc. — Spectacles
private val MFG_VUZIX = 0x060C // Vuzix — enterprise / AR smart glasses
/** Echo/Alexa Voice Service GATT (FE03 — assigned to Amazon Lab126). */
private val UUID_AVS = UUID.fromString("0000fe03-0000-1000-8000-00805f9b34fb")
@@ -63,7 +84,13 @@ object MicTargets {
"Nest" to Family.GOOGLE,
"GoogleHome" to Family.GOOGLE,
"Chromecast" to Family.GOOGLE,
"Google-Home" to Family.GOOGLE
"Google-Home" to Family.GOOGLE,
"Spectacles" to Family.GLASSES,
"Ray-Ban" to Family.GLASSES,
"RayNeo" to Family.GLASSES,
"Vuzix" to Family.GLASSES,
"XREAL" to Family.GLASSES,
"Rokid" to Family.GLASSES
)
private val SSID_HINTS: List<Pair<String, Family>> = listOf(
@@ -115,6 +142,7 @@ object MicTargets {
MFG_AMAZON -> Family.ECHO
MFG_GOOGLE -> Family.GOOGLE
MFG_YINGXIN -> Family.HIDDEN_CAM
MFG_META, MFG_META_TECH, MFG_LUXOTTICA, MFG_SNAP, MFG_VUZIX -> Family.GLASSES
else -> null
}
@@ -149,5 +177,6 @@ object MicTargets {
Family.RING -> "Ring"
Family.GOOGLE -> "Google Nest / Home"
Family.HIDDEN_CAM -> "Possible hidden mic / cam"
Family.GLASSES -> "Smart glasses"
}
}
@@ -403,7 +403,7 @@ class DetectionService : LifecycleService() {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(android.R.drawable.ic_menu_view)
.setSmallIcon(R.drawable.ic_stat_radar)
.setOngoing(true)
.setContentIntent(pi)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
@@ -9,6 +9,7 @@ import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -135,6 +136,7 @@ fun MainScreen(
animating = running,
userLocation = userLocation,
mapPoints = mapPoints,
events = events,
mapRadiusMeters = mapRadiusMeters,
onTap = { showSheet = true }
)
@@ -150,9 +152,15 @@ fun MainScreen(
Spacer(Modifier.height(24.dp))
StatusText(running = running, threat = threat, score = score, events = events)
}
Spacer(Modifier.height(24.dp))
// Push the primary action button to the bottom of the screen.
Spacer(Modifier.weight(1f))
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = onStartStop,
enabled = canStart,
@@ -200,6 +208,8 @@ fun MainScreen(
}
}
}
Spacer(Modifier.height(24.dp))
}
if (showSheet) {
@@ -230,6 +240,7 @@ private fun ThreatMapCircle(
animating: Boolean,
userLocation: Location?,
mapPoints: List<DeflockClient.AlprPoint>,
events: List<DetectionEvent>,
mapRadiusMeters: Float,
onTap: () -> Unit
) {
@@ -240,6 +251,9 @@ private fun ThreatMapCircle(
ThreatLevel.ORANGE -> ThreatColors.Orange
ThreatLevel.RED -> ThreatColors.Red
}
// Steady threat-tier ring around the circle: tier color while scanning,
// muted gray when idle. Gives the current level at a glance even over the map.
val ringColor = if (animating) activeColor else idleColor
val transition = rememberInfiniteTransition(label = "pulse")
val pulse by transition.animateFloat(
@@ -254,7 +268,9 @@ private fun ThreatMapCircle(
Box(
modifier = Modifier
.size(220.dp)
.size(300.dp)
.border(width = 6.dp, color = ringColor, shape = CircleShape)
.padding(6.dp)
.clip(CircleShape),
contentAlignment = Alignment.Center
) {
@@ -300,8 +316,10 @@ private fun ThreatMapCircle(
val ctx = LocalContext.current
// Build the marker drawables once per Composition rather than
// every recomposition — bitmap allocation isn't free.
val userDot = remember(ctx) { dotDrawable(ctx.resources, 36, DOT_USER_BLUE) }
val userMark = remember(ctx) { crosshairDrawable(ctx.resources, 46, MARK_USER_WHITE) }
val flockDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_FLOCK_RED) }
val wazeDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_WAZE_BLUE) }
val citizenDot = remember(ctx) { dotDrawable(ctx.resources, 26, DOT_CITIZEN_PURPLE) }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { c ->
@@ -317,7 +335,8 @@ private fun ThreatMapCircle(
map.controller.setCenter(GeoPoint(fix.latitude, fix.longitude))
map.overlays.clear()
// ALPR dots first, user dot last so the user draws on top.
// Source dots first (Flock red, Waze blue, Citizen purple),
// user position last so the crosshair always draws on top.
for (p in mapPoints) {
map.overlays.add(
Marker(map).apply {
@@ -329,11 +348,29 @@ private fun ThreatMapCircle(
}
)
}
for (e in events) {
val lat = e.lat ?: continue
val lon = e.lon ?: continue
val dot = when (e.source) {
DetectionSource.WAZE -> wazeDot
DetectionSource.CITIZEN -> citizenDot
else -> null
} ?: continue
map.overlays.add(
Marker(map).apply {
position = GeoPoint(lat, lon)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = dot
title = e.label
setInfoWindow(null)
}
)
}
map.overlays.add(
Marker(map).apply {
position = GeoPoint(fix.latitude, fix.longitude)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = userDot
icon = userMark
setInfoWindow(null)
}
)
@@ -25,5 +25,44 @@ internal fun dotDrawable(
return BitmapDrawable(resources, bitmap)
}
internal const val DOT_USER_BLUE = 0xFF2196F3.toInt()
internal const val DOT_FLOCK_RED = 0xFFD7263D.toInt()
/** Builds a ⌖-style position mark (register / gun-sight): a stroked circle with
* a cross through it, painted over a dark halo so it reads on any map tile. Used
* for the user's own location so it stays distinct from the colored source dots. */
internal fun crosshairDrawable(
resources: Resources,
sizePx: Int,
markColor: Int
): BitmapDrawable {
val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val c = sizePx / 2f
val r = sizePx * 0.30f
val pad = sizePx * 0.07f
val stroke = sizePx * 0.10f
val halo = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = 0xB3000000.toInt()
strokeWidth = stroke + 3f
strokeCap = Paint.Cap.ROUND
}
val mark = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = markColor
strokeWidth = stroke
strokeCap = Paint.Cap.ROUND
}
// Halo pass then color pass so the mark stays legible on light or dark tiles.
for (p in listOf(halo, mark)) {
canvas.drawCircle(c, c, r, p)
canvas.drawLine(pad, c, sizePx - pad, c, p) // horizontal
canvas.drawLine(c, pad, c, sizePx - pad, p) // vertical
}
return BitmapDrawable(resources, bitmap)
}
// Per-source marker colors so geodata reads at a glance.
internal const val DOT_FLOCK_RED = 0xFFD7263D.toInt() // Flock / DeFlock ALPR cameras
internal const val DOT_WAZE_BLUE = 0xFF2196F3.toInt() // Waze police reports
internal const val DOT_CITIZEN_PURPLE = 0xFF9C27B0.toInt() // Citizen incidents
internal const val MARK_USER_WHITE = 0xFFFFFFFF.toInt() // the user's own position (⌖)
@@ -29,6 +29,7 @@ import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.soulstone.overwatch.data.settings.Settings
import org.soulstone.overwatch.fusion.DetectionSource
import org.soulstone.overwatch.fusion.ThreatLevel
import org.soulstone.overwatch.service.DetectionService
import org.soulstone.overwatch.ui.theme.ThreatColors
@@ -54,9 +55,11 @@ fun OverlayBubble() {
val threat by DetectionService.store.threatLevel.collectAsState()
val userLocation by DetectionService.location.collectAsState()
val mapPoints by DetectionService.mapPoints.collectAsState()
val events by DetectionService.store.events.collectAsState()
val deflockProx by settings.deflockProximityM.collectAsState()
val citizenProx by settings.citizenProximityM.collectAsState()
val radius = max(deflockProx, citizenProx).toFloat()
val wazeProx by settings.wazeProximityM.collectAsState()
val radius = max(max(deflockProx, citizenProx), wazeProx).toFloat()
val activeColor = when (threat) {
ThreatLevel.GREEN -> ThreatColors.Green
@@ -76,8 +79,10 @@ fun OverlayBubble() {
label = "overlay-pulse"
)
val userDot = remember(ctx) { dotDrawable(ctx.resources, 30, DOT_USER_BLUE) }
val userMark = remember(ctx) { crosshairDrawable(ctx.resources, 34, MARK_USER_WHITE) }
val flockDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_FLOCK_RED) }
val wazeDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_WAZE_BLUE) }
val citizenDot = remember(ctx) { dotDrawable(ctx.resources, 22, DOT_CITIZEN_PURPLE) }
Box(
modifier = Modifier
@@ -128,11 +133,28 @@ fun OverlayBubble() {
}
)
}
for (e in events) {
val lat = e.lat ?: continue
val lon = e.lon ?: continue
val dot = when (e.source) {
DetectionSource.WAZE -> wazeDot
DetectionSource.CITIZEN -> citizenDot
else -> null
} ?: continue
map.overlays.add(
Marker(map).apply {
position = GeoPoint(lat, lon)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = dot
setInfoWindow(null)
}
)
}
map.overlays.add(
Marker(map).apply {
position = GeoPoint(fix.latitude, fix.longitude)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = userDot
icon = userMark
setInfoWindow(null)
}
)
@@ -100,7 +100,7 @@ fun SettingsScreen(
SourceToggle("DEFLOCK • ALPR map (Overpass)", deflock) { settings.setDeflockEnabled(it) }
SourceToggle("CITIZEN • Real-time incident feed", citizen) { settings.setCitizenEnabled(it) }
SourceToggle("WAZE • Live police reports", waze) { settings.setWazeEnabled(it) }
SourceToggle("COMMERCIAL • Nest, Ring, Echo", mic) { settings.setMicEnabled(it) }
SourceToggle("COMMERCIAL • Nest, Ring, Echo, glasses", mic) { settings.setMicEnabled(it) }
Spacer(Modifier.height(8.dp))
if (isRunning) {
Button(
@@ -1,13 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Radar: range rings + crosshair + sweep beam + a red contact blip. Drawn on
top of the @color/bg_dark adaptive background. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- lit screen glow -->
<path android:pathData="M54,54 m-36,0 a36,36 0 1,0 72,0 a36,36 0 1,0 -72,0">
<aapt:attr name="android:fillColor">
<gradient android:type="radial"
android:centerX="54" android:centerY="54" android:gradientRadius="36">
<item android:offset="0.0" android:color="#123A24" />
<item android:offset="1.0" android:color="#000B0E12" />
</gradient>
</aapt:attr>
</path>
<!-- sweep afterglow wedge -->
<path
android:pathData="M54,54 L54,19 A35,35 0 0,1 83.3,35 Z"
android:fillColor="#1FAA59"
android:pathData="M54,30 m-18,0 a18,18 0 1,0 36,0 a18,18 0 1,0 -36,0" />
android:fillAlpha="0.32" />
<!-- range rings -->
<path
android:fillColor="#F4F6FA"
android:pathData="M54,30 m-6,0 a6,6 0 1,0 12,0 a6,6 0 1,0 -12,0" />
android:pathData="M54,54 m-34,0 a34,34 0 1,0 68,0 a34,34 0 1,0 -68,0"
android:strokeColor="#1FAA59"
android:strokeWidth="2" />
<path
android:pathData="M54,54 m-23,0 a23,23 0 1,0 46,0 a23,23 0 1,0 -46,0"
android:strokeColor="#1FAA59"
android:strokeWidth="2"
android:strokeAlpha="0.85" />
<path
android:pathData="M54,54 m-12,0 a12,12 0 1,0 24,0 a12,12 0 1,0 -24,0"
android:strokeColor="#1FAA59"
android:strokeWidth="2"
android:strokeAlpha="0.85" />
<!-- crosshair -->
<path
android:pathData="M20,54 L88,54"
android:strokeColor="#1FAA59"
android:strokeWidth="1.4"
android:strokeAlpha="0.55" />
<path
android:pathData="M54,20 L54,88"
android:strokeColor="#1FAA59"
android:strokeWidth="1.4"
android:strokeAlpha="0.55" />
<!-- leading sweep beam -->
<path
android:pathData="M54,54 L83.3,35"
android:strokeColor="#33E38C"
android:strokeWidth="2.4"
android:strokeLineCap="round" />
<!-- contact blip -->
<path
android:pathData="M72,40 m-7,0 a7,7 0 1,0 14,0 a7,7 0 1,0 -14,0"
android:fillColor="#D7263D"
android:fillAlpha="0.22" />
<path
android:pathData="M72,40 m-3.3,0 a3.3,3.3 0 1,0 6.6,0 a3.3,3.3 0 1,0 -6.6,0"
android:fillColor="#D7263D" />
<!-- hub -->
<path
android:pathData="M54,54 m-2.4,0 a2.4,2.4 0 1,0 4.8,0 a2.4,2.4 0 1,0 -4.8,0"
android:fillColor="#33E38C" />
</vector>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Single-colour radar silhouette for Android 13+ themed icons (system tints it). -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M54,54 m-34,0 a34,34 0 1,0 68,0 a34,34 0 1,0 -68,0"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.2" />
<path
android:pathData="M54,54 m-23,0 a23,23 0 1,0 46,0 a23,23 0 1,0 -46,0"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.2" />
<path
android:pathData="M54,54 m-12,0 a12,12 0 1,0 24,0 a12,12 0 1,0 -24,0"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.2" />
<path
android:pathData="M20,54 L88,54"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.8" />
<path
android:pathData="M54,20 L54,88"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.8" />
<path
android:pathData="M54,54 L83.3,35"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.6"
android:strokeLineCap="round" />
<path
android:pathData="M72,40 m-3.6,0 a3.6,3.6 0 1,0 7.2,0 a3.6,3.6 0 1,0 -7.2,0"
android:fillColor="#FFFFFF" />
<path
android:pathData="M54,54 m-2.6,0 a2.6,2.6 0 1,0 5.2,0 a2.6,2.6 0 1,0 -5.2,0"
android:fillColor="#FFFFFF" />
</vector>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Status-bar / notification small icon. White silhouette; the system tints it. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12 m-10,0 a10,10 0 1,0 20,0 a10,10 0 1,0 -20,0"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.4" />
<path
android:pathData="M12,12 m-5.2,0 a5.2,5.2 0 1,0 10.4,0 a5.2,5.2 0 1,0 -10.4,0"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.4" />
<path
android:pathData="M2.5,12 L21.5,12"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.1" />
<path
android:pathData="M12,2.5 L12,21.5"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.1" />
<path
android:pathData="M12,12 L20,7.5"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.6"
android:strokeLineCap="round" />
<path
android:pathData="M16.4,8.9 m-1.8,0 a1.8,1.8 0 1,0 3.6,0 a1.8,1.8 0 1,0 -3.6,0"
android:fillColor="#FFFFFF" />
</vector>
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/bg_dark" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/bg_dark" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
BIN
View File
Binary file not shown.
View File
+46
View File
@@ -0,0 +1,46 @@
/* OVERWATCH landing — count-up stats + scroll reveal. No network, no deps. */
(function () {
'use strict';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/* count-up numbers when the stats row scrolls into view */
function countUp(el) {
const target = parseInt(el.dataset.target, 10);
if (isNaN(target)) return;
if (!target || reduceMotion) { el.textContent = String(target); return; }
const dur = 1100, start = performance.now();
(function tick(now) {
const p = Math.min(1, (now - start) / dur);
el.textContent = String(Math.round(target * (1 - Math.pow(1 - p, 3))));
if (p < 1) requestAnimationFrame(tick);
})(performance.now());
}
const statsRow = document.getElementById('stats-row');
if (statsRow) {
const nums = statsRow.querySelectorAll('.num[data-target]');
if ('IntersectionObserver' in window && !reduceMotion) {
const io = new IntersectionObserver((entries, obs) => {
entries.forEach((e) => {
if (e.isIntersecting) { nums.forEach(countUp); obs.disconnect(); }
});
}, { threshold: 0.4 });
io.observe(statsRow);
} else {
nums.forEach((el) => { el.textContent = el.dataset.target; });
}
}
/* scroll-triggered reveal */
const reveals = document.querySelectorAll('.reveal');
if (!('IntersectionObserver' in window) || reduceMotion) {
reveals.forEach((el) => el.classList.add('in'));
} else {
const ro = new IntersectionObserver((entries, obs) => {
entries.forEach((e) => {
if (e.isIntersecting) { e.target.classList.add('in'); obs.unobserve(e.target); }
});
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
reveals.forEach((el) => ro.observe(el));
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

+506
View File
@@ -0,0 +1,506 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OVERWATCH — passive surveillance detection for Android</title>
<meta name="description" content="A native Android app that passively flags nearby Flock Safety ALPRs, Axon body cameras, and police presence. Fuses BLE/WiFi radio scans, the DeFlock ALPR map, and Citizen/Waze feeds into a green→red threat tier. Listens only — never transmits.">
<meta property="og:title" content="OVERWATCH — passive surveillance detection">
<meta property="og:description" content="Android app that flags nearby Flock ALPRs, Axon body cams, and police by fusing BLE/WiFi + the DeFlock map + Citizen/Waze. Passive only — it never transmits.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://overwatch.netslum.io/">
<meta property="og:image" content="https://overwatch.netslum.io/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://overwatch.netslum.io/og.png">
<meta name="theme-color" content="#07070d">
<link rel="icon" type="image/svg+xml" href="logo.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css?v=2">
</head>
<body>
<div class="bg-mesh" aria-hidden="true">
<div class="mesh-blob mesh-blob-1"></div>
<div class="mesh-blob mesh-blob-2"></div>
<div class="mesh-blob mesh-blob-3"></div>
</div>
<nav class="nav">
<div class="container nav-inner">
<a class="nav-brand" href="#">
<img class="nav-logo" src="logo.svg" width="30" height="30" alt="" aria-hidden="true">
OVERWATCH
</a>
<div class="nav-links">
<a href="#detect">How it works</a>
<a href="#sources">Sources</a>
<a href="#tiers">Tiers</a>
<a href="#screens">Screens</a>
<a href="#download">Download</a>
<a class="nav-github" href="https://github.com/KaraZajac/OVERWATCH" aria-label="GitHub">
<svg height="18" viewBox="0 0 16 16" width="18" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
</a>
</div>
</div>
</nav>
<!-- ──────────────── HERO ──────────────── -->
<header class="hero">
<div class="container hero-inner">
<div class="hero-eyebrow">
<span class="dot dot-pulse"></span>
v0.5.3 · Android · passive
</div>
<h1 class="hero-title">
<span class="display-wordmark">OVERWATCH</span>
</h1>
<p class="hero-tag">
A native Android app that <strong>passively detects surveillance around you</strong>.
Open it, hit <strong>START</strong>, and a ring turns green → yellow →
orange → red as the engine gets more sure there's a Flock Safety ALPR,
an Axon body camera, or active police nearby.
<span class="hero-tag-pop">It only listens. It never transmits, probes, or jams.</span>
</p>
<div class="phone-hero">
<img class="phone" src="img/overwatch-main.png" width="1096" height="2560"
alt="OVERWATCH main screen: a live map inside a green &quot;all clear&quot; threat ring, centered on the user with a crosshair, and a STOP button." loading="eager">
</div>
<div class="stats-row" id="stats-row">
<div class="stat-chip"><span class="num" data-target="6">0</span><span>fused sources</span></div>
<div class="stat-chip stat-vfy"><span class="num" data-target="4">0</span><span>threat tiers</span></div>
<div class="stat-chip"><span class="num" data-target="0">0</span><span>packets sent</span></div>
<div class="stat-chip"><span class="num" data-target="17">0</span><span>releases</span></div>
</div>
<div class="cta-row">
<a class="btn btn-primary" href="https://github.com/KaraZajac/OVERWATCH/releases/latest">
↓ Download APK
</a>
<a class="btn" href="#detect">How it works</a>
<a class="btn btn-ghost" href="https://github.com/KaraZajac/OVERWATCH">
<svg height="16" viewBox="0 0 16 16" width="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
Source on GitHub
</a>
</div>
<p class="hero-warn">Passive defense only. Debug-signed APK — sideload; no Play Store. <a href="https://github.com/KaraZajac/OVERWATCH">Read the README.</a></p>
</div>
</header>
<!-- ──────────────── TRUST STRIP ──────────────── -->
<section class="trust-strip">
<div class="container">
<div class="trust-row">
<span class="trust-label">Fuses signal from</span>
<ul class="trust-items">
<li>Bluetooth-LE + WiFi radio</li>
<li>DeFlock ALPR map</li>
<li>Citizen incidents</li>
<li>Waze police alerts</li>
<li>OpenStreetMap · Overpass</li>
</ul>
</div>
</div>
</section>
<!-- ──────────────── HOW IT WORKS ──────────────── -->
<section id="detect" class="section section-feature reveal">
<div class="container">
<div class="section-head">
<span class="section-tag">how it works</span>
<h2>One tap. A live threat map.</h2>
<p class="lead">
While scanning, the circle becomes an <strong>OpenStreetMap centered on
you</strong>, wrapped in a threat-color ring for the current tier and
marked with a ⌖ crosshair. Every detection is a dot, color-coded by
source — so each mark is self-explanatory. Lock the screen and the
foreground notification keeps the tier live; the phone vibrates only
when the threat <em>escalates</em>.
</p>
</div>
<div class="feature-split">
<div class="phone-frame">
<img class="phone" src="img/overwatch-sources.png" width="856" height="2000"
alt="Detection-sources drill-down: BLE, WiFi, DeFlock, Citizen, Waze and Commercial rows, each with a color tier dot and the observations that fired it." loading="lazy">
</div>
<div class="explain-annotations">
<div class="annotation">
<span class="anno-num">1</span>
<div>
<strong>Threat-color ring</strong>
<p>The map is wrapped in the current tier's color — green, yellow,
orange, or red — so you read your situation at a glance without
opening anything.</p>
</div>
</div>
<div class="annotation">
<span class="anno-num">2</span>
<div>
<strong>Source-color dots</strong>
<p><span class="legend-dot red"></span> Flock / DeFlock cameras red ·
<span class="legend-dot blue"></span> Waze police blue ·
<span class="legend-dot purple"></span> Citizen incidents purple.
Tap any row for coordinates + a Maps deep-link.</p>
</div>
</div>
<div class="annotation">
<span class="anno-num">3</span>
<div>
<strong>Foreground notification</strong>
<p>Rebuilt on every tier change — <code>OVERWATCH • RED</code> with
the top detection's score. Priority bumps to HIGH on RED so the
system surfaces it as a heads-up, screen locked or not.</p>
</div>
</div>
<div class="annotation">
<span class="anno-num">4</span>
<div>
<strong>Escalation-only haptics</strong>
<p>A short pulse for YELLOW, double for ORANGE, escalating triple
for RED — but only on <em>upward</em> transitions, so it never
nags you while a threat lingers.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ──────────────── DETECTION SOURCES ──────────────── -->
<section id="sources" class="section section-bento reveal">
<div class="container">
<div class="section-head">
<span class="section-tag">detection sources</span>
<h2>Six sensors, one score.</h2>
<p class="lead">Every observation is scored 0100 by the confidence
engine; the on-screen tier is the <strong>max live score across all
sources</strong>. Cross-source corroboration — a BLE hit <em>and</em> a
DeFlock map match in the same spot — naturally pushes the tier higher
than either alone.</p>
</div>
<div class="bento">
<article class="bento-card bento-lg">
<div class="bento-icon">📶</div>
<h3>BLE — Bluetooth-LE</h3>
<p>
Vendor MAC OUIs (Axon, Flock Penguin / Raven, XUNTONG mfg id
<code>0x09C8</code>, "TN" serial pattern), Raven service UUIDs, and
device-name patterns. Iterates <em>every</em> manufacturer-data entry
per advert — not just the first — so it doesn't miss a buried match.
</p>
</article>
<article class="bento-card">
<div class="bento-icon">📡</div>
<h3>WiFi</h3>
<p>
BSSID OUI prefixes for Flock infrastructure (31-prefix superset) and
<code>Flock-XXXX</code> SSID patterns, polled every 35 s — just under
the Android 11+ scan throttle.
</p>
</article>
<article class="bento-card bento-cam">
<div class="bento-icon">🎥</div>
<h3>DeFlock — ALPR map</h3>
<p>
Crowdsourced ALPR camera locations within range (200 m default) via
the Overpass API (<code>overpass.deflock.org</code> → fallback
<code>overpass-api.de</code>), with a 24 h on-disk cache and failure
backoff. Refetches when you move &gt; 1.5 km.
</p>
</article>
<article class="bento-card">
<div class="bento-icon">🚨</div>
<h3>Citizen</h3>
<p>
Real-time public-safety incidents — police-relevant only,
fire/medical filtered out — within range and under 30 min old,
polled every 60 s from the Citizen trending feed.
</p>
</article>
<article class="bento-card bento-lg">
<div class="bento-icon">🚓</div>
<h3>Waze — police alerts</h3>
<p>
Live user-reported <code>POLICE</code> alerts (default 500 m,
~45 min freshness). Waze reCAPTCHA-gated its map endpoint, so
OVERWATCH reads a key-protected feed through a Caddy proxy at
<code>api.blackflagintel.com</code> that injects the key
server-side — the paid key <strong>never ships in the APK</strong>.
The app authenticates with a scoped, revocable <code>X-App-Token</code>
stored encrypted in the Android Keystore.
</p>
</article>
<article class="bento-card">
<div class="bento-icon">🏠</div>
<h3>Commercial</h3>
<p>
Nearby consumer smart-home gear (Nest, Ring, Echo, hidden cams) and
<strong>smart glasses (Meta, Snap, Vuzix)</strong> as a secondary
situational signal, riding the BLE + WiFi scans. Score-capped at
ORANGE so a cluster of doorbells never reads as ALPR-grade certainty.
</p>
</article>
</div>
</div>
</section>
<!-- ──────────────── THREAT TIERS ──────────────── -->
<section id="tiers" class="section reveal">
<div class="container">
<div class="section-head">
<span class="section-tag">confidence</span>
<h2>Four tiers. Highest live score wins.</h2>
</div>
<div class="tiers">
<div class="tier tier-green">
<div class="tier-range">&lt; 40</div>
<div class="tier-name">GREEN</div>
<p>Nothing credible. Idle shows a muted gray ring so "all clear" reads differently from "not scanning."</p>
</div>
<div class="tier tier-yellow">
<div class="tier-range">40 69</div>
<div class="tier-name">YELLOW</div>
<p>A single weak indicator. Short vibration pulse on the way up.</p>
</div>
<div class="tier tier-orange">
<div class="tier-range">70 84</div>
<div class="tier-name">ORANGE</div>
<p>High confidence — usually a hard radio hit or a close map match. Double pulse.</p>
</div>
<div class="tier tier-red">
<div class="tier-range">85 +</div>
<div class="tier-name">RED</div>
<p>Certain. Notification jumps to HIGH priority; escalating triple pulse.</p>
</div>
</div>
</div>
</section>
<!-- ──────────────── SCREENSHOTS ──────────────── -->
<section id="screens" class="section reveal">
<div class="container">
<div class="section-head">
<span class="section-tag">screenshots</span>
<h2>The whole app, top to bottom.</h2>
<p class="lead">No account, no onboarding, no cloud dashboard to
configure — open it, hit START, and drill into any source. Three taps
deep is as complex as it gets.</p>
</div>
<div class="shots">
<figure class="shot">
<img class="phone" src="img/overwatch-main.png" width="1096" height="2560"
alt="OVERWATCH main screen: a live map inside a green all-clear threat ring, centered on you with a crosshair, and a STOP button." loading="lazy">
<figcaption><strong>The threat ring</strong>A live map centered on you, wrapped in the current tier's color. Green reads &ldquo;all clear&rdquo; at a glance.</figcaption>
</figure>
<figure class="shot">
<img class="phone" src="img/overwatch-sources.png" width="856" height="2000"
alt="Detection-sources drill-down: BLE, WiFi, DeFlock, Citizen, Waze and Commercial rows, each with a color tier dot and the observations that fired it." loading="lazy">
<figcaption><strong>Source drill-down</strong>Tap the ring for every source, its live tier, and the exact observation that fired it.</figcaption>
</figure>
<figure class="shot">
<img class="phone" src="img/overwatch-settings.png" width="856" height="2000"
alt="Settings screen: per-source toggles, proximity distance sliders, and the encrypted Waze proxy token field." loading="lazy">
<figcaption><strong>Settings</strong>Toggle each source, tune proximity distances, and paste an encrypted Waze proxy token.</figcaption>
</figure>
</div>
</div>
</section>
<!-- ──────────────── CAPABILITIES ──────────────── -->
<section class="section section-bento reveal">
<div class="container">
<div class="section-head">
<span class="section-tag">built for the street</span>
<h2>Runs in your pocket, not the cloud.</h2>
</div>
<div class="bento">
<article class="bento-card bento-lg">
<div class="bento-icon">🔇</div>
<h3>Passive by design</h3>
<p>
OVERWATCH only listens — it never transmits, probes, jams, or
interferes with any device or network. The Axon advertise/fuzz code
from one of the reference projects is <strong>intentionally
excluded</strong>. It reads the airwaves and public feeds; that's it.
</p>
</article>
<article class="bento-card">
<div class="bento-icon">🫧</div>
<h3>Floating overlay</h3>
<p>
The same threat-ring map renders in a small draggable bubble over
other apps (Settings → Display over other apps), so it stays with
you while you use your phone.
</p>
</article>
<article class="bento-card">
<div class="bento-icon">🔐</div>
<h3>Keys stay off-device</h3>
<p>
The paid Waze feed key lives server-side behind the proxy; the app
holds only a scoped, revocable token, encrypted via the Android
Keystore. No secrets in the APK.
</p>
</article>
<article class="bento-card">
<div class="bento-icon">🩺</div>
<h3>Honest per-source health</h3>
<p>
The drill-down marks a source <code>Source unreachable</code> when
its scanner couldn't reach its feed — a silent empty result and a
real failure look different.
</p>
</article>
<article class="bento-card">
<div class="bento-icon">🗺</div>
<h3>Your position, not your data</h3>
<p>
Location drives the proximity checks and the map on-device. No
account, no telemetry, no analytics — the scans and feeds run and
the results stay on your phone.
</p>
</article>
</div>
</div>
</section>
<!-- ──────────────── CALLOUT ──────────────── -->
<section class="section section-callout reveal">
<div class="container">
<div class="callout">
<div class="callout-mark">🛡</div>
<div>
<h3>Situational awareness, not interference</h3>
<p>
OVERWATCH is a defensive, receive-only tool for knowing what's
watching <em>you</em> — Flock ALPRs, Axon body cameras, and police
presence — in the space you're already in. It does not touch, spoof,
or disrupt anything. Know your local laws; use it responsibly.
</p>
</div>
</div>
</div>
</section>
<!-- ──────────────── DOWNLOAD ──────────────── -->
<section id="download" class="section section-timeline reveal">
<div class="container">
<div class="section-head">
<span class="section-tag">get it</span>
<h2>Sideload in three steps.</h2>
</div>
<div class="feature-split">
<div class="phone-frame">
<img class="phone" src="img/overwatch-settings.png" width="856" height="2000"
alt="Settings screen: per-source toggles (BLE, WiFi, DeFlock, Citizen, Waze, Commercial), proximity distance sliders, and the encrypted Waze proxy token field." loading="lazy">
</div>
<div class="timeline dl-steps">
<div class="tl-col tl-shipped">
<div class="tl-tag">1 · download</div>
<ul>
<li>Grab the latest debug-signed APK from
<a href="https://github.com/KaraZajac/OVERWATCH/releases/latest">Releases</a>
(currently <strong>v0.5.3</strong>).</li>
</ul>
</div>
<div class="tl-col tl-active">
<div class="tl-tag">2 · install</div>
<ul>
<li>Sideload it — allow "install unknown apps" for your browser or files app, then open the APK.</li>
</ul>
</div>
<div class="tl-col tl-next">
<div class="tl-tag">3 · grant + start</div>
<ul>
<li>Grant location + nearby-devices + notifications, toggle the sources you want, hit <strong>START</strong>.</li>
<li>Optional: paste a Waze proxy token in Settings for police-report coverage.</li>
</ul>
</div>
</div>
</div>
<p class="tl-foot">
Build from source, file issues, or read the internals:
<a href="https://github.com/KaraZajac/OVERWATCH">github.com/KaraZajac/OVERWATCH</a>
</p>
</div>
</section>
<!-- ──────────────── FOOTER ──────────────── -->
<footer class="footer">
<div class="container footer-inner">
<div class="footer-col">
<div class="footer-brand">
<img class="nav-logo" src="logo.svg" width="26" height="26" alt="" aria-hidden="true">
OVERWATCH
</div>
<p class="footer-tag">
Passive Android surveillance-detection. Listens only — never
transmits.
</p>
</div>
<div class="footer-col">
<h4>Project</h4>
<ul>
<li><a href="https://github.com/KaraZajac/OVERWATCH">Source</a></li>
<li><a href="https://github.com/KaraZajac/OVERWATCH/releases">Releases</a></li>
<li><a href="https://github.com/KaraZajac/OVERWATCH/releases/latest">Latest APK</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Signal from</h4>
<ul>
<li><a href="https://deflock.me">DeFlock ALPR map</a></li>
<li><a href="https://citizen.com">Citizen</a></li>
<li><a href="https://www.waze.com">Waze</a></li>
<li><a href="https://www.openstreetmap.org">OpenStreetMap</a></li>
</ul>
</div>
<div class="footer-col">
<h4>More</h4>
<ul>
<li><a href="https://netslum.io/notes/overwatch-passive-counter-surveillance/">The writeup</a></li>
<li><a href="https://netslum.io">netslum.io</a></li>
</ul>
</div>
</div>
<div class="container footer-bottom">
<p>
OVERWATCH is a receive-only situational-awareness tool. It does not
transmit, probe, jam, or interfere with any device or network.
</p>
<p class="footer-meta">
v0.5.3 · <a href="https://github.com/KaraZajac/OVERWATCH">github.com/KaraZajac/OVERWATCH</a>
</p>
</div>
</footer>
<script src="app.js" defer></script>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108 108" width="108" height="108" role="img" aria-label="OVERWATCH">
<defs>
<radialGradient id="ow-bg" cx="50%" cy="40%" r="74%">
<stop offset="0" stop-color="#123a24"/>
<stop offset="1" stop-color="#0a0f12"/>
</radialGradient>
</defs>
<rect x="4" y="4" width="100" height="100" rx="26" fill="url(#ow-bg)" stroke="#1f3a2c"/>
<path d="M54,54 L54,20 A34,34 0 0,1 82.5,36 Z" fill="#1fbf6a" fill-opacity="0.30"/>
<circle cx="54" cy="54" r="33" fill="none" stroke="#10b981" stroke-width="2.4"/>
<circle cx="54" cy="54" r="22" fill="none" stroke="#10b981" stroke-width="2.2" stroke-opacity="0.82"/>
<circle cx="54" cy="54" r="11" fill="none" stroke="#10b981" stroke-width="2" stroke-opacity="0.68"/>
<path d="M54,21 V87 M21,54 H87" stroke="#10b981" stroke-width="1.3" stroke-opacity="0.5"/>
<circle cx="70" cy="40" r="4.6" fill="#ef4444"/>
</svg>

After

Width:  |  Height:  |  Size: 932 B

+35
View File
@@ -0,0 +1,35 @@
<!doctype html><html lang="en"><head><meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&family=Space+Grotesk:wght@700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 1200px; height: 630px; overflow: hidden; position: relative;
background: radial-gradient(130% 130% at 12% 8%, #101b2e 0%, #07070d 52%, #0b0712 100%);
color: #ecedf7; font-family: "Inter", sans-serif;
}
.wrap { padding: 74px 82px; height: 100%; display: flex; flex-direction: column; justify-content: center; }
.brand { display: flex; align-items: center; gap: 20px; margin-bottom: 26px; }
.mark { color: #10b981; font-size: 46px; filter: drop-shadow(0 0 18px rgba(16,185,129,.8)); }
.name { font-family: "Space Grotesk", sans-serif; font-weight: 700; font-size: 84px; letter-spacing: .01em; }
.tag { font-size: 31px; line-height: 1.38; color: #c5c5d3; font-weight: 500; max-width: 960px; }
.tag b { color: #ecedf7; font-weight: 700; }
.tiers { display: flex; gap: 22px; margin: 36px 0 26px; }
.tdot { display: flex; align-items: center; gap: 9px; font-family: "JetBrains Mono", monospace; font-size: 17px; color: #9a9aad; }
.tdot i { width: 15px; height: 15px; border-radius: 50%; }
.sources { font-family: "JetBrains Mono", monospace; font-size: 20px; color: #8a8a9d; }
.sources b { color: #34d399; }
.url { position: absolute; bottom: 48px; right: 82px; font-family: "JetBrains Mono", monospace; font-size: 21px; color: #5b5b75; }
</style></head><body>
<div class="wrap">
<div class="brand"><img src="logo.svg" style="width:80px;height:80px;border-radius:17px" alt=""><span class="name">OVERWATCH</span></div>
<div class="tag">Passive surveillance detection for Android. Flag nearby <b>Flock ALPRs</b>, <b>Axon body cams</b> &amp; <b>police</b><b>it only listens.</b></div>
<div class="tiers">
<span class="tdot"><i style="background:#22c55e;box-shadow:0 0 12px #22c55e"></i>GREEN</span>
<span class="tdot"><i style="background:#eab308;box-shadow:0 0 12px #eab308"></i>YELLOW</span>
<span class="tdot"><i style="background:#f97316;box-shadow:0 0 12px #f97316"></i>ORANGE</span>
<span class="tdot"><i style="background:#ef4444;box-shadow:0 0 12px #ef4444"></i>RED</span>
</div>
<div class="sources">BLE · WiFi · <b>DeFlock</b> · Citizen · Waze &nbsp;&nbsp; six sensors, fused</div>
</div>
<div class="url">overwatch.netslum.io</div>
</body></html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+1123
View File
File diff suppressed because it is too large Load Diff