feat(phone-focus): add recovery workflow, automation scripts, and docs

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-05-01 19:07:27 +02:00
parent b278d22750
commit 589e059eee
27 changed files with 5893 additions and 328 deletions

155
.github/skills/phone-focus-mode/SKILL.md vendored Normal file
View File

@ -0,0 +1,155 @@
# Phone Focus Mode Skill
## Overview
Focus mode is a geofence-based attention tool for a Magisk-rooted Blackview BL9000 (MTK MT6891).
When the phone is at home it enters focus mode: distracting apps are disabled and domain blocking
is enforced. Outside home, the phone returns to normal.
Scripts live in `phone_focus_mode/`. Deployment: `deploy.sh <phone_ip>` or
`ADB_SERIAL=<serial> deploy.sh`.
---
## Critical: Things That Will Brick / Factory-Wipe the Phone
### DO NOT use `pm uninstall -k --user 0` or `pm disable-user` on SYSTEM apps
MediaTek ROMs (and many others) trigger Android recovery / factory wipe on next boot when their
package manager scan finds system packages missing or disabled. This happened multiple times.
- `pm uninstall -k --user 0` → removes package from user-0 registry → survives reboot → wipe
- `pm disable-user --user 0` on system packages → package state persists → wipe
**Safe approaches:**
- `pm disable-user --user 0` is safe for 3rd-party (non-system) apps only
- For system apps (YouTube, Chrome, Play Store) use **UID firewall rules** or **DNS/hosts blocking**
- Never put system package names in `BLOCKED_SYSTEM_APPS` unless you have a tested recovery path
`BLOCKED_SYSTEM_APPS` in `config.sh` should remain **empty string** (`""`).
---
## Hosts File Blocking
### The Problem: ROM's /system partition is truly read-only
This device's system partition cannot be remounted rw — even with Magisk root, `mount -o remount,rw /system`
silently fails. `/system/etc/hosts` does not exist (no inode).
### The Solution: Magisk Systemless Hosts module
Magisk ships a "Systemless Hosts" built-in module. When enabled in the Magisk app, it magic-mounts
any file under `/data/adb/modules/hosts/system/etc/hosts` as `/system/etc/hosts` at boot.
**Required one-time setup (user must do in Magisk app):**
1. Open Magisk app → Modules tab
2. Enable "Systemless Hosts" module (toggle)
3. Reboot
This must be done manually the first time (or after a factory reset). There is no way to enable it
programmatically via ADB without user interaction on some firmware versions — `magisk --sqlite`
approach exists but is unreliable across versions.
After enabling the module, `hosts_enforcer.sh` automatically keeps it in sync by copying the
canonical hosts file to `/data/adb/modules/hosts/system/etc/hosts` every `HOSTS_CHECK_INTERVAL`
seconds. This survives reboots.
### What hosts_enforcer.sh does
1. Watches `$HOSTS_CANONICAL` (pushed by `deploy.sh` from `linux_configuration/hosts/`)
2. Copies it to `/data/adb/modules/hosts/system/etc/hosts` (Magisk module path)
3. Falls back to bind-mount / direct overwrite of `/system/etc/hosts` if it exists
4. Verifies integrity and re-syncs if tampered
### Domains blocked
Uses StevenBlack hosts with fakenews/gambling/porn/social extensions (~171k domains).
Custom YouTube/social entries in `DNS_BLOCK_HOSTS` config var. All apps including browsers
are blocked from resolving these domains — not just the YouTube app.
---
## App Blocking Strategy
### UID-based firewall (dns_enforcer.sh)
For system apps that cannot be safely disabled via `pm`, UID-based iptables rules block
web access (ports 80/443 only — DNS port 53 is deliberately NOT blocked).
**CRITICAL: Only block ports 80/443, NEVER all TCP/UDP for an UID.**
Blocking all TCP/UDP for a UID also kills DNS for that process and (on some Android versions)
breaks the system DNS cache for other apps too, making the entire phone unable to load any website.
### Focus-mode-only vs always-blocked
- `DNS_BLOCK_PACKAGES_ALWAYS`: YouTube, YouTube Music, Chrome — always blocked
- `DNS_BLOCK_PACKAGES_FOCUS_ONLY`: Play Store — blocked only during focus mode
`dns_enforcer.sh` reads `$MODE_FILE` (current_mode.txt) every `DNS_CHECK_INTERVAL` seconds
and adds/removes focus-only UIDs accordingly.
### Play Store + Aurora Store
Play Store (com.android.vending) is blocked during focus mode via UID rules.
Outside focus mode it's accessible normally.
**Aurora Store** (`com.aurora.store`) is an open-source Play Store client that works without a
Google account. Install it via: `deploy.sh <ip> --install-aurora`. It lets you install any
app from the Play Store catalog without needing Play Store's network access during focus mode
(though Play Store is accessible outside focus mode anyway).
---
## Boot Autostart
`FOCUS_BOOT_AUTOSTART=1` in `config.sh` installs `/data/adb/service.d/99-focus-mode.sh`.
`magisk_service.sh` (the service.d entry point):
- Polls `sys.boot_completed` (max 180 seconds)
- Waits `FOCUS_BOOT_DELAY_SECONDS` (max 10 seconds)
- Checks for emergency disable marker: `$STATE_DIR/disable_boot_autostart`
- Starts hosts_enforcer, dns_enforcer, focus_daemon in order
**Known issue**: `dirname "$0"` is wrong from service.d context (points to service.d, not the scripts).
`magisk_service.sh` exports `FOCUS_MODE_SCRIPT_DIR=/data/local/tmp/focus_mode` before sourcing
`config.sh` to work around this. All scripts use `${FOCUS_MODE_SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}`.
---
## MTK-Specific Notes
- `pm disable-user` on system packages persists across reboots and can trigger factory wipe
- `mount -o remount,rw /system` silently fails (hardware read-only), use Magisk module approach
- ip6tables `--uid-owner` may fail with permission error from non-service.d su contexts; use `|| true`
- Boot sequence is slow on MTK; 180s wait for `sys.boot_completed` is appropriate
---
## ADB Root
Use `su --mount-master -c 'sh -s'` so that bind mounts propagate to the global namespace:
```bash
printf '%s\n' 'your commands here' | adb shell su --mount-master -c 'sh -s'
```
Without `--mount-master`, bind mounts are invisible to other processes (they only exist in the
su session's mount namespace).
---
## Testing
Unit tests: `phone_focus_mode/lib/tests/`
```bash
bash phone_focus_mode/lib/tests/test_magisk_service.sh # 11 tests
bash phone_focus_mode/lib/tests/test_dns_enforcer.sh # 5 tests
bash phone_focus_mode/lib/tests/test_hosts_enforcer.sh # (create if needed)
```
Pre-commit: `pre-commit run --files phone_focus_mode/...`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,709 @@
---
post_title: "Phone focus recovery design"
author1: "GitHub Copilot"
post_slug: "phone-focus-recovery-design"
microsoft_alias: "copilot"
featured_image: ""
categories:
- "Documentation"
tags:
- "android"
- "adb"
- "backup"
- "shell"
ai_note: "AI-assisted design document"
summary: "Design for a rooted-Android recovery, backup, monitoring, and one-command orchestration workflow built on phone_focus_mode."
post_date: "2026-05-01"
---
## Goal
Create a repeatable rooted-Android management workflow that can:
- restore a freshly formatted phone to the previously hardened state
- back up important phone state whenever the phone appears on this PC
- monitor security and device-health drift over time
- expose one highly visible entrypoint at `scripts/run_all/run_phone.sh`
The design must build on the existing `phone_focus_mode/` deployment system
instead of replacing it with a second parallel toolchain.
## Existing foundation
The existing `phone_focus_mode/` implementation already provides the core
security stack:
- `deploy.sh` deploys the focus scripts and companion app over ADB
- `focus_daemon.sh` enforces location-based focus restrictions
- `hosts_enforcer.sh` protects `/system/etc/hosts`
- `dns_enforcer.sh` forces DNS behavior that respects the hosts file
- `launcher_enforcer.sh` keeps the approved launcher installed and pinned
- `magisk_service.sh` restores the protections automatically on boot
The new workflow should reuse these assets rather than re-implement them.
## Approved user experience
The workflow must support three main operator experiences.
### Normal day
Running:
```bash
./scripts/run_all/run_phone.sh
```
must:
- detect the phone over USB or paired wireless ADB
- take or update a backup snapshot
- collect health and security status
- repair minor drift when safe to do so
- print a concise summary of what changed and any remaining warnings
### After a format
Running:
```bash
./scripts/run_all/run_phone.sh fresh-phone
```
must:
- reconnect to the rooted phone
- restore the security stack first
- restore launcher, APKs, selected app data, and configured user files
- validate that the hardening is active again
- list any unavoidable manual follow-up actions
### If something feels wrong
Running:
```bash
./scripts/run_all/run_phone.sh doctor
```
must:
- inspect the same security and health checks as monitoring mode
- attempt repair of common drift
- stop short of broad destructive restore operations
- clearly distinguish between repaired issues and unresolved issues
This “what do I run and when?” guidance must appear in both:
- the future `phone_focus_mode/README.md` updates
- the help/usage text inside the visible wrapper script and the underlying
implementation script
## File layout
The visible entrypoint should live at the top level, while the implementation
stays with the phone project.
### Visible entrypoint
- `scripts/run_all/run_phone.sh`
Responsibilities:
- be easy to find and remember
- locate the repository root reliably
- forward arguments to the project-local implementation
- provide brief usage/help output for common flows
This script should stay thin and stable.
### Project-local implementation
- `phone_focus_mode/run_phone.sh`
Responsibilities:
- orchestrate detection, backup, monitoring, restore, and repair flows
- call or wrap `deploy.sh` rather than replacing it
- serve as the canonical implementation home for phone-specific logic
### Supporting libraries
- `phone_focus_mode/lib/adb_common.sh`
- `phone_focus_mode/lib/backup.sh`
- `phone_focus_mode/lib/restore.sh`
- `phone_focus_mode/lib/monitor.sh`
Responsibilities:
- isolate common shell helpers into focused modules
- keep `run_phone.sh` readable and testable
- avoid duplicating fragile ADB, path, and parsing logic
### Declarative configuration
- `phone_focus_mode/backup_manifest.sh`
Responsibilities:
- define which packages should have APK snapshots
- define which app data locations should be captured
- define which media/user directories should be synced
- define health thresholds and alerting policy
- classify each restore target as safe, manual-only, or backup-only
This file should be the user-editable scope definition rather than burying
every backup decision in shell code. The manifest should be shell-native so
the implementation does not need a separate JSON parser dependency just to
load backup scope.
### PC automation assets
- `phone_focus_mode/systemd/install_pc_phone_automation.sh`
- `phone_focus_mode/systemd/phone-auto-sync.service`
- `phone_focus_mode/systemd/phone-auto-sync.timer`
Responsibilities:
- install user-level automation on the PC
- periodically call the visible wrapper in safe `auto` mode
- serve as a fallback when hotplug or live discovery is imperfect
## Backup storage layout
Backups must be stored outside the Git workspace in a configurable local host
path. This avoids polluting the repository with large APKs, app data, media,
and other binary artifacts that violate the workspaces normal storage rules.
Recommended structure:
- `../testsAndMisc_binaries/phone_focus_backups/<device-id>/latest/`
- `../testsAndMisc_binaries/phone_focus_backups/<device-id>/history/<timestamp>/`
Only small text manifests or reports may live in-repo when helpful. APKs,
media, databases, and app-data payloads must stay in the external backup root.
Each snapshot should contain the following subdirectories.
### `device_info/`
- device properties
- Android version and build fingerprint
- installed package inventory
- partition and storage information
- serial and connection metadata
### `security_state/`
- generated canonical hosts file
- launcher APK snapshot and pinned activity metadata
- focus-mode logs and status files
- daemon and enforcer health snapshots
- DNS and firewall status outputs
### `apks/`
- selected APK exports for reinstallable apps
### `app_data/`
- configured rooted data pulls for selected packages
### `media/`
- configured user-facing storage such as photos, downloads, and documents
### `monitoring/`
- device-health snapshots over time
- summarized alert reports
- JSON snapshots suitable for later tooling or diffing
## Command modes
The implementation should support explicit subcommands plus a safe default.
### Default mode: `auto`
Invoked by:
```bash
./scripts/run_all/run_phone.sh
```
Flow:
1. discover or select exactly one device
2. verify root and repository prerequisites
3. **check for fresh-format indicators** (absence of focus scripts at expected
paths, missing daemon PIDs, absent magisk module, empty/missing `STATE_DIR`,
known app whitelist not installed)
- **If format detected:** print a clearly formatted warning block naming each
missing indicator, explain that the phone appears to have been wiped, and
suggest running `fresh-phone` mode. **Exit immediately. Do nothing else.**
4. collect a quick monitoring snapshot
5. run incremental backup steps
6. inspect the security stack for drift
7. repair minor drift when the repair is low risk
8. print a summary with warnings and any skipped actions
`auto` mode must never perform any restore or re-deployment action. It is
read-and-report only when the phone looks healthy, and detect-and-warn only
when the phone looks wiped.
### `fresh-phone`
Invoked by:
```bash
./scripts/run_all/run_phone.sh fresh-phone
```
Flow:
1. connect to the target phone
2. verify root and backup availability
3. record a pre-change snapshot
4. restore security assets first
5. restore launcher snapshot and home activity
6. restore selected APKs
7. restore selected app data
8. restore configured user files
9. run full verification
10. print manual follow-up steps, if any
If the phone is not yet in the minimum expected state, the workflow must stop
with a precise checklist rather than performing a partial restore.
### `backup`
Flow:
1. detect or connect device
2. create timestamped snapshot directory
3. collect metadata, APKs, app data, media, and security state
4. update the `latest/` snapshot pointer or mirror
5. prune history according to retention policy
### `monitor`
Flow:
1. detect or connect device
2. collect health and security state
3. compare against thresholds and prior snapshots
4. emit human-readable and machine-readable reports
5. return nonzero exit status on severe drift
### `doctor`
Flow:
1. run the monitoring checks
2. attempt low-risk repairs
3. restart missing daemons or re-push missing security assets if needed
4. stop before broad data restore actions
5. print repaired vs unresolved issues clearly
## Repair policy by mode
The implementation must use an explicit repair allowlist instead of treating
“minor drift” as an open-ended concept.
### Repairs allowed in `auto`
- restart managed daemons when scripts and state already exist
- restart the companion status app when it is already part of the setup
- reassert hosts, DNS, or launcher enforcement when the required backing files
already exist locally and on-device
- re-run deployment of the security stack when the drift is clearly limited to
managed `phone_focus_mode` assets
### Repairs forbidden in `auto`
- broad APK restore
- app-data restore
- media restore
- any action that changes user data outside the managed security stack
- any destructive cleanup of backup history
### Repairs allowed in `doctor`
- everything allowed in `auto`
- reinstall the companion app
- restore launcher snapshot and HOME activity when launcher backup metadata is
present
- re-push missing managed security assets from the local project state
### Repairs forbidden in `doctor`
- broad app-data restore
- media restore
- destructive reset of on-device state outside the managed security stack
### Actions allowed only in `fresh-phone`
- APK reinstall from backup
- selected app-data restore according to manifest policy
- configured media and user-file restore
Any action outside these allowlists must require explicit future design or
manual operator intent.
## Device detection and connection policy
The workflow must support both USB and wireless ADB.
Selection order:
1. use an explicitly supplied serial if present
2. use the only already-connected device if there is exactly one
3. use a saved wireless endpoint when available
4. try controlled wireless discovery fallback
5. fail with a clear message when multiple candidate devices exist
The workflow must avoid acting on the wrong device silently.
### Trusted identity requirements
The implementation should persist and verify a trusted identity record for the
managed phone, including:
- preferred ADB serial or wireless endpoint
- device model
- Android build fingerprint
- a stable property such as serial or hardware identifier when available
The script must refuse to proceed automatically when:
- more than one viable device is connected
- the connected device identity no longer matches the trusted record
- both USB and wireless sessions point to ambiguous or conflicting targets
### First-run and post-format prerequisites
`fresh-phone` cannot assume that all prerequisites already exist. Before any
restore work, the script must verify:
- USB debugging is authorized or wireless debugging is paired
- ADB can reach the device reliably
- Magisk and root are available
- root shell commands succeed in the expected mount namespace
If any prerequisite is missing, the command must stop and print the manual
steps required to continue, such as USB authorization, Magisk installation, or
first-time wireless pairing.
## Architecture boundary with existing deployment code
The implementation must not duplicate the core deployment logic already present
in `phone_focus_mode/deploy.sh`.
Rules:
- `deploy.sh` remains the deployment primitive for pushing security assets and
bringing up the phone hardening stack
- `run_phone.sh` may wrap or call `deploy.sh`, but must not reimplement its
file-push, daemon-start, or root-verification logic in parallel
- shared ADB and device-selection helpers may be extracted into common library
functions when that reduces duplication across both scripts
### Concrete integration path
The implementation plan should follow this sequence:
1. extract transport-agnostic ADB targeting helpers into
`phone_focus_mode/lib/adb_common.sh`
2. refactor `deploy.sh` so it can operate on a resolved target serial or
selected device abstraction rather than assuming a raw phone IP only
3. make `phone_focus_mode/run_phone.sh` the orchestration layer that performs
selection, backup, monitoring, and then delegates deployment work to
`deploy.sh`
This path preserves the proven deployment behavior while making it compatible
with USB and wireless device selection.
This keeps the new orchestration layer from drifting away from the proven
deployment flow.
## Monitoring scope
Monitoring should cover all user-requested areas.
### Battery wear and thermal state
- battery level
- charge status
- health and temperature if exposed
- evidence of abnormal thermal throttling or overheating
### Storage pressure and filesystem issues
- free space on major storage locations
- install/update failures caused by storage exhaustion
- signs of partition or package-management problems
### Performance and resource drift
- memory pressure indicators
- unusually heavy processes
- persistent crash or restart loops in the managed daemons
### Security drift
- focus daemon running or not
- hosts enforcer running or not
- DNS enforcer running or not
- launcher enforcer running or not
- companion app installed or missing
### Network and DNS bypass drift
- Private DNS re-enabled
- expected firewall chain missing
- hosts target hash or mount mismatch
- launcher default changed away from the protected launcher
### Boot persistence drift
- Magisk `service.d` script missing or no longer executable
- expected on-device files missing from `focus_mode`
- companion app missing when it is required for status visibility
## Monitoring report contract
Every monitoring run should produce two outputs:
- a concise human-readable summary
- a machine-readable report file suitable for later diffing and automation
Recommended report path pattern:
- `<backup-root>/<device-id>/monitoring/<timestamp>.json`
- `<backup-root>/<device-id>/monitoring/latest.json`
Recommended trusted-device record path:
- `${XDG_STATE_HOME:-$HOME/.local/state}/phone_focus_mode/trusted_device.sh`
Recommended runtime-automation state paths:
- `${XDG_STATE_HOME:-$HOME/.local/state}/phone_focus_mode/locks/`
- `${XDG_STATE_HOME:-$HOME/.local/state}/phone_focus_mode/last_run/`
Daily automation must not dirty the repository working tree merely by being
used. Trusted-device metadata, lock files, cooldown markers, and last-run
timestamps should therefore live in machine-local state outside the repo.
Recommended latest-backup pointer behavior:
- `latest/` should be a symlink to the newest history snapshot when the host
filesystem supports symlinks
- otherwise `latest/` may be refreshed as a copied mirror of the newest
snapshot
The machine-readable report should distinguish at least these severities:
- `ok`
- `warn`
- `error`
- `fatal`
Each reported check should record:
- check name
- status/severity
- evidence source
- short message
- whether the issue is repairable automatically
Important checks should verify more than just PID existence. For example:
- hosts protection should confirm both the canonical hash and the active
target mount/content state
- DNS protection should confirm Private DNS settings and firewall chain
presence
- launcher protection should confirm installation, stored snapshot metadata,
and current default HOME activity
- boot persistence should confirm the expected Magisk boot script is present
`monitor` should exit nonzero on severe drift. `doctor` should use the same
report format while additionally recording what was repaired.
For the initial implementation, “severe drift” should mean any of:
- target device identity mismatch
- no root access when root-dependent checks are required
- hosts enforcement missing or failing integrity checks
- DNS enforcement missing when it was previously configured
- launcher protection missing when launcher protection was previously
configured
- missing boot persistence for the managed stack
History retention and pruning policy should be locked down in the
implementation plan, but the initial default should favor safety over
aggressive deletion.
## Restore priority order
When the phone is freshly formatted or in a degraded state, restore in this
priority order:
1. connection and root verification
2. security scripts and boot persistence
3. canonical hosts and DNS protections
4. launcher enforcement and companion app
5. APK reinstall support
6. selected app data
7. media and user files
## Backup and restore policy classification
Each manifest entry should specify how it may be restored.
Suggested fields:
- `name`
- `kind` (`apk`, `app_data`, `media`, `security_state`)
- `backup_paths`
- `restore_policy` (`safe_restore`, `manual_only`, `backup_only`)
- `requires_root`
- `requires_version_match`
- `integrity_check`
- `contains_secrets`
The implementation must never silently restore unsupported or risky payloads.
When restore safety is uncertain, it should back up the data and report it as
manual-only.
### Conservative v1 restore policy
For the initial implementation, app-data restore should default to the most
conservative stance:
- no app-data entries should ship as `safe_restore` by default
- app-data items should default to `manual_only` unless a later design change
explicitly promotes a named package after validation
- APK restore, security-state restore, and configured media/file restore may
proceed according to their own manifest policies without implying that
private app-data restore is equally safe
This keeps v1 focused on reliable security recovery and host-side backups while
avoiding premature promises about rooted Android app-data portability.
### Canonical manifest examples
The manifest should include entries with a concrete shell-native shape. For
example:
```bash
APK_ITEMS=(
"com.qqlabs.minimalistlauncher|safe_restore|yes|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"
)
```
Where each pipe-delimited record maps to:
- name or package
- source path
- restore policy
- requires root
- requires integrity check
The implementation may wrap these records with helper functions, but should
keep the manifest format simple enough to read and edit without custom tools.
This ordering ensures the phone becomes safe again before broader recovery
work continues.
## Documentation and discoverability requirements
The final implementation must make the workflow obvious in at least two
places.
### README requirements
`phone_focus_mode/README.md` should document:
- the visible wrapper location
- the three core user flows:
- normal day
- after format
- if something feels wrong
- examples for `auto`, `fresh-phone`, `doctor`, `backup`, and `monitor`
- backup scope and monitoring expectations
### Script help-text requirements
Both `scripts/run_all/run_phone.sh` and `phone_focus_mode/run_phone.sh`
should expose help text that includes the memorable usage guidance:
- run the wrapper with no arguments for everyday backup and minor repair
- run `fresh-phone` after a format
- run `doctor` when the phone seems unhealthy or protections drifted
This is not just documentation; it is part of the usability contract.
## Automation safety rules
Because the phone may appear repeatedly over USB or Wi-Fi, automation must be
conservative.
Required safeguards:
- single-instance lock to prevent overlapping runs
- cooldown window so repeated reconnects do not trigger backup storms
- clear separation between lightweight `auto` mode and heavier full-backup
behavior
- retry and backoff rules for transient ADB failures
- no automatic `fresh-phone` restore without explicit user intent
## Testing and verification expectations
The implementation phase should follow strict shell hygiene and repository
quality rules.
- use `set -euo pipefail`
- prefer reusable functions over repeated ADB snippets
- validate parameters and environment clearly
- keep destructive operations explicit and well-logged
- add tests where practical for shell logic or parser behavior
- run `pre-commit run --files <changed-files>` before claiming completion
Verification must include:
- shell syntax validation
- targeted script execution in safe modes
- README/help-text verification against the approved user flows
- evidence that backups and monitoring output are actually produced
## Constraints and non-goals
The design deliberately does not promise impossible guarantees.
- It cannot make a rooted phone impossible to tamper with locally.
- It cannot safely restore every apps private data without app-specific risk.
- It should prefer explicit warnings over pretending unsupported restores are
safe.
The goal is a robust, repeatable, operator-friendly recovery and monitoring
system, not an infallible anti-root fortress.
## Open implementation notes
- Reuse code patterns already present in `linux_configuration/scripts/utils/`
and `python_pkg/screen_locker/_phone_verification.py` where they help with
ADB detection and wireless reconnection.
- Keep the wrapper stable even if the internal phone implementation evolves.
- Preserve the existing `deploy.sh` value rather than rewriting it from
scratch.
- Make backup scope declarative so expanding or narrowing coverage does not
require editing core shell control flow.

View File

@ -1,186 +1,131 @@
# Phone Focus Mode
## Phone focus mode
Location-based app restriction for a rooted Android phone using wireless ADB.
Rooted-Android hardening + recovery workflow for daily backup/monitoring and
post-format recovery.
When within ~500m of home: only whitelisted productive apps remain usable.
When outside that radius: all apps work normally.
The visible entrypoint is:
```bash
./scripts/run_all/run_phone.sh
```
That wrapper forwards to `phone_focus_mode/run_phone.sh`, which orchestrates
backup, monitoring, drift repair, and full recovery.
## Quick usage
### Normal day
```bash
./scripts/run_all/run_phone.sh
```
This runs `auto` mode:
- verifies and selects one device (USB or paired wireless ADB)
- checks format indicators first
- if phone appears wiped: prints warning + suggests `fresh-phone`, then exits
- otherwise collects monitoring snapshot, runs incremental backup, applies only
low-risk minor repairs, prints summary
`auto` never restores APK/media and never re-deploys.
### After a factory reset
```bash
./scripts/run_all/run_phone.sh fresh-phone
```
This mode:
- verifies prerequisites (ADB auth, root, Magisk runtime)
- takes pre-change snapshot
- restores security stack by delegating to `deploy.sh`
- restores safe APK/media backup items
- takes post-restore snapshot and prints required manual follow-up steps
### If something looks wrong
```bash
./scripts/run_all/run_phone.sh doctor
```
This mode:
- runs monitoring checks
- repairs common drift (daemon restarts, hosts file re-push)
- re-runs deployment only when boot persistence is missing
- avoids broad data restore actions
### Other modes
```bash
./scripts/run_all/run_phone.sh backup
./scripts/run_all/run_phone.sh monitor
./scripts/run_all/run_phone.sh --help
```
## Device targeting
Both the wrapper and `deploy.sh` support explicit device selection:
```bash
ADB_SERIAL=<device-serial> ./scripts/run_all/run_phone.sh auto
ADB_SERIAL=<device-serial> bash phone_focus_mode/deploy.sh --status
```
`deploy.sh` still supports the existing phone-IP flow:
```bash
bash phone_focus_mode/deploy.sh 192.168.1.42 --status
```
## 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
- rooted phone with Magisk installed
- USB debugging enabled and authorized (or paired wireless ADB)
- `adb` available on PC (`sudo pacman -S android-tools` on Arch Linux)
- location services enabled on phone
## Setup (first time)
## Setup essentials
### 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:
1. Set home coordinates in `phone_focus_mode/config_secrets.sh`.
2. Optionally tune whitelist and behavior in `phone_focus_mode/config.sh`.
3. Perform initial deploy:
```bash
./deploy.sh <phone_ip> --find-pkg stronglift
./deploy.sh <phone_ip> --find-pkg anki
./deploy.sh <phone_ip> --find-pkg pomodoro
bash phone_focus_mode/deploy.sh <phone_ip>
```
Then add the correct package name to `WHITELIST` in `config.sh`.
## Systemd automation (PC user service)
### 4. Deploy
Install timer-based periodic runs:
```bash
chmod +x deploy.sh
./deploy.sh 192.168.1.42 # replace with your phone's IP
bash phone_focus_mode/systemd/install_pc_phone_automation.sh
```
This:
This installs user units under `~/.config/systemd/user/`:
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
- `phone-auto-sync.service`
- `phone-auto-sync.timer` (every 30 minutes, persistent)
## Usage
```bash
./deploy.sh <ip> --status # Current mode, location, distance from home
./deploy.sh <ip> --log # View recent daemon log
./deploy.sh <ip> --list # List all apps + whitelist status
./deploy.sh <ip> --enable # Force focus mode ON (for testing)
./deploy.sh <ip> --disable # Force focus mode OFF
./deploy.sh <ip> --stop # Stop daemon entirely (restores all apps)
./deploy.sh <ip> --start # Start daemon
./deploy.sh <ip> --restart # Restart daemon (picks up config changes)
./deploy.sh <ip> --pull-log # Download log file to your PC
```
## How it works
```
Every 60 seconds:
get_location() ─── dumpsys location ──► lat,lon
calc_distance() ─── Haversine formula ──► meters
├── within radius? ──► enable_focus_mode()
│ pm disable-user all non-whitelisted apps
│ record which apps were disabled
└── outside radius? ──► disable_focus_mode()
pm enable each app in the disabled list
```
**Hysteresis:** 50m buffer prevents rapid toggling at the boundary. You must travel
`radius - 50m` inward to trigger lock, and `radius + 50m` outward to unlock.
**Fail-safe:** If location is unavailable for 5 consecutive checks (~5 minutes),
focus mode is automatically disabled so you can't be locked out.
**State persistence:** The daemon records exactly which apps _it_ disabled
(in `/data/local/tmp/focus_mode/disabled_by_focus.txt`), so it never accidentally
re-enables apps that were already disabled by the user before focus mode ran.
## On-device control (without PC)
From a root terminal app (e.g. Termux + tsu):
```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
## Relevant files
| 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 |
| `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 |
| ------------------------------------- | ------------------------------------------ |
| `scripts/run_all/run_phone.sh` | Thin, visible wrapper for daily use |
| `phone_focus_mode/run_phone.sh` | Main orchestration logic |
| `phone_focus_mode/lib/adb_common.sh` | ADB selection, locking, identity helpers |
| `phone_focus_mode/lib/backup.sh` | Incremental backup logic |
| `phone_focus_mode/lib/monitor.sh` | Security/health checks and reports |
| `phone_focus_mode/lib/restore.sh` | Safe restore helpers used by `fresh-phone` |
| `phone_focus_mode/deploy.sh` | Security-stack deployment primitive |
| `phone_focus_mode/backup_manifest.sh` | Declarative backup/restore scope |
## Hosts hardening
## Notes
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 <ip> --hosts-status
./deploy.sh <ip> --hosts-log
```
## Periodic rescan / Play Store
The focus daemon now **re-scans every tick** (not just on first entry). If
you re-enable an app via Play Store or `pm enable`, it gets re-disabled
within `CHECK_INTERVAL_FOCUS` seconds. `com.android.vending` (Play Store),
`com.*.packageinstaller`, and popular terminal apps are also uninstalled
`--user 0` in focus mode to close the usual bypass paths. Google Play
Services (`com.google.android.gms`) is left alone so banking apps work.
## Updating
After editing `config.sh` (e.g. changing whitelist):
```bash
./deploy.sh <ip> # re-pushes all files
# or just the config:
adb push config.sh /data/local/tmp/focus_mode/config.sh
./deploy.sh <ip> --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 <ip> --find-pkg <keyword>` to find the exact package name
- Package names are case-sensitive
- Backup scope and restore policies live in `phone_focus_mode/backup_manifest.sh`.
- Sensitive coordinates should stay in `config_secrets.sh` and out of version
control.
- On-device direct control remains available via `focus_ctl.sh`.

View File

@ -0,0 +1,85 @@
#!/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

View File

@ -8,7 +8,13 @@
# ============================================================
# --- Home location (loaded from config_secrets.sh, not tracked by git) ---
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
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
. "$SCRIPT_DIR/config_secrets.sh"
# --- Radius in meters ---
@ -39,12 +45,27 @@ export STATUS_FILE="$STATE_DIR/status.json"
# re-check. focus_daemon.sh polls for it and skips the remainder of its sleep.
export RECHECK_TRIGGER="$STATE_DIR/trigger_recheck"
# --- 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="/data/adb/focus_mode/hosts.canonical"
export HOSTS_CANONICAL="$STATE_DIR/hosts.canonical"
export HOSTS_TARGET="/system/etc/hosts"
export HOSTS_SHA_FILE="/data/adb/focus_mode/hosts.sha256"
export HOSTS_SHA_FILE="$STATE_DIR/hosts.sha256"
export HOSTS_CHECK_INTERVAL=15
export HOSTS_LOG="$STATE_DIR/hosts_enforcer.log"
@ -102,17 +123,54 @@ export DNS_DOH_IPV6="
2a10:50c0::ad1:ff
2a10:50c0::ad2:ff
"
# Additional content hosts to block at the firewall layer. This is a fallback
# for ROMs where /etc/hosts cannot be mounted (read-only partitions with no
# hosts inode). dns_enforcer.sh resolves these hostnames periodically and
# rejects traffic to their current endpoints on ports 80/443.
export DNS_BLOCK_HOSTS="
youtube.com
www.youtube.com
m.youtube.com
youtu.be
youtubei.googleapis.com
www.youtube-nocookie.com
googlevideo.com
ytimg.com
"
# Block network for selected distraction/system apps at firewall level by UID.
# This avoids pm disable/uninstall on system packages (which can destabilize
# boot on some vendor ROMs) while still making the apps effectively unusable.
#
# DNS_BLOCK_PACKAGES_ALWAYS: blocked at all times. Use for hard-distraction
# apps that should never have web access (YouTube app, YouTube Music, the
# stock browser). Hosts-file blocking handles their *content*; the UID rule
# keeps them from using DoH/QUIC fallbacks.
# DNS_BLOCK_PACKAGES_FOCUS_ONLY: blocked only while focus mode is active
# (current_mode.txt = focus). Use for apps that have legitimate use outside
# focus mode (Play Store for installing apps you want, package installer).
export DNS_BLOCK_PACKAGES_ALWAYS="
com.google.android.youtube
com.google.android.apps.youtube.music
com.android.chrome
"
export DNS_BLOCK_PACKAGES_FOCUS_ONLY="
com.android.vending
"
# Backwards-compat: code paths still referencing DNS_BLOCK_PACKAGES treat it
# as the always-blocked list.
export DNS_BLOCK_PACKAGES="$DNS_BLOCK_PACKAGES_ALWAYS"
# --- 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).
export LAUNCHER_PACKAGE="com.qqlabs.minimalistlauncher"
export LAUNCHER_APK="/data/adb/focus_mode/minimalist_launcher.apk"
export LAUNCHER_SHA_FILE="/data/adb/focus_mode/minimalist_launcher.sha256"
export LAUNCHER_APK="$STATE_DIR/minimalist_launcher.apk"
export LAUNCHER_SHA_FILE="$STATE_DIR/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"
export LAUNCHER_ACTIVITY_FILE="$STATE_DIR/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 <pkg>`.
@ -125,6 +183,11 @@ com.google.android.apps.nexuslauncher
"
export LAUNCHER_CHECK_INTERVAL=15
export LAUNCHER_LOG="$STATE_DIR/launcher_enforcer.log"
# Boot-time launcher enforcement is intentionally opt-in. Starting it from
# Magisk service.d can strand the phone on a broken HOME configuration if the
# snapshot is stale or the launcher update changed components. Keep this off by
# default and only enable it after verifying the launcher snapshot is healthy.
export LAUNCHER_BOOT_AUTOSTART=0
# ============================================================
# WHITELISTED APPS
@ -183,6 +246,9 @@ com.google.android.gm
ch.protonmail.android
com.microsoft.teams
# --- App installation alternative (keep visible in focus mode) ---
com.aurora.store
# --- Manga reader ---
eu.kanade.tachiyomi.sy
@ -222,38 +288,20 @@ com.xiaomi.smarthome
# ============================================================
export BLOCKED_SYSTEM_APPS="
# --- Browsers ---
com.android.chrome
com.chrome.beta
com.chrome.dev
com.chrome.canary
com.sec.android.app.sbrowser
com.opera.browser
com.opera.mini.native
com.brave.browser
com.vivaldi.browser
com.microsoft.emmx
com.kiwibrowser.browser
com.duckduckgo.mobile.android
# --- Package installers / stores ---
# Blocking these prevents re-installing or re-enabling apps while in
# focus mode. Play Services (com.google.android.gms) is intentionally
# left enabled because banking apps require it.
com.android.vending
com.google.market
com.android.packageinstaller
com.google.android.packageinstaller
com.android.documentsui
com.google.android.documentsui
# --- Shells / terminals that could be used to bypass restrictions ---
com.termux
com.termux.api
com.termux.boot
jackpal.androidterm
com.server.auditor.ssh.client
org.connectbot
# *** 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: caused 3 wipes on BL9000).
#
# System apps (Chrome, YouTube, Play Store, etc.) are enforced via
# DNS+iptables in dns_enforcer.sh instead — that layer is stateless and
# requires no cleanup on reboot.
#
# Only user-installed 3rd-party apps (pm list packages -3) are safe for
# pm disable-user because the MTK bootloop trigger only fires on missing or
# disabled ROM/system components, not on user-installed packages.
"
# --- System / essential packages that must NEVER be disabled ---

View File

@ -18,9 +18,26 @@ PHONE_IP="${1:-}"
ACTION="${2:---deploy}"
REMOTE_DIR="/data/local/tmp/focus_mode"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
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[@]}" "$@"
}
usage() {
echo "Usage: $0 <phone_ip> [action]"
echo " or: ADB_SERIAL=<serial> $0 [action]"
echo ""
echo "Actions:"
echo " (none) Full deploy"
@ -39,6 +56,7 @@ usage() {
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"
@ -74,6 +92,10 @@ check_coords() {
}
check_ip() {
if [[ -n "${ADB_SERIAL:-}" ]]; then
return 0
fi
if [ -z "$PHONE_IP" ]; then
echo "ERROR: Phone IP not provided."
echo ""
@ -82,6 +104,17 @@ check_ip() {
}
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
@ -90,6 +123,7 @@ connect_adb() {
echo "Make sure wireless ADB is enabled and the phone is reachable."
exit 1
fi
ADB_TARGET=(-s "$PHONE_IP:5555")
echo "Connected."
}
@ -98,7 +132,64 @@ connect_adb() {
# namespace — required for any status checks that inspect the hosts bind
# mount, /data/adb/focus_mode files, or for starting daemons.
adb_root() {
adb -s "$PHONE_IP:5555" shell su --mount-master -c "$1"
local command_text="$1"
printf '%s\n' "$command_text" | adb_cmd shell su --mount-master -c "sh -s"
}
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}'
}
# ============================================================
# 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"
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"
}
# ============================================================
@ -122,18 +213,18 @@ do_deploy() {
echo "[3/7] Creating directories on device..."
# Use world-writable staging dir so non-root adb push works
adb -s "$PHONE_IP:5555" shell "mkdir -p /data/local/tmp/focus_stage"
adb_root "mkdir -p $REMOTE_DIR /data/adb/service.d /data/adb/focus_mode"
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"
echo "[4/7] Uploading scripts..."
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/config.sh" "/data/local/tmp/focus_stage/config.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/focus_daemon.sh" "/data/local/tmp/focus_stage/focus_daemon.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/dns_enforcer.sh" "/data/local/tmp/focus_stage/dns_enforcer.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/launcher_enforcer.sh" "/data/local/tmp/focus_stage/launcher_enforcer.sh"
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/magisk_service.sh" "/data/local/tmp/focus_stage/99-focus-mode.sh"
adb_cmd push "$SCRIPT_DIR/config.sh" "/data/local/tmp/focus_stage/config.sh"
adb_cmd push "$SCRIPT_DIR/focus_daemon.sh" "/data/local/tmp/focus_stage/focus_daemon.sh"
adb_cmd push "$SCRIPT_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh"
adb_cmd push "$SCRIPT_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh"
adb_cmd push "$SCRIPT_DIR/dns_enforcer.sh" "/data/local/tmp/focus_stage/dns_enforcer.sh"
adb_cmd push "$SCRIPT_DIR/launcher_enforcer.sh" "/data/local/tmp/focus_stage/launcher_enforcer.sh"
adb_cmd push "$SCRIPT_DIR/magisk_service.sh" "/data/local/tmp/focus_stage/99-focus-mode.sh"
# Generate and upload the canonical hosts file (StevenBlack + custom entries).
# This mirrors what linux_configuration/hosts/install.sh installs on the PC.
@ -142,12 +233,18 @@ do_deploy() {
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 -s "$PHONE_IP:5555" push "$HOSTS_TMP" "/data/local/tmp/focus_stage/hosts.canonical"
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"
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
@ -159,7 +256,7 @@ do_deploy() {
echo " config_secrets.sh already exists on phone - skipping (preserving real coords)"
else
echo " Pushing config_secrets.sh (first install)..."
adb -s "$PHONE_IP:5555" push "$SCRIPT_DIR/config_secrets.sh" "/data/local/tmp/focus_stage/config_secrets.sh"
adb_cmd push "$SCRIPT_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
@ -170,55 +267,111 @@ do_deploy() {
adb_root "cp /data/local/tmp/focus_stage/hosts_enforcer.sh $REMOTE_DIR/hosts_enforcer.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/launcher_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh"
if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$SCRIPT_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 -s "$PHONE_IP:5555" shell "test -f /data/local/tmp/focus_stage/hosts.canonical" 2>/dev/null; then
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 /data/adb/focus_mode/hosts.canonical 2>/dev/null; true"
adb_root "cp /data/local/tmp/focus_stage/hosts.canonical /data/adb/focus_mode/hosts.canonical"
adb_root "chmod 644 /data/adb/focus_mode/hosts.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 /data/adb/focus_mode/hosts.sha256 2>/dev/null; true"
adb_root "sha256sum /data/adb/focus_mode/hosts.canonical | awk '{print \$1}' > /data/adb/focus_mode/hosts.sha256 2>/dev/null || md5sum /data/adb/focus_mode/hosts.canonical | awk '{print \$1}' > /data/adb/focus_mode/hosts.sha256"
adb_root "chmod 644 /data/adb/focus_mode/hosts.sha256"
adb_root "chattr +i /data/adb/focus_mode/hosts.canonical 2>/dev/null; true"
adb_root "chattr +i /data/adb/focus_mode/hosts.sha256 2>/dev/null; true"
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"
# ---- Magisk Systemless Hosts module (REQUIRED) ----
# This module magic-mounts /data/adb/modules/hosts/system/etc/hosts
# as /system/etc/hosts at boot — the only way to create that file on
# this ROM's hardware-read-only system partition.
#
# The module must be ENABLED in the Magisk app by the user (one-time,
# after each factory reset). We CANNOT enable it programmatically.
# Without it, no app-level hosts blocking is possible, so we STOP here
# and require user action before the deploy can proceed.
local magisk_hosts_ok=0
if adb_root "test -d /data/adb/modules/hosts" 2>/dev/null; then
if adb_root "test ! -f /data/adb/modules/hosts/disable -a ! -f /data/adb/modules/hosts/remove" 2>/dev/null; then
magisk_hosts_ok=1
fi
fi
if [[ "$magisk_hosts_ok" -eq 0 ]]; then
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ ACTION REQUIRED — Deploy cannot continue ║"
echo "╠══════════════════════════════════════════════════════════════════╣"
echo "║ The Magisk 'Systemless Hosts' module is not enabled. ║"
echo "║ Without it, hosts-file blocking is impossible on this device ║"
echo "║ (the system partition is hardware read-only even with root). ║"
echo "║ ║"
echo "║ Steps to fix: ║"
echo "║ 1. Open the Magisk app on the phone ║"
echo "║ 2. Tap the Modules tab (puzzle-piece icon) ║"
echo "║ 3. Find 'Systemless Hosts' and toggle it ON ║"
echo "║ 4. Reboot the phone when prompted ║"
echo "║ 5. Re-run this deploy command ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""
exit 1
fi
adb_root "mkdir -p /data/adb/modules/hosts/system/etc"
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"
echo " Magisk hosts module populated ($(adb_root "wc -l < /data/adb/modules/hosts/system/etc/hosts" 2>/dev/null | tr -d ' ') lines). Reboot to activate /system/etc/hosts."
fi
adb_root "rm -rf /data/local/tmp/focus_stage"
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" || true
if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$SCRIPT_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"
# 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" || true
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 "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"
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"
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"
# 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 /data/adb/focus_mode/hosts.canonical" 2>/dev/null; then
adb -s "$PHONE_IP:5555" shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/hosts_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
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 >/dev/null 2>/dev/null &'
fi
# Start DNS enforcer (forces Private DNS off, blocks DoH/DoT). Always on.
adb -s "$PHONE_IP:5555" shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/dns_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/dns_enforcer.sh </dev/null >/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 /data/adb/focus_mode/minimalist_launcher.apk" 2>/dev/null; then
adb -s "$PHONE_IP:5555" shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
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 >/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
adb -s "$PHONE_IP:5555" shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/focus_daemon.sh </dev/null >/dev/null 2>/dev/null &'
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/focus_daemon.sh </dev/null >/dev/null 2>/dev/null &'
sleep 4
# ---- Companion status notification app ----
@ -232,16 +385,16 @@ do_deploy() {
fi
if [ -f "$APK" ]; then
echo " Installing APK..."
adb -s "$PHONE_IP:5555" install -r "$APK" >/dev/null || true
adb_cmd install -r "$APK" >/dev/null || true
# Grant runtime permission (Android 13+ requires it for notifications).
adb -s "$PHONE_IP:5555" shell pm grant com.kuhy.focusstatus android.permission.POST_NOTIFICATIONS >/dev/null 2>&1 || true
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 -s "$PHONE_IP:5555" shell dumpsys package com.kuhy.focusstatus 2>/dev/null | grep -oE 'userId=[0-9]+' | head -1 | cut -d= -f2)"
APP_UID="$(adb_cmd shell dumpsys package com.kuhy.focusstatus 2>/dev/null | grep -oE 'userId=[0-9]+' | head -1 | cut -d= -f2)"
if [ -n "$APP_UID" ]; then
adb -s "$PHONE_IP:5555" 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
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 -s "$PHONE_IP:5555" shell am start -n com.kuhy.focusstatus/.LaunchActivity >/dev/null 2>&1 || true
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"
@ -254,7 +407,9 @@ do_deploy() {
echo "Checking status..."
adb_root "sh $REMOTE_DIR/focus_ctl.sh status"
echo ""
echo "The daemon will auto-start on every boot via Magisk service.d."
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"
@ -262,6 +417,7 @@ do_deploy() {
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)"
}
# ============================================================
@ -276,7 +432,7 @@ do_control() {
do_pull_log() {
connect_adb
echo "Downloading log..."
adb -s "$PHONE_IP:5555" pull "$REMOTE_DIR/focus_mode.log" "./focus_mode_$(date +%Y%m%d_%H%M%S).log"
adb_cmd pull "$REMOTE_DIR/focus_mode.log" "./focus_mode_$(date +%Y%m%d_%H%M%S).log"
echo "Done."
}
@ -288,7 +444,7 @@ do_find_pkg() {
fi
connect_adb
echo "Packages matching '$filter':"
adb -s "$PHONE_IP:5555" shell pm list packages | grep -i "$filter" | sed 's/^package:/ /'
adb_cmd shell pm list packages | grep -i "$filter" | sed 's/^package:/ /'
}
do_snapshot_launcher() {
@ -305,7 +461,7 @@ do_snapshot_launcher() {
# 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 -s "$PHONE_IP:5555" shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
sleep 3
adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-status"
}
@ -333,5 +489,6 @@ case "$ACTION" in
--launcher-status) do_control "launcher-status" ;;
--launcher-log) connect_adb; adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-log 100" ;;
--snapshot-launcher) do_snapshot_launcher ;;
--install-aurora) do_install_aurora ;;
*) echo "Unknown action: $ACTION"; usage ;;
esac

View File

@ -25,16 +25,119 @@
# leaves tamper logs.
# ============================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_DIR="${FOCUS_MODE_SCRIPT_DIR:-$(cd "$(dirname "$0")" && pwd)}"
# shellcheck source=config.sh
. "$SCRIPT_DIR/config.sh"
PIDFILE="$STATE_DIR/dns_enforcer.pid"
DNS_BLOCK_IPV4_FILE="$STATE_DIR/dns_block_ipv4.txt"
DNS_BLOCK_IPV6_FILE="$STATE_DIR/dns_block_ipv6.txt"
DNS_BLOCK_UID_FILE="$STATE_DIR/dns_block_uids.txt"
mkdir -p "$STATE_DIR"
touch "$DNS_LOG"
chmod 666 "$DNS_LOG" 2>/dev/null || true
append_unique_line() {
local file="$1"
local value="$2"
[ -z "$value" ] && return 0
[ -f "$file" ] || : > "$file"
if ! grep -qxF "$value" "$file" 2>/dev/null; then
echo "$value" >> "$file"
fi
}
extract_ping_ip() {
# Extract the host IP from the first line of ping output:
# Ping example.com (1.2.3.4): ...
# Ping example.com (2a00:...): ...
printf '%s\n' "$1" | sed -n 's/^[^(]*(\([^)]*\)).*/\1/p' | head -1
}
extract_package_uid() {
# Parse one line from `cmd package list packages -U`, e.g.:
# package:com.android.chrome uid:10153
printf '%s\n' "$1" | sed -n 's/.* uid:\([0-9][0-9]*\).*/\1/p' | head -1
}
resolve_package_uid() {
local pkg="$1"
local line uid
line="$(cmd package list packages -U 2>/dev/null | grep -E "^package:${pkg}( |$)" | head -1 || true)"
uid="$(extract_package_uid "$line")"
if echo "$uid" | grep -Eq '^[0-9]+$'; then
echo "$uid"
fi
}
resolve_ipv4() {
local host="$1"
local line ip
line="$(toybox ping -4 -c 1 -W 1 "$host" 2>/dev/null | head -1 || true)"
ip="$(extract_ping_ip "$line")"
if echo "$ip" | grep -Eq '^[0-9]+(\.[0-9]+){3}$'; then
echo "$ip"
fi
}
resolve_ipv6() {
local host="$1"
local line ip
line="$(toybox ping -6 -c 1 -W 1 "$host" 2>/dev/null | head -1 || true)"
ip="$(extract_ping_ip "$line")"
if echo "$ip" | grep -Eq '^[0-9A-Fa-f:]+$'; then
echo "$ip"
fi
}
refresh_blocked_content_ips() {
: > "$DNS_BLOCK_IPV4_FILE"
: > "$DNS_BLOCK_IPV6_FILE"
local host ip4 ip6
for host in $DNS_BLOCK_HOSTS; do
[ -z "$host" ] && continue
[ "${host#\#}" != "$host" ] && continue
ip4="$(resolve_ipv4 "$host")"
ip6="$(resolve_ipv6 "$host")"
append_unique_line "$DNS_BLOCK_IPV4_FILE" "$ip4"
append_unique_line "$DNS_BLOCK_IPV6_FILE" "$ip6"
done
}
refresh_blocked_app_uids() {
: > "$DNS_BLOCK_UID_FILE"
local pkg uid
# Always-blocked packages (hard distractions: YouTube, Chrome, ...).
for pkg in $DNS_BLOCK_PACKAGES_ALWAYS; do
[ -z "$pkg" ] && continue
[ "${pkg#\#}" != "$pkg" ] && continue
uid="$(resolve_package_uid "$pkg")"
append_unique_line "$DNS_BLOCK_UID_FILE" "$uid"
done
# Focus-mode-only packages (Play Store etc. - usable outside focus mode).
if [ "$(cat "$MODE_FILE" 2>/dev/null)" = "focus" ]; then
for pkg in $DNS_BLOCK_PACKAGES_FOCUS_ONLY; do
[ -z "$pkg" ] && continue
[ "${pkg#\#}" != "$pkg" ] && continue
uid="$(resolve_package_uid "$pkg")"
append_unique_line "$DNS_BLOCK_UID_FILE" "$uid"
done
fi
}
log() {
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
@ -136,6 +239,36 @@ fill_chain_v4() {
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
done
# Content-block fallback: reject HTTP/HTTPS to resolved endpoints of
# DNS_BLOCK_HOSTS. This is used on ROMs where hosts-file enforcement is
# impossible (no writable hosts inode on read-only partitions).
if [ -f "$DNS_BLOCK_IPV4_FILE" ]; then
while IFS= read -r ip; do
[ -z "$ip" ] && continue
iptables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 80 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
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
done < "$DNS_BLOCK_IPV4_FILE"
fi
# App-level web block: block HTTP/HTTPS for selected package UIDs.
# Only ports 80 and 443 are blocked so DNS (port 53) and system services
# still work — the apps just can't load web content or stream video.
if [ -f "$DNS_BLOCK_UID_FILE" ]; then
while IFS= read -r uid; do
[ -z "$uid" ] && continue
iptables -A "$DNS_IPT_CHAIN" -m owner --uid-owner "$uid" -p tcp --dport 80 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
iptables -A "$DNS_IPT_CHAIN" -m owner --uid-owner "$uid" -p tcp --dport 443 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
iptables -A "$DNS_IPT_CHAIN" -m owner --uid-owner "$uid" -p udp --dport 443 -j REJECT \
--reject-with icmp-port-unreachable 2>/dev/null || true
done < "$DNS_BLOCK_UID_FILE"
fi
}
fill_chain_v6() {
@ -158,9 +291,36 @@ fill_chain_v6() {
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 53 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
done
if [ -f "$DNS_BLOCK_IPV6_FILE" ]; then
while IFS= read -r ip; do
[ -z "$ip" ] && continue
ip6tables -A "$DNS_IPT_CHAIN" -d "$ip" -p tcp --dport 80 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
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
done < "$DNS_BLOCK_IPV6_FILE"
fi
if [ -f "$DNS_BLOCK_UID_FILE" ]; then
while IFS= read -r uid; do
[ -z "$uid" ] && continue
ip6tables -A "$DNS_IPT_CHAIN" -m owner --uid-owner "$uid" -p tcp --dport 80 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
ip6tables -A "$DNS_IPT_CHAIN" -m owner --uid-owner "$uid" -p tcp --dport 443 -j REJECT \
--reject-with tcp-reset 2>/dev/null || true
ip6tables -A "$DNS_IPT_CHAIN" -m owner --uid-owner "$uid" -p udp --dport 443 -j REJECT \
--reject-with icmp6-port-unreachable 2>/dev/null || true
done < "$DNS_BLOCK_UID_FILE"
fi
}
enforce_iptables() {
refresh_blocked_content_ips
refresh_blocked_app_uids
if command -v iptables >/dev/null 2>&1; then
ensure_chain iptables && fill_chain_v4
fi
@ -196,4 +356,6 @@ main() {
done
}
if [ "${FOCUS_MODE_DNS_ENFORCER_TESTING:-0}" != "1" ]; then
main "$@"
fi

View File

@ -14,6 +14,30 @@ SCRIPT_DIR="/data/local/tmp/focus_mode"
PIDFILE="$STATE_DIR/daemon.pid"
recover_pidfile() {
local pidfile="$1"
local script_name="$2"
local pid
if [ -f "$pidfile" ]; then
pid="$(cat "$pidfile" 2>/dev/null)"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "$pid"
return 0
fi
fi
pid="$(pgrep -f "$script_name" 2>/dev/null | head -1)"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
echo "$pid" > "$pidfile" 2>/dev/null || true
chmod 666 "$pidfile" 2>/dev/null || true
echo "$pid"
return 0
fi
return 1
}
# ---- Logging ----
log() {
local ts
@ -54,13 +78,7 @@ usage() {
# 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
recover_pidfile "$PIDFILE" "focus_daemon.sh"
}
cmd_start() {
@ -275,13 +293,7 @@ cmd_whitelist() {
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
recover_pidfile "$HOSTS_PIDFILE" "hosts_enforcer.sh"
}
cmd_hosts_status() {
@ -368,13 +380,7 @@ cmd_hosts_log() {
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
recover_pidfile "$DNS_PIDFILE" "dns_enforcer.sh"
}
cmd_dns_status() {
@ -461,13 +467,7 @@ 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
recover_pidfile "$LAUNCHER_PIDFILE" "launcher_enforcer.sh"
}
cmd_launcher_snapshot() {

View File

@ -96,7 +96,17 @@ init() {
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.sh first."
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
@ -193,27 +203,24 @@ enable_focus_mode() {
done < "$tmp_pkgs"
rm -f "$tmp_pkgs"
# Uninstall-for-user-0 any blocked system apps (Play Store, browsers,
# package installer UI, terminal apps). pm uninstall is idempotent:
# re-running it on already-uninstalled-for-user-0 packages is a no-op.
local uninstalled_sys="$STATE_DIR/uninstalled_sys.txt"
[ "$first_entry" -eq 1 ] && : > "$uninstalled_sys"
# List of packages installed for user 0 (one per line, "package:" prefix).
local user0_pkgs="$STATE_DIR/user0_pkgs.txt"
pm list packages --user 0 2>/dev/null | sed 's/^package://' > "$user0_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 grep -qxF "$pkg" "$user0_pkgs" 2>/dev/null; then
if pm uninstall -k --user 0 "$pkg" >/dev/null 2>&1; then
grep -qxF "$pkg" "$uninstalled_sys" 2>/dev/null \
|| echo "$pkg" >> "$uninstalled_sys"
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
fi
done < "$blocked_sys"
rm -f "$user0_pkgs"
CURRENT_MODE="focus"
echo "focus" > "$MODE_FILE"
@ -235,15 +242,9 @@ disable_focus_mode() {
local count=0
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
# Re-install system apps that were uninstalled for user
if [ -f "$STATE_DIR/uninstalled_sys.txt" ] && [ -s "$STATE_DIR/uninstalled_sys.txt" ]; then
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
pm install-existing --user 0 "$pkg" >/dev/null 2>&1
done < "$STATE_DIR/uninstalled_sys.txt"
: > "$STATE_DIR/uninstalled_sys.txt"
fi
# Re-enable all disabled apps
# 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))

View File

@ -19,7 +19,7 @@ import android.os.Looper;
*/
public final class StatusService extends Service {
private static final String CHANNEL_ID = "focus_status";
private static final String CHANNEL_ID = "focus_status_persistent";
private static final int NOTIF_ID = 1042;
private static final long REFRESH_MS = 5_000L;
@ -88,7 +88,7 @@ public final class StatusService extends Service {
}
NotificationChannel ch = new NotificationChannel(
CHANNEL_ID, "Focus Mode Status",
NotificationManager.IMPORTANCE_LOW);
NotificationManager.IMPORTANCE_DEFAULT);
ch.setDescription("Persistent status of the focus-mode daemon");
ch.setShowBadge(false);
ch.setSound(null, null);

View File

@ -28,6 +28,14 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/config.sh"
PIDFILE="$STATE_DIR/hosts_enforcer.pid"
MISSING_TARGET_LOGGED=0
MAGISK_HOSTS_LOGGED=0
# Magisk "Systemless Hosts" module path. When this module is enabled,
# Magisk magic-mounts files placed under its system/ tree onto the live
# /system at boot. Copying our canonical hosts there makes Magisk overlay
# /system/etc/hosts on next boot, even on read-only system partitions.
MAGISK_HOSTS_MODULE_DIR="/data/adb/modules/hosts"
MAGISK_HOSTS_TARGET="$MAGISK_HOSTS_MODULE_DIR/system/etc/hosts"
mkdir -p "$STATE_DIR" "$(dirname "$HOSTS_CANONICAL")"
touch "$HOSTS_LOG"
@ -89,6 +97,10 @@ is_bind_mounted_correctly() {
[ -n "$target_hash" ] && [ "$target_hash" = "$canonical_hash" ]
}
has_hosts_target() {
[ -f "$HOSTS_TARGET" ]
}
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.
@ -123,13 +135,32 @@ make_target_writable_once() {
}
assert_bind_mount() {
if ! has_hosts_target; then
# Target file doesn't exist yet - try to create it by directly writing
# /system (remount rw briefly). On Magisk-rooted devices this usually
# works because Magisk intercepts the remount. If it fails we fall back
# to the firewall-only path and log a warning.
log "hosts target missing - attempting to create $HOSTS_TARGET via /system remount"
make_target_writable_once
if ! has_hosts_target; then
if [ "$MISSING_TARGET_LOGGED" -eq 0 ]; then
log "WARN: could not create $HOSTS_TARGET on this ROM (hosts bind enforcement disabled)"
MISSING_TARGET_LOGGED=1
fi
return 0
fi
log "Created and populated $HOSTS_TARGET directly"
return 0
fi
if is_bind_mounted_correctly; then
return 0
fi
# Something is in the way (OEM overlay or previous partial mount).
unmount_existing_hosts_mount
# Try plain bind mount - no remount-rw of /system needed.
if mount --bind "$HOSTS_CANONICAL" "$HOSTS_TARGET" 2>/dev/null; then
# Android toybox mount commonly supports "-o bind" but not "--bind".
if mount -o bind "$HOSTS_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 $HOSTS_CANONICAL over $HOSTS_TARGET"
@ -152,6 +183,44 @@ ensure_canonical_immutable() {
chattr +i "$HOSTS_CANONICAL" 2>/dev/null || true
}
# Populate the Magisk "Systemless Hosts" module. Magisk's magic mount picks
# up files under /data/adb/modules/<id>/system/ at boot and overlays them
# onto the live /system tree. By placing our canonical hosts there we get
# /system/etc/hosts on next boot even on ROMs whose system partition is
# truly read-only (where remount,rw silently fails).
# Returns 0 if module is present and now in sync, 1 otherwise.
populate_magisk_hosts_module() {
if [ ! -d "$MAGISK_HOSTS_MODULE_DIR" ]; then
if [ "$MAGISK_HOSTS_LOGGED" -eq 0 ]; then
log "WARN: Magisk hosts module dir absent ($MAGISK_HOSTS_MODULE_DIR); enable 'Systemless Hosts' in the Magisk app."
MAGISK_HOSTS_LOGGED=1
fi
return 1
fi
if [ -f "$MAGISK_HOSTS_MODULE_DIR/disable" ] || [ -f "$MAGISK_HOSTS_MODULE_DIR/remove" ]; then
if [ "$MAGISK_HOSTS_LOGGED" -eq 0 ]; then
log "WARN: Magisk hosts module is disabled or pending removal"
MAGISK_HOSTS_LOGGED=1
fi
return 1
fi
mkdir -p "$(dirname "$MAGISK_HOSTS_TARGET")" 2>/dev/null || true
local module_hash canonical_hash
module_hash="$(sha256_of "$MAGISK_HOSTS_TARGET")"
canonical_hash="$(sha256_of "$HOSTS_CANONICAL")"
if [ -n "$module_hash" ] && [ "$module_hash" = "$canonical_hash" ]; then
return 0
fi
if cp "$HOSTS_CANONICAL" "$MAGISK_HOSTS_TARGET" 2>/dev/null; then
chmod 644 "$MAGISK_HOSTS_TARGET" 2>/dev/null || true
log "Synced canonical hosts -> Magisk module ($MAGISK_HOSTS_TARGET); active after next reboot"
MAGISK_HOSTS_LOGGED=0
return 0
fi
log "ERROR: failed to copy canonical hosts to $MAGISK_HOSTS_TARGET"
return 1
}
verify_and_restore() {
if [ ! -f "$HOSTS_CANONICAL" ]; then
log "ERROR: canonical hosts missing at $HOSTS_CANONICAL"
@ -177,6 +246,16 @@ verify_and_restore() {
return 1
fi
if ! has_hosts_target; then
if [ "$MISSING_TARGET_LOGGED" -eq 0 ]; then
log "WARN: hosts target missing on this ROM: $HOSTS_TARGET (integrity checks skipped)"
MISSING_TARGET_LOGGED=1
fi
return 0
fi
MISSING_TARGET_LOGGED=0
# Live target integrity check
local actual_target
actual_target="$(sha256_of "$HOSTS_TARGET")"
@ -199,7 +278,10 @@ main() {
log "hosts_enforcer started (PID=$$)"
ensure_canonical_immutable
# Initial assertion
# Seed the Magisk systemless hosts module so /system/etc/hosts gets
# magic-mounted on next boot.
populate_magisk_hosts_module || true
# Initial assertion (covers the case where target already exists).
assert_bind_mount || true
# Seed sha file if missing
@ -210,6 +292,7 @@ main() {
fi
while true; do
populate_magisk_hosts_module || true
verify_and_restore
rotate_log
sleep "$HOSTS_CHECK_INTERVAL"

View File

@ -0,0 +1,262 @@
#!/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"
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 }'
}
_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
}
adb_save_trusted_device() {
adb_collect_identity
mkdir -p "${_PHONE_STATE_DIR}"
{
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}"
} >"${TRUSTED_DEVICE_FILE}"
chmod 600 "${TRUSTED_DEVICE_FILE}"
_info "Saved trusted device record: model='${DEVICE_MODEL}' serial='${DEVICE_SERIAL}'"
}
adb_verify_trusted_identity() {
local saved_model=""
local saved_fingerprint=""
local saved_serial=""
if [[ ! -f "${TRUSTED_DEVICE_FILE}" ]]; then
_warn "No trusted device record found. Run 'fresh-phone' to enroll this device."
return 0
fi
if ! _load_trusted_device_values; then
_fatal "Trusted device record exists but could not be read: ${TRUSTED_DEVICE_FILE}"
fi
saved_serial="${TRUSTED_SERIAL_LOADED:-}"
saved_model="${TRUSTED_MODEL_LOADED:-}"
saved_fingerprint="${TRUSTED_FINGERPRINT_LOADED:-}"
adb_collect_identity
if [[ -n "${saved_serial}" && "${DEVICE_SERIAL}" != "${saved_serial}" ]]; then
_fatal "Device identity mismatch: expected serial '${saved_serial}', got '${DEVICE_SERIAL}'. Refusing to proceed automatically."
fi
if [[ -n "${saved_model}" && "${DEVICE_MODEL}" != "${saved_model}" ]]; then
_fatal "Device identity mismatch: expected model '${saved_model}', got '${DEVICE_MODEL}'. Refusing to proceed automatically."
fi
if [[ -n "${saved_fingerprint}" && "${DEVICE_FINGERPRINT}" != "${saved_fingerprint}" ]]; then
_fatal "Device identity mismatch: expected fingerprint '${saved_fingerprint}', got '${DEVICE_FINGERPRINT}'. Refusing to proceed automatically."
fi
_info "Device identity verified: ${DEVICE_SERIAL} (${DEVICE_MODEL})"
}
_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}"
}

220
phone_focus_mode/lib/backup.sh Executable file
View File

@ -0,0 +1,220 @@
#!/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}"
}

483
phone_focus_mode/lib/monitor.sh Executable file
View File

@ -0,0 +1,483 @@
#!/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
readonly _MONITOR_REMOTE_DIR="/data/local/tmp/focus_mode"
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_HOSTS_TARGET="/system/etc/hosts"
readonly _MONITOR_BOOT_SCRIPT="/data/adb/service.d/99-focus-mode.sh"
readonly _MONITOR_LAUNCHER_PACKAGE="com.qqlabs.minimalistlauncher"
readonly _MONITOR_LAUNCHER_ACTIVITY_FILE="/data/local/tmp/focus_mode/minimalist_launcher.activity"
readonly _MONITOR_COMPANION_PACKAGE="com.kuhy.focusstatus"
readonly _MONITOR_DNS_CHAIN="FOCUS_DNS_BLOCK"
readonly _MONITOR_HOSTS_CANDIDATES="/system/etc/hosts /etc/hosts /vendor/etc/hosts /system/system/etc/hosts"
_mon_escape_json() {
local escaped="$1"
escaped=${escaped//\\/\\\\}
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' ' ' </proc/${pid}/cmdline | grep -q ${script_name}" >/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 ))
}
monitor_print_format_warning() {
local -a missing_indicators=("$@")
local -a box_lines=(
""
"The following expected components were NOT found:"
)
local indicator=""
for indicator in "${missing_indicators[@]}"; do
box_lines+=("${indicator}")
done
box_lines+=(
""
"This strongly suggests the phone was factory-reset or formatted."
""
"Next step: run the full recovery workflow:"
" ./scripts/run_all/run_phone.sh fresh-phone"
""
"Do NOT run 'auto' mode — it will not restore anything."
)
_box "PHONE APPEARS TO HAVE BEEN WIPED" "${box_lines[@]}"
}
_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}"
}
_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
}
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}"
{
printf '{"timestamp":"%s","device":"%s","checks":[\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$( _mon_escape_json "${ADB_SERIAL}" )"
paste -sd ',' "${tmp_checks}"
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 - "${report_path}" <<'PY'
import json
import sys
report_path = sys.argv[1]
with open(report_path, encoding="utf-8") as handle:
report = json.load(handle)
counts = {"ok": 0, "warn": 0, "error": 0, "fatal": 0}
issues = []
for check in report.get("checks", []):
status = check.get("status", "warn")
counts[status] = counts.get(status, 0) + 1
if status in {"warn", "error", "fatal"}:
issues.append((status, check.get("check", "unknown"), check.get("message", "")))
print("\n=== Monitoring Summary ===")
print(
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:
print("\nIssues found:")
for status, check_name, message in issues:
print(f" [{status}] {check_name}: {message}")
print("==========================\n")
PY
}
monitor_severity_exit() {
local snapshot_dir="$1"
local report_path="${snapshot_dir}/report.json"
[[ -f "${report_path}" ]] || return 0
python - "${report_path}" <<'PY'
import json
import sys
report_path = sys.argv[1]
with open(report_path, encoding="utf-8") as handle:
report = json.load(handle)
has_severe = any(
check.get("status") in {"fatal", "error"}
for check in report.get("checks", [])
)
raise SystemExit(1 if has_severe else 0)
PY
}

162
phone_focus_mode/lib/restore.sh Executable file
View File

@ -0,0 +1,162 @@
#!/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
}

View File

@ -0,0 +1,228 @@
#!/usr/bin/env bash
# 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'
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
}
test_box_does_not_crash() {
_box "Test title" "line 1" "line 2" >/dev/null 2>&1
}
test_check_cooldown_zero_allows() {
LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-zero"
adb_check_cooldown 0 "test_marker"
}
test_check_cooldown_blocks_when_marker_fresh() {
LAST_RUN_DIR="${TEST_TMPDIR}/cooldown-block"
mkdir -p "${LAST_RUN_DIR}"
date +%s >"${LAST_RUN_DIR}/fresh_marker"
if adb_check_cooldown 9999 "fresh_marker"; then
return 1
fi
return 0
}
test_mark_last_run_creates_marker() {
LAST_RUN_DIR="${TEST_TMPDIR}/mark-last-run"
adb_mark_last_run "run_test"
[[ -f "${LAST_RUN_DIR}/run_test" ]]
}
test_acquire_lock_creates_lock_directory() {
LOCK_DIR="${TEST_TMPDIR}/lock-dir-create"
LOCK_FILE=""
adb_acquire_lock >/dev/null 2>&1
[[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]]
_adb_release_lock
}
test_acquire_lock_removes_stale_lock_directory() {
LOCK_DIR="${TEST_TMPDIR}/lock-dir-stale"
LOCK_FILE="${LOCK_DIR}/run_phone.lock"
mkdir -p "${LOCK_FILE}"
printf '999999\n' >"${LOCK_FILE}/pid"
adb_acquire_lock >/dev/null 2>&1
[[ -d "${LOCK_FILE}" && -f "${LOCK_FILE}/pid" ]]
_adb_release_lock
}
test_sanitize_device_string_removes_dangerous_chars() {
local sanitized
sanitized="$(_sanitize_device_string $'abc DEF-._:/$`";\n')"
[[ "${sanitized}" == 'abc DEF-._:/' ]]
}
test_save_trusted_device_sanitizes_and_quotes() {
local trusted_contents
export ADB_SERIAL='SERIAL123$() ;'
adb_save_trusted_device
[[ -f "${TRUSTED_DEVICE_FILE}" ]] || return 1
trusted_contents="$(cat "${TRUSTED_DEVICE_FILE}")"
[[ "${trusted_contents}" != *'$('* ]]
[[ "${trusted_contents}" != *';'* ]]
[[ "${trusted_contents}" != *'`'* ]]
local -a trusted_values=()
mapfile -t trusted_values < <(
bash -c '
set -euo pipefail
source "$1"
printf "%s\n" "${TRUSTED_SERIAL:-}" "${TRUSTED_MODEL:-}" "${TRUSTED_FINGERPRINT:-}"
' bash "${TRUSTED_DEVICE_FILE}"
)
[[ "${trusted_values[0]:-}" == 'SERIAL123 ' ]]
[[ "${trusted_values[1]:-}" == 'Pixel 7rm -rf /dangerline2' ]]
[[ "${trusted_values[2]:-}" == 'google/pixel:14/UP1A.231005.007/evilcmd' ]]
}
test_verify_root_uses_root_shell() {
export ADB_SERIAL='SERIAL123'
adb_verify_root >/dev/null 2>&1
}
test_select_device_rejects_multiple_devices_even_with_trusted_record() {
unset ADB_SERIAL
adb_list_serials() {
printf 'SERIAL123\nSERIAL999\n'
}
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
TRUSTED_SERIAL='SERIAL123'
TRUSTED_MODEL='Pixel 7rm -rf /dangerline2'
TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd'
EOF
if (adb_select_device >/dev/null 2>&1); then
return 1
fi
return 0
}
test_verify_trusted_identity_accepts_exact_match() {
export ADB_SERIAL='SERIAL123'
adb_save_trusted_device
adb_verify_trusted_identity >/dev/null 2>&1
}
test_verify_trusted_identity_rejects_model_mismatch() {
export ADB_SERIAL='SERIAL123'
adb_save_trusted_device
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
TRUSTED_SERIAL='SERIAL123'
TRUSTED_MODEL='Different Model'
TRUSTED_FINGERPRINT='google/pixel:14/UP1A.231005.007/evilcmd'
EOF
if (adb_verify_trusted_identity >/dev/null 2>&1); then
return 1
fi
return 0
}
test_verify_trusted_identity_rejects_fingerprint_mismatch() {
export ADB_SERIAL='SERIAL123'
adb_save_trusted_device
cat >"${TRUSTED_DEVICE_FILE}" <<'EOF'
TRUSTED_SERIAL='SERIAL123'
TRUSTED_MODEL='Pixel 7rm -rf /dangerline2'
TRUSTED_FINGERPRINT='different/fingerprint'
EOF
if (adb_verify_trusted_identity >/dev/null 2>&1); then
return 1
fi
return 0
}
run_test "_box output without crash" test_box_does_not_crash
run_test "adb_check_cooldown 0 returns 0 (proceed)" test_check_cooldown_zero_allows
run_test "adb_check_cooldown blocks when marker is fresh" test_check_cooldown_blocks_when_marker_fresh
run_test "adb_mark_last_run creates marker file" test_mark_last_run_creates_marker
run_test "adb_acquire_lock creates atomic lock directory" test_acquire_lock_creates_lock_directory
run_test "adb_acquire_lock replaces stale lock directory" test_acquire_lock_removes_stale_lock_directory
run_test "_sanitize_device_string strips dangerous characters" test_sanitize_device_string_removes_dangerous_chars
run_test "adb_save_trusted_device sanitizes and safely quotes values" test_save_trusted_device_sanitizes_and_quotes
run_test "adb_verify_root succeeds when root shell returns ok" test_verify_root_uses_root_shell
run_test "adb_select_device rejects multiple devices even with trusted record" test_select_device_rejects_multiple_devices_even_with_trusted_record
run_test "adb_verify_trusted_identity accepts exact saved identity" test_verify_trusted_identity_accepts_exact_match
run_test "adb_verify_trusted_identity rejects model mismatch" test_verify_trusted_identity_rejects_model_mismatch
run_test "adb_verify_trusted_identity rejects fingerprint mismatch" test_verify_trusted_identity_rejects_fingerprint_mismatch
printf '\nResults: %d passed, %d failed\n' "${PASS}" "${FAIL}"
[[ "${FAIL}" -eq 0 ]]

View File

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Unit tests for dns_enforcer.sh helper functions.
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"
}
TMPDIR_TEST="$(mktemp -d)"
trap 'rm -rf "${TMPDIR_TEST}"' EXIT
cat >"${TMPDIR_TEST}/config.sh" <<EOF
#!/system/bin/sh
export STATE_DIR="${TMPDIR_TEST}"
export MODE_FILE="${TMPDIR_TEST}/current_mode.txt"
export DNS_LOG="${TMPDIR_TEST}/dns.log"
export DNS_IPT_CHAIN="FOCUS_DNS_BLOCK"
export DNS_CHECK_INTERVAL=20
export DNS_DOH_IPV4=""
export DNS_DOH_IPV6=""
export DNS_BLOCK_HOSTS=""
export DNS_BLOCK_PACKAGES_ALWAYS=""
export DNS_BLOCK_PACKAGES_FOCUS_ONLY=""
EOF
export FOCUS_MODE_DNS_ENFORCER_TESTING=1
export FOCUS_MODE_SCRIPT_DIR="${TMPDIR_TEST}"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/../../dns_enforcer.sh"
line_ipv4='Ping youtube.com (142.251.98.190): 56(84) bytes.'
line_ipv6='Ping youtube.com (2a00:1450:4025:800::5b): 56(84) bytes.'
if [[ "$(extract_ping_ip "$line_ipv4")" == "142.251.98.190" ]]; then
_t_pass "extract_ping_ip parses IPv4"
else
_t_fail "extract_ping_ip failed for IPv4"
fi
if [[ "$(extract_ping_ip "$line_ipv6")" == "2a00:1450:4025:800::5b" ]]; then
_t_pass "extract_ping_ip parses IPv6"
else
_t_fail "extract_ping_ip failed for IPv6"
fi
pkg_line='package:com.android.chrome uid:10153'
if [[ "$(extract_package_uid "$pkg_line")" == "10153" ]]; then
_t_pass "extract_package_uid parses uid from package line"
else
_t_fail "extract_package_uid failed for package line"
fi
bad_pkg_line='package:com.android.chrome'
if [[ -z "$(extract_package_uid "$bad_pkg_line")" ]]; then
_t_pass "extract_package_uid returns empty when uid is missing"
else
_t_fail "extract_package_uid should return empty when uid missing"
fi
block_file="${TMPDIR_TEST}/block.txt"
: >"$block_file"
append_unique_line "$block_file" "1.2.3.4"
append_unique_line "$block_file" "1.2.3.4"
append_unique_line "$block_file" "5.6.7.8"
if [[ "$(wc -l < "$block_file" | tr -d ' ')" == "2" ]]; then
_t_pass "append_unique_line de-duplicates values"
else
_t_fail "append_unique_line should avoid duplicates"
fi
printf '\nResults: %d passed, %d failed\n' "$PASS" "$FAIL"
[[ "$FAIL" -eq 0 ]]

View File

@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Unit tests for magisk_service.sh boot safety helpers.
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"
}
TMPDIR_TEST="$(mktemp -d)"
trap 'rm -rf "${TMPDIR_TEST}"' EXIT
cat >"${TMPDIR_TEST}/config.sh" <<EOF
#!/system/bin/sh
export FOCUS_BOOT_AUTOSTART=0
export LAUNCHER_BOOT_AUTOSTART=0
export FOCUS_BOOT_DELAY_SECONDS=10
export FOCUS_BOOT_EMERGENCY_DISABLE_FILE="${TMPDIR_TEST}/disable_boot_autostart"
export LAUNCHER_APK="${TMPDIR_TEST}/minimalist_launcher.apk"
export LAUNCHER_ACTIVITY_FILE="${TMPDIR_TEST}/minimalist_launcher.activity"
EOF
export FOCUS_MODE_MAGISK_SERVICE_TESTING=1
export FOCUS_MODE_SCRIPT_DIR="${TMPDIR_TEST}"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/../../magisk_service.sh"
if boot_config_ready; then
_t_pass "boot config is ready when config.sh exists"
else
_t_fail "boot config should be ready when config.sh exists"
fi
if ! should_start_launcher_enforcer; then
_t_pass "launcher boot autostart defaults to disabled"
else
_t_fail "launcher boot autostart should be disabled by default"
fi
if ! should_start_boot_stack; then
_t_pass "global boot autostart defaults to disabled"
else
_t_fail "global boot autostart should default to disabled"
fi
if [ "$(boot_delay_seconds)" = "10" ]; then
_t_pass "boot delay defaults to 10 seconds"
else
_t_fail "boot delay should default to 10 seconds"
fi
if ! boot_emergency_disabled; then
_t_pass "boot emergency disable defaults to off"
else
_t_fail "boot emergency disable should be off when marker missing"
fi
touch "${TMPDIR_TEST}/disable_boot_autostart"
if boot_emergency_disabled; then
_t_pass "boot emergency disable activates when marker file exists"
else
_t_fail "boot emergency disable should activate with marker file"
fi
rm -f "${TMPDIR_TEST}/disable_boot_autostart"
mv "${TMPDIR_TEST}/config.sh" "${TMPDIR_TEST}/config.sh.bak"
if ! boot_config_ready; then
_t_pass "boot config is not ready when config.sh is missing"
else
_t_fail "boot config should be missing when config.sh is absent"
fi
mv "${TMPDIR_TEST}/config.sh.bak" "${TMPDIR_TEST}/config.sh"
printf 'apk' >"${TMPDIR_TEST}/minimalist_launcher.apk"
printf 'pkg/.Activity' >"${TMPDIR_TEST}/minimalist_launcher.activity"
cat >"${TMPDIR_TEST}/config.sh" <<EOF
#!/system/bin/sh
export FOCUS_BOOT_AUTOSTART=1
export LAUNCHER_BOOT_AUTOSTART=1
export FOCUS_BOOT_DELAY_SECONDS=999
export FOCUS_BOOT_EMERGENCY_DISABLE_FILE="${TMPDIR_TEST}/disable_boot_autostart"
export LAUNCHER_APK="${TMPDIR_TEST}/minimalist_launcher.apk"
export LAUNCHER_ACTIVITY_FILE="${TMPDIR_TEST}/minimalist_launcher.activity"
EOF
if should_start_boot_stack; then
_t_pass "global boot autostart can be explicitly enabled"
else
_t_fail "global boot autostart should enable when configured"
fi
if [ "$(boot_delay_seconds)" = "10" ]; then
_t_pass "boot delay is capped at 10 seconds"
else
_t_fail "boot delay should clamp to 10 seconds"
fi
if should_start_launcher_enforcer; then
_t_pass "launcher boot autostart enables only with snapshot artifacts present"
else
_t_fail "launcher boot autostart should enable when explicitly armed with artifacts"
fi
rm -f "${TMPDIR_TEST}/minimalist_launcher.activity"
if ! should_start_launcher_enforcer; then
_t_pass "launcher boot autostart stays off when activity snapshot is missing"
else
_t_fail "launcher boot autostart should refuse missing activity snapshot"
fi
printf '\nResults: %d passed, %d failed\n' "${PASS}" "${FAIL}"
[[ "${FAIL}" -eq 0 ]]

View File

@ -0,0 +1,154 @@
#!/usr/bin/env bash
# Unit tests for monitor.sh helpers that do not require a real device.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
adb_root_shell() {
return 1
}
_info() {
:
}
_warn() {
:
}
_error() {
:
}
_fatal() {
printf 'FATAL: %s\n' "$*" >&2
exit 1
}
_box() {
:
}
FORMAT_INDICATORS=()
FORMAT_DETECTION_MIN_MISSING=2
BATTERY_WARN_BELOW=20
STORAGE_WARN_BELOW_MB=500
ADB_SERIAL="test-serial"
: "${FORMAT_DETECTION_MIN_MISSING}"
: "${BATTERY_WARN_BELOW}"
: "${STORAGE_WARN_BELOW_MB}"
: "${ADB_SERIAL}"
: "${FORMAT_INDICATORS[*]}"
source "${SCRIPT_DIR}/../monitor.sh"
PASS=0
FAIL=0
_t_pass() {
PASS=$((PASS + 1))
printf ' OK: %s\n' "$1"
}
_t_fail() {
FAIL=$((FAIL + 1))
printf ' FAIL: %s\n' "$1"
}
TMPDIR_TEST="$(mktemp -d)"
trap 'rm -rf "${TMPDIR_TEST}"' EXIT
line="$(_mon_check "test_check" "ok" "some_cmd" "all good" "false")"
if [[ "${line}" == *'"check":"test_check"'* ]]; then
_t_pass "_mon_check outputs check name"
else
_t_fail "_mon_check missing check name"
fi
if [[ "${line}" == *'"status":"ok"'* ]]; then
_t_pass "_mon_check outputs status"
else
_t_fail "_mon_check missing status"
fi
if [[ -z "$(_monitor_resolve_hosts_target)" ]]; then
_t_pass "_monitor_resolve_hosts_target returns empty when no hosts file exists"
else
_t_fail "_monitor_resolve_hosts_target should be empty when all candidates are missing"
fi
adb_root_shell() {
case "$*" in
'test -f /system/etc/hosts') return 1 ;;
'test -f /etc/hosts') return 1 ;;
'test -f /vendor/etc/hosts') return 0 ;;
'test -f /system/system/etc/hosts') return 1 ;;
*) return 1 ;;
esac
}
if [[ "$(_monitor_resolve_hosts_target)" == "/vendor/etc/hosts" ]]; then
_t_pass "_monitor_resolve_hosts_target picks first existing candidate"
else
_t_fail "_monitor_resolve_hosts_target did not return expected candidate"
fi
adb_root_shell() {
return 1
}
if ! monitor_is_formatted 2>/dev/null; then
_t_pass "monitor_is_formatted returns 1 when no indicators are missing"
else
_t_fail "monitor_is_formatted should return 1 with empty FORMAT_INDICATORS"
fi
if monitor_severity_exit "${TMPDIR_TEST}/nonexistent"; then
_t_pass "monitor_severity_exit returns 0 when no report exists"
else
_t_fail "monitor_severity_exit should return 0 for missing report"
fi
printf '%s\n' '{"checks":[{"check":"x","status":"fatal","source":"s","message":"m","repairable":false}]}' > "${TMPDIR_TEST}/report.json"
if ! monitor_severity_exit "${TMPDIR_TEST}"; then
_t_pass "monitor_severity_exit returns 1 on fatal status"
else
_t_fail "monitor_severity_exit should return 1 on fatal status"
fi
printf '%s\n' '{"checks":[{"check":"x","status":"ok","source":"s","message":"mentions fatal and error in text","repairable":false}]}' > "${TMPDIR_TEST}/report.json"
if monitor_severity_exit "${TMPDIR_TEST}"; then
_t_pass "monitor_severity_exit ignores free-text fatal/error words in message"
else
_t_fail "monitor_severity_exit should only inspect the JSON status field"
fi
FORMAT_INDICATORS=(
"present|test -f /present"
"missing-one|test -f /missing-one"
"missing-two|test -f /missing-two"
)
: "${FORMAT_INDICATORS[*]}"
adb_root_shell() {
case "$*" in
'test -f /present')
return 0
;;
*)
return 1
;;
esac
}
mapfile -t missing_indicators < <(monitor_check_format_indicators)
if [[ ${#missing_indicators[@]} -eq 2 ]] && [[ "${missing_indicators[0]}" == "missing-one" ]] && [[ "${missing_indicators[1]}" == "missing-two" ]]; then
_t_pass "monitor_check_format_indicators prints only missing indicator names"
else
_t_fail "monitor_check_format_indicators did not return the expected missing indicators"
fi
printf '\nResults: %d passed, %d failed\n' "${PASS}" "${FAIL}"
[[ "${FAIL}" -eq 0 ]]

View File

@ -6,17 +6,155 @@
# Magisk executes everything in service.d on boot with root.
# ============================================================
# Wait for system to be fully booted before starting daemons
sleep 120
set -eu
SCRIPT_DIR="/data/local/tmp/focus_mode"
SCRIPT_DIR="${FOCUS_MODE_SCRIPT_DIR:-/data/local/tmp/focus_mode}"
# Ensure scripts are executable
chmod +x "$SCRIPT_DIR/focus_daemon.sh"
chmod +x "$SCRIPT_DIR/focus_ctl.sh"
chmod +x "$SCRIPT_DIR/hosts_enforcer.sh"
chmod +x "$SCRIPT_DIR/dns_enforcer.sh"
chmod +x "$SCRIPT_DIR/launcher_enforcer.sh"
load_launcher_config() {
if [ -f "$SCRIPT_DIR/config.sh" ]; then
export FOCUS_MODE_SCRIPT_DIR="$SCRIPT_DIR"
# shellcheck source=/dev/null
. "$SCRIPT_DIR/config.sh"
return 0
fi
return 1
}
boot_config_ready() {
[ -f "$SCRIPT_DIR/config.sh" ]
}
launcher_boot_autostart_enabled() {
load_launcher_config || return 1
[ "${LAUNCHER_BOOT_AUTOSTART:-0}" = "1" ]
}
launcher_boot_snapshot_ready() {
load_launcher_config || return 1
[ -s "${LAUNCHER_APK:-}" ] && [ -s "${LAUNCHER_ACTIVITY_FILE:-}" ]
}
should_start_boot_stack() {
load_launcher_config || return 1
[ "${FOCUS_BOOT_AUTOSTART:-0}" = "1" ]
}
boot_delay_seconds() {
load_launcher_config || {
echo 10
return 0
}
raw_delay="${FOCUS_BOOT_DELAY_SECONDS:-10}"
case "$raw_delay" in
''|*[!0-9]*)
echo 10
return 0
;;
esac
# Safety cap requested by user: keep post-boot delay short.
if [ "$raw_delay" -gt 10 ]; then
echo 10
return 0
fi
echo "$raw_delay"
}
boot_emergency_disable_file() {
load_launcher_config || {
echo "$SCRIPT_DIR/disable_boot_autostart"
return 0
}
echo "${FOCUS_BOOT_EMERGENCY_DISABLE_FILE:-$SCRIPT_DIR/disable_boot_autostart}"
}
boot_emergency_disabled() {
marker_file="$(boot_emergency_disable_file)"
[ -f "$marker_file" ]
}
wait_for_boot_completed() {
elapsed=0
max_wait="${FOCUS_BOOT_WAIT_MAX_SECONDS:-180}"
while [ "$elapsed" -lt "$max_wait" ]; do
if [ "$(getprop sys.boot_completed 2>/dev/null || true)" = "1" ]; then
return 0
fi
sleep 1
elapsed=$((elapsed + 1))
done
return 1
}
wait_for_boot_config() {
elapsed=0
max_wait="${FOCUS_BOOT_WAIT_MAX_SECONDS:-180}"
while [ "$elapsed" -lt "$max_wait" ]; do
if boot_config_ready; then
return 0
fi
sleep 1
elapsed=$((elapsed + 1))
done
return 1
}
should_start_launcher_enforcer() {
launcher_boot_autostart_enabled && launcher_boot_snapshot_ready
}
safe_chmod() {
if [ -f "$1" ]; then
chmod +x "$1"
fi
}
start_launcher_enforcer_if_safe() {
if should_start_launcher_enforcer; then
setsid sh "$SCRIPT_DIR/launcher_enforcer.sh" </dev/null >/dev/null 2>&1 &
return 0
fi
return 1
}
main() {
if ! wait_for_boot_config; then
exit 0
fi
if ! should_start_boot_stack; then
exit 0
fi
if boot_emergency_disabled; then
exit 0
fi
if ! wait_for_boot_completed; then
exit 0
fi
sleep "$(boot_delay_seconds)"
if boot_emergency_disabled; then
exit 0
fi
# Ensure scripts are executable.
safe_chmod "$SCRIPT_DIR/focus_daemon.sh"
safe_chmod "$SCRIPT_DIR/focus_ctl.sh"
safe_chmod "$SCRIPT_DIR/hosts_enforcer.sh"
safe_chmod "$SCRIPT_DIR/dns_enforcer.sh"
safe_chmod "$SCRIPT_DIR/launcher_enforcer.sh"
# Start hosts enforcer FIRST - it must bind-mount the hosts file before
# the user has a chance to exploit it. This runs even outside focus mode
@ -28,11 +166,17 @@ setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" </dev/null >/dev/null 2>&1 &
# bypass it (e.g. Chrome's built-in secure DNS). Always on.
setsid sh "$SCRIPT_DIR/dns_enforcer.sh" </dev/null >/dev/null 2>&1 &
# Start launcher enforcer - keeps Minimalist Phone installed and pinned as
# the default HOME. Always on (not location-gated).
setsid sh "$SCRIPT_DIR/launcher_enforcer.sh" </dev/null >/dev/null 2>&1 &
# Start launcher enforcer only when boot autostart is explicitly enabled
# and a valid launcher snapshot exists. This avoids boot loops or a blank
# HOME screen caused by stale launcher state after OTA updates/resets.
start_launcher_enforcer_if_safe || true
# Start focus daemon in a new session (detached from any controlling terminal)
# Start focus daemon in a new session (detached from any controlling terminal).
setsid sh "$SCRIPT_DIR/focus_daemon.sh" </dev/null >/dev/null 2>&1 &
exit 0
}
if [ "${FOCUS_MODE_MAGISK_SERVICE_TESTING:-0}" != "1" ]; then
main "$@"
fi

229
phone_focus_mode/run_phone.sh Executable file
View File

@ -0,0 +1,229 @@
#!/usr/bin/env bash
# run_phone.sh — Phone focus mode orchestrator.
#
# Usage:
# run_phone.sh [auto] Everyday: backup, monitor, repair minor drift.
# If the phone looks formatted, shows a warning
# and exits — does nothing else.
# run_phone.sh fresh-phone Full recovery after a factory reset.
# run_phone.sh backup Incremental backup only.
# run_phone.sh monitor Health and security snapshot only.
# run_phone.sh doctor Diagnose and repair drift without data restore.
# run_phone.sh --help | -h Show this help.
#
# Environment variables:
# ADB_SERIAL Target a specific device by serial.
# PHONE_BACKUP_ROOT Override backup root (default: ~/phone_backups).
# PHONE_IP Wireless ADB host (used by deploy.sh when ADB_SERIAL unset).
set -euo pipefail
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/adb_common.sh
source "${_SCRIPT_DIR}/lib/adb_common.sh"
# shellcheck source=backup_manifest.sh
source "${_SCRIPT_DIR}/backup_manifest.sh"
# shellcheck source=lib/monitor.sh
source "${_SCRIPT_DIR}/lib/monitor.sh"
# shellcheck source=lib/backup.sh
source "${_SCRIPT_DIR}/lib/backup.sh"
# shellcheck source=lib/restore.sh
source "${_SCRIPT_DIR}/lib/restore.sh"
SUBCOMMAND="${1:-auto}"
shift 2>/dev/null || true
case "${SUBCOMMAND}" in
--help|-h|help)
grep '^#' "${BASH_SOURCE[0]}" | grep -v '^#!/' | grep -v '^# shellcheck ' | sed 's/^# \?//'
exit 0
;;
auto|fresh-phone|backup|monitor|doctor) ;;
*)
_fatal "Unknown subcommand: '${SUBCOMMAND}'. Run with --help for usage."
;;
esac
_setup_common() {
adb_select_device "${ADB_SERIAL:-}"
adb_verify_root
adb_verify_trusted_identity
adb_collect_identity
}
_auto_repair_minor_drift() {
local snapshot_dir="$1"
local report="${snapshot_dir}/report.json"
local repaired=0
[[ -f "${report}" ]] || return 0
if grep -q '"check":"focus_daemon","status":"error"' "${report}" 2>/dev/null; then
if adb_root_shell "test -x /data/adb/service.d/99-focus-mode.sh" >/dev/null 2>&1; then
_info "Repair: restarting focus daemons via boot script"
adb_root_shell "sh /data/adb/service.d/99-focus-mode.sh" >/dev/null 2>&1 || true
repaired=$(( repaired + 1 ))
else
_warn "Cannot restart daemons: boot script missing. Run 'fresh-phone' or 'doctor'."
fi
fi
[[ ${repaired} -gt 0 ]] && _info "Minor repairs applied: ${repaired}" || true
}
cmd_auto() {
adb_acquire_lock
if ! adb_check_cooldown "${COOLDOWN_AUTO_SECS}" "auto"; then
_info "auto: cooldown active, nothing to do."
exit 0
fi
_setup_common
# Detect fresh format FIRST and exit immediately if detected.
local -a missing
mapfile -t missing < <(monitor_check_format_indicators)
local missing_count="${#missing[@]}"
if (( missing_count >= FORMAT_DETECTION_MIN_MISSING )); then
monitor_print_format_warning "${missing[@]}"
exit 2
fi
local device_id="${ADB_SERIAL//[^a-zA-Z0-9_-]/_}"
local snapshot_dir=""
snapshot_dir="${PHONE_BACKUP_ROOT}/${device_id}/monitoring/$(date -u +%Y%m%dT%H%M%SZ)"
monitor_collect_snapshot "${snapshot_dir}"
backup_run_incremental "${device_id}"
_auto_repair_minor_drift "${snapshot_dir}"
monitor_print_summary "${snapshot_dir}"
adb_mark_last_run "auto"
monitor_severity_exit "${snapshot_dir}" || exit 1
}
cmd_fresh_phone() {
_info "=== fresh-phone: Full recovery mode ==="
adb_select_device "${ADB_SERIAL:-}"
restore_verify_prerequisites
adb_collect_identity
local device_id="${ADB_SERIAL//[^a-zA-Z0-9_-]/_}"
local backup_root="${PHONE_BACKUP_ROOT}/${device_id}/latest"
local has_backup=0
if [[ -d "${backup_root}" ]]; then
has_backup=1
else
_warn "No backup found at ${backup_root}. Proceeding with security-stack-only setup."
fi
local pre_snapshot=""
pre_snapshot="${PHONE_BACKUP_ROOT}/${device_id}/monitoring/pre_restore_$(date -u +%Y%m%dT%H%M%SZ)"
monitor_collect_snapshot "${pre_snapshot}" || true
# Delegate security restore to deploy.sh via restore helper.
restore_security_stack
if (( has_backup == 1 )); then
restore_apks "${backup_root}"
restore_media "${backup_root}"
else
_warn "Skipping APK/media restore because no backup snapshot is available."
fi
local post_snapshot=""
post_snapshot="${PHONE_BACKUP_ROOT}/${device_id}/monitoring/post_restore_$(date -u +%Y%m%dT%H%M%SZ)"
monitor_collect_snapshot "${post_snapshot}" || true
monitor_print_summary "${post_snapshot}"
adb_save_trusted_device
if (( has_backup == 1 )); then
restore_print_manual_steps "${backup_root}"
else
_box "BACKUP RESTORE SKIPPED" \
"" \
"Security stack deployment completed, but no backup snapshot was found." \
"Next steps:" \
" ▶ Run './scripts/run_all/run_phone.sh backup' after stabilization" \
" ▶ Reinstall required apps that are not part of deploy assets" \
" ▶ Reconfigure app data manually"
fi
}
cmd_backup() {
_setup_common
local device_id="${ADB_SERIAL//[^a-zA-Z0-9_-]/_}"
backup_run_incremental "${device_id}"
}
cmd_monitor() {
_setup_common
local device_id="${ADB_SERIAL//[^a-zA-Z0-9_-]/_}"
local snapshot_dir=""
snapshot_dir="${PHONE_BACKUP_ROOT}/${device_id}/monitoring/$(date -u +%Y%m%dT%H%M%SZ)"
monitor_collect_snapshot "${snapshot_dir}"
monitor_print_summary "${snapshot_dir}"
monitor_severity_exit "${snapshot_dir}" || exit 1
}
cmd_doctor() {
_setup_common
local device_id="${ADB_SERIAL//[^a-zA-Z0-9_-]/_}"
local snapshot_dir=""
snapshot_dir="${PHONE_BACKUP_ROOT}/${device_id}/monitoring/doctor_$(date -u +%Y%m%dT%H%M%SZ)"
local report=""
local repaired=0
monitor_collect_snapshot "${snapshot_dir}"
report="${snapshot_dir}/report.json"
local daemon=""
for daemon in focus_daemon hosts_enforcer dns_enforcer launcher_enforcer; do
if grep -q "\"check\":\"${daemon}\",\"status\":\"error\"" "${report}" 2>/dev/null; then
_info "Doctor: restarting ${daemon}"
adb_root_shell "pgrep -f ${daemon}.sh | xargs kill -9 2>/dev/null || true" >/dev/null 2>&1 || true
adb_root_shell "nohup sh /data/adb/focus_mode/${daemon}.sh </dev/null >/dev/null 2>&1 &" >/dev/null 2>&1 || \
_warn "Could not restart ${daemon}"
repaired=$(( repaired + 1 ))
fi
done
# Re-deploy is allowed only for managed security-stack drift.
if grep -q '"check":"boot_persistence","status":"fatal"' "${report}" 2>/dev/null; then
_info "Doctor: boot script missing — re-running deploy.sh"
restore_security_stack
repaired=$(( repaired + 1 ))
fi
if grep -q '"check":"hosts_integrity","status":"error"' "${report}" 2>/dev/null; then
if [[ -f "${PHONE_BACKUP_ROOT}/${device_id}/latest/security_state/data/local/tmp/focus_mode/hosts.canonical" ]]; then
_info "Doctor: restoring canonical hosts from backup"
adb_cmd push \
"${PHONE_BACKUP_ROOT}/${device_id}/latest/security_state/data/local/tmp/focus_mode/hosts.canonical" \
"/data/local/tmp/focus_mode/hosts.canonical"
repaired=$(( repaired + 1 ))
else
_warn "Doctor: hosts integrity failed but no backup copy available. Run fresh-phone."
fi
fi
monitor_collect_snapshot "${snapshot_dir}_after"
monitor_print_summary "${snapshot_dir}_after"
_info "Doctor complete. Repairs applied: ${repaired}"
monitor_severity_exit "${snapshot_dir}_after" || {
_warn "Unresolved issues remain after doctor run."
exit 1
}
}
case "${SUBCOMMAND}" in
auto) cmd_auto ;;
fresh-phone) cmd_fresh_phone ;;
backup) cmd_backup ;;
monitor) cmd_monitor ;;
doctor) cmd_doctor ;;
esac

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
# install_pc_phone_automation.sh — Install user-level systemd automation for
# periodic phone sync. Runs as the current user (no sudo required).
set -euo pipefail
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_SYSTEMD_USER_DIR="${HOME}/.config/systemd/user"
mkdir -p "${_SYSTEMD_USER_DIR}"
cp "${_SCRIPT_DIR}/phone-auto-sync.service" "${_SYSTEMD_USER_DIR}/"
cp "${_SCRIPT_DIR}/phone-auto-sync.timer" "${_SYSTEMD_USER_DIR}/"
systemctl --user daemon-reload
systemctl --user enable --now phone-auto-sync.timer
printf 'Installed and enabled phone-auto-sync.timer\n'
printf 'Next run: '
systemctl --user list-timers phone-auto-sync.timer --no-legend | awk '{print $1, $2}' || \
printf '(check with: systemctl --user list-timers)\n'

