From a5209b68789e12bba7388a88e555507ffc26970b Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 23 Aug 2026 18:32:58 +0200 Subject: [PATCH] Extract phone_focus_mode together with focus_policy github.com/kuhyx/phone-focus-mode The two halves go as one repo: focus_policy parses phone_focus_mode's config.sh into the typed policy that focus-owner consumes, and nothing else references it. 77 commits of history, plus the two Python suites from linux_configuration/tests/ and the six mutation fixtures from meta/scripts/fixtures/, all of which named phone_focus_mode paths. Running the suites -- not reading them -- turned up a real defect the extraction would otherwise have inherited. config.sh sources the untracked config_secrets.sh, and ctl_libs_harness.sh never pinned HOME_LAT/HOME_LON, so the distance assertions were checking against whatever real coordinates that file happened to hold. The suite passed on this machine and could not run anywhere else, and a real home location was load-bearing in a pass/fail condition. The harness now pins a pair 4300 m south of the coordinate its own case seeds, and config.sh sources the secrets file only when it exists. 136/136 pass with no secrets file present. Before making the repo public I scanned the whole history for the known home coordinates and for any committed config_secrets.sh: zero hits, and the file was never tracked. The new repo also gained a .gitignore, since none came across in the filter-repo pass. test_magisk_service.sh hangs. It hangs identically in the pre-extraction baseline worktree and appears in no CI list here, so the new repo's CI skips it with that reason rather than pretending it passes. check_python_location.sh loses its phone_focus_mode exception now that the directory is gone; every tracked .py still passes it. The .gitignore rules for config_secrets.sh and the keystore STAY -- the working copy is still on this machine and still holds a real location. Depth 262 -> 248. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp --- .gitignore | 8 +- .pre-commit-config.yaml | 2 +- .prettierignore | 12 - AGENTS.md | 8 +- .../extract-phone-focus-mode-2026-08-23.json | 21 ++ .../extract-phone-focus-mode-2026-08-23.json | 54 ++++ linux_configuration/tests/conftest.py | 2 - .../tests/test_monitor_report.py | 156 ----------- .../tests/test_strip_workout_hosts.py | 139 ---------- meta/pyproject.toml | 2 +- meta/scripts/check_python_location.sh | 55 ++-- .../fixtures/mutations/backup_capture.json | 84 ------ meta/scripts/fixtures/mutations/ctl_libs.json | 138 ---------- .../fixtures/mutations/curfew_net.json | 108 -------- .../fixtures/mutations/daemon_libs.json | 204 -------------- .../fixtures/mutations/hosts_libs.json | 108 -------- meta/scripts/fixtures/mutations/monitor.json | 114 -------- meta/shell-coverage-allowlist.txt | 9 - phone_focus_mode/README.md | 159 ----------- phone_focus_mode/backup_manifest.sh | 85 ------ .../batch3_bloatware_uninstall.sh | 185 ------------- phone_focus_mode/config.sh | 250 ------------------ phone_focus_mode/config_curfew.sh | 50 ---- phone_focus_mode/config_dns.sh | 86 ------ phone_focus_mode/config_launcher.sh | 29 -- phone_focus_mode/config_paths.sh | 118 --------- phone_focus_mode/config_tether.sh | 44 --- phone_focus_mode/ctl_curfew.sh | 152 ----------- phone_focus_mode/ctl_daemon.sh | 198 -------------- phone_focus_mode/ctl_dns.sh | 100 ------- phone_focus_mode/ctl_hosts.sh | 118 --------- phone_focus_mode/ctl_launcher.sh | 154 ----------- phone_focus_mode/ctl_tether.sh | 104 -------- phone_focus_mode/ctl_usage.sh | 144 ---------- phone_focus_mode/ctl_workout.sh | 122 --------- phone_focus_mode/curfew_enforcer.sh | 244 ----------------- phone_focus_mode/curfew_net.sh | 156 ----------- phone_focus_mode/daemon_apps.sh | 165 ------------ phone_focus_mode/daemon_location.sh | 94 ------- phone_focus_mode/daemon_state.sh | 140 ---------- phone_focus_mode/deploy.sh | 241 ----------------- phone_focus_mode/deploy_actions.sh | 83 ------ phone_focus_mode/deploy_daemons.sh | 148 ----------- phone_focus_mode/deploy_gps.sh | 83 ------ phone_focus_mode/deploy_install.sh | 151 ----------- phone_focus_mode/deploy_magisk.sh | 135 ---------- phone_focus_mode/deploy_phases.sh | 163 ------------ phone_focus_mode/distraction_purge.sh | 162 ------------ phone_focus_mode/dns_enforcer.sh | 171 ------------ phone_focus_mode/dns_iptables.sh | 170 ------------ phone_focus_mode/docs/night-curfew.md | 150 ----------- phone_focus_mode/docs/policy-lists.md | 101 ------- .../docs/updating-and-troubleshooting.md | 36 --- .../docs/youtube-block-unrooted.md | 166 ------------ phone_focus_mode/enable_persistent_adb.sh | 193 -------------- phone_focus_mode/focus_ctl.sh | 171 ------------ phone_focus_mode/focus_daemon.sh | 206 --------------- .../focus_status_app/AndroidManifest.xml | 72 ----- phone_focus_mode/focus_status_app/build.sh | 111 -------- .../com/kuhy/focusstatus/BootReceiver.java | 27 -- .../kuhy/focusstatus/CurfewDemoReceiver.java | 41 --- .../focusstatus/CurfewToggleReceiver.java | 39 --- .../com/kuhy/focusstatus/LaunchActivity.java | 18 -- .../com/kuhy/focusstatus/RecheckReceiver.java | 32 --- .../java/com/kuhy/focusstatus/RootShell.java | 49 ---- .../java/com/kuhy/focusstatus/Status.java | 101 ------- .../com/kuhy/focusstatus/StatusService.java | 247 ----------------- phone_focus_mode/hosts_enforcer.sh | 207 --------------- phone_focus_mode/hosts_magisk.sh | 166 ------------ phone_focus_mode/hosts_mount.sh | 91 ------- phone_focus_mode/launcher_enforcer.sh | 179 ------------- phone_focus_mode/lib/adb_common.sh | 160 ----------- phone_focus_mode/lib/adb_locking.sh | 76 ------ phone_focus_mode/lib/adb_trusted.sh | 135 ---------- phone_focus_mode/lib/backup.sh | 220 --------------- phone_focus_mode/lib/backup_capture.sh | 119 --------- phone_focus_mode/lib/monitor.sh | 216 --------------- phone_focus_mode/lib/monitor_checks_health.sh | 138 ---------- phone_focus_mode/lib/monitor_checks_policy.sh | 136 ---------- phone_focus_mode/lib/monitor_report.py | 99 ------- phone_focus_mode/lib/restore.sh | 162 ------------ .../lib/tests/adb_test_harness.sh | 76 ------ .../lib/tests/backup_capture_harness.sh | 107 -------- .../lib/tests/ctl_cases_commands.sh | 250 ------------------ .../lib/tests/ctl_cases_curfew.sh | 144 ---------- .../lib/tests/ctl_cases_daemon.sh | 178 ------------- .../lib/tests/ctl_libs_harness.sh | 165 ------------ .../lib/tests/curfew_net_cases_chain.sh | 173 ------------ .../lib/tests/curfew_net_cases_lifecycle.sh | 121 --------- .../lib/tests/curfew_net_harness.sh | 170 ------------ .../lib/tests/daemon_cases_apps.sh | 143 ---------- .../lib/tests/daemon_cases_location.sh | 228 ---------------- .../lib/tests/daemon_cases_state.sh | 184 ------------- .../lib/tests/daemon_cases_status.sh | 80 ------ .../lib/tests/daemon_cases_sweep.sh | 173 ------------ .../lib/tests/daemon_libs_harness.sh | 160 ----------- .../lib/tests/dns_iptables_harness.sh | 174 ------------ .../lib/tests/hosts_cases_assert.sh | 114 -------- .../lib/tests/hosts_cases_cache.sh | 149 ----------- .../lib/tests/hosts_cases_magisk.sh | 129 --------- .../lib/tests/hosts_cases_mount.sh | 150 ----------- .../lib/tests/hosts_libs_harness.sh | 198 -------------- .../lib/tests/monitor_cases_health.sh | 249 ----------------- .../lib/tests/monitor_cases_policy.sh | 178 ------------- phone_focus_mode/lib/tests/monitor_harness.sh | 210 --------------- phone_focus_mode/lib/tests/test_adb_common.sh | 134 ---------- .../lib/tests/test_adb_trusted.sh | 105 -------- .../lib/tests/test_backup_capture.sh | 162 ------------ phone_focus_mode/lib/tests/test_ctl_libs.sh | 22 -- phone_focus_mode/lib/tests/test_curfew_net.sh | 23 -- .../lib/tests/test_daemon_libs.sh | 25 -- .../lib/tests/test_dns_iptables.sh | 235 ---------------- phone_focus_mode/lib/tests/test_hosts_libs.sh | 24 -- .../lib/tests/test_magisk_service.sh | 121 --------- phone_focus_mode/lib/tests/test_monitor.sh | 26 -- .../lib/tests/test_tether_enforcer.sh | 138 ---------- phone_focus_mode/magisk_service.sh | 59 ----- phone_focus_mode/phone_backup.sh | 229 ---------------- phone_focus_mode/restore_apps.sh | 184 ------------- phone_focus_mode/run_phone.sh | 234 ---------------- phone_focus_mode/strip_workout_hosts.py | 70 ----- .../systemd/install_pc_phone_automation.sh | 20 -- .../systemd/phone-auto-sync.service | 9 - .../systemd/phone-auto-sync.timer | 10 - phone_focus_mode/tether_enforcer.sh | 182 ------------- phone_focus_mode/tether_iptables.sh | 103 -------- phone_focus_mode/workout_detector.sh | 174 ------------ python_pkg/focus_policy/__init__.py | 26 -- python_pkg/focus_policy/__main__.py | 80 ------ python_pkg/focus_policy/blocklist.py | 170 ------------ python_pkg/focus_policy/export.py | 211 --------------- python_pkg/focus_policy/loader.py | 183 ------------- python_pkg/focus_policy/model.py | 216 --------------- python_pkg/focus_policy/tests/__init__.py | 1 - .../focus_policy/tests/test_blocklist.py | 215 --------------- python_pkg/focus_policy/tests/test_export.py | 160 ----------- .../tests/test_export_invariants.py | 135 ---------- python_pkg/focus_policy/tests/test_loader.py | 249 ----------------- python_pkg/focus_policy/tests/test_main.py | 110 -------- python_pkg/focus_policy/tests/test_model.py | 148 ----------- .../focus_policy/tests/test_model_policy.py | 168 ------------ 141 files changed, 115 insertions(+), 17641 deletions(-) create mode 100644 docs/superpowers/contracts/extract-phone-focus-mode-2026-08-23.json create mode 100644 docs/superpowers/evidence/extract-phone-focus-mode-2026-08-23.json delete mode 100644 linux_configuration/tests/test_monitor_report.py delete mode 100644 linux_configuration/tests/test_strip_workout_hosts.py delete mode 100644 meta/scripts/fixtures/mutations/backup_capture.json delete mode 100644 meta/scripts/fixtures/mutations/ctl_libs.json delete mode 100644 meta/scripts/fixtures/mutations/curfew_net.json delete mode 100644 meta/scripts/fixtures/mutations/daemon_libs.json delete mode 100644 meta/scripts/fixtures/mutations/hosts_libs.json delete mode 100644 meta/scripts/fixtures/mutations/monitor.json delete mode 100644 phone_focus_mode/README.md delete mode 100755 phone_focus_mode/backup_manifest.sh delete mode 100755 phone_focus_mode/batch3_bloatware_uninstall.sh delete mode 100755 phone_focus_mode/config.sh delete mode 100755 phone_focus_mode/config_curfew.sh delete mode 100755 phone_focus_mode/config_dns.sh delete mode 100755 phone_focus_mode/config_launcher.sh delete mode 100755 phone_focus_mode/config_paths.sh delete mode 100755 phone_focus_mode/config_tether.sh delete mode 100755 phone_focus_mode/ctl_curfew.sh delete mode 100755 phone_focus_mode/ctl_daemon.sh delete mode 100755 phone_focus_mode/ctl_dns.sh delete mode 100755 phone_focus_mode/ctl_hosts.sh delete mode 100755 phone_focus_mode/ctl_launcher.sh delete mode 100755 phone_focus_mode/ctl_tether.sh delete mode 100755 phone_focus_mode/ctl_usage.sh delete mode 100755 phone_focus_mode/ctl_workout.sh delete mode 100755 phone_focus_mode/curfew_enforcer.sh delete mode 100755 phone_focus_mode/curfew_net.sh delete mode 100755 phone_focus_mode/daemon_apps.sh delete mode 100755 phone_focus_mode/daemon_location.sh delete mode 100755 phone_focus_mode/daemon_state.sh delete mode 100755 phone_focus_mode/deploy.sh delete mode 100755 phone_focus_mode/deploy_actions.sh delete mode 100755 phone_focus_mode/deploy_daemons.sh delete mode 100755 phone_focus_mode/deploy_gps.sh delete mode 100755 phone_focus_mode/deploy_install.sh delete mode 100755 phone_focus_mode/deploy_magisk.sh delete mode 100755 phone_focus_mode/deploy_phases.sh delete mode 100755 phone_focus_mode/distraction_purge.sh delete mode 100755 phone_focus_mode/dns_enforcer.sh delete mode 100755 phone_focus_mode/dns_iptables.sh delete mode 100644 phone_focus_mode/docs/night-curfew.md delete mode 100644 phone_focus_mode/docs/policy-lists.md delete mode 100644 phone_focus_mode/docs/updating-and-troubleshooting.md delete mode 100644 phone_focus_mode/docs/youtube-block-unrooted.md delete mode 100755 phone_focus_mode/enable_persistent_adb.sh delete mode 100755 phone_focus_mode/focus_ctl.sh delete mode 100755 phone_focus_mode/focus_daemon.sh delete mode 100644 phone_focus_mode/focus_status_app/AndroidManifest.xml delete mode 100755 phone_focus_mode/focus_status_app/build.sh delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/BootReceiver.java delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewDemoReceiver.java delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewToggleReceiver.java delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/LaunchActivity.java delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RecheckReceiver.java delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RootShell.java delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/Status.java delete mode 100644 phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/StatusService.java delete mode 100755 phone_focus_mode/hosts_enforcer.sh delete mode 100755 phone_focus_mode/hosts_magisk.sh delete mode 100755 phone_focus_mode/hosts_mount.sh delete mode 100755 phone_focus_mode/launcher_enforcer.sh delete mode 100755 phone_focus_mode/lib/adb_common.sh delete mode 100755 phone_focus_mode/lib/adb_locking.sh delete mode 100755 phone_focus_mode/lib/adb_trusted.sh delete mode 100755 phone_focus_mode/lib/backup.sh delete mode 100755 phone_focus_mode/lib/backup_capture.sh delete mode 100755 phone_focus_mode/lib/monitor.sh delete mode 100755 phone_focus_mode/lib/monitor_checks_health.sh delete mode 100755 phone_focus_mode/lib/monitor_checks_policy.sh delete mode 100755 phone_focus_mode/lib/monitor_report.py delete mode 100755 phone_focus_mode/lib/restore.sh delete mode 100755 phone_focus_mode/lib/tests/adb_test_harness.sh delete mode 100755 phone_focus_mode/lib/tests/backup_capture_harness.sh delete mode 100755 phone_focus_mode/lib/tests/ctl_cases_commands.sh delete mode 100755 phone_focus_mode/lib/tests/ctl_cases_curfew.sh delete mode 100755 phone_focus_mode/lib/tests/ctl_cases_daemon.sh delete mode 100755 phone_focus_mode/lib/tests/ctl_libs_harness.sh delete mode 100755 phone_focus_mode/lib/tests/curfew_net_cases_chain.sh delete mode 100755 phone_focus_mode/lib/tests/curfew_net_cases_lifecycle.sh delete mode 100755 phone_focus_mode/lib/tests/curfew_net_harness.sh delete mode 100755 phone_focus_mode/lib/tests/daemon_cases_apps.sh delete mode 100755 phone_focus_mode/lib/tests/daemon_cases_location.sh delete mode 100755 phone_focus_mode/lib/tests/daemon_cases_state.sh delete mode 100755 phone_focus_mode/lib/tests/daemon_cases_status.sh delete mode 100755 phone_focus_mode/lib/tests/daemon_cases_sweep.sh delete mode 100755 phone_focus_mode/lib/tests/daemon_libs_harness.sh delete mode 100755 phone_focus_mode/lib/tests/dns_iptables_harness.sh delete mode 100755 phone_focus_mode/lib/tests/hosts_cases_assert.sh delete mode 100755 phone_focus_mode/lib/tests/hosts_cases_cache.sh delete mode 100755 phone_focus_mode/lib/tests/hosts_cases_magisk.sh delete mode 100755 phone_focus_mode/lib/tests/hosts_cases_mount.sh delete mode 100755 phone_focus_mode/lib/tests/hosts_libs_harness.sh delete mode 100755 phone_focus_mode/lib/tests/monitor_cases_health.sh delete mode 100755 phone_focus_mode/lib/tests/monitor_cases_policy.sh delete mode 100755 phone_focus_mode/lib/tests/monitor_harness.sh delete mode 100755 phone_focus_mode/lib/tests/test_adb_common.sh delete mode 100755 phone_focus_mode/lib/tests/test_adb_trusted.sh delete mode 100755 phone_focus_mode/lib/tests/test_backup_capture.sh delete mode 100755 phone_focus_mode/lib/tests/test_ctl_libs.sh delete mode 100755 phone_focus_mode/lib/tests/test_curfew_net.sh delete mode 100755 phone_focus_mode/lib/tests/test_daemon_libs.sh delete mode 100755 phone_focus_mode/lib/tests/test_dns_iptables.sh delete mode 100755 phone_focus_mode/lib/tests/test_hosts_libs.sh delete mode 100755 phone_focus_mode/lib/tests/test_magisk_service.sh delete mode 100755 phone_focus_mode/lib/tests/test_monitor.sh delete mode 100755 phone_focus_mode/lib/tests/test_tether_enforcer.sh delete mode 100755 phone_focus_mode/magisk_service.sh delete mode 100755 phone_focus_mode/phone_backup.sh delete mode 100755 phone_focus_mode/restore_apps.sh delete mode 100755 phone_focus_mode/run_phone.sh delete mode 100755 phone_focus_mode/strip_workout_hosts.py delete mode 100755 phone_focus_mode/systemd/install_pc_phone_automation.sh delete mode 100644 phone_focus_mode/systemd/phone-auto-sync.service delete mode 100644 phone_focus_mode/systemd/phone-auto-sync.timer delete mode 100755 phone_focus_mode/tether_enforcer.sh delete mode 100755 phone_focus_mode/tether_iptables.sh delete mode 100755 phone_focus_mode/workout_detector.sh delete mode 100644 python_pkg/focus_policy/__init__.py delete mode 100644 python_pkg/focus_policy/__main__.py delete mode 100644 python_pkg/focus_policy/blocklist.py delete mode 100644 python_pkg/focus_policy/export.py delete mode 100644 python_pkg/focus_policy/loader.py delete mode 100644 python_pkg/focus_policy/model.py delete mode 100644 python_pkg/focus_policy/tests/__init__.py delete mode 100644 python_pkg/focus_policy/tests/test_blocklist.py delete mode 100644 python_pkg/focus_policy/tests/test_export.py delete mode 100644 python_pkg/focus_policy/tests/test_export_invariants.py delete mode 100644 python_pkg/focus_policy/tests/test_loader.py delete mode 100644 python_pkg/focus_policy/tests/test_main.py delete mode 100644 python_pkg/focus_policy/tests/test_model.py delete mode 100644 python_pkg/focus_policy/tests/test_model_policy.py diff --git a/.gitignore b/.gitignore index c791c48c..62f4893d 100644 --- a/.gitignore +++ b/.gitignore @@ -352,16 +352,18 @@ python_pkg/*/cache/ # FVM Version Cache .fvm/ -# Focus mode secrets (contains home GPS coordinates) +# Focus mode secrets (contains home GPS coordinates). phone_focus_mode/ moved +# to its own repo, but the working copy is still on this machine and still +# holds a real location, so the ignore stays until the directory is gone. phone_focus_mode/config_secrets.sh +phone_focus_mode/focus_status_app/build +phone_focus_mode/focus_status_app/debug.keystore # Generated output files out.txt # Build output symlinks (point to ../testsAndMisc_builds/) # Patterns without trailing / to match symlinks, not just directories -phone_focus_mode/focus_status_app/build -phone_focus_mode/focus_status_app/debug.keystore .worktrees .claude/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c6598f0d..f2963a81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -133,7 +133,7 @@ repos: language: script types: [shell] require_serial: true - exclude: ^(\.git/|phone_focus_mode/lib/tests/|tests/) + exclude: ^(\.git/|tests/) - id: shell-coverage-ratchet name: Every shell library has a test suite beside it # The ratchet from meta/shell-coverage-allowlist.txt: a lib under a diff --git a/.prettierignore b/.prettierignore index 248a56d7..8e03917c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,15 +7,3 @@ **/dist/ **/coverage/ node_modules/ - -# Captured real-world payloads used as test fixtures. These must stay -# byte-faithful to what the device or service actually emits — a real Polish -# e-paragon arrives as minified single-line JSON, and the tests read the file -# as raw text, not only as parsed JSON. Pretty-printing one silently broke -# `accepts bare JPK payload and bare JWS token`, whose regex matches -# `"data":"..."` with no space after the colon. -billsplit/test/fixtures/ - -# Generated by `python3 -m python_pkg.focus_policy` from phone_focus_mode/config.sh. -# json.dumps cannot reproduce prettier's single-line-short-array style, so -# formatting it here would conflict with every regeneration. diff --git a/AGENTS.md b/AGENTS.md index 4572c068..1dc396d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,6 @@ Mixed-language monorepo: Python packages, Bash automation, and several apps. | `python_pkg/` | Python packages — every `.py` in the repo belongs here or in an allowlisted dir | | `meta/` | Repo-wide tooling: `pyproject.toml`, `requirements.txt`, `run.sh`, `lint_python.sh`, `.fvmrc`, `scripts/` | | `linux_configuration/` | Arch setup, i3 config, system maintenance, and the tracked git hooks | -| `phone_focus_mode/` | GPS-based Android focus enforcer (Bash, ADB, Magisk) | | `billsplit/` | Receipt bill-splitting Flutter app (Dart) | | `bottles/` | Small standalone project | | `docs/` | Reference docs; `docs/superpowers/` holds AI workflow artifacts | @@ -32,7 +31,9 @@ Extracted to their own repos: [`steam-backlog-enforcer`](https://github.com/kuhy [`system-maintenance`](https://github.com/kuhyx/system-maintenance) (lives at `~/system-maintenance`), [`android-guardian`](https://github.com/kuhyx/android-guardian) (lives at -`~/android-guardian`). +`~/android-guardian`), +[`phone-focus-mode`](https://github.com/kuhyx/phone-focus-mode) (with +`python_pkg/focus_policy`). Archived work: [`testsAndMisc-archive`](https://github.com/kuhyx/testsAndMisc-archive). ## Adding a Flutter app @@ -47,8 +48,7 @@ committed. To add another, repeat **both** halves or the icons fail silently: Icons come from the shared generator in `python_pkg/app_icons/`. Dart style is gated by CI only (`.github/workflows/billsplit-ci.yml`), not by a local hook. Keep app dirs free of Python: the `check-python-location` hook requires every -`.py` to sit under `python_pkg/` (or `linux_configuration/`, `phone_focus_mode/`, -`meta/scripts/`). `billsplit/`'s coverage gate therefore lives at +`.py` to sit under `python_pkg/` (or `linux_configuration/`, `meta/scripts/`). `billsplit/`'s coverage gate therefore lives at `python_pkg/billsplit_coverage/`. ## Git Workflow diff --git a/docs/superpowers/contracts/extract-phone-focus-mode-2026-08-23.json b/docs/superpowers/contracts/extract-phone-focus-mode-2026-08-23.json new file mode 100644 index 00000000..42be5ddd --- /dev/null +++ b/docs/superpowers/contracts/extract-phone-focus-mode-2026-08-23.json @@ -0,0 +1,21 @@ +{ + "title": "Extract phone_focus_mode with focus_policy into one public repo", + "objective": "Move the GPS focus enforcer and its Python policy half out together, since focus_policy parses phone_focus_mode's config.sh and is referenced by nothing else. The repo is public and the subject matter is a home location, so proving no coordinate leaks is part of the job, not an afterthought.", + "acceptance_criteria": [ + "Both trees land in one public repo with full history and python_pkg.focus_policy imports rewritten.", + "The full git history contains no real home coordinate and no committed config_secrets.sh, verified BEFORE the repo is made public.", + "The new repo has a .gitignore covering config_secrets.sh; none came across in the filter-repo pass.", + "The Python suites pass from a clean clone: 161 tests.", + "The shell suites pass with NO config_secrets.sh present, which means the harness must pin its own coordinates instead of inheriting real ones.", + "config.sh can be sourced without a secrets file, so CI and a fresh clone work.", + "Any suite that does not pass is skipped in CI with a stated reason, and its behaviour is compared against the pre-extraction baseline to establish whether the split caused it.", + "The monorepo's remaining tests still pass and no config, hook or workflow names a removed path.", + "pre-commit passes without --no-verify." + ], + "out_of_scope": [ + "hosts and digital_wellbeing, deferred while the guard-lib repoint is pending.", + "Fixing test_magisk_service.sh, which was already broken and is not a regression from this split.", + "python-tests.yml's stale lichess_bot and tests/ path filters, absent since May 2026." + ], + "verifier": "git log --all -p greps clean for the home coordinates; pytest reports 161 passed in the new repo; the shell suites run with the secrets file moved away; pytest linux_configuration/tests reports 178 passed here; check_shell_coverage.sh --all and check_python_location.sh over every .py both exit 0; check_directory_depth.sh --all drops 262 -> 248." +} diff --git a/docs/superpowers/evidence/extract-phone-focus-mode-2026-08-23.json b/docs/superpowers/evidence/extract-phone-focus-mode-2026-08-23.json new file mode 100644 index 00000000..b79d3e37 --- /dev/null +++ b/docs/superpowers/evidence/extract-phone-focus-mode-2026-08-23.json @@ -0,0 +1,54 @@ +{ + "intent": "Extract phone_focus_mode together with python_pkg/focus_policy into one public repo, and fix the test suite's hidden dependency on an untracked secrets file rather than carrying it across.", + "scope": [ + "phone_focus_mode/ (109 files) + python_pkg/focus_policy/ (14) -> github.com/kuhyx/phone-focus-mode", + "2 Python suites from linux_configuration/tests/ and 6 mutation fixtures from meta/scripts/fixtures/mutations/ moved with them", + "conftest.py, meta/pyproject.toml, .pre-commit-config.yaml, .gitignore, .prettierignore, shell-coverage-allowlist.txt cleaned", + "check_python_location.sh: the phone_focus_mode exception removed now that the directory is gone", + "Non-goal: hosts and digital_wellbeing, deferred at the user's request" + ], + "changes": [ + "Extracted both trees in one filter-repo pass (77 commits), hoisting phone_focus_mode to the root, python_pkg/focus_policy to focus_policy/, and rewriting python_pkg.focus_policy imports plus the mutation fixtures' phone_focus_mode/ path prefix.", + "Gave the new repo a conftest putting the root and lib/ on sys.path, a pyproject, a .shellcheckrc and CI covering Python, shellcheck and the shell suites.", + "Added a .gitignore to the new repo: none came across in the filter-repo pass and the repo is public. config_secrets.sh is ignored.", + "ctl_libs_harness.sh now pins HOME_LAT/HOME_LON to a pair 4300 m south of the coordinate its case seeds, instead of inheriting whatever config_secrets.sh holds.", + "config.sh sources config_secrets.sh only when it exists, so a clean clone and CI can run.", + "Tightened check_python_location.sh and the AGENTS.md rule it documents, dropping the exception for a directory that no longer exists." + ], + "verification": [ + { + "command": "python -m pytest tests focus_policy -q (extracted repo)", + "result": "pass", + "evidence": "161 passed." + }, + { + "command": "bash lib/tests/test_*.sh with NO config_secrets.sh present", + "result": "pass", + "evidence": "10 of 11 suites pass, including test_ctl_libs.sh at 136/136. Before the fix it failed 'status distance should be ~4300 m, got 25555' with a placeholder secrets file and could not run at all without one." + }, + { + "command": "bash lib/tests/test_magisk_service.sh, in the extracted repo AND the pre-extraction baseline worktree", + "result": "fail in both (pre-existing)", + "evidence": "Times out (exit 124) in the monorepo baseline too, and is named in no CI list there. Carried across knowingly; the new CI skips it explicitly with that reason rather than hiding it." + }, + { + "command": "git log --all -p | grep for the known home coordinates; git log for config_secrets.sh", + "result": "pass", + "evidence": "Zero matches across all history and the file was never committed, checked before the repo was made public. Remaining coordinate literals are the Warsaw city-centre test fixture (52.2297, 21.0122)." + }, + { + "command": "pytest linux_configuration/tests; check_shell_coverage.sh --all; check_python_location.sh over every tracked .py", + "result": "pass", + "evidence": "178 passed; coverage gate exit 0; all Python files pass the tightened location hook. Depth 262 -> 248." + } + ], + "risks": [ + "test_magisk_service.sh remains broken. It is skipped in CI with a stated reason, not silently dropped, but it is a real gap the new repo inherits.", + "python-tests.yml still filters on python_pkg/lichess_bot and tests/, neither of which has existed since the May 2026 archive commit. Pre-existing and harmless (a filter on a missing path just never matches); left alone rather than widening this change.", + "The distance assertion now encodes a synthetic home 4300 m from the fixture. If the seeded fix ever changes, the pinned pair must change with it - the harness comment says so." + ], + "rollback": [ + "git revert to restore both trees, the two tests and the mutation fixtures, then re-add the conftest entries, pylint sys.path elements, pre-commit exclude and the location-hook exception.", + "Re-run pytest linux_configuration/tests and check_shell_coverage.sh --all." + ] +} diff --git a/linux_configuration/tests/conftest.py b/linux_configuration/tests/conftest.py index 5d2ada7d..d666e7c4 100644 --- a/linux_configuration/tests/conftest.py +++ b/linux_configuration/tests/conftest.py @@ -23,8 +23,6 @@ _REPO_ROOT = Path(__file__).resolve().parents[2] # Each standalone script directory whose Python modules these tests import. _SCRIPT_DIRS = ( _REPO_ROOT / "meta" / "scripts", # validate_evidence, validate_contract - _REPO_ROOT / "phone_focus_mode" / "lib", # monitor_report - _REPO_ROOT / "phone_focus_mode", # strip_workout_hosts _REPO_ROOT / "linux_configuration" / "scripts" diff --git a/linux_configuration/tests/test_monitor_report.py b/linux_configuration/tests/test_monitor_report.py deleted file mode 100644 index 5a6abbda..00000000 --- a/linux_configuration/tests/test_monitor_report.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Tests for phone_focus_mode/lib/monitor_report.py (report summary/severity).""" - -from __future__ import annotations - -import json -from typing import TYPE_CHECKING - -import monitor_report -from monitor_report import _field, _has_severe, _load_checks, _render_summary, main - -if TYPE_CHECKING: - from pathlib import Path - - import pytest - - -def _write(tmp_path: Path, obj: object) -> Path: - """Write ``obj`` as JSON to a temp file and return its path.""" - path = tmp_path / "report.json" - path.write_text(json.dumps(obj), encoding="utf-8") - return path - - -class TestLoadChecks: - """``_load_checks`` tolerates malformed reports.""" - - def test_reads_checks(self, tmp_path: Path) -> None: - """A well-formed report yields its checks list.""" - path = _write(tmp_path, {"checks": [{"status": "ok"}]}) - assert _load_checks(path) == [{"status": "ok"}] - - def test_non_dict_report_yields_empty(self, tmp_path: Path) -> None: - """A non-object report yields no checks.""" - assert _load_checks(_write(tmp_path, [1, 2])) == [] - - def test_non_list_checks_yields_empty(self, tmp_path: Path) -> None: - """A non-list ``checks`` value yields no checks.""" - assert _load_checks(_write(tmp_path, {"checks": "nope"})) == [] - - -class TestField: - """``_field`` reads string fields with a default.""" - - def test_reads_present_string(self) -> None: - """A present string field is returned.""" - assert _field({"status": "ok"}, "status", "warn") == "ok" - - def test_default_when_missing(self) -> None: - """A missing field falls back to the default.""" - assert _field({}, "status", "warn") == "warn" - - def test_default_when_not_string(self) -> None: - """A non-string field falls back to the default.""" - assert _field({"status": 5}, "status", "warn") == "warn" - - def test_default_when_check_not_dict(self) -> None: - """A non-dict check falls back to the default.""" - assert _field("nope", "status", "warn") == "warn" - - -class TestRenderSummary: - """``_render_summary`` produces the counts header and issue list.""" - - def test_counts_and_issues(self) -> None: - """Counts and a per-issue line are rendered.""" - checks: list[object] = [ - {"status": "ok", "check": "a", "message": ""}, - {"status": "warn", "check": "b", "message": "drift"}, - {"status": "error", "check": "c", "message": "boom"}, - ] - out = _render_summary(checks) - assert "ok=1" in out - assert "warn=1" in out - assert "error=1" in out - assert "[warn] b: drift" in out - assert "[error] c: boom" in out - - def test_no_issues_section_when_all_ok(self) -> None: - """With no problems, the issues section is omitted.""" - out = _render_summary([{"status": "ok", "check": "a", "message": ""}]) - assert "Issues found:" not in out - # Footer line plus a trailing blank line, matching the original output. - assert out.endswith("==========================\n\n") - - -class TestHasSevere: - """``_has_severe`` flags fatal/error checks.""" - - def test_true_for_error(self) -> None: - """An error status is severe.""" - assert _has_severe([{"status": "error"}]) is True - - def test_true_for_fatal(self) -> None: - """A fatal status is severe.""" - assert _has_severe([{"status": "fatal"}]) is True - - def test_false_for_ok_warn(self) -> None: - """ok/warn statuses are not severe.""" - assert _has_severe([{"status": "ok"}, {"status": "warn"}]) is False - - -class TestMain: - """The CLI dispatches on mode and reports severity via exit code.""" - - def test_bad_usage_returns_2(self, monkeypatch: pytest.MonkeyPatch) -> None: - """An unknown mode is a usage error (rc 2).""" - monkeypatch.setattr( - monitor_report.sys, "argv", ["monitor_report", "bogus", "x"] - ) - assert main() == 2 - - def test_summary_prints_and_returns_0( - self, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], - ) -> None: - """Summary mode prints the report and returns 0.""" - path = _write( - tmp_path, {"checks": [{"status": "warn", "check": "b", "message": "m"}]} - ) - monkeypatch.setattr( - monitor_report.sys, - "argv", - ["monitor_report", "summary", str(path)], - ) - assert main() == 0 - assert "Monitoring Summary" in capsys.readouterr().out - - def test_severity_returns_1_on_error( - self, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - """Severity mode returns 1 when a severe check exists.""" - path = _write(tmp_path, {"checks": [{"status": "error"}]}) - monkeypatch.setattr( - monitor_report.sys, - "argv", - ["monitor_report", "severity", str(path)], - ) - assert main() == 1 - - def test_severity_returns_0_when_clean( - self, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - """Severity mode returns 0 when no severe check exists.""" - path = _write(tmp_path, {"checks": [{"status": "ok"}]}) - monkeypatch.setattr( - monitor_report.sys, - "argv", - ["monitor_report", "severity", str(path)], - ) - assert main() == 0 diff --git a/linux_configuration/tests/test_strip_workout_hosts.py b/linux_configuration/tests/test_strip_workout_hosts.py deleted file mode 100644 index 61fa2f23..00000000 --- a/linux_configuration/tests/test_strip_workout_hosts.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Tests for phone_focus_mode/strip_workout_hosts.py (hosts workout variant).""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import strip_workout_hosts -from strip_workout_hosts import _strip, main - -if TYPE_CHECKING: - from pathlib import Path - - import pytest - -_HOSTS = """\ -# header -127.0.0.1 localhost -0.0.0.0 youtube.com www.youtube.com -0.0.0.0 ads.example.com -0.0.0.0 m.youtube.com alias.youtube.com -0.0.0.0 keepme.com -""" - - -def _src(tmp_path: Path) -> Path: - """Write the sample hosts file and return its path.""" - path = tmp_path / "hosts" - path.write_text(_HOSTS, encoding="utf-8") - return path - - -class TestStrip: - """``_strip`` removes lines mapping unblocked domains.""" - - def test_removes_matching_and_alias_lines(self, tmp_path: Path) -> None: - """Entries whose name or any alias is unblocked are dropped.""" - dest = tmp_path / "out" - _strip( - _src(tmp_path), - dest, - frozenset({"youtube.com", "www.youtube.com", "m.youtube.com"}), - ) - result = dest.read_text(encoding="utf-8") - assert "youtube.com" not in result # both youtube entries gone - assert "ads.example.com" in result - assert "keepme.com" in result - assert "localhost" in result - assert "# header" in result - - def test_empty_unblock_keeps_everything(self, tmp_path: Path) -> None: - """An empty unblock set copies the file verbatim.""" - dest = tmp_path / "out" - _strip(_src(tmp_path), dest, frozenset()) - assert dest.read_text(encoding="utf-8") == _HOSTS - - def test_comment_and_blank_lines_preserved(self, tmp_path: Path) -> None: - """Comment and blank lines are never stripped.""" - src = tmp_path / "hosts" - src.write_text("# c\n\n0.0.0.0 block.me\n", encoding="utf-8") - dest = tmp_path / "out" - _strip(src, dest, frozenset({"block.me"})) - assert dest.read_text(encoding="utf-8") == "# c\n\n" - - -class TestMain: - """The CLI reads paths from argv and domains from the environment.""" - - def test_bad_arg_count_returns_2(self, monkeypatch: pytest.MonkeyPatch) -> None: - """A wrong number of path arguments is a usage error (rc 2).""" - monkeypatch.setattr( - strip_workout_hosts.sys, "argv", ["strip_workout_hosts", "only-one"] - ) - assert main() == 2 - - def test_strips_via_env( - self, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - """Domains from the env var drive the stripping.""" - src = _src(tmp_path) - dest = tmp_path / "out" - monkeypatch.setenv("WORKOUT_UNBLOCK_DOMAINS", "youtube.com m.youtube.com") - monkeypatch.setattr( - strip_workout_hosts.sys, - "argv", - ["strip_workout_hosts", str(src), str(dest)], - ) - assert main() == 0 - result = dest.read_text(encoding="utf-8") - assert "www.youtube.com" not in result - assert "keepme.com" in result - - -class TestSubdomainMatching: - """Regression tests for the exact-match bug fixed on 2026-08-05. - - The original implementation tested ``name.lower() in unblock``, i.e. exact - set membership. YouTube streams video from dynamically named CDN hosts, so - an allowlist entry of ``googlevideo.com`` released nothing that actually - carried video and playback stayed blocked during workouts. - """ - - def test_releases_dynamic_cdn_subdomains(self, tmp_path: Path) -> None: - """A parent domain releases the CDN hosts beneath it.""" - src = tmp_path / "hosts" - src.write_text( - "0.0.0.0 googlevideo.com\n" - "0.0.0.0 r1---sn-4g5e6nls.googlevideo.com\n" - "0.0.0.0 r1---sn-4g5lne7s.googlevideo.com\n" - "0.0.0.0 keepme.com\n", - encoding="utf-8", - ) - dest = tmp_path / "out" - _strip(src, dest, frozenset({"googlevideo.com"})) - result = dest.read_text(encoding="utf-8") - assert "googlevideo.com" not in result - assert "keepme.com" in result - - def test_does_not_release_lookalike_domains(self, tmp_path: Path) -> None: - """Suffix matching must not be fooled by substring similarity.""" - src = tmp_path / "hosts" - src.write_text( - "0.0.0.0 notgooglevideo.com\n0.0.0.0 googlevideo.com.evil.net\n", - encoding="utf-8", - ) - dest = tmp_path / "out" - _strip(src, dest, frozenset({"googlevideo.com"})) - result = dest.read_text(encoding="utf-8") - assert "notgooglevideo.com" in result - assert "googlevideo.com.evil.net" in result - - def test_case_and_trailing_dot_are_normalised(self, tmp_path: Path) -> None: - """Host names are compared case-insensitively and without the root dot.""" - src = tmp_path / "hosts" - src.write_text("0.0.0.0 WWW.YouTube.COM.\n", encoding="utf-8") - dest = tmp_path / "out" - _strip(src, dest, frozenset({"youtube.com"})) - assert dest.read_text(encoding="utf-8") == "" diff --git a/meta/pyproject.toml b/meta/pyproject.toml index 56744713..65c51bfb 100644 --- a/meta/pyproject.toml +++ b/meta/pyproject.toml @@ -170,7 +170,7 @@ disallow_untyped_decorators = false # bare name (the same dirs linux_configuration/tests/conftest.py adds to # sys.path at runtime) resolve under static analysis instead of raising E0401. # Paths are relative to the repo root, which is pre-commit's working directory. -init-hook = "import sys; sys.path[:0] = ['meta/scripts', 'python_pkg', 'phone_focus_mode', 'phone_focus_mode/lib', 'linux_configuration/scripts/single_use/utils', 'linux_configuration/scripts/periodic_background/digital_wellbeing', 'linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools']" +init-hook = "import sys; sys.path[:0] = ['meta/scripts', 'python_pkg', 'linux_configuration/scripts/single_use/utils', 'linux_configuration/scripts/periodic_background/digital_wellbeing', 'linux_configuration/scripts/single_use/misc/testsAndMisc-bash/tools']" # Analyse import fallback blocks analyse-fallback-blocks = true # Pickle collected data for later comparisons diff --git a/meta/scripts/check_python_location.sh b/meta/scripts/check_python_location.sh index 55c138c8..a631f97a 100755 --- a/meta/scripts/check_python_location.sh +++ b/meta/scripts/check_python_location.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Check that all Python files are under python_pkg/. -# Exceptions: linux_configuration/, phone_focus_mode/, Bash/, +# Exceptions: linux_configuration/, Bash/, # and vendored/generated directories. # Used as a pre-commit hook; receives staged file paths as arguments. @@ -9,36 +9,39 @@ set -uo pipefail errors=() for file in "$@"; do - # Only check .py files - [[ "$file" != *.py ]] && continue + # Only check .py files + [[ "$file" != *.py ]] && continue - # Skip files already under python_pkg/ - [[ "$file" == python_pkg/* ]] && continue + # Skip files already under python_pkg/ + [[ "$file" == python_pkg/* ]] && continue - # Skip allowed directories (non-Python projects with some Python scripts) - case "$file" in - linux_configuration/*|phone_focus_mode/*|scripts/*|meta/scripts/*) continue ;; - esac + # Skip allowed directories (non-Python projects with some Python scripts) + case "$file" in + linux_configuration/* | scripts/* | meta/scripts/*) continue ;; + esac - # Skip vendored/generated directories - skip=0 - for part in /.venv/ /venv/ /__pycache__/ /build/ /dist/ /node_modules/ /.git/; do - case "/$file" in - *"$part"*) skip=1; break ;; - esac - done - [[ $skip -eq 1 ]] && continue + # Skip vendored/generated directories + skip=0 + for part in /.venv/ /venv/ /__pycache__/ /build/ /dist/ /node_modules/ /.git/; do + case "/$file" in + *"$part"*) + skip=1 + break + ;; + esac + done + [[ $skip -eq 1 ]] && continue - errors+=("$file") + errors+=("$file") done if [[ ${#errors[@]} -gt 0 ]]; then - echo "ERROR: Python files must be under python_pkg/." - echo "The following files are in the wrong location:" - for err in "${errors[@]}"; do - echo " $err" - done - echo "" - echo "Move them with: git mv python_pkg/" - exit 1 + echo "ERROR: Python files must be under python_pkg/." + echo "The following files are in the wrong location:" + for err in "${errors[@]}"; do + echo " $err" + done + echo "" + echo "Move them with: git mv python_pkg/" + exit 1 fi diff --git a/meta/scripts/fixtures/mutations/backup_capture.json b/meta/scripts/fixtures/mutations/backup_capture.json deleted file mode 100644 index c75284e0..00000000 --- a/meta/scripts/fixtures/mutations/backup_capture.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "test": "phone_focus_mode/lib/tests/test_backup_capture.sh", - "timeout": 180, - "mutations": [ - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "sed 's/^package://'", - "replace": "cat", - "describe": "stop stripping the package: prefix" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "tr -d '\\r'", - "replace": "cat", - "describe": "stop stripping carriage returns" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "comm -13", - "replace": "comm -12", - "describe": "invert the purged-package derivation" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "pm list packages -3", - "replace": "pm list packages", - "describe": "dump all packages as third-party" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "mapfile -t pkgs <\"${dir}/packages-third-party.txt\"", - "replace": "read -r pkgs <\"${dir}/packages-third-party.txt\"", - "describe": "reintroduce the stdin-draining apk bug" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "for path in \"${paths[@]}\"; do", - "replace": "for path in \"${paths[0]}\"; do", - "describe": "pull only the first split of a split app" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "_warn \"no APK path for ${pkg}\"", - "replace": ":", - "describe": "silence the missing-APK-path warning" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "_warn \"could not pull ${base} for ${pkg}\"", - "replace": ":", - "describe": "silence the failed-pull warning" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "_warn \"partial or failed pull: ${d}\"", - "replace": ":", - "describe": "silence the sdcard partial-pull warning" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "if adb_cmd shell \"test -d /sdcard/${d}\"", - "replace": "if ! adb_cmd shell \"test -d /sdcard/${d}\"", - "describe": "invert the sdcard directory probe" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "for ns in system secure global; do", - "replace": "for ns in system; do", - "describe": "drop two settings namespaces" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "grep -qx 'com.celzero.bravedns'", - "replace": "grep -qvx 'com.celzero.bravedns'", - "describe": "invert the RethinkDNS detection" - }, - { - "file": "phone_focus_mode/lib/backup_capture.sh", - "find": "mkdir -p \"${dir}/sdcard/keepass\"", - "replace": ":", - "describe": "stop creating the keepass directory" - } - ] -} diff --git a/meta/scripts/fixtures/mutations/ctl_libs.json b/meta/scripts/fixtures/mutations/ctl_libs.json deleted file mode 100644 index 08d84c41..00000000 --- a/meta/scripts/fixtures/mutations/ctl_libs.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "test": "phone_focus_mode/lib/tests/test_ctl_libs.sh", - "timeout": 300, - "mutations": [ - { - "file": "phone_focus_mode/ctl_curfew.sh", - "find": "[ \"$now\" -ge \"$start\" ] || [ \"$now\" -lt \"$end\" ]", - "replace": "[ \"$now\" -ge \"$start\" ] && [ \"$now\" -lt \"$end\" ]", - "describe": "break the midnight-wrapping curfew window" - }, - { - "file": "phone_focus_mode/ctl_curfew.sh", - "find": "case \"$now\" in '' | *[!0-9]*) return 1 ;; esac", - "replace": "case \"$now\" in '' | *[!0-9]*) return 0 ;; esac", - "describe": "fail CLOSED on a broken clock instead of open" - }, - { - "file": "phone_focus_mode/ctl_curfew.sh", - "find": "while [ \"${n#0}\" != \"$n\" ] && [ \"${#n}\" -gt 1 ]; do n=\"${n#0}\"; done", - "replace": ":", - "describe": "stop stripping leading zeros (octal parse)" - }, - { - "file": "phone_focus_mode/ctl_curfew.sh", - "find": "\ttouch \"$CURFEW_FORCE_FILE\"", - "replace": "\t:", - "describe": "curfew-test-on stops creating the force file" - }, - { - "file": "phone_focus_mode/ctl_curfew.sh", - "find": "cmd_curfew_off() {\n\ttouch \"$CURFEW_OVERRIDE_FILE\"", - "replace": "cmd_curfew_off() {\n\t:", - "describe": "curfew-off stops creating the 2am escape hatch" - }, - { - "file": "phone_focus_mode/ctl_curfew.sh", - "find": "cmd_curfew_on() {\n\trm -f \"$CURFEW_OVERRIDE_FILE\"", - "replace": "cmd_curfew_on() {\n\t:", - "describe": "curfew-on stops clearing the override" - }, - { - "file": "phone_focus_mode/ctl_tether.sh", - "find": "\ttouch \"$TETHER_FORCE_FILE\"", - "replace": "\t:", - "describe": "tether-test-on stops creating the force file" - }, - { - "file": "phone_focus_mode/ctl_tether.sh", - "find": "cmd_tether_test_off() {\n\trm -f \"$TETHER_FORCE_FILE\"", - "replace": "cmd_tether_test_off() {\n\t:", - "describe": "tether-test-off stops clearing the force file" - }, - { - "file": "phone_focus_mode/ctl_dns.sh", - "find": "if iptables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then", - "replace": "if ! iptables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then", - "describe": "invert the v4 DNS chain presence check" - }, - { - "file": "phone_focus_mode/ctl_dns.sh", - "find": "if ip6tables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then", - "replace": "if ! ip6tables -L \"$DNS_IPT_CHAIN\" >/dev/null 2>&1; then", - "describe": "invert the v6 DNS chain presence check" - }, - { - "file": "phone_focus_mode/ctl_dns.sh", - "find": "${mode:-}", - "replace": "${mode}", - "describe": "render an unset private DNS mode as blank" - }, - { - "file": "phone_focus_mode/ctl_hosts.sh", - "find": "cmd_hosts_start() {\n\tlocal pid\n\tpid=\"$(hosts_enforcer_pid)\"\n\tif [ -n \"$pid\" ]; then", - "replace": "cmd_hosts_start() {\n\tlocal pid\n\tpid=\"$(hosts_enforcer_pid)\"\n\tif [ -z \"$pid\" ]; then", - "describe": "invert the hosts already-running guard" - }, - { - "file": "phone_focus_mode/ctl_workout.sh", - "find": "if [ ! -x \"$WORKOUT_SQLITE3_BIN\" ]; then", - "replace": "if [ -x \"$WORKOUT_SQLITE3_BIN\" ]; then", - "describe": "invert the sqlite3 precondition" - }, - { - "file": "phone_focus_mode/ctl_daemon.sh", - "find": "local mode=\"unknown\"", - "replace": "local mode=\"\"", - "describe": "report an absent mode as blank rather than unknown" - }, - { - "file": "phone_focus_mode/ctl_daemon.sh", - "find": "echo \"Location: unavailable\"", - "replace": ":", - "describe": "stay silent when no GPS fix is available" - }, - { - "file": "phone_focus_mode/ctl_daemon.sh", - "find": "R=6371000", - "replace": "R=1", - "describe": "wrong Earth radius in cmd_status's Haversine (awk kcov cannot see)" - }, - { - "file": "phone_focus_mode/ctl_daemon.sh", - "find": "pm enable \"$pkg\" >/dev/null 2>&1 && count=$((count + 1))", - "replace": "count=$((count + 1))", - "describe": "count apps as restored without enabling them" - }, - { - "file": "phone_focus_mode/ctl_daemon.sh", - "find": "\t\t: >\"$DISABLED_APPS_FILE\"", - "replace": "\t\t:", - "describe": "leave the disabled record in place after restoring" - }, - { - "file": "phone_focus_mode/ctl_daemon.sh", - "find": "\ttouch \"$RECHECK_TRIGGER\"", - "replace": "\t:", - "describe": "recheck stops writing the trigger" - }, - { - "file": "phone_focus_mode/ctl_daemon.sh", - "find": "echo \"Daemon not running - start it first with: focus_ctl.sh start\"\n\t\treturn 1", - "replace": "echo \"Daemon not running - start it first with: focus_ctl.sh start\"", - "describe": "recheck continues after reporting no daemon" - }, - { - "file": "phone_focus_mode/ctl_usage.sh", - "find": "\t\t*.*) ;;", - "replace": "\t\t*) ;;", - "describe": "accept non-dotted tokens as package names" - }, - { - "file": "phone_focus_mode/ctl_usage.sh", - "find": "\t\tcase \"$pkg\" in\n\t\t*[!A-Za-z0-9._]*) continue ;;\n\t\tesac", - "replace": "\t\t:", - "describe": "accept packages containing illegal characters" - } - ] -} diff --git a/meta/scripts/fixtures/mutations/curfew_net.json b/meta/scripts/fixtures/mutations/curfew_net.json deleted file mode 100644 index 68b639ae..00000000 --- a/meta/scripts/fixtures/mutations/curfew_net.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "test": "phone_focus_mode/lib/tests/test_curfew_net.sh", - "timeout": 180, - "mutations": [ - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "-I OUTPUT 1 -j", - "replace": "-I OUTPUT 2 -j", - "describe": "stop pinning the OUTPUT jump at position 1" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "iptw \"$ipt\" -N \"$CURFEW_NET_IPT_CHAIN\" 2>/dev/null || return 1", - "replace": "iptw \"$ipt\" -N \"$CURFEW_NET_IPT_CHAIN\" 2>/dev/null || true", - "describe": "swallow a failed chain creation" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "iptw \"$ipt\" -I OUTPUT 1 -j \"$CURFEW_NET_IPT_CHAIN\" 2>/dev/null || return 1", - "replace": "iptw \"$ipt\" -I OUTPUT 1 -j \"$CURFEW_NET_IPT_CHAIN\" 2>/dev/null || true", - "describe": "swallow a failed jump insert" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "iptw \"$ipt\" -F \"$CURFEW_NET_IPT_CHAIN\" 2>/dev/null || return 1", - "replace": "iptw \"$ipt\" -F \"$CURFEW_NET_IPT_CHAIN\" 2>/dev/null || true", - "describe": "swallow a failed flush and layer onto stale rules" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "-o lo -j ACCEPT", - "replace": "-o lo -j REJECT", - "describe": "reject loopback" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "--state ESTABLISHED,RELATED -j ACCEPT", - "replace": "--state ESTABLISHED,RELATED -j REJECT", - "describe": "reject established flows" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "--uid-owner 0 -j ACCEPT", - "replace": "--uid-owner 0 -j REJECT", - "describe": "cut off root" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "-p udp --dport 53 -j ACCEPT", - "replace": "-p udp --dport 5353 -j ACCEPT", - "describe": "allow the wrong DNS port" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "--uid-owner 10000-19999 -j REJECT", - "replace": "--uid-owner 10000-19999 -j ACCEPT", - "describe": "let every app through instead of cutting it off" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "case \"$uid\" in ''|*[!0-9]*) continue ;; esac", - "replace": ":", - "describe": "pass non-numeric uids to iptables" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "$1 == p {", - "replace": "$1 != p {", - "describe": "invert the package-to-uid match" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "mv \"$CURFEW_NET_UID_CACHE.tmp\" \"$CURFEW_NET_UID_CACHE\"", - "replace": "cp \"$CURFEW_NET_UID_CACHE.tmp\" \"$CURFEW_NET_UID_CACHE\"", - "describe": "leave the uid cache temp file behind" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "rebuilds=$((rebuilds + 1))", - "replace": ":", - "describe": "stop counting net_hold rebuilds" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "[ \"${CURFEW_NET_ENABLED:-0}\" = \"1\" ] || return 0", - "replace": ":", - "describe": "ignore the net-layer disable switch" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "NET_BUILT=1", - "replace": ":", - "describe": "never mark the chain as built" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "iptw \"$ipt\" -X \"$CURFEW_NET_IPT_CHAIN\" 2>/dev/null || true", - "replace": ":", - "describe": "leave the chain behind on teardown" - }, - { - "file": "phone_focus_mode/curfew_net.sh", - "find": "rm -f \"$map\"", - "replace": ":", - "describe": "leak the uid map temp file" - } - ] -} diff --git a/meta/scripts/fixtures/mutations/daemon_libs.json b/meta/scripts/fixtures/mutations/daemon_libs.json deleted file mode 100644 index ab3bdbc7..00000000 --- a/meta/scripts/fixtures/mutations/daemon_libs.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "test": "phone_focus_mode/lib/tests/test_daemon_libs.sh", - "timeout": 300, - "mutations": [ - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "R = 6371000.0", - "replace": "R = 1.0", - "describe": "wrong Earth radius in the Haversine (awk body kcov cannot measure)" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "sdlat * sdlat + cos(lat1) * cos(lat2) * sdlon * sdlon", - "replace": "sdlat * sdlat", - "describe": "drop the longitude term from the Haversine" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "PI / 180.0", - "replace": "PI", - "describe": "skip the degrees-to-radians conversion" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "head -1", - "replace": "tail -1", - "describe": "take the stalest GPS fix instead of the freshest" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "[0-9]{4,}", - "replace": "[0-9]{1,}", - "describe": "accept imprecise coordinates as a real fix" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "[ \"$now\" -ge \"$start\" ] || [ \"$now\" -lt \"$end\" ]", - "replace": "[ \"$now\" -ge \"$start\" ] && [ \"$now\" -lt \"$end\" ]", - "describe": "break the midnight-wrapping curfew window" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "[ \"$now\" -ge \"$start\" ] && [ \"$now\" -lt \"$end\" ]\n\telse", - "replace": "[ \"$now\" -gt \"$start\" ] && [ \"$now\" -lt \"$end\" ]\n\telse", - "describe": "exclude the start minute of a same-day window" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "'' | *[!0-9]*) return 1 ;;", - "replace": "'' | *[!0-9]*) return 0 ;;", - "describe": "fail CLOSED on a broken clock instead of open" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "while [ \"${n#0}\" != \"$n\" ] && [ \"${#n}\" -gt 1 ]; do n=\"${n#0}\"; done", - "replace": ":", - "describe": "stop stripping leading zeros (octal parse)" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "[ ! -e \"$CURFEW_OVERRIDE_FILE\" ]", - "replace": "[ -e \"$CURFEW_OVERRIDE_FILE\" ]", - "describe": "invert the curfew override" - }, - { - "file": "phone_focus_mode/daemon_location.sh", - "find": "_CURFEW_TICK_CACHED=1", - "replace": ":", - "describe": "never set the per-tick curfew memo" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "[ \"${n:-0}\" -lt 30 ]", - "replace": "[ \"${n:-0}\" -lt 0 ]", - "describe": "never warn about a truncated whitelist" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "[ \"${n:-0}\" -lt 10 ]", - "replace": "[ \"${n:-0}\" -lt 0 ]", - "describe": "never warn about a truncated night whitelist" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "grep -v '^[[:space:]]*#'", - "replace": "cat", - "describe": "keep comment lines as whitelist entries" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "cut -d/ -f1", - "replace": "cat", - "describe": "record the IME component instead of its package" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "[ \"$sms\" != \"null\" ]", - "replace": "true", - "describe": "record a literal null as the SMS handler" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "grep -qxF", - "replace": "grep -qF", - "describe": "match default handlers by substring instead of exactly" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "sort -u \"$tmp\" -o \"$f\"", - "replace": ":", - "describe": "never write the default-handler file" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "if curfew_active; then cf=1; else cf=0; fi", - "replace": "cf=0", - "describe": "always report curfew inactive in the snapshot" - }, - { - "file": "phone_focus_mode/daemon_state.sh", - "find": "\"$DISABLED_APPS_FILE\" 2>/dev/null | tr -d ' ' || echo 0", - "replace": "/dev/null 2>/dev/null | tr -d ' ' || echo 0", - "describe": "always report zero disabled apps" - }, - { - "file": "phone_focus_mode/daemon_apps.sh", - "find": "list=\"$STATE_DIR/night_whitelist.txt\"", - "replace": "list=\"$STATE_DIR/whitelist.txt\"", - "describe": "use the permissive day list during the night curfew" - }, - { - "file": "phone_focus_mode/daemon_apps.sh", - "find": "if grep -qxF \"$pkg\" \"$list\" 2>/dev/null; then", - "replace": "if grep -qF \"$pkg\" \"$list\" 2>/dev/null; then", - "describe": "match the whitelist by substring instead of exactly" - }, - { - "file": "phone_focus_mode/daemon_apps.sh", - "find": "\"$prefix\"*) return 0 ;;", - "replace": "*\"$prefix\"*) return 0 ;;", - "describe": "match sysprotect anywhere instead of as a prefix" - }, - { - "file": "phone_focus_mode/daemon_apps.sh", - "find": "is_default_handler \"$pkg\" && return 0", - "replace": ":", - "describe": "drop the default-handler safety net" - }, - { - "file": "phone_focus_mode/daemon_apps.sh", - "find": "if ! curfew_active &&", - "replace": "if true &&", - "describe": "let the browser guard resurrect a browser during curfew" - }, - { - "file": "phone_focus_mode/daemon_apps.sh", - "find": "done <\"$tmp_pkgs\"", - "replace": "done /dev/null 2>&1 || true", - "replace": ":", - "describe": "skip reinstalling for user 0 before enabling" - } - ] -} diff --git a/meta/scripts/fixtures/mutations/hosts_libs.json b/meta/scripts/fixtures/mutations/hosts_libs.json deleted file mode 100644 index 8bb772d3..00000000 --- a/meta/scripts/fixtures/mutations/hosts_libs.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "test": "phone_focus_mode/lib/tests/test_hosts_libs.sh", - "timeout": 180, - "mutations": [ - { - "file": "phone_focus_mode/hosts_mount.sh", - "find": "[ -n \"$target_hash\" ] && [ \"$target_hash\" = \"$canonical_hash\" ]", - "replace": "true", - "describe": "call any target correctly mounted" - }, - { - "file": "phone_focus_mode/hosts_mount.sh", - "find": "[ -n \"$target_hash\" ] && ", - "replace": "", - "describe": "treat a missing target as mounted" - }, - { - "file": "phone_focus_mode/hosts_mount.sh", - "find": "[ \"$attempts\" -ge 5 ]", - "replace": "[ \"$attempts\" -ge 500 ]", - "describe": "raise the unmount give-up threshold" - }, - { - "file": "phone_focus_mode/hosts_mount.sh", - "find": "mount -o remount,ro \"$system_mount\"", - "replace": ":", - "describe": "leave /system mounted read-write" - }, - { - "file": "phone_focus_mode/hosts_mount.sh", - "find": "chattr +i \"$HOSTS_TARGET\"", - "replace": ":", - "describe": "never re-lock the hosts target" - }, - { - "file": "phone_focus_mode/hosts_mount.sh", - "find": "log \"Bind mount reported success but target still mismatches - unmounting\"", - "replace": ":", - "describe": "silence the no-op bind detection" - }, - { - "file": "phone_focus_mode/hosts_mount.sh", - "find": "\n return 1\n}", - "replace": "\n return 0\n}", - "describe": "claim success when nothing fixed the target" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "if [ \"$module_hash\" != \"$canonical_hash\" ]; then", - "replace": "if true; then", - "describe": "rewrite the module on every poll" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "\n protect_magisk_module\n}", - "replace": "\n :\n}", - "describe": "stop re-locking the module directory" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "for marker in disable remove update; do", - "replace": "for marker in disable; do", - "describe": "ignore the remove and update markers" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "removed=$((removed + 1))", - "replace": ":", - "describe": "stop counting removed markers" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "chattr +i \"$module_dir\"", - "replace": ":", - "describe": "never lock the module directory" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "[ -f \"${profile_dir}prefs.js\" ] || continue", - "replace": ":", - "describe": "write to non-profile directories" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "network.trr.mode\", 5", - "replace": "network.trr.mode\", 0", - "describe": "set the DoH pref to the wrong mode" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "chattr +i \"$HOSTS_CANONICAL\"", - "replace": ":", - "describe": "leave the canonical mutable" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "if [ \"$current_pid\" = \"$last_pid\" ]; then", - "replace": "if false; then", - "describe": "restart netd on every tick" - }, - { - "file": "phone_focus_mode/hosts_magisk.sh", - "find": "echo \"${new_pid:-$current_pid}\" > \"$stamp_file\"", - "replace": ":", - "describe": "never stamp the netd pid" - } - ] -} diff --git a/meta/scripts/fixtures/mutations/monitor.json b/meta/scripts/fixtures/mutations/monitor.json deleted file mode 100644 index d05bd6de..00000000 --- a/meta/scripts/fixtures/mutations/monitor.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "test": "phone_focus_mode/lib/tests/test_monitor.sh", - "timeout": 180, - "mutations": [ - { - "file": "phone_focus_mode/lib/monitor.sh", - "find": "escaped=${escaped//\\\"/\\\\\\\"}", - "replace": ":", - "describe": "drop the JSON quote escaping" - }, - { - "file": "phone_focus_mode/lib/monitor.sh", - "find": "escaped=${escaped//$'\\n'/\\\\n}", - "replace": ":", - "describe": "drop the newline escaping" - }, - { - "file": "phone_focus_mode/lib/monitor.sh", - "find": "(( missing_count >= FORMAT_DETECTION_MIN_MISSING ))", - "replace": "(( missing_count > FORMAT_DETECTION_MIN_MISSING ))", - "describe": "off-by-one the format threshold" - }, - { - "file": "phone_focus_mode/lib/monitor.sh", - "find": "adb_root_shell \"kill -0 ${pid} >/dev/null 2>&1\" >/dev/null 2>&1 || return 1", - "replace": "adb_root_shell \"kill -0 ${pid} >/dev/null 2>&1\" >/dev/null 2>&1 || return 0", - "describe": "make pid_matches_script always succeed" - }, - { - "file": "phone_focus_mode/lib/monitor.sh", - "find": "printf '%s' \"${candidate}\"", - "replace": "printf ''", - "describe": "make hosts candidate resolution return empty" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_health.sh", - "find": "status=\"warn\"", - "replace": "status=\"ok\"", - "describe": "silence every health warning" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_health.sh", - "find": "status=\"fatal\"", - "replace": "status=\"ok\"", - "describe": "silence the format-indicator fatal" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_health.sh", - "find": "(( level < BATTERY_WARN_BELOW ))", - "replace": "(( level > BATTERY_WARN_BELOW ))", - "describe": "invert the battery threshold" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_health.sh", - "find": "(( free_mb < STORAGE_WARN_BELOW_MB ))", - "replace": "(( free_mb > STORAGE_WARN_BELOW_MB ))", - "describe": "invert the storage threshold" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_health.sh", - "find": "free_mb=$((free_kb / 1024))", - "replace": "free_mb=$((free_kb / 512))", - "describe": "wrong KB-to-MB divisor" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_health.sh", - "find": "\"${daemon_name} is NOT running\" \"true\"", - "replace": "\"${daemon_name} is NOT running\" \"false\"", - "describe": "mark a dead daemon unrepairable" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_policy.sh", - "find": "\"hosts_integrity\" \"fatal\"", - "replace": "\"hosts_integrity\" \"ok\"", - "describe": "call a missing canonical hosts file ok" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_policy.sh", - "find": "\"${expected_hash}\" == \"${actual_hash}\"", - "replace": "\"${expected_hash}\" != \"${actual_hash}\"", - "describe": "invert the hosts hash comparison" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_policy.sh", - "find": "chain_present=\"yes\"", - "replace": "chain_present=\"no\"", - "describe": "never observe the DNS chain" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_policy.sh", - "find": "\"launcher_state\" \"fatal\"", - "replace": "\"launcher_state\" \"warn\"", - "describe": "downgrade the missing-launcher fatal" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_policy.sh", - "find": "\"launcher_state\" \"error\"", - "replace": "\"launcher_state\" \"ok\"", - "describe": "call a launcher mismatch ok" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_policy.sh", - "find": "\"boot_persistence\" \"fatal\"", - "replace": "\"boot_persistence\" \"ok\"", - "describe": "call a missing boot script ok" - }, - { - "file": "phone_focus_mode/lib/monitor_checks_policy.sh", - "find": "\"companion_app\" \"warn\"", - "replace": "\"companion_app\" \"ok\"", - "describe": "call a missing companion app ok" - } - ] -} diff --git a/meta/shell-coverage-allowlist.txt b/meta/shell-coverage-allowlist.txt index c0f4a265..0115c3e4 100644 --- a/meta/shell-coverage-allowlist.txt +++ b/meta/shell-coverage-allowlist.txt @@ -77,12 +77,3 @@ meta/lib/lint_output.sh meta/scripts/lib/disk_cleanup_common.sh meta/scripts/lib/trace_prefix.sh meta/scripts/lib/trace_stubs.sh -phone_focus_mode/lib/adb_common.sh -phone_focus_mode/lib/adb_locking.sh -phone_focus_mode/lib/adb_trusted.sh -phone_focus_mode/lib/backup_capture.sh -phone_focus_mode/lib/backup.sh -phone_focus_mode/lib/monitor_checks_health.sh -phone_focus_mode/lib/monitor_checks_policy.sh -phone_focus_mode/lib/monitor.sh -phone_focus_mode/lib/restore.sh diff --git a/phone_focus_mode/README.md b/phone_focus_mode/README.md deleted file mode 100644 index 0df08a8e..00000000 --- a/phone_focus_mode/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# 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` - -```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: - -```bash -./deploy.sh --find-pkg stronglift -./deploy.sh --find-pkg anki -./deploy.sh --find-pkg pomodoro -``` - -Then add the correct package name to `WHITELIST` in `config.sh`. - -### 4. Deploy - -```bash -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 - -```bash -./deploy.sh --status # Current mode, location, distance from home -./deploy.sh --log # View recent daemon log -./deploy.sh --list # List all apps + whitelist status -./deploy.sh --enable # Force focus mode ON (for testing) -./deploy.sh --disable # Force focus mode OFF -./deploy.sh --stop # Stop daemon entirely (restores all apps) -./deploy.sh --start # Start daemon -./deploy.sh --restart # Restart daemon (picks up config changes) -./deploy.sh --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): - -```sh -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: - -```bash -./deploy.sh --hosts-status -./deploy.sh --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. - -- [Night curfew and the hotspot/tethering block](docs/night-curfew.md) -- [Updating and troubleshooting](docs/updating-and-troubleshooting.md) diff --git a/phone_focus_mode/backup_manifest.sh b/phone_focus_mode/backup_manifest.sh deleted file mode 100755 index 4d1c9dbc..00000000 --- a/phone_focus_mode/backup_manifest.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash -# backup_manifest.sh — Declarative backup and restore scope definition. -# Source this file; do not execute directly. -# Fields: name|source_path|restore_policy|requires_root|integrity_check -# -# restore_policy values: -# safe_restore — may be restored automatically -# manual_only — backed up but never restored without operator action -# backup_only — backed up but restore is not yet implemented - -set -euo pipefail - -FORMAT_DETECTION_MIN_MISSING=2 - -PHONE_BACKUP_ROOT="${PHONE_BACKUP_ROOT:-${HOME}/phone_backups}" - -APK_ITEMS=( - "com.qqlabs.minimalistlauncher|safe_restore|no|yes" - "com.kuhy.focusstatus|safe_restore|no|yes" -) - -APP_DATA_ITEMS=( - "com.beemdevelopment.aegis|/data/data/com.beemdevelopment.aegis|manual_only|yes|yes" -) - -MEDIA_ITEMS=( - "photos|/sdcard/DCIM|safe_restore|no|no" - "downloads|/sdcard/Download|safe_restore|no|no" - "documents|/sdcard/Documents|safe_restore|no|no" -) - -SECURITY_STATE_FILES=( - "/data/adb/service.d/99-focus-mode.sh" - "/data/local/tmp/focus_mode/hosts.canonical" - "/data/local/tmp/focus_mode/focus_state" -) - -FORMAT_INDICATORS=( - "Magisk boot script|test -f /data/adb/service.d/99-focus-mode.sh" - "Focus mode data dir|test -d /data/local/tmp/focus_mode" - "Focus mode state dir|test -d /data/local/tmp/focus_mode" - "Focus companion app installed|pm list packages -e com.kuhy.focusstatus | grep -q com.kuhy.focusstatus" -) - -BATTERY_WARN_BELOW=20 -STORAGE_WARN_BELOW_MB=500 -COOLDOWN_AUTO_SECS=300 -HISTORY_KEEP_DAYS=30 - -backup_manifest_validate() { - local apk_entry="" - local app_data_entry="" - local indicator_entry="" - local media_entry="" - local security_file="" - - : "${PHONE_BACKUP_ROOT}" - : "${FORMAT_DETECTION_MIN_MISSING}" - : "${BATTERY_WARN_BELOW}" - : "${STORAGE_WARN_BELOW_MB}" - : "${COOLDOWN_AUTO_SECS}" - : "${HISTORY_KEEP_DAYS}" - - for apk_entry in "${APK_ITEMS[@]}"; do - [[ -n "${apk_entry}" ]] || return 1 - done - - for app_data_entry in "${APP_DATA_ITEMS[@]}"; do - [[ -n "${app_data_entry}" ]] || return 1 - done - - for media_entry in "${MEDIA_ITEMS[@]}"; do - [[ -n "${media_entry}" ]] || return 1 - done - - for security_file in "${SECURITY_STATE_FILES[@]}"; do - [[ -n "${security_file}" ]] || return 1 - done - - for indicator_entry in "${FORMAT_INDICATORS[@]}"; do - [[ -n "${indicator_entry}" ]] || return 1 - done -} - -backup_manifest_validate diff --git a/phone_focus_mode/batch3_bloatware_uninstall.sh b/phone_focus_mode/batch3_bloatware_uninstall.sh deleted file mode 100755 index 156cfaba..00000000 --- a/phone_focus_mode/batch3_bloatware_uninstall.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/bin/bash -# ============================================================ -# BL-9000 Bloatware Uninstall -# -# Removes Blackview OEM bloatware and specified Google apps -# using `pm uninstall --user 0` (soft-remove, reversible via -# `pm install-existing --user 0 `). No reboots between -# packages — one optional reboot at the end. -# -# Usage: -# ./batch3_bloatware_uninstall.sh [--list] [--reboot] -# -# --list Dry-run: show which packages would be removed. -# --reboot Reboot the phone after all removals. -# -# Device selection (pick one): -# ADB_SERIAL=BL9000EEA0000102 ./batch3_bloatware_uninstall.sh -# PHONE_IP=192.168.1.x ./batch3_bloatware_uninstall.sh -# ============================================================ - -set -euo pipefail - -DRY_RUN=0 -DO_REBOOT=0 - -for arg in "$@"; do - case "$arg" in - --list) DRY_RUN=1 ;; - --reboot) DO_REBOOT=1 ;; - *) echo "Unknown flag: $arg"; exit 1 ;; - esac -done - -# ---- Device targeting (mirrors deploy.sh) ---- -ADB_TARGET=() -if [[ -n "${ADB_SERIAL:-}" ]]; then - ADB_TARGET=(-s "${ADB_SERIAL}") -elif [[ -n "${PHONE_IP:-}" ]]; then - echo "Connecting to ${PHONE_IP}:5555 ..." - adb connect "${PHONE_IP}:5555" - ADB_TARGET=(-s "${PHONE_IP}:5555") -fi - -adb_cmd() { adb "${ADB_TARGET[@]}" "$@"; } - -# Requires --mount-master so the command runs in the global mount namespace. -adb_root() { - printf '%s\n' "$1" | adb_cmd shell su --mount-master -c "sh -s" -} - -# ============================================================ -# PACKAGES TO REMOVE -# -# All removed with `pm uninstall --user 0` — the APK stays in -# /system so a factory reset or `pm install-existing` restores -# it. Safe to run even if a package is absent (skipped). -# ============================================================ - -# ---- Blackview OEM bloatware (BL-9000 confirmed packages) ---- -BV_BLOATWARE=( - com.blackview.apkupgrade # Blackview OTA updater - com.blackview.bvworkspace # BV desktop workspace - com.blackview.childmode # Child mode - com.blackview.cplog # CPU/hardware logger - com.blackview.darkmode.one # Dark mode theme variant - com.blackview.darkmode.two - com.blackview.darkmode.three - com.blackview.easytrans # BV easy-transfer tool - com.blackview.filetrans # BV file-transfer tool - com.blackview.focusmode # BV own focus mode (replaced by ours) - com.blackview.frozenapp # App freezer - com.blackview.gamemode # Game mode panel - com.blackview.health # BV health tracker - com.blackview.helper # BV AI assistant / helper - # NOTE: com.blackview.launcher is intentionally NOT removed here. - # Blackview embeds com.android.quickstep.RecentsActivity inside this APK — - # removing it kills the "swipe up for recent apps" gesture system-wide. - # Focus mode already disables it as a HOME competitor via LAUNCHER_COMPETITORS. - # com.blackview.launcher - # com.blackview.launcher.overlay.framework - com.blackview.leftscreen # Left swipe panel - com.blackview.notebook # BV notes app - com.blackview.qrcode # QR scanner (camera does it natively) - com.blackview.reversepay # Reverse wireless charging pay - com.blackview.smscode # SMS code extractor - com.blackview.systemmanager # BV system manager - com.blackview.theme.color.mode0 # Color themes (8 variants) - com.blackview.theme.color.mode1 - com.blackview.theme.color.mode2 - com.blackview.theme.color.mode3 - com.blackview.theme.color.mode4 - com.blackview.theme.color.mode5 - com.blackview.theme.color.mode6 - com.blackview.theme.color.mode7 - com.blackview.theme.config - com.blackview.theme.icon.clearwave # Icon themes - com.blackview.theme.icon.oil - com.blackview.tool # BV diagnostic tool - com.blackview.userfeedback # BV telemetry / feedback - com.blackview.wallpaper # BV wallpaper collection - com.blackview.wallpaperpicker - com.blackview.wallpaperpicker.overlay - com.blackview.weather # BV weather widget -) - -# ---- Google apps explicitly requested for removal ---- -GOOGLE_REMOVE=( - com.android.chrome # Google Chrome (Firefox is whitelisted) - com.google.android.youtube # YouTube app (hosts-blocked anyway) - com.google.android.apps.youtube.music # YouTube Music -) - -ALL_PACKAGES=("${BV_BLOATWARE[@]}" "${GOOGLE_REMOVE[@]}") - -# ============================================================ -# Main -# ============================================================ -echo "============================================================" -if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY RUN — packages that would be removed:" -else - echo "BL-9000 bloatware removal" -fi -echo "============================================================" - -echo "" -echo "[1] Verifying device connection..." -if ! adb_cmd get-state >/dev/null 2>&1; then - echo "ERROR: No ADB device reachable." - echo " Set ADB_SERIAL= or PHONE_IP= and retry." - exit 1 -fi - -echo "[2] Verifying root..." -if ! adb_root "id" 2>/dev/null | grep -q "uid=0"; then - echo "ERROR: Root shell failed. Ensure Magisk is installed." - exit 1 -fi -echo " Root confirmed." -echo "" - -removed=0 -skipped=0 -errors=0 - -for pkg in "${ALL_PACKAGES[@]}"; do - # Check presence - if ! adb_cmd shell pm list packages 2>/dev/null | grep -qx "package:${pkg}"; then - printf ' %-55s [not installed]\n' "$pkg" - (( skipped++ )) || true - continue - fi - - if [[ $DRY_RUN -eq 1 ]]; then - printf ' %-55s [would remove]\n' "$pkg" - (( removed++ )) || true - continue - fi - - printf ' Removing %-48s ... ' "$pkg" - result="$(adb_cmd shell pm uninstall --user 0 "$pkg" 2>&1 || true)" - if echo "$result" | grep -qi "success"; then - echo "OK" - (( removed++ )) || true - else - echo "FAILED (${result})" - (( errors++ )) || true - fi -done - -echo "" -echo "============================================================" -if [[ $DRY_RUN -eq 1 ]]; then - echo "Dry-run complete: ${removed} would be removed, ${skipped} not installed." - echo "Run without --list to apply." -else - echo "Done: ${removed} removed, ${skipped} skipped (not installed), ${errors} errors." - if [[ $DO_REBOOT -eq 1 ]]; then - echo "Rebooting phone..." - adb_cmd reboot - else - echo "Run with --reboot to reboot now, or reboot manually." - fi -fi -echo "============================================================" diff --git a/phone_focus_mode/config.sh b/phone_focus_mode/config.sh deleted file mode 100755 index e6c1ab5e..00000000 --- a/phone_focus_mode/config.sh +++ /dev/null @@ -1,250 +0,0 @@ -#!/system/bin/sh -# Focus Mode Configuration. Set HOME_LAT/HOME_LON in config_secrets.sh before -# deploying (Google Maps: right-click your apartment -> coords). -# -# The lists below stay in THIS file: focus_policy's loader finds them by -# regex over config.sh's own text, so one moved into a config_*.sh sibling -# parses as empty with no error. Rationale lives in docs/policy-lists.md. - -# --- Home location (loaded from config_secrets.sh, not tracked by git) --- -SCRIPT_DIR="${FOCUS_MODE_SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}" -# If config.sh is sourced from an external wrapper (e.g. Magisk service.d), -# $0 points to the wrapper path rather than this file's directory. Fall back -# to the canonical runtime location if config_secrets is not alongside $0. -if [ ! -f "$SCRIPT_DIR/config_secrets.sh" ] && [ -f "/data/local/tmp/focus_mode/config_secrets.sh" ]; then - SCRIPT_DIR="/data/local/tmp/focus_mode" -fi -# config_secrets.sh is deliberately untracked (it holds secrets, see -# .gitignore) and is created per-machine / pushed to the phone at deploy -# time. It does not exist in a clean checkout, so the linter cannot follow -# it there -- which is exactly what CI lints. -# shellcheck disable=SC1091 -. "$SCRIPT_DIR/config_secrets.sh" - -# --- Radius in meters --- -export RADIUS=150 - -# --- Hysteresis buffer in meters (prevents rapid toggling at boundary) --- -export HYSTERESIS=30 - -# shellcheck source=config_paths.sh -. "$SCRIPT_DIR/config_paths.sh" - -# NIGHT CURFEW (time-gated strict allow-list). Times are local 24h HHMM; -# the window wraps past midnight when START > END (e.g. 2300 -> 0500). -# see docs/policy-lists.md#night-curfew -export NIGHT_CURFEW_ENABLED=1 -export NIGHT_CURFEW_START="2300" -export NIGHT_CURFEW_END="0500" - -# shellcheck source=config_curfew.sh -. "$SCRIPT_DIR/config_curfew.sh" - -# shellcheck source=config_tether.sh -. "$SCRIPT_DIR/config_tether.sh" - -# Domains unblocked while a workout is in progress. Used by deploy.sh to -# generate $HOSTS_CANONICAL_WORKOUT (each line becomes a `0.0.0.0 ` -# match that is stripped from the canonical) and by focus_ctl.sh status. -# Comments and blank lines ignored. Keep entries lower-case. -export WORKOUT_UNBLOCK_DOMAINS=" -youtube.com -www.youtube.com -m.youtube.com -youtu.be -youtubei.googleapis.com -youtube.googleapis.com -youtube-nocookie.com -www.youtube-nocookie.com -googlevideo.com -ytimg.com -i.ytimg.com -s.ytimg.com -yt3.ggpht.com -yt3.googleusercontent.com -i9.ytimg.com -" - -# shellcheck source=config_dns.sh -. "$SCRIPT_DIR/config_dns.sh" - -# Browsers to force-stop when the hosts file is updated or restored. -# Force-stopping clears the in-process DNS cache so the next launch -# consults the system resolver (which sees our /etc/hosts blocks). -# Packages not installed on the device are silently skipped. -export BROWSER_PACKAGES=" -org.mozilla.fenix -com.android.chrome -" - -# --- Launcher enforcer state (see launcher_enforcer.sh) --- -# Keeps Minimalist Phone installed and locked as the default HOME app. -# The APK is snapshotted by `deploy.sh --snapshot-launcher` from the -# currently-installed copy (user installs once via Aurora/Play). -# The Pixel 6a's stock launcher. The previous value named the rooted -# Blackview's minimalist launcher, which is not installed here -- the exporter's -# launcher check caught it, which is the whole reason that check exists: hiding -# the launcher leaves the device with no home screen and no way back to the -# enforcer app. -export LAUNCHER_PACKAGE="com.google.android.apps.nexuslauncher" -# shellcheck source=config_launcher.sh -. "$SCRIPT_DIR/config_launcher.sh" - -# WHITELISTED APPS -- always enabled, even in focus mode. -# see docs/policy-lists.md#whitelisted-apps - -export WHITELIST=" -# Rewritten 2026-08-11 for the unrooted Pixel 6a under Device Owner. The list -# it replaced described the rooted Blackview and named apps this phone does not -# have. Everything absent from this list is hidden while at home, including -# every browser -- so no link opens at all until you leave the geofence. - -# --- Launcher (MUST be listed: hiding it leaves no home screen) --- -com.google.android.apps.nexuslauncher - -# --- The enforcer and its VPN provider (hiding either is unrecoverable) --- -com.kuhy.focus_owner -com.celzero.bravedns -com.zaneschepke.wireguardautotunnel - -# --- Phone, messaging, contacts (Polish UI: telefon / wiadomosci) --- -org.fossify.phone -org.fossify.messages -org.fossify.contacts -com.facebook.orca - -# --- Banking and identity (all device-paired over SMS; see docs) --- -pl.mbank -com.revolut.revolut -pl.infakt.infakt -pl.nask.mobywatel -com.kunzisoft.keepass.libre - -# --- kuhy's own apps --- -com.kuhy.diet_guard_app -com.kuhy.home_inventory -com.kuhy.lyricanki -com.kuhy.untools -com.kuhy.wake_alarm_sync -com.kuhy.workout_app -dev.kuhy.todo - -# --- Daily utility --- -com.google.android.calendar -com.google.android.deskclock -com.google.android.apps.maps -com.sosauce.cutecalc -com.ichi2.anki -com.metrolist.music -eu.kanade.tachiyomi.sy - -# com.android.vending -- day only. NEVER put a dollar-sign reference or a -# double quote in this string, not even in a comment: both break the list -# silently. see docs/policy-lists.md#why-the-play-store-is-in-the-day-list -com.android.vending - -# --- Workout tracking (always beneficial; must stay enabled to export runs) --- -org.runnerup -org.runnerup.free -" - -# ALLOWED PACKAGE PREFIXES -- why prefixes, and how narrow to keep them: -# see docs/policy-lists.md#allowed-package-prefixes - -export ALLOWED_PREFIXES=" -# Manga reader + its per-source extension apks. -eu.kanade.tachiyomi -" - -# Prefixes that survive the curfew as well. Must be a subset of -# $ALLOWED_PREFIXES, mirroring the NIGHT_WHITELIST/WHITELIST subset rule. -export NIGHT_ALLOWED_PREFIXES=" -eu.kanade.tachiyomi -" - -# NIGHT CURFEW WHITELIST -- what stays enabled at night and why, plus the -# prefix exception: see docs/policy-lists.md#night-curfew-whitelist - -export NIGHT_WHITELIST=" -# Curfew (23:00-05:00 at home) is strictly tighter than the day list: only what -# is needed to answer the phone, reach a bank, or handle an emergency. Rewritten -# 2026-08-11 alongside the day list. -com.kuhy.focus_owner -com.celzero.bravedns -com.google.android.apps.nexuslauncher -org.fossify.phone -org.fossify.messages -org.fossify.contacts -pl.mbank -com.revolut.revolut -pl.infakt.infakt -pl.nask.mobywatel -com.kunzisoft.keepass.libre -com.kuhy.wake_alarm_sync -com.google.android.deskclock -com.metrolist.music -org.runnerup -org.runnerup.free -# dev.kuhy.todo: deliberate loosening of the night rule. -# see docs/policy-lists.md#why-devkuhytodo-is-in-the-night-list -dev.kuhy.todo -" - -# ============================================================ -# BLOCKED SYSTEM APPS -# System apps that should be disabled in focus mode. -# These are NOT covered by third-party package blocking. - -# --- System / essential packages that must NEVER be disabled --- -# Prefix-matched. Why pl.infakt.infakt is here rather than allowlisted: -# see docs/policy-lists.md#system-packages-that-must-never-be-disabled -export SYSTEM_NEVER_DISABLE=" -pl.infakt.infakt -# The always-on VPN provider. Hiding it is self-defeating in the worst way: -# it is the network-level YouTube block, and with DISALLOW_CONFIG_VPN pinned -# to a hidden package the device can lose general connectivity rather than -# just the filter. Must outrank every enforcement branch. -com.celzero.bravedns -com.android.launcher -com.android.settings -com.android.systemui -com.android.phone -com.android.dialer -com.android.contacts -com.android.mms -com.android.messaging -com.android.providers -com.android.inputmethod -com.android.shell -com.android.permissioncontroller -com.android.bluetooth -com.android.nfc -com.android.wifi -com.android.certinstaller -com.android.vpndialogs -com.android.se -com.android.emergency -com.android.camera -com.android.documentsui -com.android.externalstorage -com.android.keychain -com.android.location -com.android.networkstack -com.android.captiveportallogin -com.google.android.gms -com.google.android.gsf -com.google.android.ext.services -com.google.android.ext.shared -com.google.android.webview -com.google.android.trichromelibrary -com.google.android.inputmethod.latin -com.google.android.setupwizard -com.google.android.permissioncontroller -com.google.android.deskclock -com.google.android.dialer -com.google.android.contacts -com.google.android.apps.messaging -android -com.mediatek -com.qualcomm -" diff --git a/phone_focus_mode/config_curfew.sh b/phone_focus_mode/config_curfew.sh deleted file mode 100755 index b57c119f..00000000 --- a/phone_focus_mode/config_curfew.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# config_curfew.sh — the night-curfew enforcer's settings: poll interval, log -# and state paths, the grayscale/DND/network feature switches, and the -# per-UID network chain. -# -# Sourced by config.sh where these definitions used to sit. -# -# The curfew WINDOW ($NIGHT_CURFEW_START/END/ENABLED) is deliberately NOT -# here: focus_policy's loader reads those out of config.sh's text. Only names -# the loader never reads may live in a sibling. - -# --- Curfew enforcer (grayscale + DND + per-UID network allow-list) --- -# See curfew_enforcer.sh. Always-on like dns_enforcer, but only ACTS while the -# curfew window is open AND focus mode is ON. Re-applies every interval so a -# manual toggle in Settings snaps back ("hard to turn off"). NOTE: snap-back is -# the realistic lock; true impossibility would mean blocking the Settings app, -# which risks system instability, so we deliberately do not. -export CURFEW_ENFORCER_INTERVAL=5 -export CURFEW_ENFORCER_LOG="$STATE_DIR/curfew_enforcer.log" -export CURFEW_ENFORCER_STATE="$STATE_DIR/curfew_applied" -# Grayscale: force the display to monochrome via the accessibility daltonizer. -export CURFEW_GRAYSCALE_ENABLED=1 -# DND: force Do-Not-Disturb to alarms-only so notifications stop pulling you in -# while the morning alarm still rings. -export CURFEW_DND_ENABLED=1 -# Per-UID internet allow-list. DEFAULT OFF: highest-risk layer, must be proven -# on-device (`focus_ctl.sh curfew-test-on`) before it is trusted to fire -# unattended at 23:00. When on, only $NIGHT_WHITELIST app UIDs (plus -# root/system/shell + DNS) get network; every other app is cut off. It is also -# largely redundant with the app-disable layer, so leaving it off is safe. -export CURFEW_NET_ENABLED=1 -export CURFEW_NET_IPT_CHAIN="FOCUS_CURFEW_NET" -# Android's netd periodically reasserts the whole `filter` table via -# iptables-restore, which atomically erases our custom chain (proven on-device: -# the chain vanishes ~every 5s). The main enforcer tick (5s) rebuilds it but -# leaves up-to-5s leak windows. This watchdog re-pins the chain every -# CURFEW_NET_REASSERT_INTERVAL seconds *from a cached UID list* (no pm fork). -# A 1s interval was measured on-device to peg netd at ~30% CPU continuously -# (an `iptables -L` fork every second, all night), overheating the phone - -# throttled to 15s (matches HOSTS_CHECK_INTERVAL/LAUNCHER_CHECK_INTERVAL -# pacing) as a deliberate tradeoff: leak window grows from <=1s to <=15s. -# The proper fix is netd's own per-UID firewall (ndc/eBPF), tracked as a -# follow-up; this remains an interim stopgap either way. -export CURFEW_NET_REASSERT_INTERVAL=15 -export CURFEW_NET_UID_CACHE="$STATE_DIR/curfew_net_uids.txt" -# Manual test toggle: `focus_ctl.sh curfew-test-on` writes this file to force -# curfew ACTIVE regardless of clock, so the whole stack can be validated during -# the day. `curfew-test-off` removes it. -export CURFEW_FORCE_FILE="$STATE_DIR/curfew_force_on" diff --git a/phone_focus_mode/config_dns.sh b/phone_focus_mode/config_dns.sh deleted file mode 100755 index f6053910..00000000 --- a/phone_focus_mode/config_dns.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# config_dns.sh — the DNS enforcer's settings: poll interval, log and iptables -# chain names, the trusted DoT host, and the DoH endpoint lists that -# dns_iptables.sh turns into reject rules. -# -# Sourced by config.sh where these definitions used to sit. -# -# Nothing python_pkg/focus_policy/loader.py reads may move into a sibling: it -# regex-scans the text of config.sh ALONE, so a name moved out becomes an -# empty value with no error at all. See the pinned list in config_paths.sh. - -# --- DNS enforcer state (see dns_enforcer.sh) --- -# The hosts file is only consulted by the *system* resolver. Apps using -# DNS-over-HTTPS (DoH, e.g. Chrome's built-in secure DNS) or DNS-over-TLS -# (DoT, e.g. Android 9+ Private DNS "opportunistic" mode) bypass it. -# The DNS enforcer pins Private DNS to OFF and blocks well-known DoH/DoT -# endpoints so lookups fall back to the system resolver -> hosts file. -export DNS_CHECK_INTERVAL=20 -export DNS_LOG="$STATE_DIR/dns_enforcer.log" -# --- Trusted DoT resolver (opt-in, empty = old behaviour) --- -# Set this to the hostname of a DoT resolver you control, and the enforcer -# switches from "no DoT at all" to "only YOUR DoT". Two things change: -# 1. an ACCEPT for that resolver's IPs is inserted BEFORE the blanket -# 853 REJECT, so the connection survives; -# 2. private_dns_mode is pinned to "hostname" with this specifier, instead -# of being forced off -- so the phone cannot silently fall back to a -# resolver that does not filter. -# Leave EMPTY to keep the original behaviour (Private DNS forced off, all -# 853 rejected). Do not point this at a public resolver: the whole point is -# that the resolver applies your blocklist. See python_pkg/focus_policy. -export DNS_TRUSTED_DOT_HOST="" -# Where to resolve DNS_TRUSTED_DOT_HOST when Private DNS is pinned to it. -# Chicken-and-egg: the resolver's own name must resolve without using it. -# Space-separated IPv4/IPv6 literals; leave empty to resolve at runtime. -export DNS_TRUSTED_DOT_IPS="" -# iptables chain used exclusively by us; we flush+refill it every check. -export DNS_IPT_CHAIN="FOCUS_DNS_BLOCK" -# DoH/DoT endpoints to DROP. Well-known public resolvers used by browsers -# and OS when Private DNS is enabled. Updating this list is cheap — just -# edit and redeploy. -export DNS_DOH_HOSTS=" -dns.google -dns64.dns.google -dns.quad9.net -dns.cloudflare.com -one.one.one.one -cloudflare-dns.com -mozilla.cloudflare-dns.com -chrome.cloudflare-dns.com -dns.nextdns.io -doh.opendns.com -dns.adguard-dns.com -dns.adguard.com -dns.controld.com -" -# IPv4/IPv6 literals used by DoT (port 853) and DoH (port 443). Anything -# not already resolved via /etc/hosts still needs literal-IP blocks. -export DNS_DOH_IPV4=" -8.8.8.8 -8.8.4.4 -1.1.1.1 -1.0.0.1 -9.9.9.9 -149.112.112.112 -94.140.14.14 -94.140.15.15 -208.67.222.222 -208.67.220.220 -45.90.28.0 -45.90.30.0 -104.16.248.249 -104.16.249.249 -" -export DNS_DOH_IPV6=" -2001:4860:4860::8888 -2001:4860:4860::8844 -2606:4700:4700::1111 -2606:4700:4700::1001 -2620:fe::fe -2620:fe::9 -2a10:50c0::ad1:ff -2a10:50c0::ad2:ff -2606:4700::6810:f8f9 -2606:4700::6810:f9f9 -" diff --git a/phone_focus_mode/config_launcher.sh b/phone_focus_mode/config_launcher.sh deleted file mode 100755 index 5cbb3fde..00000000 --- a/phone_focus_mode/config_launcher.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# config_launcher.sh — the minimalist-launcher enforcer's settings: where the -# APK and its hash live, which launchers count as competitors, and the poll -# interval and log path. -# -# Sourced by config.sh where these definitions used to sit. -# -# $LAUNCHER_PACKAGE is deliberately NOT here: focus_policy's loader reads it -# out of config.sh's text and would silently see an empty value instead. - -export LAUNCHER_APK="/data/adb/focus_mode/minimalist_launcher.apk" -export LAUNCHER_SHA_FILE="/data/adb/focus_mode/minimalist_launcher.sha256" -# Captured home-activity component (package/.Activity). Saved by -# --snapshot-launcher so the enforcer knows which component to pin as HOME. -export LAUNCHER_ACTIVITY_FILE="/data/adb/focus_mode/minimalist_launcher.activity" -# Competing launchers to disable so the "pick a launcher" dialog has -# nothing else to offer. Matched exactly; add more with `focus_ctl.sh -# launcher-disable-other `. -# com.blackview.launcher is intentionally excluded from LAUNCHER_COMPETITORS: -# Blackview embeds com.android.quickstep.RecentsActivity inside this APK, -# so disabling it kills the system-wide "swipe up for recent apps" gesture. -export LAUNCHER_COMPETITORS=" -com.android.launcher -com.android.launcher3 -com.google.android.apps.nexuslauncher -" -export LAUNCHER_CHECK_INTERVAL=15 -export LAUNCHER_LOG="$STATE_DIR/launcher_enforcer.log" diff --git a/phone_focus_mode/config_paths.sh b/phone_focus_mode/config_paths.sh deleted file mode 100755 index edef233b..00000000 --- a/phone_focus_mode/config_paths.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# config_paths.sh — the shared plumbing: poll intervals, the log file, the -# state/mode/status paths, the hosts-enforcer and workout-detector settings, -# and the boot-autostart gate. -# -# Sourced by config.sh where these definitions used to sit, after $STATE_DIR -# (which stays in config.sh, since every one of these paths is built from it). -# -# WHAT MUST NOT MOVE OUT OF config.sh -# ----------------------------------- -# python_pkg/focus_policy/loader.py builds the whole policy by running a regex -# over the TEXT OF config.sh ALONE (plus a sibling config_secrets.sh). A name -# moved into any of these config_*.sh siblings becomes invisible to it, and -# the loader does not fail -- it silently yields an empty set, which reads as -# "nothing is whitelisted". These fifteen therefore stay in config.sh: -# -# ALLOWED_PREFIXES BROWSER_PACKAGES HOME_LAT HOME_LON HYSTERESIS -# LAUNCHER_PACKAGE NIGHT_ALLOWED_PREFIXES NIGHT_CURFEW_ENABLED -# NIGHT_CURFEW_END NIGHT_CURFEW_START NIGHT_WHITELIST RADIUS -# SYSTEM_NEVER_DISABLE WHITELIST WORKOUT_UNBLOCK_DOMAINS -# -# Check before and after any change here -- the hash must not move: -# python3 -m python_pkg.focus_policy --config phone_focus_mode/config.sh \ -# | sha256sum - -# Every path below is built from this, so it leads the file. -# --- State file (tracks which apps were disabled by focus mode) --- -STATE_DIR="/data/local/tmp/focus_mode" - -# --- Location check interval in seconds --- -# When focus mode is ON (at home): check very frequently for near-instant -# detection of leaving home (phone is charging anyway). -# When focus mode is OFF (away): check less often to save battery. -export CHECK_INTERVAL_FOCUS=10 -export CHECK_INTERVAL_NORMAL=120 -# --- Log file --- -export LOG_FILE="/data/local/tmp/focus_mode/focus_mode.log" -export LOG_MAX_LINES=500 -export DISABLED_APPS_FILE="$STATE_DIR/disabled_by_focus.txt" -export MODE_FILE="$STATE_DIR/current_mode.txt" -# Status snapshot consumed by the companion notification app (focus_status_app). -# Written by focus_daemon.sh every loop iteration. Chmod 644 so apps can read. -export STATUS_FILE="$STATE_DIR/status.json" -# Trigger file: companion app (or user) touches this to request an immediate -# re-check. focus_daemon.sh polls for it and skips the remainder of its sleep. -export RECHECK_TRIGGER="$STATE_DIR/trigger_recheck" -# Escape hatch: if this file exists, curfew is suspended (treated as daytime) -# everywhere — app list, grayscale, DND and network. Delete it to re-arm. -# WHO CAN CREATE IT (recovery paths, strongest lock first): -# * `focus_ctl.sh curfew-off` over ADB from the PC (always available daily). -# * The Magisk boot emergency-disable file ($FOCUS_BOOT_EMERGENCY_DISABLE_FILE) -# stops the whole stack at next boot. -# * A root file-manager/terminal ONLY IF one is added to $NIGHT_WHITELIST. -# None is whitelisted by default — that is deliberate ("hard to turn off"). -# The active-IME guard in focus_daemon.sh keeps the keyboard alive so whichever -# path you choose is always typable. If you want a true no-PC 2am opt-out, -# whitelist a root terminal at night (see NIGHT_WHITELIST) or wire a companion- -# app button. Until then, recovery is PC/ADB-based by design. -export CURFEW_OVERRIDE_FILE="$STATE_DIR/curfew_override" -# --- Boot-time autostart safety gate --- -# Critical safety default: do NOT auto-start focus daemons at boot unless -# explicitly enabled. This avoids device instability during early boot on -# vendor ROMs after resets/updates. -# Late-auto mode: boot stack starts only after Android reports boot complete. -export FOCUS_BOOT_AUTOSTART=1 -# Extra grace period after sys.boot_completed. Keep at or below 10 seconds. -export FOCUS_BOOT_DELAY_SECONDS=10 -# Hard timeout while waiting for sys.boot_completed in Magisk service script. -export FOCUS_BOOT_WAIT_MAX_SECONDS=180 -# Emergency kill switch file: if this marker exists on phone, boot autostart -# is skipped entirely for this boot. Create with: -# adb shell su -c 'touch /data/local/tmp/focus_mode/disable_boot_autostart' -export FOCUS_BOOT_EMERGENCY_DISABLE_FILE="$STATE_DIR/disable_boot_autostart" -# --- Hosts enforcer state (see hosts_enforcer.sh) --- -# Canonical hosts file pushed by deploy.sh. The enforcer bind-mounts this -# over /system/etc/hosts and restores any tampering. -export HOSTS_CANONICAL="$STATE_DIR/hosts.canonical" -export HOSTS_TARGET="/system/etc/hosts" -export HOSTS_SHA_FILE="$STATE_DIR/hosts.sha256" -export HOSTS_CHECK_INTERVAL=15 -export HOSTS_LOG="$STATE_DIR/hosts_enforcer.log" -# Workout-variant canonical: same content as $HOSTS_CANONICAL but with all -# YouTube-related domain blocks removed. hosts_enforcer.sh switches to this -# variant when $WORKOUT_ACTIVE_FILE contains "1" (StrongLifts workout in -# progress) and switches back when it contains "0" or is missing. -export HOSTS_CANONICAL_WORKOUT="$STATE_DIR/hosts.canonical.workout" -export HOSTS_SHA_FILE_WORKOUT="$STATE_DIR/hosts.sha256.workout" -# Magisk Systemless Hosts module path. The enforcer keeps this in sync with -# the currently-active canonical so a fresh boot sees the right hosts file. -export HOSTS_MAGISK_MODULE_FILE="/data/adb/modules/hosts/system/etc/hosts" -# --- Workout detector (see workout_detector.sh) --- -# Polls the StrongLifts SQLite DB to determine whether a workout is in -# progress. A workout is "in progress" iff there is at least one row in -# `workouts` with start>0 AND (finish IS NULL OR finish=0). While true, -# YouTube is unblocked at the hosts level (see HOSTS_CANONICAL_WORKOUT). -export WORKOUT_DETECTOR_INTERVAL=10 -export WORKOUT_ACTIVE_FILE="$STATE_DIR/workout_active" -export WORKOUT_DETECTOR_LOG="$STATE_DIR/workout_detector.log" -# Static aarch64 sqlite3 binary pushed by deploy.sh. Built from the SQLite -# amalgamation against the Android NDK; ~1.6 MB. Stored outside the repo at -# ../testsAndMisc_binaries/phone_focus_mode/sqlite3 per binary-files policy. -export WORKOUT_SQLITE3_BIN="/data/local/tmp/focus_mode/sqlite3" -export WORKOUT_DB_PATH="/data/data/com.stronglifts.app/databases/StrongLifts-Database-3" -# StrongLifts package — must be in $WHITELIST so its DB stays writable while -# focus mode is enforcing. Used here only for status/log clarity. -export WORKOUT_TRIGGER_PACKAGE="com.stronglifts.app" -export BLOCKED_SYSTEM_APPS=" -# *** INTENTIONALLY EMPTY *** -# -# pm disable-user state persists across reboots. Android always kills daemon -# processes with SIGKILL during shutdown, bypassing the shell cleanup trap. -# Any system package left disabled across a reboot can trigger MTK bootloop -# protection → recovery → factory wipe (confirmed on BL9000). -# -# System/distraction apps are enforced via DNS+iptables in dns_enforcer.sh -# instead of persistent package-disable state. -" diff --git a/phone_focus_mode/config_tether.sh b/phone_focus_mode/config_tether.sh deleted file mode 100755 index 1986c148..00000000 --- a/phone_focus_mode/config_tether.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# config_tether.sh — the tethering enforcer's settings: the feature switch, -# poll interval, log and state paths, the offload setting it snapshots, its -# iptables chain, and the override/force hooks. -# -# Sourced by config.sh where these definitions used to sit. None of these is -# read by focus_policy's loader; see the note in config_paths.sh. - -# ============================================================ -# HOTSPOT / TETHERING BLOCK (closes the "second phone" bypass) -# ============================================================ -# The hosts file, dns_enforcer and the curfew net layer all only touch this -# phone's OWN traffic (system resolver + OUTPUT chain). A tethered second -# device's packets are FORWARDed + NAT'd through us on an unfiltered path, so -# it browses freely on our mobile data. tether_enforcer.sh closes that hole. -# Always-on like dns_enforcer, but only ACTS while focus mode is ON (i.e. you -# are at home: $MODE_FILE == "focus") OR the force-test file is present. Covers -# WiFi, USB and Bluetooth tethering, since all of it traverses FORWARD. -export TETHER_ENFORCER_ENABLED=1 -export TETHER_CHECK_INTERVAL=5 -export TETHER_LOG="$STATE_DIR/tether_enforcer.log" -export TETHER_ENFORCER_STATE="$STATE_DIR/tether_applied" -# Lever 1: disable Android's hardware/BPF tether offload so forwarded traffic -# is actually seen by netfilter (otherwise the FORWARD rule can be silently -# skipped). AOSP global key Settings.Global.TETHER_OFFLOAD_DISABLED; 1=disabled. -# Snapshotted on entry and restored on exit so we never clobber a user value. -export TETHER_OFFLOAD_KEY="tether_offload_disabled" -export TETHER_OFFLOAD_SNAP="$STATE_DIR/tether_offload.snap" -# Lever 2: blanket REJECT of the FORWARD chain (both iptables + ip6tables). -# Re-pinned only when tampered (chain_intact gate, like dns_enforcer) so we do -# not fork an `iptables -L` every second — that pegged netd + overheated the -# phone during curfew-net tuning (see CURFEW_NET_REASSERT_INTERVAL note above). -export TETHER_IPT_CHAIN="FOCUS_TETHER_BLOCK" -# Lever 3 (best-effort, WiFi only): actively stop a running softAP each tick so -# the hotspot toggle visibly flips off. Version-gated (cmd wifi stop-softap is -# Android 11+). The FORWARD block remains the version-independent catch-all. -export TETHER_STOP_SOFTAP_ENABLED=1 -# Escape hatches (mirrors the curfew override/force pair): -# tether_override -> suspend the block without stopping the daemon. -# tether_force_on -> force the block ACTIVE regardless of location (daytime -# on-device validation via `focus_ctl.sh tether-test-on`). -export TETHER_OVERRIDE_FILE="$STATE_DIR/tether_override" -export TETHER_FORCE_FILE="$STATE_DIR/tether_force_on" diff --git a/phone_focus_mode/ctl_curfew.sh b/phone_focus_mode/ctl_curfew.sh deleted file mode 100755 index a5c836aa..00000000 --- a/phone_focus_mode/ctl_curfew.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_curfew.sh — focus_ctl.sh's subcommands for the night-curfew enforcer: -# status, start, stop and log. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one -# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh -# globals these read. The pidfile path below lives here rather than in -# the entry script because this file is its only reader (SC2034), and -# the linter runs without -x, so each file stands alone. - -CURFEW_PIDFILE="$STATE_DIR/curfew_enforcer.pid" - -curfew_enforcer_pid() { - if [ -f "$CURFEW_PIDFILE" ]; then - local pid - pid="$(cat "$CURFEW_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi -} - -# Replicates focus_daemon.sh::is_curfew_now for status display. -_ctl_dec() { - local n="$1" - while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done - printf '%s' "$n" -} - -ctl_is_curfew_now() { - local now start end - now="$(date +%H%M 2>/dev/null)" - case "$now" in '' | *[!0-9]*) return 1 ;; esac - now="$(_ctl_dec "$now")" - start="$(_ctl_dec "$NIGHT_CURFEW_START")" - end="$(_ctl_dec "$NIGHT_CURFEW_END")" - if [ "$start" -le "$end" ]; then - [ "$now" -ge "$start" ] && [ "$now" -lt "$end" ] - else - [ "$now" -ge "$start" ] || [ "$now" -lt "$end" ] - fi -} - -cmd_curfew_status() { - local pid - pid="$(curfew_enforcer_pid)" - echo "=== Night Curfew Status ===" - echo "Enabled: ${NIGHT_CURFEW_ENABLED}" - echo "Window: ${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END} (now $(date +%H%M))" - if [ -n "$pid" ]; then - echo "Enforcer: RUNNING (PID $pid)" - else - echo "Enforcer: STOPPED" - fi - ctl_is_curfew_now && echo "Within window: YES" || echo "Within window: no" - [ -e "$CURFEW_FORCE_FILE" ] && echo "Forced ON: YES (demo/test active)" - [ -e "$CURFEW_OVERRIDE_FILE" ] && echo "Override: YES (curfew SUSPENDED)" - if [ -f "$MODE_FILE" ]; then - echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)" - fi - [ -e "$CURFEW_ENFORCER_STATE" ] && echo "Applied now: YES (grayscale/DND locked)" || - echo "Applied now: no" - echo "Grayscale: ${CURFEW_GRAYSCALE_ENABLED} DND: ${CURFEW_DND_ENABLED} Net: ${CURFEW_NET_ENABLED}" - if iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then - echo "iptables $CURFEW_NET_IPT_CHAIN: $(iptables -S "$CURFEW_NET_IPT_CHAIN" 2>/dev/null | wc -l) rules" - else - echo "iptables $CURFEW_NET_IPT_CHAIN: absent (net curfew not applied)" - fi - if [ -f "$STATE_DIR/night_whitelist.txt" ]; then - echo "Night whitelist: $(wc -l <"$STATE_DIR/night_whitelist.txt" | tr -d ' ') apps allowed" - fi -} - -cmd_curfew_start() { - local pid - pid="$(curfew_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Curfew enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/curfew_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(curfew_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Curfew enforcer started (PID $pid)" - else - echo "ERROR: curfew enforcer failed to start. Check log: $CURFEW_ENFORCER_LOG" - fi -} - -cmd_curfew_stop() { - local pid - pid="$(curfew_enforcer_pid)" - if [ -n "$pid" ]; then - kill "$pid" 2>/dev/null - echo "Curfew enforcer stopped (PID $pid) - daytime state restored" - else - echo "Curfew enforcer not running" - fi -} - -# Test hook: force curfew ACTIVE regardless of clock/location so the whole -# stack can be validated during the day. Daemon re-checks within one tick. -cmd_curfew_test_on() { - touch "$CURFEW_FORCE_FILE" - chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true - rm -f "$CURFEW_OVERRIDE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew FORCED ON. App sweep + enforcer will engage within a few seconds." - echo "Validate: open mBank (works), keyboard (works), Firefox (gone). Then: curfew-test-off" -} - -cmd_curfew_test_off() { - rm -f "$CURFEW_FORCE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew force cleared. Back to clock-based behaviour." -} - -# Demo mode = the same force mechanism as the test hook, worded for an -# on-demand demo. The companion app's Start/Stop demo button drives the same -# force file. Easy off: curfew-demo-off (or tap "Stop demo curfew"). -cmd_curfew_demo_on() { - touch "$CURFEW_FORCE_FILE" - chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true - rm -f "$CURFEW_OVERRIDE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Demo curfew STARTED - full curfew engaged now (apps, grayscale, DND, net)." - echo "Stop any time with: curfew-demo-off (or tap 'Stop demo curfew')" -} - -cmd_curfew_demo_off() { - rm -f "$CURFEW_FORCE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Demo curfew STOPPED - back to clock-based behaviour." -} - -# Escape hatch: suspend curfew now (the 2am 'let me out' button). Survives -# until you re-arm. Reachable on-device only via ADB (this command) unless a -# root file-manager/terminal has been added to NIGHT_WHITELIST. -cmd_curfew_off() { - touch "$CURFEW_OVERRIDE_FILE" - chmod 666 "$CURFEW_OVERRIDE_FILE" 2>/dev/null || true - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew SUSPENDED (override set: $CURFEW_OVERRIDE_FILE). Re-arm with: curfew-on" -} - -cmd_curfew_on() { - rm -f "$CURFEW_OVERRIDE_FILE" - touch "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Curfew re-armed (override cleared)." -} diff --git a/phone_focus_mode/ctl_daemon.sh b/phone_focus_mode/ctl_daemon.sh deleted file mode 100755 index 58a56dcf..00000000 --- a/phone_focus_mode/ctl_daemon.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_daemon.sh — focus_ctl.sh's subcommands for the main focus daemon: -# start, stop, status, enable, disable, recheck, log and the companion -# notification status. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory. -# Sourced by focus_ctl.sh, which owns log() and the config.sh globals these -# read. $PIDFILE lives here rather than in the entry script because this file -# is its only reader (SC2034), and the linter runs without -x so each file -# stands alone. - -PIDFILE="$STATE_DIR/daemon.pid" - -# Helper to check if daemon is running -daemon_pid() { - if [ -f "$PIDFILE" ]; then - local pid - pid="$(cat "$PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi -} - -cmd_start() { - local pid - pid="$(daemon_pid)" - if [ -n "$pid" ]; then - echo "Daemon already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/focus_daemon.sh" /dev/null 2>&1 & - sleep 2 - pid="$(daemon_pid)" - if [ -n "$pid" ]; then - echo "Daemon started (PID $pid)" - else - echo "ERROR: Daemon failed to start. Check log: $LOG_FILE" - fi -} - -cmd_stop() { - local pid - pid="$(daemon_pid)" - if [ -z "$pid" ]; then - echo "Daemon not running" - # Clean up stale pidfile if present - rm -f "$PIDFILE" - else - kill -TERM "$pid" - echo "Daemon stopped (sent SIGTERM to PID $pid)" - fi -} - -cmd_status() { - local pid - pid="$(daemon_pid)" - local mode="unknown" - [ -f "$MODE_FILE" ] && mode="$(cat "$MODE_FILE")" - - echo "=== Focus Mode Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Mode: $mode" - echo "Home: $HOME_LAT, $HOME_LON (radius: ${RADIUS}m)" - echo "" - - # Show current location if available - location="$(dumpsys location 2>/dev/null | - grep -oE 'Location\[.*[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' | - grep -oE '[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' | - head -1)" - - if [ -n "$location" ]; then - lat="$(echo "$location" | cut -d',' -f1)" - lon="$(echo "$location" | cut -d',' -f2)" - dist="$(echo "$lat $lon $HOME_LAT $HOME_LON" | awk '{ - PI=3.14159265358979; R=6371000 - a1=$1*PI/180; o1=$2*PI/180 - a2=$3*PI/180; o2=$4*PI/180 - da=a2-a1; dlon=o2-o1 - x=sin(da/2)^2+cos(a1)*cos(a2)*sin(dlon/2)^2 - printf "%d", R*2*atan2(sqrt(x),sqrt(1-x)) - }')" - echo "Location: $lat, $lon" - echo "Distance: ${dist}m from home" - else - echo "Location: unavailable" - fi - - echo "" - if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then - echo "=== Apps disabled by focus mode ===" - cat "$DISABLED_APPS_FILE" - else - echo "No apps currently disabled by focus mode" - fi -} - -cmd_enable() { - # Disable daemon temporarily, force focus - echo "Forcing focus mode ON..." - . "$SCRIPT_DIR/config.sh" - - # Source common functions - inline here for standalone use - : >"$STATE_DIR/disabled_by_focus.txt" - local count=0 - for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do - # Check whitelist - whitelisted=0 - for w in $(iter_whitelist_packages); do - w_clean="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w_clean" ] && continue - [ "$pkg" = "$w_clean" ] && { - whitelisted=1 - break - } - done - [ "$whitelisted" -eq 1 ] && continue - - # Check system protection - protected=0 - for prefix in $SYSTEM_NEVER_DISABLE; do - prefix_clean="$(echo "$prefix" | tr -d '[:space:]')" - [ -z "$prefix_clean" ] && continue - case "$pkg" in - "$prefix_clean"*) - protected=1 - break - ;; - esac - done - [ "$protected" -eq 1 ] && continue - - if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then - echo "$pkg" >>"$STATE_DIR/disabled_by_focus.txt" - count=$((count + 1)) - fi - done - echo "focus" >"$MODE_FILE" - echo "Done: disabled $count apps" -} - -cmd_recheck() { - # Write the trigger file; the daemon's sleep_with_recheck() will pick it - # up within ~1 second and perform an immediate location check. - if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then - echo "Daemon not running - start it first with: focus_ctl.sh start" - return 1 - fi - touch "$RECHECK_TRIGGER" - chmod 666 "$RECHECK_TRIGGER" 2>/dev/null || true - echo "Recheck requested. Tail the log to see the next reading:" - echo " tail -f $LOG_FILE" -} - -cmd_notif_status() { - if [ -f "$STATUS_FILE" ]; then - echo "=== $STATUS_FILE ===" - cat "$STATUS_FILE" - echo - else - echo "No status snapshot yet (daemon has not written $STATUS_FILE)." - fi - if command -v dumpsys >/dev/null 2>&1; then - echo "=== Companion app state ===" - dumpsys package com.kuhy.focusstatus 2>/dev/null | grep -E 'enabled=|installed=|userId=' | head -5 || true - fi -} - -cmd_disable() { - echo "Forcing focus mode OFF..." - if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then - local count=0 - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1)) - done <"$DISABLED_APPS_FILE" - : >"$DISABLED_APPS_FILE" - echo "Done: re-enabled $count apps" - else - echo "No apps to re-enable" - fi - echo "normal" >"$MODE_FILE" -} - -cmd_log() { - local lines="${1:-50}" - if [ -f "$LOG_FILE" ]; then - tail -n "$lines" "$LOG_FILE" - else - echo "Log file not found: $LOG_FILE" - fi -} diff --git a/phone_focus_mode/ctl_dns.sh b/phone_focus_mode/ctl_dns.sh deleted file mode 100755 index 1eb5dd41..00000000 --- a/phone_focus_mode/ctl_dns.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_dns.sh — focus_ctl.sh's subcommands for the DNS enforcer: -# status, start, stop and log. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one -# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh -# globals these read. The pidfile path below lives here rather than in -# the entry script because this file is its only reader (SC2034), and -# the linter runs without -x, so each file stands alone. - -DNS_PIDFILE="$STATE_DIR/dns_enforcer.pid" - -dns_enforcer_pid() { - if [ -f "$DNS_PIDFILE" ]; then - local pid - pid="$(cat "$DNS_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi -} - -cmd_dns_status() { - local pid - pid="$(dns_enforcer_pid)" - echo "=== DNS Enforcer Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - local mode spec - mode="$(settings get global private_dns_mode 2>/dev/null)" - spec="$(settings get global private_dns_specifier 2>/dev/null)" - echo "private_dns_mode: ${mode:-}" - echo "private_dns_specifier: ${spec:-}" - if iptables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then - local v4rules - v4rules="$(iptables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)" - echo "iptables $DNS_IPT_CHAIN: $v4rules rules" - else - echo "iptables $DNS_IPT_CHAIN: MISSING" - fi - if ip6tables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then - local v6rules - v6rules="$(ip6tables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)" - echo "ip6tables $DNS_IPT_CHAIN: $v6rules rules" - else - echo "ip6tables $DNS_IPT_CHAIN: MISSING" - fi -} - -cmd_dns_start() { - local pid - pid="$(dns_enforcer_pid)" - if [ -n "$pid" ]; then - echo "DNS enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/dns_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(dns_enforcer_pid)" - if [ -n "$pid" ]; then - echo "DNS enforcer started (PID $pid)" - else - echo "ERROR: DNS enforcer failed to start. Check log: $DNS_LOG" - fi -} - -cmd_dns_stop() { - local pid - pid="$(dns_enforcer_pid)" - if [ -z "$pid" ]; then - echo "DNS enforcer not running" - rm -f "$DNS_PIDFILE" - else - kill -TERM "$pid" - echo "DNS enforcer stopped (sent SIGTERM to PID $pid)" - fi - # Explicit teardown of the iptables chain so maintenance work can - # use DoH. The enforcer itself leaves the chain intact on TERM to - # keep the block closed between periodic re-applies. - iptables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true - iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || true - iptables -X "$DNS_IPT_CHAIN" 2>/dev/null || true - ip6tables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true - ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || true - ip6tables -X "$DNS_IPT_CHAIN" 2>/dev/null || true - echo "iptables chain $DNS_IPT_CHAIN removed" -} - -cmd_dns_log() { - local lines="${1:-50}" - if [ -f "$DNS_LOG" ]; then - tail -n "$lines" "$DNS_LOG" - else - echo "DNS enforcer log not found: $DNS_LOG" - fi -} diff --git a/phone_focus_mode/ctl_hosts.sh b/phone_focus_mode/ctl_hosts.sh deleted file mode 100755 index 3a46148d..00000000 --- a/phone_focus_mode/ctl_hosts.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_hosts.sh — focus_ctl.sh's subcommands for the hosts-blocklist enforcer: -# status, start, stop and log. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one -# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh -# globals these read. The pidfile path below lives here rather than in -# the entry script because this file is its only reader (SC2034), and -# the linter runs without -x, so each file stands alone. - -HOSTS_PIDFILE="$STATE_DIR/hosts_enforcer.pid" - -hosts_enforcer_pid() { - if [ -f "$HOSTS_PIDFILE" ]; then - local pid - pid="$(cat "$HOSTS_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi -} - -cmd_hosts_status() { - local pid - pid="$(hosts_enforcer_pid)" - echo "=== Hosts Enforcer Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Canonical: $HOSTS_CANONICAL" - echo "Target: $HOSTS_TARGET" - if grep -qE "[[:space:]]${HOSTS_TARGET}[[:space:]]" /proc/self/mounts 2>/dev/null; then - # A mount exists on the target path, but on Android the OEM sometimes - # already mounts its own hosts file here. Trust the sha check below. - echo "Mount: present (integrity check below tells us if ours)" - else - echo "Mount: NOT mounted (unprotected)" - fi - if [ -f "$HOSTS_CANONICAL" ]; then - local expected actual - expected="$(cat "$HOSTS_SHA_FILE" 2>/dev/null)" - if command -v sha256sum >/dev/null 2>&1; then - actual="$(sha256sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')" - else - actual="$(md5sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')" - fi - echo "Expected: ${expected:-}" - echo "Actual: ${actual:-}" - if [ -n "$expected" ] && [ "$expected" = "$actual" ]; then - echo "Integrity: OK" - else - echo "Integrity: MISMATCH" - fi - else - echo "Canonical hosts file missing - run deploy.sh" - fi - # Magisk Systemless Hosts module protection state. - local module_dir="/data/adb/modules/hosts" - if [ -d "$module_dir" ]; then - local lock_state="UNLOCKED (Magisk app can disable!)" - if lsattr -d "$module_dir" 2>/dev/null | awk '{print $1}' | grep -q i; then - lock_state="LOCKED (chattr +i)" - fi - echo "Magisk dir: $module_dir [$lock_state]" - local marker_warn="" - for marker in disable remove update; do - if [ -e "$module_dir/$marker" ]; then - marker_warn="$marker_warn $marker" - fi - done - if [ -n "$marker_warn" ]; then - echo "WARN: Magisk markers present:$marker_warn (will be auto-removed by hosts_enforcer)" - fi - else - echo "Magisk dir: " - fi -} - -cmd_hosts_start() { - local pid - pid="$(hosts_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Hosts enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(hosts_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Hosts enforcer started (PID $pid)" - else - echo "ERROR: hosts enforcer failed to start. Check log: $HOSTS_LOG" - fi -} - -cmd_hosts_stop() { - local pid - pid="$(hosts_enforcer_pid)" - if [ -z "$pid" ]; then - echo "Hosts enforcer not running" - rm -f "$HOSTS_PIDFILE" - return - fi - kill -TERM "$pid" - echo "Hosts enforcer stopped (sent SIGTERM to PID $pid)" -} - -cmd_hosts_log() { - local lines="${1:-50}" - if [ -f "$HOSTS_LOG" ]; then - tail -n "$lines" "$HOSTS_LOG" - else - echo "Hosts enforcer log not found: $HOSTS_LOG" - fi -} diff --git a/phone_focus_mode/ctl_launcher.sh b/phone_focus_mode/ctl_launcher.sh deleted file mode 100755 index a5b1e1a7..00000000 --- a/phone_focus_mode/ctl_launcher.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_launcher.sh — focus_ctl.sh's subcommands for the minimalist-launcher enforcer: -# status, start, stop and log. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one -# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh -# globals these read. The pidfile path below lives here rather than in -# the entry script because this file is its only reader (SC2034), and -# the linter runs without -x, so each file stands alone. - -LAUNCHER_PIDFILE="$STATE_DIR/launcher_enforcer.pid" -DISABLED_COMPETITORS_FILE="$STATE_DIR/disabled_competitors.txt" - -launcher_enforcer_pid() { - if [ -f "$LAUNCHER_PIDFILE" ]; then - local pid - pid="$(cat "$LAUNCHER_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi -} - -cmd_launcher_snapshot() { - # Find the APK path for the currently-installed launcher and copy it - # to LAUNCHER_APK. Also capture the current HOME activity component. - local apk_path - apk_path="$(pm path "$LAUNCHER_PACKAGE" 2>/dev/null | head -1 | sed 's/^package://')" - if [ -z "$apk_path" ] || [ ! -f "$apk_path" ]; then - echo "ERROR: $LAUNCHER_PACKAGE is not installed. Install it once via Aurora/Play Store, then rerun this command." - return 1 - fi - mkdir -p "$(dirname "$LAUNCHER_APK")" - chattr -i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true - cp "$apk_path" "$LAUNCHER_APK" || return 1 - chmod 644 "$LAUNCHER_APK" - sha256sum "$LAUNCHER_APK" | awk '{print $1}' >"$LAUNCHER_SHA_FILE" - chmod 644 "$LAUNCHER_SHA_FILE" - - # Resolve the current HOME activity (or the launcher's default activity - # if it isn't yet the default). - local component - component="$(cmd package resolve-activity --brief \ - -c android.intent.category.HOME \ - -a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}')" - if [ -z "$component" ] || [ "${component%%/*}" != "$LAUNCHER_PACKAGE" ]; then - # Fall back to the launcher's MAIN/LAUNCHER activity - component="$(cmd package resolve-activity --brief \ - -c android.intent.category.LAUNCHER \ - -a android.intent.action.MAIN "$LAUNCHER_PACKAGE" 2>/dev/null | - awk 'NR==2{print}')" - fi - if [ -z "$component" ]; then - echo "ERROR: could not resolve HOME activity for $LAUNCHER_PACKAGE" - return 1 - fi - echo "$component" >"$LAUNCHER_ACTIVITY_FILE" - chmod 644 "$LAUNCHER_ACTIVITY_FILE" - - # Make snapshot immutable so even root-in-a-terminal can't overwrite - # it without first running `chattr -i`. - chattr +i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true - - echo "Snapshot saved:" - echo " APK: $LAUNCHER_APK ($(wc -c <"$LAUNCHER_APK") bytes)" - echo " SHA256: $(cat "$LAUNCHER_SHA_FILE")" - echo " Activity: $component" -} - -cmd_launcher_status() { - local pid - pid="$(launcher_enforcer_pid)" - echo "=== Launcher Enforcer Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Package: $LAUNCHER_PACKAGE" - if pm path "$LAUNCHER_PACKAGE" >/dev/null 2>&1; then - echo "Installed: YES ($(pm path "$LAUNCHER_PACKAGE" | head -1))" - else - echo "Installed: NO" - fi - local desired actual - desired="$(cat "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null)" - actual="$(cmd package resolve-activity --brief \ - -c android.intent.category.HOME -a android.intent.action.MAIN \ - 2>/dev/null | awk 'NR==2{print}')" - echo "Expected: ${desired:-}" - echo "Actual: ${actual:-}" - if [ -n "$desired" ] && [ "$desired" = "$actual" ]; then - echo "Default: OK (pinned)" - else - echo "Default: MISMATCH" - fi - echo "Snapshot: $LAUNCHER_APK" - if [ -f "$LAUNCHER_APK" ]; then - echo "Snapshot size: $(wc -c <"$LAUNCHER_APK") bytes" - fi - if [ -s "$DISABLED_COMPETITORS_FILE" ]; then - echo "Disabled competitors:" - sed 's/^/ - /' "$DISABLED_COMPETITORS_FILE" - fi -} - -cmd_launcher_start() { - local pid - pid="$(launcher_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Launcher enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/launcher_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(launcher_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Launcher enforcer started (PID $pid)" - else - echo "ERROR: launcher enforcer failed to start. Check log: $LAUNCHER_LOG" - fi -} - -cmd_launcher_stop() { - local pid - pid="$(launcher_enforcer_pid)" - if [ -z "$pid" ]; then - echo "Launcher enforcer not running" - rm -f "$LAUNCHER_PIDFILE" - else - kill -TERM "$pid" - echo "Launcher enforcer stopped (sent SIGTERM to PID $pid)" - fi - # Re-enable any competitors we disabled so the device is usable if the - # enforcer is intentionally stopped (e.g. during maintenance). - if [ -s "$DISABLED_COMPETITORS_FILE" ]; then - while read -r pkg; do - [ -z "$pkg" ] && continue - pm enable --user 0 "$pkg" >/dev/null 2>&1 && - echo "Re-enabled competing launcher: $pkg" - done <"$DISABLED_COMPETITORS_FILE" - : >"$DISABLED_COMPETITORS_FILE" - fi -} - -cmd_launcher_log() { - local lines="${1:-50}" - if [ -f "$LAUNCHER_LOG" ]; then - tail -n "$lines" "$LAUNCHER_LOG" - else - echo "Launcher enforcer log not found: $LAUNCHER_LOG" - fi -} diff --git a/phone_focus_mode/ctl_tether.sh b/phone_focus_mode/ctl_tether.sh deleted file mode 100755 index f5e9377d..00000000 --- a/phone_focus_mode/ctl_tether.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_tether.sh — focus_ctl.sh's subcommands for the tethering enforcer: -# status, start, stop and log. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one -# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh -# globals these read. The pidfile path below lives here rather than in -# the entry script because this file is its only reader (SC2034), and -# the linter runs without -x, so each file stands alone. - -TETHER_PIDFILE="$STATE_DIR/tether_enforcer.pid" - -tether_enforcer_pid() { - if [ -f "$TETHER_PIDFILE" ]; then - local pid - pid="$(cat "$TETHER_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi -} - -cmd_tether_status() { - local pid - pid="$(tether_enforcer_pid)" - echo "=== Hotspot / Tethering Block Status ===" - echo "Enabled: ${TETHER_ENFORCER_ENABLED}" - if [ -n "$pid" ]; then - echo "Enforcer: RUNNING (PID $pid)" - else - echo "Enforcer: STOPPED" - fi - if [ -f "$MODE_FILE" ]; then - echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)" - fi - [ -e "$TETHER_FORCE_FILE" ] && echo "Forced ON: YES (test active)" - [ -e "$TETHER_OVERRIDE_FILE" ] && echo "Override: YES (block SUSPENDED)" - [ -e "$TETHER_ENFORCER_STATE" ] && echo "Applied now: YES (offload off + FORWARD reject)" || - echo "Applied now: no" - local off - off="$(settings get global "$TETHER_OFFLOAD_KEY" 2>/dev/null)" - echo "tether_offload_disabled: ${off:-}" - if iptables -C FORWARD -j "$TETHER_IPT_CHAIN" >/dev/null 2>&1; then - echo "iptables $TETHER_IPT_CHAIN: pinned to FORWARD ($(iptables -S "$TETHER_IPT_CHAIN" 2>/dev/null | grep -c '^-A') rule)" - else - echo "iptables $TETHER_IPT_CHAIN: absent (block not applied)" - fi -} - -cmd_tether_start() { - local pid - pid="$(tether_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Tether enforcer already running (PID $pid)" - return - fi - setsid sh "$SCRIPT_DIR/tether_enforcer.sh" /dev/null 2>&1 & - sleep 2 - pid="$(tether_enforcer_pid)" - if [ -n "$pid" ]; then - echo "Tether enforcer started (PID $pid)" - else - echo "ERROR: tether enforcer failed to start. Check log: $TETHER_LOG" - fi -} - -cmd_tether_stop() { - local pid - pid="$(tether_enforcer_pid)" - if [ -z "$pid" ]; then - echo "Tether enforcer not running" - rm -f "$TETHER_PIDFILE" - else - kill -TERM "$pid" - echo "Tether enforcer stopped (sent SIGTERM to PID $pid)" - fi - # Explicit teardown so maintenance can tether even if the daemon was already - # dead (its own TERM handler reverts too; this is the belt-and-suspenders). - iptables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true - iptables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true - iptables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -j "$TETHER_IPT_CHAIN" 2>/dev/null || true - ip6tables -F "$TETHER_IPT_CHAIN" 2>/dev/null || true - ip6tables -X "$TETHER_IPT_CHAIN" 2>/dev/null || true - echo "FORWARD chain $TETHER_IPT_CHAIN removed (tethering restored)" -} - -cmd_tether_log() { tail -n "${1:-50}" "$TETHER_LOG" 2>/dev/null || echo "No tether log yet."; } - -# Test hook: force the block ACTIVE regardless of location so the full stack can -# be validated during the day with a real second phone on the hotspot. -cmd_tether_test_on() { - touch "$TETHER_FORCE_FILE" - chmod 666 "$TETHER_FORCE_FILE" 2>/dev/null || true - rm -f "$TETHER_OVERRIDE_FILE" - echo "Tether block FORCED ON. Enforcer engages within ${TETHER_CHECK_INTERVAL}s." - echo "Validate: second phone on the hotspot loses internet; this phone stays online." -} - -cmd_tether_test_off() { - rm -f "$TETHER_FORCE_FILE" - echo "Tether force cleared. Back to focus-mode-gated behaviour." -} diff --git a/phone_focus_mode/ctl_usage.sh b/phone_focus_mode/ctl_usage.sh deleted file mode 100755 index 220d159b..00000000 --- a/phone_focus_mode/ctl_usage.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_usage.sh — focus_ctl.sh's help text and the two commands that report on -# the whitelist: list-apps and whitelist. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory. -# Sourced by focus_ctl.sh, which owns the config.sh globals these read. -# -# NOTE on iter_whitelist_packages: its `while read` loop is fed by a pipe from -# printf, and nothing in the body reads stdin, so the pipe is safe here. That -# is NOT true of the pm-driven loops elsewhere in this repo -- see the warning -# in daemon_apps.sh before converting any redirect-driven loop to a pipe. - -# Emit one valid package name per line from WHITELIST. -# This strips comments/blank lines from the multi-line quoted string and avoids -# treating heading text (e.g. "---") as package tokens. -iter_whitelist_packages() { - printf '%s\n' "$WHITELIST" | while IFS= read -r line; do - line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" - # Blank lines only. A comment needs no case of its own: `set -- $line` - # below splits on whitespace, so a comment's first token is always "#", - # which the dot check then rejects. A dedicated `\#*)` arm here was - # provably equivalent -- a mutation removing it could not be - # distinguished by any input -- and equivalent code is a claim of a - # check that is not actually being made. - case "$line" in - "") continue ;; - esac - - # Keep first token only; ignore any inline prose if present. - # Intentional word split to grab the first token. This runs under - # /system/bin/sh on the phone, where arrays do not exist, so an - # unquoted expansion is the POSIX way to do it. - # shellcheck disable=SC2086 - set -- $line - pkg="$1" - - # Package names are dot-delimited identifiers. - case "$pkg" in - *.*) ;; - *) continue ;; - esac - case "$pkg" in - *[!A-Za-z0-9._]*) continue ;; - esac - - echo "$pkg" - done -} - -usage() { - echo "Usage: focus_ctl.sh " - echo "" - echo "Commands:" - echo " start - Start the focus mode daemon" - echo " stop - Stop the daemon and re-enable all apps" - echo " status - Show current mode, location and disabled apps" - echo " enable - Force focus mode on (regardless of location)" - echo " disable - Force focus mode off (regardless of location)" - echo " log - Show daemon log" - echo " list-apps - List all non-whitelisted third-party apps" - echo " whitelist - List currently whitelisted packages" - echo " restart - Restart the daemon" - echo " hosts-status - Show hosts enforcer state (mount + hash)" - echo " hosts-start - Start the hosts enforcer daemon" - echo " hosts-stop - Stop the hosts enforcer daemon" - echo " hosts-log - Show hosts enforcer log" - echo " dns-status - Show DNS enforcer state (Private DNS + iptables)" - echo " dns-start - Start the DNS enforcer daemon" - echo " dns-stop - Stop the DNS enforcer daemon (removes iptables chain)" - echo " dns-log - Show DNS enforcer log" - echo " launcher-status - Show launcher enforcer state" - echo " launcher-start - Start the launcher enforcer daemon" - echo " launcher-stop - Stop the launcher enforcer daemon" - echo " launcher-log - Show launcher enforcer log" - echo " launcher-snapshot - Back up currently-installed launcher APK" - echo " workout-status - Show StrongLifts workout-detection state" - echo " workout-start - Start the workout detector daemon" - echo " workout-stop - Stop the workout detector daemon (sets flag=0)" - echo " workout-log - Show workout detector log" - echo " recheck - Nudge the daemon to perform a fresh location check now" - echo " curfew-status - Show night-curfew + enforcer state" - echo " curfew-start - Start the curfew enforcer (grayscale/DND/net)" - echo " curfew-stop - Stop it and restore daytime display/DND" - echo " curfew-log - Show curfew enforcer log" - echo " curfew-test-on - Force curfew ACTIVE now (daytime validation)" - echo " curfew-test-off - Clear the test force" - echo " curfew-demo-on - Start a demo: full curfew now, easy one-tap off" - echo " curfew-demo-off - Stop the demo" - echo " curfew-off - Escape hatch: suspend curfew now (2am opt-out)" - echo " curfew-on - Re-arm curfew (clear the override)" - echo " tether-status - Show hotspot/tethering-block state" - echo " tether-start - Start the tether enforcer (FORWARD block + offload off)" - echo " tether-stop - Stop it and restore tethering (teardown FORWARD chain)" - echo " tether-log - Show tether enforcer log" - echo " tether-test-on - Force the tether block ACTIVE now (daytime validation)" - echo " tether-test-off - Clear the tether force" - echo " notif-status - Show companion status-notification details" - echo "" -} - -cmd_list_apps() { - echo "=== Third-party apps NOT in whitelist ===" - for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do - whitelisted=0 - for w in $(iter_whitelist_packages); do - w="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w" ] && continue - [ "$pkg" = "$w" ] && { - whitelisted=1 - break - } - done - if [ "$whitelisted" -eq 0 ]; then - # Check if currently disabled by focus mode - if grep -qF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null; then - echo " [BLOCKED] $pkg" - else - echo " [active] $pkg" - fi - fi - done - echo "" - echo "=== Whitelisted apps ===" - for w in $(iter_whitelist_packages); do - w="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w" ] && continue - echo " [allowed] $w" - done -} - -cmd_whitelist() { - echo "=== Whitelisted packages ===" - for w in $(iter_whitelist_packages); do - w="$(echo "$w" | tr -d '[:space:]')" - [ -z "$w" ] && continue - # Check if installed - if pm list packages "$w" 2>/dev/null | grep -qF "$w"; then - echo " [installed] $w" - else - echo " [not found] $w" - fi - done -} diff --git a/phone_focus_mode/ctl_workout.sh b/phone_focus_mode/ctl_workout.sh deleted file mode 100755 index abc9602e..00000000 --- a/phone_focus_mode/ctl_workout.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ctl_workout.sh — focus_ctl.sh's subcommands for the workout detector: -# status, start, stop and log. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one -# directory. Sourced by focus_ctl.sh, which owns log() and the config.sh -# globals these read. The pidfile path below lives here rather than in -# the entry script because this file is its only reader (SC2034), and -# the linter runs without -x, so each file stands alone. - -WORKOUT_PIDFILE="$STATE_DIR/workout_detector.pid" - -workout_detector_pid() { - if [ -f "$WORKOUT_PIDFILE" ]; then - local pid - pid="$(cat "$WORKOUT_PIDFILE")" - if kill -0 "$pid" 2>/dev/null; then - echo "$pid" - fi - fi -} - -cmd_workout_status() { - local pid - pid="$(workout_detector_pid)" - echo "=== Workout Detector Status ===" - if [ -n "$pid" ]; then - echo "Daemon: RUNNING (PID $pid)" - else - echo "Daemon: STOPPED" - fi - echo "Package: $WORKOUT_TRIGGER_PACKAGE" - if pm path "$WORKOUT_TRIGGER_PACKAGE" >/dev/null 2>&1; then - echo "Installed: YES" - else - echo "Installed: NO (detector will always report inactive)" - fi - echo "sqlite3: $WORKOUT_SQLITE3_BIN" - if [ -x "$WORKOUT_SQLITE3_BIN" ]; then - echo "sqlite3 ver: $("$WORKOUT_SQLITE3_BIN" -version 2>/dev/null | awk '{print $1}')" - else - echo "sqlite3 ver: " - fi - echo "DB path: $WORKOUT_DB_PATH" - if [ -f "$WORKOUT_DB_PATH" ]; then - echo "DB present: YES" - else - echo "DB present: NO" - fi - echo "Poll interval: ${WORKOUT_DETECTOR_INTERVAL}s" - local flag="" - if [ -f "$WORKOUT_ACTIVE_FILE" ]; then - flag="$(cat "$WORKOUT_ACTIVE_FILE" 2>/dev/null)" - fi - case "$flag" in - 1) echo "Workout flag: 1 (workout IN PROGRESS → YouTube hosts UNBLOCKED)" ;; - 0) echo "Workout flag: 0 (no workout → YouTube hosts BLOCKED)" ;; - *) echo "Workout flag: '$flag' (treated as 0, fail-closed)" ;; - esac - # Live one-shot query so the user can see ground truth without waiting - # for the next poll cycle. Best-effort — never fails the status command. - if [ -x "$WORKOUT_SQLITE3_BIN" ] && [ -f "$WORKOUT_DB_PATH" ]; then - local live_count - live_count="$("$WORKOUT_SQLITE3_BIN" "file:${WORKOUT_DB_PATH}?mode=ro" \ - "SELECT COUNT(*) FROM workouts WHERE start>0 AND (finish IS NULL OR finish=0);" \ - 2>/dev/null)" - echo "Live DB query: in-progress workouts = ${live_count:-}" - fi - if [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then - echo "Workout hosts: $HOSTS_CANONICAL_WORKOUT ($(wc -l <"$HOSTS_CANONICAL_WORKOUT" 2>/dev/null) lines)" - else - echo "Workout hosts: " - fi -} - -cmd_workout_start() { - local pid - pid="$(workout_detector_pid)" - if [ -n "$pid" ]; then - echo "Workout detector already running (PID $pid)" - return - fi - if [ ! -x "$WORKOUT_SQLITE3_BIN" ]; then - echo "ERROR: $WORKOUT_SQLITE3_BIN missing or not executable. Re-run deploy.sh." - return 1 - fi - setsid sh "$SCRIPT_DIR/workout_detector.sh" /dev/null 2>&1 & - sleep 2 - pid="$(workout_detector_pid)" - if [ -n "$pid" ]; then - echo "Workout detector started (PID $pid)" - else - echo "ERROR: Workout detector failed to start. Check log: $WORKOUT_DETECTOR_LOG" - fi -} - -cmd_workout_stop() { - local pid - pid="$(workout_detector_pid)" - if [ -z "$pid" ]; then - echo "Workout detector not running" - rm -f "$WORKOUT_PIDFILE" - else - kill -TERM "$pid" - echo "Workout detector stopped (sent SIGTERM to PID $pid)" - fi - # Fail-closed on manual stop: write 0 so the hosts enforcer reverts to - # the full-block canonical and YouTube goes back to being blocked. - printf '0\n' >"$WORKOUT_ACTIVE_FILE" 2>/dev/null || true - chmod 666 "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true - echo "workout_active flag forced to 0" -} - -cmd_workout_log() { - local lines="${1:-50}" - if [ -f "$WORKOUT_DETECTOR_LOG" ]; then - tail -n "$lines" "$WORKOUT_DETECTOR_LOG" - else - echo "Workout detector log not found: $WORKOUT_DETECTOR_LOG" - fi -} diff --git a/phone_focus_mode/curfew_enforcer.sh b/phone_focus_mode/curfew_enforcer.sh deleted file mode 100755 index 8a2dd9da..00000000 --- a/phone_focus_mode/curfew_enforcer.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ============================================================ -# Night-curfew enforcer for rooted Android. -# -# Companion to focus_daemon.sh. The daemon handles the APP layer (disabling -# everything not in $NIGHT_WHITELIST while the curfew window is open at home). -# This enforcer adds the three "make the phone boring + unreachable" layers and -# keeps them locked by re-applying every $CURFEW_ENFORCER_INTERVAL seconds: -# -# 1. Grayscale - force the display monochrome via the accessibility -# daltonizer. The single biggest behavioural deterrent. -# 2. DND - force Do-Not-Disturb to alarms-only so notifications stop -# pulling you back in, while the morning alarm still rings. -# 3. Net curfew - (default OFF) per-UID iptables allow-list: only the -# $NIGHT_WHITELIST app UIDs (plus root/system/shell + DNS) -# get network; every other app is cut off. -# -# "Locked" = snap-back: a manual toggle in Settings is reverted within one -# interval. True impossibility would require blocking the Settings app, which -# risks system instability, so we deliberately do not. -# -# Acts ONLY while curfew is active (time window or forced) AND, for the -# non-forced case, while focus mode is ON (i.e. you are at home). On the -# transition back to day it restores the snapshotted display/DND state and -# tears the iptables chain down, so daytime is left exactly as it was. -# ============================================================ - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -# shellcheck source=config.sh -. "$SCRIPT_DIR/config.sh" -# shellcheck source=curfew_net.sh -. "$SCRIPT_DIR/curfew_net.sh" - -PIDFILE="$STATE_DIR/curfew_enforcer.pid" -# Snapshot of the user's pre-curfew display/DND state, captured on entry and -# restored on exit so we never clobber settings we did not set. -GRAYSCALE_SNAP="$STATE_DIR/curfew_grayscale.snap" - -mkdir -p "$STATE_DIR" -touch "$CURFEW_ENFORCER_LOG" -chmod 666 "$CURFEW_ENFORCER_LOG" 2>/dev/null || true - -log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >> "$CURFEW_ENFORCER_LOG" -} - -rotate_log() { - local lines - lines="$(wc -l < "$CURFEW_ENFORCER_LOG" 2>/dev/null || echo 0)" - if [ "$lines" -gt 500 ]; then - local tmp="$CURFEW_ENFORCER_LOG.tmp" - tail -n 500 "$CURFEW_ENFORCER_LOG" > "$tmp" - mv "$tmp" "$CURFEW_ENFORCER_LOG" - fi -} - -acquire_lock() { - if [ -f "$PIDFILE" ]; then - local old_pid - old_pid="$(cat "$PIDFILE")" - if kill -0 "$old_pid" 2>/dev/null; then - local cmdline - cmdline="$(tr '\0' ' ' < "/proc/$old_pid/cmdline" 2>/dev/null)" - if echo "$cmdline" | grep -q "curfew_enforcer"; then - echo "curfew_enforcer already running (PID $old_pid)" - exit 0 - fi - fi - rm -f "$PIDFILE" - fi - echo $$ > "$PIDFILE" -} - -# ---- Time / activation (mirrors focus_daemon.sh::curfew_active) ---- - -_dec() { - # Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed - # as (sometimes invalid) octal by the shell's arithmetic. Keeps one digit. - local n="$1" - while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done - printf '%s' "$n" -} - -is_curfew_now() { - local now start end - now="$(date +%H%M 2>/dev/null)" - case "$now" in - ''|*[!0-9]*) return 1 ;; - esac - now="$(_dec "$now")"; start="$(_dec "$NIGHT_CURFEW_START")"; end="$(_dec "$NIGHT_CURFEW_END")" - if [ "$start" -le "$end" ]; then - [ "$now" -ge "$start" ] && [ "$now" -lt "$end" ] - else - [ "$now" -ge "$start" ] || [ "$now" -lt "$end" ] - fi -} - -at_home() { - [ -f "$MODE_FILE" ] && [ "$(cat "$MODE_FILE" 2>/dev/null)" = "focus" ] -} - -# Whether this enforcer should be applying its restrictions right now. -should_act() { - [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] || return 1 - [ -e "$CURFEW_OVERRIDE_FILE" ] && return 1 - # Forced (test hook) bypasses both the clock and the home gate so the full - # stack can be validated during the day from anywhere. - [ -e "$CURFEW_FORCE_FILE" ] && return 0 - is_curfew_now && at_home -} - -# ---- Layer 1: grayscale ---- - -apply_grayscale() { - [ "${CURFEW_GRAYSCALE_ENABLED:-0}" = "1" ] || return 0 - settings put secure accessibility_display_daltonizer_enabled 1 2>/dev/null || true - # Daltonizer "0" = full monochrome (grayscale). - settings put secure accessibility_display_daltonizer 0 2>/dev/null || true -} - -snapshot_grayscale() { - local en lv - en="$(settings get secure accessibility_display_daltonizer_enabled 2>/dev/null)" - lv="$(settings get secure accessibility_display_daltonizer 2>/dev/null)" - printf '%s\n%s\n' "${en:-0}" "${lv:-0}" > "$GRAYSCALE_SNAP" 2>/dev/null || true -} - -restore_grayscale() { - [ "${CURFEW_GRAYSCALE_ENABLED:-0}" = "1" ] || return 0 - local en lv - if [ -f "$GRAYSCALE_SNAP" ]; then - en="$(sed -n '1p' "$GRAYSCALE_SNAP")" - lv="$(sed -n '2p' "$GRAYSCALE_SNAP")" - fi - # If the snapshot is missing or "null", default to disabled (the norm). - case "$en" in ''|null) en=0 ;; esac - case "$lv" in ''|null) lv=-1 ;; esac - settings put secure accessibility_display_daltonizer_enabled "$en" 2>/dev/null || true - if [ "$lv" != "-1" ]; then - settings put secure accessibility_display_daltonizer "$lv" 2>/dev/null || true - fi -} - -# ---- Layer 2: Do-Not-Disturb (alarms only) ---- - -apply_dnd() { - [ "${CURFEW_DND_ENABLED:-0}" = "1" ] || return 0 - # alarms-only lets the morning alarm ring but silences everything else. - cmd notification set_dnd alarms >/dev/null 2>&1 || true -} - -restore_dnd() { - [ "${CURFEW_DND_ENABLED:-0}" = "1" ] || return 0 - cmd notification set_dnd off >/dev/null 2>&1 || true -} - -# ---- Layer 3: per-UID network allow-list (default OFF) ---- - - - - - -# Set to 1 once the net chain has been built in this process. Used purely to -# tell an *anomalous* mid-curfew disappearance (something outside this script -# deleted the chain) apart from the legitimate first build on curfew entry. -# Because it is a process-local var, a fresh enforcer starts empty and never -# false-flags its own initial build. -NET_BUILT="" - - - - - - -# ---- Apply / revert orchestration ---- - -enter_curfew() { - if [ ! -e "$CURFEW_ENFORCER_STATE" ]; then - snapshot_grayscale - : > "$CURFEW_ENFORCER_STATE" - log "Curfew ON - locking grayscale${CURFEW_DND_ENABLED:+ + DND}${CURFEW_NET_ENABLED:+ + net}" - fi - # Re-apply every tick so manual toggles snap back. - apply_grayscale - apply_dnd - apply_net -} - -exit_curfew() { - [ -e "$CURFEW_ENFORCER_STATE" ] || return 0 - restore_grayscale - restore_dnd - teardown_net - NET_BUILT="" - rm -f "$CURFEW_ENFORCER_STATE" "$CURFEW_NET_UID_CACHE" - log "Curfew OFF - restored display/DND, tore down net chain" -} - -cleanup() { - # On a clean stop, leave the user back in daytime state. - log "curfew_enforcer shutting down - reverting" - exit_curfew - rm -f "$PIDFILE" - exit 0 -} - -trap cleanup INT TERM - -main() { - acquire_lock - log "curfew_enforcer started (PID=$$, window=${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END}, net=${CURFEW_NET_ENABLED})" - local tick=0 act netstate rebuilds - while true; do - if should_act; then act=1; enter_curfew; else act=0; exit_curfew; fi - # Heartbeat every ~6 ticks (~30s): proves the loop is alive even when it - # is quietly re-applying. Without this, "alive but idle" and "dead" look - # identical in the log, so process death can't be inferred from silence. - tick=$((tick + 1)) - if [ "$((tick % 6))" -eq 0 ]; then - if iptw iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then - netstate=up - else - netstate=down - fi - log "heartbeat tick=$tick act=$act net=$netstate" - fi - rotate_log - # While curfew is active with the net layer on, hold the chain pinned - # against netd's table rewrites for the whole interval (fast watchdog), - # instead of sleeping blind. Otherwise a plain sleep is enough. - if [ "$act" = 1 ] && [ "${CURFEW_NET_ENABLED:-0}" = "1" ]; then - rebuilds="$(net_hold "$CURFEW_ENFORCER_INTERVAL")" - [ "${rebuilds:-0}" -gt 0 ] 2>/dev/null \ - && log "net watchdog re-pinned chain ${rebuilds}x in last ${CURFEW_ENFORCER_INTERVAL}s (netd flush)" - else - sleep "$CURFEW_ENFORCER_INTERVAL" - fi - done -} - -main "$@" diff --git a/phone_focus_mode/curfew_net.sh b/phone_focus_mode/curfew_net.sh deleted file mode 100755 index a4f208ce..00000000 --- a/phone_focus_mode/curfew_net.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# curfew_net.sh — the network layer of the night curfew: the per-UID -# iptables allow-list that cuts every app outside $NIGHT_WHITELIST off the -# network while the curfew is open. Default OFF ($CURFEW_NET_ENABLED). -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one -# directory, which is why dns_iptables.sh and tether_iptables.sh live -# beside their enforcers too. -# -# Sourced by curfew_enforcer.sh, which owns log(), the config.sh globals -# these read ($CURFEW_NET_*, $NIGHT_WHITELIST, $STATE_DIR) and $NET_BUILT, -# the build-once flag both files write. - -# Resolve the UIDs of the night-whitelisted packages. Apps not installed are -# silently skipped. Output: one numeric UID per line. -night_uids() { - local plist="$STATE_DIR/night_whitelist.txt" - [ -f "$plist" ] || return 0 - # `pm list packages -U` lines look like: "package:com.foo uid:10123" - local map="$STATE_DIR/uid_map.txt" - pm list packages -U 2>/dev/null \ - | sed 's/^package://' > "$map" - # Piped rather than redirected on `done`: kcov instruments a trailing - # `done < file` as a line bash never reports, which alone held this file - # below 100%. The loop only writes to stdout, so running it in the pipe's - # subshell changes nothing — unlike the pull_apks case in - # backup_capture.sh, nothing inside here consumes stdin. - cat "$plist" | while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - awk -v p="$pkg" '$1 == p { sub(/uid:/,"",$2); print $2 }' "$map" - done - rm -f "$map" -} - -ensure_net_chain() { - local ipt="$1" - if ! iptw "$ipt" -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then - iptw "$ipt" -N "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || return 1 - fi - # De-dupe and pin exactly one OUTPUT jump at position 1. - while iptw "$ipt" -D OUTPUT -j "$CURFEW_NET_IPT_CHAIN" 2>/dev/null; do :; done - iptw "$ipt" -I OUTPUT 1 -j "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || return 1 -} - -fill_net_chain() { - local ipt="$1" reject="$2" - iptw "$ipt" -F "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || return 1 - # Always-allowed plumbing: loopback, established flows, the OS itself, the - # daemon/ADB (root + shell), and DNS (apps resolve via netd, a different - # uid, so allow port 53 broadly or every lookup fails under the cut-off). - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -o lo -j ACCEPT 2>/dev/null || true - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m state --state ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || true - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 0 -j ACCEPT 2>/dev/null || true - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 1000 -j ACCEPT 2>/dev/null || true - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 2000 -j ACCEPT 2>/dev/null || true - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true - # Allow each whitelisted app UID, read from the cache (refreshed once per - # main tick). Reading the cache instead of calling night_uids() here keeps - # the fast watchdog fork-free (no `pm list packages` on every rebuild). - local uid - if [ -f "$CURFEW_NET_UID_CACHE" ]; then - # Piped for the same reason as night_uids above: a trailing - # `done < file` is a line kcov counts and bash never reports. The - # loop's only effect is the iptw calls, which are external commands, - # so the pipe's subshell is immaterial here. - cat "$CURFEW_NET_UID_CACHE" | while IFS= read -r uid; do - case "$uid" in ''|*[!0-9]*) continue ;; esac - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner "$uid" -j ACCEPT 2>/dev/null || true - done - fi - # Cut off every remaining ordinary APP uid (10000-19999 = user-0 app range). - # Scoped to the app range so kernel/system sockets (no owner / low uids) are - # never touched — far safer than a blanket default-DROP. - iptw "$ipt" -A "$CURFEW_NET_IPT_CHAIN" -m owner --uid-owner 10000-19999 -j REJECT \ - --reject-with "$reject" 2>/dev/null || true -} - -# Run iptables/ip6tables with a 2s xtables lock-wait. Android's netd runs its -# own concurrent `iptables-restore`; without -w our calls silently fail the -# instant netd holds the lock (proven on-device: partial 19-rule chains and -# multi-second outages). -w queues for the lock so our calls actually land. -# iptables 1.8.7 legacy supports it. $1 = binary (iptables/ip6tables). -iptw() { - local bin="$1" - shift - "$bin" -w 2 "$@" -} - -# Refresh the cached UID list (one `pm list packages -U` fork). Called once per -# main tick so the fast watchdog can rebuild from the cache without forking. -refresh_uid_cache() { - if night_uids > "$CURFEW_NET_UID_CACHE.tmp" 2>/dev/null; then - mv "$CURFEW_NET_UID_CACHE.tmp" "$CURFEW_NET_UID_CACHE" 2>/dev/null || true - fi -} - -# Rebuild the chain from cache for whichever iptables variants exist. No pm fork. -rebuild_net_from_cache() { - if command -v iptables >/dev/null 2>&1; then - ensure_net_chain iptables && fill_net_chain iptables icmp-port-unreachable - fi - if command -v ip6tables >/dev/null 2>&1; then - ensure_net_chain ip6tables && fill_net_chain ip6tables icmp6-port-unreachable - fi -} - -# Fast watchdog: for `total` seconds, every CURFEW_NET_REASSERT_INTERVAL check -# whether netd wiped our chain and, if so, re-pin it from cache. Replaces the -# plain inter-tick sleep while curfew is active so the leak window drops from -# the full 5s tick to <=1s. Echoes the number of rebuilds it performed. -net_hold() { - local total="$1" elapsed=0 rebuilds=0 step="${CURFEW_NET_REASSERT_INTERVAL:-1}" - while [ "$elapsed" -lt "$total" ]; do - sleep "$step" - elapsed=$((elapsed + step)) - if command -v iptables >/dev/null 2>&1 \ - && ! iptw iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then - rebuild_net_from_cache - rebuilds=$((rebuilds + 1)) - fi - done - echo "$rebuilds" -} - -apply_net() { - [ "${CURFEW_NET_ENABLED:-0}" = "1" ] || return 0 - refresh_uid_cache - # Discriminating probe: if we already built the chain on a prior tick but it - # is gone now, an external actor wiped it (Android netd rewriting the filter - # table, or a manual flush during debugging). Log each disappearance so the - # live test reads "flush + self-heal" vs "dead process" directly, instead of - # inferring it from log silence. - if [ -n "$NET_BUILT" ] && command -v iptables >/dev/null 2>&1 \ - && ! iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then - log "net chain $CURFEW_NET_IPT_CHAIN vanished since last tick - rebuilding (external flush?)" - fi - if command -v iptables >/dev/null 2>&1; then - ensure_net_chain iptables && fill_net_chain iptables icmp-port-unreachable - fi - if command -v ip6tables >/dev/null 2>&1; then - ensure_net_chain ip6tables && fill_net_chain ip6tables icmp6-port-unreachable - fi - NET_BUILT=1 -} - -teardown_net() { - local ipt - for ipt in iptables ip6tables; do - command -v "$ipt" >/dev/null 2>&1 || continue - while iptw "$ipt" -D OUTPUT -j "$CURFEW_NET_IPT_CHAIN" 2>/dev/null; do :; done - iptw "$ipt" -F "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || true - iptw "$ipt" -X "$CURFEW_NET_IPT_CHAIN" 2>/dev/null || true - done -} diff --git a/phone_focus_mode/daemon_apps.sh b/phone_focus_mode/daemon_apps.sh deleted file mode 100755 index 330fb330..00000000 --- a/phone_focus_mode/daemon_apps.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# daemon_apps.sh — the app layer of focus_daemon.sh: deciding whether a -# package is allowed, disabling everything that is not while focus mode is on, -# and re-enabling it afterwards. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory. -# -# Sourced by focus_daemon.sh, which owns log(), $CURRENT_MODE (written here -# and in init, read by main, so it stays with them) and the config.sh globals -# these read ($DISABLED_APPS_FILE, $MODE_FILE, $STATE_DIR). -# -# NOTE for anyone editing the `while IFS= read -r pkg; do ... done "$tmp_disabled" - - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - - if is_allowed "$pkg"; then - pm install-existing --user 0 "$pkg" >/dev/null 2>&1 || true - pm enable "$pkg" >/dev/null 2>&1 || true - log "Re-enabled allowed app during state reconciliation: $pkg" - continue - fi - - echo "$pkg" >>"$tmp_disabled" - done <"$DISABLED_APPS_FILE" - - mv "$tmp_disabled" "$DISABLED_APPS_FILE" -} - -# ---- Check if package is allowed (whitelist or system-protected) ---- -is_allowed() { - local pkg="$1" - # During the night curfew, swap the permissive day list for the strict - # night list. The sysprotect + default-handler guards below still apply on - # top of whichever list is active. - local list="$STATE_DIR/whitelist.txt" - if curfew_active; then - list="$STATE_DIR/night_whitelist.txt" - fi - # Exact match against the active whitelist file - if grep -qxF "$pkg" "$list" 2>/dev/null; then - return 0 - fi - # Prefix match against system-protect file - while IFS= read -r prefix; do - [ -z "$prefix" ] && continue - case "$pkg" in - "$prefix"*) return 0 ;; - esac - done <"$STATE_DIR/sysprotect.txt" - # Hard-stop guard: refuse to disable any package that is the current - # default handler for a critical role (Dialer / SMS / Home / Contacts). - # Without this, a misconfigured WHITELIST can disable the default Phone - # app and Android falls back to com.android.settings/.FallbackHome - - # the persistent "Phone is starting..." screen with broken SystemUI - # gestures (no swipe-up recents). Recovering requires `pm enable` over - # ADB. Treat the guard as last-resort safety net independent of WHITELIST - # contents so a future config edit can never wipe these out. - is_default_handler "$pkg" && return 0 - # The default browser is guarded only OUTSIDE curfew. At night the whole - # point is to disable browsers, so this guard must not re-allow it. - if ! curfew_active && - grep -qxF "$pkg" "$STATE_DIR/default_browser.txt" 2>/dev/null; then - return 0 - fi - return 1 -} - -enable_focus_mode() { - local first_entry=0 - if [ "$CURRENT_MODE" != "focus" ]; then - first_entry=1 - log "ENABLING focus mode - restricting non-whitelisted apps" - : >"$DISABLED_APPS_FILE" - fi - - # Refresh default-handler list every tick. The user may switch dialer / - # SMS / launcher between sweeps; the guard in is_allowed() consults this - # list so a newly-promoted handler is never disabled. - refresh_default_handlers - - # Blocked system app list is static; built once in init() (see - # build_blocked_sys_file), not rebuilt on every sweep. - local blocked_sys="$STATE_DIR/blocked_sys.txt" - - # Periodic rescan catches third-party apps the user re-enabled (e.g. via - # Play Store or `pm enable` in a terminal) since the last tick. - # -e = enabled only, so we skip apps that are already disabled. - local tmp_pkgs="$STATE_DIR/pkg_list.txt" - pm list packages -3 -e 2>/dev/null | sed 's/^package://' >"$tmp_pkgs" - local newly_disabled=0 - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - is_allowed "$pkg" && continue - if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then - grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null || - echo "$pkg" >>"$DISABLED_APPS_FILE" - newly_disabled=$((newly_disabled + 1)) - fi - done <"$tmp_pkgs" - rm -f "$tmp_pkgs" - - # Disable-for-user-0 any blocked system apps (Play Store, browsers, - # package installer UI, terminal apps). - # IMPORTANT: We intentionally use pm disable-user (NOT pm uninstall) here. - # pm uninstall -k --user 0 removes the package from Android's user-0 - # package registry. If the daemon is killed with SIGKILL during a reboot - # (bypassing the cleanup trap), those packages stay uninstalled across the - # reboot. Android's bootloop-protection (MTK and others) then detects - # missing critical system packages and triggers recovery / factory wipe. - # pm disable-user leaves the package registered but inactive, so the - # PackageManager scan at next boot succeeds and no wipe occurs. - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then - grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null || - echo "$pkg" >>"$DISABLED_APPS_FILE" - newly_disabled=$((newly_disabled + 1)) - fi - done <"$blocked_sys" - - CURRENT_MODE="focus" - echo "focus" >"$MODE_FILE" - - if [ "$first_entry" -eq 1 ]; then - local count - count=$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null || echo 0) - log "Focus mode enabled - disabled $count apps" - elif [ "$newly_disabled" -gt 0 ]; then - log "Focus mode re-sweep: re-disabled $newly_disabled apps (re-enabled by user?)" - fi - - reconcile_disabled_apps -} - -disable_focus_mode() { - [ "$CURRENT_MODE" = "normal" ] && return - log "DISABLING focus mode - re-enabling apps" - - local count=0 - if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then - # Re-enable all disabled apps (both 3rd-party and system apps). - # Both paths now use pm disable-user, so pm enable is the only - # restore command needed. - while IFS= read -r pkg; do - [ -z "$pkg" ] && continue - pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1)) - done <"$DISABLED_APPS_FILE" - : >"$DISABLED_APPS_FILE" - fi - - CURRENT_MODE="normal" - echo "normal" >"$MODE_FILE" - log "Focus mode disabled - re-enabled $count apps" -} diff --git a/phone_focus_mode/daemon_location.sh b/phone_focus_mode/daemon_location.sh deleted file mode 100755 index 3bb75d86..00000000 --- a/phone_focus_mode/daemon_location.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# daemon_location.sh — where-and-when for focus_daemon.sh: reading the phone's -# GPS fix, the great-circle distance from home, and whether the night curfew -# window is currently open. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory. -# -# Sourced by focus_daemon.sh, which owns log() and the config.sh globals these -# read ($NIGHT_CURFEW_*, $CURFEW_FORCE_FILE, $CURFEW_OVERRIDE_FILE). It also -# owns $_CURFEW_TICK_CACHED: curfew_active memoises per tick and main clears -# the memo each loop, so both files write it and it stays with main. - -# ---- Location ---- -get_location() { - dumpsys location 2>/dev/null | - grep -oE '[-]?[0-9]{1,3}\.[0-9]{4,},[-]?[0-9]{1,3}\.[0-9]{4,}' | - head -1 -} - -# ---- Distance Calculation (Haversine via awk) ---- -calc_distance() { - echo "$1 $2 $3 $4" | awk '{ - PI = 3.14159265358979323846 - R = 6371000.0 - lat1 = $1 * PI / 180.0 - lon1 = $2 * PI / 180.0 - lat2 = $3 * PI / 180.0 - lon2 = $4 * PI / 180.0 - dlat = lat2 - lat1 - dlon = lon2 - lon1 - sdlat = sin(dlat / 2.0) - sdlon = sin(dlon / 2.0) - a = sdlat * sdlat + cos(lat1) * cos(lat2) * sdlon * sdlon - c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a)) - printf "%d\n", R * c - }' -} - -# ---- Night curfew time check ---- -# Returns 0 (true) when the local clock is inside the curfew window. -# Fails OPEN (return 1 = not curfew) on a malformed clock so a broken `date` -# can never strand you behind the strict list — essentials stay reachable -# either way, but the day list is the less-surprising default. -_dec() { - # Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed - # as (sometimes invalid) octal by the shell's arithmetic. Portable across - # ash/mksh; keeps at least one digit so "0000" -> "0". - local n="$1" - while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done - printf '%s' "$n" -} - -is_curfew_now() { - local now start end - now="$(date +%H%M 2>/dev/null)" - case "$now" in - '' | *[!0-9]*) return 1 ;; - esac - now="$(_dec "$now")" - start="$(_dec "$NIGHT_CURFEW_START")" - end="$(_dec "$NIGHT_CURFEW_END")" - if [ "$start" -le "$end" ]; then - [ "$now" -ge "$start" ] && [ "$now" -lt "$end" ] - else - # Window wraps past midnight (e.g. 2300 -> 0500). - [ "$now" -ge "$start" ] || [ "$now" -lt "$end" ] - fi -} - -# Curfew is ACTIVE when enabled, not manually overridden, and either forced on -# (test hook) or inside the time window. The is_allowed() switch below consults -# this; because is_allowed() only runs during the focus-mode sweep/reconcile, -# curfew automatically takes effect only at home and is a no-op when away. -# -# Memoized once per main-loop tick (reset at the top of main()'s while loop): -# is_allowed() calls this once per enabled package in the sweep, and -# is_curfew_now forks `date` - on-device this meant up to ~N extra forks per -# 10s tick for N enabled apps, all recomputing a value that cannot change -# within a single tick. Result is cached in _CURFEW_TICK_RESULT. -curfew_active() { - if [ -n "$_CURFEW_TICK_CACHED" ]; then - [ "$_CURFEW_TICK_RESULT" = "1" ] - return - fi - _CURFEW_TICK_CACHED=1 - _CURFEW_TICK_RESULT=0 - if [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] && [ ! -e "$CURFEW_OVERRIDE_FILE" ]; then - if [ -e "$CURFEW_FORCE_FILE" ] || is_curfew_now; then - _CURFEW_TICK_RESULT=1 - fi - fi - [ "$_CURFEW_TICK_RESULT" = "1" ] -} diff --git a/phone_focus_mode/daemon_state.sh b/phone_focus_mode/daemon_state.sh deleted file mode 100755 index 2fceb76e..00000000 --- a/phone_focus_mode/daemon_state.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# daemon_state.sh — the state files focus_daemon.sh derives from config: the -# whitelists, the system-protect and blocked-system lists, the cached default -# intent handlers, and the JSON status snapshot the companion app reads. -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory. -# -# Sourced by focus_daemon.sh, which owns log() and the config.sh globals these -# read ($WHITELIST, $NIGHT_WHITELIST, $SYSTEM_NEVER_DISABLE, -# $BLOCKED_SYSTEM_APPS, $STATE_DIR, $STATUS_FILE, $RADIUS). Nothing here is -# assigned, so there is no global to keep in step. - -build_whitelist_file() { - echo "$WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' | - sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/whitelist.txt" - # Sanity check: the WHITELIST string in config.sh is fragile - any - # literal double-quote inside a comment will close the heredoc and - # silently truncate the variable. Log the parsed line count so any - # future regression is visible in the log, and warn loudly if it - # falls below a known floor (we always have ~70+ entries). - local n - n=$(wc -l <"$STATE_DIR/whitelist.txt" 2>/dev/null | tr -d ' ') - log "Whitelist parsed: $n entries" - if [ "${n:-0}" -lt 30 ]; then - log "WARN: whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside WHITELIST string" - fi -} - -build_night_whitelist_file() { - # Strict allow-list used while the night curfew is active (see config.sh - # NIGHT_WHITELIST and is_curfew_now()). Parsed exactly like the day list. - echo "$NIGHT_WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' | - sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/night_whitelist.txt" - local n - n=$(wc -l <"$STATE_DIR/night_whitelist.txt" 2>/dev/null | tr -d ' ') - log "Night-curfew whitelist parsed: $n entries" - if [ "${n:-0}" -lt 10 ]; then - log "WARN: night whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside NIGHT_WHITELIST string" - fi -} - -build_sysprotect_file() { - echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' | - sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/sysprotect.txt" -} - -# $BLOCKED_SYSTEM_APPS is a static config value (never changes without a -# daemon restart), so - like build_sysprotect_file above - this only needs -# to run once at startup, not every enable_focus_mode() sweep. -build_blocked_sys_file() { - echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' | - sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/blocked_sys.txt" -} - -# ---- Default handler detection ---- -# Refreshed once per focus_daemon tick into $STATE_DIR/default_handlers.txt. -# Each line is a package name. Lookup is a cheap grep against this file. -refresh_default_handlers() { - local f="$STATE_DIR/default_handlers.txt" - local tmp="$f.tmp" - : >"$tmp" - # Default Home (launcher). resolve-activity prints "Activity Resolver Table:" - # on line 1 and "/<.Activity>" on line 2 in --brief mode. - cmd package resolve-activity --brief \ - -c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null | - awk -F/ 'NR==2 && $1 != "" {print $1}' >>"$tmp" - # Default Dialer - local dialer - dialer="$(cmd telecom get-default-dialer 2>/dev/null | tr -d '[:space:]')" - [ -n "$dialer" ] && echo "$dialer" >>"$tmp" - # Default SMS handler (settings provider key) - local sms - sms="$(settings get secure sms_default_application 2>/dev/null | tr -d '[:space:]')" - [ -n "$sms" ] && [ "$sms" != "null" ] && echo "$sms" >>"$tmp" - # Default input method (active keyboard). Disabling the active IME with - # pm disable-user PERSISTS across reboot; a 1am reboot would then leave no - # keyboard to type any recovery command. Protect it day and night so the - # curfew can never lock you out of typing. - local ime - ime="$(settings get secure default_input_method 2>/dev/null | cut -d/ -f1)" - [ -n "$ime" ] && [ "$ime" != "null" ] && echo "$ime" >>"$tmp" - sort -u "$tmp" -o "$f" - rm -f "$tmp" - - # Default Browser handler is tracked SEPARATELY and guarded only OUTSIDE - # the curfew window (see is_allowed). During curfew the whole point is to - # disable browsers, so the default-handler guard must not resurrect them. - local bf="$STATE_DIR/default_browser.txt" - cmd package resolve-activity --brief \ - -a android.intent.action.VIEW -d http://example.com 2>/dev/null | - awk -F/ 'NR==2 && $1 != "" {print $1}' >"$bf.tmp" 2>/dev/null - mv "$bf.tmp" "$bf" 2>/dev/null || : >"$bf" -} - -is_default_handler() { - local pkg="$1" - grep -qxF "$pkg" "$STATE_DIR/default_handlers.txt" 2>/dev/null -} - -# ---- Status snapshot for companion notification app ---- -# Writes a tiny JSON file that focus_status_app reads every few seconds. -# Fields: mode, lat, lon, distance_m, threshold_m, radius_m, disabled_count, -# last_check_ts (unix), last_check_iso (human). -write_status_snapshot() { - local mode="$1" lat="$2" lon="$3" dist="$4" thr="$5" - local count iso ts cf ov frc - count="$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null | tr -d ' ' || echo 0)" - [ -z "$count" ] && count=0 - ts="$(date +%s)" - iso="$(date '+%Y-%m-%d %H:%M:%S')" - # Curfew state for the companion app: 1/0 so it slots into the existing - # numeric JSON path. "curfew" = restrictions active now; "curfew_override" - # = the escape-hatch file is set (curfew suspended). - if curfew_active; then cf=1; else cf=0; fi - if [ -e "$CURFEW_OVERRIDE_FILE" ]; then ov=1; else ov=0; fi - # "curfew_force" = the demo/test force file is set (curfew forced on - # regardless of clock). Lets the companion app show Start/Stop demo. - if [ -e "$CURFEW_FORCE_FILE" ]; then frc=1; else frc=0; fi - local tmp="$STATUS_FILE.tmp" - # Shell-emitted JSON — keep values numeric where possible, strings quoted. - { - printf '{' - printf '"mode":"%s",' "$mode" - printf '"lat":"%s",' "${lat:-}" - printf '"lon":"%s",' "${lon:-}" - printf '"distance_m":%s,' "${dist:-null}" - printf '"threshold_m":%s,' "${thr:-null}" - printf '"radius_m":%s,' "$RADIUS" - printf '"disabled_count":%s,' "$count" - printf '"curfew":%s,' "$cf" - printf '"curfew_override":%s,' "$ov" - printf '"curfew_force":%s,' "$frc" - printf '"last_check_ts":%s,' "$ts" - printf '"last_check_iso":"%s"' "$iso" - printf '}\n' - } >"$tmp" 2>/dev/null || return 0 - mv "$tmp" "$STATUS_FILE" 2>/dev/null || true - chmod 644 "$STATUS_FILE" 2>/dev/null || true -} diff --git a/phone_focus_mode/deploy.sh b/phone_focus_mode/deploy.sh deleted file mode 100755 index 360a7487..00000000 --- a/phone_focus_mode/deploy.sh +++ /dev/null @@ -1,241 +0,0 @@ -#!/bin/bash -# ============================================================ -# Focus Mode Deployment Script -# Deploys focus mode to your rooted BL-9000 via wireless ADB -# -# Usage: -# ./deploy.sh [phone_ip] - Full deploy (first time or update) -# ./deploy.sh [phone_ip] --status - Check status -# ./deploy.sh [phone_ip] --log - View log -# ./deploy.sh [phone_ip] --stop - Stop daemon -# ./deploy.sh [phone_ip] --enable - Force focus mode on -# ./deploy.sh [phone_ip] --disable - Force focus mode off -# ============================================================ - -set -euo pipefail - -_DEPLOY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# shellcheck source=deploy_actions.sh -. "${_DEPLOY_LIB_DIR}/deploy_actions.sh" -# shellcheck source=deploy_gps.sh -. "${_DEPLOY_LIB_DIR}/deploy_gps.sh" -# shellcheck source=deploy_magisk.sh -. "${_DEPLOY_LIB_DIR}/deploy_magisk.sh" -# shellcheck source=deploy_phases.sh -. "${_DEPLOY_LIB_DIR}/deploy_phases.sh" -# shellcheck source=deploy_install.sh -. "${_DEPLOY_LIB_DIR}/deploy_install.sh" -# shellcheck source=deploy_daemons.sh -. "${_DEPLOY_LIB_DIR}/deploy_daemons.sh" - -PHONE_IP="${1:-}" -ACTION="${2:---deploy}" -REMOTE_DIR="/data/local/tmp/focus_mode" -# Deliberately NOT named SCRIPT_DIR: config.sh (sourced below, at top -# level) assigns SCRIPT_DIR itself and may repoint it at the on-device -# path /data/local/tmp/focus_mode. Every path here must stay anchored to -# the checkout deploy.sh runs from. -DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" -NEEDS_GPS_FETCH=0 # set to 1 by check_coords when local coords are placeholder - -# Source shared config constants (BROWSER_PACKAGES, REMOTE_DIR, etc.) -# shellcheck source=config.sh -. "$DEPLOY_DIR/config.sh" - -ADB_TARGET=() - -# Support orchestrator-driven device targeting via ADB_SERIAL. -# When ADB_SERIAL is set, deploy.sh uses that target directly and preserves -# the existing PHONE_IP workflow when ADB_SERIAL is unset. -if [[ -n "${ADB_SERIAL:-}" ]]; then - ADB_TARGET=(-s "${ADB_SERIAL}") - if [[ -z "${PHONE_IP}" || "${PHONE_IP}" == --* ]]; then - ACTION="${PHONE_IP:---deploy}" - PHONE_IP="" - fi -fi - -adb_cmd() { - adb "${ADB_TARGET[@]}" "$@" -} - - -# ---- Pre-flight checks ---- -check_adb() { - if ! command -v adb >/dev/null 2>&1; then - echo "ERROR: adb not found. Install Android platform-tools first." - echo " Ubuntu/Debian: sudo apt install adb" - echo " Arch: sudo pacman -S android-tools" - exit 1 - fi -} - - -check_ip() { - if [[ -n "${ADB_SERIAL:-}" ]]; then - return 0 - fi - - if [ -z "$PHONE_IP" ]; then - echo "ERROR: Phone IP not provided." - echo "" - usage - fi -} - -connect_adb() { - if [[ -n "${ADB_SERIAL:-}" ]]; then - if ! adb devices | awk 'NR>1 && $2=="device"{print $1}' | grep -Fxq "${ADB_SERIAL}"; then - echo "ERROR: ADB_SERIAL '${ADB_SERIAL}' is not connected." - echo "Connect device via USB or pair wireless ADB first." - exit 1 - fi - ADB_TARGET=(-s "${ADB_SERIAL}") - echo "Using ADB_SERIAL target: ${ADB_SERIAL}" - return 0 - fi - - echo "Connecting to $PHONE_IP:5555 ..." - adb connect "$PHONE_IP:5555" - sleep 1 - if ! adb devices | grep -q "$PHONE_IP"; then - echo "ERROR: Could not connect to $PHONE_IP:5555" - echo "Make sure wireless ADB is enabled and the phone is reachable." - exit 1 - fi - ADB_TARGET=(-s "$PHONE_IP:5555") - echo "Connected." -} - -# Wrapper: run a root shell command on the phone. -# --mount-master was removed in Magisk v26+; plain su -c works and still -# runs in the correct mount namespace for our use cases. -adb_root() { - local command_text="$1" - - printf '%s\n' "$command_text" | adb_cmd shell su -c "sh -s" -} - - -# ============================================================ -# GPS HOME COORDINATE CAPTURE -# ============================================================ -# Called when config_secrets.sh has placeholder/non-numeric coords. -# Enables Android location, waits up to GPS_MAX_WAIT_SECS for a -# network/fused fix, and prints "lat lon" on stdout. All progress -# messages go to stderr so the caller can capture only the coords. -GPS_MAX_WAIT_SECS=90 - - -# ============================================================ -# MAGISK SYSTEMLESS HOSTS AUTO-INSTALL -# ============================================================ -# Creates the module dir+module.prop if absent, removes disable -# markers if disabled, then reboots the device and waits up to -# HOSTS_MODULE_REBOOT_WAIT_SECS for it to come back with the -# magic-mount active. No-ops if the module is already OK. -HOSTS_MODULE_REBOOT_WAIT_SECS=180 - - -# ============================================================ -# AURORA STORE -# ============================================================ -# Aurora Store is a free, open-source Play Store client that lets you -# install apps anonymously without a Google account. We use it so that -# Play Store (com.android.vending) can be network-blocked during focus -# mode without preventing legitimate app installs at other times. -# -# Official release APK is hosted on the Aurora OSS GitLab. We pin a -# known version tag and verify the hash on every install. -AURORA_VERSION="4.8.1" -AURORA_APK_URL="https://gitlab.com/-/project/6922885/uploads/2ee95ec85244b45cc860b63ec7a10ad6/AuroraStore-4.8.1.apk" -AURORA_PACKAGE="com.aurora.store" - - -# ============================================================ -# DEPLOY -# ============================================================ -do_deploy() { - echo "=== Focus Mode Deployer ===" - echo "" - if check_coords; then - NEEDS_GPS_FETCH=1 - else - NEEDS_GPS_FETCH=0 - fi - echo "" - - echo "[1/7] Connecting to phone..." - connect_adb - - echo "[2/7] Verifying root access..." - if ! adb_root "id" | grep -q "uid=0"; then - echo "ERROR: Could not get root shell. Is Magisk installed?" - exit 1 - fi - echo " Root confirmed." - - echo "[2.5] Ensuring Magisk Systemless Hosts module..." - ensure_magisk_hosts_module - - echo "[3/7] Creating directories on device..." - # Use world-writable staging dir so non-root adb push works - adb_cmd shell "mkdir -p /data/local/tmp/focus_stage" - adb_root "mkdir -p $REMOTE_DIR /data/adb/service.d" - adb_root "chmod 777 /data/local/tmp/focus_stage" - - _deploy_push_scripts -} - - - - - - - - - - - - - -# ============================================================ -# Entry point -# ============================================================ -check_adb -check_ip - -case "$ACTION" in ---deploy | "") do_deploy ;; ---status) do_control "status" ;; ---log) - connect_adb - adb_root "sh $REMOTE_DIR/focus_ctl.sh log 100" - ;; ---stop) do_control "stop" ;; ---start) do_control "start" ;; ---restart) do_control "restart" ;; ---enable) do_control "enable" ;; ---disable) do_control "disable" ;; ---list) do_control "list-apps" ;; ---pull-log) do_pull_log ;; ---find-pkg) do_find_pkg "$@" ;; ---hosts-status) do_control "hosts-status" ;; ---hosts-log) - connect_adb - adb_root "sh $REMOTE_DIR/focus_ctl.sh hosts-log 100" - ;; ---launcher-status) do_control "launcher-status" ;; ---launcher-log) - connect_adb - adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-log 100" - ;; ---capture-coords) do_capture_coords ;; ---snapshot-launcher) do_snapshot_launcher ;; ---install-aurora) do_install_aurora ;; -*) - echo "Unknown action: $ACTION" - usage - ;; -esac diff --git a/phone_focus_mode/deploy_actions.sh b/phone_focus_mode/deploy_actions.sh deleted file mode 100755 index bae66b3e..00000000 --- a/phone_focus_mode/deploy_actions.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash -# deploy_actions.sh — deploy.sh's non-deploy actions: the help text, passing a -# control command through to focus_ctl.sh on the phone, pulling the log, -# searching installed packages, and snapshotting the launcher APK. -# -# Sourced by deploy.sh, which owns adb_cmd, adb_root and $REMOTE_DIR. - -usage() { - echo "Usage: $0 [action]" - echo " or: ADB_SERIAL= $0 [action]" - echo "" - echo "Actions:" - echo " (none) Full deploy" - echo " --status Show daemon status and current mode" - echo " --log Tail the daemon log" - echo " --stop Stop daemon (re-enables all apps)" - echo " --start Start daemon" - echo " --restart Restart daemon" - echo " --enable Force focus mode on" - echo " --disable Force focus mode off" - echo " --list List all third-party apps and whitelist status" - echo " --pull-log Download log file locally" - echo " --find-pkg Show installed packages matching a filter (e.g. --find-pkg pomodoro)" - echo " --capture-coords Capture current GPS as home location (run after WiFi setup)" - echo " --hosts-status Show hosts enforcer status on the phone" - echo " --hosts-log Show hosts enforcer log on the phone" - echo " --launcher-status Show launcher enforcer status on the phone" - echo " --launcher-log Show launcher enforcer log on the phone" - echo " --snapshot-launcher Snapshot installed Minimalist Phone APK + default HOME" - echo " --install-aurora Download & install Aurora Store (open-source Play Store alt)" - echo "" - echo "Examples:" - echo " $0 192.168.1.42" - echo " $0 192.168.1.42 --status" - echo " $0 192.168.1.42 --find-pkg stronglift" - exit 1 -} - -# ============================================================ -# Control actions (post-deploy) -# ============================================================ -do_control() { - local ctl_cmd="$1" - connect_adb - adb_root "sh $REMOTE_DIR/focus_ctl.sh $ctl_cmd" -} - -do_pull_log() { - connect_adb - echo "Downloading log..." - adb_cmd pull "$REMOTE_DIR/focus_mode.log" "./focus_mode_$(date +%Y%m%d_%H%M%S).log" - echo "Done." -} - -do_find_pkg() { - local filter="${3:-}" - if [ -z "$filter" ]; then - echo "Usage: $0 --find-pkg " - exit 1 - fi - connect_adb - echo "Packages matching '$filter':" - adb_cmd shell pm list packages | grep -i "$filter" | sed 's/^package:/ /' -} - -do_snapshot_launcher() { - # Run the on-device snapshot command. This captures the APK + HOME - # activity of the already-installed Minimalist Phone launcher into - # /data/adb/focus_mode/ so the launcher enforcer can restore it later. - # The user must install the launcher once (via Aurora/Play) before - # running this command - we only back up what's already there. - connect_adb - echo "Snapshotting currently-installed launcher APK..." - adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-snapshot" - echo "" - echo "Starting launcher enforcer..." - # Kill any previous enforcer so it picks up the new snapshot. - adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "rm -f $REMOTE_DIR/launcher_enforcer.pid" - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh /dev/null 2>/dev/null &' - sleep 3 - adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-status" -} diff --git a/phone_focus_mode/deploy_daemons.sh b/phone_focus_mode/deploy_daemons.sh deleted file mode 100755 index 3765d5e7..00000000 --- a/phone_focus_mode/deploy_daemons.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/bin/bash -# deploy_daemons.sh — the last phases of a deploy: restarting the enforcers, -# installing the companion status-notification app, and printing the closing -# summary. -# -# Sourced by deploy.sh, which owns adb_cmd, adb_root and $REMOTE_DIR. - -# Phase 6: stop anything already running, then start each enforcer. -_deploy_start_daemons() { - echo "[6/7] Starting daemons..." - # Stop existing daemons, then start fresh - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done" - adb_root "kill \$(cat $REMOTE_DIR/daemon.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/hosts_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/dns_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/tether_enforcer.pid 2>/dev/null) 2>/dev/null; true" - adb_root "kill \$(cat $REMOTE_DIR/workout_detector.pid 2>/dev/null) 2>/dev/null; true" - sleep 1 - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done" - sleep 1 - adb_root "rm -f $REMOTE_DIR/daemon.pid $REMOTE_DIR/hosts_enforcer.pid $REMOTE_DIR/dns_enforcer.pid $REMOTE_DIR/launcher_enforcer.pid $REMOTE_DIR/tether_enforcer.pid $REMOTE_DIR/workout_detector.pid" - # Start hosts enforcer first so hosts are locked before user can react. - # Use --mount-master so bind mounts propagate to the global namespace - # (where app processes live). Without this, only our isolated `su` session - # would see the bind-mounted hosts file. - if adb_root "test -f $REMOTE_DIR/hosts.canonical" 2>/dev/null; then - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/hosts_enforcer.sh /dev/null 2>/dev/null &' - fi - # Start workout detector BEFORE the hosts enforcer's first integrity check - # so the enforcer sees a non-stale workout_active flag. The detector itself - # is harmless if no workout is in progress (it just writes 0). - if adb_root "test -x $REMOTE_DIR/sqlite3" 2>/dev/null; then - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/workout_detector.sh /dev/null 2>/dev/null &' - fi - # Start DNS enforcer (forces Private DNS off, blocks DoH/DoT). Always on. - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/dns_enforcer.sh /dev/null 2>/dev/null &' - # Start launcher enforcer only if a snapshot APK exists. If not, warn the - # user to install Minimalist Phone + run --snapshot-launcher first. - if adb_root "test -f $REMOTE_DIR/minimalist_launcher.apk" 2>/dev/null; then - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh /dev/null 2>/dev/null &' - else - echo " NOTE: launcher snapshot missing. Install Minimalist Phone via Aurora Store, then run:" - echo " $0 $PHONE_IP --snapshot-launcher" - fi - # Start night-curfew enforcer (grayscale + DND + optional net allow-list). - # Always on; self-gates on the clock + focus mode, no-op during the day. - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/curfew_enforcer.sh /dev/null 2>/dev/null &' - # Start hotspot/tethering enforcer (blocks forwarded/tethered traffic so a - # second phone on our hotspot can't bypass focus mode). Always on; self-gates - # on focus mode, no-op while away from home. - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/tether_enforcer.sh /dev/null 2>/dev/null &' - adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/focus_daemon.sh /dev/null 2>/dev/null &' - - # Wait for hosts_enforcer to apply the bind mount and restart netd. - # hosts_enforcer.sh restarts netd once at startup (takes ~4 s); we wait - # 10 s total so the network is stable before the companion-app install. - sleep 10 - - _deploy_install_companion -} - -# Phase 7: build and install the companion status-notification app. -_deploy_install_companion() { - # ---- Companion status notification app ---- - APP_DIR="$DEPLOY_DIR/focus_status_app" - APK="$APP_DIR/build/focus_status.apk" - if [ -d "$APP_DIR" ]; then - echo "[7/7] Building & installing companion status-notification app..." - needs_rebuild=0 - if [ ! -f "$APK" ]; then - needs_rebuild=1 - elif [ "$APP_DIR/AndroidManifest.xml" -nt "$APK" ]; then - needs_rebuild=1 - elif [ "$APP_DIR/build.sh" -nt "$APK" ]; then - needs_rebuild=1 - elif find "$APP_DIR/java" -name '*.java' -newer "$APK" -print -quit 2>/dev/null | grep -q .; then - # Rebuild when any Java source changed, not just the manifest. - needs_rebuild=1 - fi - if [ "$needs_rebuild" -eq 1 ]; then - echo " Building APK..." - # Non-fatal: the companion UI is optional. If the Android SDK is - # missing (build.sh fails), warn and fall back to the existing APK - # rather than aborting the whole deploy and leaving the curfew core - # un-started. - if ! (cd "$APP_DIR" && bash build.sh) >/dev/null 2>&1; then - echo " WARNING: APK build failed (Android SDK missing?)." - echo " Keeping the previously-built APK if present;" - echo " the curfew daemons/enforcers are unaffected." - fi - fi - if [ -f "$APK" ]; then - echo " Installing APK..." - adb_cmd install -r "$APK" >/dev/null || true - # Grant runtime permission (Android 13+ requires it for notifications). - adb_cmd shell pm grant com.kuhy.focusstatus android.permission.POST_NOTIFICATIONS >/dev/null 2>&1 || true - # Pre-approve Magisk SU so the app never shows the approval prompt. - APP_UID="$( - adb_cmd shell dumpsys package com.kuhy.focusstatus 2>/dev/null | - awk 'match($0, /userId=[0-9]+/) {print substr($0, RSTART + 7, RLENGTH - 7); exit}' - )" - if [ -n "$APP_UID" ]; then - adb_cmd shell "su -c 'magisk --sqlite \"INSERT OR REPLACE INTO policies (uid,policy,until,logging,notification) VALUES ($APP_UID,2,0,1,1)\"'" >/dev/null 2>&1 || true - fi - # Launch the invisible activity which kicks off the foreground service. - adb_cmd shell am start -n com.kuhy.focusstatus/.LaunchActivity >/dev/null 2>&1 || true - echo " Companion app running (look for the ongoing 'Focus Mode' notification)." - else - echo " WARNING: APK build failed - skipping companion app install" - fi - fi - - _deploy_report -} - -# Print the closing summary and the commands the user will want next. -_deploy_report() { - echo "" - echo "=== Deploy complete! ===" - echo "" - echo "Checking status..." - adb_root "sh $REMOTE_DIR/focus_ctl.sh status" - echo "" - echo "Boot autostart is disabled by default (FOCUS_BOOT_AUTOSTART=0)." - echo "No Magisk service.d hook is installed unless FOCUS_BOOT_AUTOSTART=1 in config.sh." - echo "Launcher enforcement does not auto-start on boot unless LAUNCHER_BOOT_AUTOSTART=1 is set in config.sh." - echo "" - echo "Useful commands:" - echo " $0 $PHONE_IP --status # Check mode and location" - echo " $0 $PHONE_IP --log # View daemon log" - echo " $0 $PHONE_IP --list # See all apps and whitelist status" - echo " $0 $PHONE_IP --enable # Force focus mode on for testing" - echo " $0 $PHONE_IP --disable # Force focus mode off" - echo " $0 $PHONE_IP --install-aurora # Install Aurora Store (Play Store alternative)" -} diff --git a/phone_focus_mode/deploy_gps.sh b/phone_focus_mode/deploy_gps.sh deleted file mode 100755 index 1cdf9c2d..00000000 --- a/phone_focus_mode/deploy_gps.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash -# deploy_gps.sh — the home-coordinate capture: reading a fix off the phone -# when config_secrets.sh still holds placeholders, and the --capture-coords -# action that writes one deliberately. -# -# Sourced by deploy.sh, which owns adb_cmd and $DEPLOY_DIR. $NEEDS_GPS_FETCH -# is written by check_coords here and read by the deploy run, so it stays -# declared in deploy.sh with the other run-scoped state. - -check_coords() { - local lat lon - lat="$(grep '^.*HOME_LAT=' "$DEPLOY_DIR/config.sh" "$DEPLOY_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)" - lon="$(grep '^.*HOME_LON=' "$DEPLOY_DIR/config.sh" "$DEPLOY_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)" - if [ "$lat" = "0.000000" ] && [ "$lon" = "0.000000" ]; then - echo "ERROR: Home coordinates not set (all zeros)." - exit 1 - fi - # If both look like valid floats, use them; otherwise auto-capture from - # phone GPS. The answer is the EXIT STATUS rather than an assignment to - # $NEEDS_GPS_FETCH: this file would otherwise write a global it never - # reads (SC2034), and the linter runs without -x so each file has to - # stand alone. deploy.sh sets the flag from what this returns. - if [[ -n "$lat" && "$lat" =~ ^[+-]?[0-9]+\.[0-9]+$ && -n "$lon" && "$lon" =~ ^[+-]?[0-9]+\.[0-9]+$ ]]; then - echo " Home location: $lat, $lon" - return 1 - fi - echo " Home location: placeholder — will be captured from phone GPS at deploy time." - return 0 -} - -fetch_home_coords_from_phone() { - echo " Enabling location services on phone..." >&2 - adb_cmd shell settings put secure location_mode 3 2>/dev/null || true - - echo " Waiting for network/fused location fix (up to ${GPS_MAX_WAIT_SECS}s)..." >&2 - local waited=0 coords="" - while [[ -z "$coords" && $waited -lt $GPS_MAX_WAIT_SECS ]]; do - sleep 3 - waited=$((waited + 3)) - # Format on Android 10+: " last location=Location[fused LAT,LON ...]" - local raw - raw="$(adb_cmd shell dumpsys location 2>/dev/null | - grep 'last location=Location\[' | - grep -oE '[+-]?[0-9]+\.[0-9]+,[+-]?[0-9]+\.[0-9]+' | - head -1 || true)" - [[ -n "$raw" ]] && coords="$raw" - printf '.' >&2 - done - printf '\n' >&2 - - if [[ -z "$coords" ]]; then - echo "ERROR: No location fix after ${GPS_MAX_WAIT_SECS}s." >&2 - echo " Make sure the phone has cellular or WiFi data, then retry." >&2 - echo " Or set HOME_LAT/HOME_LON manually in config_secrets.sh." >&2 - return 1 - fi - - local lat="${coords%,*}" - local lon="${coords#*,}" - echo " GPS fix acquired: ${lat}, ${lon}" >&2 - printf '%s %s' "$lat" "$lon" -} - -do_capture_coords() { - # Standalone GPS capture for post-WiFi-setup use. - # Overwrites config_secrets.sh on the phone with the current location. - connect_adb - if ! adb_root "id" 2>/dev/null | grep -q "uid=0"; then - echo "ERROR: Root not available." - exit 1 - fi - echo "Capturing home coordinates from phone GPS..." - local gps_result gps_lat gps_lon - gps_result="$(fetch_home_coords_from_phone)" - gps_lat="${gps_result% *}" - gps_lon="${gps_result#* }" - adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \ - > $REMOTE_DIR/config_secrets.sh" - echo "Home coordinates updated on phone: ${gps_lat}, ${gps_lon}" - echo "Restarting focus daemon to apply new coordinates..." - adb_root "sh $REMOTE_DIR/focus_ctl.sh restart" 2>/dev/null || true - echo "Done." -} diff --git a/phone_focus_mode/deploy_install.sh b/phone_focus_mode/deploy_install.sh deleted file mode 100755 index 4c34736b..00000000 --- a/phone_focus_mode/deploy_install.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash -# deploy_install.sh — copying the staged files into $REMOTE_DIR and setting -# their permissions. -# -# Sourced by deploy.sh, which owns adb_root and $REMOTE_DIR. -# -# _deploy_install_files holds the SECOND of the two hardcoded file lists; the -# first is the push list in deploy_phases.sh. Keep them in step: a file in one -# and not the other is the failure mode this comment exists to prevent. - -# Copy everything from the staging directory into $REMOTE_DIR. This is -# the SECOND of the two hardcoded lists -- a sibling added to the push -# list in _deploy_push_scripts must be added here too, or it is staged -# and never lands. -_deploy_install_files() { - adb_root "cp /data/local/tmp/focus_stage/config.sh $REMOTE_DIR/config.sh" - adb_root "cp /data/local/tmp/focus_stage/config_paths.sh $REMOTE_DIR/config_paths.sh" - adb_root "cp /data/local/tmp/focus_stage/config_dns.sh $REMOTE_DIR/config_dns.sh" - adb_root "cp /data/local/tmp/focus_stage/config_curfew.sh $REMOTE_DIR/config_curfew.sh" - adb_root "cp /data/local/tmp/focus_stage/config_tether.sh $REMOTE_DIR/config_tether.sh" - adb_root "cp /data/local/tmp/focus_stage/config_launcher.sh $REMOTE_DIR/config_launcher.sh" - adb_root "cp /data/local/tmp/focus_stage/focus_daemon.sh $REMOTE_DIR/focus_daemon.sh" - adb_root "cp /data/local/tmp/focus_stage/daemon_location.sh $REMOTE_DIR/daemon_location.sh" - adb_root "cp /data/local/tmp/focus_stage/daemon_state.sh $REMOTE_DIR/daemon_state.sh" - adb_root "cp /data/local/tmp/focus_stage/daemon_apps.sh $REMOTE_DIR/daemon_apps.sh" - adb_root "cp /data/local/tmp/focus_stage/focus_ctl.sh $REMOTE_DIR/focus_ctl.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_usage.sh $REMOTE_DIR/ctl_usage.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_daemon.sh $REMOTE_DIR/ctl_daemon.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_hosts.sh $REMOTE_DIR/ctl_hosts.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_dns.sh $REMOTE_DIR/ctl_dns.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_launcher.sh $REMOTE_DIR/ctl_launcher.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_workout.sh $REMOTE_DIR/ctl_workout.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_curfew.sh $REMOTE_DIR/ctl_curfew.sh" - adb_root "cp /data/local/tmp/focus_stage/ctl_tether.sh $REMOTE_DIR/ctl_tether.sh" - adb_root "cp /data/local/tmp/focus_stage/hosts_enforcer.sh $REMOTE_DIR/hosts_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/hosts_magisk.sh $REMOTE_DIR/hosts_magisk.sh" - adb_root "cp /data/local/tmp/focus_stage/hosts_mount.sh $REMOTE_DIR/hosts_mount.sh" - adb_root "cp /data/local/tmp/focus_stage/dns_enforcer.sh $REMOTE_DIR/dns_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/dns_iptables.sh $REMOTE_DIR/dns_iptables.sh" - adb_root "cp /data/local/tmp/focus_stage/launcher_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/curfew_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/curfew_net.sh $REMOTE_DIR/curfew_net.sh" - adb_root "cp /data/local/tmp/focus_stage/tether_enforcer.sh $REMOTE_DIR/tether_enforcer.sh" - adb_root "cp /data/local/tmp/focus_stage/tether_iptables.sh $REMOTE_DIR/tether_iptables.sh" - adb_root "cp /data/local/tmp/focus_stage/workout_detector.sh $REMOTE_DIR/workout_detector.sh" - if adb_cmd shell "test -f /data/local/tmp/focus_stage/sqlite3" 2>/dev/null; then - adb_root "cp /data/local/tmp/focus_stage/sqlite3 $REMOTE_DIR/sqlite3" - adb_root "chmod 0755 $REMOTE_DIR/sqlite3" - fi - if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$DEPLOY_DIR/config.sh"; then - adb_root "cp /data/local/tmp/focus_stage/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh" - else - adb_root "rm -f /data/adb/service.d/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh.disabled" - fi - # Install canonical hosts and lock it down (only if generator produced it). - if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical" 2>/dev/null; then - # chattr -i first so we can overwrite a previously-locked canonical - adb_root "chattr -i $REMOTE_DIR/hosts.canonical 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.canonical $REMOTE_DIR/hosts.canonical" - adb_root "chmod 644 $REMOTE_DIR/hosts.canonical" - # Pre-compute the sha so the enforcer does not have to seed it. - adb_root "chattr -i $REMOTE_DIR/hosts.sha256 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.sha256 $REMOTE_DIR/hosts.sha256" - adb_root "chmod 644 $REMOTE_DIR/hosts.sha256" - adb_root "chattr +i $REMOTE_DIR/hosts.canonical 2>/dev/null; true" - adb_root "chattr +i $REMOTE_DIR/hosts.sha256 2>/dev/null; true" - - # ---- Workout-variant canonical (optional) ---- - # Same lockdown treatment as the full canonical. Pushed by the workout - # hosts generator block above. Missing variant means workout_detector\ - # will simply have no relaxed file to swap to (hosts_enforcer falls\ - # back to the full canonical). - if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical.workout" 2>/dev/null; then - adb_root "chattr -i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.canonical.workout $REMOTE_DIR/hosts.canonical.workout" - adb_root "chmod 644 $REMOTE_DIR/hosts.canonical.workout" - adb_root "chattr -i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true" - adb_root "cp /data/local/tmp/focus_stage/hosts.sha256.workout $REMOTE_DIR/hosts.sha256.workout" - adb_root "chmod 644 $REMOTE_DIR/hosts.sha256.workout" - adb_root "chattr +i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true" - adb_root "chattr +i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true" - fi - - # Magisk Systemless Hosts module was ensured (and rebooted if needed) in - # step [2.5] above. Sanity-assert it's still active before writing to it. - if ! adb_root "test -f /system/etc/hosts" 2>/dev/null; then - echo "ERROR: /system/etc/hosts not magic-mounted — run deploy again." - exit 1 - fi - - adb_root "mkdir -p /data/adb/modules/hosts/system/etc" - # Drop any +i lock the runtime hosts_enforcer may have set on the - # module dir / hosts file so we can update them. The enforcer will - # re-lock on its next poll cycle. Also pre-emptively delete any - # disable/remove markers that may exist on disk before we start. - adb_root "chattr -i /data/adb/modules/hosts /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; rm -f /data/adb/modules/hosts/disable /data/adb/modules/hosts/remove /data/adb/modules/hosts/update; true" - adb_root "cp $REMOTE_DIR/hosts.canonical /data/adb/modules/hosts/system/etc/hosts" - adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts" - # Lock the module dir to block the Magisk app's "Disable" / "Remove" - # buttons (they create marker files inside the dir). Files already - # in the dir stay mutable so the runtime enforcer can still update - # the hosts file on workout state changes. - adb_root "chattr +i /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; true" - adb_root "chattr +i /data/adb/modules/hosts 2>/dev/null; true" - echo " Magisk hosts module populated ($(adb_root "wc -l < /data/adb/modules/hosts/system/etc/hosts" 2>/dev/null | tr -d ' ') lines), locked against UI-disable. Reboot to activate /system/etc/hosts." - fi - adb_root "rm -rf /data/local/tmp/focus_stage" - - # Flush in-process DNS caches of browsers. Apps like Firefox and Chrome - # cache resolved IPs internally and bypass /etc/hosts until restarted. - echo " Flushing browser DNS caches..." - # BROWSER_PACKAGES is set by the top-level `. config.sh` above and is never - # modified. shellcheck only flags it because config.sh is ALSO re-sourced - # inside a subshell further up (to re-read WORKOUT_UNBLOCK_DOMAINS from the - # freshly-staged copy); that subshell's assignments are discarded by design. - # The unquoted expansion is the intended word split over the package list. - # shellcheck disable=SC2031 - for _pkg in $BROWSER_PACKAGES; do - [ -n "$_pkg" ] || continue - adb_root "am force-stop '$_pkg' 2>/dev/null; true" - echo " force-stopped $_pkg" - done - - # Disable Firefox DNS-over-HTTPS via user.js. Firefox uses hardcoded - # Cloudflare bootstrap IPs (104.16.248.249, 104.16.249.249) to reach - # mozilla.cloudflare-dns.com, completely bypassing /etc/hosts even - # after a fresh start. TRR mode 5 disables DoH so Firefox falls back - # to the system resolver which sees our 0.0.0.0 blocks. - echo " Disabling Firefox DNS-over-HTTPS..." - adb_root "for _p in /data/data/org.mozilla.fenix/files/mozilla/*/; do - [ -f \"\${_p}prefs.js\" ] || continue - grep -qF '\"network.trr.mode\"' \"\${_p}user.js\" 2>/dev/null \ - || { printf 'user_pref(\"network.trr.mode\", 5);\\n' >> \"\${_p}user.js\" 2>/dev/null && echo \" Wrote DoH-disable pref to \${_p}user.js\"; } - done; true" - - _deploy_set_permissions -} - -# Phase 5: make the staged scripts executable and their logs writable. -_deploy_set_permissions() { - echo "[5/7] Setting permissions..." - adb_root "chmod 755 $REMOTE_DIR/config.sh $REMOTE_DIR/focus_daemon.sh $REMOTE_DIR/focus_ctl.sh $REMOTE_DIR/hosts_enforcer.sh $REMOTE_DIR/dns_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh $REMOTE_DIR/tether_enforcer.sh $REMOTE_DIR/workout_detector.sh" || true - if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$DEPLOY_DIR/config.sh"; then - adb_root "chmod 755 /data/adb/service.d/99-focus-mode.sh" - fi - adb_root "touch $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log" - # State files need 666 so the daemons can write regardless of SELinux context drift - adb_root "chmod 666 $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log" || true - - _deploy_start_daemons -} diff --git a/phone_focus_mode/deploy_magisk.sh b/phone_focus_mode/deploy_magisk.sh deleted file mode 100755 index d7f95ca8..00000000 --- a/phone_focus_mode/deploy_magisk.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -# deploy_magisk.sh — the Magisk systemless-hosts module: creating it when -# absent, clearing its disable markers, and rebooting to bring the magic mount -# up. Also carries the Aurora Store installer and the file-hash helper, both -# of which are one-off setup rather than part of a deploy run. -# -# Sourced by deploy.sh, which owns adb_cmd and adb_root. - -compute_file_hash() { - local path="$1" - - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$path" | awk '{print $1}' - return 0 - fi - - md5sum "$path" | awk '{print $1}' -} - -ensure_magisk_hosts_module() { - local state="absent" - if adb_root "test -d /data/adb/modules/hosts" >/dev/null 2>&1; then - if adb_root "test -f /data/adb/modules/hosts/disable -o -f /data/adb/modules/hosts/remove" >/dev/null 2>&1; then - state="disabled" - elif adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then - state="ok" - else - state="not-mounted" - fi - fi - - if [[ "$state" == "ok" ]]; then - echo " Magisk Systemless Hosts: active." - return 0 - fi - - echo " Magisk Systemless Hosts state: ${state} — auto-installing..." - - case "$state" in - absent) - adb_root "mkdir -p /data/adb/modules/hosts/system/etc" - # module.prop is required for Magisk to recognise and process the module. - adb_root "printf 'id=hosts\nname=Systemless Hosts\nversion=v1\nversionCode=1\nauthor=Magisk\ndescription=Replace /system/etc/hosts\n' \ - > /data/adb/modules/hosts/module.prop" - # Seed a minimal hosts file so the mount target exists at first boot. - adb_root "printf '127.0.0.1 localhost\n::1 localhost\n' \ - > /data/adb/modules/hosts/system/etc/hosts" - adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts" - ;; - disabled) - adb_root "rm -f /data/adb/modules/hosts/disable \ - /data/adb/modules/hosts/remove \ - /data/adb/modules/hosts/update" - ;; - not-mounted) - : # module exists and enabled, just needs a reboot - ;; - esac - - echo " Rebooting phone to activate Magisk Hosts module..." - adb_cmd reboot - # Give the device time to actually begin shutting down before we poll. - sleep 20 - - echo " Waiting for device to come back (up to ${HOSTS_MODULE_REBOOT_WAIT_SECS}s)..." - local waited=0 - # Re-establish wireless ADB connection if needed. - while true; do - if [[ -n "${PHONE_IP:-}" ]]; then - adb connect "${PHONE_IP}:5555" >/dev/null 2>&1 || true - fi - if adb_cmd shell echo ok 2>/dev/null | grep -q '^ok$'; then - break - fi - sleep 3 - waited=$((waited + 3)) - if [[ $waited -ge $HOSTS_MODULE_REBOOT_WAIT_SECS ]]; then - echo "ERROR: Device did not come back after ${HOSTS_MODULE_REBOOT_WAIT_SECS}s." - echo " Check USB connection or re-enable wireless ADB, then run deploy again." - exit 1 - fi - printf '.' - done - printf '\n' - - # Wait for Magisk early-init and root to be ready. - echo " Waiting for Magisk root to be available..." - waited=0 - while ! adb_root "id" 2>/dev/null | grep -q "uid=0"; do - sleep 3 - waited=$((waited + 3)) - [[ $waited -ge 60 ]] && echo "ERROR: Root not available after reboot." && exit 1 - printf '.' - done - printf '\n' - - # Final assertion: the magic-mount must now be active. - if ! adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then - echo "ERROR: /system/etc/hosts is not magic-mounted after reboot." - echo " Magisk may not have applied the module correctly." - echo " Check the Magisk app for module errors and run deploy again." - exit 1 - fi - echo " Magisk Systemless Hosts module is now active." -} - -do_install_aurora() { - connect_adb - - # Check if already installed. - if adb_cmd shell pm list packages 2>/dev/null | grep -qx "package:${AURORA_PACKAGE}"; then - echo "Aurora Store is already installed (${AURORA_PACKAGE})." - return 0 - fi - - echo "Downloading Aurora Store ${AURORA_VERSION}..." - local tmp_apk - tmp_apk="$(mktemp --suffix=.apk)" - if ! curl -fsSL --retry 3 -o "$tmp_apk" "$AURORA_APK_URL"; then - rm -f "$tmp_apk" - echo "ERROR: Failed to download Aurora Store from $AURORA_APK_URL" - echo "Manual download: https://auroraoss.com/" - return 1 - fi - - echo "Installing Aurora Store..." - if adb_cmd install -r "$tmp_apk"; then - echo "Aurora Store ${AURORA_VERSION} installed successfully." - echo "Open Aurora Store on the phone, choose 'Anonymous' login, then install apps normally." - else - echo "ERROR: adb install failed. You can side-load manually:" - echo " adb install ${tmp_apk}" - fi - rm -f "$tmp_apk" -} diff --git a/phone_focus_mode/deploy_phases.sh b/phone_focus_mode/deploy_phases.sh deleted file mode 100755 index b675ad9d..00000000 --- a/phone_focus_mode/deploy_phases.sh +++ /dev/null @@ -1,163 +0,0 @@ -#!/bin/bash -# deploy_phases.sh — the staging half of deploy.sh's deploy run: pushing every -# script to the device's staging directory, and preparing the generated assets -# (the sqlite3 binary and the canonical hosts files). -# -# Sourced by deploy.sh, which owns adb_cmd, adb_root and $DEPLOY_DIR. -# -# _deploy_push_scripts holds the FIRST of the two hardcoded file lists. The -# second is in deploy_install.sh. A new phone-side sibling has to be added to -# BOTH, or it is staged and never lands in $REMOTE_DIR -- and the script that -# sources it then fails to start with no obvious cause. - -# Phase 4: stage every script and asset on the device. BOTH hardcoded -# lists live here -- the push list below and the cp list in -# _deploy_install_files -- and a new sibling must be added to each. -_deploy_push_scripts() { - echo "[4/7] Uploading scripts..." - adb_cmd push "$DEPLOY_DIR/config.sh" "/data/local/tmp/focus_stage/config.sh" - adb_cmd push "$DEPLOY_DIR/config_paths.sh" "/data/local/tmp/focus_stage/config_paths.sh" - adb_cmd push "$DEPLOY_DIR/config_dns.sh" "/data/local/tmp/focus_stage/config_dns.sh" - adb_cmd push "$DEPLOY_DIR/config_curfew.sh" "/data/local/tmp/focus_stage/config_curfew.sh" - adb_cmd push "$DEPLOY_DIR/config_tether.sh" "/data/local/tmp/focus_stage/config_tether.sh" - adb_cmd push "$DEPLOY_DIR/config_launcher.sh" "/data/local/tmp/focus_stage/config_launcher.sh" - adb_cmd push "$DEPLOY_DIR/focus_daemon.sh" "/data/local/tmp/focus_stage/focus_daemon.sh" - adb_cmd push "$DEPLOY_DIR/daemon_location.sh" "/data/local/tmp/focus_stage/daemon_location.sh" - adb_cmd push "$DEPLOY_DIR/daemon_state.sh" "/data/local/tmp/focus_stage/daemon_state.sh" - adb_cmd push "$DEPLOY_DIR/daemon_apps.sh" "/data/local/tmp/focus_stage/daemon_apps.sh" - adb_cmd push "$DEPLOY_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh" - adb_cmd push "$DEPLOY_DIR/ctl_usage.sh" "/data/local/tmp/focus_stage/ctl_usage.sh" - adb_cmd push "$DEPLOY_DIR/ctl_daemon.sh" "/data/local/tmp/focus_stage/ctl_daemon.sh" - adb_cmd push "$DEPLOY_DIR/ctl_hosts.sh" "/data/local/tmp/focus_stage/ctl_hosts.sh" - adb_cmd push "$DEPLOY_DIR/ctl_dns.sh" "/data/local/tmp/focus_stage/ctl_dns.sh" - adb_cmd push "$DEPLOY_DIR/ctl_launcher.sh" "/data/local/tmp/focus_stage/ctl_launcher.sh" - adb_cmd push "$DEPLOY_DIR/ctl_workout.sh" "/data/local/tmp/focus_stage/ctl_workout.sh" - adb_cmd push "$DEPLOY_DIR/ctl_curfew.sh" "/data/local/tmp/focus_stage/ctl_curfew.sh" - adb_cmd push "$DEPLOY_DIR/ctl_tether.sh" "/data/local/tmp/focus_stage/ctl_tether.sh" - adb_cmd push "$DEPLOY_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh" - adb_cmd push "$DEPLOY_DIR/hosts_magisk.sh" "/data/local/tmp/focus_stage/hosts_magisk.sh" - adb_cmd push "$DEPLOY_DIR/hosts_mount.sh" "/data/local/tmp/focus_stage/hosts_mount.sh" - adb_cmd push "$DEPLOY_DIR/dns_enforcer.sh" "/data/local/tmp/focus_stage/dns_enforcer.sh" - adb_cmd push "$DEPLOY_DIR/dns_iptables.sh" "/data/local/tmp/focus_stage/dns_iptables.sh" - adb_cmd push "$DEPLOY_DIR/launcher_enforcer.sh" "/data/local/tmp/focus_stage/launcher_enforcer.sh" - adb_cmd push "$DEPLOY_DIR/curfew_enforcer.sh" "/data/local/tmp/focus_stage/curfew_enforcer.sh" - adb_cmd push "$DEPLOY_DIR/curfew_net.sh" "/data/local/tmp/focus_stage/curfew_net.sh" - adb_cmd push "$DEPLOY_DIR/tether_enforcer.sh" "/data/local/tmp/focus_stage/tether_enforcer.sh" - adb_cmd push "$DEPLOY_DIR/tether_iptables.sh" "/data/local/tmp/focus_stage/tether_iptables.sh" - adb_cmd push "$DEPLOY_DIR/workout_detector.sh" "/data/local/tmp/focus_stage/workout_detector.sh" - adb_cmd push "$DEPLOY_DIR/magisk_service.sh" "/data/local/tmp/focus_stage/99-focus-mode.sh" - - # ---- sqlite3 binary for workout_detector.sh ---- - # Stored outside the repo (binary-files policy). Built once via the NDK - # against the SQLite amalgamation; see workout_detector.sh comments for - # the recipe. ~1.6 MB stripped, aarch64, PIE, dynamically linked against - # bionic (Android 30+). - _deploy_stage_assets -} - -# Stage the generated/vendored assets: the sqlite3 binary the workout -# detector needs, and the canonical hosts files built from the shared -# blocklist generator. -_deploy_stage_assets() { - SQLITE3_BIN="$DEPLOY_DIR/../../testsAndMisc_binaries/phone_focus_mode/sqlite3" - if [ -f "$SQLITE3_BIN" ]; then - echo " Uploading sqlite3 binary ($(stat -c%s "$SQLITE3_BIN") bytes)..." - adb_cmd push "$SQLITE3_BIN" "/data/local/tmp/focus_stage/sqlite3" - else - echo " WARNING: sqlite3 binary not found at $SQLITE3_BIN" - echo " workout_detector will not function until you build & place it there." - fi - - # Generate and upload the canonical hosts file (StevenBlack + custom entries). - # This mirrors what linux_configuration/hosts/install.sh installs on the PC. - HOSTS_GENERATOR="$DEPLOY_DIR/../linux_configuration/scripts/periodic_background/hosts/generate_hosts_file.sh" - if [ -f "$HOSTS_GENERATOR" ]; then - chmod +x "$HOSTS_GENERATOR" 2>/dev/null || true - echo " Generating canonical hosts file..." - HOSTS_TMP="$(mktemp)" - HOSTS_SHA_TMP="$(mktemp)" - if bash "$HOSTS_GENERATOR" "$HOSTS_TMP"; then - hosts_hash="$(compute_file_hash "$HOSTS_TMP")" - printf '%s\n' "$hosts_hash" >"$HOSTS_SHA_TMP" - echo " Uploading canonical hosts ($(wc -l <"$HOSTS_TMP") lines)..." - adb_cmd push "$HOSTS_TMP" "/data/local/tmp/focus_stage/hosts.canonical" - adb_cmd push "$HOSTS_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256" - - # ---- Workout-variant canonical ---- - # Same content as the full canonical, with all lines that block - # any of $WORKOUT_UNBLOCK_DOMAINS removed. Used by hosts_enforcer - # while a StrongLifts workout is in progress. - HOSTS_WORKOUT_TMP="$(mktemp)" - HOSTS_WORKOUT_SHA_TMP="$(mktemp)" - # Read $WORKOUT_UNBLOCK_DOMAINS from the freshly-staged config.sh - # so the generator and the runtime always agree on the domain set. - UNBLOCK_DOMAINS="$( - # shellcheck disable=SC1091 - ( - . "$DEPLOY_DIR/config.sh" >/dev/null 2>&1 - printf '%s\n' "$WORKOUT_UNBLOCK_DOMAINS" - ) | - sed 's/[[:space:]]\{1,\}/\n/g' | - grep -vE '^[[:space:]]*(#|$)' | - sort -u - )" - if [ -n "$UNBLOCK_DOMAINS" ]; then - # Build an awk regex of exact-match domains anchored as the - # *value* column of a hosts entry (" " possibly - # followed by aliases). We strip any line whose first non-IP - # token matches one of the unblock domains. - WORKOUT_UNBLOCK_DOMAINS="$UNBLOCK_DOMAINS" \ - python3 "$DEPLOY_DIR/strip_workout_hosts.py" "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP" || - cp "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP" - workout_hash="$(compute_file_hash "$HOSTS_WORKOUT_TMP")" - printf '%s\n' "$workout_hash" >"$HOSTS_WORKOUT_SHA_TMP" - stripped_lines=$(($(wc -l <"$HOSTS_TMP") - $(wc -l <"$HOSTS_WORKOUT_TMP"))) - echo " Uploading workout-variant hosts (stripped $stripped_lines YouTube lines)..." - adb_cmd push "$HOSTS_WORKOUT_TMP" "/data/local/tmp/focus_stage/hosts.canonical.workout" - adb_cmd push "$HOSTS_WORKOUT_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256.workout" - fi - rm -f "$HOSTS_WORKOUT_TMP" "$HOSTS_WORKOUT_SHA_TMP" - - rm -f "$HOSTS_TMP" - rm -f "$HOSTS_SHA_TMP" - else - rm -f "$HOSTS_TMP" - rm -f "$HOSTS_SHA_TMP" - echo " WARNING: failed to generate hosts file - skipping hosts enforcement" - fi - else - echo " WARNING: $HOSTS_GENERATOR not found - skipping hosts enforcement" - fi - - # Only push config_secrets.sh if phone doesn't already have one - if adb_root "test -f $REMOTE_DIR/config_secrets.sh" 2>/dev/null; then - echo " config_secrets.sh already exists on phone - skipping (preserving real coords)" - elif [[ "${NEEDS_GPS_FETCH}" -eq 1 ]]; then - # Local config_secrets.sh has placeholder coords — capture current GPS from the phone. - # The phone is assumed to be at home during setup, so current location = home location. - local gps_result="" gps_lat="" gps_lon="" - if gps_result="$(fetch_home_coords_from_phone 2>&1)"; then - gps_lat="${gps_result% *}" - gps_lon="${gps_result#* }" - adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS at deploy time\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \ - > $REMOTE_DIR/config_secrets.sh" - echo " Home coordinates written to phone: ${gps_lat}, ${gps_lon}" - else - # GPS unavailable (no WiFi/cellular yet on fresh phone). - # Write stub coords — focus mode stays OFF, hosts/DNS blocking still works. - # User should run: ./deploy.sh [ip] --capture-coords after configuring WiFi. - adb_root "printf '#!/system/bin/sh\n# STUB: run ./deploy.sh --capture-coords after WiFi setup\nexport HOME_LAT=\"0.000001\"\nexport HOME_LON=\"0.000001\"\n' \ - > $REMOTE_DIR/config_secrets.sh" - echo " WARNING: GPS capture failed — focus mode location enforcement is DISABLED." - echo " Hosts/DNS blocking is active. After configuring WiFi, run:" - echo " ADB_SERIAL=${ADB_SERIAL:-\$PHONE_IP:5555} ./deploy.sh --capture-coords" - fi - else - echo " Pushing config_secrets.sh (first install)..." - adb_cmd push "$DEPLOY_DIR/config_secrets.sh" "/data/local/tmp/focus_stage/config_secrets.sh" - adb_root "cp /data/local/tmp/focus_stage/config_secrets.sh $REMOTE_DIR/config_secrets.sh" - fi - - # Move staged files into place with root - _deploy_install_files -} diff --git a/phone_focus_mode/distraction_purge.sh b/phone_focus_mode/distraction_purge.sh deleted file mode 100755 index 144603ee..00000000 --- a/phone_focus_mode/distraction_purge.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# ============================================================ -# Distraction purge — remove YouTube et al. for the primary user -# -# `pm uninstall --user 0` unregisters a package for one user while -# leaving the APK on the read-only system image. It needs no root, no -# Device Owner and no factory reset, and — unlike `pm suspend` and -# `pm disable-user`, both measured to be cleared by a reboot on this -# device — it SURVIVES A REBOOT. Verified on the Pixel 6a on -# 2026-08-09: uninstalled, rebooted, still `installed=false`. -# -# It is fully reversible: `pm install-existing --user 0 ` brings -# the app back with no download, which `--restore` does. -# -# This exists because focus_owner cannot do the job. Its sweep filters -# to third-party packages, and YouTube ships at /product/app/YouTube -# with FLAG_SYSTEM — so the Device Owner path would skip it even after -# the wipe. See docs/youtube-block-unrooted.md. -# -# Usage: -# ./distraction_purge.sh # purge (default) -# ./distraction_purge.sh --list # dry run, change nothing -# ./distraction_purge.sh --status # report current state -# ./distraction_purge.sh --restore # put every package back -# ============================================================ - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly SCRIPT_DIR -# shellcheck source=lib/adb_common.sh -source "${SCRIPT_DIR}/lib/adb_common.sh" - -# Packages removed for user 0. Keep this list narrow and deliberate: -# every entry is an app the user has decided they do not want reachable, -# not general debloating (batch3_bloatware_uninstall.sh does that). -readonly PURGE_PACKAGES=( - com.google.android.youtube - com.google.android.apps.youtube.music - # Chrome closes the web route to youtube.com, which removing the app - # alone leaves wide open. It is the only browser on this device, so - # this also removes in-app links, OAuth flows and anything else that - # wants a browser -- accepted deliberately, and the reason --restore - # exists and is a single command. - com.android.chrome -) - -MODE="purge" - -usage() { - sed -n '2,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' - exit 0 -} - -for arg in "$@"; do - case "${arg}" in - --list) MODE="list" ;; - --status) MODE="status" ;; - --restore) MODE="restore" ;; - -h | --help) usage ;; - *) - _error "Unknown flag: ${arg}" - exit 1 - ;; - esac -done - -# Reports "installed", "absent", or "missing" (not on the image at all). -package_state() { - local pkg="$1" line - line="$(adb_cmd shell "dumpsys package ${pkg} 2>/dev/null \ - | sed -n '/User 0:/,/^ *User /p' \ - | grep -oE 'installed=[a-z]+' | head -1" | tr -d '\r')" - case "${line}" in - installed=true) printf 'installed' ;; - installed=false) printf 'absent' ;; - *) printf 'missing' ;; - esac -} - -report_state() { - local pkg state - for pkg in "${PURGE_PACKAGES[@]}"; do - state="$(package_state "${pkg}")" - printf ' %-44s %s\n' "${pkg}" "${state}" - done -} - -# Exits non-zero if any package is still reachable, so the caller (or a -# timer) learns about a Play Store reinstall instead of assuming success. -purge_packages() { - local pkg state failed=0 - for pkg in "${PURGE_PACKAGES[@]}"; do - state="$(package_state "${pkg}")" - if [[ "${state}" == "missing" ]]; then - _warn "${pkg}: not present on this device, skipping" - continue - fi - if [[ "${state}" == "absent" ]]; then - _info "${pkg}: already absent" - continue - fi - if adb_cmd shell pm uninstall --user 0 "${pkg}" >/dev/null 2>&1; then - _info "${pkg}: removed for user 0" - else - _error "${pkg}: uninstall failed" - fi - # Trust the device, not the exit code: `pm uninstall` has been - # observed printing Success while leaving the package in place. - if [[ "$(package_state "${pkg}")" == "installed" ]]; then - _error "${pkg}: STILL INSTALLED after uninstall" - failed=1 - fi - done - return "${failed}" -} - -restore_packages() { - local pkg - for pkg in "${PURGE_PACKAGES[@]}"; do - if adb_cmd shell pm install-existing --user 0 "${pkg}" >/dev/null 2>&1; then - _info "${pkg}: restored" - else - _error "${pkg}: restore failed" - fi - done -} - -main() { - adb_select_device - adb_verify_trusted_identity - - case "${MODE}" in - list) - _info "Would remove for user 0 (nothing changed):" - printf ' %s\n' "${PURGE_PACKAGES[@]}" - ;; - status) - _info "Current state:" - report_state - ;; - restore) - _box "RESTORING DISTRACTION APPS" \ - "Every package below becomes reachable again." - restore_packages - _info "Final state:" - report_state - ;; - purge) - purge_packages || { - _error "One or more packages are still installed." - _info "Final state:" - report_state - exit 1 - } - _info "Final state:" - report_state - ;; - esac -} - -main "$@" diff --git a/phone_focus_mode/dns_enforcer.sh b/phone_focus_mode/dns_enforcer.sh deleted file mode 100755 index eb982ef3..00000000 --- a/phone_focus_mode/dns_enforcer.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ============================================================ -# DNS enforcer for rooted Android. -# -# Why this exists: -# /etc/hosts only works for lookups done by the *system* resolver -# using classic DNS (UDP/TCP 53). Two bypass channels defeat it: -# 1. DNS-over-TLS (DoT, port 853) - used by Android when Private -# DNS is "automatic" or set to a specific provider. -# 2. DNS-over-HTTPS (DoH, port 443) - used by Chrome/Brave's -# "Use secure DNS" feature and some apps directly. -# -# Strategy: -# 1. Force `settings global private_dns_mode off` so the OS stops -# doing DoT (there is no public DoT-by-hostname toggle). -# 2. Drop outbound traffic to a fixed list of well-known DoH/DoT -# endpoints via iptables / ip6tables so apps' fallback logic -# has to use the regular resolver, which consults /etc/hosts. -# -# Limitations: -# * A custom app that hardcodes an obscure DoH IP is not caught. -# * A root user can `iptables -F` or re-enable private DNS - but -# this loop re-asserts every $DNS_CHECK_INTERVAL seconds and -# leaves tamper logs. -# ============================================================ - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -# shellcheck source=config.sh -. "$SCRIPT_DIR/config.sh" -# Sourced after config.sh because the chain functions read its DNS_* values. -# They also call log(), defined below; that is fine because nothing in either -# file runs until main() does. A sibling, not a lib/ member: lib/ does not -# exist on the phone, and deploy.sh copies these scripts flat into $REMOTE_DIR. -# shellcheck source=dns_iptables.sh -. "$SCRIPT_DIR/dns_iptables.sh" - -PIDFILE="$STATE_DIR/dns_enforcer.pid" - -mkdir -p "$STATE_DIR" -touch "$DNS_LOG" -chmod 666 "$DNS_LOG" 2>/dev/null || true - -log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >>"$DNS_LOG" -} - -rotate_log() { - local lines - lines="$(wc -l <"$DNS_LOG" 2>/dev/null || echo 0)" - if [ "$lines" -gt 500 ]; then - local tmp="$DNS_LOG.tmp" - tail -n 500 "$DNS_LOG" >"$tmp" - mv "$tmp" "$DNS_LOG" - fi -} - -acquire_lock() { - if [ -f "$PIDFILE" ]; then - local old_pid - old_pid="$(cat "$PIDFILE")" - if kill -0 "$old_pid" 2>/dev/null; then - local cmdline - cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)" - if echo "$cmdline" | grep -q "dns_enforcer"; then - echo "dns_enforcer already running (PID $old_pid)" - exit 0 - fi - fi - rm -f "$PIDFILE" - fi - echo $$ >"$PIDFILE" -} - -# ---- Private DNS ---- - -ensure_private_dns_off() { - # Two modes, selected by DNS_TRUSTED_DOT_HOST: - # - # empty - the original behaviour. DoT is a bypass channel, so Private - # DNS is forced off and every 853 connection is rejected. - # set - DoT is the *enforcement* channel. Private DNS is pinned to - # our own filtering resolver, and pinning it is what stops the - # user picking an unfiltered one from Settings. - # - # The pinned case is what an unrooted phone needs, since it has no hosts - # file to fall back to. - if [ -n "${DNS_TRUSTED_DOT_HOST:-}" ]; then - ensure_private_dns_pinned - return - fi - - local mode - mode="$(settings get global private_dns_mode 2>/dev/null)" - # Possible values: "off", "opportunistic", "hostname", null (default=opportunistic) - if [ "$mode" != "off" ]; then - settings put global private_dns_mode off 2>/dev/null - log "Private DNS was '$mode' - forced to 'off'" - fi - # Clear any pinned DoT hostname so the "hostname" mode cannot be - # re-enabled silently by Settings UI. - local spec - spec="$(settings get global private_dns_specifier 2>/dev/null)" - if [ -n "$spec" ] && [ "$spec" != "null" ]; then - settings delete global private_dns_specifier 2>/dev/null - log "Cleared private_dns_specifier (was '$spec')" - fi -} - -ensure_private_dns_pinned() { - # Hold Private DNS on our own resolver. Re-asserted every check, so a - # change made in Settings is reverted within DNS_CHECK_INTERVAL. - local mode spec - spec="$(settings get global private_dns_specifier 2>/dev/null)" - if [ "$spec" != "$DNS_TRUSTED_DOT_HOST" ]; then - settings put global private_dns_specifier "$DNS_TRUSTED_DOT_HOST" 2>/dev/null - log "private_dns_specifier was '$spec' - pinned to '$DNS_TRUSTED_DOT_HOST'" - fi - # Set the specifier before the mode: switching to "hostname" while the - # specifier still names someone else would briefly route lookups through - # that other resolver. - mode="$(settings get global private_dns_mode 2>/dev/null)" - if [ "$mode" != "hostname" ]; then - settings put global private_dns_mode hostname 2>/dev/null - log "Private DNS was '$mode' - forced to 'hostname'" - fi -} - -# ---- iptables chain management ---- - -# ---- State-change gate ---- -# -# fill_chain_v4/v6 flush and reinsert ~50-100 iptables/ip6tables rules each -# call. Running that unconditionally every $DNS_CHECK_INTERVAL was measured -# to peg netd at ~50% CPU on-device (contending the xtables lock) even -# though the rule set never changes at runtime. Only rebuild when the chain -# is actually missing, untethered from OUTPUT, or the wrong size (i.e. -# someone/something - netd, a root shell, a flush - tampered with it). -# Self-healing behavior described in the header comment is preserved; only -# redundant identical rebuilds are skipped. - -cleanup() { - # We intentionally leave the iptables chain in place on SIGTERM so - # stopping the enforcer for maintenance does not immediately re-open - # the DoH hole. `focus_ctl.sh dns-stop` does the explicit teardown. - log "dns_enforcer shutting down" - rm -f "$PIDFILE" - exit 0 -} - -trap cleanup INT TERM - -main() { - acquire_lock - log "dns_enforcer started (PID=$$)" - - # Initial arm-up - ensure_private_dns_off - enforce_iptables - - while true; do - ensure_private_dns_off - enforce_iptables - rotate_log - sleep "$DNS_CHECK_INTERVAL" - done -} - -main "$@" diff --git a/phone_focus_mode/dns_iptables.sh b/phone_focus_mode/dns_iptables.sh deleted file mode 100755 index adb3cc82..00000000 --- a/phone_focus_mode/dns_iptables.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ============================================================ -# dns_iptables.sh — the iptables chain half of dns_enforcer.sh. -# -# Builds and maintains the DNS_IPT_CHAIN block chain: creating it and -# pinning a single OUTPUT jump, filling it for IPv4 and IPv6, computing -# the rule count that proves it is complete, and re-asserting it when it -# has drifted. -# -# Sourced by dns_enforcer.sh after config.sh, whose DNS_* values these -# functions read. They also call log(), which dns_enforcer.sh defines; -# nothing here runs until main() calls it, so definition order is fine. -# A sibling rather than a lib/ member: lib/ does not exist on the phone, -# and deploy.sh copies these scripts flat into $REMOTE_DIR. -# ============================================================ - -ensure_chain() { - local ipt="$1" - # Create the chain if missing. - if ! "$ipt" -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then - "$ipt" -N "$DNS_IPT_CHAIN" 2>/dev/null || { - log "ERROR: could not create $ipt chain $DNS_IPT_CHAIN" - return 1 - } - log "Created $ipt chain $DNS_IPT_CHAIN" - fi - # Remove ALL existing OUTPUT -> chain jumps (handles duplicates from - # previous iptables lock races where -C returned error but -I succeeded). - local removed=0 - while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do - removed=$((removed + 1)) - done - # Insert exactly one jump at position 1 of OUTPUT. - if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then - if [ "$removed" -gt 1 ]; then - log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN" - fi - else - log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt" - return 1 - fi -} -trusted_dot_ips() { - # Echo the trusted resolver's addresses of the given family, one per line. - # $1 is "4" or "6". Literals come from DNS_TRUSTED_DOT_IPS; anything else - # is ignored, because resolving the resolver's own name here would be - # circular once Private DNS is pinned to it. - local want="$1" ip - [ -z "${DNS_TRUSTED_DOT_HOST:-}" ] && return 0 - for ip in ${DNS_TRUSTED_DOT_IPS:-}; do - [ -z "$ip" ] && continue - [ "${ip#\#}" != "$ip" ] && continue - case "$ip" in - *:*) [ "$want" = "6" ] && printf '%s\n' "$ip" ;; - *) [ "$want" = "4" ] && printf '%s\n' "$ip" ;; - esac - done -} -fill_chain_v4() { - # Flush and refill so we always converge to the intended rule set. - iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1 - # Allow the one DoT resolver we trust, BEFORE the blanket reject below. - # Order matters: iptables takes the first match, so an ACCEPT appended - # after the REJECT would never be reached. - local trusted - for trusted in $(trusted_dot_ips 4); do - iptables -A "$DNS_IPT_CHAIN" -d "$trusted" -p tcp --dport 853 \ - -j ACCEPT 2>/dev/null || true - done - # Drop DoT everywhere else. This is a narrow port rule - there's no legit - # reason for arbitrary apps to talk 853/tcp on Android. - iptables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \ - --reject-with icmp-port-unreachable 2>/dev/null || true - - local ip - for ip in $DNS_DOH_IPV4; do - [ -z "$ip" ] && continue - [ "${ip#\#}" != "$ip" ] && continue - # Reject 443/tcp (DoH) and 53 (classic DNS) to well-known resolvers. - # We also block 53 so apps that try to talk to 1.1.1.1:53 directly - # (ignoring /etc/resolv.conf) still fall back to the system resolver. - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \ - --reject-with icmp-port-unreachable 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \ - --reject-with icmp-port-unreachable 2>/dev/null || true - iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - done -} -fill_chain_v6() { - ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || return 1 - local trusted - for trusted in $(trusted_dot_ips 6); do - ip6tables -A "$DNS_IPT_CHAIN" -d "$trusted" -p tcp --dport 853 \ - -j ACCEPT 2>/dev/null || true - done - ip6tables -A "$DNS_IPT_CHAIN" -p tcp --dport 853 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -p udp --dport 853 -j REJECT \ - --reject-with icmp6-port-unreachable 2>/dev/null || true - - local ip - for ip in $DNS_DOH_IPV6; do - [ -z "$ip" ] && continue - [ "${ip#\#}" != "$ip" ] && continue - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 443 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 443 -j REJECT \ - --reject-with icmp6-port-unreachable 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p udp --dport 53 -j REJECT \ - --reject-with icmp6-port-unreachable 2>/dev/null || true - ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \ - --reject-with tcp-reset 2>/dev/null || true - done -} -expected_rule_count() { - # 2 fixed DoT rules (tcp+udp/853) + 4 rules per configured DoH/DNS IP, - # plus 1 ACCEPT per trusted-resolver address of this family. $1 is the - # family ("4"/"6"); the remaining arguments are the DoH/DNS IPs. - # - # This has to match fill_chain_* exactly. chain_intact() compares the - # live rule count against this number and rebuilds on any mismatch, so - # an undercount here means a rebuild every DNS_CHECK_INTERVAL seconds -- - # a silent fork storm that also drops the connection each time. - local family="$1" - shift - local n=2 ip - for ip in "$@"; do - [ -z "$ip" ] && continue - [ "${ip#\#}" != "$ip" ] && continue - n=$((n + 4)) - done - for ip in $(trusted_dot_ips "$family"); do - n=$((n + 1)) - done - echo "$n" -} -chain_intact() { - local ipt="$1" expected="$2" actual - "$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1 || return 1 - actual="$("$ipt" -S "$DNS_IPT_CHAIN" 2>/dev/null | grep -c '^-A')" - [ "$actual" = "$expected" ] -} -enforce_iptables() { - if command -v iptables >/dev/null 2>&1; then - # Intentional word split: expected_rule_count() iterates "$@" over the - # configured IP list. This runs under /system/bin/sh on the phone, where - # arrays do not exist, so an unquoted expansion is the POSIX way to do it. - # shellcheck disable=SC2086 - if chain_intact iptables "$(expected_rule_count 4 $DNS_DOH_IPV4)"; then - : - elif ensure_chain iptables && fill_chain_v4; then - log "iptables (v4) DNS chain rebuilt (was missing/tampered)" - fi - fi - if command -v ip6tables >/dev/null 2>&1; then - # Intentional word split, as above. - # shellcheck disable=SC2086 - if chain_intact ip6tables "$(expected_rule_count 6 $DNS_DOH_IPV6)"; then - : - elif ensure_chain ip6tables && fill_chain_v6; then - log "ip6tables (v6) DNS chain rebuilt (was missing/tampered)" - fi - fi -} diff --git a/phone_focus_mode/docs/night-curfew.md b/phone_focus_mode/docs/night-curfew.md deleted file mode 100644 index f7257642..00000000 --- a/phone_focus_mode/docs/night-curfew.md +++ /dev/null @@ -1,150 +0,0 @@ -# Night curfew and the hotspot/tethering block - -## Night curfew (after 23:00 at home) - -On top of the location-based focus mode, a **time-gated curfew** makes the phone -boring and largely unusable late at night so you go to sleep instead of doom- -scrolling. It activates only when focus mode is already ON (i.e. you are at -home) **and** the local clock is inside the curfew window (default 23:00–05:00). -Out of that window, or away from home, nothing changes. - -While the curfew is active it applies three allow-list layers — _block -everything except a short essential list_: - -1. **Apps.** The daemon swaps the permissive `WHITELIST` for the strict - `NIGHT_WHITELIST` (banking, maps, calendar, clock, authenticators, gov ID, - workout/diet). Everything else — browsers, social, messaging, email, media, - manga, stores — is `pm disable-user`'d and re-enabled automatically at - 05:00. Same proven mechanism as location focus; no new disable path. -2. **Display + notifications.** `curfew_enforcer.sh` forces the screen to - **grayscale** and DND to **alarms-only**, re-applying every 5s so toggling - them off in Settings snaps back. (Snap-back is the realistic lock; truly - blocking Settings risks system instability, so it is deliberately avoided.) -3. **Internet (optional, default OFF).** A per-UID `iptables` allow-list that - gives network only to the `NIGHT_WHITELIST` apps (plus root/system/shell + - DNS) and cuts off every other app. Enable `CURFEW_NET_ENABLED=1` in - `config.sh` only after validating it on-device (see test hook below). - -### Configuration (`config.sh`) - -```sh -NIGHT_CURFEW_ENABLED=1 # master switch -NIGHT_CURFEW_START="2300" # local HHMM; window wraps past midnight -NIGHT_CURFEW_END="0500" -CURFEW_GRAYSCALE_ENABLED=1 # force monochrome -CURFEW_DND_ENABLED=1 # force DND alarms-only -CURFEW_NET_ENABLED=0 # per-UID internet allow-list (prove first!) -``` - -Edit `NIGHT_WHITELIST` (right below `WHITELIST`) to choose what stays usable at -night. Allow-list by design: when in doubt, leave it out. The active keyboard -and the core dialer/SMS/home apps are always protected automatically (a 1am -reboot can never strand you without a keyboard), and the default browser is -intentionally _not_ protected at night so it can be disabled. - -### Control - -```bash -# On-device (root shell): -focus_ctl.sh curfew-status # window, enforcer state, what's applied -focus_ctl.sh curfew-test-on # FORCE curfew now (daytime validation) -focus_ctl.sh curfew-test-off # clear the force -focus_ctl.sh curfew-off # escape hatch: suspend curfew now -focus_ctl.sh curfew-on # re-arm (clear the override) -focus_ctl.sh curfew-log # enforcer log -``` - -### Opting out at 2am (no PC) - -The companion status notification grows a **"Suspend curfew till morning"** -action while the curfew is active. Tapping it drops the override file (curfew -off until you re-arm); the label flips to **"Re-arm curfew"**. The action is -hidden during the day so it is not a casual temptation. Without the PC this is -the only on-device opt-out — by design. From the PC you can always -`./deploy.sh --restart` or run `focus_ctl.sh curfew-off` over ADB. - -### Validating before you trust it overnight - -Because a misconfigured curfew can lock apps at 2am, validate it during the day -with the force hook, **not** by waiting for 23:00: - -```bash -focus_ctl.sh curfew-test-on # mBank + keyboard work, Firefox gone, gray, DND -focus_ctl.sh curfew-test-off # blocked apps come BACK (the reconcile path) -``` - -The clock parser fails **open** (treated as daytime) on a malformed time, so a -broken `date` can never trap you behind the strict list. - -## Hotspot / tethering block (closes the "second phone" bypass) - -Every other network layer only filters **this** phone's own traffic: -`/system/etc/hosts` is consulted by the phone's system resolver, and both -`dns_enforcer` and the curfew net layer only touch the **OUTPUT** chain. When -the phone shares its mobile data as a WiFi hotspot, a tethered second phone's -packets are **FORWARDed + NAT'd** through this phone on a path none of that -covers — so the second phone browses freely and defeats focus mode. - -`tether_enforcer.sh` closes that hole. It is always-on but **only acts while -focus mode is ON** (i.e. you are at home, `current_mode.txt == focus`), and -converges three levers every `TETHER_CHECK_INTERVAL` seconds: - -1. **Disable tether offload** — sets `settings global tether_offload_disabled 1` - so forwarded traffic is actually seen by netfilter instead of being shunted - around it by the hardware/BPF fast path. Snapshotted on entry, restored on - exit. -2. **FORWARD blanket REJECT** — an `iptables`/`ip6tables` chain - (`FOCUS_TETHER_BLOCK`) pinned at position 1 of `FORWARD`, rejecting all - forwarded packets. This is the version-independent catch-all and covers - **WiFi, USB and Bluetooth** tethering. The phone's own traffic uses - OUTPUT/INPUT, never FORWARD, so normal connectivity is untouched. Rebuilt - only when tampered (chain-intact gate), so it does not fork an `iptables -L` - every second (that pegged netd and overheated the phone during curfew-net - tuning). -3. **Stop the softAP** (best-effort, WiFi only, Android 11+) — `cmd wifi -stop-softap` each tick so the hotspot toggle visibly flips back off. - -On the transition away from home it restores the offload snapshot and tears the -FORWARD chain down, leaving tethering usable. - -### Configuration (`config.sh`) - -```sh -TETHER_ENFORCER_ENABLED=1 # master switch -TETHER_CHECK_INTERVAL=5 # re-assert cadence (seconds) -TETHER_STOP_SOFTAP_ENABLED=1 # also actively kill a running softAP -``` - -### Control - -```bash -focus_ctl.sh tether-status # enforcer state, offload flag, FORWARD chain -focus_ctl.sh tether-test-on # FORCE the block now (daytime validation) -focus_ctl.sh tether-test-off # clear the force -focus_ctl.sh tether-stop # escape hatch: stop it, restore tethering -focus_ctl.sh tether-start # (re)start the enforcer -focus_ctl.sh tether-log # enforcer log -``` - -The escape hatch is `tether-stop` over ADB (or a fresh `deploy.sh`), consistent -with `dns_enforcer`/`launcher_enforcer` — there is no companion-app button. -`tether_override` (created manually) suspends the block without stopping the -daemon. - -### Validating before you trust it - -Because Android's tether offload can silently skip the FORWARD rule on some -ROMs, validate on-device with a **real second phone** — do not trust rule -counts: - -```bash -focus_ctl.sh tether-test-on # hotspot on + second phone browsing → it loses - # internet within one interval; this phone stays - # online. Watch: focus_ctl.sh tether-log -focus_ctl.sh tether-test-off # forwarding restored, second phone browses again -``` - -If the second phone keeps browsing with the block applied, offload is being -bypassed: confirm the exact global key with -`adb shell su -c 'settings list global | grep -i offload'` and update -`TETHER_OFFLOAD_KEY` in `config.sh`. diff --git a/phone_focus_mode/docs/policy-lists.md b/phone_focus_mode/docs/policy-lists.md deleted file mode 100644 index 3c31c831..00000000 --- a/phone_focus_mode/docs/policy-lists.md +++ /dev/null @@ -1,101 +0,0 @@ -# Policy list rationale - -Prose moved out of `phone_focus_mode/config.sh` to keep that file under -the repo's 250-line cap. The lists themselves stay in `config.sh` and -must: `python_pkg/focus_policy/loader.py` finds them by regex-scanning -that file's text alone, so a list moved elsewhere silently parses as -empty. - -## Allowed Package Prefixes - -ALLOWED PACKAGE PREFIXES -Matched as prefixes on whole labels, exactly like $SYSTEM_NEVER_DISABLE: -"eu.kanade.tachiyomi" covers "eu.kanade.tachiyomi.sy" and -"eu.kanade.tachiyomi.extension.all.mangadex", but not -"eu.kanade.tachiyomisomething". -This exists because Tachiyomi installs every source as its OWN apk. Listing -them individually means each newly installed extension is invisible until -this file is edited and the policy regenerated -- a recurring chore that -looks exactly like a bug from the phone. -Weaker than the exact list by construction: a prefix allows packages that do -not exist yet. Keep the prefixes narrow and vendor-specific for that reason. - -## Night Curfew Whitelist - -NIGHT CURFEW WHITELIST -These are the ONLY third-party apps that stay enabled during the curfew -window (see NIGHT_CURFEW_* above). Everything else in $WHITELIST — browsers, -social, messaging, email, stores, transit — is disabled. -Allow-list by design: when in doubt, leave it OUT. -EXCEPTION: $NIGHT_ALLOWED_PREFIXES is applied on top of this list, and it -currently carries "eu.kanade.tachiyomi" — so manga IS available during the -curfew, deliberately (chosen 2026-08-14). This paragraph used to say manga -was disabled at night; it was true until that change. Do not "restore" it -without also emptying $NIGHT_ALLOWED_PREFIXES, or the comment and the -behaviour disagree again. -Parsed exactly like $WHITELIST (one package per line, '#' comments ignored). -The sysprotect prefixes ($SYSTEM_NEVER_DISABLE) and the default-handler guard -(dialer/SMS/home/browser/IME) still apply on TOP of this list, so the active -keyboard and core system apps are protected even if omitted here. - -## System packages that must never be disabled - ---- System / essential packages that must NEVER be disabled --- -These are matched as prefixes (startswith). -You generally don't need to edit this list. -pl.infakt.infakt is the one non-system entry. Allowlisting it is weaker: -that depends on it staying in BOTH the day and night lists, and dropping it -from either would silently make it hideable. It is device-paired to a bank -over SMS, so losing access to it strands the same re-authentication chain a -hidden Messages app would. isAllowed() checks this list first, before the -curfew split, so it holds under every condition. - -## Whitelisted apps - -WHITELISTED APPS -These apps will ALWAYS remain enabled, even in focus mode. -Package names verified against installed packages on 2026-02-22. - -## Night curfew - -NIGHT CURFEW (time-gated strict allow-list) -When focus mode is ON (i.e. you are at home) AND the local clock is inside -the curfew window, the daemon switches from the permissive $WHITELIST to the -strict $NIGHT_WHITELIST: every app not on that short list is disabled. This -is the "stop using the phone after 23:00 at home" layer. The companion -enforcer (curfew_enforcer.sh) adds grayscale + DND + an optional per-UID -network allow-list on top. Times are local 24h "HHMM"; the window wraps past -midnight when START > END (e.g. 2300 -> 0500). - -## Why dev.kuhy.todo is in the night list - -Capture-only notes app. Added 2026-08-14 after a curfew-window deploy -installed it and the enforcer removed the package ~80ms later: it was in -the day list but not here, so any build shipped after 23:00 was silently -uninstalled. This is a deliberate loosening of the answer-the-phone / -reach-a-bank / handle-an-emergency rule above -- writing an idea down at -night is the one thing this app does, and losing the deploy path for six -hours a day cost more than the distraction risk. - -## Why the Play Store is in the day list - ---- App store (day only; deliberately absent from the night list) --- -NB: never write a dollar-sign variable reference inside this list. WHITELIST -is a double-quoted string, so even a comment line expands, and deploy.sh -runs under set -u, where an undefined name aborts the whole deploy. A -reference to the night list sat here and did exactly that (it is defined -below this point, so it was still unset), blocking every focus-mode deploy. -Same rule for double quotes: one in a comment ends the string early. -infakt cannot be installed or updated without Play, and it is device-paired -to a bank, so losing the ability to update it strands a re-authentication -chain. This does NOT reopen YouTube: the sweep is default-deny for -third-party packages, so anything installed from Play is hidden on the next -at-home pass, and the always-blocked set (YouTube, Chrome) is never restored -by the AWAY branch either. Reinstalling YouTube from Play is UNTESTED as a -bypass: -the sweep would re-hide it within one pass regardless, but treat the claim -that Play cannot resurrect a blocked app as unverified until someone tries. -NOTE: never use a double-quote character anywhere inside these export -blocks, not even in a comment. The policy loader matches a quoted value up -to the next quote, so one stray quote terminates the string early and -silently truncates the allowlist -- which drops apps with no error at all. diff --git a/phone_focus_mode/docs/updating-and-troubleshooting.md b/phone_focus_mode/docs/updating-and-troubleshooting.md deleted file mode 100644 index 6452386b..00000000 --- a/phone_focus_mode/docs/updating-and-troubleshooting.md +++ /dev/null @@ -1,36 +0,0 @@ -# Updating and troubleshooting - -## Updating - -After editing `config.sh` (e.g. changing whitelist): - -```bash -./deploy.sh # re-pushes all files -# or just the config: -adb push config.sh /data/local/tmp/focus_mode/config.sh -./deploy.sh --restart -``` - -## Troubleshooting - -**Location always unavailable:** - -- Enable GPS and network location on the phone -- Open Google Maps once to warm up the GPS provider -- The daemon logs every attempt; check with `--log` - -**App won't disable:** - -- Some system apps can't be disabled even as root; they're silently skipped -- Check log for "Failed to disable" warnings - -**Daemon not starting on boot:** - -- Verify Magisk is installed and `service.d` is supported -- Check `/data/adb/service.d/99-focus-mode.sh` exists and is executable -- Some Magisk versions use `/data/adb/post-fs-data.d/` instead; try both - -**Wrong package name in whitelist:** - -- Use `./deploy.sh --find-pkg ` to find the exact package name -- Package names are case-sensitive diff --git a/phone_focus_mode/docs/youtube-block-unrooted.md b/phone_focus_mode/docs/youtube-block-unrooted.md deleted file mode 100644 index a8645ad8..00000000 --- a/phone_focus_mode/docs/youtube-block-unrooted.md +++ /dev/null @@ -1,166 +0,0 @@ -# Blocking YouTube on an unrooted Pixel 6a - -Applied and verified 2026-08-09 on `23181JEGR08034` (Android 16, SDK 36). -No root, no Device Owner, no factory reset. - -## What actually works: `pm uninstall --user 0` - -Unregisters a package for one user while leaving the APK on the read-only -system image. - -| Mechanism | Survives reboot? | -| --------------------------- | ----------------------------------------- | -| `pm suspend` | No — measured | -| `pm disable-user` | No — measured | -| `dpm setApplicationHidden` | Yes — but needs Device Owner, i.e. a wipe | -| **`pm uninstall --user 0`** | **Yes — measured, twice** | - -Reversible with `pm install-existing --user 0 `: no download, no data -loss, because the APK never left the device. - -## Why focus_owner cannot do this job - -`EnforcementRunner.installedThirdPartyPackages()` filters to packages without -`FLAG_SYSTEM`. YouTube ships at `/product/app/YouTube` **with** `FLAG_SYSTEM`, -so the Device Owner sweep would skip it — even after the factory reset. The -apps most worth blocking are preinstalled, which is exactly the set that sweep -excludes. Worth fixing there separately; it does not block anything today. - -## Current state - -Removed for user 0, all reboot-verified: - -- `com.google.android.youtube` -- `com.google.android.apps.youtube.music` -- `com.android.chrome` - -Driven by `phone_focus_mode/distraction_purge.sh` (`--list`, `--status`, -`--restore`). It re-checks each package with `dumpsys` after acting rather -than trusting the `pm` exit code, and exits non-zero if anything is still -installed — so a Play Store reinstall surfaces as a failure instead of a -silent no-op. - -## Layer 2: uBlock Origin in Firefox - -Independent of the VPN, and that is the point: stopping RethinkDNS does -**not** restore YouTube in Firefox. Verified by force-stopping -`com.celzero.bravedns` and loading youtube.com, which produced _"uBlock -Origin has prevented the following page from loading — `||youtube.com^` — -found in: My filters"_. - -The filters, in uBlock's dashboard → **My filters**: - -``` -! YouTube block -||youtube.com^ -||youtu.be^ -||googlevideo.com^ -||ytimg.com^ -||youtube-nocookie.com^ -||ggpht.com^ -||yt3.ggpht.com^ -||m.youtube.com^ -``` - -Add them by writing that list to a file, pushing it to -`/sdcard/Download/`, triggering a media scan (`am broadcast -a -android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/...`, -or the picker will not see it), then using uBlock's **import** button. -Do NOT type them with `adb shell input text`: it silently strips `|` and -`^`, leaving bare domains that do not block. Press the checkmark to -apply, and confirm it greys out. - -These live in the Firefox profile, so they are **not** captured by -`phone_backup.sh` and must be re-added by hand after a wipe. - -The block page has a **Proceed** button — one tap. This layer raises the -cost of bypassing; it does not remove the bypass. - -## What is NOT blocked - -**Firefox (`org.mozilla.fenix`) is still installed and still reaches -youtube.com.** It is the only remaining browser after Chrome's removal. - -Unlike the three packages above, Firefox is user-installed -(`/data/app/...`, no `FLAG_SYSTEM`), so `pm uninstall --user 0` would -**permanently delete it and its data** — no `install-existing` recovery, a -full reinstall from the Play Store. That asymmetry is why it was left alone -rather than swept up with the rest. Decide deliberately. - -`com.google.android.webview` was checked and left in place: it is the system -WebView provider, so removing it would break in-app browsers everywhere. -Chrome and WebView are separate packages on this device, which is why -removing Chrome is safe. - -## The web path, and why DNS is parked - -Removing the app kills the recommendation feed, notifications and the -account-linked session — most of the pull. It does not stop typing -`youtube.com` into a browser. - -Previously rejected approaches, from `focus_owner/docs/`: - -- **Home-hosted DoT resolver.** Android's Private DNS fails _closed_, so PC - off = phone has no internet at all. Rejected: the phone must work with the - PC off. -- **Hosted DNS (NextDNS, AdGuard, ControlD).** No provider ingests the 185k - domain blocklist; free tiers fail _open_ past ~300k queries/month. - -The remaining candidate is a **local-VPN content blocker** — an on-device -`VpnService` that filters without any network dependency, so it works with the -PC off and does not fail closed on network loss. Not yet scoped; the VPN slot -was going to be checked when the device disconnected. Note Android allows only -one active VPN at a time, so this would conflict with any real VPN. - -## How strong this actually is - -Measured, not assumed: - -| Layer | Undo cost | -| ---------------- | ------------------------------------------------------------------------------------------------------------------- | -| App removal | **One tap.** Google Play shows a live **Install** button for YouTube (checked on device 2026-08-09; not installed). | -| RethinkDNS rules | A few taps — the VPN toggle in Settings or the quick-settings tile. | -| uBlock filters | **Proceed** on the block page, or disabling the extension. | - -Nothing here is tamper-resistant, and nothing here can be: on an -unrooted device without Device Owner, no app can prevent the user -disabling a VPN or reinstalling from Play. The layers are independent — -undoing one does not undo the others — so the cost is "three separate -places" rather than one. That is the honest ceiling. - -`DISALLOW_CONFIG_VPN` plus `setAlwaysOnVpnPackage(..., lockdown)` under -Device Owner is the only tier that removes the off switch, and it needs -the factory reset in `docs/device-owner-wipe-checklist.md`. See also the -FLAG_SYSTEM note above: `focus_owner` would need fixing before Device -Owner could touch YouTube at all. - -Also note nothing re-asserts the purge automatically. -`phone-auto-sync.timer` is inactive and its `ExecStart` points at a path -that does not exist, so `distraction_purge.sh` is manual today. - -## infakt, before any factory reset - -Checked 2026-08-09 in the web panel and on the device, because infakt was -the one app whose loss would have blocked the Device Owner wipe. - -- **2FA is SMS-only.** "Aplikacja weryfikacyjna" reads _Skonfiguruj_, i.e. - not configured, so **no TOTP seed exists** and a wipe cannot destroy - one. The registered number survives on the SIM. -- **"Logowanie" is unchecked** under _Akcje objęte weryfikacją_ — only - _Wrażliwe_ actions prompt. Plain login needs the password alone. -- The app uses Firebase push (`FirebaseInstanceIdReceiver`) for the - browser-login approval tap. Push tokens are per-install, so a reinstall - re-registers and prompts arrive at the new install. - -So login is safe: reinstall, sign in with the password. - -What does **not** survive: the banking pairing (_Usuń powiązanie z -telefonem_) and the local app PIN. Re-pair via SMS after the wipe. - -Do not save anything on that settings page casually — it warns that -changing it logs out all current mobile sessions. - -## Redoing this after a factory reset - -The purge is per-user state and does not survive a wipe. Re-run -`./distraction_purge.sh` after re-enrolling the device. diff --git a/phone_focus_mode/enable_persistent_adb.sh b/phone_focus_mode/enable_persistent_adb.sh deleted file mode 100755 index 0d9814db..00000000 --- a/phone_focus_mode/enable_persistent_adb.sh +++ /dev/null @@ -1,193 +0,0 @@ -#!/bin/bash - -# ============================================================================ -# Make adb-over-Wi-Fi permanent on the phone (port 5555). -# -# Android's "Wireless debugging" is deliberately ephemeral: the listener dies -# when you leave the settings screen or the device sleeps, and it picks a new -# random port every time it comes back. That is why every deploy so far has -# needed someone to tap through the phone first. -# -# This script waits for ANY transient wireless-debugging window, then uses it -# to switch adbd to a fixed port 5555 and installs a Magisk boot script so the -# same thing happens on every boot. After one successful run the phone is -# reachable at $PHONE_IP:5555 forever, with no tapping. -# -# Requires: root (Magisk) on the device, and the host already paired. -# -# Usage: -# ./enable_persistent_adb.sh [PHONE_IP] [KNOWN_PORT] -# PHONE_IP defaults to 192.168.1.64. -# KNOWN_PORT, if given, is tried first before falling back to a port scan. -# ============================================================================ - -set -uo pipefail - -PHONE_IP="${1:-192.168.1.64}" -readonly KNOWN_PORT="${2:-}" -readonly FIXED_PORT=5555 -# The phone is on DHCP, so its address can move. Its MAC cannot, so that is -# what we actually identify it by; PHONE_IP is only a first guess. -readonly PHONE_MAC="00:08:22:68:81:fc" -# Android allocates the wireless-debugging port from the ephemeral range. -readonly SCAN_RANGE="30000-65535" -# Give up rather than spin forever if nobody ever wakes the phone. -readonly DEADLINE_SECONDS=1800 - -readonly BOOT_SCRIPT=/data/adb/service.d/99-adb-tcp-5555.sh - -log() { printf '[persistent-adb] %s\n' "$1"; } - -require() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "Error: $1 is required but not installed" >&2 - exit 1 - fi -} - -# Echo the serial iff THIS endpoint is connected and not offline. -# -# Deliberately matches the exact ip:port rather than the first `device` line. -# After pairing, adb also auto-connects an mDNS `_adb-tls-connect._tcp` entry -# for the same phone; taking the first line made `try_connect 5555` report -# success while port 5555 was in fact closed. -connected_serial() { - local endpoint="$1" - adb devices 2>/dev/null | - awk -v want="$endpoint" '$1 == want && $2 == "device" {print $1; exit}' -} - -# Try to reach adbd. Returns 0 and sets REPLY to the serial when connected. -try_connect() { - local port="$1" endpoint - endpoint="${PHONE_IP}:${port}" - adb connect "$endpoint" >/dev/null 2>&1 || return 1 - # `connect` succeeds optimistically; the device can still be offline. - sleep 2 - REPLY="$(connected_serial "$endpoint")" - [[ -n "$REPLY" ]] -} - -# Locate the phone on the current subnet by MAC and echo its IP. -# -# DHCP can hand the phone a different address at any time, which would break a -# hardcoded PHONE_IP. A ping sweep populates the neighbour table, then we look -# the MAC up in it. Prefer this over a router DHCP reservation because it needs -# no router access and keeps working on any network the laptop joins. -discover_ip_by_mac() { - local subnet - subnet="$(ip -4 route show dev "$(ip -4 route show default | - awk '/default/ {print $5; exit}')" 2>/dev/null | - awk '/proto kernel/ {print $1; exit}')" - [[ -n "$subnet" ]] || return 1 - - nmap -sn -T4 "$subnet" >/dev/null 2>&1 - ip neigh show 2>/dev/null | - awk -v mac="$PHONE_MAC" 'tolower($5) == tolower(mac) {print $1; exit}' -} - -# Find the current ephemeral wireless-debugging port, if one is listening. -scan_for_port() { - nmap -p "$SCAN_RANGE" --open -T4 -Pn --min-rate 3000 "$PHONE_IP" 2>/dev/null | - awk -F/ '/^[0-9]+\/tcp[[:space:]]+open/ {print $1}' | head -1 -} - -# Switch adbd to the fixed port and persist it across reboots. -make_permanent() { - local serial="$1" - - if ! adb -s "$serial" shell 'su -c id' 2>/dev/null | grep -q 'uid=0'; then - echo "Error: no root on the device; cannot install the boot script." >&2 - echo " adb tcpip $FIXED_PORT alone would not survive a reboot." >&2 - return 1 - fi - - log "installing Magisk boot script at $BOOT_SCRIPT" - # service.d runs late in boot, after the network is up, which is what adbd - # needs. Restarting adbd is what actually makes it re-read the port. - adb -s "$serial" shell "su -c 'cat > $BOOT_SCRIPT'" < Wireless debugging first. -setprop service.adb.tcp.port $FIXED_PORT -stop adbd -start adbd -EOF - adb -s "$serial" shell "su -c 'chmod 755 $BOOT_SCRIPT'" - - log "switching adbd to port $FIXED_PORT now" - adb -s "$serial" shell "su -c 'setprop service.adb.tcp.port $FIXED_PORT; stop adbd; start adbd'" - # adbd restarting drops every current connection, including this one. - sleep 4 -} - -main() { - require adb - require nmap - - log "waiting for a wireless-debugging window on $PHONE_IP (up to $((DEADLINE_SECONDS / 60))m)" - log "wake the phone and open Settings -> Developer options -> Wireless debugging" - - local deadline=$((SECONDS + DEADLINE_SECONDS)) - local serial="" - - while ((SECONDS < deadline)); do - # Cheapest first: the fixed port may already be live from a prior run. - if try_connect "$FIXED_PORT"; then - serial="$REPLY" - log "already reachable on $FIXED_PORT - nothing to do" - printf '%s\n' "$serial" - return 0 - fi - - # Not at the expected address — DHCP may have moved it. Re-find the - # phone by MAC before concluding that adbd is down. - local moved - moved="$(discover_ip_by_mac)" - if [[ -n "$moved" && "$moved" != "$PHONE_IP" ]]; then - log "phone moved: $PHONE_IP -> $moved" - PHONE_IP="$moved" - if try_connect "$FIXED_PORT"; then - serial="$REPLY" - log "reachable on $FIXED_PORT at the new address" - printf '%s\n' "$serial" - return 0 - fi - fi - - if [[ -n "$KNOWN_PORT" ]] && try_connect "$KNOWN_PORT"; then - serial="$REPLY" - break - fi - - local found - found="$(scan_for_port)" - if [[ -n "$found" ]] && try_connect "$found"; then - serial="$REPLY" - break - fi - - sleep 5 - done - - if [[ -z "$serial" ]]; then - echo "Error: no wireless-debugging window appeared before the deadline." >&2 - return 1 - fi - - log "connected as $serial" - make_permanent "$serial" || return 1 - - if try_connect "$FIXED_PORT"; then - log "SUCCESS: phone is now reachable at ${PHONE_IP}:${FIXED_PORT}" - log "it will come back on this port after every reboot" - printf '%s\n' "${PHONE_IP}:${FIXED_PORT}" - return 0 - fi - - echo "Error: adbd did not come back on $FIXED_PORT after the switch." >&2 - return 1 -} - -main "$@" diff --git a/phone_focus_mode/focus_ctl.sh b/phone_focus_mode/focus_ctl.sh deleted file mode 100755 index a9678480..00000000 --- a/phone_focus_mode/focus_ctl.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ============================================================ -# Focus Mode Control Utility -# Run on the phone via: su --mount-master -c /data/local/tmp/focus_mode/focus_ctl.sh -# Or from PC via: adb shell su --mount-master -c '/data/local/tmp/focus_mode/focus_ctl.sh ' -# --mount-master is required so this script (and any daemon it spawns) joins -# the global mount namespace; otherwise the hosts bind mount is invisible and -# /data/adb/focus_mode/* checks fail due to per-session SELinux isolation. -# ============================================================ - -SCRIPT_DIR="/data/local/tmp/focus_mode" -. "$SCRIPT_DIR/config.sh" -# shellcheck source=ctl_hosts.sh -. "$SCRIPT_DIR/ctl_hosts.sh" -# shellcheck source=ctl_dns.sh -. "$SCRIPT_DIR/ctl_dns.sh" -# shellcheck source=ctl_launcher.sh -. "$SCRIPT_DIR/ctl_launcher.sh" -# shellcheck source=ctl_workout.sh -. "$SCRIPT_DIR/ctl_workout.sh" -# shellcheck source=ctl_curfew.sh -. "$SCRIPT_DIR/ctl_curfew.sh" -# shellcheck source=ctl_tether.sh -. "$SCRIPT_DIR/ctl_tether.sh" -# shellcheck source=ctl_usage.sh -. "$SCRIPT_DIR/ctl_usage.sh" -# shellcheck source=ctl_daemon.sh -. "$SCRIPT_DIR/ctl_daemon.sh" - - -# ---- Logging ---- -log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >>"$LOG_FILE" -} - - - - - - - - - - - - - - -HOSTS_PIDFILE="$STATE_DIR/hosts_enforcer.pid" - - - - - - -# ---- DNS enforcer ---- -# Hosts file only works for the system resolver. Apps using DoH/DoT bypass -# /etc/hosts entirely. The DNS enforcer forces Private DNS off and blocks -# well-known DoH/DoT endpoints so /etc/hosts is actually consulted. - -DNS_PIDFILE="$STATE_DIR/dns_enforcer.pid" - - - - - - -# ---- Launcher enforcer ---- - -LAUNCHER_PIDFILE="$STATE_DIR/launcher_enforcer.pid" -DISABLED_COMPETITORS_FILE="$STATE_DIR/disabled_competitors.txt" - - - - - - - -# ---- Workout detector ---- - -WORKOUT_PIDFILE="$STATE_DIR/workout_detector.pid" - - - - - - -# ============================================================ -# Night-curfew control (see curfew_enforcer.sh / focus_daemon.sh) -# ============================================================ -CURFEW_PIDFILE="$STATE_DIR/curfew_enforcer.pid" - - - - - - -cmd_curfew_log() { tail -n "${1:-50}" "$CURFEW_ENFORCER_LOG" 2>/dev/null || echo "No curfew log yet."; } - - - - - - - -# ============================================================ -# Hotspot / tethering block control (see tether_enforcer.sh) -# ============================================================ -TETHER_PIDFILE="$STATE_DIR/tether_enforcer.pid" - - - - - - - - -case "$1" in -start) cmd_start ;; -stop) cmd_stop ;; -status) cmd_status ;; -enable) cmd_enable ;; -disable) cmd_disable ;; -log) cmd_log "${2:-50}" ;; -list-apps) cmd_list_apps ;; -whitelist) cmd_whitelist ;; -restart) - cmd_stop - sleep 2 - cmd_start - ;; -hosts-status) cmd_hosts_status ;; -hosts-start) cmd_hosts_start ;; -hosts-stop) cmd_hosts_stop ;; -hosts-log) cmd_hosts_log "${2:-50}" ;; -dns-status) cmd_dns_status ;; -dns-start) cmd_dns_start ;; -dns-stop) cmd_dns_stop ;; -dns-log) cmd_dns_log "${2:-50}" ;; -launcher-status) cmd_launcher_status ;; -launcher-start) cmd_launcher_start ;; -launcher-stop) cmd_launcher_stop ;; -launcher-log) cmd_launcher_log "${2:-50}" ;; -launcher-snapshot) cmd_launcher_snapshot ;; -workout-status) cmd_workout_status ;; -workout-start) cmd_workout_start ;; -workout-stop) cmd_workout_stop ;; -workout-log) cmd_workout_log "${2:-50}" ;; -recheck) cmd_recheck ;; -notif-status) cmd_notif_status ;; -curfew-status) cmd_curfew_status ;; -curfew-start) cmd_curfew_start ;; -curfew-stop) cmd_curfew_stop ;; -curfew-log) cmd_curfew_log "${2:-50}" ;; -curfew-test-on) cmd_curfew_test_on ;; -curfew-test-off) cmd_curfew_test_off ;; -curfew-demo-on) cmd_curfew_demo_on ;; -curfew-demo-off) cmd_curfew_demo_off ;; -curfew-off) cmd_curfew_off ;; -curfew-on) cmd_curfew_on ;; -tether-status) cmd_tether_status ;; -tether-start) cmd_tether_start ;; -tether-stop) cmd_tether_stop ;; -tether-log) cmd_tether_log "${2:-50}" ;; -tether-test-on) cmd_tether_test_on ;; -tether-test-off) cmd_tether_test_off ;; -*) usage ;; -esac diff --git a/phone_focus_mode/focus_daemon.sh b/phone_focus_mode/focus_daemon.sh deleted file mode 100755 index 651c7e2f..00000000 --- a/phone_focus_mode/focus_daemon.sh +++ /dev/null @@ -1,206 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ============================================================ -# Focus Mode Daemon -# Runs on rooted Android device. Periodically checks GPS -# location and restricts non-whitelisted apps when near home. -# ============================================================ - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -. "$SCRIPT_DIR/config.sh" -# shellcheck source=daemon_location.sh -. "$SCRIPT_DIR/daemon_location.sh" -# shellcheck source=daemon_state.sh -. "$SCRIPT_DIR/daemon_state.sh" -# shellcheck source=daemon_apps.sh -. "$SCRIPT_DIR/daemon_apps.sh" - -PIDFILE="$STATE_DIR/daemon.pid" - -# ---- PID lock: exit if already running ---- -acquire_lock() { - mkdir -p "$STATE_DIR" - if [ -f "$PIDFILE" ]; then - local old_pid - old_pid="$(cat "$PIDFILE")" - if kill -0 "$old_pid" 2>/dev/null; then - # Verify the PID is actually a focus_daemon, not a reused PID - local cmdline - cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)" - if echo "$cmdline" | grep -q "focus_daemon"; then - echo "Daemon already running (PID $old_pid), exiting." - exit 0 - fi - fi - # Stale or reused pidfile - rm -f "$PIDFILE" - fi - echo $$ >"$PIDFILE" -} - -# ---- Logging ---- -log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >>"$LOG_FILE" -} - -rotate_log() { - local lines - lines="$(wc -l <"$LOG_FILE" 2>/dev/null || echo 0)" - if [ "$lines" -gt "$LOG_MAX_LINES" ]; then - local tmp="$LOG_FILE.tmp" - tail -n "$LOG_MAX_LINES" "$LOG_FILE" >"$tmp" - mv "$tmp" "$LOG_FILE" - fi -} - -# ---- Build helper files for fast package checks ---- - - - - - - -# ---- Initialization ---- -init() { - mkdir -p "$STATE_DIR" - touch "$LOG_FILE" - touch "$DISABLED_APPS_FILE" - # Ensure state files are writable (survives reboot / permission drift) - chmod 666 "$LOG_FILE" "$DISABLED_APPS_FILE" "$PIDFILE" 2>/dev/null - # Status file must be world-readable (companion app reads it). - # State dir must be world-writable+executable so the companion app can - # drop the recheck trigger file (it runs as a normal app UID). - chmod 777 "$STATE_DIR" 2>/dev/null - - if [ "$HOME_LAT" = "0.000000" ] && [ "$HOME_LON" = "0.000000" ]; then - log "ERROR: Home coordinates not set! Edit config_secrets.sh first." - exit 1 - fi - - if ! echo "$HOME_LAT" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then - log "ERROR: HOME_LAT is invalid ('$HOME_LAT'). Expected decimal degrees in config_secrets.sh" - exit 1 - fi - - if ! echo "$HOME_LON" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then - log "ERROR: HOME_LON is invalid ('$HOME_LON'). Expected decimal degrees in config_secrets.sh" - exit 1 - fi - - build_whitelist_file - build_night_whitelist_file - build_sysprotect_file - build_blocked_sys_file - refresh_default_handlers - rotate_log - - if [ -f "$MODE_FILE" ]; then - CURRENT_MODE="$(cat "$MODE_FILE")" - else - CURRENT_MODE="normal" - fi - - if [ "$CURRENT_MODE" = "focus" ]; then - reconcile_disabled_apps - fi - - log "Focus mode daemon started (PID=$$, mode=$CURRENT_MODE, home=$HOME_LAT,$HOME_LON, radius=${RADIUS}m)" - log "Intervals: focus=${CHECK_INTERVAL_FOCUS}s normal=${CHECK_INTERVAL_NORMAL}s" -} - - - - - - - - - -# ---- Focus Mode Control ---- - - - - -# ---- Sleep with early-wake on recheck trigger ---- -# Polls for $RECHECK_TRIGGER every second; if found, consumes it and returns -# early. The file can be touched by the companion app (via "Re-check now" -# button) or by `focus_ctl.sh recheck` from a shell. -sleep_with_recheck() { - local total="$1" - local elapsed=0 - while [ "$elapsed" -lt "$total" ]; do - if [ -e "$RECHECK_TRIGGER" ]; then - rm -f "$RECHECK_TRIGGER" 2>/dev/null - log "Manual re-check triggered" - return 0 - fi - sleep 1 - elapsed=$((elapsed + 1)) - done -} - -# ---- Signal handlers ---- -cleanup() { - log "Daemon shutting down - re-enabling all apps" - disable_focus_mode - rm -f "$PIDFILE" - exit 0 -} - -# HUP is intentionally NOT trapped so the daemon survives ADB disconnects. -# Only SIGTERM/SIGINT trigger a clean shutdown. -trap cleanup INT TERM - -# ---- Main Loop ---- -main() { - acquire_lock - init - - while true; do - # Invalidate the per-tick curfew_active() memo (see its definition). - _CURFEW_TICK_CACHED="" - - location="$(get_location)" - - if [ -n "$location" ]; then - lat="$(echo "$location" | cut -d',' -f1)" - lon="$(echo "$location" | cut -d',' -f2)" - distance="$(calc_distance "$lat" "$lon" "$HOME_LAT" "$HOME_LON")" - - if [ "$CURRENT_MODE" = "focus" ]; then - threshold=$((RADIUS + HYSTERESIS)) - else - threshold=$((RADIUS - HYSTERESIS)) - fi - - if [ "$distance" -le "$threshold" ] 2>/dev/null; then - enable_focus_mode - else - disable_focus_mode - fi - - curfew_state="day" - curfew_active && curfew_state="CURFEW" - log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE | Curfew: $curfew_state" - write_status_snapshot "$CURRENT_MODE" "$lat" "$lon" "$distance" "$threshold" - else - log "Location unavailable - defaulting to focus mode (restrictions ON)" - enable_focus_mode - write_status_snapshot "$CURRENT_MODE" "" "" "null" "null" - fi - - # Dynamic interval: shorter at home (can charge), longer away (save battery). - # sleep_with_recheck returns early if the companion app requests a recheck. - if [ "$CURRENT_MODE" = "focus" ]; then - sleep_with_recheck "$CHECK_INTERVAL_FOCUS" - else - sleep_with_recheck "$CHECK_INTERVAL_NORMAL" - fi - - rotate_log - done -} - -main "$@" diff --git a/phone_focus_mode/focus_status_app/AndroidManifest.xml b/phone_focus_mode/focus_status_app/AndroidManifest.xml deleted file mode 100644 index 9a2269d8..00000000 --- a/phone_focus_mode/focus_status_app/AndroidManifest.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/phone_focus_mode/focus_status_app/build.sh b/phone_focus_mode/focus_status_app/build.sh deleted file mode 100755 index 6799e007..00000000 --- a/phone_focus_mode/focus_status_app/build.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# ============================================================ -# Focus Status App builder (no Gradle, no Kotlin). -# Compiles a minimal Java APK with aapt2 + javac + d8 + apksigner. -# Produces: build/focus_status.apk (debug-signed) -# ============================================================ -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -cd "$SCRIPT_DIR" - -SDK="${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}" -# Pick highest-numbered build-tools directory. -# A glob into an array rather than parsing ls: a build-tools path containing a -# space or newline would otherwise be split into garbage. -_bt_dirs=("$SDK"/build-tools/*/) -BUILD_TOOLS_DIR="$(printf '%s\n' "${_bt_dirs[@]}" | sort -V | tail -1 | sed 's:/$::')" -# Pick highest-numbered platform. -_plat_dirs=("$SDK"/platforms/android-*/) -PLATFORM_DIR="$(printf '%s\n' "${_plat_dirs[@]}" | sort -V | tail -1 | sed 's:/$::')" -PLATFORM_JAR="$PLATFORM_DIR/android.jar" - -[ -d "$BUILD_TOOLS_DIR" ] || { - echo "ERROR: build-tools not found under $SDK" >&2 - exit 1 -} -[ -f "$PLATFORM_JAR" ] || { - echo "ERROR: android.jar not found at $PLATFORM_JAR" >&2 - exit 1 -} - -AAPT2="$BUILD_TOOLS_DIR/aapt2" -D8="$BUILD_TOOLS_DIR/d8" -ZIPALIGN="$BUILD_TOOLS_DIR/zipalign" -APKSIGNER="$BUILD_TOOLS_DIR/apksigner" - -for tool in "$AAPT2" "$D8" "$ZIPALIGN" "$APKSIGNER"; do - [ -x "$tool" ] || { - echo "ERROR: missing build tool: $tool" >&2 - exit 1 - } -done - -BUILD="$SCRIPT_DIR/build" -rm -rf "$BUILD" -mkdir -p "$BUILD/compiled-res" "$BUILD/classes" "$BUILD/dex" - -# ---- Compile resources (none for now, but aapt2 requires the dir) ---- -mkdir -p "$SCRIPT_DIR/res" -if find "$SCRIPT_DIR/res" -type f -print -quit | grep -q .; then - "$AAPT2" compile --dir "$SCRIPT_DIR/res" -o "$BUILD/compiled-res" -fi - -# ---- Link resources + manifest into base APK ---- -LINK_ARGS=( - --manifest "$SCRIPT_DIR/AndroidManifest.xml" - -I "$PLATFORM_JAR" - --java "$BUILD" - --min-sdk-version 29 - --target-sdk-version 35 - --version-code 1 - --version-name 1.0.0 - -o "$BUILD/base.apk" -) -# Include any compiled res archives. -for rfile in "$BUILD"/compiled-res/*.flat; do - [ -e "$rfile" ] && LINK_ARGS+=("$rfile") -done -"$AAPT2" link "${LINK_ARGS[@]}" - -# ---- Compile Java ---- -# Collect .java files (including generated R.java if resources exist). -JAVA_SRCS=() -while IFS= read -r -d '' f; do JAVA_SRCS+=("$f"); done < <(find "$SCRIPT_DIR/java" "$BUILD" -name '*.java' -print0) - -javac -source 11 -target 11 \ - -classpath "$PLATFORM_JAR" \ - -d "$BUILD/classes" \ - "${JAVA_SRCS[@]}" - -# ---- Dex ---- -CLASS_FILES=() -while IFS= read -r -d '' f; do CLASS_FILES+=("$f"); done < <(find "$BUILD/classes" -name '*.class' -print0) -"$D8" --min-api 29 --output "$BUILD/dex" "${CLASS_FILES[@]}" --lib "$PLATFORM_JAR" - -# ---- Add classes.dex into the APK ---- -cp "$BUILD/base.apk" "$BUILD/unsigned.apk" -(cd "$BUILD/dex" && zip -q "$BUILD/unsigned.apk" classes.dex) - -# ---- Align ---- -"$ZIPALIGN" -f -p 4 "$BUILD/unsigned.apk" "$BUILD/aligned.apk" - -# ---- Sign with debug key (auto-generated on first build) ---- -KEYSTORE="$SCRIPT_DIR/debug.keystore" -if [ ! -f "$KEYSTORE" ]; then - echo "Generating debug keystore..." - keytool -genkeypair -v \ - -keystore "$KEYSTORE" -storepass android -keypass android \ - -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 \ - -dname "CN=Focus Status Debug, OU=Dev, O=Dev, L=NA, ST=NA, C=NA" \ - >/dev/null 2>&1 -fi -"$APKSIGNER" sign \ - --ks "$KEYSTORE" --ks-pass pass:android \ - --key-pass pass:android \ - --out "$BUILD/focus_status.apk" \ - "$BUILD/aligned.apk" - -echo "" -echo "Built: $BUILD/focus_status.apk" -ls -l "$BUILD/focus_status.apk" diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/BootReceiver.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/BootReceiver.java deleted file mode 100644 index 23bb6fd8..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/BootReceiver.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.kuhy.focusstatus; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Autostart the StatusService when the device finishes booting so the - * persistent notification reappears without the user having to launch - * the app manually. BOOT_COMPLETED is delivered after the user unlocks - * the phone for the first time (if Direct Boot is enabled we also - * handle LOCKED_BOOT_COMPLETED to fire earlier). - */ -public final class BootReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (action == null) { - return; - } - if (!"android.intent.action.BOOT_COMPLETED".equals(action) - && !"android.intent.action.LOCKED_BOOT_COMPLETED".equals(action)) { - return; - } - context.startForegroundService(new Intent(context, StatusService.class)); - } -} diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewDemoReceiver.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewDemoReceiver.java deleted file mode 100644 index 47cf0c58..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewDemoReceiver.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.kuhy.focusstatus; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Fired when the user taps the demo-curfew action on the status notification. - * Toggles the curfew FORCE file the daemon + enforcer poll, which makes the - * full curfew engage immediately regardless of clock or location ("demo"): - * - if the force file is absent, create it (and clear any override) -> demo ON; - * - if it is present, delete it -> demo OFF. - * Then writes the recheck trigger so the daemon re-evaluates within ~1s. - * - * Lets you experience the real curfew on demand with a one-tap off switch. - * Safe because the companion app, launcher and keyboard are night-whitelisted, - * so this notification stays tappable throughout the demo. - */ -public final class CurfewDemoReceiver extends BroadcastReceiver { - private static final String FORCE = - "/data/local/tmp/focus_mode/curfew_force_on"; - private static final String OVERRIDE = - "/data/local/tmp/focus_mode/curfew_override"; - private static final String TRIGGER = - "/data/local/tmp/focus_mode/trigger_recheck"; - - @Override - public void onReceive(Context context, Intent intent) { - // Toggle the force file atomically; starting a demo also clears any - // override (which would otherwise suppress the curfew). Always nudge - // the daemon afterwards. - RootShell.run( - "if [ -e " + FORCE + " ]; then rm -f " + FORCE + "; " - + "else touch " + FORCE + " && chmod 666 " + FORCE - + " && rm -f " + OVERRIDE + "; fi; " - + "touch " + TRIGGER + " && chmod 666 " + TRIGGER); - - Intent refresh = new Intent(context, StatusService.class); - context.startForegroundService(refresh); - } -} diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewToggleReceiver.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewToggleReceiver.java deleted file mode 100644 index 242916fe..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/CurfewToggleReceiver.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.kuhy.focusstatus; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Fired when the user taps the curfew action on the status notification. - * Toggles the night-curfew escape-hatch file the daemon + enforcer poll: - * - if the override file is absent, create it -> curfew SUSPENDED; - * - if it is present, delete it -> curfew RE-ARMED. - * Then writes the recheck trigger so the daemon re-evaluates within ~1s and - * the app's next refresh reflects the new state. - * - * This is the on-device "2am opt-out" (no PC needed). It is intentionally the - * only easy way to suspend curfew; everything else is locked. The action is - * shown on the notification only while curfew is active, so it is not a - * day-time temptation. - */ -public final class CurfewToggleReceiver extends BroadcastReceiver { - private static final String OVERRIDE = - "/data/local/tmp/focus_mode/curfew_override"; - private static final String TRIGGER = - "/data/local/tmp/focus_mode/trigger_recheck"; - - @Override - public void onReceive(Context context, Intent intent) { - // Toggle atomically in one root shell: if the file exists remove it, - // else create it world-writable. Always nudge the daemon afterwards. - RootShell.run( - "if [ -e " + OVERRIDE + " ]; then rm -f " + OVERRIDE + "; " - + "else touch " + OVERRIDE + " && chmod 666 " + OVERRIDE + "; fi; " - + "touch " + TRIGGER + " && chmod 666 " + TRIGGER); - - // Immediate service refresh so the notification flips label/state now. - Intent refresh = new Intent(context, StatusService.class); - context.startForegroundService(refresh); - } -} diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/LaunchActivity.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/LaunchActivity.java deleted file mode 100644 index 51a2f9d5..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/LaunchActivity.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.kuhy.focusstatus; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; - -/** - * Tiny invisible activity so the app is launchable from the Minimalist - * Phone app list. Starts the foreground service and finishes immediately. - */ -public final class LaunchActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - startForegroundService(new Intent(this, StatusService.class)); - finish(); - } -} diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RecheckReceiver.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RecheckReceiver.java deleted file mode 100644 index f8c0b543..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RecheckReceiver.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.kuhy.focusstatus; - -import android.app.NotificationManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Fired when the user taps the "Re-check now" notification action. - * Writes the trigger file the daemon polls; the daemon will break its - * sleep and perform a fresh location check within ~1 second. - */ -public final class RecheckReceiver extends BroadcastReceiver { - private static final String TRIGGER = "/data/local/tmp/focus_mode/trigger_recheck"; - - @Override - public void onReceive(Context context, Intent intent) { - RootShell.run("touch " + TRIGGER + " && chmod 666 " + TRIGGER); - // Cause an immediate service refresh so the notification reflects - // the new reading as soon as the daemon writes it. - Intent refresh = new Intent(context, StatusService.class); - context.startForegroundService(refresh); - - NotificationManager nm = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - if (nm != null) { - // No separate toast-channel — just nudge the ongoing notif - // (next tick will show the updated "Last check" timestamp). - nm.cancel(9999); - } - } -} diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RootShell.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RootShell.java deleted file mode 100644 index 7d7665fa..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/RootShell.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.kuhy.focusstatus; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Locale; - -/** - * Tiny root shell helper. Runs commands via Magisk's `su` binary and - * returns stdout. All IO is clamped so one misbehaving command cannot - * stall the notification UI loop indefinitely. - */ -final class RootShell { - - private RootShell() {} - - /** Run a one-shot root command and return its stdout (trimmed). */ - static String run(String cmd) { - Process p = null; - try { - p = Runtime.getRuntime().exec(new String[] { "su", "-c", cmd }); - StringBuilder sb = new StringBuilder(); - try (BufferedReader br = new BufferedReader( - new InputStreamReader(p.getInputStream()))) { - String line; - while ((line = br.readLine()) != null) { - sb.append(line).append('\n'); - } - } - // Bound wait to avoid ANR when root is denied. - p.waitFor(); - return sb.toString().trim(); - } catch (IOException | InterruptedException e) { - return ""; - } finally { - if (p != null) { - p.destroy(); - } - } - } - - /** Return true if a PID file contains a live process. */ - static boolean pidAlive(String pidFilePath) { - String out = run(String.format(Locale.US, - "pid=$(cat %s 2>/dev/null); [ -n \"$pid\" ] && kill -0 \"$pid\" 2>/dev/null && echo yes", - pidFilePath)); - return "yes".equals(out); - } -} diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/Status.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/Status.java deleted file mode 100644 index 1cc2c59d..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/Status.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.kuhy.focusstatus; - -/** - * Parsed snapshot of /data/local/tmp/focus_mode/status.json. - * Minimal hand-rolled JSON reader to avoid pulling in a library. - */ -final class Status { - String mode = "unknown"; - String lat = ""; - String lon = ""; - long distanceM = -1; - long thresholdM = -1; - long radiusM = -1; - long disabledCount = 0; - long lastCheckTs = 0; - String lastCheckIso = ""; - - boolean curfewActive = false; - boolean curfewOverride = false; - boolean curfewForce = false; - - boolean daemonAlive = false; - boolean hostsAlive = false; - boolean dnsAlive = false; - boolean launcherAlive = false; - boolean curfewAlive = false; - - /** Extract a JSON string or numeric value by key. Returns "" if missing. */ - static String extract(String json, String key) { - if (json == null) { - return ""; - } - // Match either "key":"value" or "key":NUMBER or "key":null - String needle = "\"" + key + "\""; - int i = json.indexOf(needle); - if (i < 0) { - return ""; - } - int j = json.indexOf(':', i + needle.length()); - if (j < 0) { - return ""; - } - int k = j + 1; - // Skip whitespace - while (k < json.length() && Character.isWhitespace(json.charAt(k))) { - k++; - } - if (k >= json.length()) { - return ""; - } - if (json.charAt(k) == '"') { - int end = json.indexOf('"', k + 1); - if (end < 0) { - return ""; - } - return json.substring(k + 1, end); - } - int end = k; - while (end < json.length() - && "0123456789-.nul".indexOf(json.charAt(end)) >= 0) { - end++; - } - String v = json.substring(k, end); - return "null".equals(v) ? "" : v; - } - - static Status parse(String json) { - Status s = new Status(); - if (json == null || json.isEmpty()) { - return s; - } - s.mode = nonEmpty(extract(json, "mode"), "unknown"); - s.lat = extract(json, "lat"); - s.lon = extract(json, "lon"); - s.distanceM = parseLongOr(extract(json, "distance_m"), -1); - s.thresholdM = parseLongOr(extract(json, "threshold_m"), -1); - s.radiusM = parseLongOr(extract(json, "radius_m"), -1); - s.disabledCount = parseLongOr(extract(json, "disabled_count"), 0); - s.curfewActive = parseLongOr(extract(json, "curfew"), 0) == 1; - s.curfewOverride = parseLongOr(extract(json, "curfew_override"), 0) == 1; - s.curfewForce = parseLongOr(extract(json, "curfew_force"), 0) == 1; - s.lastCheckTs = parseLongOr(extract(json, "last_check_ts"), 0); - s.lastCheckIso = extract(json, "last_check_iso"); - return s; - } - - private static String nonEmpty(String v, String fallback) { - return (v == null || v.isEmpty()) ? fallback : v; - } - - private static long parseLongOr(String v, long fallback) { - if (v == null || v.isEmpty()) { - return fallback; - } - try { - return Long.parseLong(v); - } catch (NumberFormatException ignored) { - return fallback; - } - } -} diff --git a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/StatusService.java b/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/StatusService.java deleted file mode 100644 index b779e51d..00000000 --- a/phone_focus_mode/focus_status_app/java/com/kuhy/focusstatus/StatusService.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.kuhy.focusstatus; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Intent; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; - -/** - * Foreground service that keeps a persistent notification showing the - * current state of the focus-mode daemon on the rooted phone. Re-reads - * /data/local/tmp/focus_mode/status.json via root shell every ~5s and - * updates the notification text. Notification carries a "Re-check now" - * action that broadcasts to RecheckReceiver. - */ -public final class StatusService extends Service { - - private static final String CHANNEL_ID = "focus_status"; - private static final int NOTIF_ID = 1042; - private static final long REFRESH_MS = 5_000L; - - private static final String STATUS_FILE = "/data/local/tmp/focus_mode/status.json"; - private static final String DAEMON_PID = "/data/local/tmp/focus_mode/daemon.pid"; - private static final String HOSTS_PID = "/data/local/tmp/focus_mode/hosts_enforcer.pid"; - private static final String DNS_PID = "/data/local/tmp/focus_mode/dns_enforcer.pid"; - private static final String LAUNCHER_PID = "/data/local/tmp/focus_mode/launcher_enforcer.pid"; - private static final String CURFEW_PID = "/data/local/tmp/focus_mode/curfew_enforcer.pid"; - - private Handler handler; - private final Runnable tick = new Runnable() { - @Override - public void run() { - refresh(); - handler.postDelayed(this, REFRESH_MS); - } - }; - - @Override - public void onCreate() { - super.onCreate(); - handler = new Handler(Looper.getMainLooper()); - ensureChannel(); - startForeground(NOTIF_ID, buildNotification(null)); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - // Immediate refresh so the user's launch / action feels responsive. - handler.removeCallbacks(tick); - handler.post(tick); - return START_STICKY; - } - - @Override - public void onDestroy() { - handler.removeCallbacks(tick); - super.onDestroy(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - private void refresh() { - String json = RootShell.run("cat " + STATUS_FILE + " 2>/dev/null"); - Status s = Status.parse(json); - s.daemonAlive = RootShell.pidAlive(DAEMON_PID); - s.hostsAlive = RootShell.pidAlive(HOSTS_PID); - s.dnsAlive = RootShell.pidAlive(DNS_PID); - s.launcherAlive = RootShell.pidAlive(LAUNCHER_PID); - s.curfewAlive = RootShell.pidAlive(CURFEW_PID); - - NotificationManager nm = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - if (nm != null) { - nm.notify(NOTIF_ID, buildNotification(s)); - } - } - - private void ensureChannel() { - NotificationManager nm = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - if (nm == null) { - return; - } - NotificationChannel ch = new NotificationChannel( - CHANNEL_ID, "Focus Mode Status", - NotificationManager.IMPORTANCE_LOW); - ch.setDescription("Persistent status of the focus-mode daemon"); - ch.setShowBadge(false); - ch.setSound(null, null); - ch.enableVibration(false); - nm.createNotificationChannel(ch); - } - - private Notification buildNotification(Status s) { - String title; - String summary; - String big; - int icon; - - if (s == null) { - title = "Focus Mode: starting..."; - summary = "Reading status"; - big = "Contacting root daemon\u2026"; - icon = android.R.drawable.ic_menu_compass; - } else { - boolean focus = "focus".equals(s.mode); - title = focus ? "\uD83C\uDFE0 Focus: HOME" : "\u2708 Focus: AWAY"; - if (!s.daemonAlive) { - title = "\u26A0\uFE0F Focus: DAEMON DOWN"; - } - String dist = (s.distanceM < 0) - ? "?" - : (s.distanceM + "m"); - summary = "dist " + dist - + " \u00B7 disabled " + s.disabledCount - + " \u00B7 " + shortTime(s.lastCheckIso); - big = buildBigText(s); - icon = focus - ? android.R.drawable.ic_lock_idle_lock - : android.R.drawable.ic_menu_mylocation; - } - - PendingIntent recheck = PendingIntent.getBroadcast( - this, 0, - new Intent(this, RecheckReceiver.class) - .setAction("com.kuhy.focusstatus.RECHECK"), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - Notification.Builder b = new Notification.Builder(this, CHANNEL_ID) - .setSmallIcon(icon) - .setContentTitle(title) - .setContentText(summary) - .setStyle(new Notification.BigTextStyle().bigText(big)) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setShowWhen(false) - .setCategory(Notification.CATEGORY_STATUS) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .addAction(new Notification.Action.Builder( - android.R.drawable.ic_popup_sync, - "Re-check now", recheck).build()); - - // One contextual curfew action: - // - demo running -> Stop demo curfew (force toggle) - // - real curfew active -> Suspend till morning (override toggle) - // - override set, idle -> Re-arm curfew (override toggle) - // - idle / daytime -> Start demo curfew (force toggle) - // Demo lets you experience the full curfew on demand with a one-tap - // off switch; the companion app/launcher/keyboard stay whitelisted so - // the notification is always reachable to stop it. - if (s != null) { - String label; - int actionIcon; - PendingIntent pi; - if (s.curfewForce) { - label = "Stop demo curfew"; - actionIcon = android.R.drawable.ic_menu_close_clear_cancel; - pi = demoIntent(); - } else if (s.curfewActive) { - label = "Suspend curfew till morning"; - actionIcon = android.R.drawable.ic_menu_close_clear_cancel; - pi = overrideIntent(); - } else if (s.curfewOverride) { - label = "Re-arm curfew"; - actionIcon = android.R.drawable.ic_lock_idle_lock; - pi = overrideIntent(); - } else { - label = "Start demo curfew"; - actionIcon = android.R.drawable.ic_media_play; - pi = demoIntent(); - } - b.addAction(new Notification.Action.Builder(actionIcon, label, pi).build()); - } - return b.build(); - } - - private PendingIntent overrideIntent() { - return PendingIntent.getBroadcast(this, 1, - new Intent(this, CurfewToggleReceiver.class) - .setAction("com.kuhy.focusstatus.CURFEW_TOGGLE"), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } - - private PendingIntent demoIntent() { - return PendingIntent.getBroadcast(this, 2, - new Intent(this, CurfewDemoReceiver.class) - .setAction("com.kuhy.focusstatus.CURFEW_DEMO"), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } - - private static String buildBigText(Status s) { - StringBuilder sb = new StringBuilder(); - if ("focus".equals(s.mode)) { - sb.append("At home \u2014 restrictions active\n"); - } else if ("normal".equals(s.mode)) { - sb.append("Away from home \u2014 normal mode\n"); - } else { - sb.append("Mode: ").append(s.mode).append('\n'); - } - if (s.distanceM >= 0) { - sb.append("Distance: ").append(s.distanceM).append('m'); - if (s.thresholdM >= 0) { - sb.append(" (threshold ").append(s.thresholdM).append("m)"); - } - sb.append('\n'); - } - if (!s.lat.isEmpty() && !s.lon.isEmpty()) { - sb.append("GPS: ").append(s.lat).append(", ").append(s.lon).append('\n'); - } - sb.append("Disabled apps: ").append(s.disabledCount).append('\n'); - if (s.curfewForce) { - sb.append("Demo curfew: RUNNING — strict list, grayscale, DND (tap Stop)\n"); - } else if (s.curfewOverride) { - sb.append("Night curfew: SUSPENDED (tap to re-arm)\n"); - } else if (s.curfewActive) { - sb.append("Night curfew: ACTIVE — strict list, grayscale, DND\n"); - } - sb.append("Last check: ").append( - s.lastCheckIso.isEmpty() ? "never" : s.lastCheckIso).append('\n'); - sb.append("Daemons: ") - .append(tag("focus", s.daemonAlive)).append(' ') - .append(tag("hosts", s.hostsAlive)).append(' ') - .append(tag("dns", s.dnsAlive)).append(' ') - .append(tag("launcher", s.launcherAlive)).append(' ') - .append(tag("curfew", s.curfewAlive)); - return sb.toString(); - } - - private static String tag(String name, boolean ok) { - return (ok ? "\u2713" : "\u2717") + name; - } - - private static String shortTime(String iso) { - // Expect "YYYY-MM-DD HH:MM:SS"; show HH:MM:SS. - if (iso == null || iso.length() < 19) { - return iso == null ? "" : iso; - } - return iso.substring(11, 19); - } -} diff --git a/phone_focus_mode/hosts_enforcer.sh b/phone_focus_mode/hosts_enforcer.sh deleted file mode 100755 index 91121cae..00000000 --- a/phone_focus_mode/hosts_enforcer.sh +++ /dev/null @@ -1,207 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ============================================================ -# Hosts file enforcer for rooted Android. -# -# Mirrors the PC-side guard in linux_configuration/hosts/ but -# for /system/etc/hosts on Android, which has no chattr, no -# systemd, and where /system is read-only. -# -# Strategy (defense in depth): -# 1. Canonical hosts file lives at HOSTS_CANONICAL and is -# chattr +i (best-effort; ignored if kernel/fs rejects). -# 2. Bind-mount HOSTS_CANONICAL read-only over HOSTS_TARGET so -# that even `echo > /system/etc/hosts` fails for everyone, -# including root-in-a-terminal-app, without re-mounting. -# 3. A watchdog loop re-asserts the bind mount and verifies -# sha256 every HOSTS_CHECK_INTERVAL seconds. -# -# Known limitation: a user with root *and* willingness to run -# `umount /system/etc/hosts; mount -o remount,rw /system ...` -# can still bypass this. Making it "impossible without USB" is -# not achievable on a rooted phone with a local terminal. -# This enforcer closes the one-liner gap and adds logging so -# tampering leaves an audit trail. -# ============================================================ - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -. "$SCRIPT_DIR/config.sh" -# shellcheck source=hosts_magisk.sh -. "$SCRIPT_DIR/hosts_magisk.sh" -# shellcheck source=hosts_mount.sh -. "$SCRIPT_DIR/hosts_mount.sh" - -PIDFILE="$STATE_DIR/hosts_enforcer.pid" - -mkdir -p "$STATE_DIR" "$(dirname "$HOSTS_CANONICAL")" -touch "$HOSTS_LOG" -chmod 666 "$HOSTS_LOG" 2>/dev/null || true - -log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >> "$HOSTS_LOG" -} - -rotate_log() { - local lines - lines="$(wc -l < "$HOSTS_LOG" 2>/dev/null || echo 0)" - if [ "$lines" -gt 500 ]; then - local tmp="$HOSTS_LOG.tmp" - tail -n 500 "$HOSTS_LOG" > "$tmp" - mv "$tmp" "$HOSTS_LOG" - fi -} - -acquire_lock() { - if [ -f "$PIDFILE" ]; then - local old_pid - old_pid="$(cat "$PIDFILE")" - if kill -0 "$old_pid" 2>/dev/null; then - local cmdline - cmdline="$(tr '\0' ' ' < "/proc/$old_pid/cmdline" 2>/dev/null)" - if echo "$cmdline" | grep -q "hosts_enforcer"; then - echo "hosts_enforcer already running (PID $old_pid)" - exit 0 - fi - fi - rm -f "$PIDFILE" - fi - echo $$ > "$PIDFILE" -} - -sha256_of() { - # Android's toybox has sha256sum; fall back to md5sum if missing. - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" 2>/dev/null | awk '{print $1}' - else - md5sum "$1" 2>/dev/null | awk '{print $1}' - fi -} - -# ---- Workout-aware canonical selection ---- -# When workout_detector.sh writes "1" to $WORKOUT_ACTIVE_FILE, switch to -# the YouTube-relaxed canonical. Any other value (including missing file or -# unreadable) falls back to the full-block canonical (fail-closed). -workout_active() { - [ -f "$WORKOUT_ACTIVE_FILE" ] || return 1 - local v - v="$(tr -d '[:space:]' < "$WORKOUT_ACTIVE_FILE" 2>/dev/null)" - [ "$v" = "1" ] -} - -current_canonical() { - if workout_active && [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then - echo "$HOSTS_CANONICAL_WORKOUT" - else - echo "$HOSTS_CANONICAL" - fi -} - -current_sha_file() { - if workout_active && [ -f "$HOSTS_SHA_FILE_WORKOUT" ]; then - echo "$HOSTS_SHA_FILE_WORKOUT" - else - echo "$HOSTS_SHA_FILE" - fi -} - - - - - - - - - - - -verify_and_restore() { - local canonical sha_file - canonical="$(current_canonical)" - sha_file="$(current_sha_file)" - - if [ ! -f "$canonical" ]; then - log "ERROR: canonical hosts missing at $canonical" - return 1 - fi - - local expected - expected="$(cat "$sha_file" 2>/dev/null)" - if [ -z "$expected" ]; then - expected="$(sha256_of "$canonical")" - echo "$expected" > "$sha_file" - chmod 644 "$sha_file" 2>/dev/null || true - chattr +i "$sha_file" 2>/dev/null || true - fi - - # Canonical integrity check - local actual_canonical - actual_canonical="$(sha256_of "$canonical")" - if [ "$actual_canonical" != "$expected" ]; then - log "TAMPER: $(basename "$canonical") hash mismatch (expected $expected, got $actual_canonical)" - # We cannot fix the canonical from here - it is the source of truth. - # Just log and continue; deploy.sh must re-push. - return 1 - fi - - # Live target integrity check. Mismatch can mean either tampering OR a - # legitimate workout-state transition that swapped the active canonical. - # In both cases the fix is the same: re-assert the bind mount with the - # currently-active canonical. - local actual_target - actual_target="$(sha256_of "$HOSTS_TARGET")" - if [ "$actual_target" != "$expected" ]; then - if workout_active; then - log "Workout-active swap: $HOSTS_TARGET differs from workout canonical - re-mounting" - else - log "TAMPER or post-workout swap: $HOSTS_TARGET hash mismatch - restoring" - fi - assert_bind_mount - flush_browser_dns_caches - fi -} - -cleanup() { - log "hosts_enforcer shutting down" - rm -f "$PIDFILE" - exit 0 -} - -trap cleanup INT TERM - -main() { - acquire_lock - log "hosts_enforcer started (PID=$$)" - - ensure_canonical_immutable - protect_magisk_module - # Initial assertion - assert_bind_mount || true - # Restart netd so its in-memory hosts cache picks up the bind mount. - # Android 13 caches /etc/hosts at netd startup and never re-reads it; - # without this restart every DNS query bypasses our block list. - restart_netd_for_hosts_cache - flush_browser_dns_caches - - # Seed sha files if missing — one per canonical variant. - if [ ! -f "$HOSTS_SHA_FILE" ] && [ -f "$HOSTS_CANONICAL" ]; then - sha256_of "$HOSTS_CANONICAL" > "$HOSTS_SHA_FILE" - chmod 644 "$HOSTS_SHA_FILE" 2>/dev/null || true - chattr +i "$HOSTS_SHA_FILE" 2>/dev/null || true - fi - if [ ! -f "$HOSTS_SHA_FILE_WORKOUT" ] && [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then - sha256_of "$HOSTS_CANONICAL_WORKOUT" > "$HOSTS_SHA_FILE_WORKOUT" - chmod 644 "$HOSTS_SHA_FILE_WORKOUT" 2>/dev/null || true - chattr +i "$HOSTS_SHA_FILE_WORKOUT" 2>/dev/null || true - fi - - while true; do - verify_and_restore - protect_magisk_module - rotate_log - sleep "$HOSTS_CHECK_INTERVAL" - done -} - -main "$@" diff --git a/phone_focus_mode/hosts_magisk.sh b/phone_focus_mode/hosts_magisk.sh deleted file mode 100755 index c1405186..00000000 --- a/phone_focus_mode/hosts_magisk.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# hosts_magisk.sh — the persistence and cache-invalidation half of -# hosts_enforcer.sh: keeping the Magisk systemless-hosts module in step with -# the canonical blocklist, holding the canonical file immutable, and making -# already-running resolvers notice a change (Firefox DoH, browser DNS caches, -# netd). -# -# A flat sibling, not lib/: deploy.sh pushes phone scripts into one directory, -# which is why dns_iptables.sh, tether_iptables.sh and curfew_net.sh live -# beside their enforcers too. -# -# Sourced by hosts_enforcer.sh, which owns log() and the config.sh globals -# these read ($HOSTS_MAGISK_MODULE_FILE, $HOSTS_CANONICAL, -# $HOSTS_CANONICAL_WORKOUT, $BROWSER_PACKAGES, $STATE_DIR). Nothing is -# assigned here, so there is no global to keep in step. - -# Keep the Magisk Systemless Hosts module file in sync with the currently -# active canonical so that a future reboot mounts the correct variant. We -# only rewrite when the contents differ (cheap hash compare) to avoid -# touching the module dir on every loop iteration. -sync_magisk_module() { - local canonical="$1" - [ -n "$canonical" ] && [ -f "$canonical" ] || return 0 - local module_dir - module_dir="$(dirname "$(dirname "$(dirname "$HOSTS_MAGISK_MODULE_FILE")")")" - [ -d "$module_dir" ] || return 0 - local module_hash canonical_hash - module_hash="$(sha256_of "$HOSTS_MAGISK_MODULE_FILE")" - canonical_hash="$(sha256_of "$canonical")" - if [ "$module_hash" != "$canonical_hash" ]; then - # Drop +i on the module dir + file long enough to update, then re-lock. - chattr -i "$module_dir" 2>/dev/null || true - chattr -i "$HOSTS_MAGISK_MODULE_FILE" 2>/dev/null || true - cp "$canonical" "$HOSTS_MAGISK_MODULE_FILE" 2>/dev/null || return 0 - chmod 644 "$HOSTS_MAGISK_MODULE_FILE" 2>/dev/null || true - chattr +i "$HOSTS_MAGISK_MODULE_FILE" 2>/dev/null || true - log "Synced Magisk module hosts to $(basename "$canonical")" - fi - # Always re-assert dir-level lock at the end so a partial earlier failure - # leaves us in the locked state on the next iteration. - protect_magisk_module -} - -# Defense against the user disabling the Magisk Systemless Hosts module via -# the Magisk app UI. The "Disable" / "Remove" buttons work by creating a -# marker file inside the module directory: -# -# /data/adb/modules/hosts/disable # disable on next reboot -# /data/adb/modules/hosts/remove # uninstall on next reboot -# -# We do TWO things every poll cycle: -# 1. Delete those markers if they appeared (so a reboot still loads us) -# 2. Set chattr +i on the module directory itself so the Magisk app cannot -# create those markers in the first place. The +i flag on a directory -# blocks adding/removing/renaming entries inside it, which is exactly -# what `touch disable` does. Files already inside remain mutable, so -# sync_magisk_module() can still rewrite the hosts file (it briefly -# drops the lock above to handle the rare case where it must). -# -# To intentionally disable the module for maintenance, run from a root -# shell on the phone: -# focus_ctl.sh hosts-stop -# chattr -i /data/adb/modules/hosts -# touch /data/adb/modules/hosts/disable -protect_magisk_module() { - local module_dir - module_dir="$(dirname "$(dirname "$(dirname "$HOSTS_MAGISK_MODULE_FILE")")")" - [ -d "$module_dir" ] || return 0 - # Step 1: nuke any disable/remove markers that may have been created - # since the last poll. We have to chattr -i first because either of the - # two locks below may already be in effect. - local removed=0 - for marker in disable remove update; do - local f="$module_dir/$marker" - if [ -e "$f" ]; then - chattr -i "$module_dir" 2>/dev/null || true - chattr -i "$f" 2>/dev/null || true - if rm -f "$f" 2>/dev/null; then - removed=$((removed + 1)) - log "TAMPER: removed Magisk module marker $f" - fi - fi - done - # Step 2: re-lock the module dir so the Magisk app cannot recreate them - # via its UI. Best-effort - if the kernel/fs rejects +i, the runtime - # delete loop above is still our safety net. - chattr +i "$module_dir" 2>/dev/null || true - return $removed -} - -# Write user.js to every Firefox profile to hard-disable DNS-over-HTTPS. -# Firefox uses hardcoded Cloudflare bootstrap IPs (104.16.248.249 etc.) to -# reach mozilla.cloudflare-dns.com, bypassing /etc/hosts entirely. -# TRR mode 5 = DoH disabled; the pref is re-applied on every flush so it -# survives Firefox's automatic pref-reset logic. -disable_firefox_doh() { - local profile_dir - for profile_dir in /data/data/org.mozilla.fenix/files/mozilla/*/; do - # Only write to real profile directories (they contain prefs.js). - [ -f "${profile_dir}prefs.js" ] || continue - grep -qF '"network.trr.mode"' "${profile_dir}user.js" 2>/dev/null \ - || { printf 'user_pref("network.trr.mode", 5);\n' >> "${profile_dir}user.js" 2>/dev/null \ - && log "Wrote DoH-disable pref to ${profile_dir}user.js"; } - done -} - -# Force-stop browsers so their in-process DNS caches are cleared. -# Apps like Firefox and Chrome cache resolved IPs internally; without -# a fresh start they continue reaching blocked domains despite hosts. -# Called at daemon startup and after every detected restore/tamper. -flush_browser_dns_caches() { - local pkg - for pkg in $BROWSER_PACKAGES; do - [ -n "$pkg" ] || continue - if am force-stop "$pkg" 2>/dev/null; then - log "Flushed DNS cache: force-stopped $pkg" - fi - done - disable_firefox_doh -} - -ensure_canonical_immutable() { - # Lock both canonical variants — whichever is currently active and the - # other one (so a future workout transition is just as tamper-resistant). - chmod 644 "$HOSTS_CANONICAL" 2>/dev/null || true - chattr +i "$HOSTS_CANONICAL" 2>/dev/null || true - if [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then - chmod 644 "$HOSTS_CANONICAL_WORKOUT" 2>/dev/null || true - chattr +i "$HOSTS_CANONICAL_WORKOUT" 2>/dev/null || true - fi -} - -# Restart netd so it re-reads the bind-mounted hosts file from disk. -# Android 13's DNS resolver (libnetd_resolv.so) caches /etc/hosts entirely -# in memory when netd starts. Our bind mount updates the on-disk file but -# netd's in-memory cache stays stale until netd restarts. -# -# We use a PID-stamp file: if netd's PID hasn't changed since our last -# restart, we already restarted it in this boot session and skip the work. -# This avoids a network blip on every enforcer restart, while still -# triggering a reload if netd itself has been cycled. -restart_netd_for_hosts_cache() { - local stamp_file="$STATE_DIR/netd_restart.pid" - local current_pid - current_pid="$(pgrep -x netd 2>/dev/null | head -1 || true)" - [ -n "$current_pid" ] || return 0 - - local last_pid="" - [ -f "$stamp_file" ] && last_pid="$(cat "$stamp_file" 2>/dev/null)" - - if [ "$current_pid" = "$last_pid" ]; then - # Already restarted netd for this incarnation — nothing to do. - return 0 - fi - - log "Restarting netd (PID $current_pid) to reload hosts file cache (~3s network pause)..." - stop netd 2>/dev/null || true - sleep 2 - start netd 2>/dev/null || true - sleep 2 - local new_pid - new_pid="$(pgrep -x netd 2>/dev/null | head -1 || true)" - echo "${new_pid:-$current_pid}" > "$stamp_file" 2>/dev/null || true - log "netd restarted (new PID ${new_pid:-unknown}) — hosts cache is now live" -} diff --git a/phone_focus_mode/hosts_mount.sh b/phone_focus_mode/hosts_mount.sh deleted file mode 100755 index 37159c3f..00000000 --- a/phone_focus_mode/hosts_mount.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# hosts_mount.sh — the bind-mount half of hosts_enforcer.sh: proving the -# blocklist is actually mounted over $HOSTS_TARGET, clearing a stale or wrong -# mount, and making the target writable once on ROMs that ship it read-only. -# -# A flat sibling, not lib/, for the same reason as hosts_magisk.sh: deploy.sh -# pushes phone scripts into one directory. -# -# Sourced by hosts_enforcer.sh, which owns log() and $HOSTS_TARGET, the only -# config.sh global these read. Nothing is assigned here. - -is_bind_mounted_correctly() { - # Android devices often already have /system/etc/hosts as its own mount - # point (OEM overlay / f2fs block). A mere "path is in /proc/self/mounts" - # check is not enough - we must verify the mounted content matches our - # currently-active canonical by hash (which depends on workout state). - # No early -f guard on the target: sha256_of returns empty for a file - # that does not exist, so the -n check below already rejects that case. - # The guard was provably equivalent — a mutation removing it could not be - # distinguished by any test — and equivalent code is a claim of a check - # that is not actually being made. - local target_hash canonical_hash canonical - canonical="$(current_canonical)" - target_hash="$(sha256_of "$HOSTS_TARGET")" - canonical_hash="$(sha256_of "$canonical")" - [ -n "$target_hash" ] && [ "$target_hash" = "$canonical_hash" ] -} - -unmount_existing_hosts_mount() { - # If anything else is already mounted on /system/etc/hosts (OEM overlay - # or a previous failed bind), unmount it so we can take its place. - local attempts=0 - while grep -qE "[[:space:]]${HOSTS_TARGET}[[:space:]]" /proc/self/mounts 2>/dev/null; do - if [ "$attempts" -ge 5 ]; then - log "Could not fully unmount $HOSTS_TARGET after 5 attempts" - return 1 - fi - umount "$HOSTS_TARGET" 2>/dev/null \ - || umount -l "$HOSTS_TARGET" 2>/dev/null \ - || break - attempts=$((attempts + 1)) - done - return 0 -} - -make_target_writable_once() { - # /system is usually mounted read-only. Make it rw just long enough - # to overwrite HOSTS_TARGET with the canonical content, then remount ro. - local system_mount canonical - canonical="$(current_canonical)" - system_mount="$(awk '$2=="/system"{print $2; exit}' /proc/self/mounts)" - if [ -z "$system_mount" ]; then - system_mount="/system" - fi - mount -o remount,rw "$system_mount" 2>/dev/null || true - chattr -i "$HOSTS_TARGET" 2>/dev/null || true - cp "$canonical" "$HOSTS_TARGET" 2>/dev/null || true - chmod 644 "$HOSTS_TARGET" 2>/dev/null || true - chattr +i "$HOSTS_TARGET" 2>/dev/null || true - mount -o remount,ro "$system_mount" 2>/dev/null || true -} - -assert_bind_mount() { - if is_bind_mounted_correctly; then - return 0 - fi - # Something is in the way (OEM overlay or previous partial mount). - unmount_existing_hosts_mount - local canonical - canonical="$(current_canonical)" - # Try plain bind mount - no remount-rw of /system needed. - if mount --bind "$canonical" "$HOSTS_TARGET" 2>/dev/null; then - mount -o remount,ro,bind "$HOSTS_TARGET" 2>/dev/null || true - if is_bind_mounted_correctly; then - log "Bind-mounted $canonical over $HOSTS_TARGET" - sync_magisk_module "$canonical" - return 0 - fi - log "Bind mount reported success but target still mismatches - unmounting" - umount "$HOSTS_TARGET" 2>/dev/null || true - fi - # Bind failed - fall back to direct overwrite of /system/etc/hosts. - log "Bind mount failed - falling back to direct overwrite" - make_target_writable_once - if is_bind_mounted_correctly; then - sync_magisk_module "$canonical" - return 0 - fi - return 1 -} diff --git a/phone_focus_mode/launcher_enforcer.sh b/phone_focus_mode/launcher_enforcer.sh deleted file mode 100755 index 2f8f352d..00000000 --- a/phone_focus_mode/launcher_enforcer.sh +++ /dev/null @@ -1,179 +0,0 @@ -#!/system/bin/sh -# shellcheck shell=ash -# ============================================================ -# Launcher enforcer for rooted Android. -# -# Goal: -# 1. Keep $LAUNCHER_PACKAGE installed at all times. If it is -# uninstalled (with or without `-k`), reinstall from -# $LAUNCHER_APK snapshot within $LAUNCHER_CHECK_INTERVAL. -# 2. Keep it pinned as the default HOME activity. If the user -# switches launchers via Settings or the picker, restore it. -# 3. Prevent competing launchers ($LAUNCHER_COMPETITORS) from -# being offered by `pm disable-user`-ing them. -# -# Known limitation: a user with root in a terminal can still -# stop this daemon and change HOME. That's the same threat model -# as hosts_enforcer.sh - this closes the "tap to uninstall / pick -# a new launcher" gap and leaves a tamper trail in the log. -# ============================================================ - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -# shellcheck source=config.sh -. "$SCRIPT_DIR/config.sh" - -PIDFILE="$STATE_DIR/launcher_enforcer.pid" -# Tracks competitors we disabled ourselves, so `launcher-stop` can undo them. -DISABLED_COMPETITORS_FILE="$STATE_DIR/disabled_competitors.txt" - -mkdir -p "$STATE_DIR" "$(dirname "$LAUNCHER_APK")" -touch "$LAUNCHER_LOG" "$DISABLED_COMPETITORS_FILE" -chmod 666 "$LAUNCHER_LOG" "$DISABLED_COMPETITORS_FILE" 2>/dev/null || true - -log() { - local ts - ts="$(date '+%Y-%m-%d %H:%M:%S')" - echo "[$ts] $1" >> "$LAUNCHER_LOG" -} - -rotate_log() { - local lines - lines="$(wc -l < "$LAUNCHER_LOG" 2>/dev/null || echo 0)" - if [ "$lines" -gt 500 ]; then - local tmp="$LAUNCHER_LOG.tmp" - tail -n 500 "$LAUNCHER_LOG" > "$tmp" - mv "$tmp" "$LAUNCHER_LOG" - fi -} - -acquire_lock() { - if [ -f "$PIDFILE" ]; then - local old_pid - old_pid="$(cat "$PIDFILE")" - if kill -0 "$old_pid" 2>/dev/null; then - local cmdline - cmdline="$(tr '\0' ' ' < "/proc/$old_pid/cmdline" 2>/dev/null)" - if echo "$cmdline" | grep -q "launcher_enforcer"; then - echo "launcher_enforcer already running (PID $old_pid)" - exit 0 - fi - fi - rm -f "$PIDFILE" - fi - echo $$ > "$PIDFILE" -} - -# ---- Package state helpers ---- - -pkg_installed() { - # `pm path` exits 0 and prints package: line when present. - pm path "$1" >/dev/null 2>&1 -} - -pkg_enabled() { - # `pm list packages -e` lists enabled packages. Use exact match. - pm list packages -e 2>/dev/null | grep -qxE "package:$1" -} - -current_home_component() { - # Returns "pkg/Activity" of the current default HOME, or "" if ambiguous. - cmd package resolve-activity --brief -c android.intent.category.HOME \ - -a android.intent.action.MAIN 2>/dev/null \ - | awk 'NR==2{print}' -} - -# ---- Enforcement actions ---- - -reinstall_launcher() { - if [ ! -f "$LAUNCHER_APK" ]; then - log "ERROR: cannot reinstall - APK snapshot missing at $LAUNCHER_APK" - return 1 - fi - local expected actual - expected="$(cat "$LAUNCHER_SHA_FILE" 2>/dev/null)" - actual="$(sha256sum "$LAUNCHER_APK" 2>/dev/null | awk '{print $1}')" - if [ -n "$expected" ] && [ "$expected" != "$actual" ]; then - log "ERROR: APK snapshot hash mismatch (expected $expected, got $actual) - refusing to install" - return 1 - fi - log "REINSTALL: $LAUNCHER_PACKAGE missing - installing from snapshot" - # -g grants all runtime permissions so the launcher starts clean - # without blocking on a permission dialog that itself may need the - # launcher to be usable. - if pm install -r -g "$LAUNCHER_APK" >/dev/null 2>&1; then - log "REINSTALL: $LAUNCHER_PACKAGE installed successfully" - return 0 - fi - # Fallback: `pm install` without -g on older Androids - if pm install -r "$LAUNCHER_APK" >/dev/null 2>&1; then - log "REINSTALL: $LAUNCHER_PACKAGE installed (without -g)" - return 0 - fi - log "ERROR: pm install failed" - return 1 -} - -ensure_home_pinned() { - local desired actual - desired="$(cat "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null)" - if [ -z "$desired" ]; then - return 0 # not armed yet; deploy.sh --snapshot-launcher writes this - fi - actual="$(current_home_component)" - if [ "$actual" = "$desired" ]; then - return 0 - fi - log "HOME: default is '$actual' not '$desired' - restoring" - cmd package set-home-activity "$desired" >/dev/null 2>&1 || \ - log "ERROR: set-home-activity failed for $desired" -} - -disable_competitors() { - # Disable every competitor that is still enabled. Remember what we - # disabled so `launcher-stop` can re-enable. - echo "$LAUNCHER_COMPETITORS" | while read -r pkg; do - [ -z "$pkg" ] && continue - [ "${pkg#\#}" != "$pkg" ] && continue # skip comments - if pkg_installed "$pkg" && pkg_enabled "$pkg"; then - if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then - log "Disabled competing launcher: $pkg" - grep -qxE "$pkg" "$DISABLED_COMPETITORS_FILE" 2>/dev/null \ - || echo "$pkg" >> "$DISABLED_COMPETITORS_FILE" - fi - fi - done -} - -# ---- Main loop ---- - -cleanup() { - log "launcher_enforcer shutting down" - rm -f "$PIDFILE" - exit 0 -} - -trap cleanup INT TERM - -main() { - acquire_lock - log "launcher_enforcer started (PID=$$)" - - # Initial arm-up - if ! pkg_installed "$LAUNCHER_PACKAGE"; then - reinstall_launcher || true - fi - ensure_home_pinned - disable_competitors - - while true; do - if ! pkg_installed "$LAUNCHER_PACKAGE"; then - reinstall_launcher || true - fi - ensure_home_pinned - disable_competitors - rotate_log - sleep "$LAUNCHER_CHECK_INTERVAL" - done -} - -main "$@" diff --git a/phone_focus_mode/lib/adb_common.sh b/phone_focus_mode/lib/adb_common.sh deleted file mode 100755 index 4ec690de..00000000 --- a/phone_focus_mode/lib/adb_common.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash -# lib/adb_common.sh — ADB device selection, identity, and root helpers. -# Source this file; do not execute directly. -set -euo pipefail - -_PHONE_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/phone_focus_mode" -TRUSTED_DEVICE_FILE="${_PHONE_STATE_DIR}/trusted_device.sh" -# One file per enrolled device, so a second phone does not evict the first. -# The legacy single-device record above is still honoured when present. -TRUSTED_DEVICE_DIR="${_PHONE_STATE_DIR}/trusted_devices" -LOCK_DIR="${_PHONE_STATE_DIR}/locks" -LAST_RUN_DIR="${_PHONE_STATE_DIR}/last_run" -LOCK_FILE="" - -_info() { - printf '\033[0;34m[INFO]\033[0m %s\n' "$*" >&2 -} - -_warn() { - printf '\033[0;33m[WARN]\033[0m %s\n' "$*" >&2 -} - -_error() { - printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2 -} - -_fatal() { - printf '\033[0;31m[FATAL]\033[0m %s\n' "$*" >&2 - exit 1 -} - -_box() { - local title="$1" - shift - - printf '\n\033[1;33m╔══════════════════════════════════════════╗\033[0m\n' >&2 - printf '\033[1;33m║ %-42s║\033[0m\n' "${title}" >&2 - printf '\033[1;33m╚══════════════════════════════════════════╝\033[0m\n' >&2 - for line in "$@"; do - printf ' %s\n' "${line}" >&2 - done -} - -adb_list_serials() { - adb devices 2>/dev/null | - awk 'NR > 1 && $2 ~ /^(device|offline|unauthorized)$/ { print $1 }' -} - -# Reads one enrolled-device record into TRUSTED_*_LOADED. -# Takes the path explicitly, defaulting to the legacy single-device record. -# Callers that need a specific record pass it rather than shadowing the global, -# which kept the value from leaking into later calls without relying on a -# `local` of a name declared in another file. -_load_trusted_device_values() { - local file_path="${1:-${TRUSTED_DEVICE_FILE}}" - local -a loaded_values=() - - [[ -f "${file_path}" ]] || return 1 - - if ! mapfile -t loaded_values < <( - bash -c ' - set -euo pipefail - source "$1" - printf "%s\n" "${TRUSTED_SERIAL:-}" "${TRUSTED_MODEL:-}" "${TRUSTED_FINGERPRINT:-}" - ' bash "${file_path}" - ); then - _warn "Trusted device record is unreadable: ${file_path}" - return 1 - fi - - TRUSTED_SERIAL_LOADED="${loaded_values[0]:-}" - TRUSTED_MODEL_LOADED="${loaded_values[1]:-}" - TRUSTED_FINGERPRINT_LOADED="${loaded_values[2]:-}" - export TRUSTED_SERIAL_LOADED TRUSTED_MODEL_LOADED TRUSTED_FINGERPRINT_LOADED -} - -adb_select_device() { - local requested="${1:-${ADB_SERIAL:-}}" - local found=0 - local serial="" - local -a serials=() - - mapfile -t serials < <(adb_list_serials) - - if [[ ${#serials[@]} -eq 0 ]]; then - _fatal "No ADB device found. Connect via USB or pair wireless ADB first." - fi - - if [[ -n "${requested}" ]]; then - for serial in "${serials[@]}"; do - if [[ "${serial}" == "${requested}" ]]; then - found=1 - break - fi - done - - if [[ "${found}" -ne 1 ]]; then - _fatal "Requested device '${requested}' not found. Connected: ${serials[*]}" - fi - - export ADB_SERIAL="${requested}" - return 0 - fi - - if [[ ${#serials[@]} -eq 1 ]]; then - export ADB_SERIAL="${serials[0]}" - _info "Auto-selected device: ${ADB_SERIAL}" - return 0 - fi - - _fatal "Multiple ADB devices found (${serials[*]}) and no target specified. Use --serial or set ADB_SERIAL." -} - -adb_cmd() { - adb -s "${ADB_SERIAL:?adb_select_device must be called first}" "$@" -} - -adb_verify_root() { - local result="" - - result="$(adb_cmd shell su --mount-master -c "echo ok" 2>/dev/null || true)" - if [[ "${result}" != "ok" ]]; then - _fatal "Root check failed on ${ADB_SERIAL}. Ensure Magisk is installed and ADB root is authorized." - fi - - _info "Root verified on ${ADB_SERIAL}" -} - -adb_root_shell() { - local command_text="$*" - - printf '%s\n' "${command_text}" | adb_cmd shell su --mount-master -c "sh -s" -} - -_sanitize_device_string() { - printf '%s' "$1" | tr -cd 'A-Za-z0-9 ._:/-' -} - -adb_collect_identity() { - local raw_fingerprint="" - local raw_model="" - - raw_model="$(adb_cmd shell getprop ro.product.model 2>/dev/null | tr -d '\r')" - raw_fingerprint="$(adb_cmd shell getprop ro.build.fingerprint 2>/dev/null | tr -d '\r')" - - DEVICE_MODEL="$(_sanitize_device_string "${raw_model}")" - DEVICE_FINGERPRINT="$(_sanitize_device_string "${raw_fingerprint}")" - DEVICE_SERIAL="$(_sanitize_device_string "${ADB_SERIAL}")" - export DEVICE_MODEL DEVICE_FINGERPRINT DEVICE_SERIAL -} - -# The enrolled-device record. Sourced from this file's own directory so every -# existing consumer keeps getting the whole API from one source line. -# shellcheck source=adb_trusted.sh -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/adb_trusted.sh" - -# Run lock and cooldown helpers. Sourced last, from this file's own directory, -# so every existing consumer keeps getting the whole API from one source line. -# shellcheck source=adb_locking.sh -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/adb_locking.sh" diff --git a/phone_focus_mode/lib/adb_locking.sh b/phone_focus_mode/lib/adb_locking.sh deleted file mode 100755 index ccbce6e0..00000000 --- a/phone_focus_mode/lib/adb_locking.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# lib/adb_locking.sh — single-instance run lock and per-marker cooldowns. -# Sourced by adb_common.sh; do not execute directly, and do not source it -# on its own: it reads LOCK_DIR, LAST_RUN_DIR and LOCK_FILE, and calls the -# _info/_warn/_fatal helpers, all of which adb_common.sh defines first. - -_adb_release_lock() { - if [[ -n "${LOCK_FILE}" && -d "${LOCK_FILE}" ]]; then - rm -rf "${LOCK_FILE}" - fi -} - -adb_acquire_lock() { - local lock_pid_file="" - local old_pid="" - - mkdir -p "${LOCK_DIR}" - LOCK_FILE="${LOCK_DIR}/run_phone.lock" - lock_pid_file="${LOCK_FILE}/pid" - - if mkdir "${LOCK_FILE}" 2>/dev/null; then - printf '%s\n' "$$" >"${lock_pid_file}" - trap _adb_release_lock EXIT INT TERM - _info "Acquired run lock (PID $$)" - return 0 - fi - - old_pid="$(cat "${lock_pid_file}" 2>/dev/null || printf '')" - if [[ -n "${old_pid}" ]] && kill -0 "${old_pid}" 2>/dev/null; then - _fatal "Another run_phone.sh instance is already running (PID ${old_pid}). Aborting." - fi - - _warn "Stale lock directory found (PID ${old_pid} no longer running). Removing." - rm -rf "${LOCK_FILE}" - - if ! mkdir "${LOCK_FILE}" 2>/dev/null; then - _fatal "Could not acquire run lock at ${LOCK_FILE}. Another process may have raced us." - fi - - printf '%s\n' "$$" >"${lock_pid_file}" - trap _adb_release_lock EXIT INT TERM - _info "Acquired run lock (PID $$)" -} - -adb_check_cooldown() { - local cooldown_secs="${1:-300}" - local elapsed=0 - local last_run=0 - local marker_name="${2:-default}" - local marker="${LAST_RUN_DIR}/${marker_name}" - local now=0 - - mkdir -p "${LAST_RUN_DIR}" - if [[ -f "${marker}" ]]; then - last_run="$(cat "${marker}")" - if [[ "${last_run}" =~ ^[0-9]+$ ]]; then - now="$(date +%s)" - elapsed=$((now - last_run)) - if ((elapsed < cooldown_secs)); then - _info "Cooldown active: last run ${elapsed}s ago, cooldown is ${cooldown_secs}s. Skipping." - return 1 - fi - else - _warn "Ignoring invalid cooldown marker: ${marker}" - fi - fi - - return 0 -} - -adb_mark_last_run() { - local marker_name="${1:-default}" - - mkdir -p "${LAST_RUN_DIR}" - date +%s >"${LAST_RUN_DIR}/${marker_name}" -} diff --git a/phone_focus_mode/lib/adb_trusted.sh b/phone_focus_mode/lib/adb_trusted.sh deleted file mode 100755 index a646c0cd..00000000 --- a/phone_focus_mode/lib/adb_trusted.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -# lib/adb_trusted.sh — the enrolled-device record: saving, listing, forgetting, -# and refusing to act on a phone that is not enrolled. -# Sourced by adb_common.sh; do not execute directly, and do not source it on -# its own: it uses TRUSTED_DEVICE_DIR/TRUSTED_DEVICE_FILE, the DEVICE_* vars and -# the _info/_warn/_error/_fatal helpers that adb_common.sh defines first. - -ADB_ENROLLED_DEVICE_FILE="" - -# Path of the per-device record for a serial. Serials are sanitised to -# [A-Za-z0-9._-] by _sanitize_device_string, so this cannot escape the dir. -_trusted_device_path() { - printf '%s/%s.sh' "${TRUSTED_DEVICE_DIR}" "$1" -} - -adb_save_trusted_device() { - local record="" - - adb_collect_identity - mkdir -p "${TRUSTED_DEVICE_DIR}" - chmod 700 "${TRUSTED_DEVICE_DIR}" - record="$(_trusted_device_path "${DEVICE_SERIAL}")" - - { - printf '# Auto-generated trusted device record — do not edit manually.\n' - printf 'TRUSTED_SERIAL=%q\n' "${DEVICE_SERIAL}" - printf 'TRUSTED_MODEL=%q\n' "${DEVICE_MODEL}" - printf 'TRUSTED_FINGERPRINT=%q\n' "${DEVICE_FINGERPRINT}" - } >"${record}" - - chmod 600 "${record}" - ADB_ENROLLED_DEVICE_FILE="${record}" - export ADB_ENROLLED_DEVICE_FILE - _info "Enrolled device: model='${DEVICE_MODEL}' serial='${DEVICE_SERIAL}'" -} - -# Serials with a per-device record, newest first is not meaningful here so -# the order is whatever the glob yields. -adb_list_trusted_serials() { - local record="" - [[ -d "${TRUSTED_DEVICE_DIR}" ]] || return 0 - for record in "${TRUSTED_DEVICE_DIR}"/*.sh; do - [[ -e "${record}" ]] || continue - basename "${record}" .sh - done -} - -adb_forget_trusted_device() { - local serial="${1:?serial required}" - local record="" - record="$(_trusted_device_path "$(_sanitize_device_string "${serial}")")" - if [[ -f "${record}" ]]; then - rm -f "${record}" - _info "Removed trusted device record for '${serial}'" - else - _warn "No trusted device record for '${serial}'" - fi -} - -# Compares the connected device against one record. Returns 0 on a match, -# 1 when the record is for a different device, and 2 when the record names -# this serial but its model or fingerprint has changed -- which is the case -# worth shouting about, since a serial is the easiest field to spoof and a -# changed fingerprint means the device was reflashed. -_matches_trusted_record() { - local record="${1:?record required}" - local saved_serial="" saved_model="" saved_fingerprint="" - - # Passed explicitly rather than shadowing the global, so a per-record value - # cannot change which file later callers read. - if ! _load_trusted_device_values "${record}"; then - _fatal "Trusted device record exists but could not be read: ${record}" - fi - - saved_serial="${TRUSTED_SERIAL_LOADED:-}" - saved_model="${TRUSTED_MODEL_LOADED:-}" - saved_fingerprint="${TRUSTED_FINGERPRINT_LOADED:-}" - - [[ -n "${saved_serial}" && "${DEVICE_SERIAL}" != "${saved_serial}" ]] && return 1 - - if [[ -n "${saved_model}" && "${DEVICE_MODEL}" != "${saved_model}" ]]; then - _error "Device '${DEVICE_SERIAL}' is enrolled but its model changed: expected '${saved_model}', got '${DEVICE_MODEL}'." - return 2 - fi - - if [[ -n "${saved_fingerprint}" && "${DEVICE_FINGERPRINT}" != "${saved_fingerprint}" ]]; then - _error "Device '${DEVICE_SERIAL}' is enrolled but its build fingerprint changed." - _error " expected: ${saved_fingerprint}" - _error " got: ${DEVICE_FINGERPRINT}" - return 2 - fi - - return 0 -} - -# Fails closed: an unknown device aborts. Any ENROLLED device is accepted, -# so several phones can be managed from one PC without re-enrolling on -# every switch. Only a completely empty store is permissive, and that is -# the first-run case where there is nothing yet to contradict. -adb_verify_trusted_identity() { - local record="" rc=0 - local -a records=() - - if [[ -d "${TRUSTED_DEVICE_DIR}" ]]; then - for record in "${TRUSTED_DEVICE_DIR}"/*.sh; do - [[ -e "${record}" ]] && records+=("${record}") - done - fi - # Legacy single-device record stays authoritative until it is migrated, - # so an existing install keeps its guarantee without any manual step. - [[ -f "${_PHONE_STATE_DIR}/trusted_device.sh" ]] && - records+=("${_PHONE_STATE_DIR}/trusted_device.sh") - - if [[ ${#records[@]} -eq 0 ]]; then - _warn "No trusted device record found. Run 'fresh-phone' to enroll this device." - return 0 - fi - - adb_collect_identity - - for record in "${records[@]}"; do - _matches_trusted_record "${record}" && { - _info "Device identity verified: ${DEVICE_SERIAL} (${DEVICE_MODEL})" - return 0 - } - rc=$? - # A tampered/reflashed match must abort rather than fall through to - # the next record, which would silently downgrade the check. - [[ "${rc}" -eq 2 ]] && _fatal "Refusing to proceed automatically." - done - - _error "Device '${DEVICE_SERIAL}' (${DEVICE_MODEL}) is not enrolled." - _error "Enrolled: $(adb_list_trusted_serials | tr '\n' ' ')" - _fatal "Refusing to proceed automatically. Enroll it first if this is expected." -} diff --git a/phone_focus_mode/lib/backup.sh b/phone_focus_mode/lib/backup.sh deleted file mode 100755 index df084861..00000000 --- a/phone_focus_mode/lib/backup.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env bash -# lib/backup.sh — Incremental backup of APKs, app data, media, and security state. -# Requires: adb_common.sh and backup_manifest.sh sourced, ADB_SERIAL set. -set -euo pipefail - -readonly _BACKUP_REMOTE_DIR="/data/local/tmp/focus_mode" - -_backup_validate_package_name() { - local package_name="$1" - - [[ "${package_name}" =~ ^[A-Za-z0-9._-]+$ ]] || _fatal "Unsafe package name rejected: ${package_name}" -} - -backup_make_snapshot_dir() { - local device_id="$1" - local timestamp="" - local snapshot_dir="" - - timestamp="$(date -u +%Y%m%dT%H%M%SZ)" - snapshot_dir="${PHONE_BACKUP_ROOT}/${device_id}/history/${timestamp}" - mkdir -p \ - "${snapshot_dir}/device_info" \ - "${snapshot_dir}/security_state" \ - "${snapshot_dir}/apks" \ - "${snapshot_dir}/app_data" \ - "${snapshot_dir}/media" \ - "${snapshot_dir}/monitoring" - printf '%s' "${snapshot_dir}" -} - -_backup_update_latest() { - local snapshot_dir="$1" - local device_root="$2" - local latest_dir="${device_root}/latest" - - rm -rf "${latest_dir}" - if ln -s "${snapshot_dir}" "${latest_dir}" 2>/dev/null; then - return 0 - fi - - mkdir -p "${latest_dir}" - cp -a "${snapshot_dir}/." "${latest_dir}/" -} - -backup_device_info() { - local output_dir="$1/device_info" - - _info "Backing up device info → ${output_dir}" - adb_cmd shell getprop >"${output_dir}/getprop.txt" 2>/dev/null || true - adb_cmd shell pm list packages -f >"${output_dir}/packages_full.txt" 2>/dev/null || true - adb_cmd shell pm list packages >"${output_dir}/packages.txt" 2>/dev/null || true - adb_cmd shell df >"${output_dir}/df.txt" 2>/dev/null || true - printf '%s\n' "${ADB_SERIAL}" >"${output_dir}/serial.txt" - adb_cmd shell getprop ro.product.model >"${output_dir}/model.txt" 2>/dev/null || true - adb_cmd shell getprop ro.build.fingerprint >"${output_dir}/fingerprint.txt" 2>/dev/null || true -} - -backup_security_state() { - local output_dir="$1/security_state" - local source_path="" - local destination_dir="" - - _info "Backing up security state → ${output_dir}" - - for source_path in "${SECURITY_STATE_FILES[@]}"; do - destination_dir="${output_dir}$(dirname "${source_path}")" - mkdir -p "${destination_dir}" - adb_root_shell "cat '${source_path}'" >"${destination_dir}/$(basename "${source_path}")" 2>/dev/null || \ - _warn "Could not back up ${source_path} (may not exist yet)" - done - - adb_root_shell "sh '${_BACKUP_REMOTE_DIR}/focus_ctl.sh' status" >"${output_dir}/focus_status.txt" 2>/dev/null || true - adb_root_shell "sh '${_BACKUP_REMOTE_DIR}/focus_ctl.sh' hosts-status" >"${output_dir}/hosts_status.txt" 2>/dev/null || true - adb_root_shell "sh '${_BACKUP_REMOTE_DIR}/focus_ctl.sh' dns-status" >"${output_dir}/dns_status.txt" 2>/dev/null || true - adb_root_shell "sh '${_BACKUP_REMOTE_DIR}/focus_ctl.sh' launcher-status" >"${output_dir}/launcher_status.txt" 2>/dev/null || true - adb_root_shell "sh '${_BACKUP_REMOTE_DIR}/focus_ctl.sh' notif-status" >"${output_dir}/notif_status.txt" 2>/dev/null || true - adb_root_shell "settings get global private_dns_mode" >"${output_dir}/private_dns_mode.txt" 2>/dev/null || true -} - -backup_apks() { - local output_dir="$1/apks" - local entry="" - local package_name="" - local restore_policy="" - local apk_path="" - local destination_dir="" - - _info "Backing up APKs → ${output_dir}" - - for entry in "${APK_ITEMS[@]}"; do - package_name="${entry%%|*}" - restore_policy="$(printf '%s' "${entry}" | cut -d'|' -f2)" - _backup_validate_package_name "${package_name}" - - apk_path="$(adb_cmd shell "pm path ${package_name} | head -1 | sed 's/^package://'" 2>/dev/null | tr -d '\r' || true)" - if [[ -z "${apk_path}" ]]; then - _warn "APK not found on device: ${package_name}" - continue - fi - - destination_dir="${output_dir}/${package_name}" - mkdir -p "${destination_dir}" - if adb_cmd pull "${apk_path}" "${destination_dir}/base.apk" >/dev/null 2>&1; then - _info " Backed up ${package_name} (policy: ${restore_policy})" - else - _warn " Failed to pull APK for ${package_name}" - fi - printf '%s\n' "${restore_policy}" >"${destination_dir}/restore_policy.txt" - if [[ "${restore_policy}" == "manual_only" ]]; then - printf '%s\n' 'NOTE: manual_only — restore requires explicit operator action' >>"${destination_dir}/restore_policy.txt" - elif [[ "${restore_policy}" == "backup_only" ]]; then - printf '%s\n' 'NOTE: backup_only — backed up for safekeeping; automated restore is not implemented' >>"${destination_dir}/restore_policy.txt" - fi - done -} - -backup_app_data() { - local output_dir="$1/app_data" - local entry="" - local package_name="" - local data_path="" - local restore_policy="" - local destination_dir="" - local parent_dir="" - local base_dir="" - local tar_command="" - - _info "Backing up app data → ${output_dir}" - - for entry in "${APP_DATA_ITEMS[@]}"; do - package_name="${entry%%|*}" - data_path="$(printf '%s' "${entry}" | cut -d'|' -f2)" - restore_policy="$(printf '%s' "${entry}" | cut -d'|' -f3)" - _backup_validate_package_name "${package_name}" - - destination_dir="${output_dir}/${package_name}" - mkdir -p "${destination_dir}" - - if ! adb_root_shell "test -d '${data_path}'" >/dev/null 2>&1; then - _warn "App data path not found: ${data_path} for ${package_name}" - continue - fi - - parent_dir="$(dirname "${data_path}")" - base_dir="$(basename "${data_path}")" - tar_command="tar -czf - -C '${parent_dir}' '${base_dir}'" - if adb_root_shell "${tar_command}" >"${destination_dir}/data.tar.gz" 2>/dev/null; then - _info " Backed up app data for ${package_name} (policy: ${restore_policy})" - else - _warn " Failed to back up app data for ${package_name}" - fi - - printf '%s\n' "${restore_policy}" >"${destination_dir}/restore_policy.txt" - if [[ "${restore_policy}" == "manual_only" ]]; then - printf '%s\n' 'NOTE: manual_only — restore requires explicit operator action' >>"${destination_dir}/restore_policy.txt" - elif [[ "${restore_policy}" == "backup_only" ]]; then - printf '%s\n' 'NOTE: backup_only — backed up for safekeeping; automated restore is not implemented' >>"${destination_dir}/restore_policy.txt" - fi - done -} - -backup_media() { - local output_dir="$1/media" - local entry="" - local name="" - local on_device_path="" - local restore_policy="" - local destination_dir="" - - _info "Backing up media → ${output_dir}" - - for entry in "${MEDIA_ITEMS[@]}"; do - name="${entry%%|*}" - on_device_path="$(printf '%s' "${entry}" | cut -d'|' -f2)" - restore_policy="$(printf '%s' "${entry}" | cut -d'|' -f3)" - destination_dir="${output_dir}/${name}" - mkdir -p "${destination_dir}" - - if adb_cmd pull "${on_device_path}" "${destination_dir}/" >/dev/null 2>&1; then - _info " Backed up media/${name} from ${on_device_path} (policy: ${restore_policy})" - else - _warn " Could not pull media/${name} from ${on_device_path}" - fi - printf '%s\n' "${restore_policy}" >"${destination_dir}/restore_policy.txt" - if [[ "${restore_policy}" == "manual_only" ]]; then - printf '%s\n' 'NOTE: manual_only — restore requires explicit operator action' >>"${destination_dir}/restore_policy.txt" - elif [[ "${restore_policy}" == "backup_only" ]]; then - printf '%s\n' 'NOTE: backup_only — backed up for safekeeping; automated restore is not implemented' >>"${destination_dir}/restore_policy.txt" - fi - done -} - -backup_prune_history() { - local device_root="$1" - local history_dir="${device_root}/history" - - [[ -d "${history_dir}" ]] || return 0 - _info "Pruning history older than ${HISTORY_KEEP_DAYS} days in ${history_dir}" - find "${history_dir}" -maxdepth 1 -mindepth 1 -type d -mtime "+${HISTORY_KEEP_DAYS}" -print -exec rm -rf '{}' + || true -} - -backup_run_incremental() { - local device_id="$1" - local device_root="${PHONE_BACKUP_ROOT}/${device_id}" - local snapshot_dir="" - - snapshot_dir="$(backup_make_snapshot_dir "${device_id}")" - _info "Starting incremental backup to ${snapshot_dir}" - - backup_device_info "${snapshot_dir}" - backup_security_state "${snapshot_dir}" - backup_apks "${snapshot_dir}" - backup_app_data "${snapshot_dir}" - backup_media "${snapshot_dir}" - _backup_update_latest "${snapshot_dir}" "${device_root}" - backup_prune_history "${device_root}" - - _info "Backup complete: ${snapshot_dir}" - printf '%s' "${snapshot_dir}" -} diff --git a/phone_focus_mode/lib/backup_capture.sh b/phone_focus_mode/lib/backup_capture.sh deleted file mode 100755 index a40ec497..00000000 --- a/phone_focus_mode/lib/backup_capture.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash -# lib/backup_capture.sh — the capture half of phone_backup.sh: package lists, -# third-party APKs, shared-storage directories, settings dumps and the -# focus-mode state. Each function is handed the output directory to fill. -# -# Sourced by phone_backup.sh, which owns adb_common.sh (adb_cmd, adb_shell, -# _info, _warn). SDCARD_DIRS lives here rather than in the entry script -# because pull_sdcard is its only reader, and a file must not assign a global -# it never reads (SC2034) — shellcheck runs without -x, so each file stands -# alone. - -# Directories pulled wholesale from shared storage. Anything not -# listed here is not backed up, so keep the list honest. -# Written on one line rather than one entry per line: kcov instruments a -# multi-line literal's continuation lines but bash only ever reports the -# statement at its first line, so a list spanning N lines is permanently -# stuck at 1/N covered however thoroughly it is tested. -readonly SDCARD_DIRS=(DCIM Pictures Download Documents Movies Music Signal RunnerUp) - -dump_packages() { - local dir="$1" - adb_cmd shell pm list packages | sed 's/^package://' | tr -d '\r' | - sort >"${dir}/packages-all.txt" - adb_cmd shell pm list packages -3 | sed 's/^package://' | tr -d '\r' | - sort >"${dir}/packages-third-party.txt" - # Packages removed for user 0 read as uninstalled here; recorded - # separately so a restore knows what was deliberately purged. - adb_cmd shell pm list packages -u | sed 's/^package://' | tr -d '\r' | - sort >"${dir}/packages-including-removed.txt" - comm -13 "${dir}/packages-all.txt" "${dir}/packages-including-removed.txt" \ - >"${dir}/packages-removed-for-user0.txt" - _info "Packages: $(wc -l <"${dir}/packages-third-party.txt") third-party, $(wc -l <"${dir}/packages-removed-for-user0.txt") removed for user 0" -} - -pull_apks() { - local dir="$1" pkg="" path="" n=0 base="" - local -a pkgs=() paths=() - mkdir -p "${dir}/apks" - - # Read the list into an array FIRST. Iterating the file with - # `while read < file` breaks after one package, because `adb` - # inside the loop inherits and drains that same stdin -- measured: - # it pulled 1 of 54 before this was fixed. - mapfile -t pkgs <"${dir}/packages-third-party.txt" - - for pkg in "${pkgs[@]}"; do - [[ -n "${pkg}" ]] || continue - # A split app reports several paths (base + split_config.*). - # All of them are needed for a working offline reinstall, so - # each package gets its own directory rather than one file. - mapfile -t paths < <(adb_cmd shell pm path "${pkg}" 2>/dev/null | sed 's/^package://' | tr -d '\r') - [[ "${#paths[@]}" -gt 0 ]] || { - _warn "no APK path for ${pkg}" - continue - } - mkdir -p "${dir}/apks/${pkg}" - for path in "${paths[@]}"; do - [[ -n "${path}" ]] || continue - base="$(basename "${path}")" - adb_cmd pull "${path}" "${dir}/apks/${pkg}/${base}" >/dev/null 2>&1 || - _warn "could not pull ${base} for ${pkg}" - done - n=$((n + 1)) - done - _info "APKs pulled: ${n} packages, $(find "${dir}/apks" -name '*.apk' | wc -l) files" -} - -pull_sdcard() { - local dir="$1" d="" - mkdir -p "${dir}/sdcard" - for d in "${SDCARD_DIRS[@]}"; do - if adb_cmd shell "test -d /sdcard/${d}" 2>/dev/null; then - _info "Pulling /sdcard/${d}" - adb_cmd pull "/sdcard/${d}" "${dir}/sdcard/" >/dev/null 2>&1 || - _warn "partial or failed pull: ${d}" - fi - done - # Same stdin caveat as pull_apks: collect first, pull second. - local -a kdbx_paths=() - mapfile -t kdbx_paths < <(adb_cmd shell 'find /sdcard -iname "*.kdbx" 2>/dev/null' | tr -d '\r') - local kdbx="" - for kdbx in "${kdbx_paths[@]}"; do - [[ -n "${kdbx}" ]] || continue - _info "Pulling KeePass database: ${kdbx}" - mkdir -p "${dir}/sdcard/keepass" - adb_cmd pull "${kdbx}" "${dir}/sdcard/keepass/" >/dev/null 2>&1 || true - done -} - -dump_settings() { - local dir="$1" ns="" - for ns in system secure global; do - adb_cmd shell settings list "${ns}" | tr -d '\r' | sort \ - >"${dir}/settings-${ns}.txt" - done - _info "Settings captured (system/secure/global)" -} - -dump_focus_state() { - local dir="$1" - local out="${dir}/focus-state.md" rethink="no" purged="" - - # Built as one printf rather than a `{ ... } >file` group of many: kcov - # counts the group's closing brace as an instrumented line that bash never - # reports, and a run of individual redirects instead draws SC2129. One - # statement satisfies both, and the repo forbids suppressing either. - grep -qx 'com.celzero.bravedns' "${dir}/packages-all.txt" && rethink="yes" - purged="$(cat "${dir}/packages-removed-for-user0.txt")" - - printf '%s\n%s\n\n%s\n%s\n\n%s%s\n%s\n%s\n' \ - '# Focus-mode state at backup time' \ - '# Restore with: phone_focus_mode/distraction_purge.sh' \ - '## Packages removed for user 0' \ - "${purged}" \ - '## RethinkDNS installed: ' "${rethink}" \ - '## uBlock filters live in the Firefox profile and CANNOT be' \ - '## backed up from adb; see docs/youtube-block-unrooted.md' \ - >"${out}" -} diff --git a/phone_focus_mode/lib/monitor.sh b/phone_focus_mode/lib/monitor.sh deleted file mode 100755 index 8b9d7675..00000000 --- a/phone_focus_mode/lib/monitor.sh +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env bash -# lib/monitor.sh — Security and health monitoring for the managed phone. -# Requires: adb_common.sh sourced, ADB_SERIAL set, backup_manifest.sh sourced. -set -euo pipefail - -# Directory of this library, used to locate sibling helper scripts (BASH_SOURCE -# resolves to monitor.sh even when sourced, so the path is stable). -_MONITOR_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly _MONITOR_LIB_DIR - -# Constants read here or by both probe libraries. Those read by exactly one of -# them live in that file instead: a file must not assign a global it never -# reads (SC2034), and shellcheck runs without -x so each file stands alone. -readonly _MONITOR_REMOTE_DIR="/data/local/tmp/focus_mode" -readonly _MONITOR_HOSTS_TARGET="/system/etc/hosts" -readonly _MONITOR_LAUNCHER_PACKAGE="com.qqlabs.minimalistlauncher" -readonly _MONITOR_LAUNCHER_ACTIVITY_FILE="/data/local/tmp/focus_mode/minimalist_launcher.activity" -readonly _MONITOR_HOSTS_CANDIDATES="/system/etc/hosts /etc/hosts /vendor/etc/hosts /system/system/etc/hosts" - -# Sourced after the constants above, which the probes read. Both libraries call -# helpers defined below (_mon_check, _trim_output, _safe_adb_root_output); that -# is fine because nothing here runs until monitor_collect_snapshot is called. -# shellcheck source=monitor_checks_health.sh -. "${_MONITOR_LIB_DIR}/monitor_checks_health.sh" -# shellcheck source=monitor_checks_policy.sh -. "${_MONITOR_LIB_DIR}/monitor_checks_policy.sh" - -_mon_escape_json() { - local escaped="$1" - - escaped=${escaped//\\/\\\\} - # The quote must be backslash-escaped in the pattern. Written bare as - # ${escaped//"/\\"} bash reads the quote as opening a quoted string, so the - # pattern silently becomes `/\` with an empty replacement and no quote is - # ever escaped — which emitted invalid JSON for any message containing one. - escaped=${escaped//\"/\\\"} - escaped=${escaped//$'\n'/\\n} - escaped=${escaped//$'\r'/\\r} - printf '%s' "${escaped}" -} - -_mon_check() { - local check_name="$1" - local status="$2" - local source_cmd="$3" - local message="$4" - local repairable="${5:-false}" - - printf '{"check":"%s","status":"%s","source":"%s","message":"%s","repairable":%s}\n' \ - "$(_mon_escape_json "${check_name}")" \ - "$(_mon_escape_json "${status}")" \ - "$(_mon_escape_json "${source_cmd}")" \ - "$(_mon_escape_json "${message}")" \ - "${repairable}" -} - -_safe_adb_root_output() { - adb_root_shell "$@" 2>/dev/null || true -} - -_trim_output() { - printf '%s' "$1" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' -} - -_monitor_read_pidfile() { - local pidfile="$1" - local pid="" - - pid="$(_trim_output "$(_safe_adb_root_output "if [ -f ${pidfile} ]; then cat ${pidfile}; fi")")" - printf '%s' "${pid}" -} - -_monitor_resolve_hosts_target() { - local candidate="" - - if adb_root_shell "test -f ${_MONITOR_HOSTS_TARGET}" >/dev/null 2>&1; then - printf '%s' "${_MONITOR_HOSTS_TARGET}" - return 0 - fi - - for candidate in ${_MONITOR_HOSTS_CANDIDATES}; do - if adb_root_shell "test -f ${candidate}" >/dev/null 2>&1; then - printf '%s' "${candidate}" - return 0 - fi - done - - printf '' -} - -_monitor_pid_matches_script() { - local pid="$1" - local script_name="$2" - - adb_root_shell "kill -0 ${pid} >/dev/null 2>&1" >/dev/null 2>&1 || return 1 - - adb_root_shell "tr '\\0' ' ' /dev/null 2>&1 && return 0 - - # Some Android shells hide/normalize cmdline under su; if PID is alive, - # trust the pidfile check to avoid false negatives. - return 0 -} - -monitor_check_format_indicators() { - local indicator="" - local description="" - local command_text="" - - for indicator in "${FORMAT_INDICATORS[@]}"; do - description="${indicator%%|*}" - command_text="${indicator#*|}" - if ! adb_root_shell "${command_text}" >/dev/null 2>&1; then - printf '%s\n' "${description}" - fi - done -} - -monitor_count_missing_format_indicators() { - local -a missing_indicators=() - - mapfile -t missing_indicators < <(monitor_check_format_indicators) - printf '%s\n' "${#missing_indicators[@]}" -} - -monitor_is_formatted() { - local missing_count="" - - missing_count="$(monitor_count_missing_format_indicators)" - [[ "${missing_count}" =~ ^[0-9]+$ ]] || _fatal "Format-detection helper returned a non-numeric count: ${missing_count}" - (( missing_count >= FORMAT_DETECTION_MIN_MISSING )) -} - -# Each array element is appended on its own statement rather than listed -# across a multi-line literal: kcov instruments the continuation lines but -# bash only ever reports the statement's first line, so a literal spanning N -# lines is permanently stuck at 1/N covered however thoroughly it is tested. -monitor_print_format_warning() { - local -a missing_indicators=("$@") - local -a box_lines=() - local indicator="" - - box_lines+=("") - box_lines+=("The following expected components were NOT found:") - - for indicator in "${missing_indicators[@]}"; do - box_lines+=(" ✗ ${indicator}") - done - - box_lines+=("") - box_lines+=("This strongly suggests the phone was factory-reset or formatted.") - box_lines+=("") - box_lines+=("Next step: run the full recovery workflow:") - box_lines+=(" ./scripts/run_all/run_phone.sh fresh-phone") - box_lines+=("") - box_lines+=("Do NOT run 'auto' mode — it will not restore anything.") - - _box "PHONE APPEARS TO HAVE BEEN WIPED" "${box_lines[@]}" -} - - - - - -monitor_collect_snapshot() { - local snapshot_dir="$1" - local tmp_checks="" - local report_path="${snapshot_dir}/report.json" - - mkdir -p "${snapshot_dir}" - tmp_checks="$(mktemp)" - - _check_format_indicators "${tmp_checks}" - _check_battery "${tmp_checks}" - _check_storage "${tmp_checks}" - _check_daemon "focus_daemon" "focus_daemon.sh" "${_MONITOR_REMOTE_DIR}/daemon.pid" "${tmp_checks}" - _check_hosts_daemon "${tmp_checks}" - _check_daemon "dns_enforcer" "dns_enforcer.sh" "${_MONITOR_REMOTE_DIR}/dns_enforcer.pid" "${tmp_checks}" - _check_launcher_daemon "${tmp_checks}" - _check_hosts_integrity "${tmp_checks}" - _check_dns "${tmp_checks}" - _check_launcher "${tmp_checks}" - _check_companion_app "${tmp_checks}" - _check_boot_persistence "${tmp_checks}" - - # Redirected per statement rather than as a `{ ... } >file` group: kcov - # counts the group's closing brace as an instrumented line that bash never - # reports, which alone held this file below 100%. - printf '{"timestamp":"%s","device":"%s","checks":[\n' \ - "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(_mon_escape_json "${ADB_SERIAL}")" >"${report_path}" - paste -sd ',' "${tmp_checks}" >>"${report_path}" - printf '\n]}\n' >>"${report_path}" - - cp "${report_path}" "$(dirname "${snapshot_dir}")/latest.json" 2>/dev/null || true - rm -f "${tmp_checks}" -} - -monitor_print_summary() { - local snapshot_dir="$1" - local report_path="${snapshot_dir}/report.json" - - [[ -f "${report_path}" ]] || { - _warn "No report found at ${report_path}" - return 0 - } - - python "${_MONITOR_LIB_DIR}/monitor_report.py" summary "${report_path}" -} - -monitor_severity_exit() { - local snapshot_dir="$1" - local report_path="${snapshot_dir}/report.json" - - [[ -f "${report_path}" ]] || return 0 - - python "${_MONITOR_LIB_DIR}/monitor_report.py" severity "${report_path}" -} diff --git a/phone_focus_mode/lib/monitor_checks_health.sh b/phone_focus_mode/lib/monitor_checks_health.sh deleted file mode 100755 index 9c8ae015..00000000 --- a/phone_focus_mode/lib/monitor_checks_health.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# lib/monitor_checks_health.sh — device-health probes behind monitor.sh: -# format indicators, battery, storage, and the three enforcer daemons. -# Each _check_* appends one JSON object to the checks file it is given. -# -# Sourced by monitor.sh, which defines every _MONITOR_* constant these read -# and the _mon_check/_trim_output/_monitor_read_pidfile helpers they call. -# Nothing is assigned here, so there is no constant to keep in step. - -_check_format_indicators() { - local outfile="$1" - local -a missing_indicators=() - local status="ok" - local message="All format indicators are present" - - mapfile -t missing_indicators < <(monitor_check_format_indicators) - if (( ${#missing_indicators[@]} >= FORMAT_DETECTION_MIN_MISSING )); then - status="fatal" - message="Missing ${#missing_indicators[@]} format indicators: ${missing_indicators[*]}" - elif (( ${#missing_indicators[@]} > 0 )); then - status="warn" - message="Missing ${#missing_indicators[@]} format indicators: ${missing_indicators[*]}" - fi - - _mon_check "format_indicators" "${status}" "FORMAT_INDICATORS" "${message}" "false" >>"${outfile}" -} - -_check_battery() { - local outfile="$1" - local level="" - local health="" - local temp="" - local status="ok" - local message="" - - level="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/level:/{print \$2; exit}'")")" - health="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/health:/{print \$2; exit}'")")" - temp="$(_trim_output "$(_safe_adb_root_output "dumpsys battery | awk -F': ' '/temperature:/{print \$2; exit}'")")" - - if [[ ! "${level}" =~ ^[0-9]+$ ]]; then - status="warn" - message="Battery level unavailable" - elif (( level < BATTERY_WARN_BELOW )); then - status="warn" - message="Battery low: ${level}% (threshold ${BATTERY_WARN_BELOW}%)" - else - message="Battery level ${level}%, health ${health:-unknown}, temp ${temp:-unknown}" - fi - - _mon_check "battery" "${status}" "dumpsys battery" "${message}" "false" >>"${outfile}" -} - -_check_storage() { - local outfile="$1" - local free_kb="" - local free_mb=0 - local status="ok" - local message="" - - free_kb="$(_trim_output "$(_safe_adb_root_output "df /sdcard 2>/dev/null | awk 'NR==2{print \$4; exit}'")")" - if [[ ! "${free_kb}" =~ ^[0-9]+$ ]]; then - free_kb="$(_trim_output "$(_safe_adb_root_output "df /storage/emulated/0 2>/dev/null | awk 'NR==2{print \$4; exit}'")")" - fi - - if [[ "${free_kb}" =~ ^[0-9]+$ ]]; then - free_mb=$((free_kb / 1024)) - if (( free_mb < STORAGE_WARN_BELOW_MB )); then - status="warn" - message="Low storage: ${free_mb} MB free (threshold ${STORAGE_WARN_BELOW_MB} MB)" - else - message="Free storage: ${free_mb} MB" - fi - else - status="warn" - message="Free storage unavailable" - fi - - _mon_check "storage" "${status}" "df /sdcard" "${message}" "false" >>"${outfile}" -} - -_check_daemon() { - local daemon_name="$1" - local script_name="$2" - local pidfile="$3" - local outfile="$4" - local pid="" - local pgrep_pid="" - - pid="$(_monitor_read_pidfile "${pidfile}")" - if [[ "${pid}" =~ ^[0-9]+$ ]] && _monitor_pid_matches_script "${pid}" "${script_name}"; then - _mon_check "${daemon_name}" "ok" "${pidfile}" "${daemon_name} running (PID ${pid})" "false" >>"${outfile}" - return - fi - - pgrep_pid="$(_trim_output "$(_safe_adb_root_output "pgrep -f '${script_name}' 2>/dev/null | head -1")")" - if [[ "${pgrep_pid}" =~ ^[0-9]+$ ]] && adb_root_shell "kill -0 ${pgrep_pid} >/dev/null 2>&1" >/dev/null 2>&1; then - _mon_check "${daemon_name}" "ok" "pgrep -f ${script_name}" "${daemon_name} running (PID ${pgrep_pid})" "false" >>"${outfile}" - return - fi - - _mon_check "${daemon_name}" "error" "${pidfile}" "${daemon_name} is NOT running" "true" >>"${outfile}" -} - -_check_hosts_daemon() { - local outfile="$1" - local resolved_target="" - - resolved_target="$(_monitor_resolve_hosts_target)" - if [[ -z "${resolved_target}" ]]; then - _mon_check "hosts_enforcer" "warn" "hosts target probe" \ - "No hosts target file exists on this ROM; hosts daemon check skipped" "false" >>"${outfile}" - return - fi - - _check_daemon "hosts_enforcer" "hosts_enforcer.sh" "${_MONITOR_REMOTE_DIR}/hosts_enforcer.pid" "${outfile}" -} - -_check_launcher_daemon() { - local outfile="$1" - local has_snapshot="no" - local launcher_installed="no" - - if adb_root_shell "test -s '${_MONITOR_LAUNCHER_ACTIVITY_FILE}'" >/dev/null 2>&1; then - has_snapshot="yes" - fi - - if adb_root_shell "pm path '${_MONITOR_LAUNCHER_PACKAGE}' >/dev/null 2>&1" >/dev/null 2>&1; then - launcher_installed="yes" - fi - - if [[ "${has_snapshot}" == "no" && "${launcher_installed}" == "no" ]]; then - _mon_check "launcher_enforcer" "warn" "launcher optional probe" \ - "Launcher enforcer check skipped (launcher not configured yet)" "false" >>"${outfile}" - return - fi - - _check_daemon "launcher_enforcer" "launcher_enforcer.sh" "${_MONITOR_REMOTE_DIR}/launcher_enforcer.pid" "${outfile}" -} diff --git a/phone_focus_mode/lib/monitor_checks_policy.sh b/phone_focus_mode/lib/monitor_checks_policy.sh deleted file mode 100755 index 3bb30e6c..00000000 --- a/phone_focus_mode/lib/monitor_checks_policy.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env bash -# lib/monitor_checks_policy.sh — enforcement-integrity probes behind monitor.sh: -# the hosts blocklist, DNS chain, launcher, companion app and boot persistence. -# Each _check_* appends one JSON object to the checks file it is given. -# -# Sourced by monitor.sh, which defines _MONITOR_LAUNCHER_PACKAGE and -# _MONITOR_LAUNCHER_ACTIVITY_FILE (shared with the health probes) plus the -# _mon_check/_trim_output/_safe_adb_root_output helpers these call. -# The constants below are read only here, so they are assigned only here. - -readonly _MONITOR_HOSTS_CANONICAL="/data/local/tmp/focus_mode/hosts.canonical" -readonly _MONITOR_HOSTS_SHA_FILE="/data/local/tmp/focus_mode/hosts.sha256" -readonly _MONITOR_BOOT_SCRIPT="/data/adb/service.d/99-focus-mode.sh" -readonly _MONITOR_COMPANION_PACKAGE="com.kuhy.focusstatus" -readonly _MONITOR_DNS_CHAIN="FOCUS_DNS_BLOCK" - -_check_hosts_integrity() { - local outfile="$1" - local hosts_target="" - local expected_hash="" - local actual_hash="" - - hosts_target="$(_monitor_resolve_hosts_target)" - if [[ -z "${hosts_target}" ]]; then - _mon_check "hosts_integrity" "warn" "hosts target probe" \ - "No hosts file target exists on this ROM; hosts integrity check skipped" "false" >>"${outfile}" - return - fi - - if ! adb_root_shell "test -f ${_MONITOR_HOSTS_CANONICAL}" >/dev/null 2>&1; then - _mon_check "hosts_integrity" "fatal" "${_MONITOR_HOSTS_CANONICAL}" \ - "Canonical hosts file missing at ${_MONITOR_HOSTS_CANONICAL}" "true" >>"${outfile}" - return - fi - - expected_hash="$(_trim_output "$(_safe_adb_root_output "cat ${_MONITOR_HOSTS_SHA_FILE} 2>/dev/null")")" - actual_hash="$(_trim_output "$(_safe_adb_root_output "sha256sum ${hosts_target} 2>/dev/null | awk '{print \$1}'")")" - - if [[ -z "${expected_hash}" || -z "${actual_hash}" ]]; then - _mon_check "hosts_integrity" "error" "${hosts_target}" \ - "Could not read hosts integrity hashes" "true" >>"${outfile}" - elif [[ "${expected_hash}" == "${actual_hash}" ]]; then - _mon_check "hosts_integrity" "ok" "${hosts_target}" \ - "Hosts file matches canonical (${actual_hash:0:12}…)" "false" >>"${outfile}" - else - _mon_check "hosts_integrity" "error" "${hosts_target}" \ - "Hosts mismatch: active ${actual_hash:0:12}… != expected ${expected_hash:0:12}…" "true" >>"${outfile}" - fi -} - -_check_dns() { - local outfile="$1" - local private_dns_mode="" - local chain_present="no" - local status="ok" - local message="" - - private_dns_mode="$(_trim_output "$(_safe_adb_root_output "settings get global private_dns_mode 2>/dev/null")")" - if adb_root_shell "iptables -L ${_MONITOR_DNS_CHAIN} >/dev/null 2>&1 && ip6tables -L ${_MONITOR_DNS_CHAIN} >/dev/null 2>&1" >/dev/null 2>&1; then - chain_present="yes" - fi - - if [[ "${private_dns_mode}" == "off" || "${private_dns_mode}" == "null" || -z "${private_dns_mode}" ]]; then - if [[ "${chain_present}" == "yes" ]]; then - message="Private DNS disabled and DNS firewall chains present" - else - status="error" - message="Private DNS disabled, but DNS firewall chains are missing" - fi - else - status="error" - message="Private DNS is enabled (mode=${private_dns_mode})" - fi - - _mon_check "dns_enforcement" "${status}" "settings get global private_dns_mode" "${message}" "true" >>"${outfile}" -} - -_check_launcher() { - local outfile="$1" - local desired_activity="" - local actual_activity="" - local has_snapshot="no" - - if adb_root_shell "test -s '${_MONITOR_LAUNCHER_ACTIVITY_FILE}'" >/dev/null 2>&1; then - has_snapshot="yes" - fi - - if ! adb_root_shell "pm path '${_MONITOR_LAUNCHER_PACKAGE}' >/dev/null 2>&1" >/dev/null 2>&1; then - if [[ "${has_snapshot}" == "yes" ]]; then - _mon_check "launcher_state" "fatal" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ - "Minimalist launcher is not installed but snapshot metadata exists" "true" >>"${outfile}" - else - _mon_check "launcher_state" "warn" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ - "Minimalist launcher is not installed (optional until snapshot is configured)" "false" >>"${outfile}" - fi - return - fi - - desired_activity="$(_trim_output "$(_safe_adb_root_output "cat ${_MONITOR_LAUNCHER_ACTIVITY_FILE} 2>/dev/null")")" - actual_activity="$(_trim_output "$(_safe_adb_root_output "cmd package resolve-activity --brief -c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}'")")" - - if [[ -z "${desired_activity}" ]]; then - _mon_check "launcher_state" "warn" "cat ${_MONITOR_LAUNCHER_ACTIVITY_FILE}" \ - "Launcher snapshot metadata is missing or empty" "true" >>"${outfile}" - elif [[ -n "${actual_activity}" && "${desired_activity}" != "${actual_activity}" ]]; then - _mon_check "launcher_state" "error" "cmd package resolve-activity" \ - "Launcher default mismatch: expected ${desired_activity}, got ${actual_activity}" "true" >>"${outfile}" - else - _mon_check "launcher_state" "ok" "pm path ${_MONITOR_LAUNCHER_PACKAGE}" \ - "Minimalist launcher installed and HOME activity aligned" "false" >>"${outfile}" - fi -} - -_check_companion_app() { - local outfile="$1" - - if adb_root_shell "pm list packages -e '${_MONITOR_COMPANION_PACKAGE}' 2>/dev/null | grep -q '${_MONITOR_COMPANION_PACKAGE}'" >/dev/null 2>&1; then - _mon_check "companion_app" "ok" "pm list packages -e ${_MONITOR_COMPANION_PACKAGE}" \ - "Focus companion app is installed" "false" >>"${outfile}" - else - _mon_check "companion_app" "warn" "pm list packages -e ${_MONITOR_COMPANION_PACKAGE}" \ - "Focus companion app is missing" "true" >>"${outfile}" - fi -} - -_check_boot_persistence() { - local outfile="$1" - - if adb_root_shell "test -x ${_MONITOR_BOOT_SCRIPT}" >/dev/null 2>&1; then - _mon_check "boot_persistence" "ok" "test -x ${_MONITOR_BOOT_SCRIPT}" \ - "Magisk boot script present and executable" "false" >>"${outfile}" - else - _mon_check "boot_persistence" "fatal" "test -x ${_MONITOR_BOOT_SCRIPT}" \ - "Magisk boot script missing or not executable" "true" >>"${outfile}" - fi -} diff --git a/phone_focus_mode/lib/monitor_report.py b/phone_focus_mode/lib/monitor_report.py deleted file mode 100755 index 1160db60..00000000 --- a/phone_focus_mode/lib/monitor_report.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -"""Render or grade a phone-monitoring report produced by ``monitor.sh``. - -Two modes, selected by ``argv[1]``: - -* ``summary`` — print a human-readable status summary to stdout (always exit 0). -* ``severity`` — exit 1 if any check is ``fatal``/``error``, else exit 0 (no output). - -``argv[2]`` is the path to the ``report.json`` snapshot. Kept as a standalone -module (not inline ``python < list[object]: - """Return the ``checks`` array from the report, or [] if absent/malformed.""" - data: object = json.loads(report_path.read_text(encoding="utf-8")) - if not isinstance(data, dict): - return [] - checks: object = data.get("checks", []) - return checks if isinstance(checks, list) else [] - - -def _field(check: object, key: str, default: str) -> str: - """Read a string field from a check object, falling back to ``default``.""" - if isinstance(check, dict): - value = check.get(key, default) - if isinstance(value, str): - return value - return default - - -def _render_summary(checks: list[object]) -> str: - """Build the multi-line monitoring summary string for the given checks.""" - counts = dict.fromkeys(_COUNT_KEYS, 0) - issues: list[tuple[str, str, str]] = [] - for check in checks: - status = _field(check, "status", "warn") - counts[status] = counts.get(status, 0) + 1 - if status in _ISSUE_STATUSES: - issues.append( - ( - status, - _field(check, "check", "unknown"), - _field(check, "message", ""), - ), - ) - - lines = [ - "", - "=== Monitoring Summary ===", - f" ok={counts.get('ok', 0):<3} warn={counts.get('warn', 0):<3} " - f"error={counts.get('error', 0):<3} fatal={counts.get('fatal', 0):<3}", - ] - if issues: - lines.append("") - lines.append("Issues found:") - lines.extend( - f" [{status}] {name}: {message}" for status, name, message in issues - ) - lines.append("==========================") - lines.append("") - return "\n".join(lines) + "\n" - - -def _has_severe(checks: list[object]) -> bool: - """Return True if any check has a fatal/error status.""" - return any(_field(check, "status", "warn") in _SEVERE_STATUSES for check in checks) - - -def main() -> int: - """Dispatch on ``argv[1]`` (summary|severity) and ``argv[2]`` (report path).""" - args = sys.argv[1:] - expected_args = 2 - if len(args) != expected_args or args[0] not in {"summary", "severity"}: - sys.stderr.write("usage: monitor_report.py {summary|severity} \n") - return 2 - mode, report_path = args[0], Path(args[1]) - checks = _load_checks(report_path) - if mode == "summary": - sys.stdout.write(_render_summary(checks)) - return 0 - return 1 if _has_severe(checks) else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/phone_focus_mode/lib/restore.sh b/phone_focus_mode/lib/restore.sh deleted file mode 100755 index 43d2ffa6..00000000 --- a/phone_focus_mode/lib/restore.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# lib/restore.sh — Restore security stack, APKs, and media after format. -# Requires: adb_common.sh, backup_manifest.sh sourced, ADB_SERIAL set. -set -euo pipefail - -_RESTORE_PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -readonly _RESTORE_PROJECT_DIR - -_restore_validate_package_name() { - local package_name="$1" - - [[ "${package_name}" =~ ^[A-Za-z0-9._-]+$ ]] || _fatal "Unsafe package name rejected: ${package_name}" -} - -restore_verify_prerequisites() { - local -a problems=() - - if ! adb_cmd get-state >/dev/null 2>&1; then - problems+=("ADB device not reachable. Ensure USB debugging is authorized or wireless ADB is paired.") - fi - - if ! adb_root_shell "echo root_ok" 2>/dev/null | grep -q '^root_ok$'; then - problems+=("Root shell failed. Ensure Magisk is installed and ADB root is authorized in Magisk settings.") - fi - - if ! adb_root_shell "test -d /data/adb && command -v magisk >/dev/null 2>&1" >/dev/null 2>&1; then - problems+=("Magisk runtime not detected. Install Magisk and verify rooted debugging is enabled.") - fi - - if [[ ${#problems[@]} -gt 0 ]]; then - _box "PREREQUISITES NOT MET — CANNOT CONTINUE" \ - "" \ - "Please resolve the following before running fresh-phone:" \ - "${problems[@]/#/ ✗ }" \ - "" \ - "Manual steps for a freshly formatted phone:" \ - " 1. Install Magisk from https://github.com/topjohnwu/Magisk" \ - " 2. In Magisk → Settings → enable 'Rooted Debugging'" \ - " 3. On phone: Settings → Developer options → enable 'USB Debugging'" \ - " 4. Authorize this PC on the phone when prompted" \ - " 5. Pair wireless ADB again if you normally use it" - return 1 - fi - - _info "All restore prerequisites verified" -} - -restore_security_stack() { - local deploy_script="${_RESTORE_PROJECT_DIR}/deploy.sh" - - _info "Restoring security stack via deploy.sh" - [[ -x "${deploy_script}" ]] || _fatal "deploy.sh not found or not executable at ${deploy_script}" - env ADB_SERIAL="${ADB_SERIAL}" PHONE_IP="${PHONE_IP:-}" bash "${deploy_script}" "${PHONE_IP:-}" -} - -restore_apks() { - local backup_dir="$1" - local apk_dir="${backup_dir}/apks" - local entry="" - local package_name="" - local restore_policy="" - local apk_path="" - - [[ -d "${apk_dir}" ]] || { - _warn "No APK backup found at ${apk_dir}" - return 0 - } - - _info "Restoring APKs from ${apk_dir}" - for entry in "${APK_ITEMS[@]}"; do - package_name="${entry%%|*}" - restore_policy="$(printf '%s' "${entry}" | cut -d'|' -f2)" - _restore_validate_package_name "${package_name}" - - if [[ "${restore_policy}" != "safe_restore" ]]; then - _info " Skipping ${package_name} (policy: ${restore_policy})" - continue - fi - - apk_path="${apk_dir}/${package_name}/base.apk" - if [[ ! -f "${apk_path}" ]]; then - _warn " APK not found in backup: ${package_name}" - continue - fi - - if adb_cmd install -r "${apk_path}" >/dev/null 2>&1; then - _info " Installed ${package_name}" - else - _warn " Failed to install ${package_name}" - fi - done -} - -restore_media() { - local backup_dir="$1" - local media_dir="${backup_dir}/media" - local entry="" - local name="" - local on_device_path="" - local restore_policy="" - local source_dir="" - - [[ -d "${media_dir}" ]] || { - _warn "No media backup found at ${media_dir}" - return 0 - } - - _info "Restoring media from ${media_dir}" - for entry in "${MEDIA_ITEMS[@]}"; do - name="${entry%%|*}" - on_device_path="$(printf '%s' "${entry}" | cut -d'|' -f2)" - restore_policy="$(printf '%s' "${entry}" | cut -d'|' -f3)" - - if [[ "${restore_policy}" != "safe_restore" ]]; then - _info " Skipping media/${name} (policy: ${restore_policy})" - continue - fi - - source_dir="${media_dir}/${name}" - if [[ ! -d "${source_dir}" ]]; then - _warn " Media backup not found: ${source_dir}" - continue - fi - - if adb_cmd push "${source_dir}/." "${on_device_path}/" >/dev/null 2>&1; then - _info " Restored media/${name}" - else - _warn " Failed to restore media/${name}" - fi - done -} - -restore_print_manual_steps() { - local backup_dir="$1" - local -a steps=() - local entry="" - local package_name="" - local data_path="" - local restore_policy="" - - for entry in "${APP_DATA_ITEMS[@]}"; do - package_name="${entry%%|*}" - data_path="$(printf '%s' "${entry}" | cut -d'|' -f2)" - restore_policy="$(printf '%s' "${entry}" | cut -d'|' -f3)" - _restore_validate_package_name "${package_name}" - - if [[ "${restore_policy}" == "manual_only" ]]; then - steps+=("App data for ${package_name}: backup at ${backup_dir}/app_data/${package_name}/data.tar.gz (source ${data_path})") - fi - done - - steps+=("Re-enter coordinates in phone_focus_mode/config_secrets.sh if needed") - steps+=("Re-authorize wireless ADB pairing if you use it") - steps+=("Verify Magisk modules are re-installed if any were previously in use") - - if [[ ${#steps[@]} -gt 0 ]]; then - _box "MANUAL FOLLOW-UP STEPS REQUIRED" \ - "" \ - "The automated restore is complete. You must handle the following manually:" \ - "${steps[@]/#/ ▶ }" - fi -} diff --git a/phone_focus_mode/lib/tests/adb_test_harness.sh b/phone_focus_mode/lib/tests/adb_test_harness.sh deleted file mode 100755 index 3eb20c86..00000000 --- a/phone_focus_mode/lib/tests/adb_test_harness.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# lib/tests/adb_test_harness.sh — shared fixtures for the adb_common test files. -# Sourced by test_adb_common.sh and test_adb_trusted.sh; not a test file itself. -# -# Provides: the PASS/FAIL counters, run_test, a TEST_TMPDIR with an EXIT trap, -# an XDG_STATE_HOME redirect so no test touches the real state dir, the adb() -# mock, and the two injection payloads the sanitiser is tested against. -# Unit tests for adb_common.sh helper functions (no real device needed). -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PASS=0 -FAIL=0 - -_t_pass() { - PASS=$((PASS + 1)) - printf ' OK: %s\n' "$1" -} - -_t_fail() { - FAIL=$((FAIL + 1)) - printf ' FAIL: %s\n' "$1" -} - -TEST_TMPDIR="$(mktemp -d)" -trap 'rm -rf "${TEST_TMPDIR}"' EXIT - -export XDG_STATE_HOME="${TEST_TMPDIR}/state" -mkdir -p "${XDG_STATE_HOME}" - -source "${SCRIPT_DIR}/../adb_common.sh" - -ADB_MOCK_MODEL=$'Pixel "7";$(rm -rf /)`danger`\nline2' -# Single quotes are the whole point: this is an injection payload that must -# reach the code under test LITERALLY. Expanding $evil / `cmd` here would -# execute them in the test harness and test nothing. -# shellcheck disable=SC2016 -ADB_MOCK_FINGERPRINT='google/pixel:14/UP1A.231005.007/$evil;`cmd`' - -adb() { - if [[ "$#" -eq 1 && "$1" == "devices" ]]; then - printf 'List of devices attached\nSERIAL123\tdevice\n' - return 0 - fi - - if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.product.model" ]]; then - printf '%s\r\n' "${ADB_MOCK_MODEL}" - return 0 - fi - - if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "getprop" && "$5" == "ro.build.fingerprint" ]]; then - printf '%s\r\n' "${ADB_MOCK_FINGERPRINT}" - return 0 - fi - - if [[ "$1" == "-s" && "$3" == "shell" && "$4" == "su" && "$5" == "--mount-master" && "$6" == "-c" && "$7" == "echo ok" ]]; then - printf 'ok\n' - return 0 - fi - - printf 'Unexpected adb invocation:' >&2 - printf ' %q' "$@" >&2 - printf '\n' >&2 - return 1 -} - -run_test() { - local name="$1" - shift - - if "$@"; then - _t_pass "${name}" - else - _t_fail "${name}" - fi -} diff --git a/phone_focus_mode/lib/tests/backup_capture_harness.sh b/phone_focus_mode/lib/tests/backup_capture_harness.sh deleted file mode 100755 index e673f2e3..00000000 --- a/phone_focus_mode/lib/tests/backup_capture_harness.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash -# lib/tests/backup_capture_harness.sh — the fake device behind the -# backup_capture.sh tests. -# -# Sourced, not executed. Every function in backup_capture.sh reaches the -# phone through adb_cmd, so mocking that one function covers the library -# without pulling in adb_common.sh's device-detection surface. The mock -# records what it was asked to pull, which is what makes the two -# stdin-draining bugs the comments in pull_apks/pull_sdcard describe -# testable: a regression there pulls 1 package instead of all of them. -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -PASS=0 -FAIL=0 - -_t_pass() { - PASS=$((PASS + 1)) - printf ' OK: %s\n' "$1" -} - -_t_fail() { - FAIL=$((FAIL + 1)) - printf ' FAIL: %s\n' "$1" -} - -_t_eq() { - local want="$1" got="$2" what="$3" - if [[ "$got" == "$want" ]]; then - _t_pass "$what" - else - _t_fail "$what (want '${want}', got '${got}')" - fi -} - -TEST_TMPDIR="$(mktemp -d)" -trap 'rm -rf "${TEST_TMPDIR}"' EXIT - -readonly DEV="${TEST_TMPDIR}/device" -mkdir -p "${DEV}" - -_info() { :; } -_warn() { printf 'warn: %s\n' "$*" >>"${DEV}/warnings"; } - -# The fake device. Dispatch is by pattern because the real calls carry -# trailing redirections and quoted find expressions. -adb_cmd() { - local args="$*" - - case "${args}" in - 'shell pm list packages') - printf 'package:com.android.systemui\npackage:com.example.one\npackage:com.example.two\r\n' - ;; - 'shell pm list packages -3') - printf 'package:com.example.one\npackage:com.example.two\r\n' - ;; - 'shell pm list packages -u') - printf 'package:com.android.systemui\npackage:com.example.one\npackage:com.example.two\npackage:com.purged.app\r\n' - ;; - 'shell pm path com.example.one') - printf 'package:/data/app/one/base.apk\npackage:/data/app/one/split_config.xxhdpi.apk\r\n' - ;; - 'shell pm path com.example.two') - printf 'package:/data/app/two/base.apk\r\n' - ;; - 'shell pm path '*) - return 1 - ;; - 'pull '*) - local src="${args#pull }" - local dest="${src#* }" - src="${src%% *}" - printf '%s\n' "${src}" >>"${DEV}/pulled" - [[ -f "${DEV}/fail_pull" ]] && return 1 - # A directory pull lands a directory; an APK pull lands a file. - if [[ "${dest}" == */ ]]; then - mkdir -p "${dest}" - else - mkdir -p "$(dirname "${dest}")" - printf 'apk\n' >"${dest}" - fi - ;; - 'shell test -d /sdcard/'*) - local d="${args##*/sdcard/}" - [[ -f "${DEV}/nodir_${d}" ]] && return 1 - return 0 - ;; - *'iname "*.kdbx"'*) - cat "${DEV}/kdbx" 2>/dev/null || printf '' - ;; - 'shell settings list '*) - local ns="${args##* }" - printf 'a_key=1\nb_key=2\r\n' | sed "s/^/${ns}_/" - ;; - *) - printf 'unexpected adb_cmd: %s\n' "${args}" >&2 - return 1 - ;; - esac -} - -# shellcheck source=../backup_capture.sh -. "${SCRIPT_DIR}/../backup_capture.sh" - -OUT="${TEST_TMPDIR}/out" -mkdir -p "${OUT}" diff --git a/phone_focus_mode/lib/tests/ctl_cases_commands.sh b/phone_focus_mode/lib/tests/ctl_cases_commands.sh deleted file mode 100755 index e67372da..00000000 --- a/phone_focus_mode/lib/tests/ctl_cases_commands.sh +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env bash -# lib/tests/ctl_cases_commands.sh — assertions for focus_ctl.sh's subcommands, -# against the device stubs in test_ctl_libs.sh. -# -# The start/stop/status trio is the same shape in all seven enforcer -# libraries, so it is driven through tables rather than seven near-identical -# blocks: the duplication is in the subject, and repeating it here would add -# nothing and trip the jscpd gate. The per-enforcer specifics that tables -# cannot express get their own cases below. -set -euo pipefail - -# Run a subcommand for its side effects only. These are CLI entry points whose -# exit status is not part of their contract, and an unguarded nonzero return -# aborts the whole file under set -e. -_run_quiet() { - "$1" >/dev/null 2>&1 || return 0 -} - -# _start_case