testsAndMisc/phone_focus_mode
Krzysztof kuhy Rudnicki d59932b262
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Allow Metrolist during the night curfew, deploy to phone
com.metrolist.music was on the day allowlist but missing from the night
list, so it was hidden between 23:00 and 05:00. Add it to
night_allowed_packages in focus_owner's policy asset (the list the
installed Device Owner app actually enforces) and mirror it into
phone_focus_mode's NIGHT_WHITELIST so the two sources do not drift.

policy.json ships as a bundled Flutter asset, so the edit is inert until
the APK is rebuilt: deployed via phone_deploy.sh (build 4,
data-preserving install -r, Device Owner intact).

Verified on the phone at 23:1x device time with the curfew live:
hidden=true before, hidden=false after, and Metrolist launches and holds
the foreground across several enforcer ticks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7Wzqp2ub6gW5uVBH5qmhn
2026-08-22 23:20:27 +02:00
..
docs config.sh: split 571 -> 250, with a fingerprint that actually works 2026-08-17 17:17:02 +02:00
focus_status_app Fail the build on any shellcheck finding; fix the 222 the old gate hid 2026-08-03 19:49:11 +02:00
lib focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
systemd feat(phone-focus): add recovery workflow, automation scripts, and docs 2026-05-01 19:07:27 +02:00
backup_manifest.sh feat(phone-focus): add recovery workflow, automation scripts, and docs 2026-05-01 19:07:27 +02:00
batch3_bloatware_uninstall.sh fix(phone): restore Blackview launcher for recents gesture 2026-05-31 16:27:08 +02:00
config_curfew.sh config.sh: split 571 -> 250, with a fingerprint that actually works 2026-08-17 17:17:02 +02:00
config_dns.sh config.sh: split 571 -> 250, with a fingerprint that actually works 2026-08-17 17:17:02 +02:00
config_launcher.sh config.sh: split 571 -> 250, with a fingerprint that actually works 2026-08-17 17:17:02 +02:00
config_paths.sh config.sh: split 571 -> 250, with a fingerprint that actually works 2026-08-17 17:17:02 +02:00
config_tether.sh config.sh: split 571 -> 250, with a fingerprint that actually works 2026-08-17 17:17:02 +02:00
config.sh Allow Metrolist during the night curfew, deploy to phone 2026-08-22 23:20:27 +02:00
ctl_curfew.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
ctl_daemon.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
ctl_dns.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
ctl_hosts.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
ctl_launcher.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
ctl_tether.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
ctl_usage.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
ctl_workout.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
curfew_enforcer.sh curfew_enforcer: split 367 -> 244 + 156, net layer at 100% 2026-08-17 16:18:05 +02:00
curfew_net.sh curfew_enforcer: split 367 -> 244 + 156, net layer at 100% 2026-08-17 16:18:05 +02:00
daemon_apps.sh focus_daemon: split 543 -> 206 + 165 + 140 + 94, with 110 tests 2026-08-17 16:54:04 +02:00
daemon_location.sh focus_daemon: split 543 -> 206 + 165 + 140 + 94, with 110 tests 2026-08-17 16:54:04 +02:00
daemon_state.sh focus_daemon: split 543 -> 206 + 165 + 140 + 94, with 110 tests 2026-08-17 16:54:04 +02:00
deploy_actions.sh deploy: split 875 -> 241 + 6, finishing phone_focus_mode 2026-08-17 17:53:52 +02:00
deploy_daemons.sh deploy: split 875 -> 241 + 6, finishing phone_focus_mode 2026-08-17 17:53:52 +02:00
deploy_gps.sh deploy: split 875 -> 241 + 6, finishing phone_focus_mode 2026-08-17 17:53:52 +02:00
deploy_install.sh deploy: split 875 -> 241 + 6, finishing phone_focus_mode 2026-08-17 17:53:52 +02:00
deploy_magisk.sh deploy: split 875 -> 241 + 6, finishing phone_focus_mode 2026-08-17 17:53:52 +02:00
deploy_phases.sh deploy: split 875 -> 241 + 6, finishing phone_focus_mode 2026-08-17 17:53:52 +02:00
deploy.sh deploy: split 875 -> 241 + 6, finishing phone_focus_mode 2026-08-17 17:53:52 +02:00
distraction_purge.sh Block YouTube on the unrooted Pixel, and trust more than one phone 2026-08-10 19:16:01 +02:00
dns_enforcer.sh dns_enforcer: split 325 -> 171 + 170, and give it a working test 2026-08-17 15:10:10 +02:00
dns_iptables.sh dns_enforcer: split 325 -> 171 + 170, and give it a working test 2026-08-17 15:10:10 +02:00
enable_persistent_adb.sh phone_focus_mode: reach the phone over adb without touching it 2026-08-03 13:33:45 +02:00
focus_ctl.sh focus_ctl: split 1091 -> 171 + 8 siblings, one per enforcer 2026-08-17 17:46:04 +02:00
focus_daemon.sh focus_daemon: split 543 -> 206 + 165 + 140 + 94, with 110 tests 2026-08-17 16:54:04 +02:00
hosts_enforcer.sh hosts_enforcer: split 421 -> 207 + 166 + 91, both libs at 100% 2026-08-17 16:30:56 +02:00
hosts_magisk.sh hosts_enforcer: split 421 -> 207 + 166 + 91, both libs at 100% 2026-08-17 16:30:56 +02:00
hosts_mount.sh hosts_enforcer: split 421 -> 207 + 166 + 91, both libs at 100% 2026-08-17 16:30:56 +02:00
launcher_enforcer.sh phone_focus_mode: add persistent home-mode status notification 2026-04-20 15:33:46 +02:00
magisk_service.sh phone_focus_mode: block hotspot/tethering to close the second-phone bypass 2026-07-13 10:48:59 +02:00
phone_backup.sh phone_backup: split 333 -> 229 + 114, with the library at 100% 2026-08-17 16:02:25 +02:00
README.md Bring every markdown file under the cap 2026-08-16 19:25:59 +02:00
restore_apps.sh Make the enforcer real: block YouTube, and refuse to be uninstalled 2026-08-10 21:12:53 +02:00
run_phone.sh Fix the 11 shellcheck findings that kept CI red since b83ce5d 2026-08-04 20:55:02 +02:00
strip_workout_hosts.py Make focus policy and blocklist usable without root; fix workout CDN unblock 2026-08-05 09:15:02 +02:00
tether_enforcer.sh tether_enforcer: split 267 -> 182 + 103, and grow both deploy lists 2026-08-17 14:38:53 +02:00
tether_iptables.sh tether_enforcer: split 267 -> 182 + 103, and grow both deploy lists 2026-08-17 14:38:53 +02:00
workout_detector.sh Apply focus-mode, screen-locker, and steam backlog updates 2026-05-03 22:41:53 +02:00

