Android app (WebView wrapper)
The Android app is a thin native shell: the whole UI is the web dashboard served by the Flask app, loaded in a full-screen WebView. Same principle as lms-material-app. Don't rebuild dashboard UI natively — if a feature belongs on screen, it belongs in the web page; the app only provides shell concerns (window, settings, discovery, a native bridge).
Everything lives under android/. Package com.werdeil.lyriondashboard, Kotlin, Gradle KTS, minSdk = 26 (Android 8.0), targetSdk = 35, compileSdk = 37, Java/JVM 17.
Layout
android/app/src/main/
├── java/com/werdeil/lyriondashboard/
│ ├── MainActivity.kt # the WebView host + JS bridge
│ ├── SettingsActivity.kt # native settings screen
│ ├── ServerDiscovery.kt # LMS auto-discovery (UDP broadcast)
│ └── Insets.kt # edge-to-edge window insets
├── res/values/strings.xml # EN strings (values-fr/ = FR)
├── res/xml/preferences.xml # settings screen definition
└── AndroidManifest.xml
Build & lint
cd android
./gradlew assembleDebug # -> app/build/outputs/apk/debug/app-debug.apk
./gradlew lintDebug # Android Lint (gates CI)
CI is .github/workflows/android.yml: on every push touching android/** it runs lintDebug then assembleDebug and uploads the debug APK as an artifact. The release APK is built and attached only when a GitHub release is published (see the release skill).
Signing: debug vs release
- Debug is signed with a committed
app/debug.keystore (standard debug credentials) on purpose — so every ephemeral CI runner signs with the same key and Android will update the app over a previous install instead of refusing. Debug builds also use an applicationIdSuffix = ".debug" and a distinct label, so they install side by side with the release app.
- Release signing is driven by environment variables (
ANDROID_KEYSTORE_PATH and friends) so the release keystore is never committed. Without them a local release build stays unsigned, which is also what F-Droid wants — but the release workflow refuses to publish an unsigned APK: it validates the keystore up front and runs apksigner verify on the result (see the release skill).
Versioning (must stay static)
versionName and versionCode in app/build.gradle.kts are static literals, committed, because F-Droid builds from the tagged source and parses them from that file. versionCode packs the semver as X*10000 + Y*100 + Z. Don't compute them at build time. Bumping is the Release workflow's job — see the release skill.
The JS ↔ native bridge
MainActivity injects a window.LyrionApp object into the page. The web JS detects it, adds body.in-app, and wires the header menu to bridge.openMenu() / openSettings() and the card's pull-to-refresh gesture to bridge.reload() (see the frontend skill). If you add a native capability the page should call, expose it as a method on that bridge and guard the page-side use behind the object's presence, matching the existing pattern.
Auto-discovery
ServerDiscovery finds the Lyrion server via UDP broadcast on port 3483 (the standard LMS discovery protocol) and suggests http://<host>:1111 (assumes the dashboard runs on the same host). Caveat baked into the UX: discovery can't cross a VPN tunnel, so the settings screen lets the user enter the URL manually. Cleartext HTTP is allowed (LAN-only app).
Strings / i18n
App strings are bilingual like the web app: res/values/strings.xml (EN) and res/values-fr/strings.xml (FR) must carry the same keys. Store-listing texts live under fastlane/metadata/android/{en-US,fr-FR}/. Add a new string to both locales — never one only. The same tree holds images/icon.png, the 512x512 render of static/favicon.svg that F-Droid shows on the app page: its indexer cannot render the adaptive-icon XML, so without that bitmap the listing has no icon. full_description.txt is never hard-wrapped — F-Droid clients keep every newline, so a paragraph wrapped at 70 columns renders ragged; one paragraph (and one bullet) is one line, however long.
Checklist
- Keep it a shell — new UI goes in the web page, not native views.
- New user string → both
values/ and values-fr/ strings.xml.
- New native capability the page uses → method on
window.LyrionApp, guarded page-side by the bridge's presence.
- Leave
versionName/versionCode static; bump via the Release workflow.
./gradlew lintDebug assembleDebug clean before pushing (CI runs both).