testsAndMisc/phone_focus_mode/workout_detector.sh

175 lines
5.5 KiB
Bash
Raw Normal View History

#!/system/bin/sh
# shellcheck shell=ash
# ============================================================
# Workout detector for rooted Android.
#
# Why this exists:
# The user wants YouTube unblocked ONLY while a StrongLifts workout
# is currently in progress (i.e. started but not yet finished). This
# daemon writes a 1/0 flag to $WORKOUT_ACTIVE_FILE; hosts_enforcer.sh
# reads the flag and swaps the active canonical hosts file between
# the full block ($HOSTS_CANONICAL) and the workout-relaxed variant
# ($HOSTS_CANONICAL_WORKOUT) on transitions.
#
# Detection signal:
# StrongLifts persists every workout to $WORKOUT_DB_PATH (SQLite).
# The `workouts` table has columns `start` (epoch ms) and `finish`
# (epoch ms, NULL/0 while in progress). The single source of truth:
#
# SELECT COUNT(*) FROM workouts
# WHERE start > 0 AND (finish IS NULL OR finish = 0);
#
# Returns 1 during a workout, 0 otherwise. Verified empirically: every
# completed row in the user's history has both fields populated; only
# live workouts leave finish=NULL.
#
# Why other signals were rejected:
# * stronglifts_timer_running pref → only true between sets (rest
# timer); flips on/off every minute during a workout.
# * Foreground notification → posted only during rest timer.
# * Foreground activity → only true when actively staring at the app,
# which is rarely the case while lifting.
#
# Failure mode:
# Fail closed. Any error (sqlite3 missing, DB locked, query non-zero
# exit, malformed output) writes "0" so YouTube stays blocked. Stale
# data is preferred over an open door.
#
# Read-only DB access:
# Uses sqlite3's URI form `file:<path>?mode=ro` to avoid touching the
# app's WAL/SHM files or holding a write lock that StrongLifts could
# contend with.
# ============================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=config.sh
. "$SCRIPT_DIR/config.sh"
PIDFILE="$STATE_DIR/workout_detector.pid"
mkdir -p "$STATE_DIR"
touch "$WORKOUT_DETECTOR_LOG"
chmod 666 "$WORKOUT_DETECTOR_LOG" 2>/dev/null || true
log() {
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$ts] $1" >> "$WORKOUT_DETECTOR_LOG"
}
rotate_log() {
local lines
lines="$(wc -l < "$WORKOUT_DETECTOR_LOG" 2>/dev/null || echo 0)"
if [ "$lines" -gt 500 ]; then
local tmp="$WORKOUT_DETECTOR_LOG.tmp"
tail -n 500 "$WORKOUT_DETECTOR_LOG" > "$tmp"
mv "$tmp" "$WORKOUT_DETECTOR_LOG"
fi
}
acquire_lock() {
if [ -f "$PIDFILE" ]; then
local old_pid
old_pid="$(cat "$PIDFILE")"
if kill -0 "$old_pid" 2>/dev/null; then
local cmdline
cmdline="$(tr '\0' ' ' < "/proc/$old_pid/cmdline" 2>/dev/null)"
if echo "$cmdline" | grep -q "workout_detector"; then
echo "workout_detector already running (PID $old_pid)"
exit 0
fi
fi
rm -f "$PIDFILE"
fi
echo $$ > "$PIDFILE"
}
# Write the flag atomically and chmod 666 so other daemons (running under
# different SELinux contexts) can read it. Returns 0 always; callers do not
# branch on success.
write_flag() {
local value="$1"
local tmp="$WORKOUT_ACTIVE_FILE.tmp"
printf '%s\n' "$value" > "$tmp"
mv "$tmp" "$WORKOUT_ACTIVE_FILE"
chmod 666 "$WORKOUT_ACTIVE_FILE" 2>/dev/null || true
}
# Query StrongLifts DB. On success echoes "0" or "1"; on failure echoes
# nothing and returns non-zero so the caller can fail closed.
query_workout_active() {
if [ ! -x "$WORKOUT_SQLITE3_BIN" ]; then
return 1
fi
if [ ! -f "$WORKOUT_DB_PATH" ]; then
# App not installed or DB not yet created → no workout possible.
echo 0
return 0
fi
local count
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>>"$WORKOUT_DETECTOR_LOG"
)" || return 1
case "$count" in
0) echo 0 ;;
[1-9]*) echo 1 ;;
*)
log "ERROR: unexpected sqlite output: '$count'"
return 1
;;
esac
return 0
}
cleanup() {
log "workout_detector shutting down"
# Fail closed on shutdown — assume no workout so YouTube stays blocked.
write_flag 0
rm -f "$PIDFILE"
exit 0
}
trap cleanup INT TERM
main() {
acquire_lock
log "workout_detector started (PID=$$, db=$WORKOUT_DB_PATH, interval=${WORKOUT_DETECTOR_INTERVAL}s)"
local last_state="-1"
while true; do
local new_state
if new_state="$(query_workout_active)"; then
:
else
new_state=0
log "WARN: query failed, defaulting workout_active=0 (fail-closed)"
fi
if [ "$new_state" != "$last_state" ]; then
write_flag "$new_state"
if [ "$new_state" = "1" ]; then
log "STATE: workout STARTED → YouTube unblock requested"
else
# last_state="-1" is the very first iteration — log the
# initial baseline distinctly so it is obvious in the log.
if [ "$last_state" = "-1" ]; then
log "STATE: initial workout_active=0 (no in-progress workout)"
else
log "STATE: workout FINISHED → YouTube re-block requested"
fi
fi
last_state="$new_state"
fi
rotate_log
sleep "$WORKOUT_DETECTOR_INTERVAL"
done
}
main "$@"