Both were measured, not theorised. `pm uninstall --user 0 com.celzero.bravedns` succeeded while the VPN was pinned and DISALLOW_CONFIG_VPN was set -- I ran it to check whether the provider was protected, and it removed the app, took the DNS filter with it, and left the always-on pin aimed at a package that no longer existed. The second hole was quieter: without lockdown, stopping the tunnel let traffic flow normally, so the filter only held while it was running. So the provider now gets setUninstallBlocked, re-asserted every pass, and the pin runs with lockdown. Lockdown is a real hazard and is deliberate: if the VPN app breaks, the device has no connectivity until ownership is released. That is only survivable because the release path lives in the enforcer app and needs no network. releaseDeviceOwner reads the pinned package before unpinning it, then unblocks its uninstall. Reversed, getAlwaysOnVpnPackage returns null after the unpin and the provider would stay unremovable forever -- the same trap the self-uninstall ordering already avoids. Home coordinates are provisioned. The pass now logs "applied AT_HOME" rather than LOCATION_UNKNOWN, so enforcement is finally geofenced rather than permanent-everywhere, driven by a real fix. `pm uninstall --user 0` on the provider now returns DELETE_FAILED_OWNER_BLOCKED. What this does NOT fix: RethinkDNS's blocklists are gone with the uninstall, since they live in app-private storage. The filter is running with no rules and youtube.com resolves normally until they are re-entered. And nothing here stops those rules being switched off from inside the app -- no device owner API can. 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.