feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split #2

Merged
sirius merged 1 commit from wt/t_b7176a1b into main 2026-09-10 23:16:45 +00:00
Owner

Summary

Android SDK + messenger in the same repo, alongside the Linux TUI. Not a WebView, not a rewrite: src/tui.rs is untouched and the crate still builds, tests and clips clean on its default features.

SDKcrates/onionwire-sdk, a UniFFI façade over the same onionwire crate the TUI runs on. Identity, invite encode/decode, friend upsert-by-pubkey, list, send/receive, rotate and wipe all delegate to the existing code — no second protocol — so an Android peer and a Linux peer that exchange invites interoperate. Exposed as a Forgejo release asset (onionwire-sdk-0.1.0.aar + .sha256).

APKandroid/, Kotlin + Jetpack Compose + Material 3. :sdk builds the AAR (UniFFI bindings generated at build time + libonionwire_sdk.so via cargo-ndk); :app is a Compose messenger that depends on :sdk only.

Arti on Android: the one real decision

Arti's TLS backends are non-additive — exactly one of native-tls and rustls may be enabled per feature resolution. The TUI keeps native-tls (OpenSSL, unchanged). Android has no OpenSSL in the NDK, so the SDK crate selects rustls + static-sqlite.

Rather than unify the two, crates/onionwire-sdk is a standalone Cargo workspace that path-depends on the root package without joining its feature graph. The root Cargo.toml only grows two pass-through features:

[features]
default = ["native-tls"]
native-tls = ["arti-client/native-tls"]
rustls = ["arti-client/rustls"]

cargo build for the Linux crate therefore selects the exact same backend it always did. Flagging it because it is a manifest change to the TUI crate even though behaviour is identical.

Spike result

arti-client 0.46 with tokio, onion-service-client, onion-service-service, compression, rustls, static-sqlite cross-compiles for aarch64-linux-android with NDK r28c. No go/no-go blocker; this is a real build, not a clearnet fake.

Layout

/                       existing Linux crate — behaviour intact
crates/onionwire-sdk/   UniFFI wrapper (own workspace)  -> AAR
android/
  settings.gradle.kts
  sdk/                  Android library -> AAR
  app/                  Compose APK, depends on :sdk
scripts/build-android-local.sh

Verification (real output)

$ cargo test --locked
test result: ok. 1 passed  (lib)
  + backup 9, dispatch 6, hs 8, pay 10, profile 11, protocol 3, qr 6,
    ratelimit 2, rotate 7, store 14, tui_chrome 10, wallet 5, wipe 8
  tests/rotate_hs.rs, tests/tor_hs.rs, tests/two_node.rs — 3 ignored (live Tor)
0 failed

$ cargo clippy --locked --all-targets -- -D warnings
Finished — no warnings

$ ./target/release/onionwire --version
onionwire 0.2.0

$ scripts/build-android-local.sh
onionwire-0.1.0-android-arm64-v8a.apk: OK
onionwire-sdk-0.1.0.aar: OK

dist/onionwire-0.1.0-android-arm64-v8a.apk   33630776 bytes
dist/onionwire-sdk-0.1.0.aar                  6909527 bytes

$ aapt2 dump badging dist/onionwire-0.1.0-android-arm64-v8a.apk
package: name='com.siriusdevops.onionwire' versionCode='1' versionName='0.1.0'
minSdkVersion:'26'  targetSdkVersion:'36'
uses-permission: name='android.permission.INTERNET'   # and nothing else

$ zipalign -c -P 16 -v 4 ...apk
Verification successful                      # 16 KB page size

$ readelf -lW libonionwire_sdk.so | grep LOAD
  LOAD ... R E 0x4000                        # p_align = 16384

$ readelf -dW libonionwire_sdk.so | grep NEEDED
  libdl.so  libm.so  libc.so                 # no OpenSSL

$ apksigner verify --print-certs -v ...apk
Verifies / APK Signature Scheme v2: true / CN=Android Debug

