mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 11:43:10 +02:00
feat(phone-focus): add recovery workflow, automation scripts, and docs
This commit is contained in:
parent
b278d22750
commit
589e059eee
155
.github/skills/phone-focus-mode/SKILL.md
vendored
Normal file
155
.github/skills/phone-focus-mode/SKILL.md
vendored
Normal 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/...`
|
||||
2053
docs/superpowers/plans/2026-05-01-phone-focus-recovery.md
Normal file
2053
docs/superpowers/plans/2026-05-01-phone-focus-recovery.md
Normal file
File diff suppressed because it is too large
Load Diff
709
docs/superpowers/specs/2026-05-01-phone-focus-recovery-design.md
Normal file
709
docs/superpowers/specs/2026-05-01-phone-focus-recovery-design.md
Normal 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 workspace’s 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 app’s 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.
|
||||
@ -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
|
||||
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:
|
||||
|
||||
Open Google Maps, right-click your apartment → copy the coordinates shown.
|
||||
```bash
|
||||
bash phone_focus_mode/deploy.sh <phone_ip>
|
||||
```
|
||||
|
||||
### 2. Edit `config_secrets.sh`
|
||||
## Systemd automation (PC user service)
|
||||
|
||||
```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:
|
||||
Install timer-based periodic runs:
|
||||
|
||||
```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/systemd/install_pc_phone_automation.sh
|
||||
```
|
||||
|
||||
Then add the correct package name to `WHITELIST` in `config.sh`.
|
||||
This installs user units under `~/.config/systemd/user/`:
|
||||
|
||||
### 4. Deploy
|
||||
- `phone-auto-sync.service`
|
||||
- `phone-auto-sync.timer` (every 30 minutes, persistent)
|
||||
|
||||
```bash
|
||||
chmod +x deploy.sh
|
||||
./deploy.sh 192.168.1.42 # replace with your phone's IP
|
||||
```
|
||||
## Relevant files
|
||||
|
||||
This:
|
||||
| File | Purpose |
|
||||
| ------------------------------------- | ------------------------------------------ |
|
||||
| `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 |
|
||||
|
||||
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
|
||||
## Notes
|
||||
|
||||
## 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
|
||||
|
||||
| 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 |
|
||||
|
||||
## Hosts hardening
|
||||
|
||||
A second daemon, `hosts_enforcer.sh`, locks the phone's `/system/etc/hosts`
|
||||
to the same blocklist installed by `linux_configuration/hosts/install.sh`
|
||||
on the PC. Three layers:
|
||||
|
||||
1. Canonical copy at `/data/adb/focus_mode/hosts.canonical` is `chattr +i`.
|
||||
2. It is bind-mounted read-only over `/system/etc/hosts` at boot.
|
||||
3. A watchdog verifies a sha256 every 15 seconds and restores on mismatch.
|
||||
|
||||
This blocks the common `echo > /etc/hosts` one-liner from a terminal app.
|
||||
It is NOT a guarantee against a determined root user on the device itself —
|
||||
a real "impossible without USB" gate would require removing `su` access,
|
||||
which would break the rest of this system. The watchdog at least ensures
|
||||
tampering is logged and reverted within ~15s.
|
||||
|
||||
Status and logs:
|
||||
|
||||
```bash
|
||||
./deploy.sh <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`.
|
||||
|
||||
85
phone_focus_mode/backup_manifest.sh
Executable file
85
phone_focus_mode/backup_manifest.sh
Executable 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
|
||||
@ -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 ---
|
||||
|
||||
@ -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"
|
||||
adb_root "cp /data/local/tmp/focus_stage/99-focus-mode.sh /data/adb/service.d/99-focus-mode.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
|
||||
adb_root "chmod 755 /data/adb/service.d/99-focus-mode.sh"
|
||||
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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
main "$@"
|
||||
if [ "${FOCUS_MODE_DNS_ENFORCER_TESTING:-0}" != "1" ]; then
|
||||
main "$@"
|
||||
fi
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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"
|
||||
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null \
|
||||
|| echo "$pkg" >> "$DISABLED_APPS_FILE"
|
||||
newly_disabled=$((newly_disabled + 1))
|
||||
fi
|
||||
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
||||
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null \
|
||||
|| echo "$pkg" >> "$DISABLED_APPS_FILE"
|
||||
newly_disabled=$((newly_disabled + 1))
|
||||
fi
|
||||
done < "$blocked_sys"
|
||||
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))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -87,8 +87,8 @@ public final class StatusService extends Service {
|
||||
return;
|
||||
}
|
||||
NotificationChannel ch = new NotificationChannel(
|
||||
CHANNEL_ID, "Focus Mode Status",
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
CHANNEL_ID, "Focus Mode Status",
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
ch.setDescription("Persistent status of the focus-mode daemon");
|
||||
ch.setShowBadge(false);
|
||||
ch.setSound(null, null);
|
||||
|
||||
@ -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"
|
||||
|
||||
262
phone_focus_mode/lib/adb_common.sh
Executable file
262
phone_focus_mode/lib/adb_common.sh
Executable 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
220
phone_focus_mode/lib/backup.sh
Executable 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
483
phone_focus_mode/lib/monitor.sh
Executable 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
162
phone_focus_mode/lib/restore.sh
Executable 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
|
||||
}
|
||||
228
phone_focus_mode/lib/tests/test_adb_common.sh
Executable file
228
phone_focus_mode/lib/tests/test_adb_common.sh
Executable 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 ]]
|
||||
83
phone_focus_mode/lib/tests/test_dns_enforcer.sh
Executable file
83
phone_focus_mode/lib/tests/test_dns_enforcer.sh
Executable 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 ]]
|
||||
121
phone_focus_mode/lib/tests/test_magisk_service.sh
Executable file
121
phone_focus_mode/lib/tests/test_magisk_service.sh
Executable 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 ]]
|
||||
154
phone_focus_mode/lib/tests/test_monitor.sh
Executable file
154
phone_focus_mode/lib/tests/test_monitor.sh
Executable 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 ]]
|
||||
@ -6,33 +6,177 @@
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# because hosts hardening should always be active.
|
||||
setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" </dev/null >/dev/null 2>&1 &
|
||||
return 1
|
||||
}
|
||||
|
||||
# Start DNS enforcer - forces Private DNS off and blocks DoH/DoT endpoints
|
||||
# so the hosts file actually gets consulted by apps that would otherwise
|
||||
# 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 &
|
||||
boot_config_ready() {
|
||||
[ -f "$SCRIPT_DIR/config.sh" ]
|
||||
}
|
||||
|
||||
# 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 &
|
||||
launcher_boot_autostart_enabled() {
|
||||
load_launcher_config || return 1
|
||||
[ "${LAUNCHER_BOOT_AUTOSTART:-0}" = "1" ]
|
||||
}
|
||||
|
||||
# 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 &
|
||||
launcher_boot_snapshot_ready() {
|
||||
load_launcher_config || return 1
|
||||
[ -s "${LAUNCHER_APK:-}" ] && [ -s "${LAUNCHER_ACTIVITY_FILE:-}" ]
|
||||
}
|
||||
|
||||
exit 0
|
||||
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
|
||||
# because hosts hardening should always be active.
|
||||
setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" </dev/null >/dev/null 2>&1 &
|
||||
|
||||
# Start DNS enforcer - forces Private DNS off and blocks DoH/DoT endpoints
|
||||
# so the hosts file actually gets consulted by apps that would otherwise
|
||||
# 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 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).
|
||||
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
229
phone_focus_mode/run_phone.sh
Executable 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
|
||||
20
phone_focus_mode/systemd/install_pc_phone_automation.sh
Executable file
20
phone_focus_mode/systemd/install_pc_phone_automation.sh
Executable 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'
|
||||
9
phone_focus_mode/systemd/phone-auto-sync.service
Normal file
9
phone_focus_mode/systemd/phone-auto-sync.service
Normal 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
|
||||
10
phone_focus_mode/systemd/phone-auto-sync.timer
Normal file
10
phone_focus_mode/systemd/phone-auto-sync.timer
Normal 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
20
run.sh
Executable 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
22
scripts/run_all/run_phone.sh
Executable 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}" "$@"
|
||||
Loading…
Reference in New Issue
Block a user