Skip to main content

tauri_plugin_background_service/
capabilities.rs

1//! Platform capability reporting.
2//!
3//! [`CapabilityProvider`] builds [`PlatformCapabilities`]
4//! per platform based on the plugin's knowledge of OS-specific background execution guarantees.
5//! Each platform has different survival characteristics — the provider reports them honestly
6//! without overpromising.
7
8use crate::models::{LifecycleGuarantee, LifecycleMode, Platform, PlatformCapabilities};
9
10/// Builds platform-specific background execution capabilities.
11///
12/// Exposes per-platform methods for direct testing and a unified
13/// [`CapabilityProvider::capabilities`] entry point used by the
14/// `get_platform_capabilities` Tauri command.
15pub struct CapabilityProvider;
16
17impl CapabilityProvider {
18    /// Returns capabilities for Android.
19    ///
20    /// Android uses a foreground service (FGS) for background execution.
21    pub fn android() -> PlatformCapabilities {
22        PlatformCapabilities {
23            platform: Platform::Android,
24            lifecycle_mode: LifecycleMode::AndroidForegroundService,
25            survives_app_close: LifecycleGuarantee::BestEffort,
26            survives_reboot: LifecycleGuarantee::BestEffort,
27            survives_force_quit: LifecycleGuarantee::Unsupported,
28            background_execution: LifecycleGuarantee::Guaranteed,
29            limitations: vec![
30                "OEM battery optimization may kill foreground services".into(),
31                "Force stop suppresses receivers and jobs until user launches app".into(),
32                "Android 15 dataSync foreground service has 6-hour cumulative timeout per 24h window".into(),
33                "Boot receiver cannot start dataSync FGS on Android 15+".into(),
34            ],
35            required_setup: vec![
36                "FOREGROUND_SERVICE permission in manifest".into(),
37                "Foreground service type and matching permission declared".into(),
38                "Persistent notification channel configured".into(),
39            ],
40        }
41    }
42
43    /// Returns capabilities for iOS.
44    ///
45    /// iOS uses `BGTaskScheduler` for background execution.
46    pub fn ios() -> PlatformCapabilities {
47        PlatformCapabilities {
48            platform: Platform::Ios,
49            lifecycle_mode: LifecycleMode::IosBgTaskScheduler,
50            survives_app_close: LifecycleGuarantee::BestEffort,
51            survives_reboot: LifecycleGuarantee::BestEffort,
52            survives_force_quit: LifecycleGuarantee::Unsupported,
53            background_execution: LifecycleGuarantee::BestEffort,
54            limitations: vec![
55                "Cannot guarantee continuous background execution".into(),
56                "Force-quit makes app ineligible for BGTask relaunch".into(),
57                "BGAppRefreshTask has ~30s execution window".into(),
58                "BGProcessingTask has variable execution window (minutes to hours)".into(),
59            ],
60            required_setup: vec![
61                "UIBackgroundModes in Info.plist (fetch, processing)".into(),
62                "BGTaskSchedulerPermittedIdentifiers in Info.plist".into(),
63            ],
64        }
65    }
66
67    /// Returns capabilities for desktop in-process mode.
68    ///
69    /// The service runs in the same process as the app.
70    pub fn desktop_in_process(platform: Platform) -> PlatformCapabilities {
71        PlatformCapabilities {
72            platform,
73            lifecycle_mode: LifecycleMode::DesktopInProcess,
74            survives_app_close: LifecycleGuarantee::Unsupported,
75            survives_reboot: LifecycleGuarantee::Unsupported,
76            survives_force_quit: LifecycleGuarantee::Unsupported,
77            background_execution: LifecycleGuarantee::Guaranteed,
78            limitations: vec!["Service runs in-app process; terminates when app closes".into()],
79            required_setup: vec![],
80        }
81    }
82
83    /// Returns capabilities for desktop OS-service mode.
84    ///
85    /// When `installed_and_running` is `true`, survival guarantees reflect a
86    /// properly configured OS service. When `false`, they fall back to
87    /// `Unsupported` to indicate the service is not yet set up.
88    pub fn desktop_os_service(
89        platform: Platform,
90        installed_and_running: bool,
91    ) -> PlatformCapabilities {
92        let (survives_close, survives_reboot, bg_exec) = if installed_and_running {
93            (
94                LifecycleGuarantee::Guaranteed,
95                LifecycleGuarantee::Guaranteed,
96                LifecycleGuarantee::Guaranteed,
97            )
98        } else {
99            (
100                LifecycleGuarantee::Unsupported,
101                LifecycleGuarantee::Unsupported,
102                LifecycleGuarantee::Unsupported,
103            )
104        };
105
106        PlatformCapabilities {
107            platform,
108            lifecycle_mode: LifecycleMode::DesktopOsService,
109            survives_app_close: survives_close,
110            survives_reboot,
111            survives_force_quit: LifecycleGuarantee::Unsupported,
112            background_execution: bg_exec,
113            limitations: vec!["Force quit kills the OS service".into()],
114            required_setup: vec![
115                "OS service must be installed and configured".into(),
116                "Autostart must be enabled for reboot survival".into(),
117            ],
118        }
119    }
120
121    /// Detect the current platform and lifecycle mode based on cfg flags.
122    ///
123    /// For desktop, `desktop_service_mode` controls whether the mode is
124    /// `DesktopInProcess` or `DesktopOsService`.
125    pub fn detect_platform(desktop_service_mode: Option<&str>) -> (Platform, LifecycleMode) {
126        #[cfg(target_os = "android")]
127        {
128            let _ = desktop_service_mode;
129            (Platform::Android, LifecycleMode::AndroidForegroundService)
130        }
131
132        #[cfg(target_os = "ios")]
133        {
134            let _ = desktop_service_mode;
135            (Platform::Ios, LifecycleMode::IosBgTaskScheduler)
136        }
137
138        #[cfg(not(any(target_os = "android", target_os = "ios")))]
139        {
140            let platform = Self::desktop_platform();
141            let mode = match desktop_service_mode {
142                Some("osService") => LifecycleMode::DesktopOsService,
143                _ => LifecycleMode::DesktopInProcess,
144            };
145            (platform, mode)
146        }
147    }
148
149    /// Determine the desktop platform from the current OS.
150    #[cfg(not(any(target_os = "android", target_os = "ios")))]
151    fn desktop_platform() -> Platform {
152        #[cfg(target_os = "linux")]
153        {
154            Platform::Linux
155        }
156
157        #[cfg(target_os = "macos")]
158        {
159            Platform::Macos
160        }
161
162        #[cfg(target_os = "windows")]
163        {
164            Platform::Windows
165        }
166
167        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
168        {
169            Platform::Unknown
170        }
171    }
172
173    /// Build capabilities for the given platform, mode, and state.
174    ///
175    /// This is the main entry point for the `get_platform_capabilities` command.
176    pub fn capabilities(
177        platform: Platform,
178        lifecycle_mode: LifecycleMode,
179        os_service_installed: bool,
180    ) -> PlatformCapabilities {
181        match lifecycle_mode {
182            LifecycleMode::AndroidForegroundService => Self::android(),
183            LifecycleMode::IosBgTaskScheduler => Self::ios(),
184            LifecycleMode::DesktopInProcess => Self::desktop_in_process(platform),
185            LifecycleMode::DesktopOsService => {
186                Self::desktop_os_service(platform, os_service_installed)
187            }
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    /// Expected platform for the current desktop host, computed from the same
197    /// `cfg` flags `detect_platform`/`desktop_platform` use. `detect_platform`
198    /// is host-dependent, so tests must not hardcode a single `Platform`.
199    fn host_platform() -> Platform {
200        #[cfg(target_os = "linux")]
201        {
202            Platform::Linux
203        }
204        #[cfg(target_os = "macos")]
205        {
206            Platform::Macos
207        }
208        #[cfg(target_os = "windows")]
209        {
210            Platform::Windows
211        }
212        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
213        {
214            Platform::Unknown
215        }
216    }
217
218    // --- Android capabilities ---
219
220    #[test]
221    fn android_correct_platform_and_mode() {
222        let caps = CapabilityProvider::android();
223        assert_eq!(caps.platform, Platform::Android);
224        assert_eq!(caps.lifecycle_mode, LifecycleMode::AndroidForegroundService);
225    }
226
227    #[test]
228    fn android_survives_app_close_best_effort() {
229        assert_eq!(
230            CapabilityProvider::android().survives_app_close,
231            LifecycleGuarantee::BestEffort
232        );
233    }
234
235    #[test]
236    fn android_survives_reboot_best_effort() {
237        assert_eq!(
238            CapabilityProvider::android().survives_reboot,
239            LifecycleGuarantee::BestEffort
240        );
241    }
242
243    #[test]
244    fn android_survives_force_quit_unsupported() {
245        assert_eq!(
246            CapabilityProvider::android().survives_force_quit,
247            LifecycleGuarantee::Unsupported
248        );
249    }
250
251    #[test]
252    fn android_background_execution_guaranteed() {
253        assert_eq!(
254            CapabilityProvider::android().background_execution,
255            LifecycleGuarantee::Guaranteed
256        );
257    }
258
259    #[test]
260    fn android_limitations_non_empty() {
261        let caps = CapabilityProvider::android();
262        assert!(!caps.limitations.is_empty());
263        for l in &caps.limitations {
264            assert!(!l.is_empty(), "limitation strings must not be empty");
265        }
266    }
267
268    #[test]
269    fn android_required_setup_non_empty() {
270        let caps = CapabilityProvider::android();
271        assert!(!caps.required_setup.is_empty());
272    }
273
274    // --- iOS capabilities ---
275
276    #[test]
277    fn ios_correct_platform_and_mode() {
278        let caps = CapabilityProvider::ios();
279        assert_eq!(caps.platform, Platform::Ios);
280        assert_eq!(caps.lifecycle_mode, LifecycleMode::IosBgTaskScheduler);
281    }
282
283    #[test]
284    fn ios_survives_app_close_best_effort() {
285        assert_eq!(
286            CapabilityProvider::ios().survives_app_close,
287            LifecycleGuarantee::BestEffort
288        );
289    }
290
291    #[test]
292    fn ios_survives_reboot_best_effort() {
293        assert_eq!(
294            CapabilityProvider::ios().survives_reboot,
295            LifecycleGuarantee::BestEffort
296        );
297    }
298
299    #[test]
300    fn ios_survives_force_quit_unsupported() {
301        assert_eq!(
302            CapabilityProvider::ios().survives_force_quit,
303            LifecycleGuarantee::Unsupported
304        );
305    }
306
307    #[test]
308    fn ios_background_execution_best_effort() {
309        assert_eq!(
310            CapabilityProvider::ios().background_execution,
311            LifecycleGuarantee::BestEffort
312        );
313    }
314
315    #[test]
316    fn ios_limitations_non_empty() {
317        let caps = CapabilityProvider::ios();
318        assert!(!caps.limitations.is_empty());
319        for l in &caps.limitations {
320            assert!(!l.is_empty());
321        }
322    }
323
324    // --- Desktop in-process ---
325
326    #[test]
327    fn desktop_in_process_correct_mode() {
328        let caps = CapabilityProvider::desktop_in_process(Platform::Linux);
329        assert_eq!(caps.platform, Platform::Linux);
330        assert_eq!(caps.lifecycle_mode, LifecycleMode::DesktopInProcess);
331    }
332
333    #[test]
334    fn desktop_in_process_survives_app_close_unsupported() {
335        assert_eq!(
336            CapabilityProvider::desktop_in_process(Platform::Linux).survives_app_close,
337            LifecycleGuarantee::Unsupported
338        );
339    }
340
341    #[test]
342    fn desktop_in_process_survives_reboot_unsupported() {
343        assert_eq!(
344            CapabilityProvider::desktop_in_process(Platform::Linux).survives_reboot,
345            LifecycleGuarantee::Unsupported
346        );
347    }
348
349    #[test]
350    fn desktop_in_process_background_execution_guaranteed() {
351        assert_eq!(
352            CapabilityProvider::desktop_in_process(Platform::Linux).background_execution,
353            LifecycleGuarantee::Guaranteed
354        );
355    }
356
357    #[test]
358    fn desktop_in_process_preserves_platform() {
359        assert_eq!(
360            CapabilityProvider::desktop_in_process(Platform::Linux).platform,
361            Platform::Linux
362        );
363        assert_eq!(
364            CapabilityProvider::desktop_in_process(Platform::Macos).platform,
365            Platform::Macos
366        );
367        assert_eq!(
368            CapabilityProvider::desktop_in_process(Platform::Windows).platform,
369            Platform::Windows
370        );
371    }
372
373    #[test]
374    fn desktop_in_process_limitations_non_empty() {
375        let caps = CapabilityProvider::desktop_in_process(Platform::Linux);
376        assert!(
377            !caps.limitations.is_empty(),
378            "in-process limitations must not be empty"
379        );
380        for l in &caps.limitations {
381            assert!(!l.is_empty(), "limitation strings must not be empty");
382        }
383    }
384
385    // --- Desktop OS-service ---
386
387    #[test]
388    fn desktop_os_service_installed_reports_guaranteed() {
389        let caps = CapabilityProvider::desktop_os_service(Platform::Linux, true);
390        assert_eq!(caps.platform, Platform::Linux);
391        assert_eq!(caps.lifecycle_mode, LifecycleMode::DesktopOsService);
392        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Guaranteed);
393        assert_eq!(caps.survives_reboot, LifecycleGuarantee::Guaranteed);
394        assert_eq!(caps.background_execution, LifecycleGuarantee::Guaranteed);
395    }
396
397    #[test]
398    fn desktop_os_service_not_installed_reports_unsupported() {
399        let caps = CapabilityProvider::desktop_os_service(Platform::Linux, false);
400        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Unsupported);
401        assert_eq!(caps.survives_reboot, LifecycleGuarantee::Unsupported);
402        assert_eq!(caps.background_execution, LifecycleGuarantee::Unsupported);
403    }
404
405    #[test]
406    fn desktop_os_service_force_quit_always_unsupported() {
407        assert_eq!(
408            CapabilityProvider::desktop_os_service(Platform::Linux, true).survives_force_quit,
409            LifecycleGuarantee::Unsupported
410        );
411        assert_eq!(
412            CapabilityProvider::desktop_os_service(Platform::Linux, false).survives_force_quit,
413            LifecycleGuarantee::Unsupported
414        );
415    }
416
417    #[test]
418    fn desktop_os_service_limitations_non_empty() {
419        for installed in [true, false] {
420            let caps = CapabilityProvider::desktop_os_service(Platform::Linux, installed);
421            assert!(
422                !caps.limitations.is_empty(),
423                "os-service limitations must not be empty (installed={installed})"
424            );
425            for l in &caps.limitations {
426                assert!(!l.is_empty(), "limitation strings must not be empty");
427            }
428        }
429    }
430
431    // --- capabilities() dispatch ---
432
433    #[test]
434    fn capabilities_dispatches_to_android() {
435        let caps = CapabilityProvider::capabilities(
436            Platform::Android,
437            LifecycleMode::AndroidForegroundService,
438            false,
439        );
440        assert_eq!(caps.platform, Platform::Android);
441    }
442
443    #[test]
444    fn capabilities_dispatches_to_ios() {
445        let caps = CapabilityProvider::capabilities(
446            Platform::Ios,
447            LifecycleMode::IosBgTaskScheduler,
448            false,
449        );
450        assert_eq!(caps.platform, Platform::Ios);
451    }
452
453    #[test]
454    fn capabilities_dispatches_to_desktop_in_process() {
455        let caps = CapabilityProvider::capabilities(
456            Platform::Linux,
457            LifecycleMode::DesktopInProcess,
458            false,
459        );
460        assert_eq!(caps.lifecycle_mode, LifecycleMode::DesktopInProcess);
461        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Unsupported);
462    }
463
464    #[test]
465    fn capabilities_dispatches_to_desktop_os_service_installed() {
466        let caps = CapabilityProvider::capabilities(
467            Platform::Linux,
468            LifecycleMode::DesktopOsService,
469            true,
470        );
471        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Guaranteed);
472    }
473
474    #[test]
475    fn capabilities_dispatches_to_desktop_os_service_not_installed() {
476        let caps = CapabilityProvider::capabilities(
477            Platform::Linux,
478            LifecycleMode::DesktopOsService,
479            false,
480        );
481        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Unsupported);
482    }
483
484    // --- detect_platform (host-dependent: expected platform from `cfg`) ---
485
486    #[test]
487    fn detect_platform_desktop_default_is_in_process() {
488        let (platform, mode) = CapabilityProvider::detect_platform(None);
489        assert_eq!(platform, host_platform());
490        assert_eq!(mode, LifecycleMode::DesktopInProcess);
491    }
492
493    #[test]
494    fn detect_platform_desktop_os_service_mode() {
495        let (platform, mode) = CapabilityProvider::detect_platform(Some("osService"));
496        assert_eq!(platform, host_platform());
497        assert_eq!(mode, LifecycleMode::DesktopOsService);
498    }
499
500    #[test]
501    fn detect_platform_desktop_in_process_explicit() {
502        let (platform, mode) = CapabilityProvider::detect_platform(Some("inProcess"));
503        assert_eq!(platform, host_platform());
504        assert_eq!(mode, LifecycleMode::DesktopInProcess);
505    }
506}