From 4982af9df4ddd34f1ef6e96624cb79e328f80ea7 Mon Sep 17 00:00:00 2001 From: Arnie via Claude Date: Fri, 12 Jun 2026 18:15:21 +0200 Subject: [PATCH] feat: multi-flavor APK build from dashboards.yaml Dashboard URL and label configured in dashboards.yaml; each entry becomes a product flavor with BuildConfig.DASHBOARD_URL injected at build time. APKs output as -release.apk. --- CLAUDE.md | 18 ++++++---- README.md | 36 +++++++++++++++---- app/build.gradle.kts | 35 ++++++++++++++++-- app/src/main/AndroidManifest.xml | 2 +- .../java/cz/c3c/webviewkiosk/MainActivity.kt | 9 ++--- build.gradle.kts | 6 ++++ dashboards.yaml | 7 ++++ 7 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 dashboards.yaml diff --git a/CLAUDE.md b/CLAUDE.md index a574190..6bdc714 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,7 @@ # android-webview-kiosk -Android TV app: fullscreen WebView showing one hardcoded Grafana dashboard. +Android TV app: fullscreen WebView showing a configurable Grafana dashboard. +Multiple APK flavors built from `dashboards.yaml` (repo root) — one APK per entry. Runs on the living-room Sony Bravia KD-65XE9305 (Android 8.0, API 26; Android System WebView updates independently via Play Store — currently v138). Launched remotely through the Bravia REST IP-control API by Control4 @@ -27,19 +28,22 @@ replaces it before any gradle command runs. ## Commands (user-run, from repo root) -- Tests: `nix develop --command gradle --no-daemon :app:testReleaseUnitTest` -- Release APK: `nix develop --command gradle --no-daemon :app:assembleRelease` - → `app/build/outputs/apk/release/app-release.apk` +- Tests: `nix develop --command gradle --no-daemon :app:testWeatherReleaseUnitTest` +- All APKs: `nix develop --command gradle --no-daemon :app:assembleRelease` + → `app/build/outputs/apk//release/-release.apk` +- Single APK: `nix develop --command gradle --no-daemon :app:assembleWeatherRelease` - Sideload: `adb connect :5555 && adb install -r ` (bump `versionCode` in app/build.gradle.kts first for upgrades) +- Fresh install: `adb uninstall cz.c3c.webviewkiosk && adb install ` ## Key facts - Package/applicationId: `cz.c3c.webviewkiosk`; repo name keeps the `android-` prefix, the package can't (hyphens illegal). -- Dashboard URL hardcoded in `MainActivity.DASHBOARD_URL` — changing it - means rebuild + sideload. Intentional: keeps Control4 integration to a - single parameterless launch call. +- Dashboard URL + label configured in `dashboards.yaml` (repo root). Each + entry becomes a product flavor; `BuildConfig.DASHBOARD_URL` is injected + at build time. Changing a URL means editing the YAML, rebuilding, resideloading. + Intentional: keeps Control4 integration to a single parameterless launch call. - `signing/release.keystore` is committed on purpose (private repo, LAN-only kiosk) so every build is upgrade-compatible. Don't reuse the key. - minSdk 26 is a hard floor — the TV never gets newer Android. diff --git a/README.md b/README.md index ab7af5a..bdf8017 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,47 @@ # android-webview-kiosk -Fullscreen WebView kiosk for Android TV. Shows the house Grafana dashboard: - +Fullscreen WebView kiosk for Android TV. Shows configurable Grafana dashboards +defined in `dashboards.yaml`. Target device: Sony Bravia KD-65XE9305 (Android 8.0). +Built for remote launch via the Bravia REST IP-control API, driven by Control4. -Target device: Sony Bravia KD-65XE9305 (Android 8.0). Built for remote -launch via the Bravia REST IP-control API, driven by Control4. +## Dashboards + +Defined in `dashboards.yaml` at repo root: + +```yaml +weather: + url: https://grafana.c3c.cz/public-dashboards/... + label: Weather # shown in Bravia launcher + +house_condition: + url: http://grafana.c3c.cz/public-dashboards/... + label: House Condition +``` + +Each entry produces a separate APK. Names must be valid Gradle identifiers +(letters, digits, underscores — no hyphens). ## Build + # all flavors nix develop --command gradle --no-daemon :app:assembleRelease -APK: `app/build/outputs/apk/release/app-release.apk` + # single flavor + nix develop --command gradle --no-daemon :app:assembleWeatherRelease + +APKs: `app/build/outputs/apk//release/-release.apk` ## Sideload 1. TV one-time: Settings → About → press *Build* 7× → Developer options → enable ADB debugging. 2. `adb connect :5555` -3. `adb install -r app/build/outputs/apk/release/app-release.apk` +3. `adb install -r app/build/outputs/apk/weather/release/weather-release.apk` + +Fresh install (removing old app first): + + adb uninstall cz.c3c.webviewkiosk + adb install app/build/outputs/apk//release/-release.apk Upgrades: bump `versionCode` in `app/build.gradle.kts`, rebuild, reinstall with `-r`. Same committed keystore = no uninstall needed. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 00288e2..d31f643 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,8 +1,14 @@ +import org.yaml.snakeyaml.Yaml + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } +@Suppress("UNCHECKED_CAST") +val dashboards: Map> = + Yaml().load(rootProject.file("dashboards.yaml").inputStream()) + android { namespace = "cz.c3c.webviewkiosk" compileSdk = 34 @@ -15,11 +21,12 @@ android { versionName = "0.1.0" } + buildFeatures { + buildConfig = true + } + signingConfigs { create("release") { - // Keystore is committed (private repo, LAN kiosk app) so every - // machine/CI produces the same signature and `adb install -r` - // upgrades work without uninstalling. Created in Task 6. storeFile = rootProject.file("signing/release.keystore") storePassword = "android-webview-kiosk" keyAlias = "kiosk" @@ -34,6 +41,28 @@ android { } } + flavorDimensions += "dashboard" + + productFlavors { + dashboards.forEach { (name, config) -> + create(name) { + dimension = "dashboard" + buildConfigField("String", "DASHBOARD_URL", "\"${config["url"]}\"") + resValue("string", "app_name", config["label"] ?: name) + } + } + } + + @Suppress("DEPRECATION") + applicationVariants.all { + val flavor = productFlavors.first().name + val type = buildType.name + outputs.all { + (this as com.android.build.gradle.internal.api.BaseVariantOutputImpl) + .outputFileName = "$flavor-$type.apk" + } + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4387fae..2dabc88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ android:allowBackup="false" android:banner="@drawable/banner" android:icon="@drawable/icon" - android:label="Grafana Kiosk" + android:label="@string/app_name" android:usesCleartextTraffic="true" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"> diff --git a/app/src/main/java/cz/c3c/webviewkiosk/MainActivity.kt b/app/src/main/java/cz/c3c/webviewkiosk/MainActivity.kt index f9a141f..c1fd85e 100644 --- a/app/src/main/java/cz/c3c/webviewkiosk/MainActivity.kt +++ b/app/src/main/java/cz/c3c/webviewkiosk/MainActivity.kt @@ -15,11 +15,6 @@ import android.webkit.WebViewClient class MainActivity : Activity() { - companion object { - const val DASHBOARD_URL = - "https://grafana.c3c.cz/public-dashboards/381fe3e71e164eb99dd0b10e246a36e2" - } - private lateinit var webView: WebView private val handler = Handler(Looper.getMainLooper()) private val backoff = BackoffPolicy(initialDelayMs = 2_000, maxDelayMs = 60_000) @@ -55,7 +50,7 @@ class MainActivity : Activity() { if (!request.isForMainFrame) return lastLoadFailed = true handler.postDelayed( - { view.loadUrl(DASHBOARD_URL) }, + { view.loadUrl(BuildConfig.DASHBOARD_URL) }, backoff.nextDelayMs(), ) } @@ -63,7 +58,7 @@ class MainActivity : Activity() { setContentView(webView) hideSystemUi() - webView.loadUrl(DASHBOARD_URL) + webView.loadUrl(BuildConfig.DASHBOARD_URL) } override fun onResume() { diff --git a/build.gradle.kts b/build.gradle.kts index c7ad754..ec59988 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,9 @@ +buildscript { + dependencies { + classpath("org.yaml:snakeyaml:2.2") + } +} + plugins { id("com.android.application") version "8.7.3" apply false id("org.jetbrains.kotlin.android") version "2.0.21" apply false diff --git a/dashboards.yaml b/dashboards.yaml new file mode 100644 index 0000000..7f040e1 --- /dev/null +++ b/dashboards.yaml @@ -0,0 +1,7 @@ +weather: + url: https://grafana.c3c.cz/public-dashboards/381fe3e71e164eb99dd0b10e246a36e2 + label: Weather + +house_condition: + url: http://grafana.c3c.cz/public-dashboards/7d94ddc4493741debf49d0a301e1a757 + label: House Condition