Phone Focus Mode

Location-based app restriction for a rooted Android phone using wireless ADB.

When within ~500m of home: only whitelisted productive apps remain usable. When outside that radius: all apps work normally.

Requirements

  • Rooted phone with Magisk installed
  • Wireless ADB enabled (Settings → Developer options → Wireless debugging)
  • adb installed on your PC (sudo apt install adb on Debian/Ubuntu)
  • GPS/Location enabled on the phone

Setup (first time)

1. Find your home coordinates

Open Google Maps, right-click your apartment → copy the coordinates shown.

2. Edit config_secrets.sh

HOME_LAT="-48.876667"   # your latitude
HOME_LON="-123.393333"  # your longitude

3. (Optional) Adjust the whitelist in config.sh

To find the exact package name of any app:

./deploy.sh <phone_ip> --find-pkg stronglift
./deploy.sh <phone_ip> --find-pkg anki
./deploy.sh <phone_ip> --find-pkg pomodoro

Then add the correct package name to WHITELIST in config.sh.

4. Deploy

chmod +x deploy.sh
./deploy.sh 192.168.1.42        # replace with your phone's IP

This:

  1. Pushes all scripts to /data/local/tmp/focus_mode/ on the device
  2. Installs a Magisk service.d script so the daemon auto-starts on boot
  3. Starts the daemon immediately

