ef1f9eade2
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.
59 lines
2.5 KiB
XML
59 lines
2.5 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<!-- Android TV launcher banner: 320x180 landscape.
|
|
Shown on the leanback home row. The previous build reused the square
|
|
launcher icon, which letterboxed badly. -->
|
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
android:width="320dp"
|
|
android:height="180dp"
|
|
android:viewportWidth="320"
|
|
android:viewportHeight="180">
|
|
<!-- Background -->
|
|
<path
|
|
android:fillColor="#0d1117"
|
|
android:pathData="M0,0 L320,0 L320,180 L0,180 Z" />
|
|
<!-- Subtle teal glow top-left -->
|
|
<path
|
|
android:fillColor="#1A64ffda"
|
|
android:pathData="M0,0 L160,0 L160,90 L0,90 Z" />
|
|
<!-- Subtle purple glow bottom-right -->
|
|
<path
|
|
android:fillColor="#15bb86fc"
|
|
android:pathData="M160,90 L320,90 L320,180 L160,180 Z" />
|
|
<!-- TV body, centered -->
|
|
<path
|
|
android:fillColor="#1c2333"
|
|
android:pathData="M88,56 L196,56 Q204,56 204,64 L204,116 Q204,124 196,124 L88,124 Q80,124 80,116 L80,64 Q80,56 88,56 Z" />
|
|
<!-- TV screen -->
|
|
<path
|
|
android:fillColor="#161b22"
|
|
android:pathData="M92,60 L192,60 Q196,60 196,64 L196,116 Q196,120 192,120 L92,120 Q88,120 88,116 L88,64 Q88,60 92,60 Z" />
|
|
<!-- LED glow strips -->
|
|
<path
|
|
android:fillColor="#64ffda"
|
|
android:fillAlpha="0.8"
|
|
android:pathData="M94,50 L190,50 L190,54 L94,54 Z" />
|
|
<path
|
|
android:fillColor="#bb86fc"
|
|
android:fillAlpha="0.7"
|
|
android:pathData="M72,62 L76,62 L76,118 L72,118 Z" />
|
|
<path
|
|
android:fillColor="#ff6b6b"
|
|
android:fillAlpha="0.7"
|
|
android:pathData="M208,62 L212,62 L212,118 L208,118 Z" />
|
|
<path
|
|
android:fillColor="#ffd93d"
|
|
android:fillAlpha="0.7"
|
|
android:pathData="M94,126 L190,126 L190,130 L94,130 Z" />
|
|
<!-- Wordmark "LedGrab" — drawn as paths so we don't depend on the
|
|
system font cache being warm at TV launch. -->
|
|
<!-- L -->
|
|
<path android:fillColor="#64ffda"
|
|
android:pathData="M222,72 L228,72 L228,100 L240,100 L240,106 L222,106 Z" />
|
|
<!-- e -->
|
|
<path android:fillColor="#e6edf3"
|
|
android:pathData="M244,82 L260,82 Q264,82 264,86 L264,94 L250,94 L250,100 L262,100 L262,106 L246,106 Q244,106 244,104 Z M250,86 L250,90 L258,90 L258,86 Z" />
|
|
<!-- d -->
|
|
<path android:fillColor="#e6edf3"
|
|
android:pathData="M266,72 L272,72 L272,82 L284,82 Q286,82 286,84 L286,106 L268,106 Q266,106 266,104 Z M272,88 L272,100 L280,100 L280,88 Z" />
|
|
</vector>
|