app

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

commit 691342faa94152217cab9999f9b6315359365cc7
parent d388f35614a1e5b4e92a8858464a8a22fe46f561
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 00:01:40 +0000

core: type offline geocoder unavailable states

- replace string-matched offline geocoder failure classification with typed unavailable kinds
- update desktop ios android and web backends to construct the shared unavailable state explicitly
- harden the shared ui to show stable technical details while keeping raw debug text debug-build only
- extend the android app bridge to report typed geocoder staging errors instead of inferring them from messages

Diffstat:
Mcrates/android/src/lib.rs | 12+++++-------
Mcrates/android/src/offline_geocoder.rs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mcrates/core/src/lib.rs | 50++++++++++++++++++++++++++++++++++----------------
Mcrates/core/src/offline_geocoder.rs | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/desktop/src/main.rs | 25++++++++++++-------------
Mcrates/desktop/src/offline_geocoder.rs | 70++++++++++++++++++++++++++++++++++++++++++----------------------------
Mcrates/ios/src/lib.rs | 9++++-----
Mcrates/ios/src/offline_geocoder.rs | 70+++++++++++++++++++++++++++++++++++++++++-----------------------------
Mcrates/web/src/lib.rs | 13++++++-------
Mplatforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt | 39++++++++++++++++++++++++++++++++++-----
10 files changed, 328 insertions(+), 143 deletions(-)

