LCOV - code coverage report
Current view: top level - src - events.dart Coverage Total Hit
Test: lcov.info Lines: 53.9 % 178 96
Test Date: 2026-04-30 18:23:23 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:convert';
       2              : import 'package:flutter/foundation.dart';
       3              : 
       4              : /// Record from the persistent task store.
       5              : @immutable
       6              : class TaskRecord {
       7            0 :   const TaskRecord({
       8              :     required this.taskId,
       9              :     this.tag,
      10              :     required this.status,
      11              :     required this.workerClassName,
      12              :     this.workerConfig,
      13              :     this.resultData,
      14              :     required this.createdAt,
      15              :     required this.updatedAt,
      16              :   });
      17              : 
      18              :   final String taskId;
      19              :   final String? tag;
      20              : 
      21              :   /// Status string: pending / running / completed / failed / cancelled / paused
      22              :   final String status;
      23              : 
      24              :   final String workerClassName;
      25              : 
      26              :   /// Raw worker configuration (often sanitized/redacted by native side).
      27              :   final String? workerConfig;
      28              : 
      29              :   /// Optional result data (null or decoded from JSON stored by the native side).
      30              :   final Map<String, dynamic>? resultData;
      31              : 
      32              :   final DateTime createdAt;
      33              :   final DateTime updatedAt;
      34              : 
      35            0 :   factory TaskRecord.fromMap(Map<String, dynamic> m) => TaskRecord(
      36            0 :         taskId: m['taskId'] as String,
      37            0 :         tag: m['tag'] as String?,
      38            0 :         status: m['status'] as String? ?? 'unknown',
      39            0 :         workerClassName: m['workerClassName'] as String? ?? '',
      40            0 :         workerConfig: m['workerConfig'] as String?,
      41            0 :         resultData: m['resultData'] == null
      42              :             ? null
      43            0 :             : (m['resultData'] is Map
      44            0 :                 ? Map<String, dynamic>.from(m['resultData'] as Map)
      45            0 :                 : (m['resultData'] is String
      46            0 :                     ? (() {
      47            0 :                         final decoded = jsonDecode(m['resultData'] as String);
      48            0 :                         if (decoded is Map) {
      49            0 :                           return Map<String, dynamic>.from(decoded);
      50            0 :                         } else if (decoded is List) {
      51              :                           // Wrap list in a map for TaskEvent compatibility
      52            0 :                           return {'items': decoded};
      53              :                         }
      54            0 :                         return <String, dynamic>{};
      55            0 :                       })()
      56              :                     : null)),
      57            0 :         createdAt: DateTime.fromMillisecondsSinceEpoch(
      58            0 :             (m['createdAt'] as num).toInt()),
      59            0 :         updatedAt: DateTime.fromMillisecondsSinceEpoch(
      60            0 :             (m['updatedAt'] as num).toInt()),
      61              :       );
      62              : 
      63            0 :   Map<String, dynamic> toMap() => {
      64            0 :         'taskId': taskId,
      65            0 :         'tag': tag,
      66            0 :         'status': status,
      67            0 :         'workerClassName': workerClassName,
      68            0 :         'workerConfig': workerConfig,
      69            0 :         'resultData': resultData,
      70            0 :         'createdAt': createdAt.millisecondsSinceEpoch,
      71            0 :         'updatedAt': updatedAt.millisecondsSinceEpoch,
      72              :       };
      73              : 
      74            0 :   @override
      75            0 :   String toString() => 'TaskRecord(taskId: $taskId, status: $status, '
      76            0 :       'workerClassName: $workerClassName, tag: $tag)';
      77              : }
      78              : 
      79              : /// Result of scheduling a task.
      80              : ///
      81              : /// Returned by [NativeWorkManager.enqueue] to indicate whether the OS
      82              : /// accepted the task for scheduling.
      83              : ///
      84              : /// ## Success Case
      85              : ///
      86              : /// ```dart
      87              : /// final result = await NativeWorkManager.enqueue(
      88              : ///   taskId: 'sync-data',
      89              : ///   trigger: TaskTrigger.oneTime(),
      90              : ///   worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
      91              : /// );
      92              : ///
      93              : /// if (result == ScheduleResult.accepted) {
      94              : ///   print('Task scheduled successfully');
      95              : /// }
      96              : /// ```
      97              : ///
      98              : /// ## Handling Rejection
      99              : ///
     100              : /// ```dart
     101              : /// final result = await NativeWorkManager.enqueue(
     102              : ///   taskId: 'upload-large-file',
     103              : ///   trigger: TaskTrigger.oneTime(),
     104              : ///   worker: NativeWorker.httpUpload(
     105              : ///     url: 'https://api.example.com/upload',
     106              : ///     filePath: '/data/large-file.zip',
     107              : ///   ),
     108              : /// );
     109              : ///
     110              : /// switch (result) {
     111              : ///   case ScheduleResult.accepted:
     112              : ///     showNotification('Upload scheduled');
     113              : ///     break;
     114              : ///   case ScheduleResult.rejectedOsPolicy:
     115              : ///     showError('Device cannot schedule tasks (low battery?)');
     116              : ///     break;
     117              : ///   case ScheduleResult.throttled:
     118              : ///     showWarning('Too many tasks - try again later');
     119              : ///     break;
     120              : /// }
     121              : /// ```
     122              : ///
     123              : /// ## Why Tasks Get Rejected
     124              : ///
     125              : /// **rejectedOsPolicy:**
     126              : /// - Device in power save mode
     127              : /// - Too many tasks already scheduled
     128              : /// - App in background restrictions (Android)
     129              : /// - Constraints too restrictive
     130              : ///
     131              : /// **throttled:**
     132              : /// - Too many enqueue() calls in short period
     133              : /// - OS rate limiting to prevent abuse
     134              : /// - Typical limit: ~500 tasks per hour
     135              : ///
     136              : /// ## Best Practices
     137              : ///
     138              : /// ✅ **Do** check the result and handle rejections gracefully
     139              : /// ✅ **Do** implement retry logic with exponential backoff
     140              : /// ✅ **Do** inform users if critical tasks can't be scheduled
     141              : ///
     142              : /// ❌ **Don't** assume tasks are always accepted
     143              : /// ❌ **Don't** schedule hundreds of tasks rapidly
     144              : /// ❌ **Don't** ignore throttling errors
     145              : ///
     146              : /// See also: [NativeWorkManager.enqueue]
     147              : enum ScheduleResult {
     148              :   /// Task was successfully scheduled.
     149              :   ///
     150              :   /// The OS accepted the task and will execute it according to the trigger
     151              :   /// and constraints. This is the normal success case.
     152              :   accepted,
     153              : 
     154              :   /// Task was rejected due to OS policy.
     155              :   ///
     156              :   /// Common causes:
     157              :   /// - Device in power save mode
     158              :   /// - Too many tasks already scheduled
     159              :   /// - Background execution restrictions
     160              :   /// - Constraints cannot be satisfied
     161              :   rejectedOsPolicy,
     162              : 
     163              :   /// Task was throttled (too many requests).
     164              :   ///
     165              :   /// The app is scheduling tasks too rapidly. The OS rejected this task
     166              :   /// to prevent resource abuse. Wait and retry with exponential backoff.
     167              :   throttled,
     168              : }
     169              : 
     170              : /// Policy for handling existing tasks with the same ID.
     171              : ///
     172              : /// When scheduling a task with an ID that already exists, this policy
     173              : /// determines whether to keep the existing task or replace it with the new one.
     174              : ///
     175              : /// ## Keep Existing Task
     176              : ///
     177              : /// ```dart
     178              : /// // Schedule initial sync
     179              : /// await NativeWorkManager.enqueue(
     180              : ///   taskId: 'daily-sync',
     181              : ///   trigger: TaskTrigger.periodic(Duration(hours: 24)),
     182              : ///   worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
     183              : ///   existingPolicy: ExistingTaskPolicy.keep,  // Prevent duplicate
     184              : /// );
     185              : ///
     186              : /// // Later, user changes settings - but keep the original task running
     187              : /// await NativeWorkManager.enqueue(
     188              : ///   taskId: 'daily-sync',  // Same ID
     189              : ///   trigger: TaskTrigger.periodic(Duration(hours: 12)),
     190              : ///   worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
     191              : ///   existingPolicy: ExistingTaskPolicy.keep,  // Original 24h task continues
     192              : /// );
     193              : /// ```
     194              : ///
     195              : /// ## Replace Existing Task
     196              : ///
     197              : /// ```dart
     198              : /// // Schedule initial sync
     199              : /// await NativeWorkManager.enqueue(
     200              : ///   taskId: 'daily-sync',
     201              : ///   trigger: TaskTrigger.periodic(Duration(hours: 24)),
     202              : ///   worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
     203              : /// );
     204              : ///
     205              : /// // User changes settings - update the task immediately
     206              : /// await NativeWorkManager.enqueue(
     207              : ///   taskId: 'daily-sync',  // Same ID
     208              : ///   trigger: TaskTrigger.periodic(Duration(hours: 12)),
     209              : ///   worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
     210              : ///   existingPolicy: ExistingTaskPolicy.replace,  // Cancels 24h, starts 12h
     211              : /// );
     212              : /// ```
     213              : ///
     214              : /// ## When to Use Keep
     215              : ///
     216              : /// Use `ExistingTaskPolicy.keep` when:
     217              : /// - Task is idempotent (safe to run multiple times)
     218              : /// - You want to ensure at least one execution happens
     219              : /// - Avoiding duplicate work is critical (e.g., expensive API calls)
     220              : /// - Initial scheduling during app install
     221              : ///
     222              : /// **Example:** One-time data migration
     223              : /// ```dart
     224              : /// await NativeWorkManager.enqueue(
     225              : ///   taskId: 'v2-migration',
     226              : ///   trigger: TaskTrigger.oneTime(),
     227              : ///   worker: DartWorker(callbackId: 'migrateData'),
     228              : ///   existingPolicy: ExistingTaskPolicy.keep,  // Don't duplicate if already scheduled
     229              : /// );
     230              : /// ```
     231              : ///
     232              : /// ## When to Use Replace
     233              : ///
     234              : /// Use `ExistingTaskPolicy.replace` when:
     235              : /// - User changed settings/preferences
     236              : /// - Task configuration needs updating
     237              : /// - Old parameters are no longer valid
     238              : /// - Cancelling and rescheduling is intentional
     239              : ///
     240              : /// **Example:** User changes sync frequency
     241              : /// ```dart
     242              : /// // User updates setting: hourly → every 6 hours
     243              : /// await NativeWorkManager.enqueue(
     244              : ///   taskId: 'background-sync',
     245              : ///   trigger: TaskTrigger.periodic(newInterval),
     246              : ///   worker: NativeWorker.httpSync(url: syncUrl),
     247              : ///   existingPolicy: ExistingTaskPolicy.replace,  // Apply new frequency immediately
     248              : /// );
     249              : /// ```
     250              : ///
     251              : /// ## Comparison
     252              : ///
     253              : /// | Scenario | Keep | Replace |
     254              : /// |----------|------|---------|
     255              : /// | Task already exists | New request ignored | Old task cancelled, new scheduled |
     256              : /// | Task not found | New task scheduled | New task scheduled |
     257              : /// | Typical use case | Prevent duplicates | Update configuration |
     258              : ///
     259              : /// ## Default Behavior
     260              : ///
     261              : /// If policy is not specified, `ExistingTaskPolicy.replace` is used by default.
     262              : /// This ensures the latest configuration is always applied.
     263              : ///
     264              : /// See also: [NativeWorkManager.enqueue]
     265              : enum ExistingTaskPolicy {
     266              :   /// Keep the existing task, ignore the new one.
     267              :   ///
     268              :   /// If a task with the same ID already exists, the new enqueue request
     269              :   /// is silently ignored. The existing task continues unchanged.
     270              :   ///
     271              :   /// Useful when you want to prevent duplicate tasks from being scheduled.
     272              :   keep,
     273              : 
     274              :   /// Replace the existing task with the new one.
     275              :   ///
     276              :   /// If a task with the same ID already exists, it is cancelled and replaced
     277              :   /// with the new task. Use this when updating task configuration.
     278              :   replace,
     279              : }
     280              : 
     281              : /// Current status of a task.
     282              : ///
     283              : /// Represents the lifecycle state of a scheduled task. Query task status
     284              : /// using [NativeWorkManager.getTaskStatus].
     285              : ///
     286              : /// ## Task Lifecycle
     287              : ///
     288              : /// ```
     289              : /// PENDING → RUNNING → COMPLETED
     290              : ///                   → FAILED
     291              : ///         → CANCELLED
     292              : /// ```
     293              : ///
     294              : /// ## Checking Task Status
     295              : ///
     296              : /// ```dart
     297              : /// final status = await NativeWorkManager.getTaskStatus(taskId: 'upload-photos');
     298              : ///
     299              : /// switch (status) {
     300              : ///   case TaskStatus.pending:
     301              : ///     print('Waiting for WiFi...');
     302              : ///     break;
     303              : ///   case TaskStatus.running:
     304              : ///     print('Upload in progress...');
     305              : ///     break;
     306              : ///   case TaskStatus.completed:
     307              : ///     print('Upload finished!');
     308              : ///     break;
     309              : ///   case TaskStatus.failed:
     310              : ///     print('Upload failed - will retry');
     311              : ///     break;
     312              : ///   case TaskStatus.cancelled:
     313              : ///     print('Upload cancelled by user');
     314              : ///     break;
     315              : ///   case null:
     316              : ///     print('Task not found');
     317              : ///     break;
     318              : /// }
     319              : /// ```
     320              : ///
     321              : /// ## Monitoring Multiple Tasks
     322              : ///
     323              : /// ```dart
     324              : /// Future<void> checkUploads() async {
     325              : ///   final tasks = await NativeWorkManager.getTasksByTag(tag: 'upload');
     326              : ///
     327              : ///   final pending = tasks.where((t) => t.status == TaskStatus.pending).length;
     328              : ///   final running = tasks.where((t) => t.status == TaskStatus.running).length;
     329              : ///   final completed = tasks.where((t) => t.status == TaskStatus.completed).length;
     330              : ///
     331              : ///   print('Uploads: $pending pending, $running active, $completed done');
     332              : /// }
     333              : /// ```
     334              : ///
     335              : /// ## Status Transitions
     336              : ///
     337              : /// **PENDING → RUNNING:**
     338              : /// - Constraints are met (network, battery, etc.)
     339              : /// - OS scheduler starts execution
     340              : /// - Task begins doing work
     341              : ///
     342              : /// **RUNNING → COMPLETED:**
     343              : /// - Worker returns success result
     344              : /// - All work finished successfully
     345              : /// - Task removed from queue
     346              : ///
     347              : /// **RUNNING → FAILED:**
     348              : /// - Worker throws exception
     349              : /// - Network error, timeout, etc.
     350              : /// - OS may retry automatically (periodic tasks)
     351              : ///
     352              : /// **ANY → CANCELLED:**
     353              : /// - [NativeWorkManager.cancel] called
     354              : /// - [NativeWorkManager.cancelByTag] called
     355              : /// - [NativeWorkManager.cancelAll] called
     356              : /// - Task removed from queue immediately
     357              : ///
     358              : /// ## Important Notes
     359              : ///
     360              : /// - **Completed tasks** are automatically removed after a short period (OS-dependent)
     361              : /// - **Failed periodic tasks** may be retried automatically by the OS
     362              : /// - **Cancelled tasks** cannot be resumed - must enqueue again
     363              : /// - **Running tasks** may take time to fully stop when cancelled
     364              : ///
     365              : /// See also:
     366              : /// - [NativeWorkManager.getTaskStatus] - Query status
     367              : /// - [NativeWorkManager.events] - Listen for completion events
     368              : /// - [TaskEvent] - Task completion notification
     369              : enum TaskStatus {
     370              :   /// Task is waiting to be executed.
     371              :   ///
     372              :   /// The task is scheduled but constraints are not yet met
     373              :   /// (e.g., waiting for WiFi, charging, etc.).
     374              :   pending,
     375              : 
     376              :   /// Task is currently running.
     377              :   ///
     378              :   /// The worker is actively executing. Listen to [NativeWorkManager.progress]
     379              :   /// for real-time progress updates.
     380              :   running,
     381              : 
     382              :   /// Task completed successfully.
     383              :   ///
     384              :   /// The worker finished and returned success. Completed tasks are
     385              :   /// automatically removed from the queue after a short period.
     386              :   completed,
     387              : 
     388              :   /// Task failed.
     389              :   ///
     390              :   /// The worker threw an exception or returned failure. For periodic tasks,
     391              :   /// the OS may automatically retry. For one-time tasks, the task is marked
     392              :   /// as failed and removed from the queue.
     393              :   failed,
     394              : 
     395              :   /// Task was cancelled.
     396              :   ///
     397              :   /// The task was explicitly cancelled via [NativeWorkManager.cancel],
     398              :   /// [NativeWorkManager.cancelByTag], or [NativeWorkManager.cancelAll].
     399              :   /// Cancelled tasks are removed from the queue and cannot be resumed.
     400              :   cancelled,
     401              : 
     402              :   /// Task is paused.
     403              :   ///
     404              :   /// The task was paused via [NativeWorkManager.pause] or
     405              :   /// [NativeWorkManager.pauseByTag]. Resume with [NativeWorkManager.resume].
     406              :   paused,
     407              : }
     408              : 
     409              : /// Typed error codes for failed [TaskEvent]s.
     410              : ///
     411              : /// Maps the raw error-code string from the native side into a Dart enum so
     412              : /// callers can switch on structured values instead of comparing arbitrary
     413              : /// strings from [TaskEvent.message].
     414              : ///
     415              : /// ## Usage
     416              : ///
     417              : /// ```dart
     418              : /// NativeWorkManager.events.listen((event) {
     419              : ///   if (!event.success) {
     420              : ///     switch (event.errorCode) {
     421              : ///       case NativeWorkManagerError.networkError:
     422              : ///         retryLater(event.taskId);
     423              : ///       case NativeWorkManagerError.timeout:
     424              : ///         showTimeoutDialog();
     425              : ///       case NativeWorkManagerError.securityViolation:
     426              : ///         log('Blocked by security policy: ${event.message}');
     427              : ///       case NativeWorkManagerError.unknown:
     428              : ///       case null:
     429              : ///         log('Unclassified failure: ${event.message}');
     430              : ///     }
     431              : ///   }
     432              : /// });
     433              : /// ```
     434              : enum NativeWorkManagerError {
     435              :   /// A network-layer failure (DNS, TCP, SSL, etc.).
     436              :   networkError,
     437              : 
     438              :   /// The worker exceeded its allowed execution time.
     439              :   timeout,
     440              : 
     441              :   /// The server returned a 4xx client-error response.
     442              :   httpClientError,
     443              : 
     444              :   /// The server returned a 5xx server-error response.
     445              :   httpServerError,
     446              : 
     447              :   /// The file was not found or could not be read/written.
     448              :   fileNotFound,
     449              : 
     450              :   /// Insufficient storage space to complete the operation.
     451              :   insufficientStorage,
     452              : 
     453              :   /// The request was blocked by the security validator
     454              :   /// (e.g. SSRF attempt, invalid URL scheme).
     455              :   securityViolation,
     456              : 
     457              :   /// The task was cancelled before it could complete.
     458              :   cancelled,
     459              : 
     460              :   /// The native worker threw an unhandled exception.
     461              :   workerException,
     462              : 
     463              :   /// Error string received from native is not recognised by this version of
     464              :   /// the Dart library.  Check [TaskEvent.message] for the raw value.
     465              :   unknown;
     466              : 
     467              :   /// Parse the raw error-code string sent by the native side.
     468            0 :   static NativeWorkManagerError fromString(String? raw) => switch (raw) {
     469            0 :         'NETWORK_ERROR' => networkError,
     470            0 :         'TIMEOUT' => timeout,
     471            0 :         'HTTP_CLIENT_ERROR' => httpClientError,
     472            0 :         'HTTP_SERVER_ERROR' => httpServerError,
     473            0 :         'FILE_NOT_FOUND' => fileNotFound,
     474            0 :         'INSUFFICIENT_STORAGE' => insufficientStorage,
     475            0 :         'SECURITY_VIOLATION' => securityViolation,
     476            0 :         'CANCELLED' => cancelled,
     477            0 :         'WORKER_EXCEPTION' => workerException,
     478              :         _ => unknown,
     479              :       };
     480              : 
     481              :   /// The canonical string exchanged over the platform channel.
     482            0 :   String get rawValue => switch (this) {
     483            0 :         networkError => 'NETWORK_ERROR',
     484            0 :         timeout => 'TIMEOUT',
     485            0 :         httpClientError => 'HTTP_CLIENT_ERROR',
     486            0 :         httpServerError => 'HTTP_SERVER_ERROR',
     487            0 :         fileNotFound => 'FILE_NOT_FOUND',
     488            0 :         insufficientStorage => 'INSUFFICIENT_STORAGE',
     489            0 :         securityViolation => 'SECURITY_VIOLATION',
     490            0 :         cancelled => 'CANCELLED',
     491            0 :         workerException => 'WORKER_EXCEPTION',
     492            0 :         unknown => 'UNKNOWN',
     493              :       };
     494              : }
     495              : 
     496              : /// Event emitted for task lifecycle transitions (started, completed, failed).
     497              : ///
     498              : /// Listen to [NativeWorkManager.events] to receive notifications when
     499              : /// background tasks start or finish executing. Useful for updating UI, logging,
     500              : /// or triggering follow-up actions.
     501              : ///
     502              : /// ## Distinguishing Event Types
     503              : ///
     504              : /// Check [isStarted] to determine whether this is a lifecycle notification or
     505              : /// a completion event:
     506              : ///
     507              : /// ```dart
     508              : /// NativeWorkManager.events.listen((event) {
     509              : ///   if (event.isStarted) {
     510              : ///     print('Task ${event.taskId} began executing');
     511              : ///     return;
     512              : ///   }
     513              : ///   if (event.success) {
     514              : ///     print('Task ${event.taskId} completed');
     515              : ///   } else {
     516              : ///     print('Task ${event.taskId} failed: ${event.message}');
     517              : ///   }
     518              : /// });
     519              : /// ```
     520              : ///
     521              : /// ## Basic Event Listening
     522              : ///
     523              : /// ```dart
     524              : /// void initState() {
     525              : ///   super.initState();
     526              : ///
     527              : ///   // Listen to all task completions
     528              : ///   NativeWorkManager.events.listen((event) {
     529              : ///     if (event.success) {
     530              : ///       print('✅ Task ${event.taskId} completed');
     531              : ///       if (event.resultData != null) {
     532              : ///         print('Result: ${event.resultData}');
     533              : ///       }
     534              : ///     } else {
     535              : ///       print('❌ Task ${event.taskId} failed: ${event.message}');
     536              : ///     }
     537              : ///   });
     538              : /// }
     539              : /// ```
     540              : ///
     541              : /// ## Filtering Specific Tasks
     542              : ///
     543              : /// ```dart
     544              : /// NativeWorkManager.events
     545              : ///     .where((event) => event.taskId.startsWith('sync-'))
     546              : ///     .listen((event) {
     547              : ///       if (event.success) {
     548              : ///         showNotification('Sync completed');
     549              : ///         refreshUI();
     550              : ///       } else {
     551              : ///         showError('Sync failed: ${event.message}');
     552              : ///       }
     553              : ///     });
     554              : /// ```
     555              : ///
     556              : /// ## Handling Different Task Types
     557              : ///
     558              : /// ```dart
     559              : /// NativeWorkManager.events.listen((event) {
     560              : ///   switch (event.taskId) {
     561              : ///     case 'download-images':
     562              : ///       if (event.success) {
     563              : ///         final count = event.resultData?['downloaded_count'];
     564              : ///         print('Downloaded $count images');
     565              : ///       }
     566              : ///       break;
     567              : ///
     568              : ///     case 'upload-logs':
     569              : ///       if (event.success) {
     570              : ///         clearLocalLogs();
     571              : ///       } else {
     572              : ///         scheduleRetry();
     573              : ///       }
     574              : ///       break;
     575              : ///
     576              : ///     case 'sync-contacts':
     577              : ///       if (event.success) {
     578              : ///         updateLastSyncTime(event.timestamp);
     579              : ///       }
     580              : ///       break;
     581              : ///   }
     582              : /// });
     583              : /// ```
     584              : ///
     585              : /// ## Extracting Result Data
     586              : ///
     587              : /// ```dart
     588              : /// // Worker returns data
     589              : /// @pragma('vm:entry-point')
     590              : /// Future<WorkerResult> processData(WorkerInput input) async {
     591              : ///   final result = await heavyComputation();
     592              : ///   return WorkerResult.success(data: {
     593              : ///     'processed_items': result.count,
     594              : ///     'total_size': result.sizeInBytes,
     595              : ///     'duration_ms': result.durationMs,
     596              : ///   });
     597              : /// }
     598              : ///
     599              : /// // Listen for results
     600              : /// NativeWorkManager.events
     601              : ///     .where((e) => e.taskId == 'process-data')
     602              : ///     .listen((event) {
     603              : ///       if (event.success && event.resultData != null) {
     604              : ///         final items = event.resultData!['processed_items'];
     605              : ///         final size = event.resultData!['total_size'];
     606              : ///         print('Processed $items items ($size bytes)');
     607              : ///       }
     608              : ///     });
     609              : /// ```
     610              : ///
     611              : /// ## Error Handling
     612              : ///
     613              : /// ```dart
     614              : /// NativeWorkManager.events.listen((event) {
     615              : ///   if (!event.success) {
     616              : ///     // Log error for analytics
     617              : ///     analytics.logError(
     618              : ///       taskId: event.taskId,
     619              : ///       error: event.message ?? 'Unknown error',
     620              : ///       timestamp: event.timestamp,
     621              : ///     );
     622              : ///
     623              : ///     // Notify user for critical tasks
     624              : ///     if (event.taskId == 'backup-critical-data') {
     625              : ///       showCriticalErrorDialog(event.message);
     626              : ///     }
     627              : ///
     628              : ///     // Implement retry logic
     629              : ///     if (shouldRetry(event.taskId)) {
     630              : ///       scheduleRetry(event.taskId, exponentialBackoff: true);
     631              : ///     }
     632              : ///   }
     633              : /// });
     634              : /// ```
     635              : ///
     636              : /// ## Event Fields
     637              : ///
     638              : /// - [taskId]: Unique identifier of the completed task
     639              : /// - [success]: `true` if task completed successfully, `false` if failed
     640              : /// - [message]: Error message if failed, or optional success message
     641              : /// - [resultData]: Custom data returned by the worker (if any)
     642              : /// - [timestamp]: When the task completed execution
     643              : ///
     644              : /// ## Platform Behavior
     645              : ///
     646              : /// **Android:**
     647              : /// - Events delivered via WorkManager's Result mechanism
     648              : /// - May be delayed if app is in background
     649              : /// - Guaranteed delivery when app comes to foreground
     650              : ///
     651              : /// **iOS:**
     652              : /// - Events delivered when app is active
     653              : /// - Background completion may not trigger immediate event
     654              : /// - Events batched if app was terminated
     655              : ///
     656              : /// ## Important Notes
     657              : ///
     658              : /// - Events are only delivered **while the app is running**
     659              : /// - If app is terminated, events are **not persisted**
     660              : /// - For critical outcomes, persist state in the **worker itself**
     661              : /// - Events are **fire-and-forget** (no replay mechanism)
     662              : /// - Use [NativeWorkManager.getTaskStatus] to check status if you miss events
     663              : ///
     664              : /// ## Best Practices
     665              : ///
     666              : /// ✅ **Do** listen to events for UI updates and logging
     667              : /// ✅ **Do** filter events by taskId or patterns for specific handling
     668              : /// ✅ **Do** persist important results in the worker, not just events
     669              : /// ✅ **Do** handle both success and failure cases
     670              : ///
     671              : /// ❌ **Don't** rely on events for critical state management
     672              : /// ❌ **Don't** assume events arrive in order (parallel tasks)
     673              : /// ❌ **Don't** expect events if app is terminated
     674              : ///
     675              : /// See also:
     676              : /// - [NativeWorkManager.events] - Stream of task events
     677              : /// - [TaskProgress] - Progress updates during execution
     678              : /// - [TaskStatus] - Current task status
     679              : @immutable
     680              : class TaskEvent {
     681            8 :   const TaskEvent({
     682              :     required this.taskId,
     683              :     required this.success,
     684              :     this.message,
     685              :     this.errorCode,
     686              :     this.resultData,
     687              :     required this.timestamp,
     688              :     this.isStarted = false,
     689              :     this.workerType,
     690              :   });
     691              : 
     692              :   /// ID of the task.
     693              :   final String taskId;
     694              : 
     695              :   /// `true` when the native worker has just begun execution.
     696              :   ///
     697              :   /// When this flag is set the event is a **lifecycle notification**, not a
     698              :   /// completion event. [success], [message], [errorCode], and [resultData] are
     699              :   /// irrelevant for started events. [workerType] carries the worker class name.
     700              :   ///
     701              :   /// Use this to implement `onTaskStart` semantics without depending on
     702              :   /// progress events — reliable even for fast workers that emit no progress.
     703              :   final bool isStarted;
     704              : 
     705              :   /// Worker class name, set when [isStarted] is `true`.
     706              :   ///
     707              :   /// Examples: `'HttpDownloadWorker'`, `'HttpUploadWorker'`, `'DartCallbackWorker'`.
     708              :   /// `null` for completion events.
     709              :   final String? workerType;
     710              : 
     711              :   /// Whether the task succeeded.
     712              :   final bool success;
     713              : 
     714              :   /// Optional message (error message if failed).
     715              :   final String? message;
     716              : 
     717              :   /// Typed error code for failed tasks.
     718              :   ///
     719              :   /// `null` when [success] is `true` or when the native side did not supply an
     720              :   /// error code (e.g. older plugin versions).  Switch on this field to handle
     721              :   /// failure categories without parsing [message] strings.
     722              :   final NativeWorkManagerError? errorCode;
     723              : 
     724              :   /// Optional result data from the worker.
     725              :   final Map<String, dynamic>? resultData;
     726              : 
     727              :   /// When the event occurred.
     728              :   final DateTime timestamp;
     729              : 
     730              :   /// Create from platform channel map.
     731              :   ///
     732              :   /// FIX M5: Uses null-safe access on every field. A version mismatch between
     733              :   /// native and Dart (or a platform bug) could send null for required fields;
     734              :   /// an unchecked cast would throw and close the EventChannel stream silently.
     735            5 :   factory TaskEvent.fromMap(Map<String, dynamic> map) {
     736            5 :     final started = (map['isStarted'] as bool?) ?? false;
     737            5 :     final success = (map['success'] as bool?) ?? false;
     738            5 :     final rawErrorCode = map['errorCode'] as String?;
     739            5 :     return TaskEvent(
     740            5 :       taskId: (map['taskId'] as String?) ?? '',
     741              :       isStarted: started,
     742            5 :       workerType: map['workerType'] as String?,
     743              :       success: success,
     744            5 :       message: map['message'] as String?,
     745              :       // Only parse errorCode on failure; ignore stray codes on success.
     746              :       errorCode: (!success && !started && rawErrorCode != null)
     747            0 :           ? NativeWorkManagerError.fromString(rawErrorCode)
     748              :           : null,
     749           10 :       resultData: map['resultData'] is Map
     750            8 :           ? Map<String, dynamic>.from(map['resultData'] as Map)
     751              :           : null,
     752            5 :       timestamp: map['timestamp'] != null
     753            5 :           ? DateTime.fromMillisecondsSinceEpoch(
     754           10 :               (map['timestamp'] as num).toInt())
     755            3 :           : DateTime.now(),
     756              :     );
     757              :   }
     758              : 
     759              :   /// Convert to map.
     760            6 :   Map<String, dynamic> toMap() => {
     761            6 :         'taskId': taskId,
     762            3 :         if (isStarted) 'isStarted': isStarted,
     763            3 :         if (workerType != null) 'workerType': workerType,
     764            6 :         'success': success,
     765            6 :         'message': message,
     766            3 :         if (errorCode != null) 'errorCode': errorCode!.rawValue,
     767            6 :         'resultData': resultData,
     768            9 :         'timestamp': timestamp.millisecondsSinceEpoch,
     769              :       };
     770              : 
     771            3 :   @override
     772              :   bool operator ==(Object other) =>
     773              :       identical(this, other) ||
     774            2 :       other is TaskEvent &&
     775            6 :           taskId == other.taskId &&
     776            6 :           isStarted == other.isStarted &&
     777            6 :           workerType == other.workerType &&
     778            6 :           success == other.success &&
     779            6 :           errorCode == other.errorCode &&
     780            6 :           message == other.message &&
     781            6 :           _mapsEqual(resultData, other.resultData) &&
     782            6 :           timestamp == other.timestamp;
     783              : 
     784            2 :   static bool _mapsEqual(Map<String, dynamic>? a, Map<String, dynamic>? b) {
     785              :     if (identical(a, b)) return true;
     786            0 :     if (a == null || b == null) return a == b;
     787            0 :     if (a.length != b.length) return false;
     788            0 :     for (final key in a.keys) {
     789            0 :       if (!b.containsKey(key) || b[key] != a[key]) return false;
     790              :     }
     791              :     return true;
     792              :   }
     793              : 
     794            2 :   @override
     795            2 :   int get hashCode => Object.hash(
     796            2 :       taskId,
     797            2 :       isStarted,
     798            2 :       workerType,
     799            2 :       success,
     800            2 :       errorCode,
     801            2 :       message,
     802            2 :       resultData == null
     803              :           ? null
     804            0 :           : Object.hashAll(
     805            0 :               resultData!.entries.map((e) => Object.hash(e.key, e.value))),
     806            2 :       timestamp);
     807              : 
     808            1 :   @override
     809            1 :   String toString() => isStarted
     810            0 :       ? 'TaskEvent(taskId: $taskId, isStarted: true, workerType: $workerType, timestamp: $timestamp)'
     811            1 :       : 'TaskEvent('
     812            1 :           'taskId: $taskId, '
     813            1 :           'success: $success, '
     814            1 :           '${errorCode != null ? "errorCode: ${errorCode!.rawValue}, " : ""}'
     815            1 :           'message: $message, '
     816            1 :           'timestamp: $timestamp)';
     817              : }
     818              : 
     819              : /// Progress update during task execution.
     820              : ///
     821              : /// Workers can report progress during long-running operations. Listen to
     822              : /// [NativeWorkManager.progress] to receive real-time updates and show
     823              : /// progress bars or status messages in your UI.
     824              : ///
     825              : /// ## Reporting Progress from Worker
     826              : ///
     827              : /// ```dart
     828              : /// @pragma('vm:entry-point')
     829              : /// Future<WorkerResult> downloadFiles(WorkerInput input) async {
     830              : ///   final urls = input.data['urls'] as List<String>;
     831              : ///   final total = urls.length;
     832              : ///
     833              : ///   for (var i = 0; i < urls.length; i++) {
     834              : ///     // Report progress
     835              : ///     await input.reportProgress(
     836              : ///       progress: ((i + 1) / total * 100).round(),
     837              : ///       message: 'Downloading file ${i + 1} of $total',
     838              : ///       currentStep: i + 1,
     839              : ///       totalSteps: total,
     840              : ///     );
     841              : ///
     842              : ///     await downloadFile(urls[i]);
     843              : ///   }
     844              : ///
     845              : ///   return WorkerResult.success();
     846              : /// }
     847              : /// ```
     848              : ///
     849              : /// ## Listening to Progress Updates
     850              : ///
     851              : /// ```dart
     852              : /// void initState() {
     853              : ///   super.initState();
     854              : ///
     855              : ///   // Listen to progress for all tasks
     856              : ///   NativeWorkManager.progress.listen((progress) {
     857              : ///     setState(() {
     858              : ///       _currentProgress = progress.progress;
     859              : ///       _statusMessage = progress.message ?? 'Processing...';
     860              : ///     });
     861              : ///   });
     862              : /// }
     863              : ///
     864              : /// @override
     865              : /// Widget build(BuildContext context) {
     866              : ///   return Column(
     867              : ///     children: [
     868              : ///       LinearProgressIndicator(value: _currentProgress / 100),
     869              : ///       Text(_statusMessage),
     870              : ///       if (_currentStep != null && _totalSteps != null)
     871              : ///         Text('Step $_currentStep of $_totalSteps'),
     872              : ///     ],
     873              : ///   );
     874              : /// }
     875              : /// ```
     876              : ///
     877              : /// ## Filtering Progress by Task
     878              : ///
     879              : /// ```dart
     880              : /// // Only listen to specific task's progress
     881              : /// NativeWorkManager.progress
     882              : ///     .where((p) => p.taskId == 'bulk-upload')
     883              : ///     .listen((progress) {
     884              : ///       print('Upload: ${progress.progress}% - ${progress.message}');
     885              : ///
     886              : ///       if (progress.currentStep != null && progress.totalSteps != null) {
     887              : ///         print('File ${progress.currentStep}/${progress.totalSteps}');
     888              : ///       }
     889              : ///     });
     890              : /// ```
     891              : ///
     892              : /// ## Multi-Step Task with Progress
     893              : ///
     894              : /// ```dart
     895              : /// @pragma('vm:entry-point')
     896              : /// Future<WorkerResult> processImages(WorkerInput input) async {
     897              : ///   final images = input.data['images'] as List<String>;
     898              : ///   final steps = ['Download', 'Resize', 'Compress', 'Upload'];
     899              : ///   final totalSteps = images.length * steps.length;
     900              : ///   var currentStep = 0;
     901              : ///
     902              : ///   for (var image in images) {
     903              : ///     // Download
     904              : ///     currentStep++;
     905              : ///     await input.reportProgress(
     906              : ///       progress: (currentStep / totalSteps * 100).round(),
     907              : ///       message: 'Downloading $image',
     908              : ///       currentStep: currentStep,
     909              : ///       totalSteps: totalSteps,
     910              : ///     );
     911              : ///     await downloadImage(image);
     912              : ///
     913              : ///     // Resize
     914              : ///     currentStep++;
     915              : ///     await input.reportProgress(
     916              : ///       progress: (currentStep / totalSteps * 100).round(),
     917              : ///       message: 'Resizing $image',
     918              : ///       currentStep: currentStep,
     919              : ///       totalSteps: totalSteps,
     920              : ///     );
     921              : ///     await resizeImage(image);
     922              : ///
     923              : ///     // Compress
     924              : ///     currentStep++;
     925              : ///     await input.reportProgress(
     926              : ///       progress: (currentStep / totalSteps * 100).round(),
     927              : ///       message: 'Compressing $image',
     928              : ///       currentStep: currentStep,
     929              : ///       totalSteps: totalSteps,
     930              : ///     );
     931              : ///     await compressImage(image);
     932              : ///
     933              : ///     // Upload
     934              : ///     currentStep++;
     935              : ///     await input.reportProgress(
     936              : ///       progress: (currentStep / totalSteps * 100).round(),
     937              : ///       message: 'Uploading $image',
     938              : ///       currentStep: currentStep,
     939              : ///       totalSteps: totalSteps,
     940              : ///     );
     941              : ///     await uploadImage(image);
     942              : ///   }
     943              : ///
     944              : ///   return WorkerResult.success();
     945              : /// }
     946              : /// ```
     947              : ///
     948              : /// ## Progress with Network Upload
     949              : ///
     950              : /// ```dart
     951              : /// @pragma('vm:entry-point')
     952              : /// Future<WorkerResult> uploadLargeFile(WorkerInput input) async {
     953              : ///   final file = File(input.data['filePath']);
     954              : ///   final fileSize = await file.length();
     955              : ///   var uploaded = 0;
     956              : ///
     957              : ///   await uploadWithProgress(
     958              : ///     file,
     959              : ///     onProgress: (bytes) {
     960              : ///       uploaded += bytes;
     961              : ///       final progress = (uploaded / fileSize * 100).round();
     962              : ///
     963              : ///       input.reportProgress(
     964              : ///         progress: progress,
     965              : ///         message: 'Uploaded ${uploaded ~/ 1024}KB / ${fileSize ~/ 1024}KB',
     966              : ///       );
     967              : ///     },
     968              : ///   );
     969              : ///
     970              : ///   return WorkerResult.success();
     971              : /// }
     972              : /// ```
     973              : ///
     974              : /// ## Progress Fields
     975              : ///
     976              : /// - [taskId]: Identifier of the task reporting progress
     977              : /// - [progress]: Percentage (0-100) of completion
     978              : /// - [message]: Optional human-readable status message
     979              : /// - [currentStep]: Current step number (for multi-step tasks)
     980              : /// - [totalSteps]: Total number of steps (for multi-step tasks)
     981              : ///
     982              : /// ## Platform Behavior
     983              : ///
     984              : /// **Android:**
     985              : /// - Progress delivered via WorkManager's setProgress API
     986              : /// - Updates throttled to ~1 per second to conserve resources
     987              : /// - Reliable delivery while app is active
     988              : ///
     989              : /// **iOS:**
     990              : /// - Progress delivered when app is active or backgrounded
     991              : /// - Updates batched if app is suspended
     992              : /// - May be delayed for terminated apps
     993              : ///
     994              : /// ## Important Notes
     995              : ///
     996              : /// - Progress updates are **best-effort** delivery
     997              : /// - Not guaranteed if app is terminated
     998              : /// - Updates may be **throttled** by the OS
     999              : /// - Don't rely on receiving every single update
    1000              : /// - Progress is **optional** - tasks work without it
    1001              : ///
    1002              : /// ## Performance Tips
    1003              : ///
    1004              : /// ✅ **Do** report progress at meaningful intervals (e.g., every file, every 5%)
    1005              : /// ✅ **Do** include useful messages for users
    1006              : /// ✅ **Do** use currentStep/totalSteps for multi-step tasks
    1007              : ///
    1008              : /// ❌ **Don't** report progress on every byte (too frequent)
    1009              : /// ❌ **Don't** report progress more than once per second
    1010              : /// ❌ **Don't** report progress for tasks under 5 seconds
    1011              : /// ❌ **Don't** block worker execution waiting for progress delivery
    1012              : ///
    1013              : /// ## When to Use Progress
    1014              : ///
    1015              : /// **Good for:**
    1016              : /// - File uploads/downloads (show bytes transferred)
    1017              : /// - Batch processing (show items processed)
    1018              : /// - Multi-step workflows (show current step)
    1019              : /// - Long operations (>10 seconds)
    1020              : ///
    1021              : /// **Not needed for:**
    1022              : /// - Quick tasks (<5 seconds)
    1023              : /// - Tasks with no intermediate steps
    1024              : /// - Tasks running while app is terminated
    1025              : ///
    1026              : /// See also:
    1027              : /// - [NativeWorkManager.progress] - Stream of progress updates
    1028              : /// - [NativeWorkManager.reportDartWorkerProgress] - Report from DartWorker callback
    1029              : /// - [TaskEvent] - Task completion notification
    1030              : @immutable
    1031              : class TaskProgress {
    1032            6 :   const TaskProgress({
    1033              :     required this.taskId,
    1034              :     required this.progress,
    1035              :     this.message,
    1036              :     this.currentStep,
    1037              :     this.totalSteps,
    1038              :     this.bytesDownloaded,
    1039              :     this.totalBytes,
    1040              :     this.networkSpeed,
    1041              :     this.timeRemaining,
    1042              :   });
    1043              : 
    1044              :   /// ID of the task.
    1045              :   final String taskId;
    1046              : 
    1047              :   /// Progress percentage (0-100).
    1048              :   final int progress;
    1049              : 
    1050              :   /// Optional status message.
    1051              :   final String? message;
    1052              : 
    1053              :   /// Current step number (for multi-step tasks).
    1054              :   final int? currentStep;
    1055              : 
    1056              :   /// Total number of steps.
    1057              :   final int? totalSteps;
    1058              : 
    1059              :   /// Bytes downloaded so far (download workers only).
    1060              :   ///
    1061              :   /// `null` if the worker does not report byte-level progress.
    1062              :   final int? bytesDownloaded;
    1063              : 
    1064              :   /// Total file size in bytes (download workers only).
    1065              :   ///
    1066              :   /// `null` if the server did not return a `Content-Length` header.
    1067              :   final int? totalBytes;
    1068              : 
    1069              :   /// Current download/upload speed in bytes per second.
    1070              :   ///
    1071              :   /// `null` if speed cannot be computed (e.g. task just started).
    1072              :   final double? networkSpeed;
    1073              : 
    1074              :   /// Estimated time remaining in milliseconds.
    1075              :   ///
    1076              :   /// Computed as `(totalBytes - bytesDownloaded) / networkSpeed`.
    1077              :   /// `null` if either [networkSpeed] or [totalBytes] is unavailable.
    1078              :   final Duration? timeRemaining;
    1079              : 
    1080              :   /// Whether this progress update carries byte-level information.
    1081            0 :   bool get hasNetworkInfo =>
    1082            0 :       bytesDownloaded != null && totalBytes != null && networkSpeed != null;
    1083              : 
    1084              :   /// Create from platform channel map.
    1085              :   ///
    1086              :   /// FIX M5: Null-safe access prevents crash if platform omits a required field.
    1087            8 :   factory TaskProgress.fromMap(Map<String, dynamic> map) => TaskProgress(
    1088            4 :         taskId: (map['taskId'] as String?) ?? '',
    1089            8 :         progress: (map['progress'] as num?)?.toInt() ?? 0,
    1090            4 :         message: map['message'] as String?,
    1091            6 :         currentStep: (map['currentStep'] as num?)?.toInt(),
    1092            6 :         totalSteps: (map['totalSteps'] as num?)?.toInt(),
    1093            4 :         bytesDownloaded: (map['bytesDownloaded'] as num?)?.toInt(),
    1094            4 :         totalBytes: (map['totalBytes'] as num?)?.toInt(),
    1095            4 :         networkSpeed: (map['networkSpeed'] as num? ??
    1096            4 :                 map['networkSpeedBytesPerSecond'] as num?)
    1097            1 :             ?.toDouble(),
    1098            4 :         timeRemaining: (map['timeRemainingMs'] != null ||
    1099            4 :                 map['timeRemainingSeconds'] != null)
    1100            1 :             ? Duration(
    1101            1 :                 milliseconds: (map['timeRemainingMs'] as num? ??
    1102            2 :                         (map['timeRemainingSeconds'] as num? ?? 0) * 1000)
    1103            1 :                     .toInt())
    1104              :             : null,
    1105              :       );
    1106              : 
    1107              :   /// Convert to map.
    1108            4 :   Map<String, dynamic> toMap() => {
    1109            4 :         'taskId': taskId,
    1110            4 :         'progress': progress,
    1111            4 :         'message': message,
    1112            4 :         'currentStep': currentStep,
    1113            4 :         'totalSteps': totalSteps,
    1114            2 :         if (bytesDownloaded != null) 'bytesDownloaded': bytesDownloaded,
    1115            2 :         if (totalBytes != null) 'totalBytes': totalBytes,
    1116            2 :         if (networkSpeed != null) 'networkSpeed': networkSpeed,
    1117            2 :         if (timeRemaining != null)
    1118            0 :           'timeRemainingMs': timeRemaining!.inMilliseconds,
    1119              :       };
    1120              : 
    1121            2 :   @override
    1122              :   bool operator ==(Object other) =>
    1123              :       identical(this, other) ||
    1124            1 :       other is TaskProgress &&
    1125            3 :           taskId == other.taskId &&
    1126            3 :           progress == other.progress &&
    1127            0 :           message == other.message &&
    1128            0 :           bytesDownloaded == other.bytesDownloaded &&
    1129            0 :           totalBytes == other.totalBytes &&
    1130            0 :           networkSpeed == other.networkSpeed &&
    1131            0 :           timeRemaining == other.timeRemaining;
    1132              : 
    1133            1 :   @override
    1134            1 :   int get hashCode => Object.hash(
    1135            7 :       taskId, progress, message, bytesDownloaded, totalBytes, networkSpeed, timeRemaining);
    1136              : 
    1137            1 :   @override
    1138            1 :   String toString() => 'TaskProgress('
    1139            1 :       'taskId: $taskId, '
    1140            1 :       'progress: $progress%, '
    1141            1 :       'message: $message, '
    1142            2 :       'step: $currentStep/$totalSteps, '
    1143            2 :       'bytes: $bytesDownloaded/$totalBytes, '
    1144            1 :       'speed: ${networkSpeed != null ? "${(networkSpeed! / 1024).toStringAsFixed(1)} KB/s" : "n/a"})';
    1145              : }
    1146              : 
    1147              : /// Represents a critical system-level error on the native side.
    1148              : ///
    1149              : /// These errors are usually fatal to a task or a queue (e.g., Disk Full).
    1150              : @immutable
    1151              : class SystemError {
    1152            0 :   const SystemError({
    1153              :     required this.code,
    1154              :     required this.message,
    1155              :     required this.timestamp,
    1156              :   });
    1157              : 
    1158              :   /// Unique error code (e.g. 'DISK_FULL').
    1159              :   final String code;
    1160              : 
    1161              :   /// Human-readable error description.
    1162              :   final String message;
    1163              : 
    1164              :   /// When the error occurred.
    1165              :   final DateTime timestamp;
    1166              : 
    1167            0 :   factory SystemError.fromMap(Map<String, dynamic> map) => SystemError(
    1168            0 :         code: map['code'] as String? ?? 'UNKNOWN',
    1169              :         message:
    1170            0 :             map['message'] as String? ?? 'An unexpected native error occurred',
    1171            0 :         timestamp: map['timestamp'] != null
    1172            0 :             ? DateTime.fromMillisecondsSinceEpoch(
    1173            0 :                 (map['timestamp'] as num).toInt())
    1174            0 :             : DateTime.now(),
    1175              :       );
    1176              : 
    1177            0 :   @override
    1178            0 :   String toString() => 'SystemError(code: $code, message: $message)';
    1179              : }
        

Generated by: LCOV version 2.4-0