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" /> + + +