//! TRACES: NFR-OPS-1 //! Where this platform lets the application keep notes about itself. //! //! Not the catalog, not the photographs, not the user's configuration — those //! three already have homes (`dr_sync::account::config_dir`, //! `dr_ui::library::data_root`, and the sidecars beside the images). This is //! the fourth thing: the diagnostic residue an application leaves so that a //! failure yesterday can be read about today. A log, and — when the crash //! path lands — a crash record. //! //! # Why it is its own directory and not one of the other three //! //! XDG separates them for a reason that is not tidiness. `$XDG_CONFIG_HOME` //! is what the user has chosen and would miss; `$XDG_DATA_HOME` is what the //! application built and would have to rebuild; `$XDG_STATE_HOME` is //! "state that should persist between restarts but is not important or //! portable enough for the data directory" — which is exactly a log file. It //! is also the directory nobody backs up, and a log is the one file here we //! actively want a user to be able to delete without consequence. //! //! # Android has none of those variables, so the platform must say //! //! There is no `$HOME` on Android and no XDG anything (ARCH §6.9), so the //! guesses below resolve to a path the app cannot write. `dr_sync::account` //! learned this the expensive way — the session list went to a doomed path, //! nothing failed loudly, and backgrounding the app lost the sign-in — and the //! shape of the fix is copied here deliberately: the platform entry point //! declares the directory once, before anything opens a file in it. //! //! **Which Android directory is a diagnostics decision, and it belongs to the //! caller.** `AndroidApp` offers two, and they differ in precisely the way //! that matters here: //! //! * `internal_data_path` — `/data/data//files`. Private, durable, and //! unreachable: pulling a file out of it needs `run-as` against a debuggable //! build, or root. //! * `external_data_path` — `/sdcard/Android/data//files`. Same lifetime //! (the system deletes it with the app, not under storage pressure — that is //! the *cache* directory), needs no permission since API 19, and `adb pull` //! reads it from any build. //! //! A log nobody can retrieve is not a diagnostic, so `darkroom-android` points //! this at the external one. That choice has a consequence, and it is the //! reason [`crate::diagnostics`] redacts at the sink rather than trusting call //! sites: everything written here is readable by anyone holding the device. use std::path::PathBuf; use std::sync::OnceLock; /// Declared once by the platform entry point; a guess otherwise. static STATE_DIR: OnceLock = OnceLock::new(); /// Declare where this platform keeps application state. /// /// Call it before anything opens a file — installing the logger is normally /// the very next line — because a later call is *ignored* rather than obeyed. /// That is deliberate: two callers disagreeing about the directory would /// otherwise split the log across two files depending on which ran first, and /// a silently-ignored second call leaves one log rather than two halves. /// /// Desktop needs no call. The XDG resolution below is correct there. pub fn set_state_dir(dir: PathBuf) { let _ = STATE_DIR.set(dir); } /// The directory this application's state belongs in. /// /// It is not created here. Whoever writes into it creates it, so that merely /// asking the question leaves nothing behind on a machine that never logs. pub fn state_dir() -> PathBuf { if let Some(dir) = STATE_DIR.get() { return dir.clone(); } crate::dirs::base_dir(crate::dirs::Base::State) } #[cfg(test)] mod tests { use super::*; #[test] fn a_declared_directory_is_declared_once() { // The property the entry points rely on: two callers cannot split the // log in half. Exercised on a fresh `OnceLock` rather than the global // one, which any other test in this binary may already have set. let cell: OnceLock = OnceLock::new(); assert!(cell.set(PathBuf::from("/first")).is_ok()); assert!(cell.set(PathBuf::from("/second")).is_err()); assert_eq!(cell.get(), Some(&PathBuf::from("/first"))); } }