lib/arm64-v8a/ carries libonionwire_sdk.so, libjnidispatch.so (JNA) and libandroidx.graphics.path.so.

Product decisions

  • minSdk 26 (Android 8.0). The NDK/rustc floor is 21; 26 is deliberate so there is one adaptive-icon path, one ART behaviour, and a single arm64-v8a APK inside Play's supported range. Cuts Android 7.1 and older. Documented in android/README.md with the -Ponionwire.api= escape hatch.
  • INTERNET only. No camera (invites are pasted), no contacts, no storage permission. No FOREGROUND_SERVICE/notification in v1 — the node lives while the app does, and the UI says so.
  • No backend rotation / no onion-scanning: invites are typed or pasted, matching the TUI's F2/F3 semantics.
  • Data: context.filesDir/onionwire, allowBackup=false + explicit data_extraction_rules exclusions — an auto-backup of onionwire.db would ship the wrapped message key and the plaintext identity keys off-device.
  • Release is debug-signed and not minified. Debug keystore only, no invented Play upload key. sdk/consumer-rules.pro carries the JNI keeps for when R8 is switched on together with a mapping-file upload path.
  • Rotate is a typed ROTATE confirmation in the app, never a single tap, and the result screen reports notified N/M.

Not done here

  • No tag created, no release published, v0.2.0/v0.1.1 untouched. scripts/build-android-local.sh accepts PUBLISH_TAG=<tag> when you want the assets on a new release.
  • Not runtime-tested on a physical device or emulator (no emulator/system image on this host). Verification is structural: badging, packaged .so set, exported UniFFI symbols, signature, page alignment.
  • No Monero/wallet surface in the AAR.

Reviewer notes

The Android build needs a host with SDK/NDK — Pi CI is aarch64 Linux and cannot produce an APK, which is why this is scripts/build-android-local.sh and not a workflow change.

