4a87cef98c
Bring the Android client to parity with Apple's gamepad experience and finish Wake-on-LAN. - Console/gamepad home: host carousel, aurora chrome, mTLS game-library coverflow, and an input-aware legend that switches between gamepad face buttons and a TV-remote select-ring + arrows based on the last-used input. - Wake-on-LAN: the fire-and-forget send is upgraded to wait-until-up (WakeController/WakeOverlay: resend + mDNS poll, 90s timeout, cancel/retry, fingerprint-matched so a host that cold-boots onto a new DHCP IP still connects), plus host edit (touch dialog + console form) with an auto-filled MAC. - Android TV: brand banner (android:banner), density-aware console scaling, D-pad/ remote nav (Up = Settings, Down or the pad Select button = host Options), emergency stream-exit chord, and 120Hz console refresh. - Touch UI: settings split into subpages with a tablet NavigationRail, axis-aware tab animation (horizontal on phones, vertical on the tablet rail), animated settings navigation, and a licenses screen with a back button + the real workspace version (read from Cargo.toml). - Vector Lock/controller icons (no emoji); bundled Geist font. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
153 lines
7.5 KiB
Kotlin
153 lines
7.5 KiB
Kotlin
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||
|
||
import java.util.Properties
|
||
|
||
plugins {
|
||
id("com.android.application")
|
||
// AGP 9 built-in Kotlin: NO org.jetbrains.kotlin.android. The Compose compiler plugin is
|
||
// supplied by AGP, so it's applied without a version.
|
||
id("org.jetbrains.kotlin.plugin.compose")
|
||
}
|
||
|
||
android {
|
||
namespace = "io.unom.punktfunk"
|
||
compileSdk = 37 // Android 17 — required by androidx.core 1.19.0; targetSdk stays 36 for now.
|
||
|
||
defaultConfig {
|
||
// Load from .env if it exists (local dev), otherwise from System.getenv (CI)
|
||
val envFile = project.rootProject.file(".env")
|
||
val props = Properties()
|
||
if (envFile.exists()) {
|
||
envFile.inputStream().use { props.load(it) }
|
||
}
|
||
|
||
applicationId = "io.unom.punktfunk"
|
||
// Android 9. Reaches older Android TV boxes (e.g. Amlogic streamers still on Android 9–11);
|
||
// the handful of API 31+ APIs we use are runtime-gated (Material You → brand palette, rumble
|
||
// → legacy Vibrator, NEARBY_WIFI/lights/ADPF already gated), so nothing is lost above 28.
|
||
minSdk = 28
|
||
targetSdk = 36
|
||
val vCode = (props.getProperty("VERSION_CODE") ?: System.getenv("VERSION_CODE"))
|
||
versionCode = vCode?.toInt() ?: 1
|
||
// versionName is the single project version, threaded from CI (a vX.Y.Z release or a
|
||
// canary string). versionCode stays the monotonic run number (Play rejects regressions).
|
||
// Local dev (no VERSION_NAME) falls back to the workspace version from the root Cargo.toml —
|
||
// the single source of truth — so an on-device build shows the real current version, not a
|
||
// stale placeholder.
|
||
val workspaceVersion = runCatching {
|
||
project.rootProject.file("../../Cargo.toml").readLines()
|
||
.dropWhile { !it.trim().startsWith("[workspace.package]") }
|
||
.firstOrNull { it.trim().startsWith("version") }
|
||
?.substringAfter('=')?.trim()?.trim('"')
|
||
}.getOrNull()
|
||
versionName = (props.getProperty("VERSION_NAME") ?: System.getenv("VERSION_NAME"))
|
||
?: workspaceVersion ?: "0.0.0"
|
||
// Ship 32-bit armeabi-v7a alongside 64-bit arm64-v8a: many Google TV / Android TV streamers
|
||
// (Walmart onn. 4K, Chromecast with Google TV, budget Amlogic boxes) run a 32-bit Android
|
||
// userspace, and because this app carries native code, Google Play (and a sideload installer)
|
||
// filters it as "not compatible" on those devices unless an armeabi-v7a variant is present.
|
||
// x86_64 stays for the emulator. Google keeps delivering to 32-bit TV devices (see the Aug
|
||
// 2025 "64-bit app compatibility for Google TV and Android TV" post) — the 64-bit lib is the
|
||
// required half; the 32-bit lib is what actually reaches the boxes people report failing.
|
||
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") }
|
||
}
|
||
|
||
signingConfigs {
|
||
create("release") {
|
||
// Load from .env if it exists (local dev), otherwise from System.getenv (CI)
|
||
val envFile = project.rootProject.file(".env")
|
||
val props = Properties()
|
||
if (envFile.exists()) {
|
||
envFile.inputStream().use { props.load(it) }
|
||
}
|
||
|
||
val ksFile = props.getProperty("RELEASE_KEYSTORE_FILE") ?: System.getenv("RELEASE_KEYSTORE_FILE")
|
||
if (ksFile != null) {
|
||
storeFile = file(ksFile)
|
||
storePassword = props.getProperty("RELEASE_KEYSTORE_PASSWORD") ?: System.getenv("RELEASE_KEYSTORE_PASSWORD")
|
||
keyAlias = props.getProperty("RELEASE_KEY_ALIAS") ?: System.getenv("RELEASE_KEY_ALIAS")
|
||
keyPassword = props.getProperty("RELEASE_KEY_PASSWORD") ?: System.getenv("RELEASE_KEY_PASSWORD")
|
||
}
|
||
}
|
||
}
|
||
|
||
buildTypes {
|
||
release {
|
||
isMinifyEnabled = true
|
||
isShrinkResources = true
|
||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||
signingConfig = signingConfigs.getByName("release")
|
||
}
|
||
}
|
||
|
||
buildFeatures { compose = true }
|
||
|
||
// Roborazzi/Robolectric render Compose on the host JVM (the CI screenshot harness) and need the
|
||
// merged Android resources + the app's manifest/theme available to the unit tests.
|
||
testOptions { unitTests { isIncludeAndroidResources = true } }
|
||
|
||
compileOptions {
|
||
sourceCompatibility = JavaVersion.VERSION_21
|
||
targetCompatibility = JavaVersion.VERSION_21
|
||
}
|
||
packaging {
|
||
jniLibs {
|
||
useLegacyPackaging = false
|
||
// punktfunk-core is statically linked into libpunktfunk_android.so (rlib). Its standalone
|
||
// cdylib (built because the core crate also declares crate-type = cdylib) is never loaded
|
||
// by Kotlin — drop it from the APK rather than ship ~5–9 MB of dead code.
|
||
excludes += "**/libpunktfunk_core.so"
|
||
}
|
||
}
|
||
}
|
||
|
||
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
|
||
|
||
dependencies {
|
||
implementation(project(":kit"))
|
||
|
||
val composeBom = platform("androidx.compose:compose-bom:2026.05.01")
|
||
implementation(composeBom)
|
||
|
||
implementation("androidx.core:core-ktx:1.19.0")
|
||
implementation("androidx.activity:activity-compose:1.13.0")
|
||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
|
||
|
||
implementation("androidx.compose.ui:ui")
|
||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||
implementation("androidx.compose.foundation:foundation")
|
||
implementation("androidx.compose.material3:material3")
|
||
implementation("androidx.compose.material:material-icons-core") // bottom-bar / rail tab icons
|
||
implementation("androidx.compose.material:material-icons-extended") // settings-category icons
|
||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||
|
||
// Cover-art loading for the game-library coverflow. Coil 2.x uses OkHttp under the hood, so we
|
||
// feed it the same mTLS OkHttpClient the library fetch uses (reaching the host's own art proxy).
|
||
implementation("io.coil-kt:coil-compose:2.7.0")
|
||
|
||
// Real backdrop blur for the floating console legends (RenderEffect on API 31+, a translucent
|
||
// scrim below). The gamepad UI's frosted pills sample + blur whatever scrolls behind them.
|
||
implementation("dev.chrisbanes.haze:haze:1.6.0")
|
||
|
||
// Android TV components (we target phone + TV) land in the TV-UI milestone:
|
||
// implementation("androidx.tv:tv-material:1.1.0")
|
||
// The manifest already declares leanback so the scaffold installs on TV.
|
||
|
||
// --- CI screenshot harness (Roborazzi on the JVM via Robolectric — no emulator/GPU). The
|
||
// screenshot tests render the real Compose UI with mock state; never load the JNI core, so the
|
||
// job runs `:app:testDebugUnitTest -PskipRustBuild` (see kit/build.gradle.kts). ---
|
||
testImplementation(composeBom)
|
||
testImplementation("androidx.compose.ui:ui-test-junit4")
|
||
debugImplementation("androidx.compose.ui:ui-test-manifest") // the ComponentActivity test host
|
||
testImplementation("junit:junit:4.13.2")
|
||
testImplementation("org.robolectric:robolectric:4.16.1")
|
||
testImplementation("io.github.takahirom.roborazzi:roborazzi:1.64.0")
|
||
testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.64.0")
|
||
}
|
||
|
||
// Record (write) the screenshots when the unit tests run. These tests exist to GENERATE marketing
|
||
// images, not to diff goldens, so always capture rather than verify.
|
||
tasks.withType<Test>().configureEach {
|
||
systemProperty("roborazzi.test.record", "true")
|
||
}
|