screen-locker/screen_locker/_runnerup_db.py
Krzysztof kuhy Rudnicki 3a2ad0245c
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
deps: ruff 0.17.0 (CPY001 ignored: no copyright policy)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 22:05:02 +02:00

170 lines
5.9 KiB
Python

"""Mixin: RunnerUp root-DB pull path (fallback when no TCX exports found)."""
from __future__ import annotations
import logging
from pathlib import Path
import shutil
import sqlite3
import tempfile
import time
from typing import Any
from screen_locker._constants import (
RUNNERUP_DB_SDCARD_TMP,
RUNNERUP_PACKAGES,
)
_logger = logging.getLogger(__name__)
class RunnerUpDbMixin:
"""Mixin: root DB pull for RunnerUp workout verification.
Called as fallback when no TCX export files are found for today.
Relies on _adb_shell, _run_adb, and _validate_runnerup_data from MRO.
"""
def _find_runnerup_package(self) -> str | None:
"""Return the first installed RunnerUp package name, or None."""
for pkg in RUNNERUP_PACKAGES:
ok, out = self._adb_shell(f"pm list packages {pkg}")
if ok and pkg in out:
return pkg
return None
def _pull_runnerup_db(self) -> str | None:
"""Pull RunnerUp's SQLite DB from the device to a local temp file.
Copies the DB and WAL/SHM sidecar files via root shell to ``/sdcard``
(accessible without root by adb pull), then pulls them locally.
WAL files must travel with the main DB so that ``PRAGMA wal_checkpoint``
can merge in-flight writes.
Returns the local DB path on success, or ``None`` on any failure.
"""
pkg = self._find_runnerup_package()
if pkg is None:
_logger.info("RunnerUp not installed (tried %s)", RUNNERUP_PACKAGES)
return None
db_device = f"/data/data/{pkg}/databases/runnerup.db"
tmp_dir = tempfile.mkdtemp(prefix="runnerup_verify_")
local_db = str(Path(tmp_dir) / "runnerup.db")
ok, err = self._adb_shell(
f"cp {db_device} {RUNNERUP_DB_SDCARD_TMP}",
root=True,
)
if not ok:
_logger.warning(
"Root DB pull failed for %s (%s) — this device likely has no "
"root/su access, so this fallback path cannot work here. "
"RunnerUp workouts can only be verified via TCX export files; "
"enable RunnerUp's File Synchronizer auto-export on-device.",
pkg,
err,
)
shutil.rmtree(tmp_dir, ignore_errors=True)
return None
for suffix in ("-wal", "-shm"):
self._adb_shell(
f"test -f {db_device}{suffix} "
f"&& cp {db_device}{suffix} {RUNNERUP_DB_SDCARD_TMP}{suffix} "
f"|| true",
root=True,
)
ok, _ = self._run_adb(["pull", RUNNERUP_DB_SDCARD_TMP, local_db])
if not ok:
_logger.info("adb pull of RunnerUp DB failed")
self._cleanup_runnerup_sdcard()
shutil.rmtree(tmp_dir, ignore_errors=True)
return None
for suffix in ("-wal", "-shm"):
self._run_adb(
["pull", f"{RUNNERUP_DB_SDCARD_TMP}{suffix}", f"{local_db}{suffix}"]
)
self._cleanup_runnerup_sdcard()
return local_db
def _cleanup_runnerup_sdcard(self) -> None:
"""Remove temporary RunnerUp DB files from the sdcard."""
for suffix in ("", "-wal", "-shm"):
self._adb_shell(
f"test -f {RUNNERUP_DB_SDCARD_TMP}{suffix} "
f"&& rm {RUNNERUP_DB_SDCARD_TMP}{suffix} || true",
root=True,
)
def _query_todays_run(self, db_path: str) -> dict[str, Any] | None:
"""Query the pulled RunnerUp DB for today's most recent activity.
Runs ``PRAGMA wal_checkpoint`` first so that uncommitted WAL entries
are visible (important for runs just finished before connecting).
"""
local_midnight = time.mktime((*time.localtime()[:3], 0, 0, 0, 0, 0, -1))
local_end = local_midnight + 86400
try:
with sqlite3.connect(db_path, timeout=5) as conn:
conn.execute("PRAGMA wal_checkpoint(PASSIVE)")
cursor = conn.execute(
"""
SELECT start_time, distance, time, type
FROM activity
WHERE deleted = 0
AND start_time >= ?
AND start_time < ?
ORDER BY start_time DESC
LIMIT 1
""",
(local_midnight, local_end),
)
row = cursor.fetchone()
except sqlite3.Error as exc:
_logger.warning(
"RunnerUp activity query against %s failed: %s — treating it as "
"no run recorded today",
db_path,
exc,
)
return None
if row is None:
return None
start_time, distance_m, duration_seconds, sport = row
return {
"start_time": int(start_time or 0),
"distance_m": float(distance_m or 0),
"duration_seconds": int(duration_seconds or 0),
"sport": int(sport or 0),
}
def _verify_runnerup_via_db(self) -> tuple[str, str]:
"""Verify today's run via root DB pull (fallback path)."""
db_path = self._pull_runnerup_db()
if db_path is None:
return (
"not_verified",
(
"No TCX export found and root DB access is unavailable on "
"this device — enable RunnerUp's File Synchronizer "
"auto-export so runs land in /sdcard/Documents/RunnerUp/"
),
)
try:
run_data = self._query_todays_run(db_path)
finally:
shutil.rmtree(Path(db_path).parent, ignore_errors=True)
if run_data is None:
return "not_verified", "No RunnerUp activity found for today"
return self._validate_runnerup_data(run_data)