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 (background-fetch, background-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    // --- Android capabilities ---
197
198    #[test]
199    fn android_correct_platform_and_mode() {
200        let caps = CapabilityProvider::android();
201        assert_eq!(caps.platform, Platform::Android);
202        assert_eq!(caps.lifecycle_mode, LifecycleMode::AndroidForegroundService);
203    }
204
205    #[test]
206    fn android_survives_app_close_best_effort() {
207        assert_eq!(
208            CapabilityProvider::android().survives_app_close,
209            LifecycleGuarantee::BestEffort
210        );
211    }
212
213    #[test]
214    fn android_survives_reboot_best_effort() {
215        assert_eq!(
216            CapabilityProvider::android().survives_reboot,
217            LifecycleGuarantee::BestEffort
218        );
219    }
220
221    #[test]
222    fn android_survives_force_quit_unsupported() {
223        assert_eq!(
224            CapabilityProvider::android().survives_force_quit,
225            LifecycleGuarantee::Unsupported
226        );
227    }
228
229    #[test]
230    fn android_background_execution_guaranteed() {
231        assert_eq!(
232            CapabilityProvider::android().background_execution,
233            LifecycleGuarantee::Guaranteed
234        );
235    }
236
237    #[test]
238    fn android_limitations_non_empty() {
239        let caps = CapabilityProvider::android();
240        assert!(!caps.limitations.is_empty());
241        for l in &caps.limitations {
242            assert!(!l.is_empty(), "limitation strings must not be empty");
243        }
244    }
245
246    #[test]
247    fn android_required_setup_non_empty() {
248        let caps = CapabilityProvider::android();
249        assert!(!caps.required_setup.is_empty());
250    }
251
252    // --- iOS capabilities ---
253
254    #[test]
255    fn ios_correct_platform_and_mode() {
256        let caps = CapabilityProvider::ios();
257        assert_eq!(caps.platform, Platform::Ios);
258        assert_eq!(caps.lifecycle_mode, LifecycleMode::IosBgTaskScheduler);
259    }
260
261    #[test]
262    fn ios_survives_app_close_best_effort() {
263        assert_eq!(
264            CapabilityProvider::ios().survives_app_close,
265            LifecycleGuarantee::BestEffort
266        );
267    }
268
269    #[test]
270    fn ios_survives_reboot_best_effort() {
271        assert_eq!(
272            CapabilityProvider::ios().survives_reboot,
273            LifecycleGuarantee::BestEffort
274        );
275    }
276
277    #[test]
278    fn ios_survives_force_quit_unsupported() {
279        assert_eq!(
280            CapabilityProvider::ios().survives_force_quit,
281            LifecycleGuarantee::Unsupported
282        );
283    }
284
285    #[test]
286    fn ios_background_execution_best_effort() {
287        assert_eq!(
288            CapabilityProvider::ios().background_execution,
289            LifecycleGuarantee::BestEffort
290        );
291    }
292
293    #[test]
294    fn ios_limitations_non_empty() {
295        let caps = CapabilityProvider::ios();
296        assert!(!caps.limitations.is_empty());
297        for l in &caps.limitations {
298            assert!(!l.is_empty());
299        }
300    }
301
302    // --- Desktop in-process ---
303
304    #[test]
305    fn desktop_in_process_correct_mode() {
306        let caps = CapabilityProvider::desktop_in_process(Platform::Linux);
307        assert_eq!(caps.platform, Platform::Linux);
308        assert_eq!(caps.lifecycle_mode, LifecycleMode::DesktopInProcess);
309    }
310
311    #[test]
312    fn desktop_in_process_survives_app_close_unsupported() {
313        assert_eq!(
314            CapabilityProvider::desktop_in_process(Platform::Linux).survives_app_close,
315            LifecycleGuarantee::Unsupported
316        );
317    }
318
319    #[test]
320    fn desktop_in_process_survives_reboot_unsupported() {
321        assert_eq!(
322            CapabilityProvider::desktop_in_process(Platform::Linux).survives_reboot,
323            LifecycleGuarantee::Unsupported
324        );
325    }
326
327    #[test]
328    fn desktop_in_process_background_execution_guaranteed() {
329        assert_eq!(
330            CapabilityProvider::desktop_in_process(Platform::Linux).background_execution,
331            LifecycleGuarantee::Guaranteed
332        );
333    }
334
335    #[test]
336    fn desktop_in_process_preserves_platform() {
337        assert_eq!(
338            CapabilityProvider::desktop_in_process(Platform::Linux).platform,
339            Platform::Linux
340        );
341        assert_eq!(
342            CapabilityProvider::desktop_in_process(Platform::Macos).platform,
343            Platform::Macos
344        );
345        assert_eq!(
346            CapabilityProvider::desktop_in_process(Platform::Windows).platform,
347            Platform::Windows
348        );
349    }
350
351    #[test]
352    fn desktop_in_process_limitations_non_empty() {
353        let caps = CapabilityProvider::desktop_in_process(Platform::Linux);
354        assert!(
355            !caps.limitations.is_empty(),
356            "in-process limitations must not be empty"
357        );
358        for l in &caps.limitations {
359            assert!(!l.is_empty(), "limitation strings must not be empty");
360        }
361    }
362
363    // --- Desktop OS-service ---
364
365    #[test]
366    fn desktop_os_service_installed_reports_guaranteed() {
367        let caps = CapabilityProvider::desktop_os_service(Platform::Linux, true);
368        assert_eq!(caps.platform, Platform::Linux);
369        assert_eq!(caps.lifecycle_mode, LifecycleMode::DesktopOsService);
370        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Guaranteed);
371        assert_eq!(caps.survives_reboot, LifecycleGuarantee::Guaranteed);
372        assert_eq!(caps.background_execution, LifecycleGuarantee::Guaranteed);
373    }
374
375    #[test]
376    fn desktop_os_service_not_installed_reports_unsupported() {
377        let caps = CapabilityProvider::desktop_os_service(Platform::Linux, false);
378        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Unsupported);
379        assert_eq!(caps.survives_reboot, LifecycleGuarantee::Unsupported);
380        assert_eq!(caps.background_execution, LifecycleGuarantee::Unsupported);
381    }
382
383    #[test]
384    fn desktop_os_service_force_quit_always_unsupported() {
385        assert_eq!(
386            CapabilityProvider::desktop_os_service(Platform::Linux, true).survives_force_quit,
387            LifecycleGuarantee::Unsupported
388        );
389        assert_eq!(
390            CapabilityProvider::desktop_os_service(Platform::Linux, false).survives_force_quit,
391            LifecycleGuarantee::Unsupported
392        );
393    }
394
395    #[test]
396    fn desktop_os_service_limitations_non_empty() {
397        for installed in [true, false] {
398            let caps = CapabilityProvider::desktop_os_service(Platform::Linux, installed);
399            assert!(
400                !caps.limitations.is_empty(),
401                "os-service limitations must not be empty (installed={installed})"
402            );
403            for l in &caps.limitations {
404                assert!(!l.is_empty(), "limitation strings must not be empty");
405            }
406        }
407    }
408
409    // --- capabilities() dispatch ---
410
411    #[test]
412    fn capabilities_dispatches_to_android() {
413        let caps = CapabilityProvider::capabilities(
414            Platform::Android,
415            LifecycleMode::AndroidForegroundService,
416            false,
417        );
418        assert_eq!(caps.platform, Platform::Android);
419    }
420
421    #[test]
422    fn capabilities_dispatches_to_ios() {
423        let caps = CapabilityProvider::capabilities(
424            Platform::Ios,
425            LifecycleMode::IosBgTaskScheduler,
426            false,
427        );
428        assert_eq!(caps.platform, Platform::Ios);
429    }
430
431    #[test]
432    fn capabilities_dispatches_to_desktop_in_process() {
433        let caps = CapabilityProvider::capabilities(
434            Platform::Linux,
435            LifecycleMode::DesktopInProcess,
436            false,
437        );
438        assert_eq!(caps.lifecycle_mode, LifecycleMode::DesktopInProcess);
439        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Unsupported);
440    }
441
442    #[test]
443    fn capabilities_dispatches_to_desktop_os_service_installed() {
444        let caps = CapabilityProvider::capabilities(
445            Platform::Linux,
446            LifecycleMode::DesktopOsService,
447            true,
448        );
449        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Guaranteed);
450    }
451
452    #[test]
453    fn capabilities_dispatches_to_desktop_os_service_not_installed() {
454        let caps = CapabilityProvider::capabilities(
455            Platform::Linux,
456            LifecycleMode::DesktopOsService,
457            false,
458        );
459        assert_eq!(caps.survives_app_close, LifecycleGuarantee::Unsupported);
460    }
461
462    // --- detect_platform (runs on Linux) ---
463
464    #[test]
465    fn detect_platform_desktop_default_is_in_process() {
466        let (platform, mode) = CapabilityProvider::detect_platform(None);
467        assert_eq!(platform, Platform::Linux);
468        assert_eq!(mode, LifecycleMode::DesktopInProcess);
469    }
470
471    #[test]
472    fn detect_platform_desktop_os_service_mode() {
473        let (platform, mode) = CapabilityProvider::detect_platform(Some("osService"));
474        assert_eq!(platform, Platform::Linux);
475        assert_eq!(mode, LifecycleMode::DesktopOsService);
476    }
477
478    #[test]
479    fn detect_platform_desktop_in_process_explicit() {
480        let (platform, mode) = CapabilityProvider::detect_platform(Some("inProcess"));
481        assert_eq!(platform, Platform::Linux);
482        assert_eq!(mode, LifecycleMode::DesktopInProcess);
483    }
484}