testsAndMisc/focus_owner
Krzysztof kuhy Rudnicki d0fcda3a47
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Provision home coordinates out of git; layer the workout signal; drop the test hook
The geofence needs HOME_LAT/HOME_LON, but those say where I live, so the
committed policy asset is generated with --redact-home and the real values are
pushed to app-private storage instead. push_home_location.sh greps
config_secrets.sh rather than sourcing it -- that file holds other secrets and
this script has no business evaluating them -- and refuses the shipped
REDACTED_* placeholder with a message naming the file to fill in, rather than
writing whatever the parse happened to land on.

Private storage was chosen for its lifetime as much as its permissions: it does
not survive an uninstall, so reinstalling forces re-provisioning instead of
silently inheriting a stale home. The default umask left the file 666, so the
script chmods it 600; the shell user gets Permission denied, and the
/data/local/tmp staging copy is removed after the transfer.

Every read failure returns null rather than throwing. Null reaches the
enforcement layer as location-unknown, which already fails closed into
enforcing -- so a corrupt coordinates file cannot become an unlock, and cannot
crash the service either.

Workout detection replaces a root-only SQLite read of StrongLifts' private
database. The manual source is time-boxed rather than a plain toggle, because a
workout mode left on by accident is indistinguishable from having no exception
at all. The guarded wrapper catches broadly and times out: nothing about a
workout probe failing should be able to stop enforcement running. The combined
source can name which signal opened the exception, so the UI can explain an
unlock rather than presenting it as magic. Only the two sources verified to
exist on this device will be wired -- com.kuhy.workout_app exposes nothing
readable and org.runnerup is not installed.

HideTestReceiver is deleted. It proved setApplicationHidden persistence, which
is what it was for, but it applied policy from an exported receiver and that has
no place in a build that will later hold device owner. Confirmed absent from the
rebuilt and reinstalled APK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
2026-08-09 21:42:20 +02:00
..
android Provision home coordinates out of git; layer the workout signal; drop the test hook 2026-08-09 21:42:20 +02:00
assets Serve the blocklist over DoT and hand the policy to the app as an asset 2026-08-05 09:36:11 +02:00
lib Provision home coordinates out of git; layer the workout signal; drop the test hook 2026-08-09 21:42:20 +02:00
scripts Provision home coordinates out of git; layer the workout signal; drop the test hook 2026-08-09 21:42:20 +02:00
test Provision home coordinates out of git; layer the workout signal; drop the test hook 2026-08-09 21:42:20 +02:00
.gitignore Make focus policy and blocklist usable without root; fix workout CDN unblock 2026-08-05 09:15:02 +02:00
.metadata Make focus policy and blocklist usable without root; fix workout CDN unblock 2026-08-05 09:15:02 +02:00
analysis_options.yaml Make focus policy and blocklist usable without root; fix workout CDN unblock 2026-08-05 09:15:02 +02:00
pubspec.lock Make focus policy and blocklist usable without root; fix workout CDN unblock 2026-08-05 09:15:02 +02:00
pubspec.yaml Exempt the generated policy asset from prettier 2026-08-05 09:40:44 +02:00
README.md Make focus policy and blocklist usable without root; fix workout CDN unblock 2026-08-05 09:15:02 +02:00

focus_owner

Device Owner companion app for unrooted focus mode. Currently inert: it can be provisioned as device owner, but it applies no restrictions.

Why the escape hatch ships first

dpm remove-active-admin does not work on a device owner. The only exit that avoids a factory reset is DevicePolicyManager.clearDeviceOwnerApp(), callable solely by this package on itself. An app that locks the device down before proving it can let go leaves a soft-brick recoverable only through fastboot — painful on a locked bootloader, and impossible to reach if DISALLOW_FACTORY_RESET is ever set.

So: DevicePolicyBridge.releaseDeviceOwner() is implemented, unit-tested, and reachable from the app's main screen with no PC attached. Enforcement is added only after that path has been exercised on a provisioned device.

Provisioning (NOT done yet — requires a factory reset)

dpm set-device-owner refuses to run while any account exists on the device. The Pixel 6a has 6, so provisioning means a wipe. See ~/.claude/projects/-home-kuhy-testsAndMisc/memory/device-owner-not-root-banking-works.md for what does and does not survive that, and for the Google Wallet open risk.

Once wiped, before adding any account:

adb install -r build/app/outputs/flutter-apk/app-debug.apk
adb shell dpm set-device-owner com.kuhy.focus_owner/.FocusDeviceAdminReceiver

Then immediately verify tap-to-pay and banking apps, while the release button is still reachable.

Build and verify

cd ~/testsAndMisc/focus_owner
flutter analyze && flutter test
flutter build apk --debug
adb -s 23181JEGR08034 install -r build/app/outputs/flutter-apk/app-debug.apk
adb -s 23181JEGR08034 shell am start -n com.kuhy.focus_owner/.MainActivity

The status screen reads live state over the platform channel; "Android SDK 36" confirms the Kotlin bridge is working.

Still to do

  • Consume the policy JSON from python_pkg/focus_policy (export.py).
  • Add enforcement (setApplicationHidden, addUserRestriction, setAlwaysOnVpnPackage) — only after the release path is verified on-device.
  • Verify setApplicationHidden survives a reboot. pm suspend and pm disable-user both do NOT; do not assume the DPM equivalent behaves the same until measured.