# OnionWire for Android Two artifacts out of one Gradle build: | Module | Output | What it is | |---|---|---| | `:sdk` | `sdk-release.aar` | The reusable library. UniFFI Kotlin bindings + `libonionwire_sdk.so`. Any Android app can depend on this. | | `:app` | `app-release.apk` / `app-debug.apk` | A Jetpack Compose + Material 3 messenger that depends on `:sdk` and nothing else. | The Linux TUI in the repository root is a **separate product** with a separate Cargo workspace. Nothing here turns it into a WebView, and nothing here imports its TUI code. ## Requirements * JDK 17+ (built and tested on JDK 21) * Android SDK with **platform 36** and **build-tools 36.0.0** * NDK **28.2.13676358** (NDK r28c) — also what Rust's Android link step uses * Rust with the `aarch64-linux-android` target and `cargo-ndk` * `ANDROID_HOME` set, or `sdk.dir` in `android/local.properties` (gitignored) One-time setup: ```bash sdkmanager --install "platform-tools" "platforms;android-36" \ "build-tools;36.0.0" "ndk;28.2.13676358" rustup target add aarch64-linux-android x86_64-linux-android cargo install cargo-ndk ``` ## Build ```bash cd android ./gradlew :sdk:assembleRelease :app:assembleDebug # or :app:assembleRelease ``` The Rust cross-compile and the UniFFI Kotlin bindings are wired into the Gradle tasks — there is no manual codegen step. `:sdk:preBuild` depends on them. Emulator / x86_64 slice: ```bash ./gradlew :app:assembleDebug -Ponionwire.abis=arm64-v8a,x86_64 ``` Or from the repo root, with checksums and an optional publish step: ```bash scripts/build-android-local.sh # -> dist/onionwire--android-arm64-v8a.apk PUBLISH_TAG=v0.3.0 scripts/build-android-local.sh ``` ## Decisions you should know about ### `minSdk = 26` (Android 8.0) The floor is not NDK-imposed — Rust's `aarch64-linux-android` std and NDK 28 both happily target API 21. We take 26 deliberately: * it is the first API level where adaptive icons and notification channels are unconditional, so there is one icon path instead of two; * it keeps a single `arm64-v8a` APK inside Play's currently-supported range without carrying legacy ART/JIT workarounds for pre-O devices; * the crypto stack (`ring`, `rustls`, `argon2`, `chacha20poly1305`) is exercised on 4.4+ kernels here, which is what we actually test. **Devices cut:** Android 7.1 and older. If you need those, the change is `-Ponionwire.api=24` **and** a real device test — do not bump it silently. ### 16 KB page size The NDK r28 link step emits `max-page-size=16384` for 64-bit Android, which is what Play requires for new apps. Verify with: ```bash readelf -l android/sdk/build/rustJniLibs/arm64-v8a/libonionwire_sdk.so | grep LOAD ``` ### Permissions `INTERNET` only, declared at first launch. No camera (invites are pasted, not scanned), no contacts, no storage permission — the data directory is `context.filesDir`, which is already app-private. There is **no** `FOREGROUND_SERVICE` and no keep-alive notification in v1: the node lives only while the process does, and the UI says so. Adding one is a product decision (battery, Play's FGS type declarations), not a silent fix. ### Data at rest `context.filesDir/onionwire/` (mode 0700), holding `onionwire.db` and the Arti state dir. `android:allowBackup="false"` plus explicit `data_extraction_rules` exclusions, because an auto-backup of `onionwire.db` would hand the wrapped message key *and* the plaintext identity keys to Google Drive. Chat bodies are encrypted at rest; identity keys are not — that is the locked v1 posture. ### Signing Both `debug` and `release` are signed with the **standard debug keystore** (`~/.android/debug.keystore`, auto-created by AGP). This is what makes the release APK installable for sideloading. It is **not** a Play upload key; nothing here should be uploaded to Play. When a real upload key exists, put it in `~/.gradle/gradle.properties` or CI secrets and reference it from `signingConfigs` — never in this repository. ### Release builds are not minified `isMinifyEnabled = false` for release. Keeps for the UniFFI/JNI surface already exist in `sdk/consumer-rules.pro`, but enabling R8 without a mapping-file upload path and an on-device test of the JNI surface is how you ship an `UnsatisfiedLinkError` nobody can read. Turn it on together with crash deobfuscation, not before. ## CI `.forgejo/workflows/release.yml` runs on an aarch64 Linux container on the Pi. It cannot build an APK. Android artifacts are built on a host with the SDK/NDK via `scripts/build-android-local.sh`, exactly like the x86_64 Linux binary is built via `scripts/build-release-local.sh`. ## Layout ``` android/ settings.gradle.kts build.gradle.kts # plugin aliases only gradle/libs.versions.toml # version catalog sdk/ # -> AAR build.gradle.kts # cargo-ndk + uniffi-bindgen wiring consumer-rules.pro src/main/AndroidManifest.xml app/ # -> APK build.gradle.kts src/main/java/com/siriusdevops/onionwire/ MainActivity.kt # NavHost WireViewModel.kt # owns the single Wire node ui/ # Compose screens (M3) ```