1ada5ac3341fe80bd2a266842cf0df4284648597
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1c1bbe2551 |
feat(android): foreground-app automation condition
Make the existing Application automation rule (foreground app -> activate scene) work on the Android-TV build. A Kotlin ForegroundAppBridge reads the foreground app via UsageStatsManager and lists launchable apps via LauncherApps; PlatformDetector bridges it in (ahead of the Windows-only ctypes guard) so the existing AutomationEngine / ApplicationRule / storage / deactivation modes are unchanged. New /system/installed-apps + /system/info endpoints feed an app picker that stores package names (vs process names on desktop); on Android the editor hides the match-type selector since the foreground app is the only obtainable signal. PACKAGE_USAGE_STATS is granted via an on-device button + a web-UI banner (no blanket prompt at capture start); detection degrades gracefully until granted. Zero new Python/Gradle deps (UsageStatsManager + LauncherApps are in-platform; matching only string-compares the package name, so no QUERY_ALL_PACKAGES). assembleDebug + 1897 pytest + ruff + tsc + npm build all green; independent final review (0 blockers) + security review (no critical issues). |
||
|
|
0be3f833df |
feat(android): on-device OS notification capture (NotificationListenerService)
Add an Android backend to os_notification_listener.py so notifications on the experimental Android-TV build drive the existing NotificationColorStripSource LED effects (flash/pulse/sweep, per-app colors + sounds) at app-name parity with the Windows/Linux backends. A Kotlin NotificationListenerService forwards the posting app's display label across the Chaquopy JNI boundary into a new push-based _AndroidBackend + module-level push_notification() receiver; the existing color-strip pipeline, per-app colors/filters, and history endpoint are reused unchanged. - Python: _AndroidBackend (probed first), push_notification() receiver, _LinuxBackend.probe() hardened with is_linux() to exclude Android (which also reports platform.system() == "Linux"). - Android: LedGrabNotificationListener NLS — serial single-thread executor, full crash isolation around Python.getInstance(), label-only forwarding (never notification title/body), ongoing/group-summary/self-package noise filtering. Manifest service exported + gated by BIND_NOTIFICATION_LISTENER_SERVICE (no new uses-permission). - UX: prompt-once notification-access + manual "Grant notification access" button wired into the D-pad focus chain (computed from visible controls); en/ru/zh strings. - Tests: 11 isolated unit tests — module-global + tmp_path history isolation, push routing contract, callback-exception swallowing, None app-name, and a desktop-regression lock on backend selection order. - Docs: README OS-support Android column (notification + audio cells), ANDROID-REVIEW status flipped to Implemented. Zero new Python deps; no build.gradle.kts / Chaquopy pip changes. |
||
|
|
ef1f9eade2 |
feat(android): production-readiness pass — security, perf, compat, UI/UX
Multi-axis lift to ship-quality after a full review:
Security
- ApiKeyManager: per-install random API key, persisted via SharedPreferences
with synchronous first-write; threaded into uvicorn via the
LEDGRAB_AUTH__API_KEYS env var; embedded in QR as a URL fragment (#k=)
so it never appears in HTTP requests or server logs; frontend reads
location.hash on first visit and strips it via history.replaceState
- Root.runAsRoot(argv: Array<String>) overload with POSIX shell-quoting to
eliminate the shell-injection footgun (= excluded from unquoted-safe set)
- UsbSerialBridge: ContextCompat.RECEIVER_NOT_EXPORTED + intent.package
check in the broadcast receiver for defence-in-depth across API levels
- Release builds refuse to silently fall back to debug keystore; require
ANDROID_KEYSTORE_* env vars or explicit
ANDROID_ALLOW_DEBUG_SIGNED_RELEASE=1
- Crash log retention capped at 10 entries
- Fatal-error stack trace hidden behind a toggle on the error screen
Performance
- ScreenCapture / RootScreenrecord reuse a single RGBA ByteArray per
pipeline instead of allocating per frame — eliminates ~15 MB/s GC churn
at 30 fps on low-end TV boxes
- Frame pacer switched from System.currentTimeMillis() + integer division
(~30.3 fps drift) to SystemClock.elapsedRealtimeNanos with a catch-up
accumulator
- ScreenCapture computes capture dimensions from source aspect ratio so
non-16:9 displays don't get squashed
- RootScreenrecord input pump backs off 5 ms when MediaCodec is starved,
ending a tight spin that burned a CPU core on decoder stalls
- QR cached by URL — onResume from background no longer rebuilds the
560×560 bitmap each time
- ApiKey commit() pre-warmed off Main on app startup
Compatibility
- compileSdk / targetSdk bumped to 35 (Play Store requirement)
- armeabi-v7a build path added to build script + conditionally included
in gradle splits when the matching wheel is present in android/wheels/
- Foreground service type declared as mediaProjection|specialUse with
PROPERTY_SPECIAL_USE_FGS_SUBTYPE rationale; promotion via
ServiceCompat.startForeground with the correct type per mode
- NetworkUtils picks Ethernet > Wi-Fi > VPN > cellular instead of just
activeNetwork — fixes wrong-URL on TV boxes with both Ethernet + Wi-Fi
- enableOnBackInvokedCallback=true for Android 15 predictive-back
- Splash screen API via androidx.core:core-splashscreen — hides Chaquopy
stdlib unpack delay on cold first launch
UI / UX
- All previously hardcoded English strings (root prompt, permission
denial, fatal-error screen, notification text) now localised across
en/ru/zh
- Monochrome notification icon (was a colored launcher → gray blob in
status bar)
- 320×180 TV banner (was the square launcher → squashed on Leanback row)
- ViewStub-based running panel (deferred inflation)
- ObjectAnimator pulse on the Running status dot for liveness feedback
- "Starting…" button state while root is being probed
- Autostart checkbox hidden entirely on unrooted devices
- "No network" status when getLocalIpAddress returns null
- QR fallback hint text
- Animator cancelled in onStop to avoid leaking view hierarchy
Lifecycle hardening (from review)
- RootScreenrecord: processLock serialises EOF respawn vs concurrent
stop() to prevent orphaned screenrecord processes
- CaptureService.restartRootPipeline: publish-before-start under
@Synchronized to close the orphan window during watchdog restarts
- ScreenCapture.MediaProjection.Callback.onStop just flips
running=false instead of calling stop() (which self-joined
captureThread and hung 500 ms)
- updateUI early-returns when lateinit not initialised (fatal-error path)
- Watchdog give-up bound fixed (>= instead of >, was allowing 4 attempts)
server/android_entry.py accepts an optional api_key, sets
LEDGRAB_AUTH__API_KEYS={"android":<key>} as JSON before any LedGrab
import, logs a clear error if pydantic-settings parsing doesn't land
the value back in config (defensive guard against future settings
behaviour drift).
server/static/js/app.ts: bootstrap reads #k= from location.hash,
persists to localStorage, then strips via history.replaceState.
Two independent code-review passes; 147 relevant server tests still
pass; TypeScript and ruff clean.
|
||
|
|
b3775b2f98 |
feat(android): boot-time autostart, capture watchdog, versionCode from git
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. |
||
|
|
2b5dac2c42 |
feat(devices): BLE LED controller support (SP110E/Triones/Zengge/Govee)
End-to-end BLE streaming: provider + client + per-protocol wire encoders with whole-strip averaging, desktop (bleak) and Android (Kotlin BleBridge via Chaquopy) transports, discovery with protocol-family detection that auto-fills the UI, throttled not-connected warning + 10 s reconnect cooldown so a dropped link no longer stalls the pipeline at ~30 s/frame, and an explicit asyncio.wait_for wrapper around bleak connect() since the WinRT backend doesn't always honor the timeout kwarg. Also rewrites server/restart.ps1 to be parameterized (-Port / -Module / -PythonVersion / timeouts / -Quiet), pick the right interpreter via the py launcher, pre-flight the target module, poll port readiness on both shutdown and startup, redirect child stdout/stderr so Start-Process doesn't hang on inherited Git-Bash handles, and return proper exit codes. Rolls in concurrent work: Android BLE permissions + launcher icons + ru/zh resources, Chaquopy-safe value_stream psutil fallback, setup-required modal, asset-store test coverage, and misc system/config touch-ups. |