refactor: rename dashboards→webviews, unique applicationId per flavor
build / build (push) Failing after 3m11s

Each flavor gets applicationIdSuffix = ".<name>" so the Bravia
getApplicationList API returns a distinct URI per webview, enabling
Control4 to launch specific flavors independently.

BREAKING CHANGE: applicationId changed from cz.c3c.webviewkiosk to
cz.c3c.webviewkiosk.<flavor> — uninstall old app before reinstalling.
BuildConfig.DASHBOARD_URL renamed to WEBVIEW_URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Arnie via Claude
2026-06-12 18:22:51 +02:00
parent 4982af9df4
commit a36d3869b1
5 changed files with 32 additions and 27 deletions
+9 -8
View File
@@ -1,7 +1,7 @@
# android-webview-kiosk # android-webview-kiosk
Android TV app: fullscreen WebView showing a configurable Grafana dashboard. Android TV app: fullscreen WebView kiosk.
Multiple APK flavors built from `dashboards.yaml` (repo root) — one APK per entry. Multiple APK flavors built from `webviews.yaml` (repo root) — one APK per entry.
Runs on the living-room Sony Bravia KD-65XE9305 (Android 8.0, API 26; Runs on the living-room Sony Bravia KD-65XE9305 (Android 8.0, API 26;
Android System WebView updates independently via Play Store — currently v138). Android System WebView updates independently via Play Store — currently v138).
Launched remotely through the Bravia REST IP-control API by Control4 Launched remotely through the Bravia REST IP-control API by Control4
@@ -40,17 +40,18 @@ replaces it before any gradle command runs.
- Package/applicationId: `cz.c3c.webviewkiosk`; repo name keeps the - Package/applicationId: `cz.c3c.webviewkiosk`; repo name keeps the
`android-` prefix, the package can't (hyphens illegal). `android-` prefix, the package can't (hyphens illegal).
- Dashboard URL + label configured in `dashboards.yaml` (repo root). Each - Webview URL + label configured in `webviews.yaml` (repo root). Each
entry becomes a product flavor; `BuildConfig.DASHBOARD_URL` is injected entry becomes a product flavor with `applicationIdSuffix = ".<name>"`;
at build time. Changing a URL means editing the YAML, rebuilding, resideloading. `BuildConfig.WEBVIEW_URL` is injected at build time. Changing a URL means
Intentional: keeps Control4 integration to a single parameterless launch call. editing the YAML, rebuilding, resideloading. Intentional: keeps Control4
integration to a single parameterless launch call.
- `signing/release.keystore` is committed on purpose (private repo, - `signing/release.keystore` is committed on purpose (private repo,
LAN-only kiosk) so every build is upgrade-compatible. Don't reuse the key. 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. - minSdk 26 is a hard floor — the TV never gets newer Android.
- `LEANBACK_LAUNCHER` category + `android:banner` are what make the app - `LEANBACK_LAUNCHER` category + `android:banner` are what make the app
visible to the Bravia API's `getApplicationList`/`setActiveApp`. Don't visible to the Bravia API's `getApplicationList`/`setActiveApp`. Don't
remove either. remove either.
- App URI (from `getApplicationList`): - App URIs (from `getApplicationList`) are per-flavor, e.g. weather:
`com.sony.dtv.cz.c3c.webviewkiosk.cz.c3c.webviewkiosk.MainActivity` `com.sony.dtv.cz.c3c.webviewkiosk.weather.cz.c3c.webviewkiosk.weather.MainActivity`
- `setActiveApp` alone wakes the TV from standby — no separate - `setActiveApp` alone wakes the TV from standby — no separate
`setPowerStatus` call needed. Control4 driver can skip the wake step. `setPowerStatus` call needed. Control4 driver can skip the wake step.
+14 -11
View File
@@ -1,12 +1,12 @@
# android-webview-kiosk # android-webview-kiosk
Fullscreen WebView kiosk for Android TV. Shows configurable Grafana dashboards Fullscreen WebView kiosk for Android TV. Shows configurable web pages
defined in `dashboards.yaml`. Target device: Sony Bravia KD-65XE9305 (Android 8.0). defined in `webviews.yaml`. Target device: Sony Bravia KD-65XE9305 (Android 8.0).
Built for remote launch via the Bravia REST IP-control API, driven by Control4. Built for remote launch via the Bravia REST IP-control API, driven by Control4.
## Dashboards ## Webviews
Defined in `dashboards.yaml` at repo root: Defined in `webviews.yaml` at repo root:
```yaml ```yaml
weather: weather:
@@ -18,7 +18,8 @@ house_condition:
label: House Condition label: House Condition
``` ```
Each entry produces a separate APK. Names must be valid Gradle identifiers Each entry produces a separate APK with a unique `applicationId`
(`cz.c3c.webviewkiosk.<name>`). Names must be valid Gradle identifiers
(letters, digits, underscores — no hyphens). (letters, digits, underscores — no hyphens).
## Build ## Build
@@ -38,9 +39,9 @@ APKs: `app/build/outputs/apk/<flavor>/release/<flavor>-release.apk`
2. `adb connect <tv-ip>:5555` 2. `adb connect <tv-ip>:5555`
3. `adb install -r app/build/outputs/apk/weather/release/weather-release.apk` 3. `adb install -r app/build/outputs/apk/weather/release/weather-release.apk`
Fresh install (removing old app first): Fresh install (first time or changing applicationId):
adb uninstall cz.c3c.webviewkiosk adb uninstall cz.c3c.webviewkiosk.<flavor>
adb install app/build/outputs/apk/<flavor>/release/<flavor>-release.apk adb install app/build/outputs/apk/<flavor>/release/<flavor>-release.apk
Upgrades: bump `versionCode` in `app/build.gradle.kts`, rebuild, reinstall Upgrades: bump `versionCode` in `app/build.gradle.kts`, rebuild, reinstall
@@ -51,15 +52,17 @@ with `-r`. Same committed keystore = no uninstall needed.
TV prerequisite: IP control auth = "Normal and Pre-Shared Key", Remote TV prerequisite: IP control auth = "Normal and Pre-Shared Key", Remote
start enabled (wake from deep standby). start enabled (wake from deep standby).
App URI (from `getApplicationList`): Each webview flavor has its own URI — retrieve per-flavor URIs via
`com.sony.dtv.cz.c3c.webviewkiosk.cz.c3c.webviewkiosk.MainActivity` `getApplicationList`. Example for `weather` (`cz.c3c.webviewkiosk.weather`):
`com.sony.dtv.cz.c3c.webviewkiosk.weather.cz.c3c.webviewkiosk.weather.MainActivity`
**`setActiveApp` alone wakes the TV from standby** — no separate **`setActiveApp` alone wakes the TV from standby** — no separate
`setPowerStatus` call is needed. The Control4 driver can use a single call: `setPowerStatus` call is needed. The Control4 driver can use a single call:
curl -s -X POST http://$TV_IP/sony/appControl \ curl -s -X POST http://$TV_IP/sony/appControl \
-H "X-Auth-PSK: $PSK" -H 'Content-Type: application/json' \ -H "X-Auth-PSK: $PSK" -H 'Content-Type: application/json' \
-d '{"method":"setActiveApp","id":601,"version":"1.0","params":[{"uri":"com.sony.dtv.cz.c3c.webviewkiosk.cz.c3c.webviewkiosk.MainActivity"}]}' -d '{"method":"setActiveApp","id":601,"version":"1.0","params":[{"uri":"<flavor-uri>"}]}'
If the TV is already on, `setActiveApp` brings the kiosk to the foreground. If the TV is already on, `setActiveApp` brings the kiosk to the foreground.
If in standby, it wakes and launches directly. If in standby, it wakes and launches directly.
@@ -74,6 +77,6 @@ For reference, the two-step sequence (if needed for other integrations):
# 2. launch (~6 s from quick standby, up to ~30 s from deep eco standby) # 2. launch (~6 s from quick standby, up to ~30 s from deep eco standby)
curl -s -X POST http://$TV_IP/sony/appControl \ curl -s -X POST http://$TV_IP/sony/appControl \
-H "X-Auth-PSK: $PSK" -H 'Content-Type: application/json' \ -H "X-Auth-PSK: $PSK" -H 'Content-Type: application/json' \
-d '{"method":"setActiveApp","id":601,"version":"1.0","params":[{"uri":"com.sony.dtv.cz.c3c.webviewkiosk.cz.c3c.webviewkiosk.MainActivity"}]}' -d '{"method":"setActiveApp","id":601,"version":"1.0","params":[{"uri":"<flavor-uri>"}]}'
Control4: a DriverWorks driver issuing `setActiveApp` is a separate project. Control4: a DriverWorks driver issuing `setActiveApp` is a separate project.
+7 -6
View File
@@ -6,8 +6,8 @@ plugins {
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val dashboards: Map<String, Map<String, String>> = val webviews: Map<String, Map<String, String>> =
Yaml().load(rootProject.file("dashboards.yaml").inputStream()) Yaml().load(rootProject.file("webviews.yaml").inputStream())
android { android {
namespace = "cz.c3c.webviewkiosk" namespace = "cz.c3c.webviewkiosk"
@@ -41,13 +41,14 @@ android {
} }
} }
flavorDimensions += "dashboard" flavorDimensions += "webview"
productFlavors { productFlavors {
dashboards.forEach { (name, config) -> webviews.forEach { (name, config) ->
create(name) { create(name) {
dimension = "dashboard" dimension = "webview"
buildConfigField("String", "DASHBOARD_URL", "\"${config["url"]}\"") applicationIdSuffix = ".$name"
buildConfigField("String", "WEBVIEW_URL", "\"${config["url"]}\"")
resValue("string", "app_name", config["label"] ?: name) resValue("string", "app_name", config["label"] ?: name)
} }
} }
@@ -50,7 +50,7 @@ class MainActivity : Activity() {
if (!request.isForMainFrame) return if (!request.isForMainFrame) return
lastLoadFailed = true lastLoadFailed = true
handler.postDelayed( handler.postDelayed(
{ view.loadUrl(BuildConfig.DASHBOARD_URL) }, { view.loadUrl(BuildConfig.WEBVIEW_URL) },
backoff.nextDelayMs(), backoff.nextDelayMs(),
) )
} }
@@ -58,7 +58,7 @@ class MainActivity : Activity() {
setContentView(webView) setContentView(webView)
hideSystemUi() hideSystemUi()
webView.loadUrl(BuildConfig.DASHBOARD_URL) webView.loadUrl(BuildConfig.WEBVIEW_URL)
} }
override fun onResume() { override fun onResume() {
View File