testsAndMisc/phone_focus_mode
Krzysztof kuhy Rudnicki 0f210e5532
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
monitor: backfill to 100% coverage, fixing two defects it exposed
The split shipped verbatim-proven and green, but measuring it afterwards
showed the tests barely touched it: monitor.sh 44.44%, the policy probes
7.69%, and the health probes never loaded at all. Every _check_* decides
whether phone enforcement is reported as healthy, so an untested probe is
an enforcement failure that reports itself as fine.

The mock stubs adb_root_shell at the function boundary rather than putting
a fake adb on PATH: the probes never invoke adb, they call a function from
adb_common.sh, so a PATH stub would never be consulted. Dispatch is by
pattern, not exact string, because the real commands embed awk programs
with escaped $2, nested quotes and 2>/dev/null redirections.

Two defects the new tests found:

  - _mon_escape_json never escaped double quotes. Written as
    ${escaped//"/\"}, bash reads the bare quote as opening a quoted string,
    so the pattern silently degraded to `/\` with an empty replacement. Any
    check message containing a quote produced invalid JSON.
  - monitor_print_format_warning's multi-line array literal and
    collect_snapshot's `{ ... } >file` group held 12 lines that no test
    could ever cover: kcov instruments the continuation lines and the
    closing brace, but bash only reports a statement at its first line.
    Reproduced in an isolated 9-line case before changing anything.

Assertions are mutation-proofed: 18 mutations of the probes' status
decisions, 18 killed. The first run had one survivor -- the repairable
flag was asserted nowhere -- now pinned in both directions.

  monitor.sh                104/104 = 100.00%  (was 44.44%)
  monitor_checks_policy.sh    65/65 = 100.00%  (was  7.69%)
  monitor_checks_health.sh    79/79 = 100.00%  (was     0%)
  test_monitor.sh          82 passed, 0 failed (was 9)

Cases live in two sourced files purely to stay under the 250-line cap;
test_monitor.sh remains the single entry point, so one coverage command
still measures all three subjects. Over-cap count unchanged at 22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 15:54:04 +02:00
..
docs Bring every markdown file under the cap 2026-08-16 19:25:59 +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 monitor: backfill to 100% coverage, fixing two defects it exposed 2026-08-17 15:54: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.sh Put focus_owner on the shared palette, and stop curfew hiding todo 2026-08-14 23:21:06 +02:00
curfew_enforcer.sh Fix the 11 shellcheck findings that kept CI red since b83ce5d 2026-08-04 20:55:02 +02:00
deploy.sh dns_enforcer: split 325 -> 171 + 170, and give it a working test 2026-08-17 15:10:10 +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 Fail the build on any shellcheck finding; fix the 222 the old gate hid 2026-08-03 19:49:11 +02:00
focus_daemon.sh Fail the build on any shellcheck finding; fix the 222 the old gate hid 2026-08-03 19:49:11 +02:00
hosts_enforcer.sh Fix the 11 shellcheck findings that kept CI red since b83ce5d 2026-08-04 20:55:02 +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 Back up what adb can actually reach, and block YouTube in Firefox too 2026-08-10 19:43:28 +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.