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::{
11 Arc, Mutex, OnceLock,
12 atomic::{AtomicUsize, Ordering},
13};
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 background_active() {
34 activity.set_active(true);
35 }
36 if let Ok(mut s) = slot().lock() {
37 *s = Some(activity);
38 }
39}
40
41/// Removes any registered handler (tests/teardown).
42pub fn clear_platform_background_activity() {
43 if let Ok(mut s) = slot().lock() {
44 *s = None;
45 }
46}
47
48/// The registered background-activity handler, or `None` where unsupported.
49pub fn background_activity() -> Option<BackgroundActivityRef> {
50 slot().lock().ok().and_then(|s| s.clone())
51}
52
53static ACTIVE_LEASES: AtomicUsize = AtomicUsize::new(0);
54
55/// A claim that important work needs the host's background execution allowance.
56/// The allowance remains active until every outstanding lease is dropped.
57pub struct BackgroundWorkLease {
58 active: bool,
59}
60
61impl Drop for BackgroundWorkLease {
62 fn drop(&mut self) {
63 if !self.active {
64 return;
65 }
66 self.active = false;
67 if ACTIVE_LEASES.fetch_sub(1, Ordering::AcqRel) == 1
68 && let Some(activity) = background_activity()
69 {
70 activity.set_active(false);
71 }
72 }
73}
74
75/// Acquires background execution for one independent operation.
76pub fn acquire_background_work() -> BackgroundWorkLease {
77 if ACTIVE_LEASES.fetch_add(1, Ordering::AcqRel) == 0
78 && let Some(activity) = background_activity()
79 {
80 activity.set_active(true);
81 }
82 BackgroundWorkLease { active: true }
83}
84
85/// Whether at least one background-work lease is active.
86///
87/// A platform backend reads this to decide whether the runtime keeps turning
88/// while the app is off screen. Both mobile backends drive composition from the
89/// frame path, and both stop that path when the surface is gone: Android drops
90/// its GPU resources on `TerminateWindow`, iOS stops receiving redraws. Anything
91/// posted to the UI dispatcher then waits until the user comes back, which is
92/// wrong for an app that told the OS it has work to finish.
93pub fn background_active() -> bool {
94 ACTIVE_LEASES.load(Ordering::Acquire) != 0
95}
96
97#[cfg(test)]
98#[path = "tests/background_tests.rs"]
99mod tests;