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

View File

@@ -123,11 +123,27 @@ func (c Config) Marshal() ([]byte, error) {
// registers the will {"online": false} on the same topic, so the broker
// publishes "offline" the moment the connection drops without a clean
// goodbye. That is the whole of presence detection (FR-6).
//
// A device may also attach a tamper report (FR-17): the accessibility service
// was disabled, the overlay permission was revoked, or the app was force
// stopped. It arrives on the same status topic because that is the only topic
// a device's broker ACL lets it write. Unknown fields stay ignored, so the
// shape is additive.
type Status struct {
Online *bool `json:"online"`
App string `json:"app"`
Locked *bool `json:"locked"`
At string `json:"at"`
Online *bool `json:"online"`
App string `json:"app"`
Locked *bool `json:"locked"`
At string `json:"at"`
Tamper *Tamper `json:"tamper,omitempty"`
}
// Tamper is a device-reported bypass of the kids' client (AGENTS.md FR-17).
// Kind is a short machine token such as "accessibility_disabled",
// "overlay_revoked" or "force_stopped"; Detail is free text for the alert.
type Tamper struct {
Kind string `json:"kind"`
Detail string `json:"detail,omitempty"`
At string `json:"at,omitempty"`
}
// ParseStatus reads a status payload. A report with no "online" field means the
@@ -146,6 +162,9 @@ func ParseStatus(payload []byte) (Status, error) {
// IsOnline reports the presence the payload asserts.
func (s Status) IsOnline() bool { return s.Online == nil || *s.Online }
// HasTamper reports whether the payload carries a tamper report with a kind.
func (s Status) HasTamper() bool { return s.Tamper != nil && s.Tamper.Kind != "" }
// At parses the report timestamp, falling back to the current time.
func (s Status) Timestamp(now time.Time) time.Time {
if s.At == "" {