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 <flavor>-release.apk.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# android-webview-kiosk
|
# 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;
|
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
|
||||||
@@ -27,19 +28,22 @@ replaces it before any gradle command runs.
|
|||||||
|
|
||||||
## Commands (user-run, from repo root)
|
## Commands (user-run, from repo root)
|
||||||
|
|
||||||
- Tests: `nix develop --command gradle --no-daemon :app:testReleaseUnitTest`
|
- Tests: `nix develop --command gradle --no-daemon :app:testWeatherReleaseUnitTest`
|
||||||
- Release APK: `nix develop --command gradle --no-daemon :app:assembleRelease`
|
- All APKs: `nix develop --command gradle --no-daemon :app:assembleRelease`
|
||||||
→ `app/build/outputs/apk/release/app-release.apk`
|
→ `app/build/outputs/apk/<flavor>/release/<flavor>-release.apk`
|
||||||
|
- Single APK: `nix develop --command gradle --no-daemon :app:assembleWeatherRelease`
|
||||||
- Sideload: `adb connect <tv-ip>:5555 && adb install -r <apk>`
|
- Sideload: `adb connect <tv-ip>:5555 && adb install -r <apk>`
|
||||||
(bump `versionCode` in app/build.gradle.kts first for upgrades)
|
(bump `versionCode` in app/build.gradle.kts first for upgrades)
|
||||||
|
- Fresh install: `adb uninstall cz.c3c.webviewkiosk && adb install <apk>`
|
||||||
|
|
||||||
## Key facts
|
## Key facts
|
||||||
|
|
||||||
- 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 hardcoded in `MainActivity.DASHBOARD_URL` — changing it
|
- Dashboard URL + label configured in `dashboards.yaml` (repo root). Each
|
||||||
means rebuild + sideload. Intentional: keeps Control4 integration to a
|
entry becomes a product flavor; `BuildConfig.DASHBOARD_URL` is injected
|
||||||
single parameterless launch call.
|
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,
|
- `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.
|
||||||
|
|||||||
@@ -1,23 +1,47 @@
|
|||||||
# android-webview-kiosk
|
# android-webview-kiosk
|
||||||
|
|
||||||
Fullscreen WebView kiosk for Android TV. Shows the house Grafana dashboard:
|
Fullscreen WebView kiosk for Android TV. Shows configurable Grafana dashboards
|
||||||
<https://grafana.c3c.cz/public-dashboards/381fe3e71e164eb99dd0b10e246a36e2>
|
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
|
## Dashboards
|
||||||
launch via the Bravia REST IP-control API, driven by Control4.
|
|
||||||
|
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
|
## Build
|
||||||
|
|
||||||
|
# all flavors
|
||||||
nix develop --command gradle --no-daemon :app:assembleRelease
|
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/<flavor>/release/<flavor>-release.apk`
|
||||||
|
|
||||||
## Sideload
|
## Sideload
|
||||||
|
|
||||||
1. TV one-time: Settings → About → press *Build* 7× → Developer options →
|
1. TV one-time: Settings → About → press *Build* 7× → Developer options →
|
||||||
enable ADB debugging.
|
enable ADB debugging.
|
||||||
2. `adb connect <tv-ip>:5555`
|
2. `adb connect <tv-ip>: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/<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
|
||||||
with `-r`. Same committed keystore = no uninstall needed.
|
with `-r`. Same committed keystore = no uninstall needed.
|
||||||
|
|||||||
+32
-3
@@ -1,8 +1,14 @@
|
|||||||
|
import org.yaml.snakeyaml.Yaml
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val dashboards: Map<String, Map<String, String>> =
|
||||||
|
Yaml().load(rootProject.file("dashboards.yaml").inputStream())
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "cz.c3c.webviewkiosk"
|
namespace = "cz.c3c.webviewkiosk"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
@@ -15,11 +21,12 @@ android {
|
|||||||
versionName = "0.1.0"
|
versionName = "0.1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
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")
|
storeFile = rootProject.file("signing/release.keystore")
|
||||||
storePassword = "android-webview-kiosk"
|
storePassword = "android-webview-kiosk"
|
||||||
keyAlias = "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 {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:banner="@drawable/banner"
|
android:banner="@drawable/banner"
|
||||||
android:icon="@drawable/icon"
|
android:icon="@drawable/icon"
|
||||||
android:label="Grafana Kiosk"
|
android:label="@string/app_name"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ import android.webkit.WebViewClient
|
|||||||
|
|
||||||
class MainActivity : Activity() {
|
class MainActivity : Activity() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val DASHBOARD_URL =
|
|
||||||
"https://grafana.c3c.cz/public-dashboards/381fe3e71e164eb99dd0b10e246a36e2"
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var webView: WebView
|
private lateinit var webView: WebView
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private val backoff = BackoffPolicy(initialDelayMs = 2_000, maxDelayMs = 60_000)
|
private val backoff = BackoffPolicy(initialDelayMs = 2_000, maxDelayMs = 60_000)
|
||||||
@@ -55,7 +50,7 @@ class MainActivity : Activity() {
|
|||||||
if (!request.isForMainFrame) return
|
if (!request.isForMainFrame) return
|
||||||
lastLoadFailed = true
|
lastLoadFailed = true
|
||||||
handler.postDelayed(
|
handler.postDelayed(
|
||||||
{ view.loadUrl(DASHBOARD_URL) },
|
{ view.loadUrl(BuildConfig.DASHBOARD_URL) },
|
||||||
backoff.nextDelayMs(),
|
backoff.nextDelayMs(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -63,7 +58,7 @@ class MainActivity : Activity() {
|
|||||||
|
|
||||||
setContentView(webView)
|
setContentView(webView)
|
||||||
hideSystemUi()
|
hideSystemUi()
|
||||||
webView.loadUrl(DASHBOARD_URL)
|
webView.loadUrl(BuildConfig.DASHBOARD_URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
buildscript {
|
||||||
|
dependencies {
|
||||||
|
classpath("org.yaml:snakeyaml:2.2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.7.3" apply false
|
id("com.android.application") version "8.7.3" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
|
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user