Phase 5.2: Android Usage Access telemetry + VpnService site filtering
Continue the Phase 5.1 kids' client (FR-7, FR-14, FR-18). - Usage Access telemetry: UsageCollector reads the OS per-app foreground total for the day, TelemetryStore keeps the highest value per (day, app) across restarts, and TelemetryReporter samples every 60 s and publishes the list form on restrict/<device>/telemetry. Queued rows republish when the broker link returns. - Device-local VpnService: SiteFilterVpnService routes only a fake resolver into its own tunnel, answers NXDOMAIN for a blocked host while locked, and forwards everything else upstream through a protected socket. The allowlist always wins (FR-4). No router, Pi-hole or DNS change. - MQTT link: caFile support plus the telemetry topic and payload. - Status screen: Usage Access, telemetry totals and site-filter state. - Unit tests for host matching, DNS parse/NXDOMAIN and IPv4/UDP round trip. No Device Owner, no factory reset, no managed profile. The VPN consent dialog is manual; adb cannot grant it (recorded in android/README.md). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Kids' Android client — Phase 5.1
|
||||
# Kids' Android client — Phase 5 (5.1 + 5.2)
|
||||
|
||||
The core no-wipe block path of the kids' phone app (AGENTS.md §11 Phase 5,
|
||||
FR-16 + FR-17). A normal Gradle project with its own wrapper. The toolchain is
|
||||
**not** on `.13`; it builds on `.27`:
|
||||
The no-wipe block path of the kids' phone app (AGENTS.md §11 Phase 5,
|
||||
FR-4, FR-7, FR-14, FR-16, FR-17, FR-18). A normal Gradle project with its own
|
||||
wrapper. The toolchain is **not** on `.13`; it builds on `.27`:
|
||||
|
||||
```bash
|
||||
ssh 192.168.20.27 '~/android/build.sh internet_use_control'
|
||||
@@ -16,27 +16,68 @@ That pulls this repo from Gitea and runs `./gradlew assembleDebug`. It prints
|
||||
| Piece | File | Job |
|
||||
|---|---|---|
|
||||
| Accessibility Service | `BlockAccessibilityService.kt` | Watches the foreground package (FR-16). |
|
||||
| Block overlay | `OverlayController.kt` | A full-screen `TYPE_APPLICATION_OVERLAY` window over a blocked app. |
|
||||
| Block overlay | `OverlayController.kt` | A full-screen `TYPE_ACCESSIBILITY_OVERLAY` window over a blocked app. |
|
||||
| Block rule | `BlockEngine.kt` | Blocked = locked **and** on the blocklist **and** not allowlisted (FR-4). |
|
||||
| Config transport | `MqttController.kt` | Subscribes, retained, to `restrict/<device>/config` on the 8883 TLS plane with the device's own credentials; publishes presence and tamper on `restrict/<device>/status`. |
|
||||
| Config transport | `MqttController.kt` | Subscribes, retained, to `restrict/<device>/config` on the 8883 TLS plane with the device's own credentials; publishes presence and tamper on `restrict/<device>/status`, and usage on `restrict/<device>/telemetry`. |
|
||||
| Last state | `AppPrefs.kt` | The last config is kept on disk, so a broker-less device keeps its rules (FR-14). |
|
||||
| Tamper alert | `TamperReporter.kt` | Reports a disabled service, a revoked overlay permission, or a force-stop (FR-17). |
|
||||
| Tamper alert | `TamperReporter.kt` | Reports a disabled service, a revoked overlay permission, a force-stop or a revoked VPN (FR-17). |
|
||||
| Status screen | `MainActivity.kt` | What is blocked, until when, and the health of the enforcement. |
|
||||
| Usage access | `UsageCollector.kt` | Reads the OS per-app foreground total for the day (FR-7). |
|
||||
| Local roll-up | `TelemetryStore.kt` | Keeps the highest daily total per app across restarts (FR-7, FR-14). |
|
||||
| Telemetry timer | `TelemetryReporter.kt` | Samples every 60 s and publishes the day's totals. |
|
||||
| Site filter | `SiteFilterVpnService.kt` | A device-local `VpnService` that answers NXDOMAIN for a blocked host (FR-18). |
|
||||
| Host matching | `SiteFilter.kt` | Exact/subdomain matching; the allowlist always wins (FR-4). |
|
||||
| DNS/IP wire format | `DnsMessage.kt`, `IpPacket.kt` | Minimal parse/build for the DNS forwarder. |
|
||||
|
||||
**No Device Owner. No factory reset. No managed profile.** The child can disable
|
||||
the client; the enforcement is the tamper alert, not the overlay.
|
||||
|
||||
## How the tamper alert reaches Sam
|
||||
## How the telemetry path works (FR-7)
|
||||
|
||||
The device's broker ACL only lets it write its own `status` topic, so the tamper
|
||||
report rides there:
|
||||
`PACKAGE_USAGE_STATS` is a privileged app-op, granted in **Settings → Special app
|
||||
access → Usage access**, not at install. Every 60 s the reporter reads
|
||||
`UsageStatsManager.queryUsageStats(INTERVAL_DAILY, startOfDay, now)`, which is a
|
||||
cumulative total for the day, merges it into the local roll-up (keeping the
|
||||
higher value), and publishes the list form the Control Service accepts:
|
||||
|
||||
```json
|
||||
{"online":true,"tamper":{"kind":"accessibility_disabled","detail":"the service was destroyed","at":"2026-10-13T08:00:00Z"}}
|
||||
{"day":"2026-10-13","apps":[{"name":"com.android.chrome","seconds":120}]}
|
||||
```
|
||||
|
||||
`internal/mqtt` parses it, `internal/alert` records a `tamper` alert and delivers
|
||||
it through Apprise, and `cmd/control` wires the two together.
|
||||
The message goes to `restrict/<device>/telemetry` over the 8883 TLS plane. It is
|
||||
not retained. If the broker is down the roll-up stays on disk and is republished
|
||||
as soon as the link returns (`MqttController.setOnConnected`).
|
||||
|
||||
## How the site filter works (FR-18)
|
||||
|
||||
`SiteFilterVpnService` is an ordinary `VpnService`. It adds a fake resolver
|
||||
address (`10.111.222.2`) and routes **only** that address into its own tunnel, so
|
||||
normal traffic never enters it and no router, Pi-hole or DNS server is touched.
|
||||
It reads UDP DNS on port 53, and:
|
||||
|
||||
- while the device is locked, a query whose name matches a blocklist entry (and
|
||||
not an allowlist entry) gets **NXDOMAIN**;
|
||||
- every other query is forwarded to `1.1.1.1` through a `protect()`ed socket.
|
||||
|
||||
The allowlist always wins (FR-4) and nothing is blocked while unlocked. The VPN
|
||||
is **not** the enforcement — the child can stop it — so a system revocation
|
||||
reports a `vpn_revoked` tamper (FR-17).
|
||||
|
||||
### Known limitation: VPN consent is manual
|
||||
|
||||
Android shows the VPN consent dialog the first time `VpnService.prepare()` is
|
||||
called. **`adb` cannot grant it**, so the emulator proof below cannot turn the
|
||||
site filter on. The filter is therefore **implemented and unit-tested, not
|
||||
runtime-verified**; it must be consented and checked on a real phone. Do not
|
||||
claim the VPN path is verified until that is done.
|
||||
|
||||
## Tests
|
||||
|
||||
The wire-format and matching logic is pure JVM code and runs on `.27`:
|
||||
|
||||
```bash
|
||||
ssh 192.168.20.27 'cd ~/android/internet_use_control/android && ./gradlew testDebugUnitTest'
|
||||
```
|
||||
|
||||
## Test on the headless emulator
|
||||
|
||||
@@ -47,6 +88,7 @@ ssh 192.168.20.27 '~/android/emu.sh install /tmp/webrestrict-pre/android/app/bui
|
||||
|
||||
PKG=au.audasmedia.webrestrict.kids
|
||||
adb shell appops set $PKG SYSTEM_ALERT_WINDOW allow
|
||||
adb shell appops set $PKG GET_USAGE_STATS allow
|
||||
adb shell settings put secure enabled_accessibility_services $PKG/$PKG.BlockAccessibilityService
|
||||
adb shell settings put secure accessibility_enabled 1
|
||||
|
||||
@@ -58,6 +100,19 @@ ssh 192.168.20.27 '~/android/emu.sh screenshot /tmp/shot.png'
|
||||
# Show the tamper alert: disable the service.
|
||||
adb shell settings put secure accessibility_enabled 0
|
||||
adb logcat -d -s WebRestrictTamper
|
||||
|
||||
ssh 192.168.20.27 '~/android/emu.sh stop'
|
||||
```
|
||||
|
||||
## Provisioning the broker on a real device
|
||||
|
||||
The lab CA and the per-device MQTT account are not in the repository. Push them
|
||||
instead of rebuilding the app:
|
||||
|
||||
```bash
|
||||
PKG=au.audasmedia.webrestrict.kids
|
||||
adb shell "run-as $PKG sh -c 'cat > files/lab-ca.pem'" < deploy/mosquitto/tls/ca.crt
|
||||
# then set the broker fields on the status screen, including:
|
||||
# CA file (lab) = /data/data/$PKG/files/lab-ca.pem
|
||||
```
|
||||
|
||||
The device topic is `restrict/<token>/config`, where `<token>` is the middle
|
||||
level and also the MQTT account name. `AppPrefs.caFile` wins over `caAsset`.
|
||||
|
||||
Reference in New Issue
Block a user