tauri_plugin_background_service/
capabilities.rs1use crate::models::{LifecycleGuarantee, LifecycleMode, Platform, PlatformCapabilities};
9
10pub struct CapabilityProvider;
16
17impl CapabilityProvider {
18 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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 #[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}