//! DarkRoom Android entry point. //! //! The counterpart to `darkroom-desktop`'s `main`, with two differences that //! come from the platform rather than from choice: //! //! * There are no command-line paths. Android's SAF hands out document URIs, //! not filesystem paths (ARCH §6.9), so the viewer opens with an empty //! browsing list and the library grid is the only way in. //! * Logging goes to logcat. `env_logger` writes to stderr, which Android //! discards. // `slint::android` exists only when compiling for Android, so the whole entry // point is gated on the target rather than on a feature. Without this the // crate is still a workspace member on the host, and `cargo test --workspace` // fails to compile it — a build break that only ever appears off-device. #[cfg(target_os = "android")] /// TRACES: M-13 | M-14 /// Android application entry point, called by android-activity's glue. #[no_mangle] fn android_main(app: slint::android::AndroidApp) { android_logger::init_once( android_logger::Config::default() .with_max_level(log::LevelFilter::Info) .with_tag("DarkRoom"), ); // Panics go to stderr, and Android discards stderr. Without this hook a // worker thread that panics is invisible: the process survives, the // channel it was writing to closes, and the UI reports only that // something "failed unexpectedly" with no way to find out what. std::panic::set_hook(Box::new(|info| { log::error!("panic: {info}"); })); log::info!("DarkRoom v{}", env!("CARGO_PKG_VERSION")); // Before anything opens a store: Android has no $HOME and no XDG // directories, so the default guess resolves to a path the app cannot // write. Nothing failed loudly — the session list went to a doomed path, so // the account survived only as long as the process and backgrounding the app // lost the sign-in. `internal_data_path` is the app's private directory // (ARCH §6.9). match app.internal_data_path() { Some(dir) => { log::info!("data dir: {}", dir.display()); dr_sync_nextcloud::session::set_data_dir(dir); } None => log::error!("no internal data path; settings will not persist"), } // After the data dir and before anything asks whether a model is present. install_bundled_face_models(&app); if let Err(e) = slint::android::init(app) { log::error!("Slint Android backend failed to initialise: {e}"); return; } // Empty rather than the desktop's argv: see the module note above. // // Returning from `android_main` ends the process, so a failure here is // logged rather than propagated — there is no shell to show `Err` to. if let Err(e) = dr_ui::run(Vec::new()) { log::error!("DarkRoom exited with error: {e:#}"); } } /// Unpack the face models the APK carries, if it carries any. /// /// # Why Android needs this and no other platform does /// /// The weights are not a build input and are not in the repository — the /// InsightFace grant is research-only and incompatible with this project's /// licence (docs/faces.md §2), so a desktop user fetches them, runs /// `tools/fix-face-model-shapes.sh` over them, and drops the result into /// `~/.local/share/darkroom/models/`. **That gesture does not exist on /// Android.** `internal_data_path` is app-private, `run-as` needs a debuggable /// build, and there is no picker and no fetch in the app, so a phone had no way /// to acquire a model at all and face indexing reported itself permanently off. /// /// So a locally-built APK may carry the pair in `assets/models/`, which /// `assemble-apk.sh` includes when the tree has them and omits when it does /// not. Nothing changes about what the repository holds or what a published /// build could redistribute; this only gives a self-built APK the same route a /// desktop build has always had. /// /// Absent assets are the ordinary case, not an error — the same quiet "no model /// installed" state a fresh desktop install is in. #[cfg(target_os = "android")] fn install_bundled_face_models(app: &slint::android::AndroidApp) { use std::io::Read; // The **shape-fixed** names, matching what `library::face_models` looks // for: tract cannot parse either InsightFace graph with its dynamic input // dimension, so what ships here has already been through // `tools/fix-face-model-shapes.sh`. const BUNDLED: [(&std::ffi::CStr, &str); 2] = [ (c"models/scrfd_500m_640.onnx", "scrfd_500m_640.onnx"), (c"models/arcface_mbf_b1.onnx", "arcface_mbf_b1.onnx"), ]; let dir = dr_ui::shared_face_models_dir(); let assets = app.asset_manager(); for (asset_path, name) in BUNDLED { let dest = dir.join(name); // Already unpacked. Not re-read on every launch: this is 15 MB through // a decompressor on the startup path, and the file does not change // without the APK changing, at which point the install wiped it anyway. if dest.is_file() { continue; } let Some(mut asset) = assets.open(asset_path) else { log::info!("no bundled {name} in this APK; face indexing stays off"); continue; }; let mut bytes = Vec::new(); if let Err(e) = asset.read_to_end(&mut bytes) { log::error!("bundled {name} could not be read: {e}"); continue; } if let Err(e) = std::fs::create_dir_all(&dir) { log::error!("cannot create {}: {e}", dir.display()); return; } // Written under a temporary name and renamed, because // `library::face_models` decides face indexing is available on // `is_file()` alone. A truncated write — the process backgrounded and // killed mid-copy — would otherwise leave a file that passes that test // and fails inside tract, reported to the user as a broken model rather // than a missing one. let part = dir.join(format!("{name}.part")); match std::fs::write(&part, &bytes).and_then(|()| std::fs::rename(&part, &dest)) { Ok(()) => log::info!("installed bundled {name} ({} bytes)", bytes.len()), Err(e) => { log::error!("cannot install {name}: {e}"); let _ = std::fs::remove_file(&part); } } } }