|
All checks were successful
ci / test (pull_request) Successful in 3m22s
Adds the Android product alongside the existing Linux TUI, in one repo. SDK — crates/onionwire-sdk is a UniFFI facade over the very same `onionwire` crate the TUI runs on. Identity, invite, friend upsert, send/receive, rotate and wipe all delegate; no protocol is reimplemented, so an Android peer and a Linux peer interoperate. It is its own Cargo workspace because Arti's TLS backends are non-additive: the TUI keeps native-tls (OpenSSL), Android needs rustls + static-sqlite (no OpenSSL, no system libsqlite3 in the NDK). android/ — Gradle project. :sdk produces the AAR (Kotlin bindings generated at build time + libonionwire_sdk.so via cargo-ndk), :app is a Kotlin/Compose/M3 messenger depending on :sdk only. minSdk 26, targetSdk/compileSdk 36, INTERNET-only, data in filesDir, backups excluded. Root Cargo.toml grows `native-tls` (default) and `rustls` features so exactly one Arti TLS backend is selected per build graph. The default build is unchanged: same backend, ratatui still a normal dependency, src/tui.rs untouched. Also: Store::self_fingerprint/set_petname + Node wrappers (additive only), scripts/build-android-local.sh, README sections, .gitignore for local SDK paths. |
||
|---|---|---|
| .. | ||
| app | ||
| gradle | ||
| sdk | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle.kts | ||
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-androidtarget andcargo-ndk ANDROID_HOMEset, orsdk.dirinandroid/local.properties(gitignored)
One-time setup:
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
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:
./gradlew :app:assembleDebug -Ponionwire.abis=arm64-v8a,x86_64
Or from the repo root, with checksums and an optional publish step:
scripts/build-android-local.sh # -> dist/onionwire-<v>-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-v8aAPK 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:
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)