Phase 5.1: kids' Android client (Accessibility block overlay + tamper alert)

Add android/, a normal Gradle/Kotlin project with its own wrapper. It
blocks by interruption: an Accessibility Service watches the foreground
package and a full-screen overlay covers any listed app while the device
is locked (FR-16). No Device Owner, no factory reset, no managed profile.

The client subscribes to the retained restrict/<device>/config topic on
the 8883 TLS plane, keeps the last config on disk so a broker-less device
keeps its rules (FR-14), and reports tampering on its own status topic
(FR-17): a disabled service, a revoked overlay permission, or a
force-stop seen on the next start. The Control Service now parses the
tamper field and raises an Apprise alert, so it reaches Sam.

The block path and the tamper alert are proven on the headless pixel35
emulator on .27; see android/README.md and android/tools/emu-proof.sh.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-13 20:17:22 +11:00
parent 8c42daf4d9
commit e018c3bfa8
39 changed files with 2323 additions and 7 deletions

63
android/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Kids' Android client — Phase 5.1
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`:
```bash
ssh 192.168.20.27 '~/android/build.sh internet_use_control'
```
That pulls this repo from Gitea and runs `./gradlew assembleDebug`. It prints
`APK=<path>`; the debug APK is `android/app/build/outputs/apk/debug/app-debug.apk`.
## What it does
| 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 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`. |
| 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). |
| Status screen | `MainActivity.kt` | What is blocked, until when, and the health of the enforcement. |
**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
The device's broker ACL only lets it write its own `status` topic, so the tamper
report rides there:
```json
{"online":true,"tamper":{"kind":"accessibility_disabled","detail":"the service was destroyed","at":"2026-10-13T08:00:00Z"}}
```
`internal/mqtt` parses it, `internal/alert` records a `tamper` alert and delivers
it through Apprise, and `cmd/control` wires the two together.
## Test on the headless emulator
```bash
ssh 192.168.20.27 '~/android/emu.sh start'
ssh 192.168.20.27 '~/android/emu.sh wait'
ssh 192.168.20.27 '~/android/emu.sh install /tmp/webrestrict-pre/android/app/build/outputs/apk/debug/app-debug.apk'
PKG=au.audasmedia.webrestrict.kids
adb shell appops set $PKG SYSTEM_ALERT_WINDOW allow
adb shell settings put secure enabled_accessibility_services $PKG/$PKG.BlockAccessibilityService
adb shell settings put secure accessibility_enabled 1
# Open a blocked app (the local default blocks Settings) and screenshot.
adb shell am start -a android.settings.SETTINGS
sleep 3
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'
```