170 lines
10 KiB
Markdown
170 lines
10 KiB
Markdown
# Native Android build
|
|
|
|
Architecture and issue ownership: [ADR](../docs/android/architecture.md) and
|
|
[parity matrix](../docs/android/parity.md). The native foundation implements #89 and #92. Onboarding and secure storage
|
|
are implemented by their milestone issues. Both variants now invoke Rust through
|
|
the generated UniFFI API.
|
|
|
|
## Reproducible setup
|
|
|
|
Use Rust 1.97.1 (pinned in rust-toolchain.toml), cargo-ndk 4.1.2, a JDK 25 installation
|
|
(Android Studio's JetBrains Runtime 25 is suitable), Android SDK Platform 37,
|
|
build-tools 36.0.0 and NDK 30.0.16248370. The app emits JVM 17-compatible code
|
|
and supports minSdk 26. Set JAVA_HOME and ANDROID_HOME/ANDROID_SDK_ROOT or create
|
|
an ignored local.properties containing sdk.dir. Cargo must be in the IDE/build
|
|
process PATH, or set CARGO to its absolute executable path. No machine paths are
|
|
committed.
|
|
|
|
The committed Gradle 9.8.1 wrapper verifies the distribution SHA-256. Its JAR
|
|
SHA-256 is 3b8a25775a69158b5ad2b1d17a80a88dc7b40a352a73eb27d06c612a7ce68e98,
|
|
verified against the official Gradle version metadata. AGP 9.4.1 includes Kotlin
|
|
support; the Kotlin JVM/Compose plugin pin upgrades its classpath consistently
|
|
to 2.4.21. Do not additionally apply kotlin-android with built-in Kotlin enabled.
|
|
The version catalog is the single source for Android dependency/SDK/NDK pins.
|
|
|
|
From this directory:
|
|
|
|
```sh
|
|
./gradlew auditNativePackages :core:testDebugUnitTest :app:testDebugUnitTest :app:lintDebug :app:lintRelease ktlintCheck
|
|
./gradlew :app:connectedDebugAndroidTest :rust-bridge:connectedDebugAndroidTest
|
|
```
|
|
|
|
Repeat the first command to verify configuration-cache reuse. Dependency
|
|
verification is strict by default; committed SHA-256 metadata covers resolved
|
|
build/test artifacts. Locks pin resolvable module configurations. To update an
|
|
approved version, edit the catalog, verify the upstream artifact/release/license,
|
|
then explicitly regenerate checksums/locks, inspect the diff and run the gates:
|
|
|
|
```sh
|
|
./gradlew :app:assembleDebug :app:assembleRelease :app:testDebugUnitTest :app:lintDebug ktlintCheck --write-verification-metadata sha256 --write-locks
|
|
```
|
|
|
|
## Boundaries and fixtures
|
|
|
|
The app composition root injects an Android-facing ShellRepository. Compose
|
|
features consume that contract through a Hilt ViewModel and collect immutable
|
|
StateFlow state with lifecycle awareness. Only core depends on rust-bridge;
|
|
platform implements core OS contracts. Gradle rejects forbidden module edges
|
|
and competing domain dependencies. The Rust android-build tool checks Kotlin
|
|
source/import boundaries and subprocess paths before every Android build.
|
|
These static checks accompany code review; they do not prove arbitrary Kotlin
|
|
code cannot conceal a domain implementation.
|
|
|
|
Debug controls invoke the storage-owned MobileShell::fixture through the native
|
|
API for Loading/Empty/Ready/Locked/Error. A build-generated fixture inventory is
|
|
used only as the test oracle. Release calls the same Rust Empty fixture without
|
|
debug controls; repository initialization is owned by #90. Feature code consumes
|
|
only the facade, including typed native recovery states.
|
|
|
|
## Identity, signing and update
|
|
|
|
All app build types keep de.rfc1437.ironstorage; test IDs are derived by AGP.
|
|
Version code 1 and name 1.0.1 are deterministic initial metadata. Debug signing
|
|
uses the standard local debug key. Release has no configured signing key and
|
|
produces an unsigned APK; #112 configures protected upload signing. Do not add
|
|
signing secrets or store passwords to the repository. Native symbol level FULL
|
|
and the selected three ABIs are packaged. Native runtime packaging is described
|
|
below; no development network endpoint is included.
|
|
|
|
The manifest has only the exported launcher activity. Backup/cloud/device-transfer
|
|
exclusions and cleartext denial are explicit. Feature-specific services/permissions
|
|
are added with their owning issues. Resources use values directories for future
|
|
localization; vector/adaptive/monochrome icons and R8 consumer rules are present.
|
|
The launcher uses an unchanged copy of the iPhone vault artwork from
|
|
apple/Assets.xcassets/AppIcon.appiconset/AppIcon.png, inset for Android's adaptive
|
|
mask. Its themed monochrome layer keeps the round vault, wheel and hinge motif.
|
|
Keep the two platforms' launcher branding aligned when updating that artwork.
|
|
|
|
Install updates over the existing simulator app without uninstall or data reset:
|
|
|
|
```sh
|
|
adb -s DEVICE install -r app/build/outputs/apk/debug/app-debug.apk
|
|
```
|
|
|
|
Keep the same AVD and application ID. Never wipe its data/keychain or seed a test
|
|
app-data package as an update. After closing an issue, clean its project build
|
|
outputs and Cargo target; preserve the emulator and shared SDK/toolchain caches.
|
|
AGP's leaveApksInstalledAfterRun option is enabled so instrumented checks do not
|
|
uninstall the app after running. Keep it enabled for this persistent test device.
|
|
|
|
## Rust packaging and integrity
|
|
|
|
Install the pinned build tools once:
|
|
|
|
```sh
|
|
rustup toolchain install 1.97.1 --profile minimal
|
|
rustup target add --toolchain 1.97.1 aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
|
|
cargo install cargo-ndk --version 4.1.2 --locked
|
|
```
|
|
|
|
Gradle checks the actual Cargo/cargo-ndk/NDK revisions before building. Each
|
|
variant compiles crates/android with cargo-ndk into its own generated jniLibs,
|
|
then verifies ELF architecture, 16 KB LOAD alignment, and byte-identical UniFFI
|
|
metadata across all three ABIs. A packaging step selects only the Android library
|
|
and rejects accidental inclusion of the dependency Apple cdylib. The thin Android crate re-exports the existing
|
|
UniFFI component; it duplicates no storage rules or converters. #91 owns the
|
|
platform-neutral contract extraction. No handwritten JNI or unsafe Rust is added.
|
|
|
|
The workspace's UniFFI 0.32.0 CLI generates Android-mode immutable Kotlin
|
|
records from the arm64 library, using the global defaults in uniffi.toml.
|
|
A version-checked preparation step redacts record toString, typed/internal
|
|
exception diagnostics and unexpected callback stack traces, mechanically renames
|
|
Rust error message fields to rustMessage in Kotlin,
|
|
and translates every integrity guard to a typed failure. Checksums remain enabled
|
|
and retain every native function and expected value. UniFFI checksums are u16:
|
|
JNA's generated Int return is normalized to its lower 16 bits before comparison;
|
|
AArch64 may leave unrelated upper bits in the return register. Contract versions
|
|
are compared at their full width. A failed initializer remains failed until an
|
|
app restart/update, rather than reporting a different error on each retry.
|
|
|
|
NativeRuntime performs OS library/ABI preflight, loads the library and initializes
|
|
UniFFI before entering Rust. Missing/unsupported libraries, broken APKs, contract
|
|
or checksum mismatch, and load failures yield typed, non-secret recovery states.
|
|
It rejects main-thread calls. The core facade schedules blocking native calls on
|
|
Dispatchers.IO; cancellation propagates and a superseded result cannot overwrite
|
|
newer state. Blocking fixture calls finish in Rust when a coroutine is cancelled;
|
|
no thread is forcibly stopped. Long storage operations must use their Rust-owned
|
|
cancellation methods when their feature is integrated.
|
|
|
|
No Rust-to-Kotlin platform callbacks are installed by #92. When #93 adds them,
|
|
use generated UniFFI/JNA callback adapters only: JNA attaches a calling native
|
|
thread to the JVM; retain the callback for the full Rust owner lifetime, release
|
|
it after unregistering, never assume the callback runs on the Android main thread,
|
|
and marshal UI/Activity work onto Main. Keep storage decisions in Rust and avoid
|
|
blocking the main thread or calling back into a held Rust lock.
|
|
|
|
JNA 5.19.1 is packaged as its Android AAR, filtered to arm64-v8a, armeabi-v7a and
|
|
x86_64. The release set may only be narrowed by changing the support decision in
|
|
#88, not by silently omitting an ABI. All source/lock/config files are task inputs;
|
|
outputs live only under build/. Debug and release compile independently, and
|
|
unchanged builds reuse their outputs. R8 keeps generated/JNA reflection entry
|
|
points. AGP strips release libraries and exports FULL native symbols to
|
|
app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip; retain
|
|
that file and outputs/mapping/release/mapping.txt with the matching release.
|
|
AAB base/lib entries let Play produce per-ABI splits.
|
|
|
|
The auditNativePackages gate inspects every native dependency in both APKs and
|
|
the AAB, checks all three ABI directories and both Rust/JNA libraries, and rejects
|
|
ELF segments below 16 KB alignment or APK native entries that are compressed or
|
|
not ZIP-aligned to 16 KB. Follow the official [16 KB runtime validation](https://developer.android.com/guide/practices/page-sizes):
|
|
use an actual 16 KB system image, check adb shell getconf PAGE_SIZE is 16384,
|
|
disable page-size compatibility mode, and run both instrumented test suites.
|
|
An ELF/ZIP audit is not a substitute for that runtime test. Milestone validation
|
|
is emulator-only per the user instruction of 2026-10-08; physical devices are
|
|
outside the test scope for every issue. Keep x86_64 emulator evidence separate
|
|
from the Apple Silicon arm64 emulator run.
|
|
|
|
Validation recorded on 2026-10-08 for #92:
|
|
|
|
| Environment | Verified |
|
|
| --- | --- |
|
|
| API 37 arm64 emulator, 4 KB | Generated API fixture/error/redaction tests; 25 UI states; release APK and AAB split launch; update-state preservation |
|
|
| API 37 arm64 emulator, 16 KB, compatibility disabled | Same native/UI tests, release APK/AAB launch and update-state preservation |
|
|
| Fresh source export | Three ABI debug builds with pinned tools, locked dependencies and strict artifact verification |
|
|
| All three ABI release outputs | ELF alignment and identical UniFFI metadata; stripped APK libraries and separate FULL Rust symbols; nine bundletool ABI split APKs audited |
|
|
| Physical devices | Excluded from milestone testing by user instruction |
|
|
| x86_64 runtime | System image installed; QEMU2 rejects x86_64 on this aarch64 Mac, including with acceleration disabled |
|
|
|
|
The outstanding x86_64 emulator check needs a compatible host; cross-compilation
|
|
and static package checks do not establish x86_64 runtime coverage.
|