171 lines
10 KiB
Markdown
171 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–#94, including private storage, Keystore
|
||
callbacks and build gates. Both variants invoke Rust through the generated
|
||
UniFFI API. Onboarding and the feature screens remain owned by their milestone
|
||
issues. The full [CI and emulator workflow](../docs/android/ci.md) documents the
|
||
ARM-only runner/toolchain policy and artifact audits.
|
||
|
||
## 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 two ARM ABIs are packaged. Native runtime packaging is described
|
||
below; no development network endpoint is included.
|
||
|
||
The manifest exports the launcher and the reviewed AndroidX profile receiver
|
||
protected by the privileged DUMP permission. 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
|
||
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 both ARM 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.
|
||
|
||
#93 installs Rust-to-Kotlin Keystore callbacks through generated UniFFI/JNA
|
||
adapters: 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 only. The release set is arm64-v8a and armeabi-v7a under the user amendment to
|
||
#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 both ARM 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. The user removed x86_64 Android from the current support scope on 2026-10-08.
|
||
|
||
Historical validation recorded on 2026-10-08 for #92 (before the ARM-only scope update):
|
||
|
||
| 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 historical x86_64 runtime gap is no longer a required check under the current
|
||
ARM-only support decision.
|