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