View File

@ -0,0 +1,9 @@
[Unit]
Description=Phone focus mode auto sync
After=network.target
[Service]
Type=oneshot
ExecStart=%h/testsAndMisc/scripts/run_all/run_phone.sh auto
StandardOutput=journal
StandardError=journal

View File

@ -0,0 +1,10 @@
[Unit]
Description=Run phone focus mode auto sync periodically
[Timer]
OnBootSec=5min
OnUnitActiveSec=30min
Persistent=true
[Install]
WantedBy=timers.target

20
run.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
# Easy entrypoint for system usage reports.
# Usage:
# ./run.sh # today's report to stdout
# ./run.sh --date 20260501 # specific day
# ./run.sh --top 25 # override row count
#
# Any args are forwarded to usage_report.py unchanged.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPORT_SCRIPT="$SCRIPT_DIR/linux_configuration/scripts/system-maintenance/bin/usage_report.py"
if [[ ! -f "$REPORT_SCRIPT" ]]; then
echo "Error: usage_report.py not found at: $REPORT_SCRIPT" >&2
exit 1
fi
exec python3 "$REPORT_SCRIPT" "$@"

22
scripts/run_all/run_phone.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# run_phone.sh — Visible entrypoint for the phone focus mode workflow.
#
# Quick reference:
# ./scripts/run_all/run_phone.sh Everyday: backup + monitor + minor repair.
# Shows a warning if the phone was wiped.
# ./scripts/run_all/run_phone.sh fresh-phone Full recovery after a factory reset.
# ./scripts/run_all/run_phone.sh doctor Diagnose and repair security drift.
# ./scripts/run_all/run_phone.sh backup Incremental backup only.
# ./scripts/run_all/run_phone.sh monitor Health snapshot only.
# ./scripts/run_all/run_phone.sh --help Show full usage.
set -euo pipefail
_REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
_IMPL="${_REPO_ROOT}/phone_focus_mode/run_phone.sh"
if [[ ! -x "${_IMPL}" ]]; then
printf 'ERROR: implementation script not found or not executable: %s\n' "${_IMPL}" >&2
exit 1
fi
exec bash "${_IMPL}" "$@"