app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit b25e26c7c54b6f1d22faec714876c9241d669e03
parent ca30868e90ad52e27e3ada236d2eb4ec68b3807f
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 00:07:44 +0000

android: make offline geocoder staging idempotent

- stage the android geocoder database under an app-revision path so existing staged copies can be reused
- return typed android geocoder staging errors from the app bridge without rewriting the staged asset every launch
- narrow the android crate unsafe allowance to the exported entrypoint and specific jni conversion helpers
- keep the offline geocoder init non-blocking while preserving the existing android test and host build lanes

Diffstat:
Mcrates/android/Cargo.toml | 3---
Mcrates/android/src/lib.rs | 7+++----
Mcrates/android/src/offline_geocoder.rs | 2++
Mcrates/android/src/security.rs | 2++
Mplatforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt | 20+++++++++++++++++++-
5 files changed, 26 insertions(+), 8 deletions(-)

diff --git a/crates/android/Cargo.toml b/crates/android/Cargo.toml @@ -29,6 +29,3 @@ jni.workspace = true ndk-context.workspace = true wgpu = { workspace = true, features = ["vulkan", "gles", "wgsl"] } winit.workspace = true - -[lints.rust] -unsafe_code = { level = "allow", priority = 1 } diff --git a/crates/android/src/lib.rs b/crates/android/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(unsafe_code)] - #[cfg(target_os = "android")] use android_logger::Config; #[cfg(target_os = "android")] @@ -11,7 +9,7 @@ use radroots_app_core::{APP_NAME, RadrootsApp}; #[cfg(any(target_os = "android", test))] use radroots_app_core::{ HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState, - RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, SetupActionState, + RadrootsOfflineGeocoderState, SetupActionState, }; #[cfg(any(target_os = "android", test))] use radroots_identity::RadrootsIdentity; @@ -205,7 +203,7 @@ impl AndroidBackend { #[cfg(not(target_os = "android"))] let offline_geocoder = offline_geocoder::AndroidOfflineGeocoder::from_state( RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + radroots_app_core::RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, "android offline geocoder initialization is only wired on android targets", ), ); @@ -433,6 +431,7 @@ fn run_android_app(android_app: AndroidApp) -> Result<(), String> { #[cfg(target_os = "android")] #[allow(improper_ctypes_definitions)] +#[allow(unsafe_code)] #[unsafe(no_mangle)] pub extern "C" fn android_main(android_app: AndroidApp) { if let Err(err) = run_android_app(android_app) { diff --git a/crates/android/src/offline_geocoder.rs b/crates/android/src/offline_geocoder.rs @@ -168,6 +168,7 @@ fn stage_offline_geocoder_asset() -> Result<String, (RadrootsOfflineGeocoderUnav } #[cfg(target_os = "android")] +#[allow(unsafe_code)] fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> { let context = ndk_context::android_context(); // SAFETY: ndk_context is initialized by the Android runtime before this code runs and @@ -176,6 +177,7 @@ fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> { } #[cfg(target_os = "android")] +#[allow(unsafe_code)] fn bridge_class<'local>( env: &mut JNIEnv<'local>, ) -> Result<JClass<'local>, RadrootsNostrAccountsError> { diff --git a/crates/android/src/security.rs b/crates/android/src/security.rs @@ -402,6 +402,7 @@ pub(crate) fn resolve_nostr_storage_root() -> Result<PathBuf, RadrootsNostrAccou } #[cfg(target_os = "android")] +#[allow(unsafe_code)] fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> { let context = ndk_context::android_context(); // SAFETY: ndk_context is initialized by the Android runtime before this code runs and @@ -410,6 +411,7 @@ fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> { } #[cfg(target_os = "android")] +#[allow(unsafe_code)] fn bridge_class<'local>( env: &mut JNIEnv<'local>, ) -> Result<JClass<'local>, RadrootsNostrAccountsError> { diff --git a/platforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt b/platforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt @@ -33,7 +33,14 @@ object RadRootsAndroidAppBridge { GEOCODER_ERROR_KIND_INTERNAL_ERROR, "android app bridge is not initialized", ) - val targetDir = File(context.noBackupFilesDir, "RadRoots/app/android/geocoder") + val targetDir = try { + stagedGeocoderDirectory(context) + } catch (source: Exception) { + return fail( + GEOCODER_ERROR_KIND_INTERNAL_ERROR, + "failed to resolve android geocoder revision: ${source.message ?: source.javaClass.simpleName}", + ) + } if (!targetDir.exists() && !targetDir.mkdirs()) { return fail( GEOCODER_ERROR_KIND_INITIALIZATION_FAILED, @@ -42,6 +49,11 @@ object RadRootsAndroidAppBridge { } val targetFile = File(targetDir, GEOCODER_FILE_NAME) + if (targetFile.isFile) { + lastErrorMessage = null + lastErrorKind = 0 + return targetFile.absolutePath + } return try { context.assets.open(GEOCODER_ASSET_PATH).use { input -> targetFile.outputStream().use { output -> @@ -64,6 +76,12 @@ object RadRootsAndroidAppBridge { } } + private fun stagedGeocoderDirectory(context: Context): File { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + val revision = packageInfo.lastUpdateTime.toString() + return File(context.noBackupFilesDir, "RadRoots/app/android/geocoder/$revision") + } + @JvmStatic @Synchronized fun takeLastErrorKind(): Int {