Adds .gitea/workflows/build-android.yml — Linux runner installs JDK 17, Python 3.11, Android SDK/NDK, symlinks server/src/ledgrab into the Chaquopy python source dir, and runs assembleDebug on master pushes / assembleRelease on v* tags. APK is uploaded as an artifact and attached to the Gitea release on tag push. Conditional signing config in build.gradle.kts reads keystore from env vars (CI secrets) and falls back to debug signing locally. Gradle wrapper (gradlew/gradlew.bat/ gradle-wrapper.jar) committed so CI can drive the build. Rebuilds pydantic-core wheels for arm64-v8a and x86_64 — both were missing libpython3.11.so in NEEDED, which would have crashed at import on real devices. build-pydantic-core.sh rewritten as a multi-ABI builder: selects targets via args, sets RUSTFLAGS=-C link-arg=-Wl,--no-as-needed -C link-arg=-lpython3.11 to force the symbol-resolution dependency, uses the per-ABI sysconfigdata + libpython staged in android/.build-cache/, prefers `py -3.11` on Windows (Git Bash's python3.11 is an MSStore stub), uses the .cmd clang wrapper on Windows (fixes os error 193), and verifies NEEDED via llvm-readelf after each build. abiFilters restored to the full triple in build.gradle.kts; multi-ABI debug APK builds cleanly (~99 MB).
6.9 KiB
LedGrab TODO
Android — Restore Multi-ABI Wheels
During emulator testing, we switched the build to x86 only (see android/app/build.gradle.kts abiFilters) to avoid having to keep the arm64-v8a / x86_64 pydantic-core wheels current. Before shipping, restore all three ABIs:
- Rebuild
pydantic-corewheels for all three ABIs with the current SOABI + libpython linking settings (android/build-scripts/build-pydantic-core.sh— now supportsarm64,x86_64,x86args; defaults to all three). - Verify wheels: all three now list
libpython3.11.soinNEEDED(llvm-readelf -d), automated in the build script. - Restored
abiFilters += listOf("arm64-v8a", "x86_64", "x86")inbuild.gradle.kts. Multi-ABI debug APK builds cleanly (~99 MB). - Re-test on real ARM64 Android TV hardware (still pending — only emulator-verified build).
Build cache + scripts live in android/build-scripts/ and android/.build-cache/ (junction host + sysconfigdata for each ABI).
Android CI Pipeline
Build the Android APK automatically on push/tag.
- Generate Gradle wrapper (
gradlew) and commit it - Create CI workflow (
.gitea/workflows/build-android.yml)- JDK 17 + Android SDK + NDK setup
- Python 3.11 for Chaquopy build
- Recreate the directory junction via
ln -son Linux CI ./gradlew assembleDebugon master push,assembleReleaseonv*tags (if signing secrets set)- Uploads APK as CI artifact; attaches to Gitea release on tag push
- Commit pre-built pydantic-core wheels to
android/wheels/(arm64, x86, x86_64) - APK signing for release builds — conditional signing config reads keystore from env vars (
ANDROID_KEYSTORE_PATH/_PASSWORD/_ALIAS/_KEY_PASSWORD), falls back to debug signing locally - Provision a real keystore and add the four CI secrets:
ANDROID_KEYSTORE_BASE64(base64-encoded .jks)ANDROID_KEYSTORE_PASSWORDANDROID_KEY_ALIASANDROID_KEY_PASSWORD
- Add
LedGrab-{tag}-android-release.apkrow to the release description table in.gitea/workflows/release.yml→create-releasejob - Verify the CI workflow passes end-to-end with the now-restored multi-ABI build (larger APK, longer Android build step)
Android Root Capture (No Permission Dialog, No System Indicator)
MediaProjection shows a mandatory system overlay/indicator while capturing — unavoidable on stock Android. Many cheap Android TV boxes ship pre-rooted, so an alternative root-only path would give much better UX.
- Detect root at runtime: check for
subinary,Superuser.apk, etc. - Implement
SurfaceControlCaptureEngine(new capture engine) using hiddenSurfaceControl.screenshot()API via reflection- No permission dialog
- No system capture indicator
- Direct bitmap output (no encoder/decoder roundtrip)
- Engine priority: higher than MediaProjection when root detected
- Fallback chain:
SurfaceControl(root) →MediaProjection(stock) →adb screencap(last resort) - Handle Android version differences in
SurfaceControlAPI surface (renamed/moved across API 29, 30, 33) - Alternative: shell out to
screenrecord --output-format=h264 -as root (same H.264 decode as scrcpy_client_engine, but local instead of remote ADB)
Known projects using this approach for reference: scrcpy-hidden-api, shizuku, commercial scrcpy-derived apps.
Android USB Serial Support
Drive USB LED controllers (APA102, WS2812) connected directly to the Android TV box via USB-to-serial adapters.
- Add usb-serial-for-android dependency to
android/app/build.gradle.kts - Create Kotlin
UsbSerialBridgeclass that:- Enumerates USB serial devices via Android USB Host API
- Requests user permission for USB device access
- Opens a serial connection (baud rate configurable)
- Exposes a write method callable from Python via Chaquopy
- Create Python
AndroidSerialProviderinserver/src/ledgrab/core/devices/that:- Replaces
pyserialon Android (which can't access USB ports) - Calls
UsbSerialBridgevia Chaquopy to send LED data - Registers as an alternative serial transport when
is_android()is True
- Replaces
- Add USB device permission dialog to
MainActivity(auto-triggered on device connect) - Test with common USB-to-serial chips: CH340, CP2102, FTDI
- Document supported USB LED controllers in README
Android App — Known Issues
Issues discovered during first end-to-end test on emulator (2026-04-14):
- Stop/Start capture produces no frames on second try. First capture works, but after tapping Stop and Start again, no frames reach the Python pipeline. Likely causes:
- Global MediaProjection engine state (
_active,_frame_queue) not reset on service restart - ScreenCapture listener not properly detached from ImageReader on stop
- Python uvicorn port reuse issue when server restarts
- Global MediaProjection engine state (
- Web UI tabs show persistent spinners on Dashboard, Automation, Sources, Integrations, Graph tabs (Targets tab works). All API calls return 200 OK, no console errors. Probably waiting on a specific WebSocket event that never fires when no output targets are configured/active.
- Dead keyboard/IME handling — password inputs lack autocomplete attributes (minor accessibility warning in browser console)
Performance Metrics Abstraction
The codebase has direct psutil.* calls scattered across api/routes/system.py and core/processing/metrics_history.py, with ad-hoc if psutil is not None guards sprinkled in to support Android. This couples Android platform handling to every call site.
- Refactor: introduce
MetricsProviderprotocol inutils/metrics.pywith methods likecpu_percent(),memory_info(),process_info() - Implement
PsutilMetricsProvider(desktop) andNullMetricsProvider(fallback when psutil missing) - Later:
AndroidMetricsProviderreading from/proc(see section below) - Replace all direct
psutil.*calls with the provider; only one factory location knows about psutil availability
Android Performance Metrics
Currently psutil (used for CPU/RAM monitoring in the web UI) is not available on Android via Chaquopy. Metrics calls are guarded with if psutil is not None so they return no data on Android.
- Implement Android-native metrics collection:
- CPU usage via
/proc/self/stat+/proc/statparsing (no psutil needed) - RAM usage via
/proc/meminfoorActivityManager.getMemoryInfo()through Chaquopy bridge - App-specific memory via
Debug.getMemoryInfo()(Kotlin → Python)
- CPU usage via
- Create
AndroidMetricsProviderinserver/src/ledgrab/utils/that implements the same interface as the psutil-based provider - Wire into existing metrics endpoints (
/api/v1/system/metrics) with platform detection - Consider: device battery/temperature readings for TV boxes (some have thermal throttling)
- Optional: GPU usage via
/sys/class/kgsl/kgsl-3d0/gpubusyon Adreno, Mali-specific paths for Mali GPUs