1c1bbe2551
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).
186 lines
10 KiB
XML
186 lines
10 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" />
|
|
|
|
<!-- PACKAGE_USAGE_STATS — read the foreground app for the "Application"
|
|
automation rule (foreground app -> activate scene) via UsageStatsManager.
|
|
A special-access permission: it can't be granted at runtime; the user
|
|
toggles it under Settings > Usage access (opened from MainActivity).
|
|
tools:ignore="ProtectedPermissions" silences the build warning that this
|
|
is a system/signature-level permission — it is honoured as a user-grantable
|
|
special access. NO QUERY_ALL_PACKAGES is needed: matching only compares the
|
|
foreground package NAME, and the app picker uses LauncherApps. -->
|
|
<uses-permission
|
|
android:name="android.permission.PACKAGE_USAGE_STATS"
|
|
tools:ignore="ProtectedPermissions" />
|
|
|
|
<!-- 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>
|