testsAndMisc/phone_focus_mode/deploy.sh
Krzysztof Rudnicki b83ce5d3c7
Some checks failed
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
billsplit CI / quality (push) Has been cancelled
reverse-survivors CI / check (push) Has been cancelled
Fail the build on any shellcheck finding; fix the 222 the old gate hid
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:

  - Five scripts in single_use/misc/testsAndMisc-bash/ sourced
    $SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
    at that path, so all five failed at runtime.
  - setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
    (which ends in `exit 0`) and then `exit 1` for an unknown command. The
    exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
  - Six i3blocks scripts had a `# shellcheck source=` directive missing the
    scripts/periodic_background/ path segment. The stale path had silently
    disabled source-following for all of them.
  - diagnose_arch_performance.sh word-split a newline-joined file list; when
    the find came back empty, awk got no file operands, fell through to
    reading stdin, and the diagnostic hung.
  - lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
    `return 0` while documenting "Returns: 0 if handled" -- a contract no code
    path could satisfy.
  - deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
    level, and config.sh can repoint it at the on-device path
    /data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.

The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.

17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.

Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.

The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.

third_party/ is excluded -- vendored upstream, matching the prettier hook.

Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:49:11 +02:00

834 lines
38 KiB
Bash
Executable File

#!/bin/bash
# ============================================================
# Focus Mode Deployment Script
# Deploys focus mode to your rooted BL-9000 via wireless ADB
#
# Usage:
# ./deploy.sh [phone_ip] - Full deploy (first time or update)
# ./deploy.sh [phone_ip] --status - Check status
# ./deploy.sh [phone_ip] --log - View log
# ./deploy.sh [phone_ip] --stop - Stop daemon
# ./deploy.sh [phone_ip] --enable - Force focus mode on
# ./deploy.sh [phone_ip] --disable - Force focus mode off
# ============================================================
set -euo pipefail
PHONE_IP="${1:-}"
ACTION="${2:---deploy}"
REMOTE_DIR="/data/local/tmp/focus_mode"
# Deliberately NOT named SCRIPT_DIR: config.sh (sourced below, at top
# level) assigns SCRIPT_DIR itself and may repoint it at the on-device
# path /data/local/tmp/focus_mode. Every path here must stay anchored to
# the checkout deploy.sh runs from.
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
NEEDS_GPS_FETCH=0 # set to 1 by check_coords when local coords are placeholder
# Source shared config constants (BROWSER_PACKAGES, REMOTE_DIR, etc.)
# shellcheck source=config.sh
. "$DEPLOY_DIR/config.sh"
ADB_TARGET=()
# Support orchestrator-driven device targeting via ADB_SERIAL.
# When ADB_SERIAL is set, deploy.sh uses that target directly and preserves
# the existing PHONE_IP workflow when ADB_SERIAL is unset.
if [[ -n "${ADB_SERIAL:-}" ]]; then
ADB_TARGET=(-s "${ADB_SERIAL}")
if [[ -z "${PHONE_IP}" || "${PHONE_IP}" == --* ]]; then
ACTION="${PHONE_IP:---deploy}"
PHONE_IP=""
fi
fi
adb_cmd() {
adb "${ADB_TARGET[@]}" "$@"
}
usage() {
echo "Usage: $0 <phone_ip> [action]"
echo " or: ADB_SERIAL=<serial> $0 [action]"
echo ""
echo "Actions:"
echo " (none) Full deploy"
echo " --status Show daemon status and current mode"
echo " --log Tail the daemon log"
echo " --stop Stop daemon (re-enables all apps)"
echo " --start Start daemon"
echo " --restart Restart daemon"
echo " --enable Force focus mode on"
echo " --disable Force focus mode off"
echo " --list List all third-party apps and whitelist status"
echo " --pull-log Download log file locally"
echo " --find-pkg Show installed packages matching a filter (e.g. --find-pkg pomodoro)"
echo " --capture-coords Capture current GPS as home location (run after WiFi setup)"
echo " --hosts-status Show hosts enforcer status on the phone"
echo " --hosts-log Show hosts enforcer log on the phone"
echo " --launcher-status Show launcher enforcer status on the phone"
echo " --launcher-log Show launcher enforcer log on the phone"
echo " --snapshot-launcher Snapshot installed Minimalist Phone APK + default HOME"
echo " --install-aurora Download & install Aurora Store (open-source Play Store alt)"
echo ""
echo "Examples:"
echo " $0 192.168.1.42"
echo " $0 192.168.1.42 --status"
echo " $0 192.168.1.42 --find-pkg stronglift"
exit 1
}
# ---- Pre-flight checks ----
check_adb() {
if ! command -v adb >/dev/null 2>&1; then
echo "ERROR: adb not found. Install Android platform-tools first."
echo " Ubuntu/Debian: sudo apt install adb"
echo " Arch: sudo pacman -S android-tools"
exit 1
fi
}
check_coords() {
local lat lon
lat="$(grep '^.*HOME_LAT=' "$DEPLOY_DIR/config.sh" "$DEPLOY_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)"
lon="$(grep '^.*HOME_LON=' "$DEPLOY_DIR/config.sh" "$DEPLOY_DIR/config_secrets.sh" 2>/dev/null | tail -1 | cut -d'"' -f2)"
if [ "$lat" = "0.000000" ] && [ "$lon" = "0.000000" ]; then
echo "ERROR: Home coordinates not set (all zeros)."
exit 1
fi
# If both look like valid floats, use them; otherwise auto-capture from phone GPS.
if [[ -n "$lat" && "$lat" =~ ^[+-]?[0-9]+\.[0-9]+$ && -n "$lon" && "$lon" =~ ^[+-]?[0-9]+\.[0-9]+$ ]]; then
NEEDS_GPS_FETCH=0
echo " Home location: $lat, $lon"
else
NEEDS_GPS_FETCH=1
echo " Home location: placeholder — will be captured from phone GPS at deploy time."
fi
}
check_ip() {
if [[ -n "${ADB_SERIAL:-}" ]]; then
return 0
fi
if [ -z "$PHONE_IP" ]; then
echo "ERROR: Phone IP not provided."
echo ""
usage
fi
}
connect_adb() {
if [[ -n "${ADB_SERIAL:-}" ]]; then
if ! adb devices | awk 'NR>1 && $2=="device"{print $1}' | grep -Fxq "${ADB_SERIAL}"; then
echo "ERROR: ADB_SERIAL '${ADB_SERIAL}' is not connected."
echo "Connect device via USB or pair wireless ADB first."
exit 1
fi
ADB_TARGET=(-s "${ADB_SERIAL}")
echo "Using ADB_SERIAL target: ${ADB_SERIAL}"
return 0
fi
echo "Connecting to $PHONE_IP:5555 ..."
adb connect "$PHONE_IP:5555"
sleep 1
if ! adb devices | grep -q "$PHONE_IP"; then
echo "ERROR: Could not connect to $PHONE_IP:5555"
echo "Make sure wireless ADB is enabled and the phone is reachable."
exit 1
fi
ADB_TARGET=(-s "$PHONE_IP:5555")
echo "Connected."
}
# Wrapper: run a root shell command on the phone.
# --mount-master was removed in Magisk v26+; plain su -c works and still
# runs in the correct mount namespace for our use cases.
adb_root() {
local command_text="$1"
printf '%s\n' "$command_text" | adb_cmd shell su -c "sh -s"
}
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}'
}
# ============================================================
# GPS HOME COORDINATE CAPTURE
# ============================================================
# Called when config_secrets.sh has placeholder/non-numeric coords.
# Enables Android location, waits up to GPS_MAX_WAIT_SECS for a
# network/fused fix, and prints "lat lon" on stdout. All progress
# messages go to stderr so the caller can capture only the coords.
GPS_MAX_WAIT_SECS=90
fetch_home_coords_from_phone() {
echo " Enabling location services on phone..." >&2
adb_cmd shell settings put secure location_mode 3 2>/dev/null || true
echo " Waiting for network/fused location fix (up to ${GPS_MAX_WAIT_SECS}s)..." >&2
local waited=0 coords=""
while [[ -z "$coords" && $waited -lt $GPS_MAX_WAIT_SECS ]]; do
sleep 3
waited=$((waited + 3))
# Format on Android 10+: " last location=Location[fused LAT,LON ...]"
local raw
raw="$(adb_cmd shell dumpsys location 2>/dev/null |
grep 'last location=Location\[' |
grep -oE '[+-]?[0-9]+\.[0-9]+,[+-]?[0-9]+\.[0-9]+' |
head -1 || true)"
[[ -n "$raw" ]] && coords="$raw"
printf '.' >&2
done
printf '\n' >&2
if [[ -z "$coords" ]]; then
echo "ERROR: No location fix after ${GPS_MAX_WAIT_SECS}s." >&2
echo " Make sure the phone has cellular or WiFi data, then retry." >&2
echo " Or set HOME_LAT/HOME_LON manually in config_secrets.sh." >&2
return 1
fi
local lat="${coords%,*}"
local lon="${coords#*,}"
echo " GPS fix acquired: ${lat}, ${lon}" >&2
printf '%s %s' "$lat" "$lon"
}
# ============================================================
# MAGISK SYSTEMLESS HOSTS AUTO-INSTALL
# ============================================================
# Creates the module dir+module.prop if absent, removes disable
# markers if disabled, then reboots the device and waits up to
# HOSTS_MODULE_REBOOT_WAIT_SECS for it to come back with the
# magic-mount active. No-ops if the module is already OK.
HOSTS_MODULE_REBOOT_WAIT_SECS=180
ensure_magisk_hosts_module() {
local state="absent"
if adb_root "test -d /data/adb/modules/hosts" >/dev/null 2>&1; then
if adb_root "test -f /data/adb/modules/hosts/disable -o -f /data/adb/modules/hosts/remove" >/dev/null 2>&1; then
state="disabled"
elif adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then
state="ok"
else
state="not-mounted"
fi
fi
if [[ "$state" == "ok" ]]; then
echo " Magisk Systemless Hosts: active."
return 0
fi
echo " Magisk Systemless Hosts state: ${state} — auto-installing..."
case "$state" in
absent)
adb_root "mkdir -p /data/adb/modules/hosts/system/etc"
# module.prop is required for Magisk to recognise and process the module.
adb_root "printf 'id=hosts\nname=Systemless Hosts\nversion=v1\nversionCode=1\nauthor=Magisk\ndescription=Replace /system/etc/hosts\n' \
> /data/adb/modules/hosts/module.prop"
# Seed a minimal hosts file so the mount target exists at first boot.
adb_root "printf '127.0.0.1 localhost\n::1 localhost\n' \
> /data/adb/modules/hosts/system/etc/hosts"
adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts"
;;
disabled)
adb_root "rm -f /data/adb/modules/hosts/disable \
/data/adb/modules/hosts/remove \
/data/adb/modules/hosts/update"
;;
not-mounted)
: # module exists and enabled, just needs a reboot
;;
esac
echo " Rebooting phone to activate Magisk Hosts module..."
adb_cmd reboot
# Give the device time to actually begin shutting down before we poll.
sleep 20
echo " Waiting for device to come back (up to ${HOSTS_MODULE_REBOOT_WAIT_SECS}s)..."
local waited=0
# Re-establish wireless ADB connection if needed.
while true; do
if [[ -n "${PHONE_IP:-}" ]]; then
adb connect "${PHONE_IP}:5555" >/dev/null 2>&1 || true
fi
if adb_cmd shell echo ok 2>/dev/null | grep -q '^ok$'; then
break
fi
sleep 3
waited=$((waited + 3))
if [[ $waited -ge $HOSTS_MODULE_REBOOT_WAIT_SECS ]]; then
echo "ERROR: Device did not come back after ${HOSTS_MODULE_REBOOT_WAIT_SECS}s."
echo " Check USB connection or re-enable wireless ADB, then run deploy again."
exit 1
fi
printf '.'
done
printf '\n'
# Wait for Magisk early-init and root to be ready.
echo " Waiting for Magisk root to be available..."
waited=0
while ! adb_root "id" 2>/dev/null | grep -q "uid=0"; do
sleep 3
waited=$((waited + 3))
[[ $waited -ge 60 ]] && echo "ERROR: Root not available after reboot." && exit 1
printf '.'
done
printf '\n'
# Final assertion: the magic-mount must now be active.
if ! adb_root "test -f /system/etc/hosts" >/dev/null 2>&1; then
echo "ERROR: /system/etc/hosts is not magic-mounted after reboot."
echo " Magisk may not have applied the module correctly."
echo " Check the Magisk app for module errors and run deploy again."
exit 1
fi
echo " Magisk Systemless Hosts module is now active."
}
# ============================================================
# 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"
}
# ============================================================
# DEPLOY
# ============================================================
do_deploy() {
echo "=== Focus Mode Deployer ==="
echo ""
check_coords
echo ""
echo "[1/7] Connecting to phone..."
connect_adb
echo "[2/7] Verifying root access..."
if ! adb_root "id" | grep -q "uid=0"; then
echo "ERROR: Could not get root shell. Is Magisk installed?"
exit 1
fi
echo " Root confirmed."
echo "[2.5] Ensuring Magisk Systemless Hosts module..."
ensure_magisk_hosts_module
echo "[3/7] Creating directories on device..."
# Use world-writable staging dir so non-root adb push works
adb_cmd shell "mkdir -p /data/local/tmp/focus_stage"
adb_root "mkdir -p $REMOTE_DIR /data/adb/service.d"
adb_root "chmod 777 /data/local/tmp/focus_stage"
echo "[4/7] Uploading scripts..."
adb_cmd push "$DEPLOY_DIR/config.sh" "/data/local/tmp/focus_stage/config.sh"
adb_cmd push "$DEPLOY_DIR/focus_daemon.sh" "/data/local/tmp/focus_stage/focus_daemon.sh"
adb_cmd push "$DEPLOY_DIR/focus_ctl.sh" "/data/local/tmp/focus_stage/focus_ctl.sh"
adb_cmd push "$DEPLOY_DIR/hosts_enforcer.sh" "/data/local/tmp/focus_stage/hosts_enforcer.sh"
adb_cmd push "$DEPLOY_DIR/dns_enforcer.sh" "/data/local/tmp/focus_stage/dns_enforcer.sh"
adb_cmd push "$DEPLOY_DIR/launcher_enforcer.sh" "/data/local/tmp/focus_stage/launcher_enforcer.sh"
adb_cmd push "$DEPLOY_DIR/curfew_enforcer.sh" "/data/local/tmp/focus_stage/curfew_enforcer.sh"
adb_cmd push "$DEPLOY_DIR/tether_enforcer.sh" "/data/local/tmp/focus_stage/tether_enforcer.sh"
adb_cmd push "$DEPLOY_DIR/workout_detector.sh" "/data/local/tmp/focus_stage/workout_detector.sh"
adb_cmd push "$DEPLOY_DIR/magisk_service.sh" "/data/local/tmp/focus_stage/99-focus-mode.sh"
# ---- sqlite3 binary for workout_detector.sh ----
# Stored outside the repo (binary-files policy). Built once via the NDK
# against the SQLite amalgamation; see workout_detector.sh comments for
# the recipe. ~1.6 MB stripped, aarch64, PIE, dynamically linked against
# bionic (Android 30+).
SQLITE3_BIN="$DEPLOY_DIR/../../testsAndMisc_binaries/phone_focus_mode/sqlite3"
if [ -f "$SQLITE3_BIN" ]; then
echo " Uploading sqlite3 binary ($(stat -c%s "$SQLITE3_BIN") bytes)..."
adb_cmd push "$SQLITE3_BIN" "/data/local/tmp/focus_stage/sqlite3"
else
echo " WARNING: sqlite3 binary not found at $SQLITE3_BIN"
echo " workout_detector will not function until you build & place it there."
fi
# Generate and upload the canonical hosts file (StevenBlack + custom entries).
# This mirrors what linux_configuration/hosts/install.sh installs on the PC.
HOSTS_GENERATOR="$DEPLOY_DIR/../linux_configuration/scripts/periodic_background/hosts/generate_hosts_file.sh"
if [ -f "$HOSTS_GENERATOR" ]; then
chmod +x "$HOSTS_GENERATOR" 2>/dev/null || true
echo " Generating canonical hosts file..."
HOSTS_TMP="$(mktemp)"
HOSTS_SHA_TMP="$(mktemp)"
if bash "$HOSTS_GENERATOR" "$HOSTS_TMP"; then
hosts_hash="$(compute_file_hash "$HOSTS_TMP")"
printf '%s\n' "$hosts_hash" >"$HOSTS_SHA_TMP"
echo " Uploading canonical hosts ($(wc -l <"$HOSTS_TMP") lines)..."
adb_cmd push "$HOSTS_TMP" "/data/local/tmp/focus_stage/hosts.canonical"
adb_cmd push "$HOSTS_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256"
# ---- Workout-variant canonical ----
# Same content as the full canonical, with all lines that block
# any of $WORKOUT_UNBLOCK_DOMAINS removed. Used by hosts_enforcer
# while a StrongLifts workout is in progress.
HOSTS_WORKOUT_TMP="$(mktemp)"
HOSTS_WORKOUT_SHA_TMP="$(mktemp)"
# Read $WORKOUT_UNBLOCK_DOMAINS from the freshly-staged config.sh
# so the generator and the runtime always agree on the domain set.
UNBLOCK_DOMAINS="$(
# shellcheck disable=SC1091
(
. "$DEPLOY_DIR/config.sh" >/dev/null 2>&1
printf '%s\n' "$WORKOUT_UNBLOCK_DOMAINS"
) |
sed 's/[[:space:]]\{1,\}/\n/g' |
grep -vE '^[[:space:]]*(#|$)' |
sort -u
)"
if [ -n "$UNBLOCK_DOMAINS" ]; then
# Build an awk regex of exact-match domains anchored as the
# *value* column of a hosts entry ("<ip> <domain>" possibly
# followed by aliases). We strip any line whose first non-IP
# token matches one of the unblock domains.
WORKOUT_UNBLOCK_DOMAINS="$UNBLOCK_DOMAINS" \
python3 "$DEPLOY_DIR/strip_workout_hosts.py" "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP" ||
cp "$HOSTS_TMP" "$HOSTS_WORKOUT_TMP"
workout_hash="$(compute_file_hash "$HOSTS_WORKOUT_TMP")"
printf '%s\n' "$workout_hash" >"$HOSTS_WORKOUT_SHA_TMP"
stripped_lines=$(($(wc -l <"$HOSTS_TMP") - $(wc -l <"$HOSTS_WORKOUT_TMP")))
echo " Uploading workout-variant hosts (stripped $stripped_lines YouTube lines)..."
adb_cmd push "$HOSTS_WORKOUT_TMP" "/data/local/tmp/focus_stage/hosts.canonical.workout"
adb_cmd push "$HOSTS_WORKOUT_SHA_TMP" "/data/local/tmp/focus_stage/hosts.sha256.workout"
fi
rm -f "$HOSTS_WORKOUT_TMP" "$HOSTS_WORKOUT_SHA_TMP"
rm -f "$HOSTS_TMP"
rm -f "$HOSTS_SHA_TMP"
else
rm -f "$HOSTS_TMP"
rm -f "$HOSTS_SHA_TMP"
echo " WARNING: failed to generate hosts file - skipping hosts enforcement"
fi
else
echo " WARNING: $HOSTS_GENERATOR not found - skipping hosts enforcement"
fi
# Only push config_secrets.sh if phone doesn't already have one
if adb_root "test -f $REMOTE_DIR/config_secrets.sh" 2>/dev/null; then
echo " config_secrets.sh already exists on phone - skipping (preserving real coords)"
elif [[ "${NEEDS_GPS_FETCH}" -eq 1 ]]; then
# Local config_secrets.sh has placeholder coords — capture current GPS from the phone.
# The phone is assumed to be at home during setup, so current location = home location.
local gps_result="" gps_lat="" gps_lon=""
if gps_result="$(fetch_home_coords_from_phone 2>&1)"; then
gps_lat="${gps_result% *}"
gps_lon="${gps_result#* }"
adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS at deploy time\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \
> $REMOTE_DIR/config_secrets.sh"
echo " Home coordinates written to phone: ${gps_lat}, ${gps_lon}"
else
# GPS unavailable (no WiFi/cellular yet on fresh phone).
# Write stub coords — focus mode stays OFF, hosts/DNS blocking still works.
# User should run: ./deploy.sh [ip] --capture-coords after configuring WiFi.
adb_root "printf '#!/system/bin/sh\n# STUB: run ./deploy.sh --capture-coords after WiFi setup\nexport HOME_LAT=\"0.000001\"\nexport HOME_LON=\"0.000001\"\n' \
> $REMOTE_DIR/config_secrets.sh"
echo " WARNING: GPS capture failed — focus mode location enforcement is DISABLED."
echo " Hosts/DNS blocking is active. After configuring WiFi, run:"
echo " ADB_SERIAL=${ADB_SERIAL:-\$PHONE_IP:5555} ./deploy.sh --capture-coords"
fi
else
echo " Pushing config_secrets.sh (first install)..."
adb_cmd push "$DEPLOY_DIR/config_secrets.sh" "/data/local/tmp/focus_stage/config_secrets.sh"
adb_root "cp /data/local/tmp/focus_stage/config_secrets.sh $REMOTE_DIR/config_secrets.sh"
fi
# Move staged files into place with root
adb_root "cp /data/local/tmp/focus_stage/config.sh $REMOTE_DIR/config.sh"
adb_root "cp /data/local/tmp/focus_stage/focus_daemon.sh $REMOTE_DIR/focus_daemon.sh"
adb_root "cp /data/local/tmp/focus_stage/focus_ctl.sh $REMOTE_DIR/focus_ctl.sh"
adb_root "cp /data/local/tmp/focus_stage/hosts_enforcer.sh $REMOTE_DIR/hosts_enforcer.sh"
adb_root "cp /data/local/tmp/focus_stage/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/curfew_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh"
adb_root "cp /data/local/tmp/focus_stage/tether_enforcer.sh $REMOTE_DIR/tether_enforcer.sh"
adb_root "cp /data/local/tmp/focus_stage/workout_detector.sh $REMOTE_DIR/workout_detector.sh"
if adb_cmd shell "test -f /data/local/tmp/focus_stage/sqlite3" 2>/dev/null; then
adb_root "cp /data/local/tmp/focus_stage/sqlite3 $REMOTE_DIR/sqlite3"
adb_root "chmod 0755 $REMOTE_DIR/sqlite3"
fi
if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$DEPLOY_DIR/config.sh"; then
adb_root "cp /data/local/tmp/focus_stage/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh"
else
adb_root "rm -f /data/adb/service.d/99-focus-mode.sh /data/adb/service.d/99-focus-mode.sh.disabled"
fi
# Install canonical hosts and lock it down (only if generator produced it).
if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical" 2>/dev/null; then
# chattr -i first so we can overwrite a previously-locked canonical
adb_root "chattr -i $REMOTE_DIR/hosts.canonical 2>/dev/null; true"
adb_root "cp /data/local/tmp/focus_stage/hosts.canonical $REMOTE_DIR/hosts.canonical"
adb_root "chmod 644 $REMOTE_DIR/hosts.canonical"
# Pre-compute the sha so the enforcer does not have to seed it.
adb_root "chattr -i $REMOTE_DIR/hosts.sha256 2>/dev/null; true"
adb_root "cp /data/local/tmp/focus_stage/hosts.sha256 $REMOTE_DIR/hosts.sha256"
adb_root "chmod 644 $REMOTE_DIR/hosts.sha256"
adb_root "chattr +i $REMOTE_DIR/hosts.canonical 2>/dev/null; true"
adb_root "chattr +i $REMOTE_DIR/hosts.sha256 2>/dev/null; true"
# ---- Workout-variant canonical (optional) ----
# Same lockdown treatment as the full canonical. Pushed by the workout
# hosts generator block above. Missing variant means workout_detector\
# will simply have no relaxed file to swap to (hosts_enforcer falls\
# back to the full canonical).
if adb_cmd shell "test -f /data/local/tmp/focus_stage/hosts.canonical.workout" 2>/dev/null; then
adb_root "chattr -i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true"
adb_root "cp /data/local/tmp/focus_stage/hosts.canonical.workout $REMOTE_DIR/hosts.canonical.workout"
adb_root "chmod 644 $REMOTE_DIR/hosts.canonical.workout"
adb_root "chattr -i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true"
adb_root "cp /data/local/tmp/focus_stage/hosts.sha256.workout $REMOTE_DIR/hosts.sha256.workout"
adb_root "chmod 644 $REMOTE_DIR/hosts.sha256.workout"
adb_root "chattr +i $REMOTE_DIR/hosts.canonical.workout 2>/dev/null; true"
adb_root "chattr +i $REMOTE_DIR/hosts.sha256.workout 2>/dev/null; true"
fi
# Magisk Systemless Hosts module was ensured (and rebooted if needed) in
# step [2.5] above. Sanity-assert it's still active before writing to it.
if ! adb_root "test -f /system/etc/hosts" 2>/dev/null; then
echo "ERROR: /system/etc/hosts not magic-mounted — run deploy again."
exit 1
fi
adb_root "mkdir -p /data/adb/modules/hosts/system/etc"
# Drop any +i lock the runtime hosts_enforcer may have set on the
# module dir / hosts file so we can update them. The enforcer will
# re-lock on its next poll cycle. Also pre-emptively delete any
# disable/remove markers that may exist on disk before we start.
adb_root "chattr -i /data/adb/modules/hosts /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; rm -f /data/adb/modules/hosts/disable /data/adb/modules/hosts/remove /data/adb/modules/hosts/update; true"
adb_root "cp $REMOTE_DIR/hosts.canonical /data/adb/modules/hosts/system/etc/hosts"
adb_root "chmod 644 /data/adb/modules/hosts/system/etc/hosts"
# Lock the module dir to block the Magisk app's "Disable" / "Remove"
# buttons (they create marker files inside the dir). Files already
# in the dir stay mutable so the runtime enforcer can still update
# the hosts file on workout state changes.
adb_root "chattr +i /data/adb/modules/hosts/system/etc/hosts 2>/dev/null; true"
adb_root "chattr +i /data/adb/modules/hosts 2>/dev/null; true"
echo " Magisk hosts module populated ($(adb_root "wc -l < /data/adb/modules/hosts/system/etc/hosts" 2>/dev/null | tr -d ' ') lines), locked against UI-disable. Reboot to activate /system/etc/hosts."
fi
adb_root "rm -rf /data/local/tmp/focus_stage"
# Flush in-process DNS caches of browsers. Apps like Firefox and Chrome
# cache resolved IPs internally and bypass /etc/hosts until restarted.
echo " Flushing browser DNS caches..."
# BROWSER_PACKAGES is set by the top-level `. config.sh` above and is never
# modified. shellcheck only flags it because config.sh is ALSO re-sourced
# inside a subshell further up (to re-read WORKOUT_UNBLOCK_DOMAINS from the
# freshly-staged copy); that subshell's assignments are discarded by design.
# The unquoted expansion is the intended word split over the package list.
# shellcheck disable=SC2031
for _pkg in $BROWSER_PACKAGES; do
[ -n "$_pkg" ] || continue
adb_root "am force-stop '$_pkg' 2>/dev/null; true"
echo " force-stopped $_pkg"
done
# Disable Firefox DNS-over-HTTPS via user.js. Firefox uses hardcoded
# Cloudflare bootstrap IPs (104.16.248.249, 104.16.249.249) to reach
# mozilla.cloudflare-dns.com, completely bypassing /etc/hosts even
# after a fresh start. TRR mode 5 disables DoH so Firefox falls back
# to the system resolver which sees our 0.0.0.0 blocks.
echo " Disabling Firefox DNS-over-HTTPS..."
adb_root "for _p in /data/data/org.mozilla.fenix/files/mozilla/*/; do
[ -f \"\${_p}prefs.js\" ] || continue
grep -qF '\"network.trr.mode\"' \"\${_p}user.js\" 2>/dev/null \
|| { printf 'user_pref(\"network.trr.mode\", 5);\\n' >> \"\${_p}user.js\" 2>/dev/null && echo \" Wrote DoH-disable pref to \${_p}user.js\"; }
done; true"
echo "[5/7] Setting permissions..."
adb_root "chmod 755 $REMOTE_DIR/config.sh $REMOTE_DIR/focus_daemon.sh $REMOTE_DIR/focus_ctl.sh $REMOTE_DIR/hosts_enforcer.sh $REMOTE_DIR/dns_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh $REMOTE_DIR/curfew_enforcer.sh $REMOTE_DIR/tether_enforcer.sh $REMOTE_DIR/workout_detector.sh" || true
if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$DEPLOY_DIR/config.sh"; then
adb_root "chmod 755 /data/adb/service.d/99-focus-mode.sh"
fi
adb_root "touch $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log"
# State files need 666 so the daemons can write regardless of SELinux context drift
adb_root "chmod 666 $REMOTE_DIR/disabled_by_focus.txt $REMOTE_DIR/focus_mode.log $REMOTE_DIR/hosts_enforcer.log $REMOTE_DIR/dns_enforcer.log $REMOTE_DIR/launcher_enforcer.log $REMOTE_DIR/tether_enforcer.log $REMOTE_DIR/workout_detector.log" || true
echo "[6/7] Starting daemons..."
# Stop existing daemons, then start fresh
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill \"\$p\" 2>/dev/null || true; done"
adb_root "kill \$(cat $REMOTE_DIR/daemon.pid 2>/dev/null) 2>/dev/null; true"
adb_root "kill \$(cat $REMOTE_DIR/hosts_enforcer.pid 2>/dev/null) 2>/dev/null; true"
adb_root "kill \$(cat $REMOTE_DIR/dns_enforcer.pid 2>/dev/null) 2>/dev/null; true"
adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true"
adb_root "kill \$(cat $REMOTE_DIR/tether_enforcer.pid 2>/dev/null) 2>/dev/null; true"
adb_root "kill \$(cat $REMOTE_DIR/workout_detector.pid 2>/dev/null) 2>/dev/null; true"
sleep 1
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/focus_daemon.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/hosts_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/dns_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/launcher_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/curfew_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/tether_enforcer.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done"
adb_root "for p in \$(pgrep -f '/data/local/tmp/focus_mode/workout_detector.sh' 2>/dev/null); do kill -9 \"\$p\" 2>/dev/null || true; done"
sleep 1
adb_root "rm -f $REMOTE_DIR/daemon.pid $REMOTE_DIR/hosts_enforcer.pid $REMOTE_DIR/dns_enforcer.pid $REMOTE_DIR/launcher_enforcer.pid $REMOTE_DIR/tether_enforcer.pid $REMOTE_DIR/workout_detector.pid"
# Start hosts enforcer first so hosts are locked before user can react.
# Use --mount-master so bind mounts propagate to the global namespace
# (where app processes live). Without this, only our isolated `su` session
# would see the bind-mounted hosts file.
if adb_root "test -f $REMOTE_DIR/hosts.canonical" 2>/dev/null; then
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/hosts_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
fi
# Start workout detector BEFORE the hosts enforcer's first integrity check
# so the enforcer sees a non-stale workout_active flag. The detector itself
# is harmless if no workout is in progress (it just writes 0).
if adb_root "test -x $REMOTE_DIR/sqlite3" 2>/dev/null; then
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/workout_detector.sh </dev/null >/dev/null 2>/dev/null &'
fi
# Start DNS enforcer (forces Private DNS off, blocks DoH/DoT). Always on.
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/dns_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
# Start launcher enforcer only if a snapshot APK exists. If not, warn the
# user to install Minimalist Phone + run --snapshot-launcher first.
if adb_root "test -f $REMOTE_DIR/minimalist_launcher.apk" 2>/dev/null; then
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
else
echo " NOTE: launcher snapshot missing. Install Minimalist Phone via Aurora Store, then run:"
echo " $0 $PHONE_IP --snapshot-launcher"
fi
# Start night-curfew enforcer (grayscale + DND + optional net allow-list).
# Always on; self-gates on the clock + focus mode, no-op during the day.
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/curfew_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
# Start hotspot/tethering enforcer (blocks forwarded/tethered traffic so a
# second phone on our hotspot can't bypass focus mode). Always on; self-gates
# on focus mode, no-op while away from home.
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/tether_enforcer.sh </dev/null >/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 &'
# Wait for hosts_enforcer to apply the bind mount and restart netd.
# hosts_enforcer.sh restarts netd once at startup (takes ~4 s); we wait
# 10 s total so the network is stable before the companion-app install.
sleep 10
# ---- Companion status notification app ----
APP_DIR="$DEPLOY_DIR/focus_status_app"
APK="$APP_DIR/build/focus_status.apk"
if [ -d "$APP_DIR" ]; then
echo "[7/7] Building & installing companion status-notification app..."
needs_rebuild=0
if [ ! -f "$APK" ]; then
needs_rebuild=1
elif [ "$APP_DIR/AndroidManifest.xml" -nt "$APK" ]; then
needs_rebuild=1
elif [ "$APP_DIR/build.sh" -nt "$APK" ]; then
needs_rebuild=1
elif find "$APP_DIR/java" -name '*.java' -newer "$APK" -print -quit 2>/dev/null | grep -q .; then
# Rebuild when any Java source changed, not just the manifest.
needs_rebuild=1
fi
if [ "$needs_rebuild" -eq 1 ]; then
echo " Building APK..."
# Non-fatal: the companion UI is optional. If the Android SDK is
# missing (build.sh fails), warn and fall back to the existing APK
# rather than aborting the whole deploy and leaving the curfew core
# un-started.
if ! (cd "$APP_DIR" && bash build.sh) >/dev/null 2>&1; then
echo " WARNING: APK build failed (Android SDK missing?)."
echo " Keeping the previously-built APK if present;"
echo " the curfew daemons/enforcers are unaffected."
fi
fi
if [ -f "$APK" ]; then
echo " Installing APK..."
adb_cmd install -r "$APK" >/dev/null || true
# Grant runtime permission (Android 13+ requires it for notifications).
adb_cmd shell pm grant com.kuhy.focusstatus android.permission.POST_NOTIFICATIONS >/dev/null 2>&1 || true
# Pre-approve Magisk SU so the app never shows the approval prompt.
APP_UID="$(
adb_cmd shell dumpsys package com.kuhy.focusstatus 2>/dev/null |
awk 'match($0, /userId=[0-9]+/) {print substr($0, RSTART + 7, RLENGTH - 7); exit}'
)"
if [ -n "$APP_UID" ]; then
adb_cmd shell "su -c 'magisk --sqlite \"INSERT OR REPLACE INTO policies (uid,policy,until,logging,notification) VALUES ($APP_UID,2,0,1,1)\"'" >/dev/null 2>&1 || true
fi
# Launch the invisible activity which kicks off the foreground service.
adb_cmd shell am start -n com.kuhy.focusstatus/.LaunchActivity >/dev/null 2>&1 || true
echo " Companion app running (look for the ongoing 'Focus Mode' notification)."
else
echo " WARNING: APK build failed - skipping companion app install"
fi
fi
echo ""
echo "=== Deploy complete! ==="
echo ""
echo "Checking status..."
adb_root "sh $REMOTE_DIR/focus_ctl.sh status"
echo ""
echo "Boot autostart is disabled by default (FOCUS_BOOT_AUTOSTART=0)."
echo "No Magisk service.d hook is installed unless FOCUS_BOOT_AUTOSTART=1 in config.sh."
echo "Launcher enforcement does not auto-start on boot unless LAUNCHER_BOOT_AUTOSTART=1 is set in config.sh."
echo ""
echo "Useful commands:"
echo " $0 $PHONE_IP --status # Check mode and location"
echo " $0 $PHONE_IP --log # View daemon log"
echo " $0 $PHONE_IP --list # See all apps and whitelist status"
echo " $0 $PHONE_IP --enable # Force focus mode on for testing"
echo " $0 $PHONE_IP --disable # Force focus mode off"
echo " $0 $PHONE_IP --install-aurora # Install Aurora Store (Play Store alternative)"
}
# ============================================================
# Control actions (post-deploy)
# ============================================================
do_control() {
local ctl_cmd="$1"
connect_adb
adb_root "sh $REMOTE_DIR/focus_ctl.sh $ctl_cmd"
}
do_pull_log() {
connect_adb
echo "Downloading log..."
adb_cmd pull "$REMOTE_DIR/focus_mode.log" "./focus_mode_$(date +%Y%m%d_%H%M%S).log"
echo "Done."
}
do_capture_coords() {
# Standalone GPS capture for post-WiFi-setup use.
# Overwrites config_secrets.sh on the phone with the current location.
connect_adb
if ! adb_root "id" 2>/dev/null | grep -q "uid=0"; then
echo "ERROR: Root not available."
exit 1
fi
echo "Capturing home coordinates from phone GPS..."
local gps_result gps_lat gps_lon
gps_result="$(fetch_home_coords_from_phone)"
gps_lat="${gps_result% *}"
gps_lon="${gps_result#* }"
adb_root "printf '#!/system/bin/sh\n# Home coordinates auto-captured from GPS\nexport HOME_LAT=\"${gps_lat}\"\nexport HOME_LON=\"${gps_lon}\"\n' \
> $REMOTE_DIR/config_secrets.sh"
echo "Home coordinates updated on phone: ${gps_lat}, ${gps_lon}"
echo "Restarting focus daemon to apply new coordinates..."
adb_root "sh $REMOTE_DIR/focus_ctl.sh restart" 2>/dev/null || true
echo "Done."
}
do_find_pkg() {
local filter="${3:-}"
if [ -z "$filter" ]; then
echo "Usage: $0 <ip> --find-pkg <search_term>"
exit 1
fi
connect_adb
echo "Packages matching '$filter':"
adb_cmd shell pm list packages | grep -i "$filter" | sed 's/^package:/ /'
}
do_snapshot_launcher() {
# Run the on-device snapshot command. This captures the APK + HOME
# activity of the already-installed Minimalist Phone launcher into
# /data/adb/focus_mode/ so the launcher enforcer can restore it later.
# The user must install the launcher once (via Aurora/Play) before
# running this command - we only back up what's already there.
connect_adb
echo "Snapshotting currently-installed launcher APK..."
adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-snapshot"
echo ""
echo "Starting launcher enforcer..."
# Kill any previous enforcer so it picks up the new snapshot.
adb_root "kill \$(cat $REMOTE_DIR/launcher_enforcer.pid 2>/dev/null) 2>/dev/null; true"
adb_root "rm -f $REMOTE_DIR/launcher_enforcer.pid"
adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/launcher_enforcer.sh </dev/null >/dev/null 2>/dev/null &'
sleep 3
adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-status"
}
# ============================================================
# Entry point
# ============================================================
check_adb
check_ip
case "$ACTION" in
--deploy | "") do_deploy ;;
--status) do_control "status" ;;
--log)
connect_adb
adb_root "sh $REMOTE_DIR/focus_ctl.sh log 100"
;;
--stop) do_control "stop" ;;
--start) do_control "start" ;;
--restart) do_control "restart" ;;
--enable) do_control "enable" ;;
--disable) do_control "disable" ;;
--list) do_control "list-apps" ;;
--pull-log) do_pull_log ;;
--find-pkg) do_find_pkg "$@" ;;
--hosts-status) do_control "hosts-status" ;;
--hosts-log)
connect_adb
adb_root "sh $REMOTE_DIR/focus_ctl.sh hosts-log 100"
;;
--launcher-status) do_control "launcher-status" ;;
--launcher-log)
connect_adb
adb_root "sh $REMOTE_DIR/focus_ctl.sh launcher-log 100"
;;
--capture-coords) do_capture_coords ;;
--snapshot-launcher) do_snapshot_launcher ;;
--install-aurora) do_install_aurora ;;
*)
echo "Unknown action: $ACTION"
usage
;;
esac