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:
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 {