## Summary Android SDK + messenger in the same repo, alongside the Linux TUI. Not a WebView, not a rewrite: `src/tui.rs` is untouched and the crate still builds, tests and clips clean on its default features. **SDK** — `crates/onionwire-sdk`, a UniFFI façade over the *same* `onionwire` crate the TUI runs on. Identity, invite encode/decode, friend upsert-by-pubkey, list, send/receive, rotate and wipe all delegate to the existing code — no second protocol — so an Android peer and a Linux peer that exchange invites interoperate. Exposed as a Forgejo release asset (`onionwire-sdk-0.1.0.aar` + `.sha256`). **APK** — `android/`, Kotlin + Jetpack Compose + Material 3. `:sdk` builds the AAR (UniFFI bindings generated at build time + `libonionwire_sdk.so` via cargo-ndk); `:app` is a Compose messenger that depends on `:sdk` only. ### Arti on Android: the one real decision Arti's TLS backends are **non-additive** — exactly one of `native-tls` and `rustls` may be enabled per feature resolution. The TUI keeps `native-tls` (OpenSSL, unchanged). Android has no OpenSSL in the NDK, so the SDK crate selects `rustls` + `static-sqlite`. Rather than unify the two, `crates/onionwire-sdk` is a **standalone Cargo workspace** that path-depends on the root package without joining its feature graph. The root `Cargo.toml` only grows two pass-through features: ```toml [features] default = ["native-tls"] native-tls = ["arti-client/native-tls"] rustls = ["arti-client/rustls"] ``` `cargo build` for the Linux crate therefore selects the exact same backend it always did. Flagging it because it *is* a manifest change to the TUI crate even though behaviour is identical. ### Spike result `arti-client` 0.46 with `tokio, onion-service-client, onion-service-service, compression, rustls, static-sqlite` cross-compiles for `aarch64-linux-android` with NDK r28c. No go/no-go blocker; this is a real build, not a clearnet fake. ## Layout ``` / existing Linux crate — behaviour intact crates/onionwire-sdk/ UniFFI wrapper (own workspace) -> AAR android/ settings.gradle.kts sdk/ Android library -> AAR app/ Compose APK, depends on :sdk scripts/build-android-local.sh ``` ## Verification (real output) ``` $ cargo test --locked test result: ok. 1 passed (lib) + backup 9, dispatch 6, hs 8, pay 10, profile 11, protocol 3, qr 6, ratelimit 2, rotate 7, store 14, tui_chrome 10, wallet 5, wipe 8 tests/rotate_hs.rs, tests/tor_hs.rs, tests/two_node.rs — 3 ignored (live Tor) 0 failed $ cargo clippy --locked --all-targets -- -D warnings Finished — no warnings $ ./target/release/onionwire --version onionwire 0.2.0 $ scripts/build-android-local.sh onionwire-0.1.0-android-arm64-v8a.apk: OK onionwire-sdk-0.1.0.aar: OK dist/onionwire-0.1.0-android-arm64-v8a.apk 33630776 bytes dist/onionwire-sdk-0.1.0.aar 6909527 bytes $ aapt2 dump badging dist/onionwire-0.1.0-android-arm64-v8a.apk package: name='com.siriusdevops.onionwire' versionCode='1' versionName='0.1.0' minSdkVersion:'26' targetSdkVersion:'36' uses-permission: name='android.permission.INTERNET' # and nothing else $ zipalign -c -P 16 -v 4 ...apk Verification successful # 16 KB page size $ readelf -lW libonionwire_sdk.so | grep LOAD LOAD ... R E 0x4000 # p_align = 16384 $ readelf -dW libonionwire_sdk.so | grep NEEDED libdl.so libm.so libc.so # no OpenSSL $ apksigner verify --print-certs -v ...apk Verifies / APK Signature Scheme v2: true / CN=Android Debug ``` `lib/arm64-v8a/` carries `libonionwire_sdk.so`, `libjnidispatch.so` (JNA) and `libandroidx.graphics.path.so`. ## Product decisions - **minSdk 26** (Android 8.0). The NDK/rustc floor is 21; 26 is deliberate so there is one adaptive-icon path, one ART behaviour, and a single `arm64-v8a` APK inside Play's supported range. Cuts Android 7.1 and older. Documented in `android/README.md` with the `-Ponionwire.api=` escape hatch. - **INTERNET only.** No camera (invites are pasted), no contacts, no storage permission. **No** `FOREGROUND_SERVICE`/notification in v1 — the node lives while the app does, and the UI says so. - **No backend rotation / no onion-scanning**: invites are typed or pasted, matching the TUI's F2/F3 semantics. - **Data**: `context.filesDir/onionwire`, `allowBackup=false` + explicit `data_extraction_rules` exclusions — an auto-backup of `onionwire.db` would ship the wrapped message key *and* the plaintext identity keys off-device. - **Release is debug-signed and not minified.** Debug keystore only, no invented Play upload key. `sdk/consumer-rules.pro` carries the JNI keeps for when R8 is switched on together with a mapping-file upload path. - **Rotate** is a typed `ROTATE` confirmation in the app, never a single tap, and the result screen reports `notified N/M`. ## Not done here - No tag created, no release published, `v0.2.0`/`v0.1.1` untouched. `scripts/build-android-local.sh` accepts `PUBLISH_TAG=<tag>` when you want the assets on a new release. - Not runtime-tested on a physical device or emulator (no emulator/system image on this host). Verification is structural: badging, packaged `.so` set, exported UniFFI symbols, signature, page alignment. - No Monero/wallet surface in the AAR. ## Reviewer notes The Android build needs a host with SDK/NDK — Pi CI is aarch64 Linux and cannot produce an APK, which is why this is `scripts/build-android-local.sh` and not a workflow change.
sirius added 1 commit 2026-09-10 22:18:43 +00:00
feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split
All checks were successful
ci / test (pull_request) Successful in 3m22s
672f2c9990
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.
sirius merged commit 95b0fa4dee into main 2026-09-10 23:16:45 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: sirius/onionwire#2
No description provided.