Usage

./deploy.sh <ip> --status    # Current mode, location, distance from home
./deploy.sh <ip> --log       # View recent daemon log
./deploy.sh <ip> --list      # List all apps + whitelist status
./deploy.sh <ip> --enable    # Force focus mode ON (for testing)
./deploy.sh <ip> --disable   # Force focus mode OFF
./deploy.sh <ip> --stop      # Stop daemon entirely (restores all apps)
./deploy.sh <ip> --start     # Start daemon
./deploy.sh <ip> --restart   # Restart daemon (picks up config changes)
./deploy.sh <ip> --pull-log  # Download log file to your PC

How it works

Every 60 seconds:
  get_location()  ─── dumpsys location ──► lat,lon
        │
        ▼
  calc_distance()  ─── Haversine formula ──► meters
        │
        ├── within radius?  ──► enable_focus_mode()
        │                           pm disable-user all non-whitelisted apps
        │                           record which apps were disabled
        │
        └── outside radius? ──► disable_focus_mode()
                                    pm enable each app in the disabled list

Hysteresis: 50m buffer prevents rapid toggling at the boundary. You must travel radius - 50m inward to trigger lock, and radius + 50m outward to unlock.

Fail-safe: If location is unavailable for 5 consecutive checks (~5 minutes), focus mode is automatically disabled so you can't be locked out.

State persistence: The daemon records exactly which apps it disabled (in /data/local/tmp/focus_mode/disabled_by_focus.txt), so it never accidentally re-enables apps that were already disabled by the user before focus mode ran.

On-device control (without PC)

From a root terminal app (e.g. Termux + tsu):

su --mount-master -c 'sh /data/local/tmp/focus_mode/focus_ctl.sh status'
su --mount-master -c 'sh /data/local/tmp/focus_mode/focus_ctl.sh disable'

Why --mount-master: MagiskSU puts each su -c session in an isolated mount namespace by default, so bind mounts made by the hosts enforcer would be invisible (and /data/adb/focus_mode/* checks would fail due to SELinux interactions). --mount-master joins the global namespace where the daemons (started from Magisk service.d at boot) actually live. The boot autostart script doesn't need this flag because post-fs-data already runs there.

File layout

File Purpose
config.sh Coordinates, radius, whitelist, constants
focus_daemon.sh Main daemon — runs on device, loops every 60s
focus_ctl.sh Control utility — runs on device
curfew_enforcer.sh Night-curfew enforcer — grayscale + DND + optional net
hosts_enforcer.sh Bind-mounts hosts.canonical over /system/etc/hosts
magisk_service.sh Magisk boot hook → auto-starts both daemons
deploy.sh PC-side ADB deployment and control script

Hosts hardening

A second daemon, hosts_enforcer.sh, locks the phone's /system/etc/hosts to the same blocklist installed by linux_configuration/hosts/install.sh on the PC. Three layers:

  1. Canonical copy at /data/adb/focus_mode/hosts.canonical is chattr +i.
  2. It is bind-mounted read-only over /system/etc/hosts at boot.
  3. A watchdog verifies a sha256 every 15 seconds and restores on mismatch.

This blocks the common echo > /etc/hosts one-liner from a terminal app. It is NOT a guarantee against a determined root user on the device itself — a real "impossible without USB" gate would require removing su access, which would break the rest of this system. The watchdog at least ensures tampering is logged and reverted within ~15s.

Status and logs:

./deploy.sh <ip> --hosts-status
./deploy.sh <ip> --hosts-log

Periodic rescan / Play Store

The focus daemon now re-scans every tick (not just on first entry). If you re-enable an app via Play Store or pm enable, it gets re-disabled within CHECK_INTERVAL_FOCUS seconds. com.android.vending (Play Store), com.*.packageinstaller, and popular terminal apps are also uninstalled --user 0 in focus mode to close the usual bypass paths. Google Play Services (com.google.android.gms) is left alone so banking apps work.

More detail

Split out to stay under the 250-line cap.