The decision layer and setApplicationHidden were both built and tested but never connected -- the service logged a tick. Joining them meant porting the decision to Kotlin, because a background Service has no Flutter engine and starting one per alarm to answer a pure question would be slow and would add a failure mode at precisely the moment enforcement has to be reliable. That duplication is only safe if the two cannot drift, so the Kotlin suite restates the Dart cases one for one rather than testing something adjacent. It was checked for vacuity by asserting the hidden set contained a package that is not installed: 13 tests completed, 1 failed, and it was the right one. Show is applied before hide. If a pass is interrupted -- process killed, device sleeps -- the half that ran is the half that restores access rather than the half that removes it. The launcher, the protected prefixes and the app's own package are re-checked at the point of use as well as in the policy, because hiding the dialer would be unrecoverable from the device itself. An unreadable policy yields no decision rather than an empty allowlist, which would hide every app. The next run is scheduled in a finally, so an enforcement bug costs one tick instead of ending the chain. Note for the next session: JDK 26 is the system default and breaks the Android jlink transform; the Gradle tests need JAVA_HOME=/usr/lib/jvm/java-21-openjdk, which is what Flutter itself uses. NOT verified on device. Unit tests cover the decision, but no real hide/show pass has been observed -- a managed profile was created and the app made profile owner, and the UI confirmed the policy loaded, but the session ended before a pass was triggered. The profile was removed and user 0 verified clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
||
|---|---|---|
| .. | ||
| android | ||
| assets | ||
| 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.