Boot-time startup so LedGrab has display capture and control without user
interaction on rooted TV boxes. Also folds in a batch of review findings
from the Android package audit.
Autostart
- BootReceiver fires on BOOT_COMPLETED / LOCKED_BOOT_COMPLETED /
MY_PACKAGE_REPLACED, gated by AutostartPrefs and Root.looksRooted().
Dispatches CaptureService.createRootIntent via
ContextCompat.startForegroundService. Unrooted devices are a no-op
because MediaProjection consent cannot be bypassed silently.
- AutostartPrefs: thin SharedPreferences wrapper, defaults to enabled.
Exposed as a CheckBox on the stopped panel; greyed out when not rooted.
- Manifest: RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
WAKE_LOCK permissions + the new BootReceiver.
- MainActivity prompts for battery-optimization exemption on first opt-in
so Doze/App Standby doesn't kill the FG service on phones.
Service stability
- onStartCommand now flips isRunning only after startForeground succeeds
(was stuck=true forever if the FG transition threw) and resets on
exception. Returns START_REDELIVER_INTENT for root mode so the OS can
restart the service with the original intent (no consent token to
invalidate); MediaProjection mode keeps START_NOT_STICKY.
- Watchdog coroutine monitors RootScreenrecord.framesDelivered. Respawns
the pipeline on stall (reusing the existing Python bridge — no server
restart), caps at 3 consecutive restarts before giving up.
- RootScreenrecord.framesDelivered is now an AtomicInteger, exposed as a
public property for the watchdog.
- ScreenCapture takes an onProjectionStopped lambda; when the user taps
the system Cast/Screen-capture stop banner, the whole service is torn
down instead of leaving a stale FG notification.
- MainActivity's two startForegroundService calls switch to
ContextCompat.startForegroundService, clearing pre-existing NewApi lint
errors (minSdk=24 < API 26 native method).
Build
- versionCode derived from git rev-list --count HEAD (or the
ANDROID_VERSION_CODE env var for CI). Was pinned to 1 — sideload
upgrades were silently refusing to install.
- New i18n strings (autostart_label, autostart_unavailable, version_prefix)
in en/ru/zh; version_text now uses the resource instead of string
concat.
TODO.md: new "Android Autostart on Boot" section tracking done/pending
items; real-hardware verification on a Magisk'd TV box is the remaining
checkbox.
Adds a native Android TV application that runs the full LedGrab Python
server in-process via Chaquopy. Captures the TV box screen using the
MediaProjection API and exposes the existing web UI on the device's
local network — users configure via phone/tablet browser.
Android (new /android/ module):
- Kotlin shell: MainActivity, CaptureService (foreground service),
ScreenCapture (MediaProjection + ImageReader), PythonBridge (Chaquopy).
- Polished Leanback-themed UI with QR code for easy web UI access.
- AGP 8.9 + Chaquopy 17 + Gradle 8.11 (avoids the AGP 8.7 thread-lock bug).
- Pre-built pydantic-core wheels for arm64-v8a, x86_64, x86 cross-compiled
with maturin + Android NDK, linked against Chaquopy's libpython3.11.so.
Python server platform guards:
- New utils/platform.py with is_android()/is_windows()/is_linux() helpers.
- Guard every top-level import of desktop-only packages (mss, psutil,
sounddevice, pyserial, PyAudioWPatch, etc.) with try/except ImportError.
- Android-incompatible calls gated with None-checks so the server runs on
reduced capabilities on Android (no CPU/RAM metrics, no mss displays).
- utils/image_codec.py gains a Pillow fallback for resize + JPEG encode
when cv2 is unavailable; all internal cv2.resize callers migrated.
- New android_entry.py start_server/stop_server invoked from Kotlin.
- get_displays API falls back to best available engine when mss fails.
New capture engines:
- MediaProjectionEngine: receives RGBA frames pushed from Kotlin through
a thread-safe queue; caches last frame for static-screen previews.
- ScrcpyClientEngine: optional H.264 streaming via scrcpy-client library
(priority 10, overrides the ADB-screencap engine when installed).
Frontend:
- Tab loaders previously required an apiKey; now correctly treat
"auth disabled" as authenticated (Android has no auth by default).
- Re-trigger the active tab's loader after loadServerInfo resolves
authRequired, since initTabs runs earlier.
- Add i18n keys for the demo / mediaprojection / scrcpy_client engines.
Docs:
- TODO.md: follow-ups for multi-ABI wheel rebuilds, CI pipeline, USB
serial LED controllers, root-only capture, perf metrics abstraction.
- CLAUDE.md: Android dependency sync policy (pip --exclude doesn't exist).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>