diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index dcbcf99..0a94911 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -30,23 +30,39 @@ val ledgrabVersionCode: Int = run {
android {
namespace = "com.ledgrab.android"
- compileSdk = 34
+ // SDK 35 (Android 15) — required for Play Store from Aug 2025 onward.
+ compileSdk = 35
defaultConfig {
applicationId = "com.ledgrab.android"
minSdk = 24 // Android 7.0 — covers nearly all TV boxes
- targetSdk = 34
+ targetSdk = 35
// Derived from git commit count (or ANDROID_VERSION_CODE env var
// in CI). See ledgrabVersionCode above. Was stuck at 1 before —
// sideload updates silently refused to install.
versionCode = ledgrabVersionCode
versionName = "0.7.0"
+ // ABI selection. Detect armeabi-v7a wheel presence and opt the
+ // ABI in only when the matching pydantic-core wheel is on disk —
+ // otherwise Chaquopy would fail the build searching for it. The
+ // build script (build-scripts/build-pydantic-core.sh) is the
+ // source of truth for which ABIs we *can* ship.
+ val v7Wheel = file("$rootDir/wheels").listFiles().orEmpty()
+ .any { it.name.startsWith("pydantic_core-") && it.name.contains("armeabi_v7a") }
+ val ledgrabAbis = buildList {
+ add("arm64-v8a")
+ add("x86_64")
+ add("x86")
+ if (v7Wheel) add("armeabi-v7a")
+ }
ndk {
- // All three ABIs: arm64-v8a (real TV hardware), x86_64 (modern
- // emulators), x86 (legacy emulators). Wheels in android/wheels/
- // must be kept in sync — see build-scripts/build-pydantic-core.sh.
- abiFilters += listOf("arm64-v8a", "x86_64", "x86")
+ // arm64-v8a is the primary target (real TV hardware).
+ // x86_64/x86 cover emulators.
+ // armeabi-v7a is opt-in: many pre-2018 Mecool/X96/H96 TV boxes
+ // still ship 32-bit ARMv7 — when a wheel exists in wheels/ we
+ // automatically include the ABI in builds.
+ abiFilters += ledgrabAbis
}
}
@@ -54,9 +70,12 @@ android {
// Each split contains only one native ABI's shared libraries + wheels.
splits {
abi {
+ val v7Wheel = file("$rootDir/wheels").listFiles().orEmpty()
+ .any { it.name.startsWith("pydantic_core-") && it.name.contains("armeabi_v7a") }
isEnable = true
reset()
include("arm64-v8a", "x86_64", "x86")
+ if (v7Wheel) include("armeabi-v7a")
isUniversalApk = true // also produce a fat APK for sideloading
}
}
@@ -96,10 +115,21 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
- signingConfig = if (hasCiSigning) {
- signingConfigs.getByName("release")
- } else {
- signingConfigs.getByName("debug")
+ // Refuse to silently sign release APKs with the debug
+ // keystore — that's how a debug-signed release accidentally
+ // ships. CI must provide all four signing env vars. If a
+ // local "release" build is genuinely intended for testing,
+ // set ANDROID_ALLOW_DEBUG_SIGNED_RELEASE=1 to opt out.
+ val allowDebugSigned =
+ System.getenv("ANDROID_ALLOW_DEBUG_SIGNED_RELEASE") == "1"
+ signingConfig = when {
+ hasCiSigning -> signingConfigs.getByName("release")
+ allowDebugSigned -> signingConfigs.getByName("debug")
+ else -> throw GradleException(
+ "Release builds require signing env vars " +
+ "(ANDROID_KEYSTORE_PATH/PASSWORD, ANDROID_KEY_ALIAS/PASSWORD). " +
+ "Set ANDROID_ALLOW_DEBUG_SIGNED_RELEASE=1 to force a debug-signed release."
+ )
}
}
}
@@ -175,6 +205,9 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-service:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
+ // SplashScreen API — keeps a friendly logo on screen while Chaquopy
+ // unpacks the Python stdlib on first launch (can take 1-3s).
+ implementation("androidx.core:core-splashscreen:1.0.1")
// QR code generation for displaying server URL on TV
implementation("com.google.zxing:core:3.5.3")
// USB-serial drivers (CH340, CP2102, FTDI, Prolific, CDC-ACM) for
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7eb3823..0adf48c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -26,9 +26,15 @@
-
+
+
@@ -60,27 +66,41 @@
-
+
+ android:exported="true"
+ android:theme="@style/Theme.LedGrab.Splash">
-
+
+ android:foregroundServiceType="mediaProjection|specialUse"
+ android:exported="false">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/bg_status_dot.xml b/android/app/src/main/res/drawable/bg_status_dot.xml
index 7df4689..351667b 100644
--- a/android/app/src/main/res/drawable/bg_status_dot.xml
+++ b/android/app/src/main/res/drawable/bg_status_dot.xml
@@ -1,5 +1,9 @@
+
+
diff --git a/android/app/src/main/res/drawable/ic_notification.xml b/android/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..f843195
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_splash.xml b/android/app/src/main/res/drawable/ic_splash.xml
new file mode 100644
index 0000000..fcb4adf
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_splash.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
index 6f135e7..f164676 100644
--- a/android/app/src/main/res/layout/activity_main.xml
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -32,16 +32,28 @@
android:textStyle="bold"
android:letterSpacing="0.08"
android:layout_marginBottom="12dp"
- android:fontFamily="sans-serif-light" />
+ android:fontFamily="sans-serif" />
+ android:layout_marginBottom="24dp" />
+
+
+
+ android:focusableInTouchMode="true"
+ android:nextFocusDown="@+id/autostart_check" />
+ android:focusableInTouchMode="true"
+ android:nextFocusUp="@id/toggle_button" />
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:inflatedId="@+id/running_panel"
+ android:layout="@layout/panel_running"
+ android:visibility="gone" />
diff --git a/android/app/src/main/res/layout/panel_running.xml b/android/app/src/main/res/layout/panel_running.xml
new file mode 100644
index 0000000..38f95cf
--- /dev/null
+++ b/android/app/src/main/res/layout/panel_running.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index 7474e48..9f9ecbf 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -3,12 +3,26 @@
LedGrab
Фоновая подсветка для телевизора
Начать захват
+ Запуск…
Стоп
Работает
+ Проверка root-доступа…
+ Доступ запрещён — для захвата экрана требуется разрешение
+ Нет сети — подключите Wi-Fi или Ethernet
Адрес веб-интерфейса
Сканируйте для настройки
+ или откройте этот адрес с любого устройства в сети
QR-код для веб-интерфейса
v%1$s
Запускать при загрузке (только с root)
Запуск при загрузке — недоступно (нужен root)
+ Не удалось запустить LedGrab
+ Ошибка инициализации Python:
+ Скопировать журнал
+ Показать подробности
+ Скрыть подробности
+ Захват LedGrab
+ Отображается, пока LedGrab захватывает экран.
+ LedGrab работает
+ Веб-интерфейс: %1$s
diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml
index 61ed074..c7ffc32 100644
--- a/android/app/src/main/res/values-zh/strings.xml
+++ b/android/app/src/main/res/values-zh/strings.xml
@@ -3,12 +3,26 @@
LedGrab
电视氛围灯光
开始捕获
+ 正在启动…
停止
运行中
+ 正在检查 root 权限…
+ 权限被拒绝 — 屏幕捕获需要授权
+ 无网络 — 请连接 Wi-Fi 或以太网
Web界面地址
扫码配置
+ 或在同一网络的任何设备上访问上方网址
Web界面二维码
v%1$s
开机自启(仅限 root)
开机自启 — 不可用(需要 root)
+ LedGrab 启动失败
+ Python 运行时初始化失败:
+ 复制日志
+ 显示详情
+ 隐藏详情
+ LedGrab 屏幕捕获
+ LedGrab 捕获屏幕时显示。
+ LedGrab 运行中
+ Web界面:%1$s
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 97b5eaf..6741f73 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -3,12 +3,26 @@
LedGrab
Ambient lighting for your TV
Start Capture
+ Starting…
Stop
Running
+ Checking root access…
+ Permission denied — screen capture requires authorization
+ No network — connect Wi-Fi or Ethernet
Web UI address
Scan to configure
+ or visit the URL above on any device on this network
QR code for web UI
v%1$s
Start on boot (root only)
Start on boot — unavailable (root required)
+ LedGrab failed to start
+ Python runtime initialization failed:
+ Copy log
+ Show details
+ Hide details
+ LedGrab capture
+ Shows while LedGrab is capturing the screen.
+ LedGrab Running
+ Web UI: %1$s
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
index 7cb4d52..56fee4b 100644
--- a/android/app/src/main/res/values/themes.xml
+++ b/android/app/src/main/res/values/themes.xml
@@ -12,6 +12,16 @@
- @color/teal_accent
+
+
+