mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
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
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>
544 lines
18 KiB
Bash
Executable File
544 lines
18 KiB
Bash
Executable File
#!/system/bin/sh
|
|
# shellcheck shell=ash
|
|
# ============================================================
|
|
# Focus Mode Daemon
|
|
# Runs on rooted Android device. Periodically checks GPS
|
|
# location and restricts non-whitelisted apps when near home.
|
|
# ============================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
. "$SCRIPT_DIR/config.sh"
|
|
|
|
PIDFILE="$STATE_DIR/daemon.pid"
|
|
|
|
# ---- PID lock: exit if already running ----
|
|
acquire_lock() {
|
|
mkdir -p "$STATE_DIR"
|
|
if [ -f "$PIDFILE" ]; then
|
|
local old_pid
|
|
old_pid="$(cat "$PIDFILE")"
|
|
if kill -0 "$old_pid" 2>/dev/null; then
|
|
# Verify the PID is actually a focus_daemon, not a reused PID
|
|
local cmdline
|
|
cmdline="$(tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null)"
|
|
if echo "$cmdline" | grep -q "focus_daemon"; then
|
|
echo "Daemon already running (PID $old_pid), exiting."
|
|
exit 0
|
|
fi
|
|
fi
|
|
# Stale or reused pidfile
|
|
rm -f "$PIDFILE"
|
|
fi
|
|
echo $$ >"$PIDFILE"
|
|
}
|
|
|
|
# ---- Logging ----
|
|
log() {
|
|
local ts
|
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
echo "[$ts] $1" >>"$LOG_FILE"
|
|
}
|
|
|
|
rotate_log() {
|
|
local lines
|
|
lines="$(wc -l <"$LOG_FILE" 2>/dev/null || echo 0)"
|
|
if [ "$lines" -gt "$LOG_MAX_LINES" ]; then
|
|
local tmp="$LOG_FILE.tmp"
|
|
tail -n "$LOG_MAX_LINES" "$LOG_FILE" >"$tmp"
|
|
mv "$tmp" "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
# ---- Build helper files for fast package checks ----
|
|
|
|
build_whitelist_file() {
|
|
echo "$WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' |
|
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/whitelist.txt"
|
|
# Sanity check: the WHITELIST string in config.sh is fragile - any
|
|
# literal double-quote inside a comment will close the heredoc and
|
|
# silently truncate the variable. Log the parsed line count so any
|
|
# future regression is visible in the log, and warn loudly if it
|
|
# falls below a known floor (we always have ~70+ entries).
|
|
local n
|
|
n=$(wc -l <"$STATE_DIR/whitelist.txt" 2>/dev/null | tr -d ' ')
|
|
log "Whitelist parsed: $n entries"
|
|
if [ "${n:-0}" -lt 30 ]; then
|
|
log "WARN: whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside WHITELIST string"
|
|
fi
|
|
}
|
|
|
|
build_night_whitelist_file() {
|
|
# Strict allow-list used while the night curfew is active (see config.sh
|
|
# NIGHT_WHITELIST and is_curfew_now()). Parsed exactly like the day list.
|
|
echo "$NIGHT_WHITELIST" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' |
|
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/night_whitelist.txt"
|
|
local n
|
|
n=$(wc -l <"$STATE_DIR/night_whitelist.txt" 2>/dev/null | tr -d ' ')
|
|
log "Night-curfew whitelist parsed: $n entries"
|
|
if [ "${n:-0}" -lt 10 ]; then
|
|
log "WARN: night whitelist suspiciously small ($n lines) - check config.sh for stray quotes inside NIGHT_WHITELIST string"
|
|
fi
|
|
}
|
|
|
|
build_sysprotect_file() {
|
|
echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' |
|
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/sysprotect.txt"
|
|
}
|
|
|
|
# $BLOCKED_SYSTEM_APPS is a static config value (never changes without a
|
|
# daemon restart), so - like build_sysprotect_file above - this only needs
|
|
# to run once at startup, not every enable_focus_mode() sweep.
|
|
build_blocked_sys_file() {
|
|
echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' |
|
|
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >"$STATE_DIR/blocked_sys.txt"
|
|
}
|
|
|
|
reconcile_disabled_apps() {
|
|
[ -f "$DISABLED_APPS_FILE" ] || return
|
|
|
|
local tmp_disabled="$STATE_DIR/disabled_by_focus.tmp"
|
|
: >"$tmp_disabled"
|
|
|
|
while IFS= read -r pkg; do
|
|
[ -z "$pkg" ] && continue
|
|
|
|
if is_allowed "$pkg"; then
|
|
pm install-existing --user 0 "$pkg" >/dev/null 2>&1 || true
|
|
pm enable "$pkg" >/dev/null 2>&1 || true
|
|
log "Re-enabled allowed app during state reconciliation: $pkg"
|
|
continue
|
|
fi
|
|
|
|
echo "$pkg" >>"$tmp_disabled"
|
|
done <"$DISABLED_APPS_FILE"
|
|
|
|
mv "$tmp_disabled" "$DISABLED_APPS_FILE"
|
|
}
|
|
|
|
# ---- Initialization ----
|
|
init() {
|
|
mkdir -p "$STATE_DIR"
|
|
touch "$LOG_FILE"
|
|
touch "$DISABLED_APPS_FILE"
|
|
# Ensure state files are writable (survives reboot / permission drift)
|
|
chmod 666 "$LOG_FILE" "$DISABLED_APPS_FILE" "$PIDFILE" 2>/dev/null
|
|
# Status file must be world-readable (companion app reads it).
|
|
# State dir must be world-writable+executable so the companion app can
|
|
# drop the recheck trigger file (it runs as a normal app UID).
|
|
chmod 777 "$STATE_DIR" 2>/dev/null
|
|
|
|
if [ "$HOME_LAT" = "0.000000" ] && [ "$HOME_LON" = "0.000000" ]; then
|
|
log "ERROR: Home coordinates not set! Edit config_secrets.sh first."
|
|
exit 1
|
|
fi
|
|
|
|
if ! echo "$HOME_LAT" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then
|
|
log "ERROR: HOME_LAT is invalid ('$HOME_LAT'). Expected decimal degrees in config_secrets.sh"
|
|
exit 1
|
|
fi
|
|
|
|
if ! echo "$HOME_LON" | grep -Eq '^[-]?[0-9]+(\.[0-9]+)?$'; then
|
|
log "ERROR: HOME_LON is invalid ('$HOME_LON'). Expected decimal degrees in config_secrets.sh"
|
|
exit 1
|
|
fi
|
|
|
|
build_whitelist_file
|
|
build_night_whitelist_file
|
|
build_sysprotect_file
|
|
build_blocked_sys_file
|
|
refresh_default_handlers
|
|
rotate_log
|
|
|
|
if [ -f "$MODE_FILE" ]; then
|
|
CURRENT_MODE="$(cat "$MODE_FILE")"
|
|
else
|
|
CURRENT_MODE="normal"
|
|
fi
|
|
|
|
if [ "$CURRENT_MODE" = "focus" ]; then
|
|
reconcile_disabled_apps
|
|
fi
|
|
|
|
log "Focus mode daemon started (PID=$$, mode=$CURRENT_MODE, home=$HOME_LAT,$HOME_LON, radius=${RADIUS}m)"
|
|
log "Intervals: focus=${CHECK_INTERVAL_FOCUS}s normal=${CHECK_INTERVAL_NORMAL}s"
|
|
}
|
|
|
|
# ---- Location ----
|
|
get_location() {
|
|
dumpsys location 2>/dev/null |
|
|
grep -oE '[-]?[0-9]{1,3}\.[0-9]{4,},[-]?[0-9]{1,3}\.[0-9]{4,}' |
|
|
head -1
|
|
}
|
|
|
|
# ---- Distance Calculation (Haversine via awk) ----
|
|
calc_distance() {
|
|
echo "$1 $2 $3 $4" | awk '{
|
|
PI = 3.14159265358979323846
|
|
R = 6371000.0
|
|
lat1 = $1 * PI / 180.0
|
|
lon1 = $2 * PI / 180.0
|
|
lat2 = $3 * PI / 180.0
|
|
lon2 = $4 * PI / 180.0
|
|
dlat = lat2 - lat1
|
|
dlon = lon2 - lon1
|
|
sdlat = sin(dlat / 2.0)
|
|
sdlon = sin(dlon / 2.0)
|
|
a = sdlat * sdlat + cos(lat1) * cos(lat2) * sdlon * sdlon
|
|
c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a))
|
|
printf "%d\n", R * c
|
|
}'
|
|
}
|
|
|
|
# ---- Night curfew time check ----
|
|
# Returns 0 (true) when the local clock is inside the curfew window.
|
|
# Fails OPEN (return 1 = not curfew) on a malformed clock so a broken `date`
|
|
# can never strand you behind the strict list — essentials stay reachable
|
|
# either way, but the day list is the less-surprising default.
|
|
_dec() {
|
|
# Strip leading zeros so a zero-padded HHMM ("0500", "0830") is not parsed
|
|
# as (sometimes invalid) octal by the shell's arithmetic. Portable across
|
|
# ash/mksh; keeps at least one digit so "0000" -> "0".
|
|
local n="$1"
|
|
while [ "${n#0}" != "$n" ] && [ "${#n}" -gt 1 ]; do n="${n#0}"; done
|
|
printf '%s' "$n"
|
|
}
|
|
|
|
is_curfew_now() {
|
|
local now start end
|
|
now="$(date +%H%M 2>/dev/null)"
|
|
case "$now" in
|
|
'' | *[!0-9]*) return 1 ;;
|
|
esac
|
|
now="$(_dec "$now")"
|
|
start="$(_dec "$NIGHT_CURFEW_START")"
|
|
end="$(_dec "$NIGHT_CURFEW_END")"
|
|
if [ "$start" -le "$end" ]; then
|
|
[ "$now" -ge "$start" ] && [ "$now" -lt "$end" ]
|
|
else
|
|
# Window wraps past midnight (e.g. 2300 -> 0500).
|
|
[ "$now" -ge "$start" ] || [ "$now" -lt "$end" ]
|
|
fi
|
|
}
|
|
|
|
# Curfew is ACTIVE when enabled, not manually overridden, and either forced on
|
|
# (test hook) or inside the time window. The is_allowed() switch below consults
|
|
# this; because is_allowed() only runs during the focus-mode sweep/reconcile,
|
|
# curfew automatically takes effect only at home and is a no-op when away.
|
|
#
|
|
# Memoized once per main-loop tick (reset at the top of main()'s while loop):
|
|
# is_allowed() calls this once per enabled package in the sweep, and
|
|
# is_curfew_now forks `date` - on-device this meant up to ~N extra forks per
|
|
# 10s tick for N enabled apps, all recomputing a value that cannot change
|
|
# within a single tick. Result is cached in _CURFEW_TICK_RESULT.
|
|
curfew_active() {
|
|
if [ -n "$_CURFEW_TICK_CACHED" ]; then
|
|
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
|
return
|
|
fi
|
|
_CURFEW_TICK_CACHED=1
|
|
_CURFEW_TICK_RESULT=0
|
|
if [ "${NIGHT_CURFEW_ENABLED:-0}" = "1" ] && [ ! -e "$CURFEW_OVERRIDE_FILE" ]; then
|
|
if [ -e "$CURFEW_FORCE_FILE" ] || is_curfew_now; then
|
|
_CURFEW_TICK_RESULT=1
|
|
fi
|
|
fi
|
|
[ "$_CURFEW_TICK_RESULT" = "1" ]
|
|
}
|
|
|
|
# ---- Check if package is allowed (whitelist or system-protected) ----
|
|
is_allowed() {
|
|
local pkg="$1"
|
|
# During the night curfew, swap the permissive day list for the strict
|
|
# night list. The sysprotect + default-handler guards below still apply on
|
|
# top of whichever list is active.
|
|
local list="$STATE_DIR/whitelist.txt"
|
|
if curfew_active; then
|
|
list="$STATE_DIR/night_whitelist.txt"
|
|
fi
|
|
# Exact match against the active whitelist file
|
|
if grep -qxF "$pkg" "$list" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
# Prefix match against system-protect file
|
|
while IFS= read -r prefix; do
|
|
[ -z "$prefix" ] && continue
|
|
case "$pkg" in
|
|
"$prefix"*) return 0 ;;
|
|
esac
|
|
done <"$STATE_DIR/sysprotect.txt"
|
|
# Hard-stop guard: refuse to disable any package that is the current
|
|
# default handler for a critical role (Dialer / SMS / Home / Contacts).
|
|
# Without this, a misconfigured WHITELIST can disable the default Phone
|
|
# app and Android falls back to com.android.settings/.FallbackHome -
|
|
# the persistent "Phone is starting..." screen with broken SystemUI
|
|
# gestures (no swipe-up recents). Recovering requires `pm enable` over
|
|
# ADB. Treat the guard as last-resort safety net independent of WHITELIST
|
|
# contents so a future config edit can never wipe these out.
|
|
is_default_handler "$pkg" && return 0
|
|
# The default browser is guarded only OUTSIDE curfew. At night the whole
|
|
# point is to disable browsers, so this guard must not re-allow it.
|
|
if ! curfew_active &&
|
|
grep -qxF "$pkg" "$STATE_DIR/default_browser.txt" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# ---- Default handler detection ----
|
|
# Refreshed once per focus_daemon tick into $STATE_DIR/default_handlers.txt.
|
|
# Each line is a package name. Lookup is a cheap grep against this file.
|
|
refresh_default_handlers() {
|
|
local f="$STATE_DIR/default_handlers.txt"
|
|
local tmp="$f.tmp"
|
|
: >"$tmp"
|
|
# Default Home (launcher). resolve-activity prints "Activity Resolver Table:"
|
|
# on line 1 and "<pkg>/<.Activity>" on line 2 in --brief mode.
|
|
cmd package resolve-activity --brief \
|
|
-c android.intent.category.HOME -a android.intent.action.MAIN 2>/dev/null |
|
|
awk -F/ 'NR==2 && $1 != "" {print $1}' >>"$tmp"
|
|
# Default Dialer
|
|
local dialer
|
|
dialer="$(cmd telecom get-default-dialer 2>/dev/null | tr -d '[:space:]')"
|
|
[ -n "$dialer" ] && echo "$dialer" >>"$tmp"
|
|
# Default SMS handler (settings provider key)
|
|
local sms
|
|
sms="$(settings get secure sms_default_application 2>/dev/null | tr -d '[:space:]')"
|
|
[ -n "$sms" ] && [ "$sms" != "null" ] && echo "$sms" >>"$tmp"
|
|
# Default input method (active keyboard). Disabling the active IME with
|
|
# pm disable-user PERSISTS across reboot; a 1am reboot would then leave no
|
|
# keyboard to type any recovery command. Protect it day and night so the
|
|
# curfew can never lock you out of typing.
|
|
local ime
|
|
ime="$(settings get secure default_input_method 2>/dev/null | cut -d/ -f1)"
|
|
[ -n "$ime" ] && [ "$ime" != "null" ] && echo "$ime" >>"$tmp"
|
|
sort -u "$tmp" -o "$f"
|
|
rm -f "$tmp"
|
|
|
|
# Default Browser handler is tracked SEPARATELY and guarded only OUTSIDE
|
|
# the curfew window (see is_allowed). During curfew the whole point is to
|
|
# disable browsers, so the default-handler guard must not resurrect them.
|
|
local bf="$STATE_DIR/default_browser.txt"
|
|
cmd package resolve-activity --brief \
|
|
-a android.intent.action.VIEW -d http://example.com 2>/dev/null |
|
|
awk -F/ 'NR==2 && $1 != "" {print $1}' >"$bf.tmp" 2>/dev/null
|
|
mv "$bf.tmp" "$bf" 2>/dev/null || : >"$bf"
|
|
}
|
|
|
|
is_default_handler() {
|
|
local pkg="$1"
|
|
grep -qxF "$pkg" "$STATE_DIR/default_handlers.txt" 2>/dev/null
|
|
}
|
|
|
|
# ---- Focus Mode Control ----
|
|
|
|
enable_focus_mode() {
|
|
local first_entry=0
|
|
if [ "$CURRENT_MODE" != "focus" ]; then
|
|
first_entry=1
|
|
log "ENABLING focus mode - restricting non-whitelisted apps"
|
|
: >"$DISABLED_APPS_FILE"
|
|
fi
|
|
|
|
# Refresh default-handler list every tick. The user may switch dialer /
|
|
# SMS / launcher between sweeps; the guard in is_allowed() consults this
|
|
# list so a newly-promoted handler is never disabled.
|
|
refresh_default_handlers
|
|
|
|
# Blocked system app list is static; built once in init() (see
|
|
# build_blocked_sys_file), not rebuilt on every sweep.
|
|
local blocked_sys="$STATE_DIR/blocked_sys.txt"
|
|
|
|
# Periodic rescan catches third-party apps the user re-enabled (e.g. via
|
|
# Play Store or `pm enable` in a terminal) since the last tick.
|
|
# -e = enabled only, so we skip apps that are already disabled.
|
|
local tmp_pkgs="$STATE_DIR/pkg_list.txt"
|
|
pm list packages -3 -e 2>/dev/null | sed 's/^package://' >"$tmp_pkgs"
|
|
local newly_disabled=0
|
|
while IFS= read -r pkg; do
|
|
[ -z "$pkg" ] && continue
|
|
is_allowed "$pkg" && continue
|
|
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
|
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null ||
|
|
echo "$pkg" >>"$DISABLED_APPS_FILE"
|
|
newly_disabled=$((newly_disabled + 1))
|
|
fi
|
|
done <"$tmp_pkgs"
|
|
rm -f "$tmp_pkgs"
|
|
|
|
# Disable-for-user-0 any blocked system apps (Play Store, browsers,
|
|
# package installer UI, terminal apps).
|
|
# IMPORTANT: We intentionally use pm disable-user (NOT pm uninstall) here.
|
|
# pm uninstall -k --user 0 removes the package from Android's user-0
|
|
# package registry. If the daemon is killed with SIGKILL during a reboot
|
|
# (bypassing the cleanup trap), those packages stay uninstalled across the
|
|
# reboot. Android's bootloop-protection (MTK and others) then detects
|
|
# missing critical system packages and triggers recovery / factory wipe.
|
|
# pm disable-user leaves the package registered but inactive, so the
|
|
# PackageManager scan at next boot succeeds and no wipe occurs.
|
|
while IFS= read -r pkg; do
|
|
[ -z "$pkg" ] && continue
|
|
if pm disable-user --user 0 "$pkg" >/dev/null 2>&1; then
|
|
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null ||
|
|
echo "$pkg" >>"$DISABLED_APPS_FILE"
|
|
newly_disabled=$((newly_disabled + 1))
|
|
fi
|
|
done <"$blocked_sys"
|
|
|
|
CURRENT_MODE="focus"
|
|
echo "focus" >"$MODE_FILE"
|
|
|
|
if [ "$first_entry" -eq 1 ]; then
|
|
local count
|
|
count=$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null || echo 0)
|
|
log "Focus mode enabled - disabled $count apps"
|
|
elif [ "$newly_disabled" -gt 0 ]; then
|
|
log "Focus mode re-sweep: re-disabled $newly_disabled apps (re-enabled by user?)"
|
|
fi
|
|
|
|
reconcile_disabled_apps
|
|
}
|
|
|
|
disable_focus_mode() {
|
|
[ "$CURRENT_MODE" = "normal" ] && return
|
|
log "DISABLING focus mode - re-enabling apps"
|
|
|
|
local count=0
|
|
if [ -f "$DISABLED_APPS_FILE" ] && [ -s "$DISABLED_APPS_FILE" ]; then
|
|
# Re-enable all disabled apps (both 3rd-party and system apps).
|
|
# Both paths now use pm disable-user, so pm enable is the only
|
|
# restore command needed.
|
|
while IFS= read -r pkg; do
|
|
[ -z "$pkg" ] && continue
|
|
pm enable "$pkg" >/dev/null 2>&1 && count=$((count + 1))
|
|
done <"$DISABLED_APPS_FILE"
|
|
: >"$DISABLED_APPS_FILE"
|
|
fi
|
|
|
|
CURRENT_MODE="normal"
|
|
echo "normal" >"$MODE_FILE"
|
|
log "Focus mode disabled - re-enabled $count apps"
|
|
}
|
|
|
|
# ---- Status snapshot for companion notification app ----
|
|
# Writes a tiny JSON file that focus_status_app reads every few seconds.
|
|
# Fields: mode, lat, lon, distance_m, threshold_m, radius_m, disabled_count,
|
|
# last_check_ts (unix), last_check_iso (human).
|
|
write_status_snapshot() {
|
|
local mode="$1" lat="$2" lon="$3" dist="$4" thr="$5"
|
|
local count iso ts cf ov frc
|
|
count="$(wc -l <"$DISABLED_APPS_FILE" 2>/dev/null | tr -d ' ' || echo 0)"
|
|
[ -z "$count" ] && count=0
|
|
ts="$(date +%s)"
|
|
iso="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
# Curfew state for the companion app: 1/0 so it slots into the existing
|
|
# numeric JSON path. "curfew" = restrictions active now; "curfew_override"
|
|
# = the escape-hatch file is set (curfew suspended).
|
|
if curfew_active; then cf=1; else cf=0; fi
|
|
if [ -e "$CURFEW_OVERRIDE_FILE" ]; then ov=1; else ov=0; fi
|
|
# "curfew_force" = the demo/test force file is set (curfew forced on
|
|
# regardless of clock). Lets the companion app show Start/Stop demo.
|
|
if [ -e "$CURFEW_FORCE_FILE" ]; then frc=1; else frc=0; fi
|
|
local tmp="$STATUS_FILE.tmp"
|
|
# Shell-emitted JSON — keep values numeric where possible, strings quoted.
|
|
{
|
|
printf '{'
|
|
printf '"mode":"%s",' "$mode"
|
|
printf '"lat":"%s",' "${lat:-}"
|
|
printf '"lon":"%s",' "${lon:-}"
|
|
printf '"distance_m":%s,' "${dist:-null}"
|
|
printf '"threshold_m":%s,' "${thr:-null}"
|
|
printf '"radius_m":%s,' "$RADIUS"
|
|
printf '"disabled_count":%s,' "$count"
|
|
printf '"curfew":%s,' "$cf"
|
|
printf '"curfew_override":%s,' "$ov"
|
|
printf '"curfew_force":%s,' "$frc"
|
|
printf '"last_check_ts":%s,' "$ts"
|
|
printf '"last_check_iso":"%s"' "$iso"
|
|
printf '}\n'
|
|
} >"$tmp" 2>/dev/null || return 0
|
|
mv "$tmp" "$STATUS_FILE" 2>/dev/null || true
|
|
chmod 644 "$STATUS_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
# ---- Sleep with early-wake on recheck trigger ----
|
|
# Polls for $RECHECK_TRIGGER every second; if found, consumes it and returns
|
|
# early. The file can be touched by the companion app (via "Re-check now"
|
|
# button) or by `focus_ctl.sh recheck` from a shell.
|
|
sleep_with_recheck() {
|
|
local total="$1"
|
|
local elapsed=0
|
|
while [ "$elapsed" -lt "$total" ]; do
|
|
if [ -e "$RECHECK_TRIGGER" ]; then
|
|
rm -f "$RECHECK_TRIGGER" 2>/dev/null
|
|
log "Manual re-check triggered"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
elapsed=$((elapsed + 1))
|
|
done
|
|
}
|
|
|
|
# ---- Signal handlers ----
|
|
cleanup() {
|
|
log "Daemon shutting down - re-enabling all apps"
|
|
disable_focus_mode
|
|
rm -f "$PIDFILE"
|
|
exit 0
|
|
}
|
|
|
|
# HUP is intentionally NOT trapped so the daemon survives ADB disconnects.
|
|
# Only SIGTERM/SIGINT trigger a clean shutdown.
|
|
trap cleanup INT TERM
|
|
|
|
# ---- Main Loop ----
|
|
main() {
|
|
acquire_lock
|
|
init
|
|
|
|
while true; do
|
|
# Invalidate the per-tick curfew_active() memo (see its definition).
|
|
_CURFEW_TICK_CACHED=""
|
|
|
|
location="$(get_location)"
|
|
|
|
if [ -n "$location" ]; then
|
|
lat="$(echo "$location" | cut -d',' -f1)"
|
|
lon="$(echo "$location" | cut -d',' -f2)"
|
|
distance="$(calc_distance "$lat" "$lon" "$HOME_LAT" "$HOME_LON")"
|
|
|
|
if [ "$CURRENT_MODE" = "focus" ]; then
|
|
threshold=$((RADIUS + HYSTERESIS))
|
|
else
|
|
threshold=$((RADIUS - HYSTERESIS))
|
|
fi
|
|
|
|
if [ "$distance" -le "$threshold" ] 2>/dev/null; then
|
|
enable_focus_mode
|
|
else
|
|
disable_focus_mode
|
|
fi
|
|
|
|
curfew_state="day"
|
|
curfew_active && curfew_state="CURFEW"
|
|
log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE | Curfew: $curfew_state"
|
|
write_status_snapshot "$CURRENT_MODE" "$lat" "$lon" "$distance" "$threshold"
|
|
else
|
|
log "Location unavailable - defaulting to focus mode (restrictions ON)"
|
|
enable_focus_mode
|
|
write_status_snapshot "$CURRENT_MODE" "" "" "null" "null"
|
|
fi
|
|
|
|
# Dynamic interval: shorter at home (can charge), longer away (save battery).
|
|
# sleep_with_recheck returns early if the companion app requests a recheck.
|
|
if [ "$CURRENT_MODE" = "focus" ]; then
|
|
sleep_with_recheck "$CHECK_INTERVAL_FOCUS"
|
|
else
|
|
sleep_with_recheck "$CHECK_INTERVAL_NORMAL"
|
|
fi
|
|
|
|
rotate_log
|
|
done
|
|
}
|
|
|
|
main "$@"
|