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>
2.8 KiB
2.8 KiB
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:
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:
{"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
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'