mirror of
https://git.churchofmalware.org/leviathan/OVERWATCH
synced 2026-09-24 08:35:03 +00:00
v0.5.11 — Android 15/16 + display-cutout compatibility, BLE screen-off fix
Two separate compatibility bugs, both reproduced before fixing. **Edge-to-edge.** Android 15 draws apps edge-to-edge whether or not they ask for anything targeting SDK 35, and the statusBarColor/navigationBarColor theme attributes this app relied on became no-ops at the same time. Reproduced on a fresh Android 16 emulator with a punch-hole cutout: the shipped v0.5.11-1 drew "OVERWATCH" inside the status bar next to the clock, buried the settings gear under the wifi and battery icons where it could not be tapped, and jammed the permission hint into the gesture bar. MainActivity now calls enableEdgeToEdge() explicitly — so the behaviour is the same on older releases rather than shifting under the user on an OS upgrade — and both screens pad themselves with WindowInsets.safeDrawing, which covers the system bars and the cutout. The background still runs edge to edge; only content is held clear. The overlay bubble states LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT rather than relying on it, since a draggable free-floating window could otherwise park under a camera hole. **BLE with the screen off.** Since Android 8.1 the Bluetooth stack stops delivering results for scans started with no ScanFilter once the screen turns off, and a foreground service does not exempt it — it is a stack rule, not a process-lifetime one. This app called startScan(null, ...) while promising to keep watching from a pocket, so BLE detection was silently dead in exactly the case that matters. A ScanFilter cannot express an OUI prefix, so the primary MAC-prefix method genuinely cannot run with the screen off; what can be named precisely still can. The scanner now switches on ACTION_SCREEN_ON/OFF: unfiltered while the screen is on, and a filtered scan (Raven service UUIDs, the XUNTONG manufacturer id, mic-target company ids) while it is off, capped at 16 filters because slots are a hardware resource and a silently empty scan is the worst failure this app has. The drill-down says so rather than hiding it. Verified on Android 16 (API 36) and Android 14: foreground service starts with types=0x18, all five scanners run, screen off logs "filtered scan (16 filters)" and screen on returns to unfiltered, zero scan failures, zero crashes. SOURCES.md gains a platform-constraints section covering these plus the BLE 5-starts-per-30s limit, WiFi scan throttling, and WifiManager.startScan()'s deprecation and planned removal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RsfSpnwuxCkcXKkD9XH8SR
This commit is contained in:
co-authored by
Claude Opus 5
parent
19622b87e1
commit
76faa840b1
@@ -34,7 +34,7 @@ Website: **[overwatch.netslum.io](https://overwatch.netslum.io)** · Latest re
|
||||
|
||||
| Source | What it looks at | Where it comes from |
|
||||
|---|---|---|
|
||||
| **BLE** | Bluetooth-LE advertisements: vendor MAC OUIs (Axon, Flock Penguin / Raven, XUNTONG mfg id `0x09C8`, "TN" serial pattern), Raven service UUIDs, device-name patterns — plus 18 IEEE-verified surveillance-vendor OUIs (ShotSpotter, WatchGuard/Motorola, Verkada, Avigilon Alta, Axis body cams, FLIR, Hanwha, March Networks, GeoVision, Mobotix, Sunell) with vendor-named labels | Local radio scan (BLE callback API). Iterates every manufacturer-specific data entry to find XUNTONG, not just the first. Police-exclusive OUIs (WatchGuard, ShotSpotter) score ORANGE on sight, same rationale as Axon. |
|
||||
| **BLE** | Bluetooth-LE advertisements: vendor MAC OUIs (Axon, Flock Penguin / Raven, XUNTONG mfg id `0x09C8`, "TN" serial pattern), Raven service UUIDs, device-name patterns — plus 18 IEEE-verified surveillance-vendor OUIs (ShotSpotter, WatchGuard/Motorola, Verkada, Avigilon Alta, Axis body cams, FLIR, Hanwha, March Networks, GeoVision, Mobotix, Sunell) with vendor-named labels | Local radio scan (BLE callback API). Iterates every manufacturer-specific data entry to find XUNTONG, not just the first. **Screen off:** Android suspends unfiltered scans, so the scanner switches to a filtered scan (Raven UUIDs, XUNTONG, mic company ids); OUI-prefix and name matching resume when the screen is on — see [SOURCES.md §5.1](SOURCES.md). Police-exclusive OUIs (WatchGuard, ShotSpotter) score ORANGE on sight, same rationale as Axon. |
|
||||
| **WiFi** | BSSID OUI prefixes for Flock infrastructure (31-prefix superset) + the same 18 vendor OUIs (WatchGuard 4RE in-car APs, Openpath/Alta readers, WiFi-capable cameras), `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 the detection radius (default 500 m), scored by how close each one actually is | 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. |
|
||||
| **WAZE** | User-reported `POLICE` alerts still active in the feed within the detection radius (default 500 m), up to ~45 min old, scored by distance and age | `api.openwebninja.com/waze/alerts-and-jams` — [OpenWeb Ninja](https://www.openwebninja.com)'s hosted Waze feed, called directly with **your own API key** (`X-API-Key`) entered in Settings and stored encrypted on-device. Sidesteps the reCAPTCHA gating that 403s direct `live-map/api/georss` calls. Polled every ~4 min with `alert_types=POLICE&max_jams=0` (server-side filtering, ~1.5 KB/poll), and the client re-filters by type so a silent upstream change can't let other alert types through. Alerts carry confidence (0–5) + reliability (0–10); high values nudge the score up. No key → source shows "not configured" in the drill-down. |
|
||||
@@ -255,7 +255,7 @@ without an uninstall.
|
||||
| `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, OpenWeb Ninja Waze feed, ADS-B aircraft feeds |
|
||||
| `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_CONNECTED_DEVICE`, `FOREGROUND_SERVICE_LOCATION` | Keep scanning with the screen off |
|
||||
| `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_CONNECTED_DEVICE`, `FOREGROUND_SERVICE_LOCATION` | Keep scanning with the screen off (note: a foreground service does *not* exempt BLE from the unfiltered-scan screen-off rule — see [SOURCES.md §5.1](SOURCES.md)) |
|
||||
| `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) |
|
||||
|
||||
+61
-2
@@ -405,7 +405,66 @@ ordinary traffic passing overhead never raises an alert.
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing notes
|
||||
## 5. Platform constraints that shape the radio sources
|
||||
|
||||
The two local-radio sources are limited by OS policy far more than by the
|
||||
hardware, and the limits are not obvious from the APIs.
|
||||
|
||||
### 5.1 Unfiltered BLE scans are suspended when the screen goes off
|
||||
|
||||
**Since Android 8.1, the Bluetooth stack stops delivering results for a scan
|
||||
started with no `ScanFilter` once the screen turns off, and a foreground
|
||||
service does not exempt it** — it is a stack rule, not a process-lifetime one.
|
||||
For an app whose entire promise is "keep watching while it's in your pocket",
|
||||
that is the worst possible silent failure: `startScan` returns success, the
|
||||
service stays alive, the notification keeps updating, and no BLE result ever
|
||||
arrives.
|
||||
|
||||
The catch is that **a `ScanFilter` cannot express an OUI prefix.** It can match
|
||||
an exact address, an exact name, a service UUID, or manufacturer data — and
|
||||
OUI-prefix matching is OVERWATCH's primary BLE method (Axon `00:25:df`, the 24
|
||||
Flock prefixes, the 18 vendor prefixes). There is no filter that means "any MAC
|
||||
starting with these three octets", and no filter that means "any device".
|
||||
|
||||
So the scanner switches strategy on `ACTION_SCREEN_ON` / `ACTION_SCREEN_OFF`:
|
||||
|
||||
| Screen | Scan | Covers | Loses |
|
||||
|---|---|---|---|
|
||||
| on | unfiltered | everything — OUI, name, UUID, manufacturer | — |
|
||||
| off | filtered (≤16) | Raven service UUIDs, XUNTONG manufacturer id, mic-target company ids | OUI prefixes, name substrings |
|
||||
|
||||
Filter slots are a hardware resource and chipsets differ; a common allocation is
|
||||
16. Overflow is meant to fall back to software filtering, but since a scan that
|
||||
silently returns nothing is this app's worst failure mode, the list is capped at
|
||||
16 with surveillance signatures ordered ahead of consumer ones. The BLE row in
|
||||
the drill-down states the reduced mode while the screen is off rather than
|
||||
hiding it, and Flock ALPR coverage is unaffected in that window because the map
|
||||
source does not depend on the radio.
|
||||
|
||||
### 5.2 Other limits worth knowing
|
||||
|
||||
- **BLE start-rate limit** — 5 `startScan` calls per 30 s per app. Exceed it and
|
||||
the scan *appears* to start but delivers nothing. Screen transitions are rare
|
||||
enough to stay clear of it.
|
||||
- **WiFi scan throttling** — since Android 9, foreground apps get 4 scans per
|
||||
2 minutes (background: 1 per 30 min). OVERWATCH polls every 35 s ≈ 3.4 per
|
||||
2 min, deliberately just under.
|
||||
- **`WifiManager.startScan()` is deprecated** and Google has stated the ability
|
||||
for apps to trigger scans will be removed in a future release. The scanner
|
||||
already treats it as best-effort: it registers for
|
||||
`SCAN_RESULTS_AVAILABLE_ACTION` and reads whatever the system last scanned, so
|
||||
when the trigger stops working the source degrades to system-paced results
|
||||
rather than failing.
|
||||
- **Promiscuous-mode tricks are not portable.** flock-you's `addr1` and
|
||||
wildcard-probe techniques need monitor mode; a userspace Android app sees only
|
||||
what `WifiManager` surfaces, which is BSSID and SSID.
|
||||
- **Android 14+ foreground-service types** are mandatory:
|
||||
`connectedDevice` for the radio scanners and `location` for the map/feed
|
||||
sources. Both are declared and both are passed at `startForeground` time.
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing notes
|
||||
|
||||
Emulator GPS is the main obstacle to testing the position-driven sources, and it
|
||||
has a trap worth recording:
|
||||
@@ -438,7 +497,7 @@ methods, which is easy to transpose.
|
||||
|
||||
---
|
||||
|
||||
## 6. Provenance
|
||||
## 7. Provenance
|
||||
|
||||
Reference projects studied while building (kept under a gitignored `REFERENCES/`):
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "org.soulstone.overwatch"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 26
|
||||
versionName = "0.5.10"
|
||||
versionCode = 27
|
||||
versionName = "0.5.11"
|
||||
}
|
||||
|
||||
// Fixed debug keystore committed to the repo (a debug key is non-secret — its
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.provider.Settings as AndroidSettings
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -79,6 +80,15 @@ class MainActivity : ComponentActivity() {
|
||||
permanentlyDenied.value = false // reset on activity create
|
||||
val settings = Settings.get(this)
|
||||
|
||||
// Android 15 (API 35) draws apps edge-to-edge whether or not they ask,
|
||||
// for anything targeting 35 — the old statusBarColor/navigationBarColor
|
||||
// theme attributes became no-ops at the same time. Opting in explicitly
|
||||
// makes the behaviour identical on older releases instead of the layout
|
||||
// shifting underneath the user on an upgrade; every screen then pads
|
||||
// itself with WindowInsets.safeDrawing, which covers the system bars
|
||||
// *and* a camera cutout.
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
val themeMode by settings.themeMode.collectAsState()
|
||||
OverwatchTheme(mode = themeMode) {
|
||||
|
||||
@@ -138,6 +138,16 @@ object MicTargets {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Every company id this object recognises. Exposed so BleScanner can turn
|
||||
* them into ScanFilters for screen-off scanning, where an unfiltered scan
|
||||
* is silently suspended by the Bluetooth stack.
|
||||
*/
|
||||
val COMPANY_IDS: Set<Int> = setOf(
|
||||
MFG_AMAZON, MFG_GOOGLE, MFG_YINGXIN,
|
||||
MFG_META, MFG_META_TECH, MFG_LUXOTTICA, MFG_SNAP, MFG_VUZIX
|
||||
)
|
||||
|
||||
fun matchManufacturer(companyId: Int?): Family? = when (companyId) {
|
||||
MFG_AMAZON -> Family.ECHO
|
||||
MFG_GOOGLE -> Family.GOOGLE
|
||||
|
||||
@@ -6,10 +6,15 @@ import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.le.BluetoothLeScanner
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.ParcelUuid
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -34,6 +39,27 @@ import org.soulstone.overwatch.fusion.SourceHealth
|
||||
* - For candidates, build a [ConfidenceEngine.BleObservation] and score it.
|
||||
* - Push to [DetectionStore] if score crosses ALARM_THRESHOLD (40).
|
||||
*
|
||||
* **Screen-off behaviour.** Since Android 8.1 the Bluetooth stack stops
|
||||
* delivering results for scans started with no [ScanFilter] once the screen
|
||||
* turns off, and a foreground service does not exempt it — this is a stack
|
||||
* rule, not a process-lifetime one. An unfiltered scan therefore goes silent
|
||||
* exactly when this app is most useful: in a pocket, screen locked.
|
||||
*
|
||||
* So the scanner swaps strategies on screen state:
|
||||
* - **screen on** — unfiltered, full coverage (OUI prefixes, name substrings,
|
||||
* service UUIDs, manufacturer data).
|
||||
* - **screen off** — filtered, which keeps delivering. A [ScanFilter] can only
|
||||
* express an exact address, an exact name, a service UUID or manufacturer
|
||||
* data; there is no way to express an **OUI prefix**, so MAC-prefix and
|
||||
* name-substring matching genuinely cannot run with the screen off. What
|
||||
* survives is what can be named precisely: Raven's service UUIDs and the
|
||||
* XUNTONG manufacturer id, plus the mic-target company ids when that source
|
||||
* is on.
|
||||
*
|
||||
* The gap is real and deliberate rather than hidden — [SourceHealth] says so on
|
||||
* the BLE row while the screen is off. Flock ALPR cameras stay covered in that
|
||||
* window by the map source, which is unaffected.
|
||||
*
|
||||
* Permissions: caller must hold BLUETOOTH_SCAN (API 31+) or BLUETOOTH+LOCATION (legacy).
|
||||
*/
|
||||
class BleScanner(
|
||||
@@ -47,6 +73,8 @@ class BleScanner(
|
||||
companion object {
|
||||
private const val TAG = "BleScanner"
|
||||
private const val ALARM_THRESHOLD = 40
|
||||
/** Conservative ceiling on hardware scan-filter slots. */
|
||||
private const val MAX_SCAN_FILTERS = 16
|
||||
}
|
||||
|
||||
private val bluetoothAdapter: BluetoothAdapter? by lazy {
|
||||
@@ -56,6 +84,83 @@ class BleScanner(
|
||||
|
||||
private var leScanner: BluetoothLeScanner? = null
|
||||
private var running = false
|
||||
private var screenReceiverRegistered = false
|
||||
/** True while the current scan is the filtered, screen-off variant. */
|
||||
@Volatile private var filteredMode = false
|
||||
|
||||
/**
|
||||
* Filters that keep results flowing with the screen off. Only signatures a
|
||||
* ScanFilter can actually express — see the class KDoc for what this
|
||||
* necessarily leaves out.
|
||||
*/
|
||||
private fun screenOffFilters(): List<ScanFilter> {
|
||||
val out = ArrayList<ScanFilter>()
|
||||
// Surveillance signatures first: if the list has to be trimmed below,
|
||||
// consumer gear is what should fall off the end, not a Raven detector.
|
||||
out.add(
|
||||
ScanFilter.Builder()
|
||||
.setManufacturerData(
|
||||
org.soulstone.overwatch.data.targets.Manufacturers.XUNTONG_COMPANY_ID,
|
||||
// Empty data + empty mask matches any payload for that id.
|
||||
ByteArray(0), ByteArray(0)
|
||||
).build()
|
||||
)
|
||||
for (uuid in RavenUuids.ALL) {
|
||||
out.add(ScanFilter.Builder().setServiceUuid(ParcelUuid(uuid)).build())
|
||||
}
|
||||
if (micEnabled()) {
|
||||
for (id in MicTargets.COMPANY_IDS) {
|
||||
out.add(
|
||||
ScanFilter.Builder()
|
||||
.setManufacturerData(id, ByteArray(0), ByteArray(0)).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
// Filter slots are a hardware resource and chipsets differ — a common
|
||||
// allocation is 16. Overflow is supposed to fall back to software
|
||||
// filtering, but a scan that silently returns nothing is the worst
|
||||
// failure this app can have, so cap rather than gamble.
|
||||
if (out.size > MAX_SCAN_FILTERS) {
|
||||
Log.w(TAG, "trimming ${out.size} filters to $MAX_SCAN_FILTERS")
|
||||
return out.subList(0, MAX_SCAN_FILTERS).toList()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private val screenReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(c: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SCREEN_OFF -> applyMode(filtered = true)
|
||||
Intent.ACTION_SCREEN_ON -> applyMode(filtered = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Restart the scan in the other mode. Screen transitions are rare, so this
|
||||
* stays well clear of the stack's 5-starts-per-30s ceiling. */
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun applyMode(filtered: Boolean) {
|
||||
if (!running || filteredMode == filtered) return
|
||||
val scanner = leScanner ?: return
|
||||
try {
|
||||
scanner.stopScan(scanCallback)
|
||||
val filters = if (filtered) screenOffFilters() else null
|
||||
scanner.startScan(filters, scanSettings, scanCallback)
|
||||
filteredMode = filtered
|
||||
if (filtered) {
|
||||
Log.i(TAG, "screen off — filtered scan (${filters?.size} filters)")
|
||||
SourceHealth.record(
|
||||
DetectionSource.BLE, ok = true,
|
||||
message = "Screen off — filtered scan; OUI/name matching resumes when the screen is on"
|
||||
)
|
||||
} else {
|
||||
Log.i(TAG, "screen on — unfiltered scan")
|
||||
SourceHealth.record(DetectionSource.BLE, ok = true)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "SecurityException switching scan mode", e)
|
||||
}
|
||||
}
|
||||
|
||||
private val scanSettings: ScanSettings = ScanSettings.Builder()
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
@@ -99,10 +204,28 @@ class BleScanner(
|
||||
return false
|
||||
}
|
||||
try {
|
||||
leScanner?.startScan(null, scanSettings, scanCallback)
|
||||
// Start in whichever mode matches the screen right now: the service
|
||||
// can be started from a notification action with the screen already
|
||||
// off, and an unfiltered scan there would deliver nothing at all.
|
||||
val screenOn = try {
|
||||
(context.getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager)
|
||||
?.isInteractive != false
|
||||
} catch (e: Exception) { true }
|
||||
filteredMode = !screenOn
|
||||
leScanner?.startScan(
|
||||
if (filteredMode) screenOffFilters() else null,
|
||||
scanSettings,
|
||||
scanCallback
|
||||
)
|
||||
running = true
|
||||
SourceHealth.record(DetectionSource.BLE, ok = true)
|
||||
Log.i(TAG, "BLE scan started")
|
||||
registerScreenReceiver()
|
||||
SourceHealth.record(
|
||||
DetectionSource.BLE, ok = true,
|
||||
message = if (filteredMode)
|
||||
"Screen off — filtered scan; OUI/name matching resumes when the screen is on"
|
||||
else null
|
||||
)
|
||||
Log.i(TAG, "BLE scan started (filtered=$filteredMode)")
|
||||
return true
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "SecurityException starting scan", e)
|
||||
@@ -120,9 +243,33 @@ class BleScanner(
|
||||
Log.e(TAG, "SecurityException stopping scan", e)
|
||||
}
|
||||
running = false
|
||||
filteredMode = false
|
||||
unregisterScreenReceiver()
|
||||
Log.i(TAG, "BLE scan stopped")
|
||||
}
|
||||
|
||||
private fun registerScreenReceiver() {
|
||||
if (screenReceiverRegistered) return
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
}
|
||||
// Screen on/off are protected system broadcasts, so this must be an
|
||||
// exported-style registration; ContextCompat's NOT_EXPORTED flag would
|
||||
// drop them on API 34+.
|
||||
androidx.core.content.ContextCompat.registerReceiver(
|
||||
context, screenReceiver, filter,
|
||||
androidx.core.content.ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
screenReceiverRegistered = true
|
||||
}
|
||||
|
||||
private fun unregisterScreenReceiver() {
|
||||
if (!screenReceiverRegistered) return
|
||||
try { context.unregisterReceiver(screenReceiver) } catch (_: IllegalArgumentException) { }
|
||||
screenReceiverRegistered = false
|
||||
}
|
||||
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.soulstone.overwatch.service
|
||||
|
||||
import android.os.Build
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -120,6 +121,15 @@ class OverlayManager(
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
// Keep the bubble out of a camera cutout. DEFAULT is already the
|
||||
// platform behaviour in portrait, but it is stated here because the
|
||||
// bubble is user-draggable and free-floating: on a punch-hole or
|
||||
// notch display the alternative (ALWAYS) would happily park the
|
||||
// threat circle underneath the camera.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|
||||
}
|
||||
gravity = Gravity.TOP or Gravity.START
|
||||
x = (INITIAL_X_DP * density).toInt()
|
||||
y = (INITIAL_Y_DP * density).toInt()
|
||||
|
||||
@@ -12,6 +12,9 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -93,6 +96,9 @@ fun MainScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
// Background first, then insets: the colour still runs under the
|
||||
// status bar and the cutout, only the content is held clear.
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
// Box (rather than Row + SpaceBetween) so the title is truly centered
|
||||
|
||||
@@ -8,6 +8,9 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -72,6 +75,9 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
// Inset before the scroll container so content scrolls inside the
|
||||
// safe area rather than under the bars or a cutout.
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
|
||||
+4
-4
@@ -53,7 +53,7 @@
|
||||
<div class="container hero-inner">
|
||||
<div class="hero-eyebrow">
|
||||
<span class="dot dot-pulse"></span>
|
||||
v0.5.10 · Android · passive
|
||||
v0.5.11 · Android · passive
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
<span class="display-wordmark">OVERWATCH</span>
|
||||
@@ -75,7 +75,7 @@
|
||||
<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="25">0</span><span>releases</span></div>
|
||||
<div class="stat-chip"><span class="num" data-target="26">0</span><span>releases</span></div>
|
||||
</div>
|
||||
|
||||
<div class="cta-row">
|
||||
@@ -432,7 +432,7 @@
|
||||
<ul>
|
||||
<li>Grab the latest debug-signed APK from
|
||||
<a href="https://github.com/KaraZajac/OVERWATCH/releases/latest">Releases</a>
|
||||
(currently <strong>v0.5.10</strong>).</li>
|
||||
(currently <strong>v0.5.11</strong>).</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tl-col tl-active">
|
||||
@@ -501,7 +501,7 @@
|
||||
transmit, probe, jam, or interfere with any device or network.
|
||||
</p>
|
||||
<p class="footer-meta">
|
||||
v0.5.10 · <a href="https://github.com/KaraZajac/OVERWATCH">github.com/KaraZajac/OVERWATCH</a>
|
||||
v0.5.11 · <a href="https://github.com/KaraZajac/OVERWATCH">github.com/KaraZajac/OVERWATCH</a>
|
||||
</p>
|
||||
<div class="onion-line">
|
||||
<span class="onion-label">Also on Tor</span>
|
||||
|
||||
Reference in New Issue
Block a user