mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 15:03:01 +02:00
- New companion Android app (com.kuhy.focusstatus) under phone_focus_mode/focus_status_app/ with a pure-Java, Gradle-less command-line build pipeline (build.sh). Shows an ongoing notification titled 'Focus: HOME / AWAY / DAEMON DOWN' with distance, GPS, disabled-app count, last check, daemon checkmarks, and a 'Re-check now' action button. - focus_daemon.sh: write_status_snapshot() + sleep_with_recheck() for JSON status + early-wake on trigger file. init() chmods STATE_DIR 777 so the app can drop the trigger file. - config.sh: new STATUS_FILE / RECHECK_TRIGGER; WHITELIST expanded with com.kuhy.focusstatus and 11 more user-requested apps (podcini X, mpv, bible/openbible, pkp/portalpasazera, orange, runnerup, splitbills/splitwise, xiaomi smarthome). - focus_ctl.sh: new 'recheck' + 'notif-status' subcommands. - deploy.sh: new step [7/7] builds APK, installs, grants POST_NOTIFICATIONS, pre-approves Magisk SU policy, launches foreground service. - .gitignore: exclude focus_status_app/build symlink + debug.keystore. End-to-end verified on device: notification live with real values; Re-check button triggers a daemon location check within ~1s.
365 lines
12 KiB
Bash
Executable File
365 lines
12 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="$(cat /proc/$old_pid/cmdline 2>/dev/null | tr '\0' ' ')"
|
|
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"
|
|
}
|
|
|
|
build_sysprotect_file() {
|
|
echo "$SYSTEM_NEVER_DISABLE" | grep -v '^[[:space:]]*$' \
|
|
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$STATE_DIR/sysprotect.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.sh first."
|
|
exit 1
|
|
fi
|
|
|
|
build_whitelist_file
|
|
build_sysprotect_file
|
|
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
|
|
}'
|
|
}
|
|
|
|
# ---- Check if package is allowed (whitelist or system-protected) ----
|
|
is_allowed() {
|
|
local pkg="$1"
|
|
# Exact match against whitelist file
|
|
if grep -qxF "$pkg" "$STATE_DIR/whitelist.txt" 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"
|
|
return 1
|
|
}
|
|
|
|
# ---- 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
|
|
|
|
# Build blocked system app list (used both at entry and for periodic sweep)
|
|
local blocked_sys="$STATE_DIR/blocked_sys.txt"
|
|
echo "$BLOCKED_SYSTEM_APPS" | grep -v '^[[:space:]]*#' | grep -v '^[[:space:]]*$' \
|
|
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' > "$blocked_sys"
|
|
|
|
# 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"
|
|
|
|
# Uninstall-for-user-0 any blocked system apps (Play Store, browsers,
|
|
# package installer UI, terminal apps). pm uninstall is idempotent:
|
|
# re-running it on already-uninstalled-for-user-0 packages is a no-op.
|
|
local uninstalled_sys="$STATE_DIR/uninstalled_sys.txt"
|
|
[ "$first_entry" -eq 1 ] && : > "$uninstalled_sys"
|
|
# List of packages installed for user 0 (one per line, "package:" prefix).
|
|
local user0_pkgs="$STATE_DIR/user0_pkgs.txt"
|
|
pm list packages --user 0 2>/dev/null | sed 's/^package://' > "$user0_pkgs"
|
|
while IFS= read -r pkg; do
|
|
[ -z "$pkg" ] && continue
|
|
if grep -qxF "$pkg" "$user0_pkgs" 2>/dev/null; then
|
|
if pm uninstall -k --user 0 "$pkg" >/dev/null 2>&1; then
|
|
grep -qxF "$pkg" "$uninstalled_sys" 2>/dev/null \
|
|
|| echo "$pkg" >> "$uninstalled_sys"
|
|
grep -qxF "$pkg" "$DISABLED_APPS_FILE" 2>/dev/null \
|
|
|| echo "$pkg" >> "$DISABLED_APPS_FILE"
|
|
newly_disabled=$((newly_disabled + 1))
|
|
fi
|
|
fi
|
|
done < "$blocked_sys"
|
|
rm -f "$user0_pkgs"
|
|
|
|
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-install system apps that were uninstalled for user
|
|
if [ -f "$STATE_DIR/uninstalled_sys.txt" ] && [ -s "$STATE_DIR/uninstalled_sys.txt" ]; then
|
|
while IFS= read -r pkg; do
|
|
[ -z "$pkg" ] && continue
|
|
pm install-existing --user 0 "$pkg" >/dev/null 2>&1
|
|
done < "$STATE_DIR/uninstalled_sys.txt"
|
|
: > "$STATE_DIR/uninstalled_sys.txt"
|
|
fi
|
|
# Re-enable all disabled apps
|
|
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
|
|
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')"
|
|
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 '"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
|
|
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
|
|
|
|
log "Location: $lat,$lon | Distance: ${distance}m | Threshold: ${threshold}m | Mode: $CURRENT_MODE"
|
|
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 "$@"
|