testsAndMisc/phone_focus_mode/focus_ctl.sh

940 lines
32 KiB
Bash
Raw Normal View History

2026-02-22 16:57:36 +01:00
#!/system/bin/sh
# shellcheck shell=ash
# ============================================================
# Focus Mode Control Utility
# Run on the phone via: su --mount-master -c /data/local/tmp/focus_mode/focus_ctl.sh <command>
# Or from PC via: adb shell su --mount-master -c '/data/local/tmp/focus_mode/focus_ctl.sh <command>'
# --mount-master is required so this script (and any daemon it spawns) joins
# the global mount namespace; otherwise the hosts bind mount is invisible and
# /data/adb/focus_mode/* checks fail due to per-session SELinux isolation.
2026-02-22 16:57:36 +01:00
# ============================================================
SCRIPT_DIR="/data/local/tmp/focus_mode"
. "$SCRIPT_DIR/config.sh"
PIDFILE="$STATE_DIR/daemon.pid"
# ---- Logging ----
log() {
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$ts] $1" >> "$LOG_FILE"
}
# Emit one valid package name per line from WHITELIST.
# This strips comments/blank lines from the multi-line quoted string and avoids
# treating heading text (e.g. "---") as package tokens.
iter_whitelist_packages() {
printf '%s\n' "$WHITELIST" | while IFS= read -r line; do
line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
case "$line" in
""|\#*) continue ;;
esac
# Keep first token only; ignore any inline prose if present.
set -- $line
pkg="$1"
# Package names are dot-delimited identifiers.
case "$pkg" in
*.*) ;;
*) continue ;;
esac
case "$pkg" in
*[!A-Za-z0-9._]*) continue ;;
esac
echo "$pkg"
done
}
2026-02-22 16:57:36 +01:00
usage() {
echo "Usage: focus_ctl.sh <command>"
echo ""
echo "Commands:"
echo " start - Start the focus mode daemon"
echo " stop - Stop the daemon and re-enable all apps"
echo " status - Show current mode, location and disabled apps"
echo " enable - Force focus mode on (regardless of location)"
echo " disable - Force focus mode off (regardless of location)"
echo " log - Show daemon log"
echo " list-apps - List all non-whitelisted third-party apps"
echo " whitelist - List currently whitelisted packages"
echo " restart - Restart the daemon"
echo " hosts-status - Show hosts enforcer state (mount + hash)"
echo " hosts-start - Start the hosts enforcer daemon"
echo " hosts-stop - Stop the hosts enforcer daemon"
echo " hosts-log - Show hosts enforcer log"
echo " dns-status - Show DNS enforcer state (Private DNS + iptables)"
echo " dns-start - Start the DNS enforcer daemon"
echo " dns-stop - Stop the DNS enforcer daemon (removes iptables chain)"
echo " dns-log - Show DNS enforcer log"
echo " launcher-status - Show launcher enforcer state"
echo " launcher-start - Start the launcher enforcer daemon"
echo " launcher-stop - Stop the launcher enforcer daemon"
echo " launcher-log - Show launcher enforcer log"
echo " launcher-snapshot - Back up currently-installed launcher APK"
echo " workout-status - Show StrongLifts workout-detection state"
echo " workout-start - Start the workout detector daemon"
echo " workout-stop - Stop the workout detector daemon (sets flag=0)"
echo " workout-log - Show workout detector log"
echo " recheck - Nudge the daemon to perform a fresh location check now"
echo " curfew-status - Show night-curfew + enforcer state"
echo " curfew-start - Start the curfew enforcer (grayscale/DND/net)"
echo " curfew-stop - Stop it and restore daytime display/DND"
echo " curfew-log - Show curfew enforcer log"
echo " curfew-test-on - Force curfew ACTIVE now (daytime validation)"
echo " curfew-test-off - Clear the test force"
echo " curfew-off - Escape hatch: suspend curfew now (2am opt-out)"
echo " curfew-on - Re-arm curfew (clear the override)"
echo " notif-status - Show companion status-notification details"
2026-02-22 16:57:36 +01:00
echo ""
}
# 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
2026-02-22 16:57:36 +01:00
}
cmd_start() {
local pid
pid="$(daemon_pid)"
if [ -n "$pid" ]; then
echo "Daemon already running (PID $pid)"
return
fi
setsid sh "$SCRIPT_DIR/focus_daemon.sh" </dev/null >/dev/null 2>&1 &
sleep 2
pid="$(daemon_pid)"
if [ -n "$pid" ]; then
echo "Daemon started (PID $pid)"
else
echo "ERROR: Daemon failed to start. Check log: $LOG_FILE"
fi
}
cmd_stop() {
local pid
pid="$(daemon_pid)"
if [ -z "$pid" ]; then
echo "Daemon not running"
# Clean up stale pidfile if present
rm -f "$PIDFILE"
else
kill -TERM "$pid"
echo "Daemon stopped (sent SIGTERM to PID $pid)"
fi
}
cmd_status() {
local pid
pid="$(daemon_pid)"
local mode="unknown"
[ -f "$MODE_FILE" ] && mode="$(cat "$MODE_FILE")"
echo "=== Focus Mode Status ==="
if [ -n "$pid" ]; then
echo "Daemon: RUNNING (PID $pid)"
else
echo "Daemon: STOPPED"
fi
echo "Mode: $mode"
echo "Home: $HOME_LAT, $HOME_LON (radius: ${RADIUS}m)"
echo ""
# Show current location if available
location="$(dumpsys location 2>/dev/null \
| grep -oE 'Location\[.*[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' \
| grep -oE '[-]?[0-9]{1,3}\.[0-9]+,[-]?[0-9]{1,3}\.[0-9]+' \
| head -1)"
if [ -n "$location" ]; then
lat="$(echo "$location" | cut -d',' -f1)"
lon="$(echo "$location" | cut -d',' -f2)"
dist="$(echo "$lat $lon $HOME_LAT $HOME_LON" | awk '{
PI=3.14159265358979; R=6371000
a1=$1*PI/180; o1=$2*PI/180
a2=$3*PI/180; o2=$4*PI/180
da=a2-a1; dlon=o2-o1
x=sin(da/2)^2+cos(a1)*cos(a2)*sin(dlon/2)^2
printf "%d", R*2*atan2(sqrt(x),sqrt(1-x))
}')"
echo "Location: $lat, $lon"
echo "Distance: ${dist}m from home"
else
echo "Location: unavailable"
fi
echo ""
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
echo "=== Apps disabled by focus mode ==="
cat "$DISABLED_APPS_FILE"
else
echo "No apps currently disabled by focus mode"
fi
}
cmd_enable() {
# Disable daemon temporarily, force focus
echo "Forcing focus mode ON..."
. "$SCRIPT_DIR/config.sh"
# Source common functions - inline here for standalone use
: > "$STATE_DIR/disabled_by_focus.txt"
local count=0
for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do
# Check whitelist
whitelisted=0
for w in $(iter_whitelist_packages); do
2026-02-22 16:57:36 +01:00
w_clean="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w_clean" ] && continue
[ "$pkg" = "$w_clean" ] && { whitelisted=1; break; }
done
[ "$whitelisted" -eq 1 ] && continue
# Check system protection
protected=0
for prefix in $SYSTEM_NEVER_DISABLE; do
prefix_clean="$(echo "$prefix" | tr -d '[:space:]')"
[ -z "$prefix_clean" ] && continue
case "$pkg" in
"$prefix_clean"*) protected=1; break ;;
esac
done
[ "$protected" -eq 1 ] && continue
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
echo "$pkg" >> "$STATE_DIR/disabled_by_focus.txt"
count=$((count + 1))
fi
done
echo "focus" > "$MODE_FILE"
echo "Done: disabled $count apps"
}
cmd_recheck() {
# Write the trigger file; the daemon's sleep_with_recheck() will pick it
# up within ~1 second and perform an immediate location check.
if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
echo "Daemon not running - start it first with: focus_ctl.sh start"
return 1
fi
touch "$RECHECK_TRIGGER"
chmod 666 "$RECHECK_TRIGGER" 2>/dev/null || true
echo "Recheck requested. Tail the log to see the next reading:"
echo " tail -f $LOG_FILE"
}
cmd_notif_status() {
if [ -f "$STATUS_FILE" ]; then
echo "=== $STATUS_FILE ==="
cat "$STATUS_FILE"
echo
else
echo "No status snapshot yet (daemon has not written $STATUS_FILE)."
fi
if command -v dumpsys >/dev/null 2>&1; then
echo "=== Companion app state ==="
dumpsys package com.kuhy.focusstatus 2>/dev/null | grep -E 'enabled=|installed=|userId=' | head -5 || true
fi
}
2026-02-22 16:57:36 +01:00
cmd_disable() {
echo "Forcing focus mode OFF..."
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
local count=0
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1))
done < "$DISABLED_APPS_FILE"
: > "$DISABLED_APPS_FILE"
echo "Done: re-enabled $count apps"
else
echo "No apps to re-enable"
fi
echo "normal" > "$MODE_FILE"
}
cmd_log() {
local lines="${1:-50}"
if [ -f "$LOG_FILE" ]; then
tail -n "$lines" "$LOG_FILE"
else
echo "Log file not found: $LOG_FILE"
fi
}
cmd_list_apps() {
echo "=== Third-party apps NOT in whitelist ==="
for pkg in $(pm list packages -3 2>/dev/null | sed 's/^package://'); do
whitelisted=0
for w in $(iter_whitelist_packages); do
2026-02-22 16:57:36 +01:00
w="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w" ] && continue
[ "$pkg" = "$w" ] && { whitelisted=1; break; }
done
if [ "$whitelisted" -eq 0 ]; then
# Check if currently disabled by focus mode
if grep -qF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null; then
echo " [BLOCKED] $pkg"
else
echo " [active] $pkg"
fi
fi
done
echo ""
echo "=== Whitelisted apps ==="
for w in $(iter_whitelist_packages); do
2026-02-22 16:57:36 +01:00
w="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w" ] && continue
echo " [allowed] $w"
done
}
cmd_whitelist() {
echo "=== Whitelisted packages ==="
for w in $(iter_whitelist_packages); do
2026-02-22 16:57:36 +01:00
w="$(echo "$w" | tr -d '[:space:]')"
[ -z "$w" ] && continue
# Check if installed
if pm list packages "$w" 2>/dev/null | grep -qF "$w"; then
echo " [installed] $w"
else
echo " [not found] $w"
fi
done
}
HOSTS_PIDFILE="$STATE_DIR/hosts_enforcer.pid"
hosts_enforcer_pid() {
if [ -f "$HOSTS_PIDFILE" ]; then
local pid
pid="$(cat "$HOSTS_PIDFILE")"
if kill -0 "$pid" 2>/dev/null; then
echo "$pid"
fi
fi
}
cmd_hosts_status() {
local pid
pid="$(hosts_enforcer_pid)"
echo "=== Hosts Enforcer Status ==="
if [ -n "$pid" ]; then
echo "Daemon: RUNNING (PID $pid)"
else
echo "Daemon: STOPPED"
fi
echo "Canonical: $HOSTS_CANONICAL"
echo "Target: $HOSTS_TARGET"
if grep -qE "[[:space:]]${HOSTS_TARGET}[[:space:]]" /proc/self/mounts 2>/dev/null; then
# A mount exists on the target path, but on Android the OEM sometimes
# already mounts its own hosts file here. Trust the sha check below.
echo "Mount: present (integrity check below tells us if ours)"
else
echo "Mount: NOT mounted (unprotected)"
fi
if [ -f "$HOSTS_CANONICAL" ]; then
local expected actual
expected="$(cat "$HOSTS_SHA_FILE" 2>/dev/null)"
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')"
else
actual="$(md5sum "$HOSTS_TARGET" 2>/dev/null | awk '{print $1}')"
fi
echo "Expected: ${expected:-<none>}"
echo "Actual: ${actual:-<unreadable>}"
if [ -n "$expected" ] && [ "$expected" = "$actual" ]; then
echo "Integrity: OK"
else
echo "Integrity: MISMATCH"
fi
else
echo "Canonical hosts file missing - run deploy.sh"
fi
phone_focus_mode: prevent Magisk app from disabling Systemless Hosts module The Magisk app's Modules tab "Disable" / "Remove" buttons work by creating marker files (disable, remove) in /data/adb/modules/hosts/. Tapping Disable in the app on next boot would skip the module's magic-mount of /system/etc/hosts, silently disabling all hosts-file blocking. Defense in depth: 1. deploy.sh chattr +i's the module dir + its hosts file so the Magisk app cannot create disable/remove markers (kernel returns EPERM). The +i attribute survives reboot. 2. hosts_enforcer.sh adds protect_magisk_module(): every poll cycle (and on startup) scans for disable/remove/update markers, deletes them, logs TAMPER, and re-asserts +i on the dir. Safety net in case the lock is bypassed. 3. sync_magisk_module() now drops +i briefly before its cp and re-locks via protect_magisk_module() so workout-state hosts swaps still work. 4. deploy.sh detects the previously-silent failure mode of the module being enabled on disk but not yet magic-mounted (no /system/etc/hosts) and aborts with a clear reboot-required message instead of producing a deploy that does nothing. 5. focus_ctl.sh hosts-status now prints the lock state and warns about any present markers. Verified end-to-end on BL9000EEA0000102: - Pre-reboot: chattr +i set, touch /data/adb/modules/hosts/disable returns Operation not permitted. - Post-reboot: /system/etc/hosts magic-mounted (178303 lines, sha matches canonical), lock survives reboot, ping youtube.com -> 127.0.0.1. - Tamper test: chattr -i + touch disable -> enforcer logs 'TAMPER: removed Magisk module marker' within 15s and re-locks. Documented intentional override path inline (focus_ctl.sh hosts-stop; chattr -i; touch disable).
2026-05-06 21:40:51 +02:00
# Magisk Systemless Hosts module protection state.
local module_dir="/data/adb/modules/hosts"
if [ -d "$module_dir" ]; then
local lock_state="UNLOCKED (Magisk app can disable!)"
if lsattr -d "$module_dir" 2>/dev/null | awk '{print $1}' | grep -q i; then
lock_state="LOCKED (chattr +i)"
fi
echo "Magisk dir: $module_dir [$lock_state]"
local marker_warn=""
for marker in disable remove update; do
if [ -e "$module_dir/$marker" ]; then
marker_warn="$marker_warn $marker"
fi
done
if [ -n "$marker_warn" ]; then
echo "WARN: Magisk markers present:$marker_warn (will be auto-removed by hosts_enforcer)"
fi
else
echo "Magisk dir: <missing - module not installed>"
fi
}
cmd_hosts_start() {
local pid
pid="$(hosts_enforcer_pid)"
if [ -n "$pid" ]; then
echo "Hosts enforcer already running (PID $pid)"
return
fi
setsid sh "$SCRIPT_DIR/hosts_enforcer.sh" </dev/null >/dev/null 2>&1 &
sleep 2
pid="$(hosts_enforcer_pid)"
if [ -n "$pid" ]; then
echo "Hosts enforcer started (PID $pid)"
else
echo "ERROR: hosts enforcer failed to start. Check log: $HOSTS_LOG"
fi
}
cmd_hosts_stop() {
local pid
pid="$(hosts_enforcer_pid)"
if [ -z "$pid" ]; then
echo "Hosts enforcer not running"
rm -f "$HOSTS_PIDFILE"
return
fi
kill -TERM "$pid"
echo "Hosts enforcer stopped (sent SIGTERM to PID $pid)"
}
cmd_hosts_log() {
local lines="${1:-50}"
if [ -f "$HOSTS_LOG" ]; then
tail -n "$lines" "$HOSTS_LOG"
else
echo "Hosts enforcer log not found: $HOSTS_LOG"
fi
}
# ---- DNS enforcer ----
# Hosts file only works for the system resolver. Apps using DoH/DoT bypass
# /etc/hosts entirely. The DNS enforcer forces Private DNS off and blocks
# well-known DoH/DoT endpoints so /etc/hosts is actually consulted.
DNS_PIDFILE="$STATE_DIR/dns_enforcer.pid"
dns_enforcer_pid() {
if [ -f "$DNS_PIDFILE" ]; then
local pid
pid="$(cat "$DNS_PIDFILE")"
if kill -0 "$pid" 2>/dev/null; then
echo "$pid"
fi
fi
}
cmd_dns_status() {
local pid
pid="$(dns_enforcer_pid)"
echo "=== DNS Enforcer Status ==="
if [ -n "$pid" ]; then
echo "Daemon: RUNNING (PID $pid)"
else
echo "Daemon: STOPPED"
fi
local mode spec
mode="$(settings get global private_dns_mode 2>/dev/null)"
spec="$(settings get global private_dns_specifier 2>/dev/null)"
echo "private_dns_mode: ${mode:-<unset>}"
echo "private_dns_specifier: ${spec:-<unset>}"
if iptables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
local v4rules
v4rules="$(iptables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)"
echo "iptables $DNS_IPT_CHAIN: $v4rules rules"
else
echo "iptables $DNS_IPT_CHAIN: MISSING"
fi
if ip6tables -L "$DNS_IPT_CHAIN" >/dev/null 2>&1; then
local v6rules
v6rules="$(ip6tables -S "$DNS_IPT_CHAIN" 2>/dev/null | wc -l)"
echo "ip6tables $DNS_IPT_CHAIN: $v6rules rules"
else
echo "ip6tables $DNS_IPT_CHAIN: MISSING"
fi
}
cmd_dns_start() {
local pid
pid="$(dns_enforcer_pid)"
if [ -n "$pid" ]; then
echo "DNS enforcer already running (PID $pid)"
return
fi
setsid sh "$SCRIPT_DIR/dns_enforcer.sh" </dev/null >/dev/null 2>&1 &
sleep 2
pid="$(dns_enforcer_pid)"
if [ -n "$pid" ]; then
echo "DNS enforcer started (PID $pid)"
else
echo "ERROR: DNS enforcer failed to start. Check log: $DNS_LOG"
fi
}
cmd_dns_stop() {
local pid
pid="$(dns_enforcer_pid)"
if [ -z "$pid" ]; then
echo "DNS enforcer not running"
rm -f "$DNS_PIDFILE"
else
kill -TERM "$pid"
echo "DNS enforcer stopped (sent SIGTERM to PID $pid)"
fi
# Explicit teardown of the iptables chain so maintenance work can
# use DoH. The enforcer itself leaves the chain intact on TERM to
# keep the block closed between periodic re-applies.
iptables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true
iptables -F "$DNS_IPT_CHAIN" 2>/dev/null || true
iptables -X "$DNS_IPT_CHAIN" 2>/dev/null || true
ip6tables -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null || true
ip6tables -F "$DNS_IPT_CHAIN" 2>/dev/null || true
ip6tables -X "$DNS_IPT_CHAIN" 2>/dev/null || true
echo "iptables chain $DNS_IPT_CHAIN removed"
}
cmd_dns_log() {
local lines="${1:-50}"
if [ -f "$DNS_LOG" ]; then
tail -n "$lines" "$DNS_LOG"
else
echo "DNS enforcer log not found: $DNS_LOG"
fi
}
# ---- Launcher enforcer ----
LAUNCHER_PIDFILE="$STATE_DIR/launcher_enforcer.pid"
DISABLED_COMPETITORS_FILE="$STATE_DIR/disabled_competitors.txt"
launcher_enforcer_pid() {
if [ -f "$LAUNCHER_PIDFILE" ]; then
local pid
pid="$(cat "$LAUNCHER_PIDFILE")"
if kill -0 "$pid" 2>/dev/null; then
echo "$pid"
fi
fi
}
cmd_launcher_snapshot() {
# Find the APK path for the currently-installed launcher and copy it
# to LAUNCHER_APK. Also capture the current HOME activity component.
local apk_path
apk_path="$(pm path "$LAUNCHER_PACKAGE" 2>/dev/null | head -1 | sed 's/^package://')"
if [ -z "$apk_path" ] || [ ! -f "$apk_path" ]; then
echo "ERROR: $LAUNCHER_PACKAGE is not installed. Install it once via Aurora/Play Store, then rerun this command."
return 1
fi
mkdir -p "$(dirname "$LAUNCHER_APK")"
chattr -i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true
cp "$apk_path" "$LAUNCHER_APK" || return 1
chmod 644 "$LAUNCHER_APK"
sha256sum "$LAUNCHER_APK" | awk '{print $1}' > "$LAUNCHER_SHA_FILE"
chmod 644 "$LAUNCHER_SHA_FILE"
# Resolve the current HOME activity (or the launcher's default activity
# if it isn't yet the default).
local component
component="$(cmd package resolve-activity --brief \
-c android.intent.category.HOME \
-a android.intent.action.MAIN 2>/dev/null | awk 'NR==2{print}')"
if [ -z "$component" ] || [ "${component%%/*}" != "$LAUNCHER_PACKAGE" ]; then
# Fall back to the launcher's MAIN/LAUNCHER activity
component="$(cmd package resolve-activity --brief \
-c android.intent.category.LAUNCHER \
-a android.intent.action.MAIN "$LAUNCHER_PACKAGE" 2>/dev/null \
| awk 'NR==2{print}')"
fi
if [ -z "$component" ]; then
echo "ERROR: could not resolve HOME activity for $LAUNCHER_PACKAGE"
return 1
fi
echo "$component" > "$LAUNCHER_ACTIVITY_FILE"
chmod 644 "$LAUNCHER_ACTIVITY_FILE"
# Make snapshot immutable so even root-in-a-terminal can't overwrite
# it without first running `chattr -i`.
chattr +i "$LAUNCHER_APK" "$LAUNCHER_SHA_FILE" "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null || true
echo "Snapshot saved:"
echo " APK: $LAUNCHER_APK ($(wc -c < "$LAUNCHER_APK") bytes)"
echo " SHA256: $(cat "$LAUNCHER_SHA_FILE")"
echo " Activity: $component"
}
cmd_launcher_status() {
local pid
pid="$(launcher_enforcer_pid)"
echo "=== Launcher Enforcer Status ==="
if [ -n "$pid" ]; then
echo "Daemon: RUNNING (PID $pid)"
else
echo "Daemon: STOPPED"
fi
echo "Package: $LAUNCHER_PACKAGE"
if pm path "$LAUNCHER_PACKAGE" >/dev/null 2>&1; then
echo "Installed: YES ($(pm path "$LAUNCHER_PACKAGE" | head -1))"
else
echo "Installed: NO"
fi
local desired actual
desired="$(cat "$LAUNCHER_ACTIVITY_FILE" 2>/dev/null)"
actual="$(cmd package resolve-activity --brief \
-c android.intent.category.HOME -a android.intent.action.MAIN \
2>/dev/null | awk 'NR==2{print}')"
echo "Expected: ${desired:-<not armed - run launcher-snapshot>}"
echo "Actual: ${actual:-<unresolved>}"
if [ -n "$desired" ] && [ "$desired" = "$actual" ]; then
echo "Default: OK (pinned)"
else
echo "Default: MISMATCH"
fi
echo "Snapshot: $LAUNCHER_APK"
if [ -f "$LAUNCHER_APK" ]; then
echo "Snapshot size: $(wc -c < "$LAUNCHER_APK") bytes"
fi
if [ -s "$DISABLED_COMPETITORS_FILE" ]; then
echo "Disabled competitors:"
sed 's/^/ - /' "$DISABLED_COMPETITORS_FILE"
fi
}
cmd_launcher_start() {
local pid
pid="$(launcher_enforcer_pid)"
if [ -n "$pid" ]; then
echo "Launcher enforcer already running (PID $pid)"
return
fi
setsid sh "$SCRIPT_DIR/launcher_enforcer.sh" </dev/null >/dev/null 2>&1 &
sleep 2
pid="$(launcher_enforcer_pid)"
if [ -n "$pid" ]; then
echo "Launcher enforcer started (PID $pid)"
else
echo "ERROR: launcher enforcer failed to start. Check log: $LAUNCHER_LOG"
fi
}
cmd_launcher_stop() {
local pid
pid="$(launcher_enforcer_pid)"
if [ -z "$pid" ]; then
echo "Launcher enforcer not running"
rm -f "$LAUNCHER_PIDFILE"
else
kill -TERM "$pid"
echo "Launcher enforcer stopped (sent SIGTERM to PID $pid)"
fi
# Re-enable any competitors we disabled so the device is usable if the
# enforcer is intentionally stopped (e.g. during maintenance).
if [ -s "$DISABLED_COMPETITORS_FILE" ]; then
while read -r pkg; do
[ -z "$pkg" ] && continue
pm enable --user 0 "$pkg" >/dev/null 2>&1 && \
echo "Re-enabled competing launcher: $pkg"
done < "$DISABLED_COMPETITORS_FILE"
: > "$DISABLED_COMPETITORS_FILE"
fi
}
cmd_launcher_log() {
local lines="${1:-50}"
if [ -f "$LAUNCHER_LOG" ]; then
tail -n "$lines" "$LAUNCHER_LOG"
else
echo "Launcher enforcer log not found: $LAUNCHER_LOG"
fi
}
# ---- Workout detector ----
WORKOUT_PIDFILE="$STATE_DIR/workout_detector.pid"
workout_detector_pid() {
if [ -f "$WORKOUT_PIDFILE" ]; then
local pid
pid="$(cat "$WORKOUT_PIDFILE")"
if kill -0 "$pid" 2>/dev/null; then
echo "$pid"
fi
fi
}
cmd_workout_status() {
local pid
pid="$(workout_detector_pid)"
echo "=== Workout Detector Status ==="
if [ -n "$pid" ]; then
echo "Daemon: RUNNING (PID $pid)"
else
echo "Daemon: STOPPED"
fi
echo "Package: $WORKOUT_TRIGGER_PACKAGE"
if pm path "$WORKOUT_TRIGGER_PACKAGE" >/dev/null 2>&1; then
echo "Installed: YES"
else
echo "Installed: NO (detector will always report inactive)"
fi
echo "sqlite3: $WORKOUT_SQLITE3_BIN"
if [ -x "$WORKOUT_SQLITE3_BIN" ]; then
echo "sqlite3 ver: $("$WORKOUT_SQLITE3_BIN" -version 2>/dev/null | awk '{print $1}')"
else
echo "sqlite3 ver: <missing or not executable — detector cannot query DB>"
fi
echo "DB path: $WORKOUT_DB_PATH"
if [ -f "$WORKOUT_DB_PATH" ]; then
echo "DB present: YES"
else
echo "DB present: NO"
fi
echo "Poll interval: ${WORKOUT_DETECTOR_INTERVAL}s"
local flag="<unset>"
if [ -f "$WORKOUT_ACTIVE_FILE" ]; then
flag="$(cat "$WORKOUT_ACTIVE_FILE" 2>/dev/null)"
fi
case "$flag" in
1) echo "Workout flag: 1 (workout IN PROGRESS → YouTube hosts UNBLOCKED)" ;;
0) echo "Workout flag: 0 (no workout → YouTube hosts BLOCKED)" ;;
*) echo "Workout flag: '$flag' (treated as 0, fail-closed)" ;;
esac
# Live one-shot query so the user can see ground truth without waiting
# for the next poll cycle. Best-effort — never fails the status command.
if [ -x "$WORKOUT_SQLITE3_BIN" ] && [ -f "$WORKOUT_DB_PATH" ]; then
local live_count
live_count="$("$WORKOUT_SQLITE3_BIN" "file:${WORKOUT_DB_PATH}?mode=ro" \
"SELECT COUNT(*) FROM workouts WHERE start>0 AND (finish IS NULL OR finish=0);" \
2>/dev/null)"
echo "Live DB query: in-progress workouts = ${live_count:-<query failed>}"
fi
if [ -f "$HOSTS_CANONICAL_WORKOUT" ]; then
echo "Workout hosts: $HOSTS_CANONICAL_WORKOUT ($(wc -l < "$HOSTS_CANONICAL_WORKOUT" 2>/dev/null) lines)"
else
echo "Workout hosts: <missing — deploy.sh must regenerate it>"
fi
}
cmd_workout_start() {
local pid
pid="$(workout_detector_pid)"
if [ -n "$pid" ]; then
echo "Workout detector already running (PID $pid)"
return
fi
if [ ! -x "$WORKOUT_SQLITE3_BIN" ]; then
echo "ERROR: $WORKOUT_SQLITE3_BIN missing or not executable. Re-run deploy.sh."
return 1
fi
setsid sh "$SCRIPT_DIR/workout_detector.sh" </dev/null >/dev/null 2>&1 &
sleep 2
pid="$(workout_detector_pid)"
if [ -n "$pid" ]; then
echo "Workout detector started (PID $pid)"
else
echo "ERROR: Workout detector failed to start. Check log: $WORKOUT_DETECTOR_LOG"
fi
}
cmd_workout_stop() {
local pid
pid="$(workout_detector_pid)"
if [ -z "$pid" ]; then
echo "Workout detector not running"
rm -f "$WORKOUT_PIDFILE"
else
kill -TERM "$pid"
echo "Workout detector stopped (sent SIGTERM to PID $pid)"
fi
# Fail-closed on manual stop: write 0 so the hosts enforcer reverts to
# the full-block canonical and YouTube goes back to being blocked.
printf '0\n' > "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true
chmod 666 "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true
echo "workout_active flag forced to 0"
}
cmd_workout_log() {
local lines="${1:-50}"
if [ -f "$WORKOUT_DETECTOR_LOG" ]; then
tail -n "$lines" "$WORKOUT_DETECTOR_LOG"
else
echo "Workout detector log not found: $WORKOUT_DETECTOR_LOG"
fi
}
# ============================================================
# Night-curfew control (see curfew_enforcer.sh / focus_daemon.sh)
# ============================================================
CURFEW_PIDFILE="$STATE_DIR/curfew_enforcer.pid"
curfew_enforcer_pid() {
if [ -f "$CURFEW_PIDFILE" ]; then
local pid
pid="$(cat "$CURFEW_PIDFILE")"
if kill -0 "$pid" 2>/dev/null; then
echo "$pid"
fi
fi
}
# Replicates focus_daemon.sh::is_curfew_now for status display.
_ctl_dec() {
local n="$1"
while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done
printf '%s' "$n"
}
ctl_is_curfew_now() {
local now start end
now="$(date +%H%M 2>/dev/null)"
case "$now" in ''|*[!0-9]*) return 1 ;; esac
now="$(_ctl_dec "$now")"; start="$(_ctl_dec "$NIGHT_CURFEW_START")"; end="$(_ctl_dec "$NIGHT_CURFEW_END")"
if [ "$start" -le "$end" ]; then
[ "$now" -ge "$start" ] && [ "$now" -lt "$end" ]
else
[ "$now" -ge "$start" ] || [ "$now" -lt "$end" ]
fi
}
cmd_curfew_status() {
local pid
pid="$(curfew_enforcer_pid)"
echo "=== Night Curfew Status ==="
echo "Enabled: ${NIGHT_CURFEW_ENABLED}"
echo "Window: ${NIGHT_CURFEW_START}-${NIGHT_CURFEW_END} (now $(date +%H%M))"
if [ -n "$pid" ]; then
echo "Enforcer: RUNNING (PID $pid)"
else
echo "Enforcer: STOPPED"
fi
ctl_is_curfew_now && echo "Within window: YES" || echo "Within window: no"
[ -e "$CURFEW_FORCE_FILE" ] && echo "Forced ON: YES (test hook active)"
[ -e "$CURFEW_OVERRIDE_FILE" ] && echo "Override: YES (curfew SUSPENDED)"
if [ -f "$MODE_FILE" ]; then
echo "Focus mode: $(cat "$MODE_FILE" 2>/dev/null)"
fi
[ -e "$CURFEW_ENFORCER_STATE" ] && echo "Applied now: YES (grayscale/DND locked)" \
|| echo "Applied now: no"
echo "Grayscale: ${CURFEW_GRAYSCALE_ENABLED} DND: ${CURFEW_DND_ENABLED} Net: ${CURFEW_NET_ENABLED}"
if iptables -L "$CURFEW_NET_IPT_CHAIN" >/dev/null 2>&1; then
echo "iptables $CURFEW_NET_IPT_CHAIN: $(iptables -S "$CURFEW_NET_IPT_CHAIN" 2>/dev/null | wc -l) rules"
else
echo "iptables $CURFEW_NET_IPT_CHAIN: absent (net curfew not applied)"
fi
if [ -f "$STATE_DIR/night_whitelist.txt" ]; then
echo "Night whitelist: $(wc -l < "$STATE_DIR/night_whitelist.txt" | tr -d ' ') apps allowed"
fi
}
cmd_curfew_start() {
local pid
pid="$(curfew_enforcer_pid)"
if [ -n "$pid" ]; then
echo "Curfew enforcer already running (PID $pid)"
return
fi
setsid sh "$SCRIPT_DIR/curfew_enforcer.sh" </dev/null >/dev/null 2>&1 &
sleep 2
pid="$(curfew_enforcer_pid)"
if [ -n "$pid" ]; then
echo "Curfew enforcer started (PID $pid)"
else
echo "ERROR: curfew enforcer failed to start. Check log: $CURFEW_ENFORCER_LOG"
fi
}
cmd_curfew_stop() {
local pid
pid="$(curfew_enforcer_pid)"
if [ -n "$pid" ]; then
kill "$pid" 2>/dev/null
echo "Curfew enforcer stopped (PID $pid) - daytime state restored"
else
echo "Curfew enforcer not running"
fi
}
cmd_curfew_log() { tail -n "${1:-50}" "$CURFEW_ENFORCER_LOG" 2>/dev/null || echo "No curfew log yet."; }
# Test hook: force curfew ACTIVE regardless of clock/location so the whole
# stack can be validated during the day. Daemon re-checks within one tick.
cmd_curfew_test_on() {
touch "$CURFEW_FORCE_FILE"; chmod 666 "$CURFEW_FORCE_FILE" 2>/dev/null || true
rm -f "$CURFEW_OVERRIDE_FILE"
touch "$RECHECK_TRIGGER" 2>/dev/null || true
echo "Curfew FORCED ON. App sweep + enforcer will engage within a few seconds."
echo "Validate: open mBank (works), keyboard (works), Firefox (gone). Then: curfew-test-off"
}
cmd_curfew_test_off() {
rm -f "$CURFEW_FORCE_FILE"
touch "$RECHECK_TRIGGER" 2>/dev/null || true
echo "Curfew force cleared. Back to clock-based behaviour."
}
# Escape hatch: suspend curfew now (the 2am 'let me out' button). Survives
# until you re-arm. Reachable on-device only via ADB (this command) unless a
# root file-manager/terminal has been added to NIGHT_WHITELIST.
cmd_curfew_off() {
touch "$CURFEW_OVERRIDE_FILE"; chmod 666 "$CURFEW_OVERRIDE_FILE" 2>/dev/null || true
touch "$RECHECK_TRIGGER" 2>/dev/null || true
echo "Curfew SUSPENDED (override set: $CURFEW_OVERRIDE_FILE). Re-arm with: curfew-on"
}
cmd_curfew_on() {
rm -f "$CURFEW_OVERRIDE_FILE"
touch "$RECHECK_TRIGGER" 2>/dev/null || true
echo "Curfew re-armed (override cleared)."
}
2026-02-22 16:57:36 +01:00
case "$1" in
start) cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
enable) cmd_enable ;;
disable) cmd_disable ;;
log) cmd_log "${2:-50}" ;;
list-apps) cmd_list_apps ;;
whitelist) cmd_whitelist ;;
restart) cmd_stop; sleep 2; cmd_start ;;
hosts-status) cmd_hosts_status ;;
hosts-start) cmd_hosts_start ;;
hosts-stop) cmd_hosts_stop ;;
hosts-log) cmd_hosts_log "${2:-50}" ;;
dns-status) cmd_dns_status ;;
dns-start) cmd_dns_start ;;
dns-stop) cmd_dns_stop ;;
dns-log) cmd_dns_log "${2:-50}" ;;
launcher-status) cmd_launcher_status ;;
launcher-start) cmd_launcher_start ;;
launcher-stop) cmd_launcher_stop ;;
launcher-log) cmd_launcher_log "${2:-50}" ;;
launcher-snapshot) cmd_launcher_snapshot ;;
workout-status) cmd_workout_status ;;
workout-start) cmd_workout_start ;;
workout-stop) cmd_workout_stop ;;
workout-log) cmd_workout_log "${2:-50}" ;;
recheck) cmd_recheck ;;
notif-status) cmd_notif_status ;;
curfew-status) cmd_curfew_status ;;
curfew-start) cmd_curfew_start ;;
curfew-stop) cmd_curfew_stop ;;
curfew-log) cmd_curfew_log "${2:-50}" ;;
curfew-test-on) cmd_curfew_test_on ;;
curfew-test-off) cmd_curfew_test_off ;;
curfew-off) cmd_curfew_off ;;
curfew-on) cmd_curfew_on ;;
2026-02-22 16:57:36 +01:00
*) usage ;;
esac