diff --git a/README.md b/README.md index df5e202..5169c02 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ camera, or police presence near you. | **BLE** | Bluetooth-LE advertisements: vendor MAC OUIs (Axon, Flock Penguin / Raven, XUNTONG mfg id `0x09C8`, "TN" serial pattern), Raven service UUIDs, device-name patterns | Local radio scan (BLE callback API) | | **WiFi** | BSSID OUI prefixes for Flock infrastructure (31-prefix superset), `Flock-XXXX` and other generic SSID patterns | `WifiManager.getScanResults()` polled every 35 s (just under the Android 11+ 4-scans/2-min throttle) | | **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. | -| **WAZE** | Live `POLICE` reports within configurable proximity (default 500 m) and < 10 min old | `live-map/api/georss` polled every 60 s with a small bbox around the user. **Note:** Waze added reCAPTCHA gating to this endpoint in 2025/2026; mobile clients now receive HTTP 403. Source stays wired and surfaces the failure in the drill-down sheet so it's never silently empty. | -| **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. Pulled in to replace Waze's coverage gap. | +| **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. | + +> **Why no Waze?** Waze added reCAPTCHA gating to its `live-map/api/georss` endpoint in 2025/2026. Mobile clients receive HTTP 403, and the only known workarounds (Selenium proxy on a home server, Waze for Cities partner program) aren't viable for a phone-deployed app. Citizen replaces it. Every observation is scored 0-100 by `ConfidenceEngine`. The on-screen tier is the maximum live score across all sources: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d420542..4c7bb45 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 = 5 - versionName = "0.1.4" + versionCode = 6 + versionName = "0.1.5" } buildTypes { diff --git a/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt b/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt index 091d136..6083f43 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/data/settings/Settings.kt @@ -29,9 +29,6 @@ class Settings private constructor(private val prefs: SharedPreferences) { private val _deflockEnabled = MutableStateFlow(prefs.getBoolean(KEY_DEFLOCK, true)) val deflockEnabled: StateFlow = _deflockEnabled.asStateFlow() - private val _wazeEnabled = MutableStateFlow(prefs.getBoolean(KEY_WAZE, true)) - val wazeEnabled: StateFlow = _wazeEnabled.asStateFlow() - private val _citizenEnabled = MutableStateFlow(prefs.getBoolean(KEY_CITIZEN, true)) val citizenEnabled: StateFlow = _citizenEnabled.asStateFlow() @@ -40,10 +37,10 @@ class Settings private constructor(private val prefs: SharedPreferences) { ) val deflockProximityM: StateFlow = _deflockProximityM.asStateFlow() - private val _wazeProximityM = MutableStateFlow( - prefs.getInt(KEY_WAZE_PROX, DEFAULT_WAZE_PROX) + private val _citizenProximityM = MutableStateFlow( + prefs.getInt(KEY_CITIZEN_PROX, DEFAULT_CITIZEN_PROX) ) - val wazeProximityM: StateFlow = _wazeProximityM.asStateFlow() + val citizenProximityM: StateFlow = _citizenProximityM.asStateFlow() private val _themeMode = MutableStateFlow( ThemeMode.valueOf(prefs.getString(KEY_THEME, ThemeMode.DARK.name) ?: ThemeMode.DARK.name) @@ -53,7 +50,6 @@ class Settings private constructor(private val prefs: SharedPreferences) { fun setBleEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_BLE, v) }; _bleEnabled.value = v } fun setWifiEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_WIFI, v) }; _wifiEnabled.value = v } fun setDeflockEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_DEFLOCK, v) }; _deflockEnabled.value = v } - fun setWazeEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_WAZE, v) }; _wazeEnabled.value = v } fun setCitizenEnabled(v: Boolean) { prefs.edit { putBoolean(KEY_CITIZEN, v) }; _citizenEnabled.value = v } fun setDeflockProximityM(v: Int) { @@ -62,10 +58,10 @@ class Settings private constructor(private val prefs: SharedPreferences) { _deflockProximityM.value = clamped } - fun setWazeProximityM(v: Int) { + fun setCitizenProximityM(v: Int) { val clamped = v.coerceIn(100, 5000) - prefs.edit { putInt(KEY_WAZE_PROX, clamped) } - _wazeProximityM.value = clamped + prefs.edit { putInt(KEY_CITIZEN_PROX, clamped) } + _citizenProximityM.value = clamped } fun setThemeMode(mode: ThemeMode) { @@ -78,14 +74,13 @@ class Settings private constructor(private val prefs: SharedPreferences) { private const val KEY_BLE = "src_ble" private const val KEY_WIFI = "src_wifi" private const val KEY_DEFLOCK = "src_deflock" - private const val KEY_WAZE = "src_waze" private const val KEY_CITIZEN = "src_citizen" private const val KEY_DEFLOCK_PROX = "deflock_proximity_m" - private const val KEY_WAZE_PROX = "waze_proximity_m" + private const val KEY_CITIZEN_PROX = "citizen_proximity_m" private const val KEY_THEME = "theme_mode" const val DEFAULT_DEFLOCK_PROX = 200 - const val DEFAULT_WAZE_PROX = 500 + const val DEFAULT_CITIZEN_PROX = 500 @Volatile private var INSTANCE: Settings? = null diff --git a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt index 02d3767..7ca3e54 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ConfidenceEngine.kt @@ -24,12 +24,11 @@ object ConfidenceEngine { const val W_WIFI_SSID_GENERIC = 50 const val W_WIFI_SSID_FLOCK_FMT = 65 - // Map / Waze (Phase 3 + 4) + // Map (Phase 3) const val W_DEFLOCK_NEAR = 60 // <= 200m const val W_DEFLOCK_VERY_NEAR = 85 // <= 50m - const val W_WAZE_POLICE = 55 - // Citizen (added when Waze went dark) + // Citizen (replaces Waze; Waze's reCAPTCHA gating made it unreachable) const val W_CITIZEN_INCIDENT = 55 const val B_CITIZEN_LEVEL_BUMP = 5 // level >= 2 const val B_CITIZEN_POLICE_TITLE = 5 // title contains a police-action keyword @@ -66,16 +65,6 @@ object ConfidenceEngine { val manufacturer: String? ) - /** A Waze POLICE alert observed within proximity + freshness thresholds. */ - data class WazeObservation( - val uuid: String, - val distanceMeters: Float, - val ageMs: Long, - val confidence: Int, // raw 0-5 - val reliability: Int, // raw 0-10 - val subtype: String? - ) - /** A Citizen incident observed within proximity + freshness, after the * fire/medical filter is applied. */ data class CitizenObservation( @@ -186,22 +175,6 @@ object ConfidenceEngine { return Scored(score, methods.toString().trim(), label, isAxon) } - fun scoreWaze(obs: WazeObservation): Scored { - // Plan baseline: 55 for any POLICE alert ≤500m & <10min old. - // Caller is responsible for applying the proximity + age gate before scoring. - var score = W_WAZE_POLICE - // Lightweight crowd-trust nudge: high reliability & high confidence each add a few points, - // capped well under the multi-method bonus so a corroborating BLE/WiFi hit still dominates. - if (obs.reliability >= 7) score += 5 - if (obs.confidence >= 4) score += 5 - score = score.coerceAtMost(100) - val methods = "waze_police rel=${obs.reliability} conf=${obs.confidence}" - val ageMin = (obs.ageMs / 60_000L).toInt() - val sub = obs.subtype?.let { " ($it)" } ?: "" - val label = "Police report$sub @ ${obs.distanceMeters.toInt()}m, ${ageMin}min ago" - return Scored(score, methods, label, isAxon = false) - } - fun scoreCitizen(obs: CitizenObservation): Scored { var score = W_CITIZEN_INCIDENT val tags = StringBuilder("citizen ") diff --git a/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt b/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt index 2d9e036..0f5ce04 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/fusion/SourceHealth.kt @@ -26,20 +26,17 @@ object SourceHealth { private val _ble = MutableStateFlow(Health()) private val _wifi = MutableStateFlow(Health()) private val _deflock = MutableStateFlow(Health()) - private val _waze = MutableStateFlow(Health()) private val _citizen = MutableStateFlow(Health()) val ble: StateFlow = _ble.asStateFlow() val wifi: StateFlow = _wifi.asStateFlow() val deflock: StateFlow = _deflock.asStateFlow() - val waze: StateFlow = _waze.asStateFlow() val citizen: StateFlow = _citizen.asStateFlow() fun flowFor(source: DetectionSource): StateFlow = when (source) { DetectionSource.BLE -> ble DetectionSource.WIFI -> wifi DetectionSource.DEFLOCK -> deflock - DetectionSource.WAZE -> waze DetectionSource.CITIZEN -> citizen } @@ -48,7 +45,6 @@ object SourceHealth { DetectionSource.BLE -> _ble DetectionSource.WIFI -> _wifi DetectionSource.DEFLOCK -> _deflock - DetectionSource.WAZE -> _waze DetectionSource.CITIZEN -> _citizen } target.value = Health( @@ -62,7 +58,6 @@ object SourceHealth { _ble.value = Health() _wifi.value = Health() _deflock.value = Health() - _waze.value = Health() _citizen.value = Health() } } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt index 3a4f22a..0d37132 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/fusion/ThreatLevel.kt @@ -21,4 +21,4 @@ enum class ThreatLevel(val minScore: Int) { } /** Logical signal channel — used in the drill-down UI. */ -enum class DetectionSource { BLE, WIFI, DEFLOCK, WAZE, CITIZEN } +enum class DetectionSource { BLE, WIFI, DEFLOCK, CITIZEN } diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeClient.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeClient.kt deleted file mode 100644 index d983ff4..0000000 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeClient.kt +++ /dev/null @@ -1,131 +0,0 @@ -package org.soulstone.overwatch.scan - -import android.util.Log -import java.net.HttpURLConnection -import java.net.URL -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONObject - -/** - * Fetches Waze live-map alerts in a small bounding box around the user. - * - * Endpoint (recipe from REFERENCES/wazepolice): - * https://www.waze.com/live-map/api/georss?top=&bottom=&left=&right=&env=na&types=alerts - * - * Spoofs Chrome desktop headers — the public live-map endpoint requires Referer + - * a real-looking User-Agent, otherwise returns 403. - * - * Response shape: - * { "alerts": [ - * { "uuid", "type": "POLICE", "subtype", - * "location": {"x": lon, "y": lat}, - * "pubMillis", "reportedBy", "confidence" 0-5, "reliability" 0-10 } ] } - */ -class WazeClient { - - companion object { - private const val TAG = "WazeClient" - private const val BASE = "https://www.waze.com/live-map/api/georss" - private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - private const val REFERER = "https://www.waze.com/live-map/" - private const val ORIGIN = "https://www.waze.com" - private const val TIMEOUT_MS = 10_000 - - /** Bounding box half-width in degrees — ~5.5 km N-S, varies E-W with latitude. */ - private const val BBOX_HALF_DEG = 0.05 - } - - data class Alert( - val uuid: String, - val subtype: String?, - val lat: Double, - val lon: Double, - val pubMillis: Long, - val confidence: Int, - val reliability: Int, - val reportedBy: String? - ) - - /** Outcome — distinguishes "no police alerts in area" from "couldn't reach Waze." */ - sealed class FetchResult { - data class Success(val alerts: List) : FetchResult() - data class Failed(val reason: String) : FetchResult() - } - - suspend fun fetchPoliceNear(lat: Double, lon: Double): FetchResult = withContext(Dispatchers.IO) { - val top = lat + BBOX_HALF_DEG - val bottom = lat - BBOX_HALF_DEG - val left = lon - BBOX_HALF_DEG - val right = lon + BBOX_HALF_DEG - val url = URL("$BASE?top=$top&bottom=$bottom&left=$left&right=$right&env=na&types=alerts") - val conn = (url.openConnection() as HttpURLConnection).apply { - connectTimeout = TIMEOUT_MS - readTimeout = TIMEOUT_MS - requestMethod = "GET" - instanceFollowRedirects = true - setRequestProperty("User-Agent", USER_AGENT) - setRequestProperty("Referer", REFERER) - setRequestProperty("Origin", ORIGIN) - setRequestProperty("Accept", "application/json,text/javascript,*/*;q=0.8") - setRequestProperty("Accept-Language", "en-US,en;q=0.9") - } - try { - val code = conn.responseCode - if (code == 403) { - // Waze added reCAPTCHA gating to live-map in 2025/2026; mobile - // clients can no longer hit this endpoint without browser-level - // automation. Surface this distinctly so the UI can say so. - Log.w(TAG, "Waze returned 403 (upstream reCAPTCHA gating)") - return@withContext FetchResult.Failed("Upstream blocked (HTTP 403)") - } - if (code !in 200..299) { - Log.w(TAG, "Waze returned $code") - return@withContext FetchResult.Failed("HTTP $code") - } - val body = conn.inputStream.bufferedReader().use { it.readText() } - FetchResult.Success(parsePolice(body)) - } catch (e: Exception) { - Log.w(TAG, "Waze fetch failed: ${e.message}") - FetchResult.Failed(e.message ?: e.javaClass.simpleName) - } finally { - conn.disconnect() - } - } - - private fun parsePolice(body: String): List { - if (body.isBlank()) return emptyList() - return try { - val root = JSONObject(body) - val alerts = root.optJSONArray("alerts") ?: return emptyList() - val out = ArrayList() - for (i in 0 until alerts.length()) { - val a = alerts.optJSONObject(i) ?: continue - if (a.optString("type") != "POLICE") continue - val loc = a.optJSONObject("location") ?: continue - val uuid = a.optString("uuid") - if (uuid.isBlank()) continue - val lat = loc.optDouble("y") - val lon = loc.optDouble("x") - if (lat.isNaN() || lon.isNaN()) continue - out.add( - Alert( - uuid = uuid, - subtype = a.optString("subtype").ifBlank { null }, - lat = lat, - lon = lon, - pubMillis = a.optLong("pubMillis", System.currentTimeMillis()), - confidence = a.optInt("confidence", 0), - reliability = a.optInt("reliability", 0), - reportedBy = a.optString("reportedBy").ifBlank { null } - ) - ) - } - out - } catch (e: Exception) { - Log.w(TAG, "Failed to parse Waze response: ${e.message}") - emptyList() - } - } -} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeScanner.kt b/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeScanner.kt deleted file mode 100644 index fea9392..0000000 --- a/app/src/main/kotlin/org/soulstone/overwatch/scan/WazeScanner.kt +++ /dev/null @@ -1,111 +0,0 @@ -package org.soulstone.overwatch.scan - -import android.location.Location -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.soulstone.overwatch.data.location.LocationProvider -import org.soulstone.overwatch.fusion.ConfidenceEngine -import org.soulstone.overwatch.fusion.DetectionEvent -import org.soulstone.overwatch.fusion.DetectionSource -import org.soulstone.overwatch.fusion.DetectionStore -import org.soulstone.overwatch.fusion.SourceHealth - -/** - * Polls Waze every 60s for live POLICE alerts in a small bounding box around the - * current location, then submits any inside [PROXIMITY_M] and younger than [MAX_AGE_MS]. - * - * Skips the poll cycle if location is not yet known. Network-only — no on-disk cache - * (data is real-time by definition). - */ -class WazeScanner( - private val store: DetectionStore, - private val locationProvider: LocationProvider, - private val client: WazeClient = WazeClient(), - private val proximityMeters: () -> Float = { 500f } -) { - - companion object { - private const val TAG = "WazeScanner" - private const val POLL_INTERVAL_MS = 60_000L - private const val MAX_AGE_MS = 10L * 60L * 1000L - } - - private var job: Job? = null - - fun start(scope: CoroutineScope): Boolean { - if (job != null) return true - job = scope.launch { - while (isActive) { - val fix = locationProvider.location.value - if (fix != null) { - pollOnce(fix) - } else { - Log.d(TAG, "Skip poll — no location yet") - } - delay(POLL_INTERVAL_MS) - } - } - Log.i(TAG, "WazeScanner started (interval=${POLL_INTERVAL_MS}ms)") - return true - } - - fun stop() { - job?.cancel() - job = null - Log.i(TAG, "WazeScanner stopped") - } - - private suspend fun pollOnce(fix: Location) { - val result = client.fetchPoliceNear(fix.latitude, fix.longitude) - val alerts = when (result) { - is WazeClient.FetchResult.Success -> { - SourceHealth.record(DetectionSource.WAZE, ok = true) - result.alerts - } - is WazeClient.FetchResult.Failed -> { - SourceHealth.record( - DetectionSource.WAZE, - ok = false, - message = "Waze unreachable: ${result.reason}" - ) - return - } - } - if (alerts.isEmpty()) return - val now = System.currentTimeMillis() - val limit = proximityMeters() - val out = FloatArray(1) - - for (a in alerts) { - val age = now - a.pubMillis - if (age > MAX_AGE_MS) continue - Location.distanceBetween(fix.latitude, fix.longitude, a.lat, a.lon, out) - val dist = out[0] - if (dist > limit) continue - - val obs = ConfidenceEngine.WazeObservation( - uuid = a.uuid, - distanceMeters = dist, - ageMs = age, - confidence = a.confidence, - reliability = a.reliability, - subtype = a.subtype - ) - val scored = ConfidenceEngine.scoreWaze(obs) - store.submit( - DetectionEvent( - source = DetectionSource.WAZE, - key = a.uuid, - label = scored.label, - score = scored.score, - matchedMethods = scored.methods, - rssi = null - ) - ) - } - } -} diff --git a/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt b/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt index bedf8a8..4cde857 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/service/DetectionService.kt @@ -29,7 +29,6 @@ import org.soulstone.overwatch.scan.BleScanner import org.soulstone.overwatch.scan.CitizenScanner import org.soulstone.overwatch.scan.DeflockClient import org.soulstone.overwatch.scan.DeflockScanner -import org.soulstone.overwatch.scan.WazeScanner import org.soulstone.overwatch.scan.WifiScanner /** @@ -80,13 +79,11 @@ class DetectionService : LifecycleService() { private lateinit var wifiScanner: WifiScanner private lateinit var locationProvider: LocationProvider private lateinit var deflockScanner: DeflockScanner - private lateinit var wazeScanner: WazeScanner private lateinit var citizenScanner: CitizenScanner private var pruneJob: Job? = null private var bleStarted = false private var wifiStarted = false private var deflockStarted = false - private var wazeStarted = false private var citizenStarted = false override fun onCreate() { @@ -99,13 +96,9 @@ class DetectionService : LifecycleService() { store, locationProvider, DeflockClient(this), proximityMeters = { settings.deflockProximityM.value.toFloat() } ) - wazeScanner = WazeScanner( - store, locationProvider, - proximityMeters = { settings.wazeProximityM.value.toFloat() } - ) citizenScanner = CitizenScanner( store, locationProvider, - proximityMeters = { settings.wazeProximityM.value.toFloat() } + proximityMeters = { settings.citizenProximityM.value.toFloat() } ) createNotificationChannel() } @@ -135,7 +128,6 @@ class DetectionService : LifecycleService() { if (!wifiStarted) Log.w(TAG, "WifiScanner.start() returned false (permission/adapter)") } val needsLocation = settings.deflockEnabled.value || - settings.wazeEnabled.value || settings.citizenEnabled.value if (needsLocation) { val locOk = locationProvider.start() @@ -145,9 +137,6 @@ class DetectionService : LifecycleService() { if (settings.deflockEnabled.value) { deflockScanner.start(lifecycleScope); deflockStarted = true } - if (settings.wazeEnabled.value) { - wazeScanner.start(lifecycleScope); wazeStarted = true - } if (settings.citizenEnabled.value) { citizenScanner.start(lifecycleScope); citizenStarted = true } @@ -168,7 +157,6 @@ class DetectionService : LifecycleService() { if (bleStarted) { bleScanner.stop(); bleStarted = false } if (wifiStarted) { wifiScanner.stop(); wifiStarted = false } if (deflockStarted) { deflockScanner.stop(); deflockStarted = false } - if (wazeStarted) { wazeScanner.stop(); wazeStarted = false } if (citizenStarted) { citizenScanner.stop(); citizenStarted = false } locationProvider.stop() store.clear() diff --git a/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt b/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt index 4ecfa2b..4f9f827 100644 --- a/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt +++ b/app/src/main/kotlin/org/soulstone/overwatch/ui/SettingsScreen.kt @@ -45,10 +45,9 @@ fun SettingsScreen( val ble by settings.bleEnabled.collectAsState() val wifi by settings.wifiEnabled.collectAsState() val deflock by settings.deflockEnabled.collectAsState() - val waze by settings.wazeEnabled.collectAsState() val citizen by settings.citizenEnabled.collectAsState() val deflockProx by settings.deflockProximityM.collectAsState() - val wazeProx by settings.wazeProximityM.collectAsState() + val citizenProx by settings.citizenProximityM.collectAsState() val theme by settings.themeMode.collectAsState() Column( @@ -78,7 +77,6 @@ fun SettingsScreen( SourceToggle("BLE • Bluetooth Low Energy", ble) { settings.setBleEnabled(it) } SourceToggle("WIFI • WiFi BSSID + SSID", wifi) { settings.setWifiEnabled(it) } SourceToggle("DEFLOCK • ALPR map (Overpass)", deflock) { settings.setDeflockEnabled(it) } - SourceToggle("WAZE • Live police reports (gated)", waze) { settings.setWazeEnabled(it) } SourceToggle("CITIZEN • Real-time incident feed", citizen) { settings.setCitizenEnabled(it) } Spacer(Modifier.height(8.dp)) if (isRunning) { @@ -116,12 +114,12 @@ fun SettingsScreen( onChange = { settings.setDeflockProximityM(it.toInt()) } ) SliderRow( - label = "Waze alert distance", - valueLabel = "${wazeProx} m", - value = wazeProx.toFloat(), + label = "Citizen alert distance", + valueLabel = "${citizenProx} m", + value = citizenProx.toFloat(), range = 100f..5000f, steps = 48, - onChange = { settings.setWazeProximityM(it.toInt()) } + onChange = { settings.setCitizenProximityM(it.toInt()) } ) Spacer(Modifier.height(16.dp))