The device owner is provisioned, so enforcement stops being theoretical. Three things had to be true before it was safe to switch on. Release signing. A device owner can only be replaced by an APK with the same signature, and `dpm remove-active-admin` does not work on one, so a debug-signed device owner could never be updated -- the only exit would be another factory reset. focus_owner now uses the shared key.properties config the other apps use. No minify: an untested ProGuard pass that strips a DevicePolicyManager callback produces a device owner that cannot release itself. Uninstall blocking. Device owner hides Settings' uninstall button, but `adb uninstall` still worked, and that is the route that strands ownership with no holder. setSelfUninstallBlocked closes it, re-asserted every pass so it self-heals. The ordering in releaseDeviceOwner is not stylistic: unblock, THEN clear. Reversed, the block outlives the ownership required to lift it. The allowlist. config.sh describes the rooted phone, so the hide set was computed on the PC before any pass ran -- which caught pl.infakt.infakt in it. infakt is device-paired to a bank over SMS, so hiding it would have stranded the same re-authentication chain as hiding Messages. Verified on the Pixel 6a as a real device owner: hid 27, and youtube, youtube.music, chrome, vending and videos are all hidden=true, while phone, messages, keepass, mbank, revolut, infakt, mobywatel, settings and signal stay hidden=false. `adb uninstall` returns DELETE_FAILED_DEVICE_POLICY_MANAGER. All of it survives a reboot, which is exactly what `pm suspend` and `pm disable-user` failed to do here. restore_apps.sh reinstalls the 54 backed-up packages via install-multiple, since split APKs make a bare `adb install` fail. It skips the device owner's own package by name. Enforcement is permanent for now, not geofenced: push_home_location.sh delivers coordinates through `run-as`, which is dead on a release build, so every pass decides LOCATION_UNKNOWN and fails closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
||
|---|---|---|
| .. | ||
| android | ||
| assets | ||
| docs | ||
| lib | ||
| scripts | ||
| test | ||
| .gitignore | ||
| .metadata | ||
| analysis_options.yaml | ||
| pubspec.lock | ||
| pubspec.yaml | ||
| README.md | ||
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
setApplicationHiddensurvives a reboot.pm suspendandpm disable-userboth do NOT; do not assume the DPM equivalent behaves the same until measured.