Package the Rust mobile core and generated Kotlin API for Android (#92)
Dependency security audit / rustsec (push) Failing after 24s

This commit is contained in:
hugo committed 2026-10-08 12:34:09 +02:00
1 parent e079b82b15
commit fcb36a0684
42 files changed
+1586 -172

No files matched your search

Generated
+12
View File
@@ -5433,12 +5433,22 @@ dependencies = [
"zeroize",
]
[[package]]
name = "ironstorage-android"
version = "1.0.1"
dependencies = [
"ironstorage-apple",
]
[[package]]
name = "ironstorage-android-build"
version = "1.0.1"
dependencies = [
"ironstorage",
"object",
"regex",
"uniffi",
"zip",
]
[[package]]
@@ -10550,7 +10560,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a782a48d72cfd7a2d65cfc7c691dbf5375c43104b3c195f7eccc716dcc3540c8"
dependencies = [
"anyhow",
"camino",
"cargo_metadata",
"clap",
"uniffi_bindgen",
"uniffi_core",
"uniffi_macros",
+1
View File
@@ -4,6 +4,7 @@ members = [
"apps/desktop",
"apps/tui",
"crates/apple",
"crates/android",
"crates/storage",
"crates/watch-apple",
"tools/android-build",
+14 -2
View File
@@ -31,7 +31,7 @@ The current direct dependencies are:
| [regex 1.13](https://crates.io/crates/regex/1.13.1) | Linear-time byte-oriented decrypted grep matching | MIT OR Apache-2.0 |
| [tempfile 3](https://crates.io/crates/tempfile) | Permission-restricted CLI editor session directories and cleanup | MIT OR Apache-2.0 |
| [Serde 1](https://crates.io/crates/serde), [TOML 0.9](https://crates.io/crates/toml), [shlex 1.3](https://crates.io/crates/shlex), [url 2.5](https://crates.io/crates/url) | Strict configuration and command values | MIT OR Apache-2.0 |
| [UniFFI 0.32](https://crates.io/crates/uniffi/0.32.0) | Swift bridge | MPL-2.0 |
| [UniFFI 0.32](https://crates.io/crates/uniffi/0.32.0) | Swift and Kotlin bridge | MPL-2.0 |
| [zeroize 1.9](https://crates.io/crates/zeroize/1.9.0) | Clear decrypted bytes on drop | MIT OR Apache-2.0 |
The activated transitive graph has no dependency that forces a GPL or LGPL
@@ -115,4 +115,16 @@ version catalog pins stable AndroidX, Kotlin, Hilt/KSP and formatting tools.
Espresso 3.7.0 is pinned explicitly because Compose's older transitive Espresso
cannot initialize input injection on API 37. AndroidX Test is Apache-2.0;
JUnit 4.13.2 is EPL-1.0 and test-only. ktlint and its Gradle plugin are MIT,
build-only tools. Native runtime dependency/ABI review belongs to #92.
build-only tools.
#92 packages [JNA 5.19.1](https://repo.maven.apache.org/maven2/net/java/dev/jna/jna/5.19.1/jna-5.19.1.pom)
as the Android AAR under its Apache-2.0 option (dual-licensed with LGPL-2.1-or-later).
It is the OS FFI runtime required by generated UniFFI Kotlin, not a competing
storage implementation. Its Android native libraries are subject to the same
three-ABI, ELF and 16 KB APK alignment checks as Rust. Exact resolved AAR/POM
checksums and locks are committed. cargo-ndk 4.1.2 (MIT OR Apache-2.0) is build-only;
NDK 30.0.16248370 and Rust 1.97.1 are pinned and checked. The build-only ELF/ZIP
auditor reuses locked object 0.36.7 (Apache-2.0 OR MIT) and zip 2.4.2 (MIT)
versions already present in the workspace. UniFFI's MPL-2.0 source remains available
at its recorded upstream crate version. Release attribution assembly is owned by
#112; the dependency policy and runtime matrix are in android/README.md.
+95 -15
View File
@@ -1,13 +1,13 @@
# Native Android build
Architecture and issue ownership: [ADR](../docs/android/architecture.md) and
[parity matrix](../docs/android/parity.md). This scaffold implements #89. It is
an installable native Compose foundation; onboarding, secure storage and native
runtime packaging are implemented by their milestone issues.
[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 a stable workspace Rust toolchain satisfying Cargo.toml, a JDK 25 installation
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
@@ -19,14 +19,14 @@ 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.20. Do not additionally apply kotlin-android with built-in Kotlin enabled.
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 :app:assembleDebug :app:assembleRelease :app:testDebugUnitTest :app:lintDebug ktlintCheck
./gradlew :app:connectedDebugAndroidTest
./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
@@ -50,12 +50,11 @@ 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-only Loading/Empty/Ready/Locked/Error fixtures are generated from the
storage-owned MobileShell::fixture by the Rust build tool. Debug controls select
the fixture and native tabs; these controls and generated fake states are absent
from release. Release displays the scaffold state and does not pretend to have
opened/decrypted a repository. Future #92 replaces scaffold wiring with generated
UniFFI calls; feature code continues to consume the facade.
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
@@ -64,8 +63,8 @@ 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 declared for later Rust packaging; the scaffold
ships no native secret runtime or development network endpoint.
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
@@ -87,3 +86,84 @@ 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.
+18 -15
View File
@@ -6,8 +6,10 @@ androidx.activity:activity-compose:1.13.0=debugAndroidTestCompileClasspath,debug
androidx.activity:activity-ktx:1.13.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.activity:activity:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-experimental:1.5.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.annotation:annotation:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.arch.core:core-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.autofill:autofill:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -208,6 +210,7 @@ jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.19.1=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
net.java.dev.jna:jna:5.6.0=androidLintTool
net.ltgt.gradle.incap:incap:0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
net.sf.kxml:kxml2:2.3.0=androidLintTool
@@ -228,31 +231,31 @@ org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.hamcrest:hamcrest-library:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
org.jetbrains.kotlin:compose-group-mapping:2.4.20=composeMappingProducerClasspath
org.jetbrains.kotlin:compose-group-mapping:2.4.21=composeMappingProducerClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.4.20=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.4.21=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.21=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool
org.jetbrains.kotlin:kotlin-script-runtime:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
@@ -264,8 +267,8 @@ org.jetbrains.kotlin:kotlin-stdlib:2.2.21=composeMappingProducerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.3.20=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable
org.jetbrains.kotlin:kotlin-stdlib:2.3.21=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebug,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,hiltAnnotationProcessorRelease,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
@@ -25,6 +25,10 @@ class ShellScreenTest {
hasText(tab.name) and androidx.compose.ui.test.isSelectable()
).performClick()
val page = ShellFixtures.all.getValue(state).first { it.tab == tab }
compose.waitUntil(timeoutMillis = 10_000) {
compose.onAllNodes(hasText(page.stateTitle) and isHeading())
.fetchSemanticsNodes().size == 1
}
compose.onNode(hasText(page.stateTitle) and isHeading()).assertIsDisplayed()
}
}
@@ -1,39 +0,0 @@
package de.rfc1437.ironstorage
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import de.rfc1437.ironstorage.core.ShellFixtures
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.core.ShellSnapshot
import de.rfc1437.ironstorage.core.ShellState
import de.rfc1437.ironstorage.core.ShellTab
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class DebugShellRepository : ShellRepository {
private val mutableSnapshot =
MutableStateFlow(
ShellSnapshot(ShellState.Empty, ShellFixtures.all.getValue(ShellState.Empty))
)
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
override fun selectTab(tab: ShellTab) {
mutableSnapshot.value = mutableSnapshot.value.copy(selectedTab = tab)
}
fun showFixture(state: ShellState) {
mutableSnapshot.value =
mutableSnapshot.value.copy(state = state, pages = ShellFixtures.all.getValue(state))
}
}
@Module
@InstallIn(SingletonComponent::class)
object DebugShellModule {
@Provides
@Singleton
fun shellRepository(): ShellRepository = DebugShellRepository()
}
@@ -9,6 +9,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import de.rfc1437.ironstorage.core.NativeFixtureShellRepository
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.core.ShellState
import de.rfc1437.ironstorage.features.ShellScreen
@@ -19,7 +20,9 @@ fun VariantContent(viewModel: ShellViewModel, repository: ShellRepository) {
Column(Modifier.statusBarsPadding()) {
Row(Modifier.horizontalScroll(rememberScrollState())) {
ShellState.entries.forEach { state ->
TextButton(onClick = { (repository as DebugShellRepository).showFixture(state) }) {
TextButton(onClick = {
(repository as NativeFixtureShellRepository).showFixture(state)
}) {
Text("Fixture: ${state.name}")
}
}
@@ -0,0 +1,26 @@
package de.rfc1437.ironstorage
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import de.rfc1437.ironstorage.core.NativeFixtureShellRepository
import de.rfc1437.ironstorage.core.ShellRepository
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@Module
@InstallIn(SingletonComponent::class)
object ShellModule {
@Provides
@Singleton
fun shellRepository(@ApplicationContext context: Context): ShellRepository =
NativeFixtureShellRepository(
context,
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
@@ -1,30 +0,0 @@
package de.rfc1437.ironstorage
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.core.ShellSnapshot
import de.rfc1437.ironstorage.core.ShellState
import de.rfc1437.ironstorage.core.ShellTab
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class ScaffoldShellRepository : ShellRepository {
private val mutableSnapshot = MutableStateFlow(ShellSnapshot(ShellState.Empty, emptyList()))
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
override fun selectTab(tab: ShellTab) {
mutableSnapshot.value = mutableSnapshot.value.copy(selectedTab = tab)
}
}
@Module
@InstallIn(SingletonComponent::class)
object ReleaseShellModule {
@Provides
@Singleton
fun shellRepository(): ShellRepository = ScaffoldShellRepository()
}
@@ -1,5 +1,6 @@
package de.rfc1437.ironstorage
import de.rfc1437.ironstorage.core.ShellFixtures
import de.rfc1437.ironstorage.core.ShellState
import de.rfc1437.ironstorage.core.ShellTab
import org.junit.Assert.assertEquals
@@ -7,14 +8,12 @@ import org.junit.Test
class DebugShellRepositoryTest {
@Test
fun everyFixtureHasFivePagesAndSelectionSurvivesStateChanges() {
val repository = DebugShellRepository()
repository.selectTab(ShellTab.Totp)
fun fixtureInventorySupportsEveryNativeTab() {
val selected = ShellTab.Totp
ShellState.entries.forEach { state ->
repository.showFixture(state)
assertEquals(state, repository.snapshot.value.state)
assertEquals(ShellTab.entries, repository.snapshot.value.pages.map { it.tab })
assertEquals(ShellTab.Totp, repository.snapshot.value.selectedTab)
val pages = ShellFixtures.all.getValue(state)
assertEquals(ShellTab.entries, pages.map { it.tab })
assertEquals(selected, pages.first { it.tab == selected }.tab)
}
}
}
+17
View File
@@ -62,3 +62,20 @@ val checkArchitecture = tasks.register<Exec>("checkArchitecture") {
subprojects {
tasks.matching { it.name == "preBuild" || it.name == "check" }.configureEach { dependsOn(rootProject.tasks.named("checkArchitecture")) }
}
tasks.register<Exec>("auditNativePackages") {
dependsOn(":app:assembleDebug", ":app:assembleRelease", ":app:bundleRelease")
workingDir = workspace
val packages = listOf(
layout.projectDirectory.file("app/build/outputs/apk/debug/app-debug.apk"),
layout.projectDirectory.file("app/build/outputs/apk/release/app-release-unsigned.apk"),
layout.projectDirectory.file("app/build/outputs/bundle/release/app-release.aab")
)
inputs.files(packages)
commandLine(
listOf(
cargoExecutable.get(), "+${libs.versions.rust.get()}", "run", "--locked",
"--package", "ironstorage-android-build", "--", "audit-package"
) + packages.map { it.asFile.absolutePath }
)
}
+14 -11
View File
@@ -2,6 +2,8 @@
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
# To regenerate this file, run: ./gradlew :core:dependencies --write-locks
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
ch.qos.logback:logback-classic:1.3.15=ktlint
ch.qos.logback:logback-core:1.3.15=ktlint
com.android.tools.analytics-library:protos:32.4.1=androidLintTool
@@ -75,6 +77,7 @@ jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=androidLintTool
junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.19.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
net.java.dev.jna:jna:5.6.0=androidLintTool
net.sf.kxml:kxml2:2.3.0=androidLintTool
org.apache.commons:commons-compress:1.27.1=androidLintTool
@@ -93,27 +96,27 @@ org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool
org.jetbrains.kotlin:kotlin-script-runtime:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
@@ -122,8 +125,8 @@ org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-plat
org.jetbrains.kotlin:kotlin-stdlib:2.2.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
@@ -0,0 +1,101 @@
package de.rfc1437.ironstorage.core
import android.content.Context
import de.rfc1437.ironstorage.bridge.NativeBridgeException
import de.rfc1437.ironstorage.bridge.NativeFailureKind
import de.rfc1437.ironstorage.bridge.NativeRuntime
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.MobileTab
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** #92's native fixture integration; repository initialization is owned by #90. */
class NativeFixtureShellRepository internal constructor(
private val scope: CoroutineScope,
private val load: suspend (ShellState) -> ShellSnapshot
) : ShellRepository {
constructor(context: Context, scope: CoroutineScope) : this(scope, nativeLoader(context))
private val mutableSnapshot = MutableStateFlow(ShellSnapshot(ShellState.Loading, emptyList()))
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
private val revision = AtomicLong()
private var job: Job? = null
init {
showFixture(ShellState.Empty)
}
override fun selectTab(tab: ShellTab) {
mutableSnapshot.update { it.copy(selectedTab = tab) }
}
fun showFixture(state: ShellState) {
val operation = revision.incrementAndGet()
job?.cancel()
job = scope.launch {
val result = try {
load(state)
} catch (error: NativeBridgeException) {
ShellSnapshot(ShellState.Error, emptyList(), nativeFailure = error.kind.toFacade())
}
if (revision.get() == operation) {
mutableSnapshot.update { result.copy(selectedTab = it.selectedTab) }
}
}
}
}
private fun nativeLoader(context: Context): suspend (ShellState) -> ShellSnapshot {
val runtime = NativeRuntime(context)
return { state ->
withContext(Dispatchers.IO) {
val shell = runtime.fixture(state.toNative())
val pages = shell.pages.map { page ->
ShellPage(page.tab.toFacade(), page.title, page.stateTitle, page.stateDetail)
}
val actual = shell.pages.firstOrNull()?.state
?: throw NativeBridgeException(NativeFailureKind.RustFailure)
ShellSnapshot(actual.toFacade(), pages)
}
}
}
private fun MobileShellState.toFacade(): ShellState = when (this) {
MobileShellState.LOADING -> ShellState.Loading
MobileShellState.EMPTY -> ShellState.Empty
MobileShellState.READY -> ShellState.Ready
MobileShellState.LOCKED -> ShellState.Locked
MobileShellState.ERROR -> ShellState.Error
}
private fun ShellState.toNative(): MobileShellState = when (this) {
ShellState.Loading -> MobileShellState.LOADING
ShellState.Empty -> MobileShellState.EMPTY
ShellState.Ready -> MobileShellState.READY
ShellState.Locked -> MobileShellState.LOCKED
ShellState.Error -> MobileShellState.ERROR
}
private fun MobileTab.toFacade(): ShellTab = when (this) {
MobileTab.HOME -> ShellTab.Home
MobileTab.PASSWORDS -> ShellTab.Passwords
MobileTab.SEARCH -> ShellTab.Search
MobileTab.TOTP -> ShellTab.Totp
MobileTab.PREFERENCES -> ShellTab.Preferences
}
private fun NativeFailureKind.toFacade(): NativeFailure = when (this) {
NativeFailureKind.UnsupportedAbi -> NativeFailure.UnsupportedAbi
NativeFailureKind.MissingLibrary -> NativeFailure.MissingLibrary
NativeFailureKind.ContractMismatch -> NativeFailure.ContractMismatch
NativeFailureKind.ChecksumMismatch -> NativeFailure.ChecksumMismatch
NativeFailureKind.LoadFailure -> NativeFailure.LoadFailure
NativeFailureKind.RustFailure -> NativeFailure.RustFailure
NativeFailureKind.MainThreadCall -> NativeFailure.MainThreadCall
}
@@ -14,9 +14,20 @@ data class ShellPage(
data class ShellSnapshot(
val state: ShellState,
val pages: List<ShellPage>,
val selectedTab: ShellTab = ShellTab.Home
val selectedTab: ShellTab = ShellTab.Home,
val nativeFailure: NativeFailure? = null
)
enum class NativeFailure {
UnsupportedAbi,
MissingLibrary,
ContractMismatch,
ChecksumMismatch,
LoadFailure,
RustFailure,
MainThreadCall
}
interface ShellRepository {
val snapshot: StateFlow<ShellSnapshot>
fun selectTab(tab: ShellTab)
@@ -0,0 +1,31 @@
package de.rfc1437.ironstorage.core
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Test
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class NativeFixtureShellRepositoryTest {
@Test
fun cancelledNativeCompletionCannotReplaceNewerStateOrTabSelection() = runTest {
val pending = CompletableDeferred<Unit>()
val repository = NativeFixtureShellRepository(backgroundScope) { state ->
if (state == ShellState.Ready) withContext(NonCancellable) { pending.await() }
ShellSnapshot(state, listOf(ShellPage(ShellTab.Home, "Home", state.name, "Fixture")))
}
runCurrent()
repository.showFixture(ShellState.Ready)
runCurrent()
repository.selectTab(ShellTab.Search)
repository.showFixture(ShellState.Locked)
runCurrent()
pending.complete(Unit)
runCurrent()
assertEquals(ShellState.Locked, repository.snapshot.value.state)
assertEquals(ShellTab.Search, repository.snapshot.value.selectedTab)
}
}
+17 -14
View File
@@ -6,8 +6,10 @@ androidx.activity:activity-compose:1.12.0=debugAndroidTestCompileClasspath,debug
androidx.activity:activity-ktx:1.12.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.activity:activity:1.12.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-experimental:1.5.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.9.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.annotation:annotation:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
androidx.arch.core:core-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.arch.core:core-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.autofill:autofill:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -192,6 +194,7 @@ jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.19.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
net.java.dev.jna:jna:5.6.0=androidLintTool
net.ltgt.gradle.incap:incap:0.2=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
net.sf.kxml:kxml2:2.3.0=androidLintTool
@@ -212,29 +215,29 @@ org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.4.20=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:2.4.21=kotlin-extension,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.21=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool
org.jetbrains.kotlin:kotlin-script-runtime:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath,ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
@@ -245,8 +248,8 @@ org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-
org.jetbrains.kotlin:kotlin-stdlib:2.3.20=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,kspPluginClasspath,kspPluginClasspathNonEmbeddable
org.jetbrains.kotlin:kotlin-stdlib:2.3.21=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
@@ -18,6 +18,7 @@ import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.rfc1437.ironstorage.core.NativeFailure
import de.rfc1437.ironstorage.core.ShellState
import de.rfc1437.ironstorage.core.ShellTab
@@ -44,12 +45,33 @@ fun ShellScreen(viewModel: ShellViewModel) {
) {
Text(stringResource(R.string.app_name), style = MaterialTheme.typography.headlineLarge)
val page = snapshot.pages.firstOrNull { it.tab == snapshot.selectedTab }
val nativeFailure = snapshot.nativeFailure
val title = if (nativeFailure != null) {
stringResource(R.string.native_failure_title)
} else {
page?.stateTitle ?: stringResource(R.string.scaffold_title)
}
val detail = if (nativeFailure == null) {
page?.detail ?: stringResource(R.string.scaffold_detail)
} else {
stringResource(
when (nativeFailure) {
NativeFailure.UnsupportedAbi -> R.string.native_unsupported_device
NativeFailure.MissingLibrary,
NativeFailure.ContractMismatch,
NativeFailure.ChecksumMismatch,
NativeFailure.LoadFailure -> R.string.native_update_required
NativeFailure.RustFailure,
NativeFailure.MainThreadCall -> R.string.native_operation_failed
}
)
}
Text(
page?.stateTitle ?: stringResource(R.string.scaffold_title),
title,
modifier = Modifier.semantics { heading() },
style = MaterialTheme.typography.headlineSmall
)
Text(page?.detail ?: stringResource(R.string.scaffold_detail))
Text(detail)
if (snapshot.state == ShellState.Loading) CircularProgressIndicator()
}
}
@@ -2,4 +2,8 @@
<string name="app_name">IronStorage</string>
<string name="scaffold_title">No Password Store</string>
<string name="scaffold_detail">Your password store has not been configured.</string>
<string name="native_failure_title">IronStorage Is Unavailable</string>
<string name="native_unsupported_device">This device is not supported by this app build.</string>
<string name="native_update_required">The installed app components could not be loaded or do not match. Install a complete update without clearing app data.</string>
<string name="native_operation_failed">The app could not complete this operation. Try again.</string>
</resources>
+7 -1
View File
@@ -1,6 +1,6 @@
[versions]
agp = "9.4.1"
kotlin = "2.4.20"
kotlin = "2.4.21"
ksp = "2.3.12"
hilt = "2.60.1"
compose = "2026.09.00"
@@ -17,6 +17,10 @@ ktlint = "1.7.1"
compileSdk = "37"
minSdk = "26"
ndk = "30.0.16248370"
rust = "1.97.1"
cargoNdk = "4.1.2"
jna = "5.19.1"
annotation = "1.11.0"
[libraries]
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose" }
@@ -37,6 +41,8 @@ hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hil
junit = { module = "junit:junit", version.ref = "junit" }
android-test-runner = { module = "androidx.test:runner", version.ref = "androidTest" }
android-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+255
View File
@@ -63,6 +63,11 @@
<sha256 value="f4001a709b6f7132b22a5e17c24f8b8a9b3c462a598de3b0f16cc57a4344bab6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.11.0">
<artifact name="annotation-1.11.0.module">
<sha256 value="ac8410bea7adaabc9e71285f0627c9cb929c5addd00cfc6d0ebd421dc2da1ee0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.7.0">
<artifact name="annotation-1.7.0.module">
<sha256 value="530708656d380605077ea8bca9ae1372fbd1af38e375f411d4e43263bd510c3c" origin="Generated by Gradle"/>
@@ -91,6 +96,22 @@
<sha256 value="ae1ff29a7f3965e6589acfe7f07ef39d36405d7ab278472327afd92fc6d72a3f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.11.0">
<artifact name="annotation-jvm-1.11.0.jar">
<sha256 value="a83b827cc17fd6b41c7bf5e7a2a2b40a0895b00b3e30fdfdb820b49dc7b667f3" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotation-jvm-1.11.0.module">
<sha256 value="0c8a86f2a8e20120cc13544f101047f42284de1f1dd7ca26c90056901de4c2da" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.7.0">
<artifact name="annotation-jvm-1.7.0.jar">
<sha256 value="e36b8e4b8393a4adc74e3d4ab22ad5a36396f0cea2e40b5734eae14937dfd224" origin="Generated by Gradle"/>
</artifact>
<artifact name="annotation-jvm-1.7.0.module">
<sha256 value="07ce60c377ab94e47c8c902589b9776030064fd1a7e4d5a01a38d700e35e5db4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.9.1">
<artifact name="annotation-jvm-1.9.1.jar">
<sha256 value="1e343917ebf27ba96fe4dc52b1cad7fd32b738fbc6355bb6cd5b3b305d7212d0" origin="Generated by Gradle"/>
@@ -2180,6 +2201,14 @@
<sha256 value="569b6977ee4603c965c1c46c3058fa6e969291b0160eb6964dd092cd89eadd94" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.java.dev.jna" name="jna" version="5.19.1">
<artifact name="jna-5.19.1.aar">
<sha256 value="b57125cb7d16253f0d65a80f7d3a4c3664effa711b8bdbb7f87fb572ce1624ed" origin="Generated by Gradle"/>
</artifact>
<artifact name="jna-5.19.1.pom">
<sha256 value="911b754a03b66af0fed2bbdfdc3a86807360dd036ad3b481ef5162c685e456bf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.java.dev.jna" name="jna" version="5.6.0">
<artifact name="jna-5.6.0.jar">
<sha256 value="5557e235a8aa2f9766d5dc609d67948f2a8832c2d796cea9ef1d6cbe0b3b7eaf" origin="Generated by Gradle"/>
@@ -2522,6 +2551,14 @@
<sha256 value="6e3d3c8d2a375393d64395882ac80c9c3a44e90008801a34fd99a13cbcc96bfd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="compose-compiler-gradle-plugin" version="2.4.21">
<artifact name="compose-compiler-gradle-plugin-2.4.21-gradle96.jar">
<sha256 value="17b7d35e53e23f4820e0fb9f3ad71f7ab463970d68bcd8ba7f8d71498b50395f" origin="Generated by Gradle"/>
</artifact>
<artifact name="compose-compiler-gradle-plugin-2.4.21.module">
<sha256 value="e7bc61dabd4381f4a8f53c7be30bfb5cccd971af27d55b7c39b0a37b28bef24d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="compose-group-mapping" version="2.4.20">
<artifact name="compose-group-mapping-2.4.20.jar">
<sha256 value="0205146299c4c02e19b62620e5a0276aaecc3ecd7711cf274e86e36e6a73f4ca" origin="Generated by Gradle"/>
@@ -2530,6 +2567,14 @@
<sha256 value="2fba7c50bc1933a70f6ed6d8d3b574a8e97d86912c28caa96fe7ce648a87eb48" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="compose-group-mapping" version="2.4.21">
<artifact name="compose-group-mapping-2.4.21.jar">
<sha256 value="45bb5caf737fb50cf96fd67f4a45aff8a75cae387333a5be80ad29523442d327" origin="Generated by Gradle"/>
</artifact>
<artifact name="compose-group-mapping-2.4.21.pom">
<sha256 value="a1c461dc79493649a9d1eee167c172dd32aa3260a48735751de30ee169cf4620" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="fus-statistics-gradle-plugin" version="2.4.20">
<artifact name="fus-statistics-gradle-plugin-2.4.20-gradle96.jar">
<sha256 value="0c050825d65654d0ac8f6ba9409fba824f87100df00e76300242abfe9a1b989f" origin="Generated by Gradle"/>
@@ -2538,6 +2583,14 @@
<sha256 value="cfe6fb1af3f7da01428ef4bf4e38e1c76637f4e108ac22f15dfe4c733df408d4" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="fus-statistics-gradle-plugin" version="2.4.21">
<artifact name="fus-statistics-gradle-plugin-2.4.21-gradle96.jar">
<sha256 value="44a07b9203eb2c75ceb2d33327f89b991f2d35095a96a0f1c224a012c5465367" origin="Generated by Gradle"/>
</artifact>
<artifact name="fus-statistics-gradle-plugin-2.4.21.module">
<sha256 value="3090b4e873054cdc29991ae94c92fa45afb4c145d460441d803baf6ef165bbb8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-bom" version="2.3.21">
<artifact name="kotlin-bom-2.3.21.pom">
<sha256 value="950704bad04c5143656cfca1d2c574f25fcad9cac33905ab22a57e9fdf7bfd88" origin="Generated by Gradle"/>
@@ -2551,6 +2604,14 @@
<sha256 value="184af9ac1aaf68d0c96875465e9d398926586d83119abbfe1be5979398b8916f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-statistics" version="2.4.21">
<artifact name="kotlin-build-statistics-2.4.21.jar">
<sha256 value="f917249997fe48d1eb1204906b4cabc8230bf32a1d9fb385e52311acaa6ebc68" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-build-statistics-2.4.21.pom">
<sha256 value="4ac9fe3a27069f9ad87dc36ede15fc06335663d4ae46fac09a16bb090e16038c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-api" version="2.4.0">
<artifact name="kotlin-build-tools-api-2.4.0.jar">
<sha256 value="66917335497f9ab75a36c34b3d90bad2bc8dc256c34b2413ecd0c9467fcf597d" origin="Generated by Gradle"/>
@@ -2567,6 +2628,14 @@
<sha256 value="e8cd9ffd7d0886ba1a1795cbf4a34a06681f184aa50f25686effeefb3c1f56f5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-api" version="2.4.21">
<artifact name="kotlin-build-tools-api-2.4.21.jar">
<sha256 value="c5cd6a49bd072b529ae10809111c16a7aa0f92147e95d288354017fd65799461" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-build-tools-api-2.4.21.pom">
<sha256 value="4b909e42d55242520278d5397eca1edd58921fe128942eeff95a6da279c2eb27" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-compat" version="2.4.20">
<artifact name="kotlin-build-tools-compat-2.4.20.jar">
<sha256 value="937d2961fc0b957a50c3d85a14fcc1bd0585f2229164136895b623691660c9b4" origin="Generated by Gradle"/>
@@ -2575,6 +2644,14 @@
<sha256 value="f8e9f647a00bfbc237e3ddff3891b2d4e4a947611b949281658f836def304002" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-compat" version="2.4.21">
<artifact name="kotlin-build-tools-compat-2.4.21.jar">
<sha256 value="9e71ba89b6d26e46a087d4f8b64bc2f0a774282ea779fc08b26527d0a80d4fe8" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-build-tools-compat-2.4.21.pom">
<sha256 value="9d9b4627d5b7bb8631457061275f21524d874b865550addec4f3b029d8265817" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-cri-impl" version="2.4.0">
<artifact name="kotlin-build-tools-cri-impl-2.4.0.jar">
<sha256 value="5168b866945f6ac1ffcb6d4c88cf582497249469c2e4a4eafc0bcd35b5a264a9" origin="Generated by Gradle"/>
@@ -2591,6 +2668,14 @@
<sha256 value="8af52efa886eed45c54d568b692e339b70ddc7c768bee9ab9c2d2f96ed8bb0e1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-cri-impl" version="2.4.21">
<artifact name="kotlin-build-tools-cri-impl-2.4.21.jar">
<sha256 value="d1849f9b98033f5333905409074ca90e6a0e17258cd0eb8befa47d795e695841" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-build-tools-cri-impl-2.4.21.pom">
<sha256 value="57f3c430dfec2e575bb1b64055412ace12d4ed5d9a02eb97a85dd47f53701ef1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-impl" version="2.4.0">
<artifact name="kotlin-build-tools-impl-2.4.0.jar">
<sha256 value="076c0a7f84d303f1e390774410c8678b8b584faa3f642435f93d2e4e81dabe7c" origin="Generated by Gradle"/>
@@ -2607,6 +2692,14 @@
<sha256 value="7a13ab2d818a5de92afa23004aa9baed99671bb48553b7098cf920fa785c3351" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-build-tools-impl" version="2.4.21">
<artifact name="kotlin-build-tools-impl-2.4.21.jar">
<sha256 value="3152b3e1fbaa4d48f3681734a2c0efc438359a48c8a3c5cac00d874670de1377" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-build-tools-impl-2.4.21.pom">
<sha256 value="6ef5cb90e2d8ea75faf9ec668d306b6be7628f11ebd41878a82cae3e17dbedfb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="2.2.0">
<artifact name="kotlin-compiler-embeddable-2.2.0.jar">
<sha256 value="b2f743ea5ba12f69e0f35e5d8d46069d74c8e2861087548a7e0e14a784bc4cf1" origin="Generated by Gradle"/>
@@ -2631,6 +2724,14 @@
<sha256 value="269d98cc4c68670c11e4aeb404e1c7c6f31b782321908d7268ba25cd236d59b2" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="2.4.21">
<artifact name="kotlin-compiler-embeddable-2.4.21.jar">
<sha256 value="ef19419c765e7ac8404465fa026ca8fc4dbb8a822de0fc79aa53c8dce29f1d02" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-compiler-embeddable-2.4.21.pom">
<sha256 value="e1620d2b18319cf7495dc67697ab03ba23a7d6d9da685285884a7a8aa08ae891" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-compiler-runner" version="2.4.0">
<artifact name="kotlin-compiler-runner-2.4.0.jar">
<sha256 value="ad9608152740e92d94a278b4ff2ca82c4bf5ff8ef219220e3c07aeb554638cee" origin="Generated by Gradle"/>
@@ -2647,6 +2748,14 @@
<sha256 value="4566946280971fe8d67990ee83480af480e846590f325a40951f62657e26a40a" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-compiler-runner" version="2.4.21">
<artifact name="kotlin-compiler-runner-2.4.21.jar">
<sha256 value="75cd5956405697559e6925ebc5808449a2e12002d4f017275281ad3d4c0c7985" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-compiler-runner-2.4.21.pom">
<sha256 value="619d17c885321949eaa167af0842cad8f7aa83a4a9ca1051e5de04d11a6c0f40" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-compose-compiler-plugin-embeddable" version="2.4.20">
<artifact name="kotlin-compose-compiler-plugin-embeddable-2.4.20.jar">
<sha256 value="e6fc101fd0b8592911de57f96a75b09e0b6158eaccbc71ddd6ca82e05e0386a2" origin="Generated by Gradle"/>
@@ -2655,6 +2764,14 @@
<sha256 value="413bc7cb82d49d020e3fca2f6cb9e905954b5324a86bb941b9113dd83bd77673" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-compose-compiler-plugin-embeddable" version="2.4.21">
<artifact name="kotlin-compose-compiler-plugin-embeddable-2.4.21.jar">
<sha256 value="634f5c17bed5854548208f2901071f2ead5993ffad25b34fea1668ee9e314b53" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-compose-compiler-plugin-embeddable-2.4.21.pom">
<sha256 value="d5af6aa743ce0fa940b004a6cbd1777db0a76319e1daa1fded9b03c361aa03a3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-daemon-client" version="2.4.0">
<artifact name="kotlin-daemon-client-2.4.0.jar">
<sha256 value="f546f0b3699c4984f320dd03080de05cad61064208ba8f37df0d3f8f4a7fd14a" origin="Generated by Gradle"/>
@@ -2671,6 +2788,14 @@
<sha256 value="37cff66ae34c924f0f738106eeb7234e7ace880f199de3362e993f4987025adc" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-daemon-client" version="2.4.21">
<artifact name="kotlin-daemon-client-2.4.21.jar">
<sha256 value="095903c4c4713bf91a562d226d7ba0464cf5424e76342d487deec494451ab29b" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-daemon-client-2.4.21.pom">
<sha256 value="c2f18065fa18f5e92eff14f34029e4f70052011070655120796adbce7167ad65" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="2.2.0">
<artifact name="kotlin-daemon-embeddable-2.2.0.jar">
<sha256 value="a26cc8e2d8619197cc4d115bea7766e9aa8e0f1e7876ea044ee1b9f305654601" origin="Generated by Gradle"/>
@@ -2695,6 +2820,14 @@
<sha256 value="ca631ce7c7cc2a8ecb47d3462b429004c1cd3488437b3f739e35eafea2033bfb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="2.4.21">
<artifact name="kotlin-daemon-embeddable-2.4.21.jar">
<sha256 value="7b3ef24674e5bcffcadfc4e43202ea314194939ecbcdd90a4770b2f1b04e0d07" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-daemon-embeddable-2.4.21.pom">
<sha256 value="95236a3a7e9953545e2ff7c6dc0ea7ffeb27560000f20abe89dc53790fec3c7e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin" version="2.4.20">
<artifact name="kotlin-gradle-plugin-2.4.20-gradle96.jar">
<sha256 value="6f49688f2c6715d60f5dc52d4c1ada992ce4bed6053d1bd6e579c9392d7ba0ed" origin="Generated by Gradle"/>
@@ -2703,6 +2836,14 @@
<sha256 value="8a88cf52de5fb9c829730b0f5aec1fcd35633e697871e256230f09903c74dd43" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin" version="2.4.21">
<artifact name="kotlin-gradle-plugin-2.4.21-gradle96.jar">
<sha256 value="d0a11912936c8801f1773a47e8fc51835a6ab568d73e4e1d188281955a7f575b" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-gradle-plugin-2.4.21.module">
<sha256 value="a412aecabbb40c7ed2fad5311d0114d4ca0e3a9690658ad3c4caed619e400d94" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-annotations" version="2.4.20">
<artifact name="kotlin-gradle-plugin-annotations-2.4.20.jar">
<sha256 value="fd400d73f635ed83875e75093c40a587840d4af399dcf6931239ba8fe469ef85" origin="Generated by Gradle"/>
@@ -2711,6 +2852,14 @@
<sha256 value="d903e1b1b633cdd7c85e557a4426b0c7b8584065a46fe626826a4da3790f945d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-annotations" version="2.4.21">
<artifact name="kotlin-gradle-plugin-annotations-2.4.21.jar">
<sha256 value="5c386b0fa287584497b07d62af954e30d05907e6da867221124d0bb936989bbd" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-gradle-plugin-annotations-2.4.21.pom">
<sha256 value="b0f6b87a35d4be7ae06e554efdb261ea4bc5d59fa282c23ee28061d30805ddd6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-api" version="2.4.20">
<artifact name="kotlin-gradle-plugin-api-2.4.20-gradle96.jar">
<sha256 value="fc9e18f84f1a5805532932f5293ec11121769263b396641f779e7b67fb4250a6" origin="Generated by Gradle"/>
@@ -2719,6 +2868,14 @@
<sha256 value="4bea3899dada541c1e482246b2573678eaedf611af7f1674ce0a17d5c3effdc0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-api" version="2.4.21">
<artifact name="kotlin-gradle-plugin-api-2.4.21-gradle96.jar">
<sha256 value="43b8c73f3e4f123b1a5e4bc8e62bd9b258dfa84129e6564bebb03c5fada84d23" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-gradle-plugin-api-2.4.21.module">
<sha256 value="b8ae1c9ee5976145d4fd7925012e5699e2df5df7fe9613bac41d59404a58e815" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea" version="2.4.20">
<artifact name="kotlin-gradle-plugin-idea-2.4.20.jar">
<sha256 value="086734484c7edf19a620ab1a617d097c40e03bc52602e77840ea5ebd6380e514" origin="Generated by Gradle"/>
@@ -2727,6 +2884,14 @@
<sha256 value="7a8c8d128782b4f46b5d831c7eda2513d871d321a483a43aadbe77f97224cb64" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea" version="2.4.21">
<artifact name="kotlin-gradle-plugin-idea-2.4.21.jar">
<sha256 value="086734484c7edf19a620ab1a617d097c40e03bc52602e77840ea5ebd6380e514" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-gradle-plugin-idea-2.4.21.module">
<sha256 value="ddd2bb8f319a7c468ba2fb5a404b795c69e066bd5ad2d565b47b86fa2a337606" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea-proto" version="2.4.20">
<artifact name="kotlin-gradle-plugin-idea-proto-2.4.20.jar">
<sha256 value="a3a5535287930428ecbd4a326cb9d7d7c477e9f1a81e9573c3c973940c8f1477" origin="Generated by Gradle"/>
@@ -2735,6 +2900,14 @@
<sha256 value="d059dace6e76f36808ce94e8588a603324716d07767bc874206036794ba3bd05" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea-proto" version="2.4.21">
<artifact name="kotlin-gradle-plugin-idea-proto-2.4.21.jar">
<sha256 value="4559d92c5c1758a3e1ec5728822b059b2eb4aebef13c701ee86c7688acf164d5" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-gradle-plugin-idea-proto-2.4.21.pom">
<sha256 value="3c6eff2166eb46a8474b7bcccaa7d1ca5074260c866cc4bb2ba3678216a7bc15" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugins-bom" version="2.4.20">
<artifact name="kotlin-gradle-plugins-bom-2.4.20.module">
<sha256 value="6dfa7dfda619ad248e413cf313c313705db0aa0dbc410b3a8e0d922acfa82c94" origin="Generated by Gradle"/>
@@ -2743,6 +2916,14 @@
<sha256 value="70b071dc4074995076c97c749792ebcff69f10e636d1e2f2c68de0de023e0239" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-gradle-plugins-bom" version="2.4.21">
<artifact name="kotlin-gradle-plugins-bom-2.4.21.module">
<sha256 value="04f6a73a2c34217ac31599b8ed5ff24b85ebd410d32242eb27965c7b213f56c3" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-gradle-plugins-bom-2.4.21.pom">
<sha256 value="624c0c869b3fcf1665afec9c5c51a807be0a8c983931ebb188548ddcdb055780" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-api" version="2.4.20">
<artifact name="kotlin-klib-commonizer-api-2.4.20.jar">
<sha256 value="e13c80f2f96afce37d776e77ae8a2f5c4d3c72f4b0526271694b36786f27d331" origin="Generated by Gradle"/>
@@ -2751,6 +2932,14 @@
<sha256 value="b1edb50183d4549ad0fb449498535ab4186d491d86ad315e0187bcb9f9c7496d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-api" version="2.4.21">
<artifact name="kotlin-klib-commonizer-api-2.4.21.jar">
<sha256 value="62ebe32fcf69600125b28388f0a80781276784d90e14d333bbd5080b4807220c" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-klib-commonizer-api-2.4.21.pom">
<sha256 value="186a53ce6f218e43472808dc32cab627e19c144df2517398794bdb53d2df14a0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-metadata-jvm" version="2.3.21">
<artifact name="kotlin-metadata-jvm-2.3.21.jar">
<sha256 value="00590f26a5a768777f04252c6cf93c016a3361a6bc93636df67375ad4d19557d" origin="Generated by Gradle"/>
@@ -2767,6 +2956,14 @@
<sha256 value="453ba071c89165cbdfc877ca733a4f38c01fc2f4894e9a259d7651f1176cd80c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-native-utils" version="2.4.21">
<artifact name="kotlin-native-utils-2.4.21.jar">
<sha256 value="e1858bf1fb5bbb1738c4fa0f9cfb502b911cabf313a5de4cae686d6c50d74814" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-native-utils-2.4.21.pom">
<sha256 value="9882156113d96f7911101a721cce2adefe2c8d9acc8844fb6397b8523538db37" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.10">
<artifact name="kotlin-reflect-1.6.10.jar">
<sha256 value="3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203" origin="Generated by Gradle"/>
@@ -2815,6 +3012,14 @@
<sha256 value="dd0e3846b61a6d641300958cc52e4693cd7b47bd1d626505f383b2f9d23ade0e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="2.4.21">
<artifact name="kotlin-script-runtime-2.4.21.jar">
<sha256 value="a658cf5902a1525a06c264a5e42b437e97a6679ddeef8ca641246f270cf6a013" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-script-runtime-2.4.21.pom">
<sha256 value="425d1683a903d085a1718122918242ad6ad0246c79283786697395e4d3a8c801" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="2.2.0">
<artifact name="kotlin-stdlib-2.2.0.jar">
<sha256 value="65d12d85a3b865c160db9147851712a64b10dadd68b22eea22a95bf8a8670dca" origin="Generated by Gradle"/>
@@ -2879,6 +3084,14 @@
<sha256 value="afb28052ac6c3d605c0f661816ea653e073eb039682fed35bfa6cdc704c0911c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="2.4.21">
<artifact name="kotlin-stdlib-2.4.21.jar">
<sha256 value="bd8250210584cb659847dce9cda660f8e9906e5def7fbebcea93dc6dcb6a88a3" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-stdlib-2.4.21.module">
<sha256 value="03b0df67c0cd74a948a45c5b03c4bbcfb31ab9b9879fb380adf7c187b6594b7f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="2.2.0">
<artifact name="kotlin-stdlib-common-2.2.0.module">
<sha256 value="58fc0d664fc3a67e7e6be9fdbeaedbd212dfa3e527f744f861e792cda72c0e47" origin="Generated by Gradle"/>
@@ -2924,6 +3137,14 @@
<sha256 value="29dd79ed2bc1e64f6bfd5ccf0c1336c6b6c11ee2a69e0c585e3fad2f45b5325d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-tooling-core" version="2.4.21">
<artifact name="kotlin-tooling-core-2.4.21.jar">
<sha256 value="5fc5db083803232857408ed5c25bcc307c703b04bdef6cb2c34ebf4db611a622" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-tooling-core-2.4.21.pom">
<sha256 value="b26b1cb6480001519d4650586cdc6054ea2d2d486ff332431c543eb8f76f41ce" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-util-io" version="2.4.20">
<artifact name="kotlin-util-io-2.4.20.jar">
<sha256 value="944edbec0f4438b40999e81872f6b05b0604abac6ed11d72c27846d19835f24c" origin="Generated by Gradle"/>
@@ -2932,6 +3153,14 @@
<sha256 value="840f888f360f4395c0967e17620b96d8b0cf5180efe2a214fdabd4d66677665b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-util-io" version="2.4.21">
<artifact name="kotlin-util-io-2.4.21.jar">
<sha256 value="5821175f24578ca8f1ef405bc982b3668f3f40f1a5dd28bb3283dd5215a922d1" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-util-io-2.4.21.pom">
<sha256 value="a16f898aa8c686dc0b83d556ac4adecfcadbb6662ca547fca7802bf536d6d3d3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-util-klib" version="2.4.20">
<artifact name="kotlin-util-klib-2.4.20.jar">
<sha256 value="3a58f9a82378e13ea5045266de2c09a8e5a05c44dd92131c4a83794fd3d1ceca" origin="Generated by Gradle"/>
@@ -2940,6 +3169,14 @@
<sha256 value="7fb051f47bbe01f318a1f434e2a7ed61a446fcc7a4d3ccb5a44c2b5105b0fd6b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-util-klib" version="2.4.21">
<artifact name="kotlin-util-klib-2.4.21.jar">
<sha256 value="f01f79d54968941dfa83a525ae173be93460bb85b0bf2cc9ff06f6eb0864691e" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-util-klib-2.4.21.pom">
<sha256 value="97b59495de92c789c6963e4e70d5545ceccd2f0fab67d7ce72f7c5a87b948030" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-util-klib-metadata" version="2.4.20">
<artifact name="kotlin-util-klib-metadata-2.4.20.jar">
<sha256 value="75027fd053a594ab1ff64aa360cb1d6d13e9a7e3775583508b4811e0333823d8" origin="Generated by Gradle"/>
@@ -2948,16 +3185,34 @@
<sha256 value="90c5c8092955a601898c74f7dae3945fb0d041d5191d009bab41d530f8402f4f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-util-klib-metadata" version="2.4.21">
<artifact name="kotlin-util-klib-metadata-2.4.21.jar">
<sha256 value="a47f883b82fc3b99fe3d666498b22312628afb5cc1b9b26e8188175be2da6060" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-util-klib-metadata-2.4.21.pom">
<sha256 value="a0f7f315c9f60b53458cc1e034a7c55e17619ce7db51db43efb7d3cdacbb5c6c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.jvm" name="org.jetbrains.kotlin.jvm.gradle.plugin" version="2.4.20">
<artifact name="org.jetbrains.kotlin.jvm.gradle.plugin-2.4.20.pom">
<sha256 value="f3b27910045c1e1e21cb48d68a7e177ea1a4a3dda467e546c15c30b9c098187c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.jvm" name="org.jetbrains.kotlin.jvm.gradle.plugin" version="2.4.21">
<artifact name="org.jetbrains.kotlin.jvm.gradle.plugin-2.4.21.pom">
<sha256 value="8e08073d123182e69c07d24c6dca7b98cc53a37707a73a6af7be88e38218b42b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.plugin.compose" name="org.jetbrains.kotlin.plugin.compose.gradle.plugin" version="2.4.20">
<artifact name="org.jetbrains.kotlin.plugin.compose.gradle.plugin-2.4.20.pom">
<sha256 value="9ed1b04a99d38243a15a42b37d4dc4ba84ca77f35d77c081a7c2208d439e1220" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.plugin.compose" name="org.jetbrains.kotlin.plugin.compose.gradle.plugin" version="2.4.21">
<artifact name="org.jetbrains.kotlin.plugin.compose.gradle.plugin-2.4.21.pom">
<sha256 value="ee66712b1006e971636f11554af63c56835caf315382ce23e54f59f5b8b9d4f9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.11.0">
<artifact name="kotlinx-coroutines-android-1.11.0.jar">
<sha256 value="c2cb206d27017c7d1bf5ff179787397543d13748dbabb0d7237e1585e0b29044" origin="Generated by Gradle"/>
+14 -11
View File
@@ -2,6 +2,8 @@
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
# To regenerate this file, run: ./gradlew :platform:dependencies --write-locks
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
ch.qos.logback:logback-classic:1.3.15=ktlint
ch.qos.logback:logback-core:1.3.15=ktlint
com.android.tools.analytics-library:protos:32.4.1=androidLintTool
@@ -75,6 +77,7 @@ jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=androidLintTool
junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.19.1=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
net.java.dev.jna:jna:5.6.0=androidLintTool
net.sf.kxml:kxml2:2.3.0=androidLintTool
org.apache.commons:commons-compress:1.27.1=androidLintTool
@@ -93,27 +96,27 @@ org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool
org.jetbrains.kotlin:kotlin-script-runtime:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
@@ -122,8 +125,8 @@ org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-plat
org.jetbrains.kotlin:kotlin-stdlib:2.2.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
+132
View File
@@ -1,3 +1,4 @@
import java.io.File
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -10,6 +11,8 @@ android {
ndkVersion = libs.versions.ndk.get()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -28,5 +31,134 @@ kotlin {
}
dependencies {
implementation("${libs.jna.get().module}:${libs.versions.jna.get()}@aar")
implementation(libs.annotation)
testImplementation(libs.junit)
androidTestImplementation(libs.android.test.runner)
}
val workspace = rootProject.layout.projectDirectory.dir("..").asFile
val cargoExecutable = providers.environmentVariable("CARGO").orElse(
providers.environmentVariable("PATH").map { path ->
path.split(File.pathSeparatorChar).map { File(it, "cargo") }
.firstOrNull { it.isFile && it.canExecute() }?.absolutePath
?: error("Cargo must be installed and in PATH")
}
)
val rustVersion = libs.versions.rust.get()
val ndkDirectory = androidComponents.sdkComponents.ndkDirectory
val sdkDirectory = androidComponents.sdkComponents.sdkDirectory
val rustInputs = files(
workspace.resolve("Cargo.toml"),
workspace.resolve("Cargo.lock"),
rootProject.file("rust-toolchain.toml"),
fileTree(workspace.resolve("crates")) { include("**/*.rs", "**/Cargo.toml", "**/uniffi.toml") }
)
val checkRustToolchain = tasks.register<Exec>("checkRustToolchain") {
workingDir = workspace
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--", "check-toolchain",
cargoExecutable.get(), rustVersion, ndkDirectory.get().asFile.absolutePath,
libs.versions.ndk.get(), libs.versions.cargoNdk.get()
)
}
androidComponents.onVariants { variant ->
val name = variant.name
val title = name.replaceFirstChar { it.uppercaseChar() }
val rawNative = layout.buildDirectory.dir("generated/native-raw/$name/jniLibs")
val nativeOutput = layout.buildDirectory.dir("generated/native/$name/jniLibs")
val rawBindings = layout.buildDirectory.dir("generated/bindings-raw/$name")
val bindings = layout.buildDirectory.dir("generated/bindings/$name")
val native = tasks.register<Exec>("build${title}Rust") {
dependsOn(checkRustToolchain)
workingDir = workspace
inputs.files(rustInputs)
inputs.property("rustVersion", rustVersion)
inputs.property("cargoNdk", libs.versions.cargoNdk.get())
inputs.property("ndkVersion", libs.versions.ndk.get())
inputs.property("minSdk", libs.versions.minSdk.get())
outputs.dir(rawNative)
environment("ANDROID_NDK_HOME", ndkDirectory.get().asFile.absolutePath)
environment("ANDROID_HOME", sdkDirectory.get().asFile.absolutePath)
environment(
"RUSTFLAGS",
"-D warnings -C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,-z,common-page-size=16384"
)
environment("CARGO_PROFILE_RELEASE_DEBUG", "2")
commandLine(
listOf(
cargoExecutable.get(), "+$rustVersion", "ndk",
"--target", "arm64-v8a",
"--target", "armeabi-v7a",
"--target", "x86_64",
"--platform", libs.versions.minSdk.get(),
"--output-dir", rawNative.get().asFile.absolutePath,
"build", "--locked", "--package", "ironstorage-android", "--lib"
) +
if (variant.buildType == "release") listOf("--release") else emptyList()
)
}
val audit = tasks.register<Exec>("package${title}Rust") {
dependsOn(native)
workingDir = workspace
inputs.dir(rawNative)
inputs.files(
fileTree(workspace.resolve("tools/android-build")) {
include("**/*.rs", "Cargo.toml")
}
)
outputs.dir(nativeOutput)
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--", "package-native",
rawNative.get().asFile.absolutePath, nativeOutput.get().asFile.absolutePath
)
}
val generate = tasks.register<Exec>("generate${title}RustBindings") {
dependsOn(audit)
workingDir = workspace
inputs.dir(nativeOutput)
inputs.file(rootProject.file("uniffi.toml"))
inputs.files(
fileTree(workspace.resolve("tools/android-build")) {
include("**/*.rs", "Cargo.toml")
}
)
outputs.dir(rawBindings)
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--bin", "ironstorage-uniffi-bindgen",
"--", "generate", "--library",
nativeOutput.get().file(
"arm64-v8a/libironstorage_android.so"
).asFile.absolutePath,
"--language", "kotlin", "--config",
rootProject.file(
"uniffi.toml"
).absolutePath,
"--out-dir", rawBindings.get().asFile.absolutePath, "--no-format"
)
}
val prepare = tasks.register<Exec>("prepare${title}RustBindings") {
dependsOn(generate)
workingDir = workspace
inputs.dir(rawBindings)
inputs.files(
fileTree(workspace.resolve("tools/android-build")) {
include("**/*.rs", "Cargo.toml")
}
)
outputs.dir(bindings)
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--", "prepare-bindings",
rawBindings.get().asFile.absolutePath, bindings.get().asFile.absolutePath
)
}
variant.sources.kotlin!!.addStaticSourceDirectory(bindings.get().asFile.absolutePath)
variant.sources.jniLibs!!.addStaticSourceDirectory(nativeOutput.get().asFile.absolutePath)
tasks.matching {
it.name == "pre${title}Build" || it.name == "compile${title}Kotlin"
}.configureEach { dependsOn(prepare) }
}
+7 -1
View File
@@ -1 +1,7 @@
# Native/generated keep rules are added with the UniFFI packaging in #92.
-keep class com.sun.jna.** { *; }
-keep class de.rfc1437.ironstorage.bridge.generated.** { *; }
# JNA includes optional desktop AWT helpers that Android never invokes.
-dontwarn java.awt.Component
-dontwarn java.awt.GraphicsEnvironment
-dontwarn java.awt.HeadlessException
-dontwarn java.awt.Window
+20 -13
View File
@@ -2,6 +2,12 @@
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
# To regenerate this file, run: ./gradlew :rust-bridge:dependencies --write-locks
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.test.services:storage:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.test:monitor:1.8.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.test:runner:1.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.tracing:tracing:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
ch.qos.logback:logback-classic:1.3.15=ktlint
ch.qos.logback:logback-core:1.3.15=ktlint
com.android.tools.analytics-library:protos:32.4.1=androidLintTool
@@ -73,8 +79,9 @@ io.github.oshai:kotlin-logging:5.1.0=ktlint,ktlintBaselineReporter,ktlintReporte
jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=androidLintTool
junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.19.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
net.java.dev.jna:jna:5.6.0=androidLintTool
net.sf.kxml:kxml2:2.3.0=androidLintTool
org.apache.commons:commons-compress:1.27.1=androidLintTool
@@ -91,29 +98,29 @@ org.checkerframework:checker-qual:3.43.0=androidLintTool
org.ec4j.core:ec4j-core:1.1.1=ktlint,ktlintBaselineReporter,ktlintRuleset
org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool
org.jetbrains.kotlin:kotlin-script-runtime:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
@@ -122,8 +129,8 @@ org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-plat
org.jetbrains.kotlin:kotlin-stdlib:2.2.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
@@ -0,0 +1,128 @@
package de.rfc1437.ironstorage.bridge
import android.content.ContextWrapper
import android.content.pm.ApplicationInfo
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.FfiConverterString
import de.rfc1437.ironstorage.bridge.generated.IntegrityCheckingUniffiLib
import de.rfc1437.ironstorage.bridge.generated.InternalException
import de.rfc1437.ironstorage.bridge.generated.MobileAuthenticationErrorKind
import de.rfc1437.ironstorage.bridge.generated.MobileAuthenticationFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileEntryCopy
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.MobileTab
import de.rfc1437.ironstorage.bridge.generated.UniffiRustCallStatus
import de.rfc1437.ironstorage.bridge.generated.uniffiTraitInterfaceCall
import java.io.File
import java.util.zip.ZipOutputStream
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Test
class NativeRuntimeTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
@Test
fun generatedBindingsCallEveryRustFixtureAndRedactDiagnostics() {
val runtime = NativeRuntime(context)
MobileShellState.entries.forEach { state ->
val shell = runtime.fixture(state)
assertEquals(MobileTab.entries, shell.pages.map { it.tab })
assertEquals(setOf(state), shell.pages.map { it.state }.toSet())
}
val secret = "NativeFixtureSecretMustNeverAppearInDiagnostics"
val copy = MobileEntryCopy(secret, 60uL)
assertEquals(secret, copy.value)
assertFalse(copy.toString().contains(secret))
val error = MobileAuthenticationFfiException.Failed(
MobileAuthenticationErrorKind.entries.first(),
secret,
secret
)
assertFalse(error.toString().contains(secret))
assertFalse(error.message.contains(secret))
assertFalse(InternalException(secret).toString().contains(secret))
val status = UniffiRustCallStatus()
uniffiTraitInterfaceCall<Unit>(status, { throw IllegalStateException(secret) }, {})
assertEquals(
"Native callback failed ([REDACTED])",
FfiConverterString.lift(status.error_buf)
)
}
@Test
fun wrongNativeContractAndChecksumFailWithTypedRedactedErrors() {
NativeRuntime(context).fixture(MobileShellState.EMPTY)
val version = IntegrityCheckingUniffiLib
.ffi_ironstorage_apple_uniffi_contract_version().toLong()
val checksum = IntegrityCheckingUniffiLib
.uniffi_ironstorage_apple_checksum_func_mobile_shell_fixture().toLong() and 0xffffL
NativeBindingChecks.requireMatch(-27278, 38258, NativeFailureKind.ChecksumMismatch)
for ((actual, kind) in listOf(
version to NativeFailureKind.ContractMismatch,
checksum to NativeFailureKind.ChecksumMismatch
)) {
val failure = assertThrows(NativeBindingMismatchException::class.java) {
NativeBindingChecks.requireMatch(actual, actual + 1, kind)
}
assertEquals(kind, failure.kind)
assertEquals("Rust bindings do not match the packaged native library", failure.message)
}
}
@Test
fun mainThreadAndUnreadablePackagesFailWithTypedErrors() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
val failure = assertThrows(NativeBridgeException::class.java) {
NativeRuntime(context).fixture(MobileShellState.EMPTY)
}
assertEquals(NativeFailureKind.MainThreadCall, failure.kind)
}
val packageFile = File.createTempFile("native-corrupt-package", ".apk", context.cacheDir)
try {
val info = ApplicationInfo(context.applicationInfo).apply {
sourceDir = packageFile.absolutePath
splitSourceDirs = null
nativeLibraryDir = File(context.cacheDir, "missing-native-fixture").absolutePath
}
val invalid = object : ContextWrapper(context) {
override fun getApplicationContext() = this
override fun getApplicationInfo() = info
}
val failure = assertThrows(NativeBridgeException::class.java) {
NativeRuntime(invalid).fixture(MobileShellState.EMPTY)
}
assertEquals(NativeFailureKind.LoadFailure, failure.kind)
} finally {
packageFile.delete()
}
}
@Test
fun missingLibraryAndUnsupportedAbiFailBeforeEnteringRust() {
val abiFailure = assertThrows(NativeBridgeException::class.java) {
NativeRuntime.supportedAbi(listOf("unsupported-fixture-abi"))
}
assertEquals(NativeFailureKind.UnsupportedAbi, abiFailure.kind)
val packageFile = File.createTempFile("native-empty-package", ".apk", context.cacheDir)
try {
ZipOutputStream(packageFile.outputStream()).close()
val info = ApplicationInfo(context.applicationInfo).apply {
sourceDir = packageFile.absolutePath
splitSourceDirs = null
nativeLibraryDir = File(context.cacheDir, "missing-native-fixture").absolutePath
}
val missing = object : ContextWrapper(context) {
override fun getApplicationContext() = this
override fun getApplicationInfo() = info
}
val failure = assertThrows(NativeBridgeException::class.java) {
NativeRuntime(missing).fixture(MobileShellState.EMPTY)
}
assertEquals(NativeFailureKind.MissingLibrary, failure.kind)
} finally {
packageFile.delete()
}
}
}
@@ -0,0 +1,9 @@
package de.rfc1437.ironstorage.bridge
internal object NativeBindingChecks {
fun requireMatch(actual: Long, expected: Long, kind: NativeFailureKind) {
// UniFFI checksums are u16; JNA's Int return can include unspecified upper bits.
val value = if (kind == NativeFailureKind.ChecksumMismatch) actual and 0xffffL else actual
if (value != expected) throw NativeBindingMismatchException(kind)
}
}
@@ -0,0 +1,17 @@
package de.rfc1437.ironstorage.bridge
enum class NativeFailureKind {
UnsupportedAbi,
MissingLibrary,
ContractMismatch,
ChecksumMismatch,
LoadFailure,
RustFailure,
MainThreadCall
}
class NativeBindingMismatchException(val kind: NativeFailureKind) :
LinkageError("Rust bindings do not match the packaged native library")
class NativeBridgeException(val kind: NativeFailureKind) :
Exception("Native Rust runtime unavailable: $kind")
@@ -0,0 +1,80 @@
package de.rfc1437.ironstorage.bridge
import android.content.Context
import android.os.Build
import android.os.Looper
import de.rfc1437.ironstorage.bridge.generated.MobileShell
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.mobileShellFixture
import de.rfc1437.ironstorage.bridge.generated.uniffiEnsureInitialized
import java.io.File
import java.io.IOException
import java.util.concurrent.CancellationException
import java.util.zip.ZipFile
/** OS library loading only. Callers schedule all native calls off the main thread. */
class NativeRuntime(context: Context) {
private val applicationInfo = context.applicationContext.applicationInfo
private val lock = Any()
private var initialized = false
private var initializationFailure: NativeFailureKind? = null
fun fixture(state: MobileShellState): MobileShell = call {
initialize()
mobileShellFixture(state)
}
private fun initialize() = synchronized(lock) {
initializationFailure?.let { throw NativeBridgeException(it) }
if (!initialized) {
val abi = supportedAbi(Build.SUPPORTED_ABIS.toList())
val extracted = File(applicationInfo.nativeLibraryDir, LIBRARY_FILE).isFile
val packages =
listOf(applicationInfo.sourceDir) + applicationInfo.splitSourceDirs.orEmpty()
val packaged = extracted || packages.any { path ->
ZipFile(path).use { it.getEntry("lib/$abi/$LIBRARY_FILE") != null }
}
if (!packaged) throw NativeBridgeException(NativeFailureKind.MissingLibrary)
System.loadLibrary(LIBRARY)
uniffiEnsureInitialized()
initialized = true
}
}
private fun <T> call(operation: () -> T): T {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw NativeBridgeException(NativeFailureKind.MainThreadCall)
}
try {
return operation()
} catch (error: NativeBridgeException) {
fail(error.kind)
} catch (error: CancellationException) {
throw error
} catch (error: NativeBindingMismatchException) {
fail(error.kind)
} catch (_: IOException) {
fail(NativeFailureKind.LoadFailure)
} catch (_: LinkageError) {
fail(NativeFailureKind.LoadFailure)
} catch (_: Exception) {
fail(NativeFailureKind.RustFailure)
}
}
private fun fail(kind: NativeFailureKind): Nothing {
synchronized(lock) {
if (!initialized) initializationFailure = kind
}
throw NativeBridgeException(kind)
}
companion object {
private const val LIBRARY = "ironstorage_android"
private const val LIBRARY_FILE = "libironstorage_android.so"
internal fun supportedAbi(deviceAbis: List<String>): String =
deviceAbis.firstOrNull { it in setOf("arm64-v8a", "armeabi-v7a", "x86_64") }
?: throw NativeBridgeException(NativeFailureKind.UnsupportedAbi)
}
}
+4
View File
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.97.1"
profile = "minimal"
targets = ["aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"]
+6
View File
@@ -0,0 +1,6 @@
[defaults.bindings.kotlin]
package_name = "de.rfc1437.ironstorage.bridge.generated"
cdylib_name = "ironstorage_android"
android = true
generate_immutable_records = true
omit_checksums = false
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "ironstorage-android"
description = "Android native library reusing the shared mobile UniFFI exports"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
publish = false
[lib]
name = "ironstorage_android"
crate-type = ["lib", "cdylib"]
[dependencies]
ironstorage-apple = { path = "../apple" }
+6
View File
@@ -0,0 +1,6 @@
#![forbid(unsafe_code)]
//! Android packaging of the existing mobile UniFFI component.
//! The shared component owns every conversion; no interface is copied here.
ironstorage_apple::uniffi_reexport_scaffolding!();
+14 -1
View File
@@ -6,6 +6,12 @@
//! Credential Manager, and Secret Service boundaries. Operations are serialized
//! by the parent `SecretStore`, as required by the Windows adapter contract.
#[cfg(any(
target_os = "ios",
target_os = "linux",
target_os = "macos",
target_os = "windows"
))]
use std::sync::Arc;
#[cfg(any(target_os = "ios", target_os = "macos"))]
@@ -29,7 +35,14 @@ use security_framework::{
},
};
use keyring_core::{CredentialStore, Entry};
#[cfg(any(
target_os = "ios",
target_os = "linux",
target_os = "macos",
target_os = "windows"
))]
use keyring_core::CredentialStore;
use keyring_core::Entry;
use super::{SecretLocator, SecretProtection, SecretStoreBackend, SecretStoreError};
use crate::repository::SecretBytes;
+6 -4
View File
@@ -205,11 +205,13 @@ Every issue runs the AGENTS.md Rust gates before commit. Android changes also
run build/lint/unit/instrumented checks appropriate to the issue and computer-use
validation on the existing emulator. Update by installation over the existing
app, never uninstall/reset. If unlocking needs an unavailable user, label the
issue untested and document exactly what could not be checked. Emulator evidence
cannot satisfy physical biometric/camera/Keystore/autofill or Play acceptance.
issue untested and document exactly what could not be checked. The user clarified on 2026-10-08 that validation for every milestone issue is
emulator-only; no physical devices are included in testing. Record emulator/API
limitations explicitly and use untested for an unavailable required emulator
check. This does not change production security behavior or claim Play approval.
Apple changes require XcodeGen and affected simulator builds and UI checks;
never re-enroll an already enrolled iPhone simulator in ordinary validation.
#94 owns reproducible CI and test boundaries, #105 accessibility/performance,
#111 the complete architecture/security/parity audit, and #112 physical production
install/update/Play release evidence. No release approval is implied by this ADR.
#111 the complete architecture/security/parity audit, and #112 emulator
install/update evidence and Play release preparation. No release approval is implied by this ADR.
+2 -2
View File
@@ -81,8 +81,8 @@ milestone. This list gives ownership, not permission to skip acceptance criteria
| [#108 Move/copy/delete](https://git.rfc1437.de/hugo/IronStorage/issues/108) | storage mobile_mutation/mutation plus Compose features | Rust plans/tokens/overwrite/reencryption, destination and confirmation UI |
| [#109 Credential Manager/Autofill](https://git.rfc1437.de/hugo/IronStorage/issues/109) | storage identity/permission/credential contract plus OS services | AndroidX provider and framework AutofillService, explicit selection/lease before secrets |
| [#110 TOTP catalog/discovery/search](https://git.rfc1437.de/hugo/IronStorage/issues/110) | storage mobile_totp plus Compose features | Existing hash/path/is_totp catalog, Rust progress/checkpoints/filter, no Watch |
| [#111 Parity/security/reliability audit](https://git.rfc1437.de/hugo/IronStorage/issues/111) | All above plus independent acceptance review | Completed matrix evidence, Rust/Android release gates and physical-device threat tests |
| [#112 Google Play distribution](https://git.rfc1437.de/hugo/IronStorage/issues/112) | Release tooling/listing and production app | Protected signing/Play AAB, audited manifest/licenses, physical clean install and update-preservation tests |
| [#111 Parity/security/reliability audit](https://git.rfc1437.de/hugo/IronStorage/issues/111) | All above plus independent acceptance review | Completed matrix evidence, Rust/Android release gates and emulator threat tests |
| [#112 Google Play distribution](https://git.rfc1437.de/hugo/IronStorage/issues/112) | Release tooling/listing and production app | Protected signing/Play AAB, audited manifest/licenses, emulator install and update-preservation tests |
#87 is the closed umbrella issue, not an open implementation candidate. Verify
live issues after each closure; newly created bug/critical issues may change the
+9 -1
View File
@@ -1,6 +1,7 @@
[package]
name = "ironstorage-android-build"
description = "Android fixture generation and architecture boundary checks"
description = "Android native packaging, binding generation and architecture gates"
default-run = "ironstorage-android-build"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -11,3 +12,10 @@ publish = false
[dependencies]
ironstorage.workspace = true
regex.workspace = true
uniffi = { workspace = true, features = ["cli"] }
object = { version = "0.36.7", default-features = false, features = ["read", "elf"] }
zip = { version = "2.4.2", default-features = false, features = ["deflate"] }
[[bin]]
name = "ironstorage-uniffi-bindgen"
path = "src/bindgen.rs"
+5
View File
@@ -0,0 +1,5 @@
#![forbid(unsafe_code)]
fn main() {
uniffi::uniffi_bindgen_main();
}
+146
View File
@@ -0,0 +1,146 @@
//! Narrow, version-checked hardening of UniFFI 0.32's Kotlin diagnostics.
//! Records remain immutable data classes with unchanged wire layouts/checksums.
use std::{error::Error, fs, path::Path};
use regex::Regex;
pub fn prepare(input: &Path, output: &Path) -> Result<(), Box<dyn Error>> {
if !input.is_dir() || input.is_symlink() || output.is_symlink() {
return Err("binding input and output must be distinct directories".into());
}
let input = input.canonicalize()?;
let destination = if output.exists() {
output.canonicalize()?
} else {
output
.parent()
.ok_or("output needs a parent")?
.canonicalize()?
.join(output.file_name().ok_or("output needs a name")?)
};
if input.starts_with(&destination) || destination.starts_with(&input) {
return Err("binding input/output directories must not overlap".into());
}
if output.exists() {
fs::remove_dir_all(output)?;
}
copy(&input, output)
}
fn copy(input: &Path, output: &Path) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(output)?;
let mut found = false;
for entry in fs::read_dir(input)? {
let entry = entry?;
let destination = output.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy(&entry.path(), &destination)?;
found = true;
} else if entry.file_type()?.is_file()
&& entry.path().extension().is_some_and(|ext| ext == "kt")
{
let source = fs::read_to_string(entry.path())?;
fs::write(destination, harden_source(&source)?)?;
found = true;
} else {
return Err("unexpected file in generated Kotlin bindings".into());
}
}
if !found {
return Err("UniFFI produced no Kotlin bindings".into());
}
Ok(())
}
fn harden_source(source: &str) -> Result<String, Box<dyn Error>> {
let internal = "class InternalException(message: String) : kotlin.Exception(message)";
let callback = "val err = try { e.stackTraceToString() } catch(_: Throwable) { \"\" }";
if !source.contains(internal) || source.matches(callback).count() != 2 {
return Err("UniFFI unexpected-error template changed; review secret redaction".into());
}
let source = source.replace(internal, "class InternalException(@Suppress(\"UNUSED_PARAMETER\") message: String) : kotlin.Exception(\"Native Rust failure ([REDACTED])\")")
.replace(callback, "val err = \"Native callback failed ([REDACTED])\"");
let records = Regex::new(r"(?ms)^data class (\w+) \((.*?)\n\)([^\{]*)\{\n")?;
if !records.is_match(&source) {
return Err("UniFFI record template changed; review diagnostic hardening".into());
}
let source = records.replace_all(&source, |captures: &regex::Captures<'_>| {
format!(
"{} override fun toString(): String = \"{}([REDACTED])\"\n",
&captures[0], &captures[1]
)
});
let errors = Regex::new(r#"(?m)(override val message\n\s*)get\(\) = ".*"$"#)?;
// Kotlin Exception.message is reserved; keep the Rust field/wire checksum unchanged.
let source = source
.replace(
"val `message`: kotlin.String",
"val `rustMessage`: kotlin.String",
)
.replace("value.`message`", "value.`rustMessage`");
let source = errors.replace_all(
&source,
"${1}get() = \"Rust operation failed ([REDACTED])\"",
);
let contract = "throw RuntimeException(\"UniFFI contract version mismatch: try cleaning and rebuilding your project\")";
let checksum = "throw RuntimeException(\"UniFFI API checksum mismatch: try cleaning and rebuilding your project\")";
if !source.contains(contract) || !source.contains(checksum) {
return Err(
"UniFFI integrity template changed; checksum checks must remain enabled".into(),
);
}
let checksum_guard = Regex::new(
r#"if \(lib\.(\w+)\(\) != (-?\d+)\) \{\s*throw RuntimeException\("UniFFI API checksum mismatch: try cleaning and rebuilding your project"\)\s*\}"#,
)?;
let contract_guard = Regex::new(
r#"if \(bindings_contract_version != scaffolding_contract_version\) \{\s*throw RuntimeException\("UniFFI contract version mismatch: try cleaning and rebuilding your project"\)\s*\}"#,
)?;
if !checksum_guard.is_match(&source) || !contract_guard.is_match(&source) {
return Err(
"UniFFI integrity guard changed; refusing to generate unchecked bindings".into(),
);
}
let output = checksum_guard.replace_all(&source, "de.rfc1437.ironstorage.bridge.NativeBindingChecks.requireMatch(lib.${1}().toLong(), ${2}.toLong(), de.rfc1437.ironstorage.bridge.NativeFailureKind.ChecksumMismatch)");
let output = contract_guard.replace_all(&output, "de.rfc1437.ironstorage.bridge.NativeBindingChecks.requireMatch(scaffolding_contract_version.toLong(), bindings_contract_version.toLong(), de.rfc1437.ironstorage.bridge.NativeFailureKind.ContractMismatch)").into_owned();
if output.contains(contract) || output.contains(checksum) {
return Err("UniFFI integrity guard was not translated completely".into());
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
const SOURCE: &str = "data class Secret (\n val value: String\n)\n{\n companion object\n}\nclass Failure {\n override val message\n get() = \"value=${value}\"\n}\nif (bindings_contract_version != scaffolding_contract_version) { throw RuntimeException(\"UniFFI contract version mismatch: try cleaning and rebuilding your project\") }\nif (lib.checksum() != 1234) { throw RuntimeException(\"UniFFI API checksum mismatch: try cleaning and rebuilding your project\") }\nclass InternalException(message: String) : kotlin.Exception(message)\nval err = try { e.stackTraceToString() } catch(_: Throwable) { \"\" }\nval err = try { e.stackTraceToString() } catch(_: Throwable) { \"\" }\n";
#[test]
fn redacts_record_and_error_diagnostics_without_changing_fields_or_checksums() {
let hardened = harden_source(SOURCE).unwrap();
assert!(hardened.contains("val value: String"));
assert!(hardened.contains("Secret([REDACTED])"));
assert!(!hardened.contains("get() = \"value="));
assert!(hardened.contains("requireMatch(lib.checksum().toLong(), 1234.toLong()"));
assert!(hardened.contains("NativeFailureKind.ChecksumMismatch"));
assert!(hardened.contains("NativeFailureKind.ContractMismatch"));
assert!(!hardened.contains("stackTraceToString"));
assert!(!hardened.contains("kotlin.Exception(message)"));
}
#[test]
fn fails_closed_if_upstream_templates_or_integrity_checks_change() {
assert!(harden_source("data class Secret(val value: String)").is_err());
assert!(harden_source(&SOURCE.replace("UniFFI API checksum mismatch", "changed")).is_err());
}
#[test]
fn preserves_rust_message_fields_without_colliding_with_kotlin_exception_message() {
let source = SOURCE.replace("val value: String", "val `message`: kotlin.String")
+ "FfiConverterString.write(value.`message`, buf)";
let hardened = harden_source(&source).unwrap();
assert!(hardened.contains("val `rustMessage`: kotlin.String"));
assert!(hardened.contains("FfiConverterString.write(value.`rustMessage`, buf)"));
assert!(hardened.contains("override val message"));
}
}
+34 -1
View File
@@ -3,6 +3,10 @@
use ironstorage::mobile::{MobileShell, MobileShellState};
use std::{env, error::Error, fs, path::Path};
mod bindings;
mod native;
mod toolchain;
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<_> = env::args_os().collect();
match args.as_slice() {
@@ -12,9 +16,38 @@ fn main() -> Result<(), Box<dyn Error>> {
fs::write(path, fixtures())?;
}
[_, command, path] if command == "check" => check(Path::new(path))?,
[_, command, input, output] if command == "prepare-bindings" => {
bindings::prepare(Path::new(input), Path::new(output))?;
}
[_, command, input, output] if command == "package-native" => {
native::package_directory(Path::new(input), Path::new(output))?;
}
[_, command, path, abi] if command == "audit-elf" => {
native::audit_elf(&fs::read(path)?, abi.to_str().ok_or("invalid ABI")?)?;
}
[_, command, path, abi] if command == "audit-apk-split" => {
native::audit_split(Path::new(path), abi.to_str().ok_or("invalid ABI")?)?;
}
[_, command, paths @ ..] if command == "audit-package" && !paths.is_empty() => {
for path in paths {
native::audit_package(Path::new(path))?;
}
}
[_, command, directory] if command == "audit-native" => {
native::audit_directory(Path::new(directory))?
}
[_, command, cargo, rust, ndk, ndk_version, cargo_ndk] if command == "check-toolchain" => {
toolchain::check(
Path::new(cargo),
rust.to_str().ok_or("invalid Rust version")?,
Path::new(ndk),
ndk_version.to_str().ok_or("invalid NDK version")?,
cargo_ndk.to_str().ok_or("invalid cargo-ndk version")?,
)?;
}
_ => {
return Err(
"usage: ironstorage-android-build <fixtures output.kt | check android-dir>".into(),
"usage: ironstorage-android-build <fixtures output.kt | check android-dir | prepare-bindings input output | package-native input output | audit-elf path abi | audit-native jniLibs | audit-apk-split apk abi | audit-package apk-or-aab | check-toolchain cargo rust ndk ndk-version cargo-ndk-version>".into(),
);
}
}
+208
View File
@@ -0,0 +1,208 @@
//! Verify every packaged native library, including JNA, against the ABI/page contract.
use std::{collections::BTreeMap, error::Error, fs::File, io::Read, path::Path};
use object::{Architecture, BinaryFormat, Object, ObjectSection, ObjectSegment, ObjectSymbol};
const PAGE_SIZE: u64 = 16_384;
const ABIS: [&str; 3] = ["arm64-v8a", "armeabi-v7a", "x86_64"];
pub fn package_directory(input: &Path, output: &Path) -> Result<(), Box<dyn Error>> {
audit_directory(input)?;
let input = input.canonicalize()?;
let output = if output.exists() {
if output.is_symlink() {
return Err("native output must not be a symlink".into());
}
output.canonicalize()?
} else {
output
.parent()
.ok_or("output needs a parent")?
.canonicalize()?
.join(output.file_name().ok_or("output needs a name")?)
};
if input.starts_with(&output) || output.starts_with(&input) {
return Err("native input/output must not overlap".into());
}
if output.exists() {
std::fs::remove_dir_all(&output)?;
}
for abi in ABIS {
std::fs::create_dir_all(output.join(abi))?;
// cargo-ndk also emits dependency cdylibs; ship only the Android boundary.
std::fs::copy(
input.join(abi).join("libironstorage_android.so"),
output.join(abi).join("libironstorage_android.so"),
)?;
}
Ok(())
}
pub fn audit_directory(directory: &Path) -> Result<(), Box<dyn Error>> {
let mut expected = None;
for abi in ABIS {
let bytes = std::fs::read(directory.join(abi).join("libironstorage_android.so"))?;
audit_elf(&bytes, abi)?;
let library = object::File::parse(bytes.as_slice())?;
let mut metadata = BTreeMap::new();
for symbol in library.symbols() {
let name = symbol.name()?;
if name.starts_with("UNIFFI_META_") {
let section = library.section_by_index(
symbol
.section_index()
.ok_or("UniFFI metadata has no section")?,
)?;
let value = section
.data_range(symbol.address(), symbol.size())?
.ok_or("UniFFI metadata is outside its section")?;
metadata.insert(name.to_owned(), value.to_vec());
}
}
if metadata.is_empty() {
return Err("native library has no UniFFI metadata".into());
}
if let Some(expected) = &expected {
if expected != &metadata {
return Err("UniFFI metadata differs across Android ABIs".into());
}
} else {
expected = Some(metadata);
}
}
println!("Verified three native ABIs, 16 KB ELF alignment and identical UniFFI metadata");
Ok(())
}
pub fn audit_elf(bytes: &[u8], abi: &str) -> Result<(), Box<dyn Error>> {
let expected = match abi {
"arm64-v8a" => Architecture::Aarch64,
"armeabi-v7a" => Architecture::Arm,
"x86_64" => Architecture::X86_64,
_ => return Err("unapproved native ABI".into()),
};
let library = object::File::parse(bytes)?;
if library.format() != BinaryFormat::Elf || library.architecture() != expected {
return Err("native library architecture does not match its ABI directory".into());
}
let mut segments = 0;
for segment in library.segments() {
let (offset, _) = segment.file_range();
if segment.align() < PAGE_SIZE || segment.address() % PAGE_SIZE != offset % PAGE_SIZE {
return Err("native ELF LOAD segment is not compatible with 16 KB pages".into());
}
segments += 1;
}
if segments == 0 {
return Err("native ELF has no loadable segments".into());
}
Ok(())
}
pub fn audit_package(path: &Path) -> Result<(), Box<dyn Error>> {
audit_package_abis(path, &ABIS)
}
pub fn audit_split(path: &Path, abi: &str) -> Result<(), Box<dyn Error>> {
if !ABIS.contains(&abi) || !path.extension().is_some_and(|ext| ext == "apk") {
return Err("expected an APK split for an approved ABI".into());
}
audit_package_abis(path, &[abi])
}
fn audit_package_abis(path: &Path, abis: &[&str]) -> Result<(), Box<dyn Error>> {
let apk = path.extension().is_some_and(|ext| ext == "apk");
if !apk && !path.extension().is_some_and(|ext| ext == "aab") {
return Err("expected an APK or AAB".into());
}
let mut archive = zip::ZipArchive::new(File::open(path)?)?;
let mut libraries = BTreeMap::<String, Vec<String>>::new();
for index in 0..archive.len() {
let mut file = archive.by_index(index)?;
let name = file.name().to_owned();
let native = name.strip_prefix(if apk { "lib/" } else { "base/lib/" });
let Some((abi, library)) = native.and_then(|name| name.split_once('/')) else {
continue;
};
if !library.ends_with(".so") {
continue;
}
if library == "libironstorage_apple.so" {
return Err("Android package includes the redundant Apple boundary library".into());
}
if !abis.contains(&abi) || library.contains('/') {
return Err("package contains an unapproved native library path".into());
}
if apk
&& (file.compression() != zip::CompressionMethod::Stored
|| file.data_start() % PAGE_SIZE != 0)
{
return Err(format!("{name}: APK native entry is not stored/aligned to 16 KB").into());
}
if file.size() > 512 * 1024 * 1024 {
return Err("native library exceeds the package audit bound".into());
}
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
audit_elf(&bytes, abi).map_err(|error| format!("{name}: {error}"))?;
libraries
.entry(abi.to_owned())
.or_default()
.push(library.to_owned());
}
for abi in abis {
let found = libraries
.get(*abi)
.ok_or("package is missing a supported ABI")?;
for required in ["libironstorage_android.so", "libjnidispatch.so"] {
if !found.iter().any(|name| name == required) {
return Err(format!("{abi}: missing {required}").into());
}
}
}
println!(
"Verified all packaged native libraries for {abis:?} in {}",
path.display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn elf(alignment: u64, address: u64) -> Vec<u8> {
let mut bytes = vec![0; 120];
bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
bytes[16..18].copy_from_slice(&3_u16.to_le_bytes());
bytes[18..20].copy_from_slice(&183_u16.to_le_bytes());
bytes[20..24].copy_from_slice(&1_u32.to_le_bytes());
bytes[32..40].copy_from_slice(&64_u64.to_le_bytes());
bytes[52..54].copy_from_slice(&64_u16.to_le_bytes());
bytes[54..56].copy_from_slice(&56_u16.to_le_bytes());
bytes[56..58].copy_from_slice(&1_u16.to_le_bytes());
bytes[64..68].copy_from_slice(&1_u32.to_le_bytes());
bytes[68..72].copy_from_slice(&4_u32.to_le_bytes());
bytes[80..88].copy_from_slice(&address.to_le_bytes());
bytes[96..104].copy_from_slice(&120_u64.to_le_bytes());
bytes[104..112].copy_from_slice(&120_u64.to_le_bytes());
bytes[112..120].copy_from_slice(&alignment.to_le_bytes());
bytes
}
#[test]
fn checks_real_load_alignment_congruence_and_directory_abi() {
assert!(audit_elf(&elf(PAGE_SIZE, 0), "arm64-v8a").is_ok());
assert!(audit_elf(&elf(4096, 0), "arm64-v8a").is_err());
assert!(audit_elf(&elf(PAGE_SIZE, 4), "arm64-v8a").is_err());
assert!(audit_elf(&elf(PAGE_SIZE, 0), "x86_64").is_err());
}
#[test]
fn rejects_non_elf_unapproved_abi_and_truncated_headers() {
assert!(audit_elf(b"not an ELF", "arm64-v8a").is_err());
assert!(audit_elf(b"\x7fELF", "x86_64").is_err());
assert!(audit_elf(&[], "mips").is_err());
}
}
+61
View File
@@ -0,0 +1,61 @@
//! Build-time tools only; application crates never launch these processes.
#![allow(
clippy::disallowed_types,
reason = "build-only verification of Cargo/cargo-ndk, never linked into the application"
)]
use std::{error::Error, fs, path::Path, process::Command};
pub fn check(
cargo: &Path,
rust: &str,
ndk: &Path,
ndk_version: &str,
cargo_ndk: &str,
) -> Result<(), Box<dyn Error>> {
version(
cargo,
&[&format!("+{rust}"), "--version"],
&format!("cargo {rust}"),
)?;
version(
cargo,
&[&format!("+{rust}"), "ndk", "--version"],
&format!("cargo-ndk {cargo_ndk}"),
)?;
let properties = fs::read_to_string(ndk.join("source.properties"))?;
if !properties
.lines()
.any(|line| line.trim() == format!("Pkg.Revision = {ndk_version}"))
{
return Err("Android NDK revision does not match the version catalog".into());
}
Ok(())
}
fn version(command: &Path, args: &[&str], expected: &str) -> Result<(), Box<dyn Error>> {
let result = Command::new(command).args(args).output()?;
let value = std::str::from_utf8(&result.stdout)?;
if !result.status.success() || !matches_version(value, expected) {
return Err(format!("build tool must report {expected}").into());
}
Ok(())
}
fn matches_version(value: &str, expected: &str) -> bool {
value
.trim()
.strip_prefix(expected)
.is_some_and(|suffix| suffix.is_empty() || suffix.starts_with(' '))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_nearby_and_failed_version_output() {
assert!(matches_version("cargo 1.97.1 (abc)\n", "cargo 1.97.1"));
assert!(matches_version("cargo-ndk 4.1.2\n", "cargo-ndk 4.1.2"));
assert!(!matches_version("cargo-ndk 4.1.20", "cargo-ndk 4.1.2"));
assert!(!matches_version("", "cargo-ndk 4.1.2"));
}
}