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 (background-fetch, background-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 #[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 #[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 #[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 #[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 #[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 #[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}