//! `ModeStack`: refcount-composable Android audio-mode acquire/release //! with prior-mode snapshot/restore. //! //! # Trace //! //! Sourced verbatim from SDD-108: //! //! - SDD-108 §1 (Engagement API): "`fn engage()`: increments the //! internal reference count; on the 0 → 1 transition it shall //! snapshot the current mode (`prior_mode = audioManager.mode`) and //! call `audioManager.setMode(MODE_IN_COMMUNICATION)`. On counts ≥ //! 1, it is a no-op success. `fn release()`: decrements the //! reference count; on the 1 → 0 transition it shall call //! `audioManager.setMode(prior_mode)` and clear `prior_mode`. On //! counts ≥ 1 after decrement, it is a no-op. On underflow (release //! without engage) it shall log and clamp at 0 without crashing." //! - SDD-108 §2 (Refcount semantics): "The count is an `AtomicI32` //! guarded by a Kotlin `Mutex` for the snapshot/restore critical //! section. P0 only ever observes counts in `{0, 1}` per SRS-189, //! but the design must compose for future multi-session use. Any //! two-engages-one-release sequence shall keep `prior_mode` from //! the FIRST engage; a release at count 1 restores that original //! mode." //! - SDD-108 §3 (Lifecycle binding): "`engage()` shall be called from //! the audio engine's `ensure_running()` path … AFTER //! `AndroidVoiceForegroundService.start` succeeds. `release()` //! shall be called from `shutdown_if_idle()` BEFORE //! `AndroidVoiceForegroundService.stop`." //! //! This module realises §1 + §2 as a pure data structure: the platform //! interaction (JNI getMode / setMode) is the engine integration's //! responsibility (SDD-108 §3); `ModeStack` here is panic-free pure //! Rust logic and is host-testable on all targets. /// Outcome of [`ModeStack::acquire`]. /// /// SDD-108 §1: the 0 → 1 transition is the one that must reach the /// platform; composed acquires return [`ModeAcquire::AlreadyHeld`] /// without re-snapshotting (SDD-108 §2). #[derive(Debug, PartialEq, Eq)] pub enum ModeAcquire { /// First acquire from idle. Caller MUST platform-set the desired /// target mode (e.g. `MODE_IN_COMMUNICATION`). `prior` is the /// mode that will be restored on the matching last release. FirstAcquire { /// The system mode that was active before this acquire. prior: i32, }, /// Composed acquire: mode is already engaged, no platform write /// required. Refcount has been incremented. AlreadyHeld, } /// Outcome of [`ModeStack::release`]. /// /// SDD-108 §1: the 1 → 0 transition is the one that must restore the /// snapshot. Mid-stack releases return [`ModeRelease::StillHeld`]. /// Releases against an empty stack return /// [`ModeRelease::AlreadyReleased`] (SDD-108 §1: "On underflow … /// clamp at 0 without crashing"). #[derive(Debug, PartialEq, Eq)] pub enum ModeRelease { /// Last release. Caller MUST platform-set the system mode back to /// `prior`. Snapshot has been cleared. LastRelease { /// The mode that was snapshotted on the matching first acquire. prior: i32, }, /// Refcount remains > 0 after this release; mode stays engaged. StillHeld, /// Release called against an idle stack. Programmer error; the /// stack remains idle (no panic, no underflow — SDD-108 §1). AlreadyReleased, } /// Refcount-composable audio-mode controller (SDD-108 §1, §2). /// /// `ModeStack` does NOT call the platform. The caller is responsible /// for passing the current platform mode into [`Self::acquire`] and /// for executing the platform write (setMode) when the outcome /// indicates a transition is required. This separation keeps /// `ModeStack` host-testable and `#[cfg]`-free. /// /// `ModeStack` is not internally synchronised; callers must serialise /// access (e.g. behind a `Mutex`), as SDD-108 §2 specifies for the /// Kotlin side. #[derive(Debug, Default)] pub struct ModeStack { refcount: usize, // `usize` (not `AtomicI32` as in SDD-108 §2) — synchronisation is the caller's responsibility; saturating_add prevents wrap. snapshot: Option, } impl ModeStack { /// Construct an idle stack (refcount 0, no snapshot). pub fn new() -> Self { Self { refcount: 0, snapshot: None, } } /// Acquire the audio-mode lock (SDD-108 §1). /// /// `current_platform_mode` is the mode that /// `AudioManager.getMode()` returned at the call site. On the 0 → /// 1 transition this value is snapshotted as the restore target. /// On composed acquires (count ≥ 1 prior), the snapshot is /// preserved verbatim per SDD-108 §2 ("Any two-engages-one-release /// sequence shall keep `prior_mode` from the FIRST engage"). pub fn acquire(&mut self, current_platform_mode: i32) -> ModeAcquire { if self.refcount == 0 { self.snapshot = Some(current_platform_mode); self.refcount = 1; ModeAcquire::FirstAcquire { prior: current_platform_mode, } } else { // Saturating add guards against pathological refcount // overflow without panicking; on usize this would require // ~2^64 composed acquires which is not reachable in // practice but keeps the panic-free contract honest. self.refcount = self.refcount.saturating_add(1); ModeAcquire::AlreadyHeld } } /// Release the audio-mode lock (SDD-108 §1). /// /// On the 1 → 0 transition, returns [`ModeRelease::LastRelease`] /// carrying the snapshotted prior mode that the caller must /// platform-restore. The snapshot is cleared as part of the /// transition. pub fn release(&mut self) -> ModeRelease { match self.refcount { 0 => ModeRelease::AlreadyReleased, 1 => { self.refcount = 0; debug_assert!( self.snapshot.is_some(), "ModeStack invariant: refcount==1 implies snapshot.is_some()" ); // `0` corresponds to Android `MODE_NORMAL` — a safe restore target. let prior = self.snapshot.take().unwrap_or(0); ModeRelease::LastRelease { prior } } n => { self.refcount = n - 1; ModeRelease::StillHeld } } } /// Current refcount (diagnostics only, per SDD-108 §1 /// `is_engaged()` rationale). pub fn refcount(&self) -> usize { self.refcount } /// Current snapshot (diagnostics only). `None` when idle. pub fn snapshot(&self) -> Option { self.snapshot } } #[cfg(test)] mod tests { use super::*; /// SWE4-UV-045: First acquire from idle snapshots prior mode. #[test] fn swe4_uv_045_acquire_from_idle_snapshots_prior_mode() { let mut s = ModeStack::new(); assert_eq!(s.acquire(0), ModeAcquire::FirstAcquire { prior: 0 }); assert_eq!(s.refcount(), 1); assert_eq!(s.snapshot(), Some(0)); } /// SWE4-UV-045: Second acquire returns AlreadyHeld; snapshot is /// preserved from the FIRST acquire (SDD-108 §2). #[test] fn swe4_uv_045_acquire_twice_returns_already_held() { let mut s = ModeStack::new(); let _ = s.acquire(7); // Even if a different "current" mode is reported on the // composed acquire, the snapshot must remain the first one. assert_eq!(s.acquire(99), ModeAcquire::AlreadyHeld); assert_eq!(s.refcount(), 2); assert_eq!(s.snapshot(), Some(7)); } /// SWE4-UV-045: Last release returns the snapshotted prior mode. #[test] fn swe4_uv_045_release_last_returns_prior() { let mut s = ModeStack::new(); let _ = s.acquire(7); assert_eq!(s.release(), ModeRelease::LastRelease { prior: 7 }); assert_eq!(s.refcount(), 0); assert_eq!(s.snapshot(), None); } /// SWE4-UV-045: Release at refcount 2 keeps the mode engaged. #[test] fn swe4_uv_045_release_with_refcount_two_keeps_held() { let mut s = ModeStack::new(); let _ = s.acquire(3); let _ = s.acquire(3); assert_eq!(s.release(), ModeRelease::StillHeld); assert_eq!(s.refcount(), 1); // Snapshot still pinned to first acquire. assert_eq!(s.snapshot(), Some(3)); } /// SWE4-UV-045: Release on idle stack signals programmer error /// without panicking (SDD-108 §1 "clamp at 0 without crashing"). #[test] fn swe4_uv_045_release_on_idle_signals_error() { let mut s = ModeStack::new(); assert_eq!(s.release(), ModeRelease::AlreadyReleased); assert_eq!(s.refcount(), 0); assert_eq!(s.snapshot(), None); } /// SWE4-UV-045: Balanced acquire/release pairs return to idle, and /// a subsequent acquire snapshots the (potentially new) prior /// mode — proving the snapshot is cleared on last release. #[test] fn swe4_uv_045_compose_acquire_release_pairs() { let mut s = ModeStack::new(); // Pair 1: snapshot 0, restore 0. assert_eq!(s.acquire(0), ModeAcquire::FirstAcquire { prior: 0 }); let _ = s.acquire(0); // composed assert_eq!(s.release(), ModeRelease::StillHeld); assert_eq!(s.release(), ModeRelease::LastRelease { prior: 0 }); assert_eq!(s.refcount(), 0); assert_eq!(s.snapshot(), None); // Pair 2: the platform mode is now different; the new // snapshot must reflect THAT mode, not the prior pair's. assert_eq!(s.acquire(2), ModeAcquire::FirstAcquire { prior: 2 }); assert_eq!(s.snapshot(), Some(2)); assert_eq!(s.release(), ModeRelease::LastRelease { prior: 2 }); } }