#[cfg(not(feature = "mini"))]
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(not(feature = "mini"), derive(Serialize, Deserialize))]
pub enum AppLifecycleState {
Starting,
Foreground,
Background,
Suspended,
Terminating,
}
impl AppLifecycleState {
pub fn is_active(&self) -> bool {
matches!(self, AppLifecycleState::Foreground)
}
pub fn is_visible(&self) -> bool {
matches!(self, AppLifecycleState::Foreground | AppLifecycleState::Starting)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleEvent {
WillEnterForeground,
DidEnterForeground,
WillEnterBackground,
DidEnterBackground,
WillTerminate,
MemoryWarning,
StateRestored,
}
pub type LifecycleCallback = Box<dyn FnMut(LifecycleEvent) + Send>;
#[derive(Debug, Clone)]
#[cfg_attr(not(feature = "mini"), derive(Serialize, Deserialize))]
struct LifecycleSnapshot {
state: AppLifecycleState,
total_background_secs: f64,
}
pub struct AppLifecycle {
state: AppLifecycleState,
started_at: Instant,
background_entry: Option<Instant>,
total_background_duration: std::time::Duration,
listeners: Vec<LifecycleCallback>,
}
impl AppLifecycle {
pub fn new() -> Self {
Self {
state: AppLifecycleState::Starting,
started_at: Instant::now(),
background_entry: None,
total_background_duration: std::time::Duration::ZERO,
listeners: Vec::new(),
}
}
pub fn transition(&mut self, new_state: AppLifecycleState) {
let old_state = self.state;
if old_state == new_state {
return;
}
if old_state == AppLifecycleState::Background {
if let Some(entry) = self.background_entry.take() {
self.total_background_duration += entry.elapsed();
}
}
if new_state == AppLifecycleState::Background {
self.background_entry = Some(Instant::now());
}
if new_state != AppLifecycleState::Background && new_state != AppLifecycleState::Suspended {
self.background_entry = None;
}
self.state = new_state;
match (old_state, new_state) {
(AppLifecycleState::Background, AppLifecycleState::Foreground)
| (AppLifecycleState::Suspended, AppLifecycleState::Foreground)
| (AppLifecycleState::Starting, AppLifecycleState::Foreground) => {
self.fire(LifecycleEvent::WillEnterForeground);
self.fire(LifecycleEvent::DidEnterForeground);
}
(AppLifecycleState::Foreground, AppLifecycleState::Background) => {
self.fire(LifecycleEvent::WillEnterBackground);
self.fire(LifecycleEvent::DidEnterBackground);
}
(AppLifecycleState::Foreground, AppLifecycleState::Suspended) => {
self.fire(LifecycleEvent::WillEnterBackground);
self.fire(LifecycleEvent::DidEnterBackground);
}
(_, AppLifecycleState::Terminating) => {
self.fire(LifecycleEvent::WillTerminate);
}
_ => {}
}
}
pub fn state(&self) -> AppLifecycleState {
self.state
}
pub fn uptime(&self) -> std::time::Duration {
self.started_at.elapsed()
}
pub fn total_background_duration(&self) -> std::time::Duration {
let active = self.background_entry.map_or(std::time::Duration::ZERO, |entry| {
if self.state == AppLifecycleState::Background
|| self.state == AppLifecycleState::Suspended
{
entry.elapsed()
} else {
std::time::Duration::ZERO
}
});
self.total_background_duration + active
}
pub fn add_listener(&mut self, callback: LifecycleCallback) {
self.listeners.push(callback);
}
fn fire(&mut self, event: LifecycleEvent) {
for cb in &mut self.listeners {
cb(event);
}
}
#[cfg(not(feature = "mini"))]
pub fn serialize_state(&self) -> Result<String, String> {
let snapshot = LifecycleSnapshot {
state: self.state,
total_background_secs: self.total_background_duration().as_secs_f64(),
};
serde_json::to_string(&snapshot)
.map_err(|e| format!("failed to serialize lifecycle state: {e}"))
}
#[cfg(not(feature = "mini"))]
pub fn deserialize_state(data: &str) -> Result<Self, String> {
let snapshot: LifecycleSnapshot = serde_json::from_str(data)
.map_err(|e| format!("failed to deserialize lifecycle state: {e}"))?;
let mut lc = Self {
state: snapshot.state,
started_at: Instant::now(),
background_entry: None,
total_background_duration: std::time::Duration::from_secs_f64(
snapshot.total_background_secs,
),
listeners: Vec::new(),
};
if snapshot.state == AppLifecycleState::Background
|| snapshot.state == AppLifecycleState::Suspended
{
lc.background_entry = Some(Instant::now());
}
lc.fire(LifecycleEvent::StateRestored);
Ok(lc)
}
pub fn emit_memory_warning(&mut self) {
self.fire(LifecycleEvent::MemoryWarning);
}
}
impl Default for AppLifecycle {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::sync::Arc;
use core::sync::atomic::{AtomicUsize, Ordering};
use core::time::Duration;
#[test]
fn test_state_transitions() {
let mut lc = AppLifecycle::new();
assert_eq!(lc.state(), AppLifecycleState::Starting);
assert!(lc.state().is_visible());
assert!(!lc.state().is_active());
lc.transition(AppLifecycleState::Foreground);
assert_eq!(lc.state(), AppLifecycleState::Foreground);
assert!(lc.state().is_active());
assert!(lc.state().is_visible());
lc.transition(AppLifecycleState::Background);
assert_eq!(lc.state(), AppLifecycleState::Background);
assert!(!lc.state().is_active());
assert!(!lc.state().is_visible());
lc.transition(AppLifecycleState::Suspended);
assert_eq!(lc.state(), AppLifecycleState::Suspended);
assert!(!lc.state().is_active());
assert!(!lc.state().is_visible());
lc.transition(AppLifecycleState::Terminating);
assert_eq!(lc.state(), AppLifecycleState::Terminating);
}
#[test]
fn test_same_state_transition_noop() {
let mut lc = AppLifecycle::new();
lc.transition(AppLifecycleState::Foreground);
let call_count = Arc::new(AtomicUsize::new(0));
let count = Arc::clone(&call_count);
lc.add_listener(Box::new(move |_| {
count.fetch_add(1, Ordering::SeqCst);
}));
lc.transition(AppLifecycleState::Foreground);
assert_eq!(call_count.load(Ordering::SeqCst), 0);
}
#[test]
fn test_listener_notification() {
let mut lc = AppLifecycle::new();
let events = Arc::new(std::sync::Mutex::new(Vec::new()));
let ev = Arc::clone(&events);
lc.add_listener(Box::new(move |e| {
ev.lock().unwrap().push(e);
}));
lc.transition(AppLifecycleState::Foreground);
lc.transition(AppLifecycleState::Background);
lc.transition(AppLifecycleState::Terminating);
let recorded = events.lock().unwrap();
assert_eq!(recorded.len(), 5);
assert_eq!(recorded[0], LifecycleEvent::WillEnterForeground);
assert_eq!(recorded[1], LifecycleEvent::DidEnterForeground);
assert_eq!(recorded[2], LifecycleEvent::WillEnterBackground);
assert_eq!(recorded[3], LifecycleEvent::DidEnterBackground);
assert_eq!(recorded[4], LifecycleEvent::WillTerminate);
}
#[test]
fn test_serialization_roundtrip() {
let mut lc = AppLifecycle::new();
lc.transition(AppLifecycleState::Foreground);
lc.transition(AppLifecycleState::Background);
lc.transition(AppLifecycleState::Foreground);
let json = lc.serialize_state().expect("serialize_state should succeed");
let restored =
AppLifecycle::deserialize_state(&json).expect("deserialize_state should succeed");
assert_eq!(restored.state(), AppLifecycleState::Foreground);
assert!(restored.total_background_duration() > Duration::ZERO);
}
#[test]
fn test_uptime_increases() {
let lc = AppLifecycle::new();
let u1 = lc.uptime();
std::thread::sleep(Duration::from_millis(10));
let u2 = lc.uptime();
assert!(u2 >= u1 + Duration::from_millis(5));
}
#[test]
fn test_background_duration_accumulates() {
let mut lc = AppLifecycle::new();
lc.transition(AppLifecycleState::Foreground);
assert_eq!(lc.total_background_duration(), Duration::ZERO);
lc.transition(AppLifecycleState::Background);
std::thread::sleep(Duration::from_millis(10));
lc.transition(AppLifecycleState::Foreground);
let bg = lc.total_background_duration();
assert!(bg >= Duration::from_millis(5));
lc.transition(AppLifecycleState::Background);
std::thread::sleep(Duration::from_millis(5));
lc.transition(AppLifecycleState::Foreground);
let bg2 = lc.total_background_duration();
assert!(bg2 > bg);
}
#[test]
fn test_memory_warning() {
let mut lc = AppLifecycle::new();
let warned = Arc::new(AtomicUsize::new(0));
let w = Arc::clone(&warned);
lc.add_listener(Box::new(move |e| {
if e == LifecycleEvent::MemoryWarning {
w.fetch_add(1, Ordering::SeqCst);
}
}));
lc.emit_memory_warning();
assert_eq!(warned.load(Ordering::SeqCst), 1);
}
#[test]
fn test_deserialize_preserves_state() {
let mut lc = AppLifecycle::new();
lc.transition(AppLifecycleState::Foreground);
let json = lc.serialize_state().unwrap();
let d = AppLifecycle::deserialize_state(&json).unwrap();
assert_eq!(d.state(), AppLifecycleState::Foreground);
assert!(d.state().is_active());
assert!(d.state().is_visible());
assert!(d.uptime() < Duration::from_millis(100));
}
}