testsAndMisc/focus_owner/assets/policy.json
Krzysztof kuhy Rudnicki 82e206116a
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Make the geofence tell the truth, and the phone explain itself
The enforcer failed in both directions: nothing blocked at home, and
everything blocked at the office. The comparison was never inverted --
`metres <= threshold` was always right. Three separate causes:

Only ACCESS_COARSE_LOCATION was declared. Android 12+ fuzzes coarse to a
~1-2 km grid against a 150 m fence, so a fix taken at home routinely landed
outside it, read as AWAY, and restored every app the geofence exists to hide.
FINE is now declared and self-granted by the device owner, along with coarse
and background, re-asserted every pass so a reinstall cannot silently drop it.

There were three states but two behaviours. LOCATION_UNKNOWN applies the
identical sweep to AT_HOME -- correct fail-closed design, indistinguishable
from outside. And nothing ever requested a fix: the pass read whatever another
app happened to cache, so at the office with no navigation running the cache
was empty. Passes now acquire a fix inside a location-typed foreground
service, bounded at 20 s, and treat anything older than 30 min as no fix. That
window is deliberately longer than the 15-minute cadence; a shorter one marks
the newest cache stale on every pass of an idle phone and blocks everything.

None of it was diagnosable. `adb logcat -d -s FocusOwner:V` came back empty
while dumpsys showed 82 alarms had fired, and `run-as` is refused on the
release build device owner requires. So the pass now writes a ring-buffered
JSONL record read back in-app: reason, distance vs the fence, fix
age/provider/accuracy, curfew state, and every hidden app with the rule that
hid it. Coordinates are never recorded -- a distance is enough to debug a
geofence, and this is a home address.

Also fixed along the way:

- Hysteresis keyed off `isEnforcing`, which is true under AWAY too because
  YouTube stays hidden, so the threshold was permanently radius+hysteresis.
  It now keys off whether the previous pass was inside the fence.
- The scheduler's curfew-boundary alarms were documented but never scheduled;
  both callers invoked `scheduleNext()` bare, so 05:00 could land 15 min late.
- Installing over the app cancelled its pending alarms, so `adb install -r`
  silently stopped enforcement until someone opened the app. MY_PACKAGE_REPLACED
  now re-arms it, verified on device without launching the app.
- A wrong-typed log record threw instead of being skipped, and the exception
  escaped far enough to leave the page stuck busy -- which disables the
  release-device-owner button, the one control that must work when everything
  else has gone wrong.
- `lib/enforcement.dart` was a second decision layer with no always-blocked
  concept, cited by KDoc as pinned by a `PolicyParityTest` that never existed.
  It would have made the new screen report YouTube visible while it is hidden.
  Deleted; the UI renders the Kotlin record rather than re-deciding.
- Anchoring home accepted a stale or unknown-precision fix. A wrong home
  offsets the fence everywhere and forever, which is what produced a 766 m
  reading while standing at home.

Policy: Play Store is allowed by day (not during curfew) so infakt can be
installed and updated; the sweep is default-deny for third-party packages, so
anything installed from Play is hidden on the next at-home pass. Tachiyomi's
per-source extension apks are covered by a new prefix allowlist rather than an
exact list that goes stale whenever an extension is installed.

`export.py` stopped subtracting the allowlist from `blockable_system_packages`.
Sweepable is not hideable: that list decides which system packages are
eligible for a decision, and `isAllowed` is what protects them. Removing an
allowed system package from the sweep freezes it in its last state -- it would
have stranded Play hidden permanently the moment it was allowlisted.

Verified on the Pixel 6a: FINE granted POLICY_FIXED, `applied AT_HOME` with a
0 m distance from a +/-11 m GPS fix, YouTube/Chrome/Firefox absent from
`pm list packages -e`, Play Store, infakt, RunnerUp and all five Tachiyomi
packages present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjxL9kdAzZmGqQA5tMNrZ8
2026-08-14 13:43:05 +02:00

153 lines
3.9 KiB
JSON

