From 7671e8fef8f960e8a32b1bd400aa3b522fd368b2 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Mon, 13 Jul 2026 17:03:06 -0400 Subject: [PATCH] =?UTF-8?q?v0.5.1=20=E2=80=94=20UI:=20map=20ring,=20?= =?UTF-8?q?=E2=8C=96=20crosshair,=20source-color=20dots,=20bottom=20START?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_015jgJnzMr2bdNuWcDX2xRUR --- README.md | 62 +++++++++++++------ app/build.gradle.kts | 4 +- .../org/soulstone/overwatch/ui/MainScreen.kt | 47 ++++++++++++-- .../org/soulstone/overwatch/ui/MarkerIcons.kt | 43 ++++++++++++- .../soulstone/overwatch/ui/OverlayBubble.kt | 28 ++++++++- 5 files changed, 154 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 497c9e4..0bb9609 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ 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). +Latest release: [v0.5.1](https://github.com/KaraZajac/OVERWATCH/releases) (debug-signed APK, sideload). --- @@ -26,6 +26,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 (0–5) + reliability (0–10); high values nudge the score up. No token → source shows "not configured" in the drill-down. | +| **COMMERCIAL** | Nearby consumer smart-home / voice-assistant gear (Nest, Ring, Echo, hidden cams) as a secondary situational signal | Rides the BLE + WiFi scans — OUI / device-name / service-UUID / SSID matches from `MicTargets`. Score-capped at ORANGE so a cluster of doorbells 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, ≈ $1–3/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 +46,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 +76,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 +111,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 +130,10 @@ 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. + --- ## Permissions @@ -126,10 +145,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 +162,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 +174,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 +188,26 @@ 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.0–v0.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 1–5 (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.1–v0.2.2 — Live proximity refresh, map dot markers, map + Settings UI polish. +- v0.3.0–v0.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. ## License diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0c79afb..e2e83d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "org.soulstone.overwatch" minSdk = 26 targetSdk = 35 - versionCode = 16 - versionName = "0.5.0" + versionCode = 17 + versionName = "0.5.1" } buildTypes { diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt index a0a1bf2..e447ff7 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/MainScreen.kt @@ -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, + events: List, 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) } ) diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt index a333856..3bdbaf4 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/MarkerIcons.kt @@ -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 (⌖) diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt index 9c5c518..76740b5 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/OverlayBubble.kt @@ -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) } )