cranpose_services/background.rs
1//! Background execution: ask the OS to keep running briefly after the app is
2//! backgrounded, so in-flight work (e.g. draining a recognition queue) can
3//! finish instead of being suspended immediately.
4//!
5//! The platform backend installs an implementation via
6//! [`set_platform_background_activity`] (iOS `beginBackgroundTask`, Android a
7//! foreground service). No default: [`background_activity`] returns `None` where
8//! unsupported, and the app simply runs only while foregrounded.
9
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::Arc;
12use std::sync::Mutex;
13use std::sync::OnceLock;
14
15/// Marks periods of important work so the platform grants background running
16/// time. Implementations are `Send + Sync` (the app toggles activity from its
17/// worker threads).
18pub trait BackgroundActivity: Send + Sync {
19 /// `true` when important work starts, `false` when it finishes. Calls are
20 /// balanced by the app but implementations must tolerate repeats.
21 fn set_active(&self, active: bool);
22}
23
24pub type BackgroundActivityRef = Arc<dyn BackgroundActivity>;
25
26fn slot() -> &'static Mutex<Option<BackgroundActivityRef>> {
27 static SLOT: OnceLock<Mutex<Option<BackgroundActivityRef>>> = OnceLock::new();
28 SLOT.get_or_init(|| Mutex::new(None))
29}
30
31/// Installs the platform background-activity handler, replacing any previous one.
32pub fn set_platform_background_activity(activity: BackgroundActivityRef) {
33 if let Ok(mut s) = slot().lock() {
34 *s = Some(activity);
35 }
36}
37
38/// Removes any registered handler (tests/teardown).
39pub fn clear_platform_background_activity() {
40 if let Ok(mut s) = slot().lock() {
41 *s = None;
42 }
43}
44
45/// The registered background-activity handler, or `None` where unsupported.
46pub fn background_activity() -> Option<BackgroundActivityRef> {
47 slot().lock().ok().and_then(|s| s.clone())
48}
49
50static ACTIVE: AtomicBool = AtomicBool::new(false);
51
52/// Convenience: mark background work active/inactive. The flag is recorded even
53/// where no platform handler is installed, so [`background_active`] answers the
54/// same everywhere.
55pub fn set_background_active(active: bool) {
56 ACTIVE.store(active, Ordering::SeqCst);
57 if let Some(activity) = background_activity() {
58 activity.set_active(active);
59 }
60}
61
62/// `true` between [`set_background_active(true)`](set_background_active) and the
63/// matching `false`.
64///
65/// A platform backend reads this to decide whether the runtime keeps turning
66/// while the app is off screen. Both mobile backends drive composition from the
67/// frame path, and both stop that path when the surface is gone: Android drops
68/// its GPU resources on `TerminateWindow`, iOS stops receiving redraws. Anything
69/// posted to the UI dispatcher then waits until the user comes back, which is
70/// wrong for an app that told the OS it has work to finish.
71pub fn background_active() -> bool {
72 ACTIVE.load(Ordering::SeqCst)
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use std::sync::atomic::{AtomicBool, Ordering};
79
80 #[test]
81 fn registration_round_trips() {
82 clear_platform_background_activity();
83 set_background_active(true); // no-op, no panic
84 struct Rec(AtomicBool);
85 impl BackgroundActivity for Rec {
86 fn set_active(&self, active: bool) {
87 self.0.store(active, Ordering::SeqCst);
88 }
89 }
90 let rec = Arc::new(Rec(AtomicBool::new(false)));
91 set_platform_background_activity(rec.clone());
92 set_background_active(true);
93 assert!(rec.0.load(Ordering::SeqCst));
94 assert!(background_active());
95 set_background_active(false);
96 assert!(!background_active());
97 clear_platform_background_activity();
98 set_background_active(true);
99 assert!(background_active());
100 set_background_active(false);
101 assert!(!background_active());
102 }
103}