Repository navigation
Building from Source
You don't have to build the APK yourself; there are unsigned builds on the Releases page. But if you want to change something, debug a model-specific issue, or just don't want to trust pre-built binaries, here's how.
The repo ships with a .devcontainer/ config. Open the repo in VS Code with the Dev Containers extension and it pulls down a container with the right Android SDK, JDK, and gradle already set up. From a terminal inside the container:
./gradlew assembleDebugYou'll find the APK at app/build/outputs/apk/debug/app-debug.apk. Install it:
adb install -g app/build/outputs/apk/debug/app-debug.apkIf you'd rather build directly:
-
Android SDK with
compileSdk36. Android Studio is the easy installer. - JDK 17: Android Studio bundles one; otherwise install it separately. The build targets Java 17.
-
Android Studio / AGP: the project is on Android Gradle Plugin 9 (Gradle 9.6.1 via the wrapper). AGP 9 needs a recent Android Studio — if the IDE can't sync or shows no run configuration, it's usually too old for AGP 9. Kotlin is built into AGP 9, so there's no separate Kotlin plugin; the JVM target is set with
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }. -
Gradle wrapper: the
./gradlewscript in the repo. Don't install gradle separately, just use the wrapper.
./gradlew assembleDebug # debug build
./gradlew assembleRelease # release build
./gradlew installDebug # build and install to connected deviceMinimum / target SDK is 24. These displays don't run anything newer.
Note
gradle.properties keeps android.uniquePackageNames=false. That's not optional — tensorflow-lite and tensorflow-lite-api both declare the org.tensorflow.lite namespace, which AGP 9 rejects by default. Leave it in.
The repo includes a workflow at .github/workflows/release-apk.yml that builds a signed release APK whenever you push a version tag. To cut a release:
git tag v3.0.0
git push origin v3.0.0The workflow builds app/build/outputs/apk/release/*.apk and attaches it to the GitHub release for that tag. It signs with the keystore in the SIGNING_KEYSTORE_BASE64 / SIGNING_STORE_PASSWORD / SIGNING_KEY_ALIAS / SIGNING_KEY_PASSWORD repository secrets; if those aren't set it falls back to debug signing. On a tag build it passes SE_RELEASE_VERSION so the APK's versionName/versionCode match the tag exactly (otherwise the version is a build-time 3.YYDDD.HHMM timestamp).
Signing key matters for the in-app self-update: the app only installs an update whose signature matches the currently-installed one. If you fork and cut your own releases, use a consistent key across them (replace the signing config / secrets), or devices on your builds won't self-update.
- The native side (
app/src/main/cpp/) gets built automatically via the CMake integration. It includes the TensorFlow Lite microfrontend for the voice wake-word detector and a small input-event helper for proximity on certain devices. If a build fails insidetflm/, you usually have the wrong NDK version. - ProGuard / R8 are configured but conservative. If you change something and a method on the JavaScript interface stops being called, double-check
app/proguard-rules.pro. Bridge methods need to be kept. -
minSdk = 24is intentional. Don't bump it without checking that nothing in the wall display's underlying Android version (which is on the older side) breaks.
Same as Installation:
adb install -g path/to/your-build.apk
adb rebootThe app grants WRITE_SETTINGS and the doze whitelist itself on first launch, so the manual appops / deviceidle steps are only needed as a fallback if brightness control doesn't work. See Installation.
If you're sending a PR back, two requests:
- Test on actual hardware. The codebase has model-specific paths (different sysfs nodes, inverted relays on XL/XLi, gpio_keys for proximity on X2i, etc.) and emulators won't catch those. If you only have one model, say so in the PR. Someone else with a different display can help you cover the rest.
- Keep the WebView-side and the hardware-side independent. Adding a feature that calls into hardware from a button handler is fine; adding one that only works because the WebView is loaded breaks Lite Mode.
Open issues and feature requests on the issue tracker.
Setup
Integration
Features
Reference