{
"allowed_packages": [
"com.android.vending",
"com.celzero.bravedns",
"com.facebook.orca",
"com.google.android.apps.maps",
"com.google.android.apps.nexuslauncher",
"com.google.android.calendar",
"com.google.android.deskclock",
"com.ichi2.anki",
"com.kuhy.diet_guard_app",
"com.kuhy.focus_owner",
"com.kuhy.home_inventory",
"com.kuhy.untools",
"com.kuhy.wake_alarm_sync",
"com.kuhy.workout_app",
"com.kunzisoft.keepass.libre",
"com.metrolist.music",
"com.revolut.revolut",
"com.sosauce.cutecalc",
"com.zaneschepke.wireguardautotunnel",
"dev.kuhy.todo",
"eu.kanade.tachiyomi.sy",
"org.fossify.contacts",
"org.fossify.messages",
"org.fossify.phone",
"org.runnerup",
"org.runnerup.free",
"pl.infakt.infakt",
"pl.mbank",
"pl.nask.mobywatel"
],
"allowed_prefixes": [
"eu.kanade.tachiyomi"
],
"always_blocked_packages": [
"com.android.chrome",
"com.google.android.apps.youtube.music",
"com.google.android.youtube"
],
"always_on_vpn_package": "com.celzero.bravedns",
"blockable_system_packages": [
"com.android.chrome",
"com.android.vending",
"com.google.android.apps.youtube.music",
"com.google.android.videos",
"com.google.android.youtube"
],
"browser_packages": [
"com.android.chrome",
"org.mozilla.fenix"
],
"curfew": {
"end": "05:00",
"start": "23:00"
},
"home": {
"hysteresis_m": 30.0,
"latitude": null,
"longitude": null,
"radius_m": 150.0
},
"launcher_package": "com.google.android.apps.nexuslauncher",
"never_disable_prefixes": [
"android",
"com.android.bluetooth",
"com.android.camera",
"com.android.camera2",
"com.android.captiveportallogin",
"com.android.certinstaller",
"com.android.contacts",
"com.android.dialer",
"com.android.documentsui",
"com.android.emergency",
"com.android.externalstorage",
"com.android.inputmethod",
"com.android.keychain",
"com.android.launcher",
"com.android.launcher3",
"com.android.location",
"com.android.messaging",
"com.android.mms",
"com.android.networkstack",
"com.android.nfc",
"com.android.permissioncontroller",
"com.android.phone",
"com.android.providers",
"com.android.se",
"com.android.settings",
"com.android.shell",
"com.android.systemui",
"com.android.vpndialogs",
"com.android.wifi",
"com.celzero.bravedns",
"com.google.android.apps.messaging",
"com.google.android.contacts",
"com.google.android.deskclock",
"com.google.android.dialer",
"com.google.android.ext.services",
"com.google.android.ext.shared",
"com.google.android.gms",
"com.google.android.gsf",
"com.google.android.inputmethod.latin",
"com.google.android.permissioncontroller",
"com.google.android.setupwizard",
"com.google.android.trichromelibrary",
"com.google.android.webview",
"com.mediatek",
"com.qualcomm",
"pl.infakt.infakt"
],
"night_allowed_packages": [
"com.celzero.bravedns",
"com.google.android.apps.nexuslauncher",
"com.google.android.deskclock",
"com.kuhy.focus_owner",
"com.kuhy.wake_alarm_sync",
"com.kunzisoft.keepass.libre",
"com.revolut.revolut",
"org.fossify.contacts",
"org.fossify.messages",
"org.fossify.phone",
"org.runnerup",
"org.runnerup.free",
"pl.infakt.infakt",
"pl.mbank",
"pl.nask.mobywatel"
],
"night_allowed_prefixes": [
"eu.kanade.tachiyomi"
],
"private_dns_host": "",
"schema_version": 1,
"vpn_lockdown": true,
"workout_unblock_domains": [
"googlevideo.com",
"i.ytimg.com",
"i9.ytimg.com",
"m.youtube.com",
"s.ytimg.com",
"www.youtube-nocookie.com",
"www.youtube.com",
"youtu.be",
"youtube-nocookie.com",
"youtube.com",
"youtube.googleapis.com",
"youtubei.googleapis.com",
"yt3.ggpht.com",
"yt3.googleusercontent.com",
"ytimg.com"
]
}