setApplicationHidden drops a package from the default PackageManager enumeration even though it stays installed=true, so a sweep querying with flags 0 could never see anything it had already hidden. That made packagesToShow permanently empty: measured on device, a pass following one that hid three apps reported "hid 0, restored 0" and left them hidden with no way back. Query with MATCH_UNINSTALLED_PACKAGES instead. Hiding the enforcer itself is the other unrecoverable case, since it takes the escape hatch and the trigger with it. The exporter now injects com.kuhy.focus_owner into both the day and night allowlists so the decision layer cannot propose it, and the runner keeps its point-of-use skip to cover a policy asset rendered before this fix or hand-edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
||
|---|---|---|
| .. | ||
| 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.