diff --git a/crates/android/src/lib.rs b/crates/android/src/lib.rs @@ -11,7 +11,7 @@ use radroots_app_core::{APP_NAME, RadrootsApp}; #[cfg(any(target_os = "android", test))] use radroots_app_core::{ HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState, - RadrootsOfflineGeocoderState, SetupActionState, + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, SetupActionState, }; #[cfg(any(target_os = "android", test))] use radroots_identity::RadrootsIdentity; @@ -204,12 +204,10 @@ impl AndroidBackend { #[cfg(not(target_os = "android"))] let offline_geocoder = offline_geocoder::AndroidOfflineGeocoder::from_state( - RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is not available in this android build.".to_owned(), - debug_message: - "android offline geocoder initialization is only wired on android targets" - .to_owned(), - }, + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + "android offline geocoder initialization is only wired on android targets", + ), ); Self { offline_geocoder } diff --git a/crates/android/src/offline_geocoder.rs b/crates/android/src/offline_geocoder.rs @@ -1,6 +1,8 @@ #![cfg_attr(not(target_os = "android"), allow(dead_code))] -use radroots_app_core::RadrootsOfflineGeocoderState; +use radroots_app_core::{ + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, +}; #[cfg(target_os = "android")] use radroots_geocoder::Geocoder; use std::sync::atomic::{AtomicBool, Ordering}; @@ -56,9 +58,11 @@ impl AndroidOfflineGeocoder { self.current .lock() .map(|state| state.clone()) - .unwrap_or_else(|_| RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is unavailable on this device.".to_owned(), - debug_message: "android offline geocoder state lock poisoned".to_owned(), + .unwrap_or_else(|_| { + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + "android offline geocoder state lock poisoned", + ) }) } @@ -75,26 +79,49 @@ impl AndroidOfflineGeocoder { fn initialize_offline_geocoder() -> RadrootsOfflineGeocoderState { match initialize_offline_geocoder_inner() { Ok(()) => RadrootsOfflineGeocoderState::Ready, - Err(debug_message) => classify_initialize_error(debug_message), + Err((kind, debug_message)) => { + RadrootsOfflineGeocoderState::unavailable(kind, debug_message) + } } } #[cfg(target_os = "android")] -fn initialize_offline_geocoder_inner() -> Result<(), String> { +fn initialize_offline_geocoder_inner( +) -> Result<(), (RadrootsOfflineGeocoderUnavailableKind, String)> { let staged_path = stage_offline_geocoder_asset()?; Geocoder::open_path(staged_path.as_str()) .map(|_| ()) - .map_err(|source| format!("failed to open staged android geocoder db: {source}")) + .map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + format!("failed to open staged android geocoder db: {source}"), + ) + }) } #[cfg(target_os = "android")] -fn stage_offline_geocoder_asset() -> Result<String, String> { - let java_vm = android_java_vm().map_err(|source| source.to_string())?; +fn stage_offline_geocoder_asset() -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> { + let java_vm = android_java_vm().map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + source.to_string(), + ) + })?; let mut env = java_vm .attach_current_thread() .map_err(jni_error) - .map_err(|source| source.to_string())?; - let bridge_class = bridge_class(&mut env).map_err(|source| source.to_string())?; + .map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + source.to_string(), + ) + })?; + let bridge_class = bridge_class(&mut env).map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + source.to_string(), + ) + })?; let value = env .call_static_method( &bridge_class, @@ -104,31 +131,40 @@ fn stage_offline_geocoder_asset() -> Result<String, String> { ) .and_then(|value| value.l()) .map_err(jni_error) - .map_err(|source| source.to_string())?; + .map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + source.to_string(), + ) + })?; if value.is_null() { - return Err(take_last_error_message(&mut env, &bridge_class) - .map_err(|source| source.to_string())? - .unwrap_or_else(|| "android app bridge returned no staged geocoder path".to_owned())); + let error_kind = take_last_error_kind(&mut env, &bridge_class).map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + source.to_string(), + ) + })?; + let debug_message = take_last_error_message(&mut env, &bridge_class) + .map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + source.to_string(), + ) + })? + .unwrap_or_else(|| "android app bridge returned no staged geocoder path".to_owned()); + return Err((error_kind, debug_message)); } let value = JString::from(value); env.get_string(&value) .map(|value| value.into()) - .map_err(|source| jni_error(source).to_string()) -} - -fn classify_initialize_error(debug_message: String) -> RadrootsOfflineGeocoderState { - let user_message = if debug_message.contains("asset missing") { - "Offline geocoder is not available in this build.".to_owned() - } else { - "Offline geocoder could not be initialized on this device.".to_owned() - }; - - RadrootsOfflineGeocoderState::Unavailable { - user_message, - debug_message, - } + .map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + jni_error(source).to_string(), + ) + }) } #[cfg(target_os = "android")] @@ -168,6 +204,23 @@ fn bridge_class<'local>( } #[cfg(target_os = "android")] +fn take_last_error_kind( + env: &mut JNIEnv<'_>, + bridge_class: &JClass<'_>, +) -> Result<RadrootsOfflineGeocoderUnavailableKind, RadrootsNostrAccountsError> { + let value = env + .call_static_method(bridge_class, "takeLastErrorKind", "()I", &[]) + .and_then(|value| value.i()) + .map_err(jni_error)?; + match value { + 1 => Ok(RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset), + 2 => Ok(RadrootsOfflineGeocoderUnavailableKind::InitializationFailed), + 3 => Ok(RadrootsOfflineGeocoderUnavailableKind::InternalError), + _ => Ok(RadrootsOfflineGeocoderUnavailableKind::InitializationFailed), + } +} + +#[cfg(target_os = "android")] fn take_last_error_message( env: &mut JNIEnv<'_>, bridge_class: &JClass<'_>, @@ -200,14 +253,15 @@ mod tests { #[test] fn missing_asset_maps_to_build_unavailable_message() { - let state = classify_initialize_error( - "android bundled geocoder asset missing at assets/geocoder/geonames.db".to_owned(), + let state = RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + "android bundled geocoder asset missing at assets/geocoder/geonames.db", ); assert_eq!( state, RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is not available in this build.".to_owned(), + kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, debug_message: "android bundled geocoder asset missing at assets/geocoder/geonames.db" .to_owned(), diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -8,7 +8,9 @@ mod offline_geocoder; pub const APP_NAME: &str = "Rad Roots"; -pub use offline_geocoder::RadrootsOfflineGeocoderState; +pub use offline_geocoder::{ + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetupActionState { @@ -287,16 +289,20 @@ impl RadrootsApp { ui.add_space(16.0); ui.label(state.summary_label()); - if let RadrootsOfflineGeocoderState::Unavailable { - user_message, - debug_message, - } = state - { + if let Some(user_message) = state.user_message() { ui.add_space(6.0); ui.label(user_message); ui.add_space(6.0); - ui.collapsing("Offline geocoder debug details", |ui| { - ui.monospace(debug_message); + ui.collapsing("Offline geocoder details", |ui| { + if let Some(technical_message) = state.technical_message() { + ui.label(technical_message); + } + if cfg!(debug_assertions) { + if let Some(debug_message) = state.debug_message() { + ui.add_space(6.0); + ui.monospace(debug_message); + } + } }); } } @@ -1155,10 +1161,10 @@ mod tests { ) .with_offline_geocoder_state( RadrootsOfflineGeocoderState::Initializing, - vec![Ok(Some(RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is unavailable on this device.".into(), - debug_message: "failed to open staged geocoder db".into(), - }))], + vec![Ok(Some(RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + "failed to open staged geocoder db", + )))], ), )); @@ -1166,10 +1172,22 @@ mod tests { assert_eq!( app.offline_geocoder_state, - Some(RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is unavailable on this device.".into(), - debug_message: "failed to open staged geocoder db".into(), - }) + Some(RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + "failed to open staged geocoder db", + )) + ); + assert_eq!( + app.offline_geocoder_state + .as_ref() + .and_then(RadrootsOfflineGeocoderState::user_message), + Some("Offline geocoder could not be initialized on this device.") + ); + assert_eq!( + app.offline_geocoder_state + .as_ref() + .and_then(RadrootsOfflineGeocoderState::debug_message), + Some("failed to open staged geocoder db") ); } } diff --git a/crates/core/src/offline_geocoder.rs b/crates/core/src/offline_geocoder.rs @@ -1,14 +1,63 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsOfflineGeocoderUnavailableKind { + MissingBuildAsset, + InitializationFailed, + InternalError, +} + +impl RadrootsOfflineGeocoderUnavailableKind { + pub fn technical_message(self) -> &'static str { + match self { + Self::MissingBuildAsset => { + "The offline geocoder data file is missing from this app build." + } + Self::InitializationFailed => { + "The offline geocoder data file could not be prepared on this device." + } + Self::InternalError => { + "The app could not complete offline geocoder setup because of an internal error." + } + } + } + + pub fn user_message(self) -> &'static str { + match self { + Self::MissingBuildAsset => "Offline geocoder is not available in this build.", + Self::InitializationFailed | Self::InternalError => { + "Offline geocoder could not be initialized on this device." + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsOfflineGeocoderState { Initializing, Ready, Unavailable { - user_message: String, + kind: RadrootsOfflineGeocoderUnavailableKind, debug_message: String, }, } impl RadrootsOfflineGeocoderState { + pub fn unavailable( + kind: RadrootsOfflineGeocoderUnavailableKind, + debug_message: impl Into<String>, + ) -> Self { + Self::Unavailable { + kind, + debug_message: debug_message.into(), + } + } + + pub fn debug_message(&self) -> Option<&str> { + match self { + Self::Unavailable { debug_message, .. } => Some(debug_message.as_str()), + Self::Initializing | Self::Ready => None, + } + } + pub fn summary_label(&self) -> &'static str { match self { Self::Initializing => "Offline geocoder: initializing", @@ -16,4 +65,18 @@ impl RadrootsOfflineGeocoderState { Self::Unavailable { .. } => "Offline geocoder unavailable", } } + + pub fn technical_message(&self) -> Option<&'static str> { + match self { + Self::Unavailable { kind, .. } => Some(kind.technical_message()), + Self::Initializing | Self::Ready => None, + } + } + + pub fn user_message(&self) -> Option<&'static str> { + match self { + Self::Unavailable { kind, .. } => Some(kind.user_message()), + Self::Initializing | Self::Ready => None, + } + } } diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -11,7 +11,7 @@ use radroots_app_apple_security::{ use radroots_app_core::{ APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState, RadrootsApp, RadrootsAppBackend, RadrootsOfflineGeocoderState, - SetupActionState, + RadrootsOfflineGeocoderUnavailableKind, SetupActionState, }; #[cfg(target_os = "macos")] use radroots_identity::RadrootsIdentity; @@ -64,22 +64,21 @@ impl DesktopBackend { #[cfg(target_os = "macos")] let offline_geocoder = match Self::app_data_root() { Ok(app_data_root) => DesktopOfflineGeocoder::start(app_data_root), - Err(debug_message) => { - DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder could not be initialized on this device." - .to_owned(), + Err(debug_message) => DesktopOfflineGeocoder::from_state( + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InternalError, debug_message, - }) - } + ), + ), }; #[cfg(not(target_os = "macos"))] - let offline_geocoder = - DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is not available in this desktop build.".to_owned(), - debug_message: "desktop offline geocoder initialization is only wired for macos" - .to_owned(), - }); + let offline_geocoder = DesktopOfflineGeocoder::from_state( + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + "desktop offline geocoder initialization is only wired for macos", + ), + ); Self { offline_geocoder } } diff --git a/crates/desktop/src/offline_geocoder.rs b/crates/desktop/src/offline_geocoder.rs @@ -1,4 +1,6 @@ -use radroots_app_core::RadrootsOfflineGeocoderState; +use radroots_app_core::{ + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, +}; use radroots_geocoder::Geocoder; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -40,9 +42,11 @@ impl DesktopOfflineGeocoder { self.current .lock() .map(|state| state.clone()) - .unwrap_or_else(|_| RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is unavailable on this device.".to_owned(), - debug_message: "desktop offline geocoder state lock poisoned".to_owned(), + .unwrap_or_else(|_| { + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + "desktop offline geocoder state lock poisoned", + ) }) } @@ -58,24 +62,46 @@ impl DesktopOfflineGeocoder { fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState { match initialize_offline_geocoder_inner(app_data_root) { Ok(()) => RadrootsOfflineGeocoderState::Ready, - Err(debug_message) => classify_initialize_error(debug_message), + Err((kind, debug_message)) => { + RadrootsOfflineGeocoderState::unavailable(kind, debug_message) + } } } -fn initialize_offline_geocoder_inner(app_data_root: &Path) -> Result<(), String> { - let source_path = runtime_asset_path()?; +fn initialize_offline_geocoder_inner( + app_data_root: &Path, +) -> Result<(), (RadrootsOfflineGeocoderUnavailableKind, String)> { + let source_path = runtime_asset_path().map_err(|debug_message| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + debug_message, + ) + })?; if !source_path.is_file() { - return Err(format!( - "desktop bundled geocoder asset missing at {}", - source_path.display() + return Err(( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + format!( + "desktop bundled geocoder asset missing at {}", + source_path.display() + ), )); } let staged_path = staged_db_path(app_data_root); - stage_runtime_asset(source_path.as_path(), staged_path.as_path())?; + stage_runtime_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + debug_message, + ) + })?; Geocoder::open_path(staged_path.as_path()) .map(|_| ()) - .map_err(|source| format!("failed to open staged geocoder db: {source}")) + .map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + format!("failed to open staged geocoder db: {source}"), + ) + }) } fn runtime_asset_path() -> Result<PathBuf, String> { @@ -102,19 +128,6 @@ fn stage_runtime_asset(source_path: &Path, staged_path: &Path) -> Result<(), Str Ok(()) } -fn classify_initialize_error(debug_message: String) -> RadrootsOfflineGeocoderState { - let user_message = if debug_message.contains("asset missing") { - "Offline geocoder is not available in this build.".to_owned() - } else { - "Offline geocoder could not be initialized on this device.".to_owned() - }; - - RadrootsOfflineGeocoderState::Unavailable { - user_message, - debug_message, - } -} - #[cfg(test)] mod tests { use super::*; @@ -131,14 +144,15 @@ mod tests { #[test] fn missing_asset_maps_to_build_unavailable_message() { - let state = classify_initialize_error( - "desktop bundled geocoder asset missing at /tmp/geonames.db".to_owned(), + let state = RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + "desktop bundled geocoder asset missing at /tmp/geonames.db", ); assert_eq!( state, RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is not available in this build.".to_owned(), + kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, debug_message: "desktop bundled geocoder asset missing at /tmp/geonames.db" .to_owned(), } diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs @@ -10,7 +10,7 @@ use radroots_app_core::IdentityGateState; use radroots_app_core::{ APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, ImportActionState, PasteActionState, RadrootsApp, RadrootsAppBackend, RadrootsOfflineGeocoderState, - SetupActionState, + RadrootsOfflineGeocoderUnavailableKind, SetupActionState, }; #[cfg(any(target_os = "ios", test))] use radroots_identity::RadrootsIdentity; @@ -47,11 +47,10 @@ impl IosBackend { let offline_geocoder = match storage::app_data_root() { Ok(app_data_root) => offline_geocoder::IosOfflineGeocoder::start(app_data_root), Err(debug_message) => offline_geocoder::IosOfflineGeocoder::from_state( - RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder could not be initialized on this device." - .to_owned(), + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InternalError, debug_message, - }, + ), ), }; diff --git a/crates/ios/src/offline_geocoder.rs b/crates/ios/src/offline_geocoder.rs @@ -1,6 +1,8 @@ #![cfg_attr(not(target_os = "ios"), allow(dead_code))] -use radroots_app_core::RadrootsOfflineGeocoderState; +use radroots_app_core::{ + RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, +}; use radroots_geocoder::Geocoder; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -42,9 +44,11 @@ impl IosOfflineGeocoder { self.current .lock() .map(|state| state.clone()) - .unwrap_or_else(|_| RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is unavailable on this device.".to_owned(), - debug_message: "ios offline geocoder state lock poisoned".to_owned(), + .unwrap_or_else(|_| { + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + "ios offline geocoder state lock poisoned", + ) }) } @@ -60,24 +64,43 @@ impl IosOfflineGeocoder { fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState { match initialize_offline_geocoder_inner(app_data_root) { Ok(()) => RadrootsOfflineGeocoderState::Ready, - Err(debug_message) => classify_initialize_error(debug_message), + Err((kind, debug_message)) => { + RadrootsOfflineGeocoderState::unavailable(kind, debug_message) + } } } -fn initialize_offline_geocoder_inner(app_data_root: &Path) -> Result<(), String> { - let source_path = bundled_asset_path()?; +fn initialize_offline_geocoder_inner( + app_data_root: &Path, +) -> Result<(), (RadrootsOfflineGeocoderUnavailableKind, String)> { + let source_path = bundled_asset_path().map_err(|debug_message| { + ( + RadrootsOfflineGeocoderUnavailableKind::InternalError, + debug_message, + ) + })?; if !source_path.is_file() { - return Err(format!( - "ios bundled geocoder asset missing at {}", - source_path.display() + return Err(( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + format!("ios bundled geocoder asset missing at {}", source_path.display()), )); } let staged_path = staged_db_path(app_data_root); - stage_bundled_asset(source_path.as_path(), staged_path.as_path())?; + stage_bundled_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + debug_message, + ) + })?; Geocoder::open_path(staged_path.as_path()) .map(|_| ()) - .map_err(|source| format!("failed to open staged ios geocoder db: {source}")) + .map_err(|source| { + ( + RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, + format!("failed to open staged ios geocoder db: {source}"), + ) + }) } fn bundled_asset_path() -> Result<PathBuf, String> { @@ -104,19 +127,6 @@ fn stage_bundled_asset(source_path: &Path, staged_path: &Path) -> Result<(), Str Ok(()) } -fn classify_initialize_error(debug_message: String) -> RadrootsOfflineGeocoderState { - let user_message = if debug_message.contains("asset missing") { - "Offline geocoder is not available in this build.".to_owned() - } else { - "Offline geocoder could not be initialized on this device.".to_owned() - }; - - RadrootsOfflineGeocoderState::Unavailable { - user_message, - debug_message, - } -} - #[cfg(test)] mod tests { use super::*; @@ -137,15 +147,17 @@ mod tests { #[test] fn missing_asset_maps_to_build_unavailable_message() { - let state = classify_initialize_error( - "ios bundled geocoder asset missing at /tmp/geonames.db".to_owned(), + let state = RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + "ios bundled geocoder asset missing at /tmp/geonames.db", ); assert_eq!( state, RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is not available in this build.".to_owned(), - debug_message: "ios bundled geocoder asset missing at /tmp/geonames.db".to_owned(), + kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + debug_message: "ios bundled geocoder asset missing at /tmp/geonames.db" + .to_owned(), } ); } diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs @@ -16,7 +16,8 @@ use nostr_browser_signer::{BrowserSigner, Error as BrowserSignerError}; #[cfg(target_arch = "wasm32")] use radroots_app_core::{ HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, RadrootsApp, - RadrootsAppBackend, RadrootsOfflineGeocoderState, SetupActionState, + RadrootsAppBackend, RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, + SetupActionState, }; #[cfg(target_arch = "wasm32")] @@ -82,12 +83,10 @@ impl WebBackend { } fn offline_geocoder_unavailable_state() -> RadrootsOfflineGeocoderState { - RadrootsOfflineGeocoderState::Unavailable { - user_message: "Offline geocoder is not available in this web build.".to_owned(), - debug_message: - "radroots-geocoder currently depends on rusqlite and is not wired for wasm runtime initialization." - .to_owned(), - } + RadrootsOfflineGeocoderState::unavailable( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + "radroots-geocoder currently depends on rusqlite and is not wired for wasm runtime initialization.", + ) } } 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 @@ -7,6 +7,9 @@ import java.io.FileNotFoundException object RadRootsAndroidAppBridge { private const val GEOCODER_ASSET_PATH = "geocoder/geonames.db" private const val GEOCODER_FILE_NAME = "geonames.db" + private const val GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET = 1 + private const val GEOCODER_ERROR_KIND_INITIALIZATION_FAILED = 2 + private const val GEOCODER_ERROR_KIND_INTERNAL_ERROR = 3 @Volatile private var appContext: Context? = null @@ -14,6 +17,9 @@ object RadRootsAndroidAppBridge { @Volatile private var lastErrorMessage: String? = null + @Volatile + private var lastErrorKind: Int = 0 + @JvmStatic fun initialize(context: Context) { appContext = context.applicationContext @@ -22,10 +28,17 @@ object RadRootsAndroidAppBridge { @JvmStatic @Synchronized fun stageOfflineGeocoderAsset(): String? { - val context = appContext ?: return fail("android app bridge is not initialized") + val context = appContext + ?: return fail( + GEOCODER_ERROR_KIND_INTERNAL_ERROR, + "android app bridge is not initialized", + ) val targetDir = File(context.noBackupFilesDir, "RadRoots/app/android/geocoder") if (!targetDir.exists() && !targetDir.mkdirs()) { - return fail("failed to create android geocoder directory: ${targetDir.absolutePath}") + return fail( + GEOCODER_ERROR_KIND_INITIALIZATION_FAILED, + "failed to create android geocoder directory: ${targetDir.absolutePath}", + ) } val targetFile = File(targetDir, GEOCODER_FILE_NAME) @@ -36,16 +49,31 @@ object RadRootsAndroidAppBridge { } } lastErrorMessage = null + lastErrorKind = 0 targetFile.absolutePath } catch (_: FileNotFoundException) { - fail("android bundled geocoder asset missing at assets/$GEOCODER_ASSET_PATH") + fail( + GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET, + "android bundled geocoder asset missing at assets/$GEOCODER_ASSET_PATH", + ) } catch (source: Exception) { - fail("failed to stage android geocoder asset: ${source.message ?: source.javaClass.simpleName}") + fail( + GEOCODER_ERROR_KIND_INITIALIZATION_FAILED, + "failed to stage android geocoder asset: ${source.message ?: source.javaClass.simpleName}", + ) } } @JvmStatic @Synchronized + fun takeLastErrorKind(): Int { + val value = lastErrorKind + lastErrorKind = 0 + return value + } + + @JvmStatic + @Synchronized fun takeLastErrorMessage(): String? { val value = lastErrorMessage lastErrorMessage = null @@ -53,7 +81,8 @@ object RadRootsAndroidAppBridge { } @Synchronized - private fun fail(message: String): String? { + private fun fail(kind: Int, message: String): String? { + lastErrorKind = kind lastErrorMessage = message return null }