4bf3fe65db
Add on-device webcam capture to the experimental Android-TV build. Desktop captures webcams via OpenCV (no Chaquopy/Android wheel); this adds a push-based AndroidCameraEngine that plugs into the same selection path desktop uses (capture template engine_type="android_camera" + display_index, HAS_OWN_DISPLAYS). A Kotlin CameraBridge (Camera2) enumerates cameras and opens them on demand — only while a capture source is active, driven Python->Kotlin via a guarded jclass singleton (BleBridge pattern) — converts each frame YUV_420_888->RGB, and pushes RGB bytes into a module-level queue mirroring mediaprojection_engine.py. Cameras surface as selectable displays like the desktop OpenCV engine; the data-driven capture-template UI is unchanged. No new Python deps; no new Gradle deps (Camera2 is in-platform). Engine: ENGINE_PRIORITY=0 (never auto-selected over MediaProjection=100; explicit engine_type only). Single-camera ownership is serialized with a lock + ref-count (same-camera streams attach, different-camera refused, last release stops), mirroring the desktop CameraEngine guard. Permission: CAMERA requested at capture-start, gated on FEATURE_CAMERA_ANY so camera-less TV boxes never prompt; graceful degradation when denied. The service is promoted with the camera FGS type (+ FOREGROUND_SERVICE_CAMERA) only when CAMERA is already granted, so backgrounded capture keeps working without risking a failed startForeground on camera-less boxes (camera can't ride the MediaProjection token the way audio playback capture does). Reviewed via multi-agent adversarial pass (13 findings -> 4 fixed: device leak on session-failure, multi-stream collision, camera FGS type, i18n key; 9 refuted). Tests: 18 new desktop-CI tests (no device needed); full suite 1883 passed. Verified: assembleDebug BUILD SUCCESSFUL, ruff clean. Docs: ANDROID-REVIEW/android-webcam-capture-plan.md (design), updated android-missing-functionality.md + README feature table + en/ru/zh locales.
174 lines
9.2 KiB
XML
174 lines
9.2 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
xmlns:tools="http://schemas.android.com/tools">
|
|
|
|
<!-- BLE scanning and connecting — API ≥31 uses granular permissions;
|
|
older releases need BLUETOOTH + ACCESS_FINE_LOCATION for scanning.
|
|
neverForLocation avoids the location permission dialog on API 31+. -->
|
|
<uses-permission android:name="android.permission.BLUETOOTH"
|
|
android:maxSdkVersion="30" />
|
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
|
android:maxSdkVersion="30" />
|
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
|
android:maxSdkVersion="30" />
|
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
|
|
android:usesPermissionFlags="neverForLocation"
|
|
tools:targetApi="s" />
|
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
|
|
<!-- BLE hardware — required=false so non-BT boxes still install. -->
|
|
<uses-feature
|
|
android:name="android.hardware.bluetooth_le"
|
|
android:required="false" />
|
|
|
|
<!-- Network access for WLED HTTP/UDP, web UI, MQTT -->
|
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
|
|
<!-- Foreground service permissions.
|
|
FOREGROUND_SERVICE_MEDIA_PROJECTION: required on API 34+ for the
|
|
MediaProjection capture path.
|
|
FOREGROUND_SERVICE_SPECIAL_USE: required on API 34+ for the root
|
|
screenrecord capture path (it doesn't use MediaProjection).
|
|
Both are declared because the service may run in either mode. -->
|
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
|
<!-- FOREGROUND_SERVICE_CAMERA (API 34+): required to keep camera access while
|
|
the app is backgrounded during on-device webcam capture. The service is
|
|
promoted with the `camera` FGS type ONLY when CAMERA is already granted
|
|
(see CaptureService.onStartCommand) — unlike audio playback capture (which
|
|
rides the MediaProjection token under the mediaProjection type), the camera
|
|
has no such coupling and needs its own FGS type to survive backgrounding. -->
|
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
|
|
|
<!-- POST_NOTIFICATIONS for Android 13+ foreground service notification -->
|
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
|
|
<!-- RECORD_AUDIO for on-device system-playback capture (AudioPlaybackCapture,
|
|
API 29+) feeding audio-reactive lighting. Runtime "dangerous" permission,
|
|
requested in MainActivity; capture degrades gracefully when denied.
|
|
Playback capture runs under the existing mediaProjection FGS type, so no
|
|
FOREGROUND_SERVICE_MICROPHONE / microphone FGS type is needed (that would
|
|
only be required if the mic-fallback path ran inside the service). -->
|
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
|
|
<!-- CAMERA for on-device webcam capture (Camera2). Runtime "dangerous"
|
|
permission, requested in MainActivity gated on FEATURE_CAMERA_ANY so
|
|
camera-less TV boxes never see the prompt; capture degrades gracefully
|
|
when denied. The camera is opened ON DEMAND (only while a camera
|
|
capture source is active). To keep capturing after the app is
|
|
backgrounded, the service is promoted with the `camera` FGS type
|
|
(FOREGROUND_SERVICE_CAMERA above) — but only when CAMERA is already
|
|
granted, so a camera-less / not-yet-granted box never risks a failed
|
|
service start. -->
|
|
<uses-permission android:name="android.permission.CAMERA" />
|
|
|
|
<!-- Autostart on boot — BootReceiver spawns CaptureService in root
|
|
mode so capture resumes without the user touching the remote. -->
|
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
<!-- Exempt from Doze/App Standby so the FG service isn't killed
|
|
overnight on phones; essentially a no-op on TV boxes. -->
|
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
|
<!-- Optional wake lock for sustained-performance boxes that aggressively
|
|
sleep the CPU with the display off. -->
|
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
|
|
<!-- Android TV declarations -->
|
|
<uses-feature
|
|
android:name="android.software.leanback"
|
|
android:required="true" />
|
|
<uses-feature
|
|
android:name="android.hardware.touchscreen"
|
|
android:required="false" />
|
|
|
|
<!-- USB host — for USB-to-TTL adapters driving Adalight/AmbiLED
|
|
controllers. required=false so phones without USB host still install. -->
|
|
<uses-feature
|
|
android:name="android.hardware.usb.host"
|
|
android:required="false" />
|
|
|
|
<!-- Camera hardware — for on-device webcam capture. required=false so
|
|
camera-less TV boxes (the common case) still install; the camera
|
|
engine simply reports no displays on such devices. camera.any covers
|
|
built-in (front/back) and external/USB-UVC cameras the platform
|
|
routes through Camera2. -->
|
|
<uses-feature
|
|
android:name="android.hardware.camera.any"
|
|
android:required="false" />
|
|
|
|
<application
|
|
android:name=".LedGrabApp"
|
|
android:allowBackup="false"
|
|
android:enableOnBackInvokedCallback="true"
|
|
android:icon="@mipmap/ic_launcher"
|
|
android:label="@string/app_name"
|
|
android:banner="@drawable/banner_tv"
|
|
android:networkSecurityConfig="@xml/network_security_config"
|
|
android:theme="@style/Theme.LedGrab">
|
|
|
|
<!-- TV launcher activity. Boots through the SplashScreen theme so
|
|
the (sometimes multi-second) Chaquopy stdlib unpack doesn't
|
|
show as a black screen on first launch. -->
|
|
<activity
|
|
android:name=".MainActivity"
|
|
android:exported="true"
|
|
android:theme="@style/Theme.LedGrab.Splash">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.MAIN" />
|
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
</intent-filter>
|
|
</activity>
|
|
|
|
<!-- Foreground service for screen capture + Python server.
|
|
Declares BOTH mediaProjection AND specialUse: only one is
|
|
active at a time but Android needs to see the union of
|
|
possible types up-front so it doesn't kill the service when
|
|
we promote it with a different type at runtime.
|
|
FOREGROUND_SERVICE_TYPE_SPECIAL_USE on API 34+ requires the
|
|
PROPERTY_SPECIAL_USE_FGS_SUBTYPE rationale below. -->
|
|
<service
|
|
android:name=".CaptureService"
|
|
android:foregroundServiceType="mediaProjection|specialUse|camera"
|
|
android:exported="false">
|
|
<property
|
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
android:value="Root-mode screen capture for ambient LED sync. Uses /system/bin/screenrecord on rooted devices to avoid MediaProjection's persistent capture indicator overlay, which is required for the always-on ambient-lighting use case." />
|
|
</service>
|
|
|
|
<!-- Notification capture — a NotificationListenerService bound by
|
|
system_server. exported="true" is REQUIRED here (the system binds
|
|
it cross-process) and intentionally diverges from CaptureService
|
|
(exported="false"); access is gated by the system-held
|
|
BIND_NOTIFICATION_LISTENER_SERVICE permission, so no new
|
|
<uses-permission> is needed. The user grants access via
|
|
Settings > Notification access (opened from MainActivity). -->
|
|
<service
|
|
android:name=".LedGrabNotificationListener"
|
|
android:label="@string/notification_listener_label"
|
|
android:exported="true"
|
|
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
|
<intent-filter>
|
|
<action android:name="android.service.notification.NotificationListenerService" />
|
|
</intent-filter>
|
|
</service>
|
|
|
|
<!-- Autostart — fires on device boot (and package replace).
|
|
On rooted devices, launches CaptureService directly so capture
|
|
resumes without the user tapping Start. Unrooted devices are
|
|
no-op because MediaProjection consent cannot be bypassed. -->
|
|
<receiver
|
|
android:name=".BootReceiver"
|
|
android:exported="true"
|
|
android:enabled="true">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
</intent-filter>
|
|
</receiver>
|
|
</application>
|
|
|
|
</manifest>
|