Line data Source code
1 : /// Defines when a task should be executed.
2 : sealed class TaskTrigger {
3 12 : const TaskTrigger();
4 :
5 : /// Convert to map for platform channel.
6 : Map<String, dynamic> toMap();
7 :
8 : /// Execute once after an optional delay.
9 : ///
10 : /// The most common trigger type. Schedules a task to run once, either
11 : /// immediately or after a specified delay.
12 : ///
13 : /// ## Immediate Execution
14 : ///
15 : /// ```dart
16 : /// await NativeWorkManager.enqueue(
17 : /// taskId: 'immediate-task',
18 : /// trigger: TaskTrigger.oneTime(),
19 : /// worker: NativeWorker.httpRequest(url: 'https://api.example.com/ping'),
20 : /// );
21 : /// ```
22 : ///
23 : /// ## Delayed Execution
24 : ///
25 : /// ```dart
26 : /// // Execute after 5 minutes
27 : /// await NativeWorkManager.enqueue(
28 : /// taskId: 'delayed-task',
29 : /// trigger: TaskTrigger.oneTime(Duration(minutes: 5)),
30 : /// worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
31 : /// );
32 : ///
33 : /// // Execute after 1 hour
34 : /// await NativeWorkManager.enqueue(
35 : /// taskId: 'hourly-reminder',
36 : /// trigger: TaskTrigger.oneTime(Duration(hours: 1)),
37 : /// worker: DartWorker(callbackId: 'sendNotification'),
38 : /// );
39 : /// ```
40 : ///
41 : /// ## Platform Behavior
42 : ///
43 : /// **Android:**
44 : /// - Immediate tasks (no delay) execute as soon as constraints are met
45 : /// - Delayed tasks use WorkManager's initial delay
46 : /// - Timing is approximate (not exact)
47 : /// - OS may defer execution to optimize battery
48 : ///
49 : /// **iOS:**
50 : /// - Uses BGProcessingTask for background execution
51 : /// - Execution timing is opportunistic (OS decides)
52 : /// - May not run immediately even with zero delay
53 : /// - Requires app to be in background for reasonable time
54 : ///
55 : /// ## Common Pitfalls
56 : ///
57 : /// ❌ **Don't** expect exact timing (use `exact()` for that)
58 : /// ❌ **Don't** use for time-critical operations
59 : /// ✅ **Do** use for most one-time background tasks
60 : /// ✅ **Do** add appropriate constraints
61 : ///
62 : /// ## See Also
63 : ///
64 : /// - [periodic] - For recurring tasks
65 : /// - [exact] - For alarm-style exact timing
66 : /// - [windowed] - For execution within a time range
67 : const factory TaskTrigger.oneTime([Duration initialDelay]) = OneTimeTrigger;
68 :
69 : /// Execute periodically at a fixed interval.
70 : ///
71 : /// Schedules a task to run repeatedly at the specified interval. Perfect for
72 : /// data syncing, periodic updates, or scheduled cleanup operations.
73 : ///
74 : /// **Important:** Minimum interval is 15 minutes on Android (OS limitation).
75 : ///
76 : /// ## Basic Periodic Task
77 : ///
78 : /// ```dart
79 : /// // Sync every hour
80 : /// await NativeWorkManager.enqueue(
81 : /// taskId: 'hourly-sync',
82 : /// trigger: TaskTrigger.periodic(Duration(hours: 1)),
83 : /// worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
84 : /// constraints: Constraints.networkRequired,
85 : /// );
86 : /// ```
87 : ///
88 : /// ## Daily Cleanup
89 : ///
90 : /// ```dart
91 : /// // Clean up cache daily at opportune time
92 : /// await NativeWorkManager.enqueue(
93 : /// taskId: 'daily-cleanup',
94 : /// trigger: TaskTrigger.periodic(Duration(days: 1)),
95 : /// worker: DartWorker(callbackId: 'cleanupCache'),
96 : /// );
97 : /// ```
98 : ///
99 : /// ## With Flex Interval (Android)
100 : ///
101 : /// ```dart
102 : /// // Sync every 6 hours, with 30-minute flex window
103 : /// // Task can run between 5.5 and 6 hours after last execution
104 : /// await NativeWorkManager.enqueue(
105 : /// taskId: 'flexible-sync',
106 : /// trigger: TaskTrigger.periodic(
107 : /// Duration(hours: 6),
108 : /// flexInterval: Duration(minutes: 30),
109 : /// ),
110 : /// worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
111 : /// );
112 : /// ```
113 : ///
114 : /// ## Minimum Interval Example
115 : ///
116 : /// ```dart
117 : /// // Minimum allowed interval (15 minutes)
118 : /// await NativeWorkManager.enqueue(
119 : /// taskId: 'frequent-check',
120 : /// trigger: TaskTrigger.periodic(Duration(minutes: 15)),
121 : /// worker: NativeWorker.httpRequest(url: 'https://api.example.com/status'),
122 : /// );
123 : /// ```
124 : ///
125 : /// ## Periodic Task with Initial Delay
126 : ///
127 : /// ```dart
128 : /// // Run every hour, but wait for the first hour before starting
129 : /// await NativeWorkManager.enqueue(
130 : /// taskId: 'delayed-periodic-sync',
131 : /// trigger: TaskTrigger.periodic(
132 : /// Duration(hours: 1),
133 : /// initialDelay: Duration(hours: 1),
134 : /// ),
135 : /// worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
136 : /// );
137 : /// ```
138 : ///
139 : /// ## Skip First Run
140 : ///
141 : /// ```dart
142 : /// // Run every day, but don't run right now - wait for the first 24h
143 : /// await NativeWorkManager.enqueue(
144 : /// taskId: 'daily-sync',
145 : /// trigger: TaskTrigger.periodic(
146 : /// Duration(days: 1),
147 : /// runImmediately: false,
148 : /// ),
149 : /// worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
150 : /// );
151 : /// ```
152 : ///
153 : /// ## Parameters
154 : ///
155 : /// **[interval]** - Time between executions.
156 : /// - Must be at least 15 minutes on Android
157 : /// - Throws `ArgumentError` if less than 15 minutes
158 : /// - iOS uses this as a hint (not guaranteed)
159 : ///
160 : /// **[flexInterval]** *(optional)* - Flex time window (Android only).
161 : /// - Allows Android to optimize execution time within this window
162 : /// - Example: 6-hour interval with 30-min flex = execute between 5.5-6 hours
163 : /// - Improves battery life by batching work
164 : /// - Ignored on iOS
165 : ///
166 : /// **[initialDelay]** *(optional)* - Delay before the very first execution.
167 : /// - On Android, the task will only run after this delay has passed.
168 : /// - Useful for scheduling tasks that shouldn't run immediately upon registration.
169 : /// - Supported on iOS (mapped to `earliestBeginDate`).
170 : ///
171 : /// **[runImmediately]** *(optional)* - Whether to run the task immediately.
172 : /// - Defaults to `true`.
173 : /// - If `false`, the first execution will happen after one [interval] has passed.
174 : /// - On Android, this is natively supported in WorkManager 2.1+.
175 : /// - On iOS, this is simulated by setting an initial delay equal to [interval].
176 : ///
177 : /// ## Platform Behavior
178 : ///
179 : /// **Android:**
180 : /// - Uses WorkManager PeriodicWorkRequest
181 : /// - Minimum interval: 15 minutes (OS limitation)
182 : /// - Flex interval helps Android optimize battery usage
183 : /// - Initial delay allows delaying the first run (WorkManager 2.1+)
184 : /// - Timing is approximate, not exact
185 : ///
186 : /// **iOS:**
187 : /// - Uses BGAppRefreshTask
188 : /// - Interval is a suggestion, not guaranteed
189 : /// - Initial delay supported via `earliestBeginDate`
190 : /// - OS decides actual execution timing
191 : /// - May run less frequently to save battery
192 : ///
193 : /// ## When to Use
194 : ///
195 : /// ✅ **Use periodic trigger for:**
196 : /// - Data synchronization every N hours
197 : /// - Periodic content updates
198 : /// - Scheduled cleanup operations
199 : /// - Background refresh of local data
200 : ///
201 : /// ❌ **Don't use periodic for:**
202 : /// - Intervals < 15 minutes (will throw error)
203 : /// - Time-sensitive operations (use `exact` instead)
204 : /// - User-initiated actions (use `oneTime` instead)
205 : ///
206 : /// ## Common Pitfalls
207 : ///
208 : /// ❌ **Don't** use intervals less than 15 minutes
209 : /// ❌ **Don't** expect exact timing (OS optimizes for battery)
210 : /// ❌ **Don't** rely on precise scheduling on iOS
211 : /// ❌ **Don't** schedule too many periodic tasks (battery drain)
212 : /// ✅ **Do** use flexInterval on Android for better battery life
213 : /// ✅ **Do** use initialDelay if the first run shouldn't happen immediately
214 : /// ✅ **Do** use `runImmediately: false` to skip the first execution
215 : /// ✅ **Do** combine with network constraints for data sync
216 : ///
217 : /// ## Battery Impact
218 : ///
219 : /// Periodic tasks can impact battery life:
220 : /// - Use longest acceptable interval
221 : /// - Add flex interval on Android
222 : /// - Use appropriate constraints (WiFi, charging)
223 : /// - Keep task execution time short
224 : ///
225 : /// ## See Also
226 : ///
227 : /// - [oneTime] - For one-time execution
228 : /// - [windowed] - For execution within a time window
229 : /// - [Constraints] - To optimize battery usage
230 : const factory TaskTrigger.periodic(
231 : Duration interval, {
232 : Duration? flexInterval,
233 : Duration? initialDelay,
234 : bool runImmediately,
235 : }) = PeriodicTrigger;
236 :
237 : /// Execute at an exact time (alarm-style).
238 : ///
239 : /// Schedules a task to run at a specific DateTime. Unlike oneTime, this
240 : /// attempts to execute at the EXACT specified time, like an alarm.
241 : ///
242 : /// **⚠️ Platform Limitations:** Exact timing has significant limitations,
243 : /// especially on iOS. Consider if you really need exact timing before using.
244 : ///
245 : /// ## Schedule for Specific Time
246 : ///
247 : /// ```dart
248 : /// // Schedule for tomorrow at 9 AM
249 : /// final tomorrow9am = DateTime.now()
250 : /// .add(Duration(days: 1))
251 : /// .copyWith(hour: 9, minute: 0, second: 0);
252 : ///
253 : /// await NativeWorkManager.enqueue(
254 : /// taskId: 'morning-reminder',
255 : /// trigger: TaskTrigger.exact(tomorrow9am),
256 : /// worker: DartWorker(callbackId: 'sendReminder'),
257 : /// );
258 : /// ```
259 : ///
260 : /// ## Schedule for 2 Hours from Now
261 : ///
262 : /// ```dart
263 : /// await NativeWorkManager.enqueue(
264 : /// taskId: 'delayed-notification',
265 : /// trigger: TaskTrigger.exact(
266 : /// DateTime.now().add(Duration(hours: 2)),
267 : /// ),
268 : /// worker: DartWorker(callbackId: 'showNotification'),
269 : /// );
270 : /// ```
271 : ///
272 : /// ## Platform Behavior & Limitations
273 : ///
274 : /// **Android (API 31+):**
275 : /// - Uses AlarmManager for exact timing
276 : /// - **Requires SCHEDULE_EXACT_ALARM permission** (auto-granted)
277 : /// - User can revoke permission in Settings
278 : /// - Task may fail if permission revoked
279 : /// - Battery optimization may still defer task
280 : /// - Most reliable of the two platforms
281 : ///
282 : /// **iOS:**
283 : /// - **Severely limited - NOT recommended**
284 : /// - Cannot guarantee code execution at exact time
285 : /// - Uses UNNotification as workaround
286 : /// - Requires user interaction to run code
287 : /// - Not suitable for background tasks
288 : /// - Consider using `oneTime` or `windowed` instead
289 : ///
290 : /// ## When to Use
291 : ///
292 : /// ✅ **Use exact trigger for (Android only):**
293 : /// - Alarm clock functionality
294 : /// - Scheduled reminders
295 : /// - Time-sensitive operations
296 : /// - Exact appointment notifications
297 : ///
298 : /// ❌ **Don't use exact trigger for:**
299 : /// - iOS apps (very limited)
300 : /// - Background data sync (use `periodic` instead)
301 : /// - Flexible timing tasks (use `oneTime` or `windowed`)
302 : /// - Battery-sensitive operations
303 : ///
304 : /// ## Common Pitfalls
305 : ///
306 : /// ❌ **Don't** rely on this for iOS (use notifications instead)
307 : /// ❌ **Don't** assume permission is always granted on Android
308 : /// ❌ **Don't** use for routine background tasks
309 : /// ❌ **Don't** forget to check if scheduledTime is in future
310 : /// ✅ **Do** check if time is in future before scheduling
311 : /// ✅ **Do** handle Android permission denial gracefully
312 : /// ✅ **Do** consider `oneTime` or `windowed` alternatives
313 : /// ✅ **Do** use local notifications for UI alerts
314 : ///
315 : /// ## Alternative Solutions
316 : ///
317 : /// For most use cases, consider these alternatives:
318 : ///
319 : /// ```dart
320 : /// // Instead of exact alarm, use windowed:
321 : /// TaskTrigger.windowed(
322 : /// earliest: Duration(hours: 2),
323 : /// latest: Duration(hours: 2, minutes: 15),
324 : /// )
325 : ///
326 : /// // Or use delayed oneTime:
327 : /// TaskTrigger.oneTime(Duration(hours: 2))
328 : ///
329 : /// // For UI notifications, use flutter_local_notifications:
330 : /// await flutterLocalNotificationsPlugin.zonedSchedule(
331 : /// id,
332 : /// title,
333 : /// body,
334 : /// scheduledDateTime,
335 : /// notificationDetails,
336 : /// );
337 : /// ```
338 : ///
339 : /// ## Permission Handling (Android)
340 : ///
341 : /// ```dart
342 : /// // Check if exact alarm permission is available
343 : /// if (Platform.isAndroid && Build.VERSION.SDK_INT >= 31) {
344 : /// final alarmManager = AlarmManager();
345 : /// if (!await alarmManager.canScheduleExactAlarms()) {
346 : /// // Show dialog explaining need for permission
347 : /// // Direct user to settings
348 : /// }
349 : /// }
350 : /// ```
351 : ///
352 : /// ## See Also
353 : ///
354 : /// - [oneTime] - For flexible one-time execution
355 : /// - [windowed] - For execution within a time window
356 : /// - [periodic] - For recurring tasks
357 : const factory TaskTrigger.exact(DateTime scheduledTime) = ExactTrigger;
358 :
359 : /// Execute within a time window.
360 : ///
361 : /// Schedules a task to run sometime between two time points. More flexible
362 : /// than exact timing, allowing the OS to optimize battery usage by choosing
363 : /// the best time within the window.
364 : ///
365 : /// ## Execute Between 1-2 Hours from Now
366 : ///
367 : /// ```dart
368 : /// await NativeWorkManager.enqueue(
369 : /// taskId: 'flexible-sync',
370 : /// trigger: TaskTrigger.windowed(
371 : /// earliest: Duration(hours: 1),
372 : /// latest: Duration(hours: 2),
373 : /// ),
374 : /// worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
375 : /// );
376 : /// ```
377 : ///
378 : /// ## Night-Time Processing
379 : ///
380 : /// ```dart
381 : /// // Execute sometime between 2-4 AM (low usage period)
382 : /// await NativeWorkManager.enqueue(
383 : /// taskId: 'night-processing',
384 : /// trigger: TaskTrigger.windowed(
385 : /// earliest: Duration(hours: 6), // 6 hours from now
386 : /// latest: Duration(hours: 8), // 8 hours from now
387 : /// ),
388 : /// worker: DartWorker(callbackId: 'processLargeDataset'),
389 : /// constraints: Constraints(
390 : /// requiresCharging: true,
391 : /// requiresWifi: true,
392 : /// ),
393 : /// );
394 : /// ```
395 : ///
396 : /// ## Platform Behavior
397 : ///
398 : /// **Android:**
399 : /// - Uses WorkManager's OneTimeWorkRequest with flex time
400 : /// - OS chooses optimal execution time within window
401 : /// - Batches with other work for battery efficiency
402 : ///
403 : /// **iOS:**
404 : /// - Uses BGProcessingTask
405 : /// - Window is advisory (OS may defer further)
406 : /// - Best effort scheduling
407 : ///
408 : /// ## When to Use
409 : ///
410 : /// ✅ **Use windowed trigger for:**
411 : /// - Flexible data synchronization
412 : /// - Background processing that isn't time-critical
413 : /// - Heavy tasks that should run during low-usage periods
414 : /// - Operations that can benefit from being batched with other work
415 : ///
416 : /// ❌ **Don't use windowed for:**
417 : /// - Time-critical operations
418 : /// - User-initiated actions (use `oneTime` instead)
419 : ///
420 : /// ## See Also
421 : ///
422 : /// - [oneTime] - For simple delayed execution
423 : /// - [exact] - For alarm-style exact timing
424 : const factory TaskTrigger.windowed({
425 : required Duration earliest,
426 : required Duration latest,
427 : }) = WindowedTrigger;
428 :
429 : /// Execute when a content URI changes (Android only).
430 : ///
431 : /// **Android Only:** Monitors Android ContentProvider for changes and triggers
432 : /// task execution when detected. Perfect for reacting to media changes, contact
433 : /// updates, or file system modifications.
434 : ///
435 : /// **iOS:** Not supported - will return error on enqueue.
436 : ///
437 : /// ## Monitor Media Store for New Photos
438 : ///
439 : /// ```dart
440 : /// await NativeWorkManager.enqueue(
441 : /// taskId: 'photo-backup',
442 : /// trigger: TaskTrigger.contentUri(
443 : /// uri: Uri.parse('content://media/external/images/media'),
444 : /// triggerForDescendants: true,
445 : /// ),
446 : /// worker: DartWorker(callbackId: 'backupNewPhotos'),
447 : /// constraints: Constraints(requiresWifi: true),
448 : /// );
449 : /// ```
450 : ///
451 : /// ## Monitor Contact Changes
452 : ///
453 : /// ```dart
454 : /// await NativeWorkManager.enqueue(
455 : /// taskId: 'contact-sync',
456 : /// trigger: TaskTrigger.contentUri(
457 : /// uri: Uri.parse('content://com.android.contacts/contacts'),
458 : /// triggerForDescendants: false,
459 : /// ),
460 : /// worker: NativeWorker.httpSync(url: 'https://api.example.com/contacts/sync'),
461 : /// );
462 : /// ```
463 : ///
464 : /// ## Common Content URIs
465 : ///
466 : /// - **Images:** `content://media/external/images/media`
467 : /// - **Videos:** `content://media/external/video/media`
468 : /// - **Audio:** `content://media/external/audio/media`
469 : /// - **Downloads:** `content://downloads/public_downloads`
470 : /// - **Contacts:** `content://com.android.contacts/contacts`
471 : ///
472 : /// ## Parameters
473 : ///
474 : /// **[triggerForDescendants]** - Monitor child URIs too.
475 : /// - `true`: Triggers for changes in descendant URIs
476 : /// - `false`: Only triggers for exact URI match
477 : /// - Example: With `content://media/external` and `true`, changes to
478 : /// `content://media/external/images/media/123` will also trigger
479 : ///
480 : /// ## Platform Notes
481 : ///
482 : /// **Android:** Uses WorkManager ContentUriTriggers
483 : /// **iOS:** ❌ Not supported - task will be rejected
484 : ///
485 : /// ## When to Use
486 : ///
487 : /// ✅ **Use contentUri for (Android only):**
488 : /// - Auto-backup new photos/videos
489 : /// - React to contact changes
490 : /// - Monitor downloads folder
491 : /// - Sync when media is added
492 : ///
493 : /// ## Common Pitfalls
494 : ///
495 : /// ❌ **Don't** use on iOS (will fail)
496 : /// ❌ **Don't** forget `triggerForDescendants` for broad monitoring
497 : /// ✅ **Do** add platform check before using
498 : /// ✅ **Do** combine with appropriate constraints
499 : ///
500 : /// ## See Also
501 : ///
502 : /// - [oneTime] - For one-time execution
503 : /// - [periodic] - For time-based recurring tasks
504 : const factory TaskTrigger.contentUri({
505 : required Uri uri,
506 : bool triggerForDescendants,
507 : }) = ContentUriTrigger;
508 :
509 : /// Execute when battery is NOT low (Android only).
510 : ///
511 : /// **Android Only:** Triggers when battery level is above the "low" threshold
512 : /// (typically above 15%). Useful for battery-friendly operations.
513 : ///
514 : /// **iOS:** Not supported - returns REJECTED_OS_POLICY.
515 : ///
516 : /// ```dart
517 : /// // Schedule backup that only runs when battery is okay
518 : /// await NativeWorkManager.enqueue(
519 : /// taskId: 'safe-backup',
520 : /// trigger: TaskTrigger.batteryOkay(),
521 : /// worker: NativeWorker.httpUpload(
522 : /// url: 'https://api.example.com/backup',
523 : /// filePath: '/data/backup.zip',
524 : /// ),
525 : /// );
526 : /// ```
527 : ///
528 : /// **Note:** Consider using `Constraints(requiresBatteryNotLow: true)` with
529 : /// `oneTime` trigger instead for more control.
530 : const factory TaskTrigger.batteryOkay() = BatteryOkayTrigger;
531 :
532 : /// Execute when battery IS low (Android only).
533 : ///
534 : /// **Android Only:** Triggers when battery drops below 15%. Useful for warning
535 : /// users or reducing background activity.
536 : ///
537 : /// **iOS:** Not supported - returns REJECTED_OS_POLICY.
538 : ///
539 : /// ```dart
540 : /// // Notify user to charge device
541 : /// await NativeWorkManager.enqueue(
542 : /// taskId: 'low-battery-warning',
543 : /// trigger: TaskTrigger.batteryLow(),
544 : /// worker: DartWorker(callbackId: 'showLowBatteryNotification'),
545 : /// );
546 : /// ```
547 : const factory TaskTrigger.batteryLow() = BatteryLowTrigger;
548 :
549 : /// Execute when device is idle (Android only).
550 : ///
551 : /// **Android Only:** Triggers when device enters idle/Doze mode (screen off,
552 : /// stationary, not charging). Perfect for maintenance tasks.
553 : ///
554 : /// **iOS:** Not supported - returns REJECTED_OS_POLICY.
555 : ///
556 : /// ```dart
557 : /// // Database maintenance during idle time
558 : /// await NativeWorkManager.enqueue(
559 : /// taskId: 'db-maintenance',
560 : /// trigger: TaskTrigger.deviceIdle(),
561 : /// worker: DartWorker(callbackId: 'optimizeDatabase'),
562 : /// );
563 : /// ```
564 : ///
565 : /// **Use case:** Database vacuuming, cache cleanup, index optimization.
566 : const factory TaskTrigger.deviceIdle() = DeviceIdleTrigger;
567 :
568 : /// Execute when storage is low (Android only).
569 : ///
570 : /// **Android Only:** Triggers when device storage drops below threshold.
571 : /// Useful for cleanup operations.
572 : ///
573 : /// **iOS:** Not supported - returns REJECTED_OS_POLICY.
574 : ///
575 : /// ```dart
576 : /// // Auto-cleanup when storage is low
577 : /// await NativeWorkManager.enqueue(
578 : /// taskId: 'emergency-cleanup',
579 : /// trigger: TaskTrigger.storageLow(),
580 : /// worker: DartWorker(callbackId: 'deleteOldCache'),
581 : /// );
582 : /// ```
583 : ///
584 : /// **Use case:** Delete old files, clear caches, compress data.
585 : const factory TaskTrigger.storageLow() = StorageLowTrigger;
586 : }
587 :
588 : /// Execute once after an optional delay.
589 : class OneTimeTrigger extends TaskTrigger {
590 9 : const OneTimeTrigger([this.initialDelay = Duration.zero]);
591 :
592 : /// Delay before execution.
593 : final Duration initialDelay;
594 :
595 2 : @override
596 2 : Map<String, dynamic> toMap() => {
597 : 'type': 'oneTime',
598 4 : 'initialDelayMs': initialDelay.inMilliseconds,
599 : };
600 :
601 1 : @override
602 : bool operator ==(Object other) =>
603 : identical(this, other) ||
604 4 : other is OneTimeTrigger && initialDelay == other.initialDelay;
605 :
606 1 : @override
607 2 : int get hashCode => initialDelay.hashCode;
608 :
609 1 : @override
610 2 : String toString() => 'TaskTrigger.oneTime($initialDelay)';
611 : }
612 :
613 : /// Execute periodically at a fixed interval.
614 : class PeriodicTrigger extends TaskTrigger {
615 4 : const PeriodicTrigger(
616 : this.interval, {
617 : this.flexInterval,
618 : this.initialDelay,
619 : this.runImmediately = true,
620 : });
621 :
622 : /// Interval between executions.
623 : final Duration interval;
624 :
625 : /// Flex time window for execution.
626 : final Duration? flexInterval;
627 :
628 : /// Initial delay before first execution.
629 : final Duration? initialDelay;
630 :
631 : /// Whether to run the task immediately.
632 : final bool runImmediately;
633 :
634 : static const Duration _androidMinInterval = Duration(minutes: 15);
635 : static const Duration _androidMinFlex = Duration(minutes: 5);
636 :
637 3 : @override
638 : Map<String, dynamic> toMap() {
639 6 : if (interval < _androidMinInterval) {
640 1 : throw ArgumentError.value(
641 1 : interval,
642 1 : 'interval',
643 : 'Periodic interval must be at least 15 minutes on Android. '
644 2 : 'Received ${interval.inMinutes} min. '
645 : 'Android WorkManager silently clamps values below 15 min, masking bugs. '
646 : 'Use Duration(minutes: 15) or longer.',
647 : );
648 : }
649 3 : final flex = flexInterval;
650 1 : if (flex != null && flex < _androidMinFlex) {
651 0 : throw ArgumentError.value(
652 : flex,
653 0 : 'flexInterval',
654 : 'flexInterval must be at least 5 minutes on Android. '
655 0 : 'Received ${flex.inMinutes} min. '
656 : 'WorkManager rejects values below 5 min with IllegalArgumentException at runtime.',
657 : );
658 : }
659 : assert(
660 3 : runImmediately || (initialDelay?.inMilliseconds ?? 0) == 0,
661 : 'runImmediately: false and initialDelay cannot both be set — behaviour is undefined. '
662 : 'Use one or the other: initialDelay to specify an exact first-run offset, '
663 : 'or runImmediately: false to defer by one full interval.',
664 : );
665 3 : return {
666 : 'type': 'periodic',
667 6 : 'intervalMs': interval.inMilliseconds,
668 4 : 'flexMs': flexInterval?.inMilliseconds,
669 5 : 'initialDelayMs': initialDelay?.inMilliseconds,
670 3 : 'runImmediately': runImmediately,
671 : };
672 : }
673 :
674 1 : @override
675 : bool operator ==(Object other) =>
676 : identical(this, other) ||
677 1 : other is PeriodicTrigger &&
678 3 : interval == other.interval &&
679 3 : flexInterval == other.flexInterval &&
680 3 : initialDelay == other.initialDelay &&
681 3 : runImmediately == other.runImmediately;
682 :
683 1 : @override
684 : int get hashCode =>
685 5 : Object.hash(interval, flexInterval, initialDelay, runImmediately);
686 :
687 1 : @override
688 : String toString() =>
689 5 : 'TaskTrigger.periodic($interval, flex: $flexInterval, initialDelay: $initialDelay, runImmediately: $runImmediately)';
690 : }
691 :
692 : /// Execute at an exact time.
693 : class ExactTrigger extends TaskTrigger {
694 3 : const ExactTrigger(this.scheduledTime);
695 :
696 : /// The exact time to execute.
697 : final DateTime scheduledTime;
698 :
699 2 : @override
700 2 : Map<String, dynamic> toMap() => {
701 : 'type': 'exact',
702 4 : 'scheduledTimeMs': scheduledTime.millisecondsSinceEpoch,
703 : };
704 :
705 1 : @override
706 : bool operator ==(Object other) =>
707 : identical(this, other) ||
708 4 : other is ExactTrigger && scheduledTime == other.scheduledTime;
709 :
710 1 : @override
711 2 : int get hashCode => scheduledTime.hashCode;
712 :
713 1 : @override
714 2 : String toString() => 'TaskTrigger.exact($scheduledTime)';
715 : }
716 :
717 : /// Execute within a time window.
718 : class WindowedTrigger extends TaskTrigger {
719 2 : const WindowedTrigger({
720 : required this.earliest,
721 : required this.latest,
722 : });
723 :
724 : /// Earliest time to start.
725 : final Duration earliest;
726 :
727 : /// Latest time to complete.
728 : final Duration latest;
729 :
730 2 : @override
731 2 : Map<String, dynamic> toMap() => {
732 : 'type': 'windowed',
733 4 : 'earliestMs': earliest.inMilliseconds,
734 4 : 'latestMs': latest.inMilliseconds,
735 : };
736 :
737 1 : @override
738 : bool operator ==(Object other) =>
739 : identical(this, other) ||
740 1 : other is WindowedTrigger &&
741 3 : earliest == other.earliest &&
742 3 : latest == other.latest;
743 :
744 1 : @override
745 3 : int get hashCode => Object.hash(earliest, latest);
746 :
747 1 : @override
748 3 : String toString() => 'TaskTrigger.windowed($earliest - $latest)';
749 : }
750 :
751 : /// Execute when a content URI changes (Android only).
752 : class ContentUriTrigger extends TaskTrigger {
753 3 : const ContentUriTrigger({
754 : required this.uri,
755 : this.triggerForDescendants = false,
756 : });
757 :
758 : /// Content URI to observe.
759 : ///
760 : /// Common examples:
761 : /// - `Uri.parse('content://media/external/images/media')` - MediaStore images
762 : /// - `Uri.parse('content://media/external/video/media')` - MediaStore videos
763 : /// - `Uri.parse('content://com.android.contacts/contacts')` - Contacts
764 : final Uri uri;
765 :
766 : /// If true, triggers for changes in descendant URIs as well.
767 : ///
768 : /// Example: If uri is `content://media/external` and this is true,
769 : /// changes to `content://media/external/images/media/123` will also trigger.
770 : final bool triggerForDescendants;
771 :
772 3 : @override
773 3 : Map<String, dynamic> toMap() => {
774 : 'type': 'contentUri',
775 6 : 'uriString': uri.toString(),
776 3 : 'triggerForDescendants': triggerForDescendants,
777 : };
778 :
779 1 : @override
780 : bool operator ==(Object other) =>
781 : identical(this, other) ||
782 1 : other is ContentUriTrigger &&
783 3 : uri == other.uri &&
784 3 : triggerForDescendants == other.triggerForDescendants;
785 :
786 1 : @override
787 3 : int get hashCode => Object.hash(uri, triggerForDescendants);
788 :
789 1 : @override
790 : String toString() =>
791 3 : 'TaskTrigger.contentUri($uri, descendants: $triggerForDescendants)';
792 : }
793 :
794 : /// Execute when battery is NOT low (Android only).
795 : class BatteryOkayTrigger extends TaskTrigger {
796 1 : const BatteryOkayTrigger();
797 :
798 1 : @override
799 1 : Map<String, dynamic> toMap() => {
800 : 'type': 'batteryOkay',
801 : };
802 :
803 1 : @override
804 : bool operator ==(Object other) =>
805 1 : identical(this, other) || other is BatteryOkayTrigger;
806 :
807 1 : @override
808 1 : int get hashCode => 'batteryOkay'.hashCode;
809 :
810 1 : @override
811 : String toString() => 'TaskTrigger.batteryOkay()';
812 : }
813 :
814 : /// Execute when battery IS low (Android only).
815 : class BatteryLowTrigger extends TaskTrigger {
816 1 : const BatteryLowTrigger();
817 :
818 1 : @override
819 1 : Map<String, dynamic> toMap() => {
820 : 'type': 'batteryLow',
821 : };
822 :
823 1 : @override
824 : bool operator ==(Object other) =>
825 1 : identical(this, other) || other is BatteryLowTrigger;
826 :
827 1 : @override
828 1 : int get hashCode => 'batteryLow'.hashCode;
829 :
830 1 : @override
831 : String toString() => 'TaskTrigger.batteryLow()';
832 : }
833 :
834 : /// Execute when device is idle (Android only).
835 : class DeviceIdleTrigger extends TaskTrigger {
836 1 : const DeviceIdleTrigger();
837 :
838 1 : @override
839 1 : Map<String, dynamic> toMap() => {
840 : 'type': 'deviceIdle',
841 : };
842 :
843 1 : @override
844 : bool operator ==(Object other) =>
845 1 : identical(this, other) || other is DeviceIdleTrigger;
846 :
847 1 : @override
848 1 : int get hashCode => 'deviceIdle'.hashCode;
849 :
850 1 : @override
851 : String toString() => 'TaskTrigger.deviceIdle()';
852 : }
853 :
854 : /// Execute when storage is low (Android only).
855 : class StorageLowTrigger extends TaskTrigger {
856 1 : const StorageLowTrigger();
857 :
858 1 : @override
859 1 : Map<String, dynamic> toMap() => {
860 : 'type': 'storageLow',
861 : };
862 :
863 1 : @override
864 : bool operator ==(Object other) =>
865 1 : identical(this, other) || other is StorageLowTrigger;
866 :
867 1 : @override
868 1 : int get hashCode => 'storageLow'.hashCode;
869 :
870 1 : @override
871 : String toString() => 'TaskTrigger.storageLow()';
872 : }
|