mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:07:16 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
210 lines
12 KiB
Python
210 lines
12 KiB
Python
"""Constants for the screen locker module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from gatelock import default_runtime_dir
|
|
from gatelock.log_integrity import DEFAULT_HMAC_KEY_FILE
|
|
|
|
# Ephemeral runtime state (self-clears on reboot, same directory gatelock's
|
|
# own Arbiter uses) -- not repo-tracked data, so it does not belong next to
|
|
# log.json below.
|
|
INSTANCE_LOCK_FILE = default_runtime_dir() / "screen_locker-instance.lock"
|
|
|
|
# Single-sourced from gatelock so the literal key path can't drift. The key is
|
|
# root:root 0644 ON PURPOSE, never 0600: leetcode-guard, book-guard and
|
|
# anki-guard run as user timers and sign their ledgers with it, and this
|
|
# locker signs log.json, rest_days.json and the workout ledger as the user.
|
|
# Nothing in this repo may chmod it; root ownership keeps it from being
|
|
# replaced, which is what the signatures rely on.
|
|
HMAC_KEY_FILE = DEFAULT_HMAC_KEY_FILE
|
|
# The workout ledger (earned_time.WORKOUT.ledger): one signed credit row per
|
|
# verified RunnerUp TCX credit and per rest day, resolved at call time.
|
|
WORKOUT_LEDGER_RELATIVE = Path(".local/share/workout_locker/ledger.json")
|
|
PHONE_PENALTY_DELAY_DEMO = 10
|
|
PHONE_PENALTY_DELAY_PRODUCTION = 100
|
|
# Penalty added to phone-penalty timer when ADB / phone unavailable
|
|
# (so unplugging the phone does not become an easy path to the manual log).
|
|
NO_PHONE_EXTRA_LOCKOUT_SECONDS = 480 # extra 8 minutes on top of base
|
|
# Manual (unverified) workout rate-limiting. Once either
|
|
# window is exhausted the "Log Manual Workout" option disappears entirely.
|
|
MANUAL_WORKOUT_BUDGET_PER_7_DAYS = 2
|
|
MANUAL_WORKOUT_BUDGET_PER_30_DAYS = 10
|
|
# A manual workout may be logged AHEAD for a date up to this many days from
|
|
# today (never a past date); it earns its credit on that date, not before.
|
|
MANUAL_WORKOUT_MAX_DAYS_AHEAD = 7
|
|
# Manual entries use the shared MIN_WORKOUT_DURATION_MINUTES bar below —
|
|
# every workout type answers to the same minimum, whatever the sport.
|
|
# Minimum chars for the "what was done" activity-details field.
|
|
MANUAL_WORKOUT_DESCRIPTION_MIN_CHARS = 40
|
|
# Minimum chars for each reflection field (what went well / to improve / feeling).
|
|
MANUAL_WORKOUT_REFLECTION_MIN_CHARS = 20
|
|
MANUAL_WORKOUT_RPE_MIN = 1
|
|
MANUAL_WORKOUT_RPE_MAX = 10
|
|
ADB_TIMEOUT = 15
|
|
# Workout app JSON candidate paths on the phone, in the order the app prefers
|
|
# when writing (see sync_service.dart). The app writes to the primary /sdcard/
|
|
# path first and only falls back to its app-external files dir if /sdcard/ is
|
|
# not writable, so the locker must check both — primary first.
|
|
WORKOUT_APP_JSON_REMOTES = (
|
|
"/sdcard/workout_result.json",
|
|
"/storage/emulated/0/Android/data/com.kuhy.workout_app/files/workout_result.json",
|
|
)
|
|
# Port the workout app's HTTP server listens on (no ADB/developer-options needed).
|
|
WORKOUT_HTTP_PORT = 8765
|
|
# The ONE duration bar every workout type answers to — weightlifting, manual
|
|
# entries and runs alike. This is the ADVERTISED number: it is the only one
|
|
# that may ever appear in a message the user reads.
|
|
MIN_WORKOUT_DURATION_MINUTES = 40
|
|
# ...and this is the leeway that is deliberately INVISIBLE. A session is
|
|
# actually accepted at MIN_WORKOUT_DURATION_MINUTES - this, so 35 minutes
|
|
# passes while every string still says 40. That gap is intentional (the user
|
|
# asked for it explicitly): do NOT "fix" the inconsistency by advertising the
|
|
# real cutoff, and never interpolate WORKOUT_DURATION_ACCEPT_MINUTES into an
|
|
# f-string.
|
|
WORKOUT_DURATION_LEEWAY_MINUTES = 5
|
|
# Derived so the advertised bar and the accept bar cannot drift apart.
|
|
# Comparisons use this; messages use MIN_WORKOUT_DURATION_MINUTES. Every
|
|
# caller compares with `>=` — one operator across all workout types, so a
|
|
# 35.0-minute session cannot pass one gate and fail another.
|
|
WORKOUT_DURATION_ACCEPT_MINUTES = (
|
|
MIN_WORKOUT_DURATION_MINUTES - WORKOUT_DURATION_LEEWAY_MINUTES
|
|
)
|
|
RUNNERUP_PACKAGES = ("org.runnerup", "org.runnerup.free")
|
|
RUNNERUP_DB_SDCARD_TMP = "/sdcard/.runnerup_tmp_verification.db"
|
|
# A run qualifies on EITHER criterion — they are an OR, not an AND. A short
|
|
# but long-lasting run counts, and so does a fast 5 km that takes under the
|
|
# duration bar. Do not reinstate an AND here: it would reject exactly the
|
|
# slow-run case this pair exists to allow. The duration arm is the shared
|
|
# MIN_WORKOUT_DURATION_MINUTES above; only the distance arm is run-specific.
|
|
MIN_RUN_DISTANCE_KM = 3.5 # inclusive: "as low as 3.5 km"
|
|
# GPS distance tracking undershoots on tight turns/tree cover/urban canyons;
|
|
# a run within 5% of the minimum is accepted rather than rejected on
|
|
# measurement noise. This is SEPARATE from WORKOUT_DURATION_LEEWAY_MINUTES:
|
|
# that one is a deliberate concession to the user, this one corrects a
|
|
# measurement error. Duration now gets a leeway too, but for the other reason.
|
|
RUNNERUP_DISTANCE_TOLERANCE = 0.05
|
|
MAX_CLOCK_SKEW_SECONDS = 300 # 5 minutes max time skew from NTP
|
|
EARLY_BIRD_START_HOUR = 5
|
|
EARLY_BIRD_END_HOUR = 8
|
|
EARLY_BIRD_END_MINUTE = 30
|
|
HEAT_SKIP_TEMP_THRESHOLD: int = 33 # °C — above this the heat-skip dialog is offered
|
|
HEAT_SKIP_CITY: str = "Warsaw"
|
|
SHUTDOWN_CONFIG_FILE = Path("/etc/shutdown-schedule.conf")
|
|
# Helper script path (relative to this file)
|
|
ADJUST_SHUTDOWN_SCRIPT = Path(__file__).resolve().parent / "adjust_shutdown_schedule.sh"
|
|
# JSON list of ISO date strings ("YYYY-MM-DD") for which the screen lock is skipped.
|
|
SCHEDULED_SKIPS_FILE = Path(__file__).resolve().parent / "scheduled_skips.json"
|
|
# State file tracking streak, skip credits, and early-bird extension weeks.
|
|
EXTRA_BENEFITS_FILE = Path(__file__).resolve().parent / "extra_benefits_state.json"
|
|
# State file storing the base (pre-bonus) shutdown hours and last reset date.
|
|
SHUTDOWN_BASE_FILE = Path(__file__).resolve().parent / "shutdown_base.json"
|
|
# HMAC-signed rest days, declared ahead (see _rest_day), and how far the grace
|
|
# floor has lifted today's shutdown above the earned time (see _grace_floor).
|
|
REST_DAY_FILE = Path(__file__).resolve().parent / "rest_days.json"
|
|
REST_DAYS_PER_ISO_WEEK = 2
|
|
GRACE_STATE_FILE = Path(__file__).resolve().parent / "grace_floor.json"
|
|
# A day's RunnerUp walks (commute legs) count as the workout once their
|
|
# MOVING time sums to this; moving = trackpoint pairs at walking speed.
|
|
WALK_MIN_MOVING_MINUTES = 40
|
|
WALK_SPEED_MPS = (0.5, 3.0)
|
|
WALK_MAX_GAP_SECONDS = 30
|
|
# Workout records this PC must keep deleted in the sync store, pushed as
|
|
# tombstones on every tick (see _sync_tombstones for why once is not enough).
|
|
SYNC_TOMBSTONES_FILE = Path(__file__).resolve().parent / "sync_tombstones.json"
|
|
# Future-dated manual workouts logged on THIS PC, re-published on every push
|
|
# until their date (see _manual_pending for why a one-shot publish is erased).
|
|
MANUAL_PENDING_FILE = Path(__file__).resolve().parent / "manual_pending.json"
|
|
# Self-expiring marker: "logged in during today's early-bird window, still
|
|
# waiting to see if a real workout shows up." Not a log.json entry —
|
|
# it's a same-day pending flag, checked against its own "date" field.
|
|
EARLY_BIRD_PENDING_FILE = Path(__file__).resolve().parent / "early_bird_pending.json"
|
|
|
|
# The PC's copy of the phone's morning session, written HMAC-signed by
|
|
# wake-alarm (`python -m wake_alarm._session --refresh`, every 2 min through
|
|
# the morning). Read here, never fetched: wake-alarm is the only Firebase
|
|
# reader. XDG state rather than a sibling checkout, because the previous
|
|
# reader pointed at "../wake_alarm/wake_state.json" -- a path that stopped
|
|
# existing when the repos split -- and returned "no skip" for a year without
|
|
# a single error. Keep the path in step with wake_alarm/_constants.py.
|
|
MORNING_SESSION_FILE = (
|
|
Path.home() / ".local" / "state" / "wake_alarm" / "morning_session.json"
|
|
)
|
|
|
|
# A separate file from MORNING_SESSION_FILE: that one always describes
|
|
# *today* and is overwritten at the date rollover, so it cannot carry a
|
|
# marker forward to tomorrow. wake-alarm writes this one when a Tue/Wed/Thu
|
|
# ring goes unanswered; today == penalty_date means today's relaxed-day
|
|
# leniency is off. Keep the path in step with wake_alarm/_constants.py.
|
|
WORKDAY_PENALTY_FILE = (
|
|
Path.home() / ".local" / "state" / "wake_alarm" / "workday_penalty.json"
|
|
)
|
|
|
|
# Directories where RunnerUp writes per-activity TCX exports (File Synchronizer).
|
|
# Listed in preference order; both resolve to the same path on most devices.
|
|
RUNNERUP_EXPORT_DIRS: tuple[str, ...] = (
|
|
"/sdcard/Documents/RunnerUp",
|
|
"/storage/emulated/0/Documents/RunnerUp",
|
|
)
|
|
|
|
# Where RunnerUp's WebDAV synchroniser drops the same exports on this machine
|
|
# (dufs, see ~/src/dufs-cloud), and where endurain-import moves them once
|
|
# Endurain has them. Read straight off the disk, so a run is credited without
|
|
# the phone being adb-reachable -- on 2026-09-16 the TCX landed here at 20:08
|
|
# and the phone was only plugged in at 20:25. Same filenames as the phone
|
|
# export, optionally prefixed with the device name (RunnerUp_Pixel_6a_...).
|
|
RUNNERUP_WEBDAV_DIRS: tuple[Path, ...] = (
|
|
Path.home() / "data" / "cloud" / "RunnerUp",
|
|
Path.home() / "data" / "cloud" / "RunnerUp" / "processed",
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sync (phone workout data) — built on the shared crdt-sync library, Firebase
|
|
# RTDB only since 2026-10-09 (the shared ~/.config/crdt-sync/ credential). The
|
|
# old GitHub repo kuhyx/syncs/screen-locker-sync is a frozen archive: nothing
|
|
# reads or writes it. Every device pushes to devices/<device id>/log.json; the
|
|
# PC reads every device's log and pushes its OWN workouts (see _manual_push).
|
|
# ---------------------------------------------------------------------------
|
|
# The phone's pre-uuid device id. Still read (the phone may not have migrated
|
|
# yet), but no longer the only path the reader knows about -- it discovers
|
|
# every device directory.
|
|
SYNC_PHONE_DEVICE_ID: str = "phone"
|
|
# The id this machine pushed manual workouts under before per-install uuids.
|
|
# Kept so the sync layer still recognises devices/pc/ as its own and does not
|
|
# re-merge its own history as a peer's; drop to None once that path is gone.
|
|
SYNC_LEGACY_DEVICE_ID: str | None = "pc"
|
|
# This machine's sync id, minted once and persisted. A fixed "pc" collides as
|
|
# soon as a second machine takes the same role, and a reinstall would inherit
|
|
# the previous install's CRDT identity.
|
|
SYNC_DEVICE_ID_FILE: Path = (
|
|
Path.home() / ".local" / "share" / "screen_locker" / ".device_id"
|
|
)
|
|
# Revision cache for cross-device sync. Without it every push re-uploads and
|
|
# every pull re-downloads regardless of change, which is the traffic the
|
|
# Firebase free tier's monthly budget depends on not happening.
|
|
SYNC_STATE_FILE: Path = (
|
|
Path.home() / ".local" / "share" / "screen_locker" / "sync_state.json"
|
|
)
|
|
SYNC_TIMEOUT_SECONDS: float = 10.0
|
|
|
|
# Published while the locker has DECIDED to lock but is waiting for a
|
|
# higher-ranked holder (wake_alarm) to release the screen. From outside the
|
|
# process that state is indistinguishable from a hang: on 2026-08-30 the
|
|
# locker sat in gatelock's queue for 2h58m and the only evidence anywhere was
|
|
# a single INFO line at the moment it started waiting.
|
|
QUEUE_STATE_FILE: Path = (
|
|
Path.home() / ".local" / "share" / "screen_locker" / "queue_state.json"
|
|
)
|
|
|
|
# Sync runs from the morning routine, which is started right after boot and
|
|
# right after resume — often seconds before networking is actually up. Such a
|
|
# run used to fail outright, and reported it as a permissions problem
|
|
# (2026-07-20: six workouts "FAILED" at 08:04 with the network still down, then
|
|
# pushed fine by hand minutes later on the same token). So retry before giving
|
|
# up. Exponential backoff from SYNC_RETRY_DELAY_SECONDS gives 2s + 4s + 8s =
|
|
# ~14s of patience — enough for DHCP/DNS to come up, without making a genuinely
|
|
# offline run drag the whole morning routine.
|
|
SYNC_RETRY_ATTEMPTS: int = 4
|
|
SYNC_RETRY_DELAY_SECONDS: float = 2.0
|