Files
proton-stream/docs/ANDROID.md
T
nvrl 8e42906a7f feat(core,app,android,cli,ios): sync the library and settings, verify a share before adding it
Sync carried only watch positions, so a second device started empty. The
per-installation history file now also carries per-key registers
(pstr-core::profile): every share with its link and custom password, the
playback, appearance and metadata settings, the TMDB key, hand-picked
matches and per-title track choices. Newer updated_at wins per key, a
removal travels as an empty register, and a first sync stamps at 0 so a
fresh install takes the library the other devices have instead of
overwriting it with defaults. Volume, mute and the device-only settings
(cache budget, Wi-Fi only, background audio, hardware decoding) stay out.
The secrets live only in the end-to-end encrypted Drive file, never in
shares.json. FORMAT_VERSION stays 1; an older build ignores the registers.

Adding a share stored it before opening the link, so a first failure left a
broken entry and every retry said "already configured". add_verified opens
the link first and stores nothing on failure, and only the new share is
crawled afterwards, so another broken share cannot fail the add.

Android drops picture-in-picture: the window sized the video wrongly and
background audio already covers leaving the app.

iOS sizes the Metal drawable on every layout, so the picture no longer
stretches after a rotation or a Split View resize. Settings show a save at
once rather than after the match run, and a failed save reverts and says
why. Both phone clients gain an activity banner naming what is running, and
the add-share, metadata and change-match forms show progress and errors in
place.
2026-10-09 03:15:39 +02:00

5.7 KiB

Android Development

The Android client lives in android/; its Rust boundary is crates/pstr-android. It targets Android 12 (API 31) and newer on arm64-v8a phones/tablets and x86_64 emulators. Compose owns navigation and screens, while UniFFI exposes the existing catalog, share, streaming, watch-state, and offline logic. Keep reusable behavior in the Rust crates rather than reimplementing it in Kotlin.

Toolchain

Install JDK 17, Android SDK Platform/Build Tools 36, NDK 30.0.16248370, Rust 1.96, Gradle 8.13, and the native helpers:

rustup target add aarch64-linux-android x86_64-linux-android
cargo install cargo-ndk --version 4.1.2 --locked

Set ANDROID_HOME (or ANDROID_SDK_ROOT); cargo-ndk discovers the NDK below that SDK. The workspace-provided uniffi-bindgen binary keeps binding generation on the same UniFFI version as the bridge. Use bash scripts/build-android.sh debug for an installable debug APK, bash scripts/build-android.sh check for Android lint/unit tests, and bash scripts/build-android.sh release for release APK/AAB output. Gradle first generates UniFFI Kotlin into android/app/build/generated/source/uniffi, then builds both Rust ABIs into android/app/build/generated/jniLibs. Generated files belong under build/ and must not be committed.

versionName always comes from [workspace.package].version in Cargo.toml, so a tagged build cannot drift from the desktop release. The default versionCode is major * 1,000,000 + minor * 1,000 + patch (0.1.1 becomes 1001). Set ANDROID_VERSION_CODE only when Play requires a higher monotonic code; Gradle rejects non-integers, zero, and values above Android's limit. The Android workflow tests both the default derivation and the override path.

Local Run and Verification

Start an API 31+ emulator or attach a device, then run:

bash scripts/build-android.sh debug
adb install -r android/app/build/outputs/apk/debug/app-debug.apk

Before submitting Android work, run the Rust workspace gate from the repository root, then:

bash scripts/build-android.sh check          # ktlint, Android lint, host and screenshot tests
bash scripts/android-acceptance.sh --release # the on-device matrix, minified APK

check needs no device. Run the matrix with --release where you can: it installs the signed minified APK and runs instrumentation against it, which is the only path that exercises R8 — the step that has broken this app five distinct ways that a debug build cannot reproduce. Without the ANDROID_RELEASE_* variables below, drop --release and the debug APK is used instead. The matrix — phone and tablet layouts, rotation, process recreation, playback and background controls, download cancellation/resume, and an offline launch with networking disabled — is scripts/android-acceptance.sh; --list prints every case and what it needs, and PSTR_ACCEPTANCE_ONLY=name runs one. Five cases need a real share, given as PSTR_ACCEPTANCE_SHARE_URL and PSTR_ACCEPTANCE_SHARE_PASSWORD; they reach the device as instrumentation arguments and are stored nowhere. Without them those cases report skip. There is no Picture-in-Picture: it was removed after a PiP window that never sized its video right proved less use than background audio and the lock-screen transport. docs/TESTING.md has the layer-by-layer detail, including why lint runs against a baseline.

Requires JDK 17. build-android.sh locates it, because a newer JVM does not fail with a version message — the Kotlin compiler throws IllegalArgumentException: 26.0.2 from an IntelliJ version parser.

Release Signing

Release signing is optional locally and never stored in the repository. Set all four variables before bash scripts/build-android.sh signed:

ANDROID_RELEASE_STORE_FILE
ANDROID_RELEASE_STORE_PASSWORD
ANDROID_RELEASE_KEY_ALIAS
ANDROID_RELEASE_KEY_PASSWORD

GitHub Actions expects the keystore itself as the base64-encoded ANDROID_RELEASE_KEYSTORE_BASE64 secret and the other three values as secrets. Keep the keystore and passwords in a durable external secret manager; losing the key prevents users from upgrading an installed APK.

A v* tag runs release.yml, which calls android.yml with signed_release: true and publishes the signed APK as proton-stream-<version>-android.apk next to the desktop artifacts, together with the mpv-android-source-<rev>.tar.gz corresponding source for the bundled libmpv. The AAB is kept as the android-bundle workflow artifact only; nothing uploads it to Play. A manual android.yml run with signed_release builds the same signed files without publishing them.

Licensing and Native Playback

The Android application is GPL-3.0-or-later; the shared Rust crates remain MIT. See android/LICENSE.md and android/THIRD_PARTY_NOTICES.md. Before distributing a binary with bundled libmpv, pin its exact source revision/build configuration, package the notices, and retain the corresponding source plus build scripts required by the GPL. The current playback service scaffold is not a substitute for the final bundled libmpv integration.

bash scripts/build-libmpv-android.sh pins and stages libmpv for both supported ABIs, plus a source archive and revision record, under android/app/build/generated/mpv. Gradle runs that task before native builds, packages the staged libraries, and compiles pstr_mpv, the JNI/EGL adapter. It feeds libmpv through the Rust stream C ABI; decrypted bytes remain in native memory. PlaybackService owns the mpv core for background audio and media controls, while the activity supplies the current Surface. Do not publish an Android binary until this exact pipeline is verified on real arm64 hardware and an x86_64 emulator.