mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 16:54:07 +01:00
db1d8f8247
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| db1d8f8247 |
Pin RethinkDNS as always-on VPN, the layer that reaches every app
Hiding the YouTube package stops the app. It does nothing about youtube.com in Firefox, in a webview, or in any client nobody has enumerated. This is that layer. setAlwaysOnVpnPackage from the device owner is not the mechanism that failed on 2026-08-09 -- that was `settings put secure always_on_vpn_app`, which writes successfully, survives a reboot, and is ignored by VpnManager. The DPM call takes effect: dumpsys now reports mAlwaysOnVpnPackage=com.celzero.bravedns. Lockdown stays off. It drops all traffic whenever the tunnel is down, and the wipe checklist names it together with the DISALLOW_* restrictions as the combination most likely to leave an unrecoverable device. DISALLOW_CONFIG_VPN is deferred for the same reason: locking VPN configuration before the filter is confirmed working would pin a broken setup in place. The exporter emits always_on_vpn_package only when the provider is also in never_disable_prefixes, and RethinkDNS was added there. Pinning a package the sweep can hide is the worst case -- the restriction would point at a hidden app and cost general connectivity, not just the filter. The pin is re-asserted every pass and, unlike the uninstall block, does not track the decision: the network filter must not lift when the geofence says AWAY. releaseDeviceOwner now unpins the VPN and clears the restriction before clearing ownership, for the same reason it already unblocked uninstall. Not yet active, and the commit does not pretend otherwise: the tunnel needs a one-time in-app VPN consent tap that adb cannot grant, and RethinkDNS is at first-run state because its wildcard rules live in app-private storage that the backup never reached. They have to be re-entered by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| b1de6f8d38 |
Take the geofence out of YouTube's reach, and pin infakt on
Leaving the house was an off switch. The AWAY branch returned early with packagesToShow = every installed package, so walking 150m from home restored YouTube -- which is the exact affordance device owner was provisioned to remove. always_blocked_packages is exempt from all four branches: AWAY hides them, the allowlist loop cannot show them, and the workout release skips them, because otherwise "go for a run" is the off switch instead. Chrome is included as a second route to the same content; Firefox stays, and carries the uBlock filters. Play is deliberately left geofenced so apps can still be installed away from home -- and a hidden package cannot be reinstalled from Play anyway. infakt moves from the allowlist to SYSTEM_NEVER_DISABLE. Allowlisting only held while it stayed in BOTH the day and night lists, and dropping it from either would silently make it hideable; isAllowed() consults the never-disable list before the curfew split, so it now holds under every condition. It is device-paired to a bank over SMS, so losing it strands the same re-authentication chain a hidden Messages app would. The exporter now refuses an always-blocked package that is not also blockable: the runner filters unopted system packages out of installedPackages, so such an entry would never be hidden and nothing would say so. Verified: 128 Python tests at 100% branch coverage, 16 Kotlin tests, the new ones canary-checked by breaking an assertion and confirming the suite actually fails. On device, youtube/music/chrome/vending are hidden=true, infakt hidden=false, uninstall still refused. The AWAY path itself is unit-tested but NOT verified on hardware: with no home_location.json every pass decides LOCATION_UNKNOWN, so the branch is unreachable on this phone until coordinates can be provisioned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| 6bf7343c0f |
Make the enforcer real: block YouTube, and refuse to be uninstalled
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 |
|||
| eeaf9590da |
Let the sweep hide preinstalled apps, one named package at a time
The Device Owner sweep skipped every FLAG_SYSTEM package, which meant it could not touch the apps it exists to block: YouTube ships at /product/app/YouTube as a system app, and so does Chrome. Deleting the filter is not the fix. Measured on the Pixel 6a, that would expose 320 system packages of which 243 match no allowlist entry and no never_disable_prefix -- including com.android.cellbroadcastreceiver (emergency alerts), com.android.credentialmanager and com.android.devicelockcontroller. So system apps stay default-deny and blockable_system_packages is the opt-in. com.android.vending is on the list deliberately: leaving Play reachable makes every other removal a one-tap undo. The list lives in the Python exporter rather than config.sh because it is a safety boundary, and a shell typo should not be able to widen it. The exporter subtracts anything already allowed, so an asset can never state that a package is both allowed and blockable. Kotlin reads the field with a new optionalStringSet: an asset predating the field parses to the empty set instead of throwing, because a throw makes decide() skip the pass and silently disable enforcement. Verified on-device under a profile owner: hid 1 for YouTube, restored 1 when temporarily allowed, and cellbroadcastreceiver, credentialmanager, devicelockcontroller and settings all left hidden=false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| f33fd03a16 |
focus_owner: keep the unhide path reachable, never hide the enforcer
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 |
|||
| a96605dc4b |
Wire the enforcement decision to the DPM hide call
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 |
|||
| cc804dd311 |
Run enforcement from exact alarms, and re-arm it before first unlock
The decision layer needed a runtime. A polling loop was the obvious design and the wrong one: Doze can skip a poll indefinitely, and skipping the 05:00 boundary leaves the curfew allowlist applied into the morning -- the user finds their banking app missing with no obvious cause. The transitions are known ahead of time, so they get exact alarms. The service wakes, applies, schedules the next run and stops rather than sitting resident. BootReceiver takes LOCKED_BOOT_COMPLETED as well as BOOT_COMPLETED, because the latter does not arrive until first unlock. That is not theoretical: this phone sat at RUNNING_LOCKED for minutes after an earlier reboot in this session, with no network and no app able to start. Relying on BOOT_COMPLETED alone freezes enforcement state across a reboot -- safe for hiding, not for unhiding, which is the direction that strands you without a dialer. The schedule is re-armed before unlock; the pass itself waits until storage is readable. The first on-device run logged "exact alarms not permitted", and dumpsys showed what that costs: window=+11m14s, maxWhenElapsed=+25m51s, so a 15-minute fallback could slip to 26 minutes. SCHEDULE_EXACT_ALARM is user-revocable and denied by default on Android 16; USE_EXACT_ALARM is the permission for apps whose function is acting at specific times, and is granted at install. After the change: window=0, exactAllowReason=policy_permission, whenElapsed equal to maxWhenElapsed. The service reports "not DO/PO - no enforcement applied" and does nothing when unprovisioned, rather than failing quietly, and a failure inside a pass cannot stop the next run being scheduled -- an enforcement bug should degrade one tick, not end the chain. Not yet wired: the service logs a tick rather than calling setApplicationHidden with the decision. Both halves are tested; joining them is next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| d0fcda3a47 |
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 |
|||
| 1b7958143d |
Prove DPM app-hiding survives a reboot; build the enforcement decision layer
The whole Device Owner design assumed setApplicationHidden persists across a reboot, and that had never been tested -- while both shell primitives were measured to be cleared by one. Settling it looked like it needed a factory reset. It does not: a managed profile is removable and takes its profile-owner grant with it, so the DPM API can be exercised and then erased. pm create-user --profileOf 0 --managed -> user 10 dpm set-profile-owner --user 10 ... ... hide, reboot, re-check ... pm remove-user 10 hidden=true before the reboot, hidden=true after, agreed by dumpsys and by the DPM API itself. Afterwards: Device Owner Type -1, no restrictions, and Chrome in user 0 untouched. Scope limit, stated because it is easy to overclaim: this proves the API's hidden state persists under PROFILE OWNER inside a work profile. It does not prove a DEVICE OWNER can hide a given app in user 0, which is what the design actually needs -- DO there additionally cannot touch the launcher, the default dialer/SMS/IME or packages the system deems critical, and the call can return false silently. That must be tested in the first minute after provisioning, while clearDeviceOwnerApp() is still a cheap exit. On top of that, EnforcementDecision turns policy plus conditions into a hide/show set. It is a pure function, so the whole decision layer is tested without a device, a clock or a location provider. packagesToShow is deliberately the complete allowed set rather than a delta. Failing to hide something is a mild disappointment; failing to UNHIDE strands the user without a dialer or a bank at 09:00 because the phone rebooted during curfew. Applying the full set on every evaluation makes a missed run self-repairing, which matters because BOOT_COMPLETED does not fire until first unlock -- observed directly, the phone sat at RUNNING_LOCKED for minutes after a reboot with no network at all. The workout exception is expressed as packages rather than domains. The rooted system unblocked youtube.com in a hosts file; hiding an app cannot target a domain, so the package set is derived from the configured domains and config.sh stays the source of truth for intent. Signal sources were checked on the device before being built: StrongLifts is granted health.WRITE_EXERCISE and Health Connect is installed, so that path is viable; com.kuhy.workout_app exposes only a MainActivity and Flutter boilerplate; org.runnerup is not installed here at all. Only the two that can actually produce a signal will be wired. HideTestReceiver is temporary and must be deleted before any provisioned build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| f636d2b21b |
Make focus policy and blocklist usable without root; fix workout CDN unblock
phone_focus_mode is built for a rooted phone: the hosts layer is a read-only
bind mount over /system/etc/hosts, chosen so that even root-in-a-terminal
cannot overwrite it. None of that ports to a stock locked device. This splits
out the parts that are device-independent so one policy can drive both.
python_pkg/focus_policy parses config.sh rather than copying it, so the shell
config stays the source of truth and the two cannot drift. It rejects the
shipped REDACTED_* coordinate placeholder instead of defaulting to (0,0),
which would move "home" to the Atlantic and quietly disable the location gate.
blocklist.py converts the hosts file to domains for a DoT resolver or a local
VpnService. Three properties of the real 194k-line file make the obvious parser
wrong, and all three are handled: generate_hosts_file.sh *comments out*
deliberate unblocks rather than deleting them, so stripping '#' re-blocks
facebook/messenger/linkedin/4chan; the StevenBlack header carries loopback and
IPv6 housekeeping that would blackhole localhost; and ~1060 upstream annotation
tokens ('#[adware.zeno]', '"spam') render as address=/"spam/, which makes
dnsmasq refuse to start -- taking DNS down rather than blocking anything.
Verified with dnsmasq --test over all 185681 generated domains.
strip_workout_hosts.py had the same class of bug on the live rooted phone:
exact set membership meant the allowlisted googlevideo.com never matched the
dynamic r*---sn-*.googlevideo.com hosts YouTube actually streams from, so the
workout unblock was only partly working. Now suffix-matched (17 lines stripped,
was 14). The three new tests were checked against the original code in a
throwaway worktree; two of them fail there, so they pin the fix rather than
passing vacuously.
focus_owner is a device-owner app that deliberately does nothing yet. It ships
clearDeviceOwnerApp() implemented and tested first, because dpm
remove-active-admin does not work on a device owner: an app that locks the
device down before proving it can let go leaves a soft-brick recoverable only
through fastboot on a locked bootloader. Built, installed and launched on the
Pixel 6a; dumpsys afterwards confirms Device Owner Type -1 and no user
restrictions, i.e. the phone is unchanged.
Measured while scoping this, and not yet reflected in any enforcement code:
on Android 16 both pm suspend and pm disable-user are cleared by a reboot, so
shell-level app blocking is per-session only. setApplicationHidden is assumed
to persist under Device Owner but is untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
|