Files
internet_use_control/android/README.md
Sam Rolfe e018c3bfa8 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>
2026-10-13 20:17:22 +11:00

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'