Skip to main content

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;