LCOV - code coverage report
Current view: top level - src - native_work_manager.dart Coverage Total Hit
Test: lcov.info Lines: 31.2 % 282 88
Test Date: 2026-04-30 18:23:23 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : import 'dart:convert';
       3              : import 'dart:ui';
       4              : import 'dart:developer' as developer;
       5              : 
       6              : import 'package:flutter/foundation.dart';
       7              : import 'package:flutter/services.dart';
       8              : import 'package:flutter/widgets.dart';
       9              : 
      10              : import 'constraints.dart';
      11              : import 'enqueue_request.dart';
      12              : import 'events.dart';
      13              : import 'observability.dart';
      14              : import 'platform_interface.dart';
      15              : import 'remote_trigger.dart';
      16              : import 'middleware.dart';
      17              : import 'task_chain.dart';
      18              : import 'task_graph.dart';
      19              : import 'task_handler.dart';
      20              : import 'task_trigger.dart';
      21              : 
      22              : import 'worker.dart';
      23              : 
      24              : /// Main entry point for scheduling native background tasks.
      25              : ///
      26              : /// NativeWorkManager provides a unified API for scheduling background tasks
      27              : /// on both Android and iOS. It uses Kotlin Multiplatform (KMP) under the hood
      28              : /// for native performance.
      29              : ///
      30              : /// ## 🛡️ The Pure 10 Architecture
      31              : ///
      32              : /// This library is built on the **"Pure Native"** principle — utilizing 100%
      33              : /// platform-native APIs without third-party dependencies.
      34              : ///
      35              : /// - **Reliability (10/10)**: **Native Watchdog** automatically recovers tasks
      36              : ///   stuck in 'running' state after an app crash or system reboot.
      37              : /// - **Persistence (10/10)**: Uses **Atomic File Writing** (iOS) and
      38              : ///   **SQLite WAL Mode** (Android) to guarantee data integrity.
      39              : /// - **Privacy (10/10)**: **Sensitive Data Redaction** recursively strips
      40              : ///   tokens, passwords, and cookies before persisting tasks to disk.
      41              : /// - **Performance (10/10)**: **Zero Flutter Engine overhead** for native workers,
      42              : ///   consuming ~2MB RAM vs ~50MB for traditional plugins.
      43              : ///
      44              : /// ## Quick Start
      45              : ///
      46              : /// ```dart
      47              : /// void main() async {
      48              : ///   WidgetsFlutterBinding.ensureInitialized();
      49              : ///   // Initialize with optional cleanup and concurrency settings
      50              : ///   await NativeWorkManager.initialize(
      51              : ///     maxConcurrentTasks: 4,
      52              : ///     cleanupAfterDays: 7,
      53              : ///   );
      54              : ///   runApp(MyApp());
      55              : /// }
      56              : ///
      57              : /// // Schedule a secure periodic sync
      58              : /// await NativeWorkManager.enqueue(
      59              : ///   taskId: 'daily-sync',
      60              : ///   trigger: TaskTrigger.periodic(Duration(hours: 24)),
      61              : ///   worker: NativeWorker.httpSync(
      62              : ///     url: 'https://api.example.com/sync',
      63              : ///     headers: {'Authorization': 'Bearer $token'}, // Auto-redacted in store!
      64              : ///   ),
      65              : ///   constraints: Constraints.networkRequired,
      66              : /// );
      67              : /// ```
      68              : ///
      69              : /// ## Execution Modes
      70              : ///
      71              : /// ### 🚀 Mode 1: Native Workers (Recommended)
      72              : /// Runs directly in Kotlin (Android) or Swift (iOS).
      73              : /// **RAM Usage**: ~2MB per task.
      74              : /// **Startup Time**: <50ms.
      75              : ///
      76              : /// ### 🧩 Mode 2: Dart Workers
      77              : /// Runs Dart code in a headless isolate.
      78              : /// **RAM Usage**: ~50MB per task.
      79              : /// **Startup Time**: 1000ms - 2000ms.
      80              : /// Includes **Isolate Caching**: The engine stays "warm" for 5 minutes after
      81              : /// completion to speed up consecutive tasks.
      82              : /// Top-level callback dispatcher for background Dart execution.
      83              : ///
      84              : /// This function is invoked by the native side when initializing
      85              : /// the Flutter Engine for Dart workers. It sets up the MethodChannel
      86              : /// and signals that Dart is ready to receive callback invocations.
      87              : ///
      88              : /// DO NOT call this function directly - it's only for native side.
      89              : ///
      90              : /// The @pragma annotation prevents the Dart compiler from tree-shaking
      91              : /// this function in release builds, which is critical for background execution.
      92            0 : @pragma('vm:entry-point')
      93              : Future<void> _callbackDispatcher() async {
      94              :   // Ensure Flutter binding is initialized
      95            0 :   WidgetsFlutterBinding.ensureInitialized();
      96              : 
      97              :   // Setup MethodChannel for receiving callback invocations
      98              :   const channel = MethodChannel('dev.brewkits/dart_worker_channel');
      99              : 
     100              :   // Signal that Dart is ready.
     101              :   // Await the invokeMethod call and handle errors explicitly — an unawaited future
     102              :   // would silently swallow any PlatformException, causing the native 10-second
     103              :   // timeout to fire and destroy the engine.
     104            0 :   await channel.invokeMethod<void>('dartReady').catchError((Object e) {
     105            0 :     developer.log('ERROR: dartReady signal failed: $e');
     106              :   });
     107              : 
     108              :   // Handle callback invocations
     109            0 :   channel.setMethodCallHandler((call) async {
     110            0 :     if (call.method == 'executeCallback') {
     111              :       // Guard against null arguments from native side.
     112              :       // null args indicates a platform channel protocol error or native
     113              :       // serialization failure — distinct from a worker with no input
     114              :       // (which arrives as args != null but args['input'] == null).
     115            0 :       final args = call.arguments as Map?;
     116              :       if (args == null) {
     117            0 :         developer.log(
     118              :           '[NativeWorkManager] CRITICAL: null arguments in executeCallback.\n'
     119              :           'This is a platform channel protocol error, not a worker failure.\n'
     120              :           'Check native logs (Logcat / Xcode console) for serialization errors.',
     121              :           level: 1000, // SHOUT — highest severity
     122              :         );
     123              :         return false;
     124              :       }
     125              : 
     126              :       // Extract callback handle and input
     127              :       // DART-005: Use num cast then toInt() to handle both int and int64 from native.
     128            0 :       final callbackId = args['callbackId'] as String? ?? '<unknown>';
     129            0 :       final callbackHandleRaw = args['callbackHandle'];
     130            0 :       final callbackHandle = callbackHandleRaw is int
     131              :           ? callbackHandleRaw
     132            0 :           : (callbackHandleRaw is num ? callbackHandleRaw.toInt() : null);
     133              :       if (callbackHandle == null) {
     134            0 :         developer.log(
     135              :             'ERROR in _callbackDispatcher: missing or invalid callbackHandle '
     136            0 :             '(got ${callbackHandleRaw.runtimeType})');
     137              :         return false;
     138              :       }
     139            0 :       final inputJson = args['input'] as String?;
     140              : 
     141              :       try {
     142              :         // Convert handle back to callback function
     143            0 :         final callbackInfo = PluginUtilities.getCallbackFromHandle(
     144            0 :           CallbackHandle.fromRawHandle(callbackHandle),
     145              :         );
     146              : 
     147              :         if (callbackInfo == null) {
     148            0 :           throw StateError(
     149              :             'Failed to resolve callback handle: $callbackHandle. '
     150              :             'Ensure the callback is a top-level or static function.',
     151              :           );
     152              :         }
     153              : 
     154              :         // Validate function signature before invoking.
     155              :         // Dart's `as` cast does not check function signatures at cast-time — it
     156              :         // only fails when the function is called, producing a cryptic TypeError.
     157            0 :         if (callbackInfo is! DartWorkerCallback) {
     158            0 :           throw StateError(
     159            0 :             'Callback handle $callbackHandle resolved to ${callbackInfo.runtimeType} '
     160              :             'but DartWorkerCallback (Future<bool> Function(Map<String, dynamic>?)) was expected. '
     161              :             'Ensure the registered function has the correct signature.',
     162              :           );
     163              :         }
     164              :         final callback = callbackInfo;
     165              : 
     166              :         // Parse input JSON if present
     167              :         Map<String, dynamic>? input;
     168              :         // Check if inputJson is literally the string "null" or empty, and treat it as no input
     169            0 :         if (inputJson != null && inputJson.isNotEmpty && inputJson != "null") {
     170              :           try {
     171            0 :             input = jsonDecode(inputJson) as Map<String, dynamic>;
     172              :           } catch (e) {
     173            0 :             throw FormatException(
     174            0 :               'Failed to parse callback input JSON: "$inputJson"',
     175              :               inputJson,
     176              :             );
     177              :           }
     178              :         }
     179              : 
     180              :         // Execute the callback with a hard timeout to prevent indefinite hangs.
     181              :         // 25 s leaves a 5 s safety buffer within iOS BGAppRefreshTask's 30 s budget.
     182              :         // For longer work use Constraints(bgTaskType: BGTaskType.processing).
     183            0 :         final result = await callback(input).timeout(
     184              :           const Duration(seconds: 25),
     185            0 :           onTimeout: () {
     186            0 :             developer.log(
     187              :               '[NativeWorkManager] DartWorker callback "$callbackId" timed out '
     188              :               'after 25 s. Consider:\n'
     189              :               '  • Breaking the work into smaller tasks.\n'
     190              :               '  • Using Constraints(bgTaskType: BGTaskType.processing) for '
     191              :               'tasks that need up to 10 minutes (iOS).',
     192              :               level: 900,
     193              :             );
     194              :             return false;
     195              :           },
     196              :         );
     197              : 
     198              :         // Return execution result to native side
     199              :         return result;
     200              :       } catch (e, stackTrace) {
     201              :         // Log error for debugging
     202            0 :         developer.log('ERROR in _callbackDispatcher: $e');
     203            0 :         developer.log('Stack trace: $stackTrace');
     204              : 
     205              :         // Return false to indicate failure
     206              :         return false;
     207              :       }
     208              :     }
     209              : 
     210            0 :     throw MissingPluginException('Unknown method: ${call.method}');
     211              :   });
     212              : }
     213              : 
     214              : class NativeWorkManager {
     215            0 :   NativeWorkManager._();
     216              : 
     217              :   static bool _initialized = false;
     218              :   static bool _enforceHttps = false;
     219              :   static bool _blockPrivateIPs = false;
     220              :   static bool _registerPlugins = false;
     221              : 
     222              :   /// Whether HTTPS is enforced for all background HTTP tasks.
     223           17 :   @internal
     224              :   static bool get enforceHttps => _enforceHttps;
     225              : 
     226              :   /// Whether private/loopback IPs are blocked for background HTTP tasks.
     227           17 :   @internal
     228              :   static bool get blockPrivateIPs => _blockPrivateIPs;
     229              : 
     230              :   /// Whether plugins are registered in the background Flutter Engine.
     231            1 :   @internal
     232              :   static bool get registerPluginsEnabled => _registerPlugins;
     233              : 
     234              :   /// Internal testing only - resets security flags.
     235            2 :   @visibleForTesting
     236              :   static void resetSecurityFlags() {
     237              :     _enforceHttps = false;
     238              :     _blockPrivateIPs = false;
     239              :     _registerPlugins = false;
     240              :   }
     241              : 
     242              :   /// Internal testing only - resets initialized state so initialize() can be
     243              :   /// called again with different parameters in the same test process.
     244            2 :   @visibleForTesting
     245              :   static void resetInitializedState() {
     246              :     _initialized = false;
     247              :     _initCompleter = null;
     248              :     _enforceHttps = false;
     249              :     _blockPrivateIPs = false;
     250              :     _registerPlugins = false;
     251              :   }
     252              : 
     253              :   // Completer used to make concurrent initialize() calls wait on the first one
     254              :   // rather than racing past the _initialized flag check.
     255              :   static Completer<void>? _initCompleter;
     256           15 :   static final Map<String, DartWorkerCallback> _dartWorkers = {};
     257              : 
     258              :   // Observability
     259              :   static StreamSubscription<TaskEvent>? _observabilityEventSub;
     260              :   static StreamSubscription<TaskProgress>? _observabilityProgressSub;
     261              : 
     262              :   /// Map of callback IDs to their serializable handles.
     263              :   /// Handles can be passed across isolates, unlike function closures.
     264           15 :   static final Map<String, int> _callbackHandles = {};
     265              : 
     266              :   // ═══════════════════════════════════════════════════════════════════════════
     267              :   // INITIALIZATION
     268              :   // ═══════════════════════════════════════════════════════════════════════════
     269              : 
     270              :   /// Initialize the work manager.
     271              :   ///
     272              :   /// **REQUIRED:** Must be called before any other method, typically in `main()`.
     273              :   ///
     274              :   /// ## Basic Usage (Native Workers Only)
     275              :   ///
     276              :   /// ```dart
     277              :   /// void main() async {
     278              :   ///   WidgetsFlutterBinding.ensureInitialized();
     279              :   ///   await NativeWorkManager.initialize();
     280              :   ///   runApp(MyApp());
     281              :   /// }
     282              :   /// ```
     283              :   ///
     284              :   /// ## Advanced Usage (With Dart Workers)
     285              :   ///
     286              :   /// ```dart
     287              :   /// void main() async {
     288              :   ///   WidgetsFlutterBinding.ensureInitialized();
     289              :   ///   await NativeWorkManager.initialize(
     290              :   ///     dartWorkers: {
     291              :   ///       'customSync': (input) async {
     292              :   ///         // Your custom Dart logic
     293              :   ///         final data = await fetchDataFromLocalDb();
     294              :   ///         await uploadToServer(data);
     295              :   ///         return true; // true = success, false = failure
     296              :   ///       },
     297              :   ///       'cleanup': (input) async {
     298              :   ///         await cleanupOldFiles();
     299              :   ///         return true;
     300              :   ///       },
     301              :   ///     },
     302              :   ///     debugMode: true,  // Shows notifications for all task events
     303              :   ///   );
     304              :   ///   runApp(MyApp());
     305              :   /// }
     306              :   /// ```
     307              :   ///
     308              :   /// ## Parameters
     309              :   ///
     310              :   /// **[dartWorkers]** - Optional map of callback IDs to worker functions.
     311              :   /// - Only needed if you want to run Dart code in background (Mode 2)
     312              :   /// - Each callback receives optional input data as `Map<String, dynamic>`
     313              :   /// - Must return `Future<bool>` (true = success, false = failure)
     314              :   /// - Callbacks run in a background isolate with Flutter Engine
     315              :   ///
     316              :   /// **[debugMode]** - Enable debug notifications (default: false).
     317              :   /// - Shows notifications when tasks complete with execution time
     318              :   /// - Displays success/failure status
     319              :   /// - **Only works in debug builds** - automatically disabled in release
     320              :   /// - Useful for development and debugging
     321              :   ///
     322              :   /// ## Platform Considerations
     323              :   ///
     324              :   /// **Android:**
     325              :   /// - No special setup required
     326              :   /// - Debug notifications use NotificationManager
     327              :   /// - Automatically creates debug notification channel
     328              :   ///
     329              :   /// **iOS:**
     330              :   /// - Debug mode requests notification permissions on first run
     331              :   /// - Uses UNUserNotificationCenter
     332              :   /// - BGTaskScheduler setup is automatic (reads Info.plist)
     333              :   ///
     334              :   /// ## Common Pitfalls
     335              :   ///
     336              :   /// ❌ **Don't** call initialize() multiple times - it's idempotent but wasteful
     337              :   /// ❌ **Don't** forget to call this before scheduling tasks
     338              :   /// ❌ **Don't** register anonymous functions as dartWorkers (won't work in background)
     339              :   /// ✅ **Do** call this in main() before runApp()
     340              :   /// ✅ **Do** use named top-level or static functions for dartWorkers
     341              :   ///
     342              :   /// ## See Also
     343              :   ///
     344              :   /// - [enqueue] - Schedule a background task
     345              :   /// - [DartWorker] - Create a Dart callback worker
     346              :   /// - [NativeWorker] - Create a native worker (no Flutter Engine)
     347            5 :   static Future<void> initialize({
     348              :     Map<String, DartWorkerCallback>? dartWorkers,
     349              :     bool debugMode = false,
     350              :     int maxConcurrentTasks = 4,
     351              :     int diskSpaceBufferMB = 20,
     352              : 
     353              :     /// Automatically delete completed/failed/cancelled task records older than
     354              :     /// this many days during initialize(). Set to 0 to disable auto-cleanup.
     355              :     /// Default: 30 days (prevents unbounded SQLite growth on long-running apps).
     356              :     int cleanupAfterDays = 30,
     357              : 
     358              :     /// When true, all HTTP workers reject plain HTTP URLs and only allow HTTPS.
     359              :     /// Useful for apps that require transport security across all background tasks.
     360              :     /// Default: false (backward compatible).
     361              :     bool enforceHttps = false,
     362              : 
     363              :     /// When true, HTTP workers block requests to private/loopback IP ranges
     364              :     /// (10.x, 172.16-31.x, 192.168.x, 127.x, ::1) to prevent SSRF attacks.
     365              :     /// Has no effect on hostnames — only parsed IP literals are checked.
     366              :     /// Default: false (backward compatible).
     367              :     bool blockPrivateIPs = false,
     368              : 
     369              :     /// When true, the background Flutter Engine will automatically register all
     370              :     /// plugins (calls GeneratedPluginRegistrant). This is required if you want
     371              :     /// to use other plugins (like flutter_local_notifications) inside your
     372              :     /// Dart workers.
     373              :     ///
     374              :     /// WARNING: Enabling this may increase RAM usage (~10-20MB) and may cause
     375              :     /// side-effects if other plugins perform cleanup when the background
     376              :     /// engine is destroyed (e.g. disconnecting Bluetooth or stopping audio).
     377              :     ///
     378              :     /// ALTERNATIVE: If you leave this false, you can still register specific
     379              :     /// plugins natively via `NativeWorkmanagerPlugin.setPluginRegistrantCallback`.
     380              :     ///
     381              :     /// Default: false (Zero-Engine I/O principle).
     382              :     bool registerPlugins = false,
     383              :   }) async {
     384              :     // If already initializing (concurrent calls), wait on the in-flight future.
     385            3 :     if (_initCompleter != null) return _initCompleter!.future;
     386              :     if (_initialized) return;
     387              : 
     388            5 :     _initCompleter = Completer<void>();
     389              :     try {
     390            5 :       await _initializeInternal(
     391              :         dartWorkers: dartWorkers,
     392              :         debugMode: debugMode,
     393              :         maxConcurrentTasks: maxConcurrentTasks,
     394              :         diskSpaceBufferMB: diskSpaceBufferMB,
     395              :         cleanupAfterDays: cleanupAfterDays,
     396              :         enforceHttps: enforceHttps,
     397              :         blockPrivateIPs: blockPrivateIPs,
     398              :         registerPlugins: registerPlugins,
     399              :       );
     400              :       _initialized = true;
     401            5 :       _initCompleter!.complete();
     402              :     } catch (e, st) {
     403              :       final completer = _initCompleter;
     404              :       _initCompleter = null; // allow retry on failure
     405            0 :       completer?.completeError(e, st);
     406              :       rethrow;
     407              :     }
     408              :   }
     409              : 
     410            5 :   static Future<void> _initializeInternal({
     411              :     Map<String, DartWorkerCallback>? dartWorkers,
     412              :     bool debugMode = false,
     413              :     int maxConcurrentTasks = 4,
     414              :     int diskSpaceBufferMB = 20,
     415              :     int cleanupAfterDays = 30,
     416              :     bool enforceHttps = false,
     417              :     bool blockPrivateIPs = false,
     418              :     bool registerPlugins = false,
     419              :   }) async {
     420              :     _enforceHttps = enforceHttps;
     421              :     _blockPrivateIPs = blockPrivateIPs;
     422              :     _registerPlugins = registerPlugins;
     423              : 
     424              :     // Phase 2: Register DevTools Extension Service
     425            5 :     registerDevToolsExtensions();
     426              : 
     427              :     // Register Dart workers and compute their handles
     428              :     if (dartWorkers != null) {
     429            2 :       _dartWorkers.addAll(dartWorkers);
     430              : 
     431              :       // Compute callback handles for each worker
     432            2 :       for (final entry in dartWorkers.entries) {
     433            1 :         final callbackId = entry.key;
     434            1 :         final callback = entry.value;
     435              : 
     436              :         // Get the handle for this callback
     437            1 :         final handle = PluginUtilities.getCallbackHandle(callback);
     438              : 
     439              :         if (handle == null) {
     440            0 :           throw StateError(
     441              :             'Failed to get callback handle for "$callbackId". '
     442              :             'Ensure the callback is a top-level or static function, '
     443              :             'NOT an anonymous function or instance method.\n'
     444              :             '\n'
     445              :             'Example CORRECT:\n'
     446              :             '  Future<bool> myCallback(Map<String, dynamic>? input) async { ... }\n'
     447              :             '  dartWorkers: {"myCallback": myCallback}\n'
     448              :             '\n'
     449              :             'Example WRONG:\n'
     450              :             '  dartWorkers: {"bad": (input) async => true} // Anonymous function!',
     451              :           );
     452              :         }
     453              : 
     454            3 :         _callbackHandles[callbackId] = handle.toRawHandle();
     455              :       }
     456              :     }
     457              : 
     458              :     // Set up callback executor
     459           10 :     NativeWorkManagerPlatform.instance.setCallbackExecutor(
     460              :       _executeDartCallback,
     461              :     );
     462              : 
     463              :     // Set up chain enqueue callback
     464              :     TaskChainBuilder.enqueueCallback = _enqueueChain;
     465              : 
     466              :     // Get callback handle for the dispatcher if any Dart workers registered
     467              :     int? callbackHandle;
     468            1 :     if (dartWorkers != null && dartWorkers.isNotEmpty) {
     469              :       // Get handle of the callback dispatcher
     470            1 :       callbackHandle = PluginUtilities.getCallbackHandle(
     471              :         _callbackDispatcher,
     472            1 :       )?.toRawHandle();
     473              :     }
     474              : 
     475              :     // Initialize platform with config.
     476           10 :     await NativeWorkManagerPlatform.instance.initialize(
     477              :       callbackHandle: callbackHandle,
     478              :       debugMode: debugMode,
     479              :       maxConcurrentTasks: maxConcurrentTasks,
     480              :       diskSpaceBufferMB: diskSpaceBufferMB,
     481              :       cleanupAfterDays: cleanupAfterDays,
     482              :       enforceHttps: enforceHttps,
     483              :       blockPrivateIPs: blockPrivateIPs,
     484              :       registerPlugins: registerPlugins,
     485              :     );
     486              :     // _initialized is set by the caller (initialize()) after this returns.
     487              :   }
     488              : 
     489            5 :   static void _checkInitialized() {
     490              :     if (!_initialized) {
     491            2 :       throw StateError(
     492              :         'NativeWorkManager not initialized. '
     493              :         'Call NativeWorkManager.initialize() first.',
     494              :       );
     495              :     }
     496              :   }
     497              : 
     498            0 :   static Future<bool> _executeDartCallback(
     499              :     String callbackId,
     500              :     Map<String, dynamic>? input,
     501              :   ) async {
     502            0 :     final callback = _dartWorkers[callbackId];
     503              :     if (callback == null) {
     504            0 :       throw StateError('No Dart worker registered for: $callbackId');
     505              :     }
     506              : 
     507            0 :     return callback(input);
     508              :   }
     509              : 
     510              :   // ═══════════════════════════════════════════════════════════════════════════
     511              :   // TASK SCHEDULING
     512              :   // ═══════════════════════════════════════════════════════════════════════════
     513              : 
     514              :   /// Schedule a background task.
     515              :   ///
     516              :   /// This is the primary method for scheduling work to be executed in the background.
     517              :   /// Tasks can be one-time, periodic, or triggered by specific conditions.
     518              :   ///
     519              :   /// ## Basic Examples
     520              :   ///
     521              :   /// **Immediate one-time task:**
     522              :   /// ```dart
     523              :   /// await NativeWorkManager.enqueue(
     524              :   ///   taskId: 'quick-sync',
     525              :   ///   trigger: TaskTrigger.oneTime(),
     526              :   ///   worker: NativeWorker.httpRequest(
     527              :   ///     url: 'https://api.example.com/ping',
     528              :   ///     method: HttpMethod.post,
     529              :   ///   ),
     530              :   /// );
     531              :   /// ```
     532              :   ///
     533              :   /// **Delayed task:**
     534              :   /// ```dart
     535              :   /// await NativeWorkManager.enqueue(
     536              :   ///   taskId: 'delayed-upload',
     537              :   ///   trigger: TaskTrigger.oneTime(delay: Duration(minutes: 15)),
     538              :   ///   worker: NativeWorker.httpUpload(
     539              :   ///     url: 'https://api.example.com/upload',
     540              :   ///     filePath: '/path/to/file.jpg',
     541              :   ///   ),
     542              :   /// );
     543              :   /// ```
     544              :   ///
     545              :   /// **Periodic task (every hour):**
     546              :   /// ```dart
     547              :   /// await NativeWorkManager.enqueue(
     548              :   ///   taskId: 'hourly-sync',
     549              :   ///   trigger: TaskTrigger.periodic(Duration(hours: 1)),
     550              :   ///   worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
     551              :   ///   constraints: Constraints.networkRequired,
     552              :   /// );
     553              :   /// ```
     554              :   ///
     555              :   /// **Task with constraints:**
     556              :   /// ```dart
     557              :   /// await NativeWorkManager.enqueue(
     558              :   ///   taskId: 'battery-safe-task',
     559              :   ///   trigger: TaskTrigger.oneTime(),
     560              :   ///   worker: NativeWorker.httpDownload(
     561              :   ///     url: 'https://example.com/large-file.zip',
     562              :   ///     savePath: '/path/to/save.zip',
     563              :   ///   ),
     564              :   ///   constraints: Constraints(
     565              :   ///     requiresCharging: true,
     566              :   ///     requiresWifi: true,
     567              :   ///   ),
     568              :   /// );
     569              :   /// ```
     570              :   ///
     571              :   /// **Tagged tasks for bulk operations:**
     572              :   /// ```dart
     573              :   /// // Schedule multiple tasks with same tag
     574              :   /// for (var i = 0; i < 5; i++) {
     575              :   ///   await NativeWorkManager.enqueue(
     576              :   ///     taskId: 'upload-$i',
     577              :   ///     trigger: TaskTrigger.oneTime(),
     578              :   ///     worker: NativeWorker.httpUpload(
     579              :   ///       url: 'https://api.example.com/upload',
     580              :   ///       filePath: '/path/to/file$i.jpg',
     581              :   ///     ),
     582              :   ///     tag: 'batch-upload',  // Same tag for all
     583              :   ///   );
     584              :   /// }
     585              :   ///
     586              :   /// // Later, cancel all at once
     587              :   /// await NativeWorkManager.cancelByTag(tag: 'batch-upload');
     588              :   /// ```
     589              :   ///
     590              :   /// ## Parameters
     591              :   ///
     592              :   /// **[taskId]** *(required)* - Unique identifier for the task.
     593              :   /// - Must not be empty
     594              :   /// - Used to cancel, query, or update the task
     595              :   /// - If duplicate ID exists, behavior depends on [existingPolicy]
     596              :   ///
     597              :   /// **[trigger]** *(required)* - When the task should execute.
     598              :   /// - `TaskTrigger.oneTime()` - Execute once (optionally with delay)
     599              :   /// - `TaskTrigger.periodic(duration)` - Repeat every duration (min 15 minutes)
     600              :   /// - See [TaskTrigger] for all options
     601              :   ///
     602              :   /// **[worker]** *(required)* - What work to perform.
     603              :   /// - `NativeWorker.*` - Native workers (no Flutter Engine, fast startup)
     604              :   /// - `DartWorker` - Run Dart code (requires Flutter Engine)
     605              :   /// - See [NativeWorker] and [DartWorker] for details
     606              :   ///
     607              :   /// **[constraints]** *(optional)* - Execution conditions (default: no constraints).
     608              :   /// - `Constraints.networkRequired` - Requires any network
     609              :   /// - `Constraints.heavyTask` - Requires charging + WiFi
     610              :   /// - Custom: `Constraints(requiresCharging: true, ...)`
     611              :   /// - See [Constraints] for all options
     612              :   ///
     613              :   /// **[existingPolicy]** *(optional)* - Handle duplicate task IDs (default: replace).
     614              :   /// - `ExistingTaskPolicy.replace` - Cancel old, schedule new
     615              :   /// - `ExistingTaskPolicy.keep` - Keep old, ignore new
     616              :   ///
     617              :   /// **[tag]** *(optional)* - Group related tasks for bulk operations.
     618              :   /// - Must not be empty string (use null if no tag)
     619              :   /// - Use with [cancelByTag] and [getTasksByTag]
     620              :   /// - Multiple tasks can share the same tag
     621              :   ///
     622              :   /// ## Platform Considerations
     623              :   ///
     624              :   /// **Android:**
     625              :   /// - Periodic tasks have minimum interval of 15 minutes (OS limitation)
     626              :   /// - Constraints enforced by WorkManager
     627              :   /// - Exact timing not guaranteed (OS may defer tasks)
     628              :   ///
     629              :   /// **iOS:**
     630              :   /// - Periodic tasks use BGAppRefreshTask (runs opportunistically)
     631              :   /// - One-time tasks use BGProcessingTask
     632              :   /// - Task may not run immediately even without delay
     633              :   /// - Charging/battery constraints are advisory only
     634              :   ///
     635              :   /// ## Common Pitfalls
     636              :   ///
     637              :   /// ❌ **Don't** use periodic intervals < 15 minutes (will throw error)
     638              :   /// ❌ **Don't** expect exact timing - OS may defer tasks
     639              :   /// ❌ **Don't** use empty string for tag (use null instead)
     640              :   /// ❌ **Don't** schedule too many tasks (performance impact)
     641              :   /// ✅ **Do** use tags for managing related tasks
     642              :   /// ✅ **Do** use constraints to optimize battery life
     643              :   /// ✅ **Do** handle task failure gracefully
     644              :   ///
     645              :   /// ## Error Handling
     646              :   ///
     647              :   /// This method may throw:
     648              :   /// - `ArgumentError` - Invalid parameters (empty taskId, invalid URL, etc.)
     649              :   /// - `StateError` - NativeWorkManager not initialized or unregistered DartWorker
     650              :   ///
     651              :   /// ## Returns
     652              :   ///
     653              :   /// A [TaskHandler] which allows tracking progress and completion of this specific task,
     654              :   /// and contains the [ScheduleResult] from the OS.
     655              :   ///
     656              :   /// ## See Also
     657              :   ///
     658              :   /// - [TaskHandler] - Controller for tracking task progress and result
     659              :   /// - [cancel] - Cancel a specific task
     660              :   /// - [cancelByTag] - Cancel all tasks with a tag
     661              :   /// - [cancelAll] - Cancel all scheduled tasks
     662              :   /// - [NativeWorkManager.getTaskStatus] - Check task status
     663              :   /// - [TaskTrigger] - Available trigger types
     664              :   /// - [Constraints] - Available constraints
     665            3 :   static Future<TaskHandler> enqueue({
     666              :     required String taskId,
     667              :     TaskTrigger trigger = const TaskTrigger.oneTime(),
     668              :     required Worker worker,
     669              :     Constraints constraints = const Constraints(),
     670              :     ExistingTaskPolicy existingPolicy = ExistingTaskPolicy.replace,
     671              :     String? tag,
     672              :   }) async {
     673            3 :     _checkInitialized();
     674              : 
     675              :     // Validation
     676            3 :     if (taskId.isEmpty) {
     677            0 :       throw ArgumentError(
     678              :         'taskId cannot be empty. '
     679            0 :         'Use a unique identifier like "sync-${DateTime.now().millisecondsSinceEpoch}"',
     680              :       );
     681              :     }
     682              : 
     683              :     // Validate tag if provided
     684            0 :     if (tag != null && tag.isEmpty) {
     685            0 :       throw ArgumentError(
     686              :         'tag cannot be empty string. '
     687              :         'Either provide a valid tag or omit the parameter.',
     688              :       );
     689              :     }
     690              : 
     691              :     // Validate periodic trigger interval (Android minimum)
     692            3 :     if (trigger is PeriodicTrigger) {
     693            2 :       if (trigger.interval < const Duration(minutes: 15)) {
     694            2 :         throw ArgumentError(
     695              :           'Periodic interval must be at least 15 minutes on Android.\n'
     696            2 :           'Current: ${trigger.interval.inMinutes} minutes\n'
     697              :           'Minimum: 15 minutes\n'
     698              :           'Use TaskTrigger.oneTime() for immediate execution.',
     699              :         );
     700              :       }
     701            3 :       if (trigger.initialDelay != null && trigger.initialDelay!.isNegative) {
     702            2 :         throw ArgumentError(
     703              :           'initialDelay cannot be negative.\n'
     704            1 :           'Current: ${trigger.initialDelay}',
     705              :         );
     706              :       }
     707              :     }
     708              : 
     709              :     // ExactTrigger is Android-only. BGTaskScheduler on iOS does not support
     710              :     // exact alarm scheduling — tasks fire within an OS-determined window that
     711              :     // can be hours late. Throw early so developers get a clear error rather
     712              :     // than silent misfires that are hard to debug.
     713            3 :     if (trigger is ExactTrigger &&
     714            0 :         defaultTargetPlatform == TargetPlatform.iOS) {
     715            0 :       throw UnsupportedError(
     716              :         'ExactTrigger is not supported on iOS.\n'
     717              :         'BGTaskScheduler cannot guarantee exact timing — tasks may run '
     718              :         'hours after the scheduled time depending on OS conditions.\n'
     719              :         'Alternatives:\n'
     720              :         '  • TaskTrigger.windowed() — approximate window (recommended)\n'
     721              :         '  • TaskTrigger.oneTime()  — run as soon as possible\n'
     722              :         '  • Local notifications (flutter_local_notifications) for user-visible alarms',
     723              :       );
     724              :     }
     725              : 
     726              :     // Validate DartWorker registration and prepare worker data
     727              :     Worker workerToEnqueue = worker;
     728              :     Constraints finalConstraints = constraints;
     729              : 
     730            3 :     if (worker is DartWorker) {
     731            6 :       if (!_dartWorkers.containsKey(worker.callbackId)) {
     732            0 :         throw StateError(
     733            0 :           'Dart worker "${worker.callbackId}" not registered.\n'
     734              :           'Register it in NativeWorkManager.initialize():\n'
     735              :           '  await NativeWorkManager.initialize(\n'
     736              :           '    dartWorkers: {\n'
     737            0 :           '      "${worker.callbackId}": (input) async { ... },\n'
     738              :           '    },\n'
     739              :           '  );',
     740              :         );
     741              :       }
     742              : 
     743              :       // iOS safety: DartWorkers are heavy by definition because they spin up
     744              :       // a Flutter Engine. Force BGProcessingTask (60s+) instead of
     745              :       // BGAppRefreshTask (30s) to prevent immediate OS kills.
     746            4 :       if (defaultTargetPlatform == TargetPlatform.iOS &&
     747            2 :           !constraints.isHeavyTask) {
     748            2 :         finalConstraints = constraints.copyWith(isHeavyTask: true);
     749            2 :         developer.log(
     750              :           'NativeWorkManager: DartWorker on iOS detected. Promoting to heavy task '
     751              :           '(isHeavyTask=true) to prevent OS termination.',
     752              :           name: 'NativeWorkManager',
     753              :         );
     754              :       }
     755              : 
     756              :       // Get the callback handle for this worker
     757            6 :       final callbackHandle = _callbackHandles[worker.callbackId];
     758              :       if (callbackHandle == null) {
     759            0 :         throw StateError(
     760            0 :           'INTERNAL ERROR: Callback handle not found for "${worker.callbackId}". '
     761              :           'This should never happen. Please report this bug.',
     762              :         );
     763              :       }
     764              : 
     765              :       // Create enhanced DartWorker with callback handle
     766            2 :       workerToEnqueue = DartWorkerInternal(
     767            2 :         callbackId: worker.callbackId,
     768              :         callbackHandle: callbackHandle,
     769            2 :         input: worker.input,
     770            2 :         autoDispose: worker.autoDispose,
     771            2 :         timeoutMs: worker.timeoutMs,
     772              :       );
     773              :     }
     774              : 
     775            6 :     final scheduleResult = await NativeWorkManagerPlatform.instance.enqueue(
     776              :       taskId: taskId,
     777              :       trigger: trigger,
     778              :       worker: workerToEnqueue,
     779              :       constraints: finalConstraints,
     780              :       existingPolicy: existingPolicy,
     781              :       tag: tag,
     782              :     );
     783              : 
     784            3 :     return TaskHandler(
     785              :       taskId: taskId,
     786              :       scheduleResult: scheduleResult,
     787              :     );
     788              :   }
     789              : 
     790              :   /// Cancel all tasks with a specific tag.
     791              :   ///
     792              :   /// This is the recommended way to cancel groups of related tasks. Much more
     793              :   /// efficient than canceling tasks individually.
     794              :   ///
     795              :   /// ## Example - Batch Upload Cancellation
     796              :   ///
     797              :   /// ```dart
     798              :   /// // Schedule multiple upload tasks with same tag
     799              :   /// for (var i = 0; i < 10; i++) {
     800              :   ///   await NativeWorkManager.enqueue(
     801              :   ///     taskId: 'upload-$i',
     802              :   ///     trigger: TaskTrigger.oneTime(),
     803              :   ///     worker: NativeWorker.httpUpload(
     804              :   ///       url: 'https://api.example.com/upload',
     805              :   ///       filePath: files[i],
     806              :   ///     ),
     807              :   ///     tag: 'batch-upload',
     808              :   ///   );
     809              :   /// }
     810              :   ///
     811              :   /// // User cancels upload - cancel all 10 tasks at once
     812              :   /// await NativeWorkManager.cancelByTag(tag: 'batch-upload');
     813              :   /// ```
     814              :   ///
     815              :   /// ## Example - Feature-Based Cancellation
     816              :   ///
     817              :   /// ```dart
     818              :   /// // Tag tasks by feature
     819              :   /// await NativeWorkManager.enqueue(
     820              :   ///   taskId: 'photo-sync',
     821              :   ///   trigger: TaskTrigger.periodic(Duration(hours: 6)),
     822              :   ///   worker: ...,
     823              :   ///   tag: 'photo-feature',
     824              :   /// );
     825              :   ///
     826              :   /// await NativeWorkManager.enqueue(
     827              :   ///   taskId: 'photo-cleanup',
     828              :   ///   trigger: TaskTrigger.periodic(Duration(days: 1)),
     829              :   ///   worker: ...,
     830              :   ///   tag: 'photo-feature',
     831              :   /// );
     832              :   ///
     833              :   /// // User disables photo feature - cancel all related tasks
     834              :   /// await NativeWorkManager.cancelByTag(tag: 'photo-feature');
     835              :   /// ```
     836              :   ///
     837              :   /// ## Parameters
     838              :   ///
     839              :   /// **[tag]** *(required)* - The tag to match.
     840              :   /// - Must not be empty
     841              :   /// - Case-sensitive exact match
     842              :   /// - Only tasks with exactly this tag will be canceled
     843              :   ///
     844              :   /// ## Behavior
     845              :   ///
     846              :   /// - Cancels ALL tasks that have the specified tag
     847              :   /// - Does nothing if no tasks have the tag (no error thrown)
     848              :   /// - Tasks are canceled immediately (won't execute even if scheduled)
     849              :   /// - Running tasks may complete before cancellation takes effect
     850              :   ///
     851              :   /// ## Platform Notes
     852              :   ///
     853              :   /// **Android:** Uses WorkManager.cancelAllWorkByTag()
     854              :   /// **iOS:** Cancels all BGTaskRequest instances with matching identifier prefix
     855              :   ///
     856              :   /// ## See Also
     857              :   ///
     858              :   /// - [cancel] - Cancel a specific task by ID
     859              :   /// - [cancelAll] - Cancel all scheduled tasks
     860              :   /// - [getTasksByTag] - Query tasks by tag
     861            0 :   static Future<void> cancelByTag({required String tag}) async {
     862            0 :     _checkInitialized();
     863              : 
     864            0 :     if (tag.isEmpty) {
     865            0 :       throw ArgumentError('tag cannot be empty');
     866              :     }
     867              : 
     868            0 :     return NativeWorkManagerPlatform.instance.cancelByTag(tag: tag);
     869              :   }
     870              : 
     871              :   /// Get all tasks with a specific tag.
     872              :   ///
     873              :   /// Returns a list of task IDs that have the given tag.
     874              :   ///
     875              :   /// ```dart
     876              :   /// List<String> syncTasks = await NativeWorkManager.getTasksByTag(tag: 'sync-group');
     877              :   /// print('Found ${syncTasks.length} sync tasks');
     878              :   /// ```
     879            0 :   static Future<List<String>> getTasksByTag({required String tag}) async {
     880            0 :     _checkInitialized();
     881              : 
     882            0 :     if (tag.isEmpty) {
     883            0 :       throw ArgumentError('tag cannot be empty');
     884              :     }
     885              : 
     886            0 :     return NativeWorkManagerPlatform.instance.getTasksByTag(tag: tag);
     887              :   }
     888              : 
     889              :   /// Get all tags currently in use.
     890              :   ///
     891              :   /// Returns a list of all unique tags that have been assigned to tasks.
     892              :   ///
     893              :   /// ```dart
     894              :   /// List<String> allTags = await NativeWorkManager.getAllTags();
     895              :   /// print('Active tag groups: $allTags');
     896              :   /// ```
     897            0 :   static Future<List<String>> getAllTags() async {
     898            0 :     _checkInitialized();
     899            0 :     return NativeWorkManagerPlatform.instance.getAllTags();
     900              :   }
     901              : 
     902              :   /// Cancel a specific task by its ID.
     903              :   ///
     904              :   /// Use this to cancel individual tasks. For canceling multiple related tasks,
     905              :   /// consider using [cancelByTag] instead.
     906              :   ///
     907              :   /// ## Example
     908              :   ///
     909              :   /// ```dart
     910              :   /// // Schedule a task
     911              :   /// await NativeWorkManager.enqueue(
     912              :   ///   taskId: 'daily-sync',
     913              :   ///   trigger: TaskTrigger.periodic(Duration(days: 1)),
     914              :   ///   worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
     915              :   /// );
     916              :   ///
     917              :   /// // Later, cancel it
     918              :   /// await NativeWorkManager.cancel(taskId: 'daily-sync');
     919              :   /// ```
     920              :   ///
     921              :   /// ## Parameters
     922              :   ///
     923              :   /// **[taskId]** *(required)* - The ID of the task to cancel.
     924              :   ///
     925              :   /// ## Behavior
     926              :   ///
     927              :   /// - Cancels the task with the specified ID
     928              :   /// - Does nothing if task doesn't exist (no error thrown)
     929              :   /// - Task won't execute after cancellation
     930              :   /// - If task is currently running, may complete before cancellation takes effect
     931              :   ///
     932              :   /// ## See Also
     933              :   ///
     934              :   /// - [cancelByTag] - Cancel multiple tasks by tag
     935              :   /// - [cancelAll] - Cancel all tasks
     936            2 :   static Future<void> cancel({required String taskId}) async {
     937            2 :     _checkInitialized();
     938            0 :     return NativeWorkManagerPlatform.instance.cancel(taskId: taskId);
     939              :   }
     940              : 
     941              :   /// Cancel all scheduled tasks.
     942              :   ///
     943              :   /// **Use with caution!** This cancels every task managed by NativeWorkManager,
     944              :   /// including periodic tasks, delayed tasks, and tasks from other parts of your app.
     945              :   ///
     946              :   /// ## Example - App Logout
     947              :   ///
     948              :   /// ```dart
     949              :   /// // User logs out - cancel all background sync tasks
     950              :   /// await NativeWorkManager.cancelAll();
     951              :   /// ```
     952              :   ///
     953              :   /// ## Example - Reset App State
     954              :   ///
     955              :   /// ```dart
     956              :   /// Future<void> resetApp() async {
     957              :   ///   // Clear all background tasks
     958              :   ///   await NativeWorkManager.cancelAll();
     959              :   ///
     960              :   ///   // Clear local data
     961              :   ///   await clearLocalDatabase();
     962              :   ///
     963              :   ///   // Restart app
     964              :   ///   await restartApp();
     965              :   /// }
     966              :   /// ```
     967              :   ///
     968              :   /// ## Behavior
     969              :   ///
     970              :   /// - Cancels ALL tasks regardless of ID, tag, or status
     971              :   /// - Does not throw error if no tasks exist
     972              :   /// - Running tasks may complete before cancellation takes effect
     973              :   /// - This is a destructive operation - tasks cannot be recovered
     974              :   ///
     975              :   /// ## Alternatives
     976              :   ///
     977              :   /// Consider these alternatives for more granular control:
     978              :   /// - Use [cancelByTag] to cancel groups of related tasks
     979              :   /// - Use [cancel] to cancel specific tasks
     980              :   ///
     981              :   /// ## Platform Notes
     982              :   ///
     983              :   /// **Android:** Calls WorkManager.cancelAllWork()
     984              :   /// **iOS:** Cancels all pending BGTaskRequest instances
     985              :   ///
     986              :   /// ## See Also
     987              :   ///
     988              :   /// - [cancel] - Cancel specific task
     989              :   /// - [cancelByTag] - Cancel tasks by tag
     990            1 :   static Future<void> cancelAll() async {
     991            1 :     _checkInitialized();
     992            0 :     return NativeWorkManagerPlatform.instance.cancelAll();
     993              :   }
     994              : 
     995              :   /// Report progress from inside a [DartWorker] callback.
     996              :   ///
     997              :   /// Call this from your [DartWorkerCallback] to emit progress events that
     998              :   /// will appear in [NativeWorkManager.progress] on the UI side.
     999              :   ///
    1000              :   /// The [taskId] is injected into the callback input map under the key
    1001              :   /// `'__taskId'`. Read it and forward to this method:
    1002              :   ///
    1003              :   /// ```dart
    1004              :   /// await NativeWorkManager.initialize(
    1005              :   ///   dartWorkers: {
    1006              :   ///     'processFiles': (input) async {
    1007              :   ///       final taskId = input?['__taskId'] as String?;
    1008              :   ///       for (var i = 1; i <= 10; i++) {
    1009              :   ///         await processFile(i);
    1010              :   ///         await NativeWorkManager.reportDartWorkerProgress(
    1011              :   ///           taskId: taskId,
    1012              :   ///           progress: (i / 10 * 100).round(),
    1013              :   ///           message: 'Processing file $i / 10',
    1014              :   ///         );
    1015              :   ///       }
    1016              :   ///       return true;
    1017              :   ///     },
    1018              :   ///   },
    1019              :   /// );
    1020              :   /// ```
    1021              :   ///
    1022              :   /// **Thread safety:** Safe to call from any isolate that has access to the
    1023              :   /// `dev.brewkits/dart_worker_channel` MethodChannel (i.e., the background
    1024              :   /// isolate spawned by DartWorker execution).
    1025              :   ///
    1026              :   /// Does nothing if [taskId] is null or empty (avoids need for null checks in
    1027              :   /// callers that cannot guarantee a taskId is present).
    1028            1 :   static Future<void> reportDartWorkerProgress({
    1029              :     String? taskId,
    1030              :     required int progress,
    1031              :     String? message,
    1032              :   }) async {
    1033            1 :     if (taskId == null || taskId.isEmpty) return;
    1034              :     const channel = MethodChannel('dev.brewkits/dart_worker_channel');
    1035            0 :     await channel.invokeMethod<void>('reportProgress', <String, Object?>{
    1036            0 :       'taskId': taskId,
    1037            0 :       'progress': progress.clamp(0, 100),
    1038            0 :       if (message != null) 'message': message,
    1039              :     });
    1040              :   }
    1041              : 
    1042              :   /// Get the current status of a task.
    1043              :   ///
    1044              :   /// Query the execution state of a specific task. Useful for showing
    1045              :   /// task progress in your UI or debugging task execution.
    1046              :   ///
    1047              :   /// ## Example - Show Upload Status
    1048              :   ///
    1049              :   /// ```dart
    1050              :   /// final status = await NativeWorkManager.getTaskStatus(taskId: 'photo-upload');
    1051              :   ///
    1052              :   /// switch (status) {
    1053              :   ///   case TaskStatus.enqueued:
    1054              :   ///     print('Upload is waiting to start');
    1055              :   ///     break;
    1056              :   ///   case TaskStatus.running:
    1057              :   ///     print('Upload in progress...');
    1058              :   ///     break;
    1059              :   ///   case TaskStatus.succeeded:
    1060              :   ///     print('Upload complete!');
    1061              :   ///     break;
    1062              :   ///   case TaskStatus.failed:
    1063              :   ///     print('Upload failed');
    1064              :   ///     break;
    1065              :   ///   case TaskStatus.cancelled:
    1066              :   ///     print('Upload was canceled');
    1067              :   ///     break;
    1068              :   ///   case null:
    1069              :   ///     print('Task not found');
    1070              :   ///     break;
    1071              :   /// }
    1072              :   /// ```
    1073              :   ///
    1074              :   /// ## Example - UI Integration
    1075              :   ///
    1076              :   /// ```dart
    1077              :   /// class UploadStatusWidget extends StatefulWidget {
    1078              :   ///   @override
    1079              :   ///   State createState() => _UploadStatusWidgetState();
    1080              :   /// }
    1081              :   ///
    1082              :   /// class _UploadStatusWidgetState extends State<UploadStatusWidget> {
    1083              :   ///   TaskStatus? _status;
    1084              :   ///
    1085              :   ///   @override
    1086              :   ///   void initState() {
    1087              :   ///     super.initState();
    1088              :   ///     _checkStatus();
    1089              :   ///     Timer.periodic(Duration(seconds: 1), (_) => _checkStatus());
    1090              :   ///   }
    1091              :   ///
    1092              :   ///   Future<void> _checkStatus() async {
    1093              :   ///     final status = await NativeWorkManager.getTaskStatus(taskId: 'upload');
    1094              :   ///     setState(() => _status = status);
    1095              :   ///   }
    1096              :   ///
    1097              :   ///   @override
    1098              :   ///   Widget build(BuildContext context) {
    1099              :   ///     if (_status == TaskStatus.running) {
    1100              :   ///       return CircularProgressIndicator();
    1101              :   ///     }
    1102              :   ///     return Text('Status: ${_status ?? 'Not found'}');
    1103              :   ///   }
    1104              :   /// }
    1105              :   /// ```
    1106              :   ///
    1107              :   /// ## Parameters
    1108              :   ///
    1109              :   /// **[taskId]** *(required)* - The ID of the task to query.
    1110              :   ///
    1111              :   /// ## Returns
    1112              :   ///
    1113              :   /// A [TaskStatus] enum value, or `null` if the task doesn't exist:
    1114              :   /// - `TaskStatus.enqueued` - Task is scheduled but not yet started
    1115              :   /// - `TaskStatus.running` - Task is currently executing
    1116              :   /// - `TaskStatus.succeeded` - Task completed successfully
    1117              :   /// - `TaskStatus.failed` - Task failed with an error
    1118              :   /// - `TaskStatus.cancelled` - Task was canceled
    1119              :   /// - `null` - Task not found (may have been canceled or completed long ago)
    1120              :   ///
    1121              :   /// ## Platform Considerations
    1122              :   ///
    1123              :   /// **Android:**
    1124              :   /// - Status reflects WorkManager's WorkInfo state
    1125              :   /// - Completed tasks remain queryable for some time
    1126              :   ///
    1127              :   /// **iOS:**
    1128              :   /// - Status may not be available for completed/failed tasks
    1129              :   /// - Returns null if task is not in pending queue
    1130              :   ///
    1131              :   /// ## Common Pitfalls
    1132              :   ///
    1133              :   /// ❌ **Don't** poll this method too frequently (impacts performance)
    1134              :   /// ❌ **Don't** rely on this for completed tasks (may return null)
    1135              :   /// ✅ **Do** use [events] stream for real-time task completion notifications
    1136              :   /// ✅ **Do** cache status locally if polling frequently
    1137              :   ///
    1138              :   /// ## See Also
    1139              :   ///
    1140              :   /// - [events] - Stream of task completion events
    1141              :   /// - [progress] - Stream of task progress updates
    1142              :   /// Get task status.
    1143            0 :   static Future<TaskStatus?> getTaskStatus({required String taskId}) async {
    1144            0 :     _checkInitialized();
    1145            0 :     return NativeWorkManagerPlatform.instance.getTaskStatus(taskId: taskId);
    1146              :   }
    1147              : 
    1148              :   /// Get detailed task record.
    1149            0 :   static Future<TaskRecord?> getTaskRecord({required String taskId}) async {
    1150            0 :     _checkInitialized();
    1151            0 :     return NativeWorkManagerPlatform.instance.getTaskRecord(taskId: taskId);
    1152              :   }
    1153              : 
    1154              :   // ═══════════════════════════════════════════════════════════════════════════
    1155              :   // PAUSE / RESUME
    1156              :   // ═══════════════════════════════════════════════════════════════════════════
    1157              : 
    1158              :   /// Pause a running download task.
    1159              :   ///
    1160              :   /// On **Android**, pausing cancels the WorkManager job but preserves the
    1161              :   /// partial `.tmp` file so that a subsequent [resume] call can re-enqueue
    1162              :   /// the worker and OkHttp will send a `Range:` header to resume from the
    1163              :   /// last downloaded byte. The task is stored in the SQLite task store with
    1164              :   /// status `"paused"`.
    1165              :   ///
    1166              :   /// On **iOS**, the underlying `URLSessionDownloadTask` is cancelled with
    1167              :   /// resume data. If resume data is available, [resume] will call
    1168              :   /// `resumeDownload(with:)` to continue from where it left off. If not
    1169              :   /// (e.g. the download had not yet started), [resume] re-starts from scratch
    1170              :   /// using the config saved in the task store.
    1171              :   ///
    1172              :   /// **Note:** Only `HttpDownloadWorker` tasks support pause/resume. For other
    1173              :   /// worker types the call is a no-op on the native side.
    1174            0 :   static Future<void> pause({required String taskId}) async {
    1175            0 :     _checkInitialized();
    1176            0 :     return NativeWorkManagerPlatform.instance.pauseTask(taskId: taskId);
    1177              :   }
    1178              : 
    1179              :   /// Resume a previously paused download task.
    1180              :   ///
    1181              :   /// The task must have been paused via [pause] before calling this method.
    1182              :   /// See [pause] for platform-specific behaviour.
    1183            0 :   static Future<void> resume({required String taskId}) async {
    1184            0 :     _checkInitialized();
    1185            0 :     return NativeWorkManagerPlatform.instance.resumeTask(taskId: taskId);
    1186              :   }
    1187              : 
    1188              :   /// Pause all running tasks that share a given tag.
    1189              :   ///
    1190              :   /// Looks up every task with [tag] via [getTasksByTag] then calls [pause]
    1191              :   /// on each one concurrently.  Completed, failed, or already-paused tasks
    1192              :   /// are silently skipped on the native side.
    1193              :   ///
    1194              :   /// ```dart
    1195              :   /// // Pause all active downloads in the "media" group
    1196              :   /// await NativeWorkManager.pauseByTag('media');
    1197              :   /// ```
    1198            0 :   static Future<void> pauseByTag({required String tag}) async {
    1199            0 :     _checkInitialized();
    1200              :     final taskIds =
    1201            0 :         await NativeWorkManagerPlatform.instance.getTasksByTag(tag: tag);
    1202            0 :     await Future.wait(
    1203            0 :       taskIds.map(
    1204            0 :           (id) => NativeWorkManagerPlatform.instance.pauseTask(taskId: id)),
    1205              :     );
    1206              :   }
    1207              : 
    1208              :   /// Resume all paused tasks that share a given tag.
    1209              :   ///
    1210              :   /// Looks up every task with [tag] via [getTasksByTag] then calls [resume]
    1211              :   /// on each one concurrently.  Only tasks in the `paused` state will
    1212              :   /// actually restart; other tasks are silently skipped on the native side.
    1213              :   ///
    1214              :   /// ```dart
    1215              :   /// // Resume all downloads in the "media" group
    1216              :   /// await NativeWorkManager.resumeByTag('media');
    1217              :   /// ```
    1218            0 :   static Future<void> resumeByTag({required String tag}) async {
    1219            0 :     _checkInitialized();
    1220              :     final taskIds =
    1221            0 :         await NativeWorkManagerPlatform.instance.getTasksByTag(tag: tag);
    1222            0 :     await Future.wait(
    1223            0 :       taskIds.map(
    1224            0 :           (id) => NativeWorkManagerPlatform.instance.resumeTask(taskId: id)),
    1225              :     );
    1226              :   }
    1227              : 
    1228              :   // ═══════════════════════════════════════════════════════════════════════════
    1229              :   // TASK STORE
    1230              :   // ═══════════════════════════════════════════════════════════════════════════
    1231              : 
    1232              :   /// Return all tasks from the persistent SQLite task store, newest first.
    1233              :   ///
    1234              :   /// Each [TaskRecord] contains the task ID, tag, current status, worker class
    1235              :   /// name, and timestamps. The store is updated automatically when tasks are
    1236              :   /// enqueued, completed, failed, cancelled, or paused.
    1237              :   ///
    1238              :   /// ```dart
    1239              :   /// final tasks = await NativeWorkManager.allTasks();
    1240              :   /// for (final t in tasks) {
    1241              :   ///   print('${t.taskId}: ${t.status}');
    1242              :   /// }
    1243              :   /// ```
    1244            0 :   static Future<List<TaskRecord>> allTasks() async {
    1245            0 :     _checkInitialized();
    1246            0 :     return NativeWorkManagerPlatform.instance.allTasks();
    1247              :   }
    1248              : 
    1249              :   /// Fetch the server-suggested filename for a URL by sending a HEAD request
    1250              :   /// and parsing the `Content-Disposition` header (RFC 6266).
    1251              :   ///
    1252              :   /// Returns the sanitized filename string, or `null` if the server did not
    1253              :   /// provide a `Content-Disposition: attachment; filename=…` header.
    1254              :   ///
    1255              :   /// Prefer `filename*=UTF-8''…` (RFC 5987 percent-encoded) when present.
    1256              :   ///
    1257              :   /// ```dart
    1258              :   /// final name = await NativeWorkManager.getServerFilename(
    1259              :   ///   'https://files.example.com/report.pdf',
    1260              :   /// );
    1261              :   /// // name == 'Q4_Report_2024.pdf'
    1262              :   ///
    1263              :   /// await NativeWorkManager.enqueue(
    1264              :   ///   taskId: 'dl-report',
    1265              :   ///   trigger: const TaskTrigger.oneTime(),
    1266              :   ///   worker: NativeWorker.httpDownload(
    1267              :   ///     url: 'https://files.example.com/report.pdf',
    1268              :   ///     savePath: '/path/to/downloads/$name',
    1269              :   ///   ),
    1270              :   /// );
    1271              :   /// ```
    1272            1 :   static Future<String?> getServerFilename(
    1273              :     String url, {
    1274              :     Map<String, String>? headers,
    1275              :     int timeoutMs = 30000,
    1276              :   }) {
    1277            1 :     _checkInitialized();
    1278            0 :     return NativeWorkManagerPlatform.instance.getServerFilename(
    1279              :       url: url,
    1280              :       headers: headers,
    1281              :       timeoutMs: timeoutMs,
    1282              :     );
    1283              :   }
    1284              : 
    1285              :   /// Get the current progress of all running tasks.
    1286              :   ///
    1287              :   /// Useful for restoring UI state when the app restarts, allowing you to
    1288              :   /// "re-attach" to tasks that are still executing in the background.
    1289              :   ///
    1290              :   /// Returns a map of task IDs to their most recent [TaskProgress] update.
    1291            0 :   static Future<Map<String, TaskProgress>> getRunningProgress() async {
    1292            0 :     _checkInitialized();
    1293            0 :     final raw = await NativeWorkManagerPlatform.instance.getRunningProgress();
    1294            0 :     return raw.map((k, v) =>
    1295            0 :         MapEntry(k, TaskProgress.fromMap(Map<String, dynamic>.from(v))));
    1296              :   }
    1297              : 
    1298              :   /// Open a file with the system default application.
    1299              :   ///
    1300              :   /// Launches the OS file viewer for the given [path]. Useful after a download
    1301              :   /// completes to let the user immediately view/open the downloaded file.
    1302              :   ///
    1303              :   /// On **Android**, opens via `Intent.ACTION_VIEW` with a `FileProvider` URI.
    1304              :   /// Requires the `native_workmanager.provider` FileProvider to be declared in
    1305              :   /// the app's `AndroidManifest.xml` (added automatically by the plugin).
    1306              :   ///
    1307              :   /// On **iOS**, presents a `UIDocumentInteractionController`.
    1308              :   ///
    1309              :   /// ```dart
    1310              :   /// // After download completes:
    1311              :   /// NativeWorkManager.events.listen((event) async {
    1312              :   ///   if (event.taskId == 'my-download' && event.success) {
    1313              :   ///     await NativeWorkManager.openFile('/path/to/downloaded.pdf');
    1314              :   ///   }
    1315              :   /// });
    1316              :   /// ```
    1317            1 :   static Future<void> openFile(String path, {String? mimeType}) {
    1318            1 :     _checkInitialized();
    1319            0 :     return NativeWorkManagerPlatform.instance
    1320            0 :         .openFile(path, mimeType: mimeType);
    1321              :   }
    1322              : 
    1323              :   /// Set the maximum number of concurrent downloads per host.
    1324              :   ///
    1325              :   /// Limits how many simultaneous downloads can target the same server.
    1326              :   /// Default is 2. Call after [initialize].
    1327              :   ///
    1328              :   /// ```dart
    1329              :   /// await NativeWorkManager.setMaxConcurrentPerHost(3);
    1330              :   /// ```
    1331            1 :   static Future<void> setMaxConcurrentPerHost(int max) {
    1332            1 :     _checkInitialized();
    1333            0 :     return NativeWorkManagerPlatform.instance.setMaxConcurrentPerHost(max);
    1334              :   }
    1335              : 
    1336              :   /// Return all tasks that match a given [status].
    1337              :   ///
    1338              :   /// Queries the persistent task store and filters by status.  Useful for
    1339              :   /// building download managers, queue dashboards, or retry logic.
    1340              :   ///
    1341              :   /// ```dart
    1342              :   /// final running = await NativeWorkManager.getTasksByStatus(TaskStatus.running);
    1343              :   /// final paused  = await NativeWorkManager.getTasksByStatus(TaskStatus.paused);
    1344              :   /// ```
    1345            0 :   static Future<List<TaskRecord>> getTasksByStatus(TaskStatus status) async {
    1346            0 :     _checkInitialized();
    1347            0 :     final all = await NativeWorkManagerPlatform.instance.allTasks();
    1348            0 :     return all.where((t) => t.status == status.name).toList();
    1349              :   }
    1350              : 
    1351              :   /// Pause all currently-running tasks.
    1352              :   ///
    1353              :   /// Queries the task store for every task in the `running` state, then
    1354              :   /// calls [pause] on each one concurrently.  Tasks that cannot be paused
    1355              :   /// (e.g. non-download workers) are silently skipped on the native side.
    1356              :   ///
    1357              :   /// ```dart
    1358              :   /// // Pause everything — e.g. when the user taps "Pause All"
    1359              :   /// await NativeWorkManager.pauseAll();
    1360              :   /// ```
    1361            0 :   static Future<void> pauseAll() async {
    1362            0 :     _checkInitialized();
    1363            0 :     final running = await getTasksByStatus(TaskStatus.running);
    1364            0 :     await Future.wait(
    1365            0 :       running.map((t) =>
    1366            0 :           NativeWorkManagerPlatform.instance.pauseTask(taskId: t.taskId)),
    1367              :     );
    1368              :   }
    1369              : 
    1370              :   /// Resume all currently-paused tasks.
    1371              :   ///
    1372              :   /// Queries the task store for every task in the `paused` state, then
    1373              :   /// calls [resume] on each one concurrently.
    1374              :   ///
    1375              :   /// ```dart
    1376              :   /// // Resume everything — e.g. when network becomes available again
    1377              :   /// await NativeWorkManager.resumeAll();
    1378              :   /// ```
    1379            0 :   static Future<void> resumeAll() async {
    1380            0 :     _checkInitialized();
    1381            0 :     final paused = await getTasksByStatus(TaskStatus.paused);
    1382            0 :     await Future.wait(
    1383            0 :       paused.map((t) =>
    1384            0 :           NativeWorkManagerPlatform.instance.resumeTask(taskId: t.taskId)),
    1385              :     );
    1386              :   }
    1387              : 
    1388              :   /// Enqueue multiple tasks at once and return their [ScheduleResult]s.
    1389              :   ///
    1390              :   /// Each entry in [requests] maps to one [enqueue] call.  All tasks are
    1391              :   /// submitted concurrently; the returned list preserves the input order.
    1392              :   ///
    1393              :   /// ```dart
    1394              :   /// final results = await NativeWorkManager.enqueueAll([
    1395              :   ///   EnqueueRequest(
    1396              :   ///     taskId: 'dl-1',
    1397              :   ///     trigger: TaskTrigger.oneTime(),
    1398              :   ///     worker: NativeWorker.httpDownload(url: urls[0], savePath: paths[0]),
    1399              :   ///     tag: 'batch-download',
    1400              :   ///   ),
    1401              :   ///   EnqueueRequest(
    1402              :   ///     taskId: 'dl-2',
    1403              :   ///     trigger: TaskTrigger.oneTime(),
    1404              :   ///     worker: NativeWorker.httpDownload(url: urls[1], savePath: paths[1]),
    1405              :   ///     tag: 'batch-download',
    1406              :   ///   ),
    1407              :   /// ]);
    1408              :   ///   final accepted = results.where((r) => r.scheduleResult == ScheduleResult.accepted).length;
    1409              :   ///   print('$accepted / ${results.length} tasks accepted');
    1410              :   /// ```
    1411            0 :   static Future<List<TaskHandler>> enqueueAll(
    1412              :     List<EnqueueRequest> requests,
    1413              :   ) async {
    1414            0 :     _checkInitialized();
    1415            0 :     return Future.wait(
    1416            0 :       requests.map(
    1417            0 :         (r) => enqueue(
    1418            0 :           taskId: r.taskId,
    1419            0 :           trigger: r.trigger,
    1420            0 :           worker: r.worker,
    1421            0 :           constraints: r.constraints,
    1422            0 :           existingPolicy: r.existingPolicy,
    1423            0 :           tag: r.tag,
    1424              :         ),
    1425              :       ),
    1426              :     );
    1427              :   }
    1428              : 
    1429              :   // ═══════════════════════════════════════════════════════════════════════════
    1430              :   // TASK CHAINS
    1431              :   // ═══════════════════════════════════════════════════════════════════════════
    1432              : 
    1433              :   /// Start building a task chain with a single initial task.
    1434              :   ///
    1435              :   /// Task chains allow you to define complex multi-step workflows where tasks
    1436              :   /// execute in sequence or parallel. This is the foundation for building
    1437              :   /// sophisticated background processing pipelines.
    1438              :   ///
    1439              :   /// ## Basic Sequential Chain (A → B → C)
    1440              :   ///
    1441              :   /// ```dart
    1442              :   /// await NativeWorkManager.beginWith(
    1443              :   ///   TaskRequest(
    1444              :   ///     id: 'download',
    1445              :   ///     worker: NativeWorker.httpDownload(
    1446              :   ///       url: 'https://example.com/video.mp4',
    1447              :   ///       savePath: '/tmp/video.mp4',
    1448              :   ///     ),
    1449              :   ///   ),
    1450              :   /// )
    1451              :   /// .then(TaskRequest(
    1452              :   ///   id: 'process',
    1453              :   ///   worker: DartWorker(
    1454              :   ///     callbackId: 'processVideo',
    1455              :   ///     input: {'path': '/tmp/video.mp4'},
    1456              :   ///   ),
    1457              :   /// ))
    1458              :   /// .then(TaskRequest(
    1459              :   ///   id: 'upload',
    1460              :   ///   worker: NativeWorker.httpUpload(
    1461              :   ///     url: 'https://api.example.com/videos',
    1462              :   ///     filePath: '/tmp/processed_video.mp4',
    1463              :   ///   ),
    1464              :   /// ))
    1465              :   /// .enqueue();
    1466              :   /// ```
    1467              :   ///
    1468              :   /// ## Parallel Tasks (A → \[B1, B2, B3\])
    1469              :   ///
    1470              :   /// ```dart
    1471              :   /// await NativeWorkManager.beginWith(
    1472              :   ///   TaskRequest(
    1473              :   ///     id: 'prepare-data',
    1474              :   ///     worker: DartWorker(callbackId: 'prepareData'),
    1475              :   ///   ),
    1476              :   /// )
    1477              :   /// .then([
    1478              :   ///   // These 3 tasks run in parallel
    1479              :   ///   TaskRequest(
    1480              :   ///     id: 'upload-server1',
    1481              :   ///     worker: NativeWorker.httpUpload(
    1482              :   ///       url: 'https://server1.example.com/upload',
    1483              :   ///       filePath: '/data/file.zip',
    1484              :   ///     ),
    1485              :   ///   ),
    1486              :   ///   TaskRequest(
    1487              :   ///     id: 'upload-server2',
    1488              :   ///     worker: NativeWorker.httpUpload(
    1489              :   ///       url: 'https://server2.example.com/upload',
    1490              :   ///       filePath: '/data/file.zip',
    1491              :   ///     ),
    1492              :   ///   ),
    1493              :   ///   TaskRequest(
    1494              :   ///     id: 'upload-backup',
    1495              :   ///     worker: NativeWorker.httpUpload(
    1496              :   ///       url: 'https://backup.example.com/upload',
    1497              :   ///       filePath: '/data/file.zip',
    1498              :   ///     ),
    1499              :   ///   ),
    1500              :   /// ])
    1501              :   /// .enqueue();
    1502              :   /// ```
    1503              :   ///
    1504              :   /// ## Complex Workflow with Constraints
    1505              :   ///
    1506              :   /// ```dart
    1507              :   /// await NativeWorkManager.beginWith(
    1508              :   ///   TaskRequest(
    1509              :   ///     id: 'fetch-metadata',
    1510              :   ///     worker: NativeWorker.httpRequest(
    1511              :   ///       url: 'https://api.example.com/metadata',
    1512              :   ///       method: HttpMethod.get,
    1513              :   ///     ),
    1514              :   ///   ),
    1515              :   /// )
    1516              :   /// .then(TaskRequest(
    1517              :   ///   id: 'download-file',
    1518              :   ///   worker: NativeWorker.httpDownload(
    1519              :   ///     url: 'https://cdn.example.com/large-file.zip',
    1520              :   ///     savePath: '/downloads/file.zip',
    1521              :   ///   ),
    1522              :   /// ))
    1523              :   /// .then(TaskRequest(
    1524              :   ///   id: 'extract-and-process',
    1525              :   ///   worker: DartWorker(callbackId: 'extractZip'),
    1526              :   /// ))
    1527              :   /// .named('data-sync-pipeline')
    1528              :   /// .withConstraints(Constraints.heavyTask)  // Requires charging + WiFi
    1529              :   /// .enqueue();
    1530              :   /// ```
    1531              :   ///
    1532              :   /// ## Parameters
    1533              :   ///
    1534              :   /// **[task]** *(required)* - The first task in the chain.
    1535              :   /// - Must be a [TaskRequest]
    1536              :   /// - This task executes before all other tasks in the chain
    1537              :   ///
    1538              :   /// ## Returns
    1539              :   ///
    1540              :   /// A [TaskChainBuilder] that you can use to:
    1541              :   /// - Add more tasks with `.then()`
    1542              :   /// - Set constraints with `.withConstraints()`
    1543              :   /// - Name the chain with `.named()`
    1544              :   /// - Execute the chain with `.enqueue()`
    1545              :   ///
    1546              :   /// ## Chain Execution Rules
    1547              :   ///
    1548              :   /// - **Sequential tasks:** Execute one after another (A → B → C)
    1549              :   /// - **Parallel tasks:** All start together (`[A, B, C]` → D)
    1550              :   /// - **Failure handling:** If any task fails, the entire chain stops
    1551              :   /// - **Constraints:** Applied to the entire chain, not individual tasks
    1552              :   ///
    1553              :   /// ## Common Use Cases
    1554              :   ///
    1555              :   /// 1. **Download → Process → Upload:** Fetch data, transform it, upload result
    1556              :   /// 2. **Fetch → Parallel Uploads:** Get data once, upload to multiple servers
    1557              :   /// 3. **Multi-step Data Sync:** Fetch metadata, download files, process locally
    1558              :   /// 4. **Backup Pipeline:** Compress files, encrypt, upload to multiple cloud storage
    1559              :   ///
    1560              :   /// ## Platform Considerations
    1561              :   ///
    1562              :   /// **Android:**
    1563              :   /// - Uses WorkManager's chain API
    1564              :   /// - Each task in chain is a separate Work item
    1565              :   /// - Failure of one task cancels remaining tasks
    1566              :   ///
    1567              :   /// **iOS:**
    1568              :   /// - Simulated via sequential BGTaskRequest scheduling
    1569              :   /// - Chain state tracked internally
    1570              :   /// - More reliable on iOS 15+
    1571              :   ///
    1572              :   /// ## Common Pitfalls
    1573              :   ///
    1574              :   /// ❌ **Don't** make chains too long (increases failure risk)
    1575              :   /// ❌ **Don't** use chains for independent tasks (just schedule separately)
    1576              :   /// ❌ **Don't** rely on exact timing - chains may be delayed by OS
    1577              :   /// ✅ **Do** handle failures gracefully
    1578              :   /// ✅ **Do** keep chains focused on related tasks
    1579              :   /// ✅ **Do** use constraints to ensure suitable execution conditions
    1580              :   ///
    1581              :   /// ## See Also
    1582              :   ///
    1583              :   /// - [beginWithAll] - Start chain with multiple parallel initial tasks
    1584              :   /// - [TaskRequest] - Configuration for individual tasks in chain
    1585              :   /// - [TaskChainBuilder] - Builder for constructing task chains
    1586            0 :   static TaskChainBuilder beginWith(TaskRequest task) {
    1587            0 :     _checkInitialized();
    1588            0 :     return TaskChainBuilder.internal([task]);
    1589              :   }
    1590              : 
    1591              :   /// Start building a task chain with multiple parallel initial tasks.
    1592              :   ///
    1593              :   /// Use this when you want multiple tasks to start simultaneously at the
    1594              :   /// beginning of a chain, then continue with sequential or parallel steps.
    1595              :   ///
    1596              :   /// ## Example - Parallel Download then Process
    1597              :   ///
    1598              :   /// ```dart
    1599              :   /// await NativeWorkManager.beginWithAll([
    1600              :   ///   // These 3 downloads run in parallel
    1601              :   ///   TaskRequest(
    1602              :   ///     id: 'download-file1',
    1603              :   ///     worker: NativeWorker.httpDownload(
    1604              :   ///       url: 'https://cdn.example.com/file1.zip',
    1605              :   ///       savePath: '/tmp/file1.zip',
    1606              :   ///     ),
    1607              :   ///   ),
    1608              :   ///   TaskRequest(
    1609              :   ///     id: 'download-file2',
    1610              :   ///     worker: NativeWorker.httpDownload(
    1611              :   ///       url: 'https://cdn.example.com/file2.zip',
    1612              :   ///       savePath: '/tmp/file2.zip',
    1613              :   ///     ),
    1614              :   ///   ),
    1615              :   ///   TaskRequest(
    1616              :   ///     id: 'download-file3',
    1617              :   ///     worker: NativeWorker.httpDownload(
    1618              :   ///       url: 'https://cdn.example.com/file3.zip',
    1619              :   ///       savePath: '/tmp/file3.zip',
    1620              :   ///     ),
    1621              :   ///   ),
    1622              :   /// ])
    1623              :   /// .then(TaskRequest(
    1624              :   ///   // After ALL downloads complete, process them
    1625              :   ///   id: 'merge-files',
    1626              :   ///   worker: DartWorker(callbackId: 'mergeDownloads'),
    1627              :   /// ))
    1628              :   /// .enqueue();
    1629              :   /// ```
    1630              :   ///
    1631              :   /// ## Example - Multi-Source Data Fetch
    1632              :   ///
    1633              :   /// ```dart
    1634              :   /// await NativeWorkManager.beginWithAll([
    1635              :   ///   TaskRequest(
    1636              :   ///     id: 'fetch-api1',
    1637              :   ///     worker: NativeWorker.httpRequest(
    1638              :   ///       url: 'https://api1.example.com/data',
    1639              :   ///       method: HttpMethod.get,
    1640              :   ///     ),
    1641              :   ///   ),
    1642              :   ///   TaskRequest(
    1643              :   ///     id: 'fetch-api2',
    1644              :   ///     worker: NativeWorker.httpRequest(
    1645              :   ///       url: 'https://api2.example.com/data',
    1646              :   ///       method: HttpMethod.get,
    1647              :   ///     ),
    1648              :   ///   ),
    1649              :   ///   TaskRequest(
    1650              :   ///     id: 'fetch-api3',
    1651              :   ///     worker: NativeWorker.httpRequest(
    1652              :   ///       url: 'https://api3.example.com/data',
    1653              :   ///       method: HttpMethod.get,
    1654              :   ///     ),
    1655              :   ///   ),
    1656              :   /// ])
    1657              :   /// .then(TaskRequest(
    1658              :   ///   id: 'aggregate',
    1659              :   ///   worker: DartWorker(callbackId: 'aggregateData'),
    1660              :   /// ))
    1661              :   /// .then(TaskRequest(
    1662              :   ///   id: 'upload-results',
    1663              :   ///   worker: NativeWorker.httpUpload(
    1664              :   ///     url: 'https://api.example.com/results',
    1665              :   ///     filePath: '/tmp/aggregated.json',
    1666              :   ///   ),
    1667              :   /// ))
    1668              :   /// .enqueue();
    1669              :   /// ```
    1670              :   ///
    1671              :   /// ## Parameters
    1672              :   ///
    1673              :   /// **[tasks]** *(required)* - List of tasks to execute in parallel.
    1674              :   /// - Must not be empty
    1675              :   /// - All tasks start simultaneously
    1676              :   /// - Next step waits for ALL to complete
    1677              :   ///
    1678              :   /// ## Behavior
    1679              :   ///
    1680              :   /// - All initial tasks start at the same time
    1681              :   /// - Next task in chain waits for ALL initial tasks to complete
    1682              :   /// - If ANY initial task fails, entire chain stops
    1683              :   ///
    1684              :   /// ## When to Use
    1685              :   ///
    1686              :   /// ✅ **Use beginWithAll when:**
    1687              :   /// - You need to fetch from multiple sources before processing
    1688              :   /// - You want to maximize parallelism from the start
    1689              :   /// - Initial tasks are independent but results must be combined
    1690              :   ///
    1691              :   /// ❌ **Don't use beginWithAll when:**
    1692              :   /// - Tasks depend on each other - use [beginWith] with `.then()` instead
    1693              :   /// - You just need parallel tasks with no follow-up - schedule separately
    1694              :   ///
    1695              :   /// ## See Also
    1696              :   ///
    1697              :   /// - [beginWith] - Start chain with single initial task
    1698              :   /// - [TaskChainBuilder] - Builder for constructing task chains
    1699            0 :   static TaskChainBuilder beginWithAll(List<TaskRequest> tasks) {
    1700            0 :     _checkInitialized();
    1701            0 :     if (tasks.isEmpty) {
    1702            0 :       throw ArgumentError('Tasks list cannot be empty');
    1703              :     }
    1704            0 :     return TaskChainBuilder.internal(tasks);
    1705              :   }
    1706              : 
    1707            0 :   static Future<ScheduleResult> _enqueueChain(TaskChainBuilder chain) {
    1708              :     // Convert DartWorker to DartWorkerInternal for all tasks in the chain
    1709            0 :     final convertedSteps = chain.steps.map((step) {
    1710            0 :       return step.map((task) {
    1711            0 :         final worker = task.worker;
    1712              : 
    1713              :         // Check if worker is DartWorker and needs conversion
    1714            0 :         if (worker is DartWorker) {
    1715              :           // Get the callback handle for this worker
    1716            0 :           final callbackHandle = _callbackHandles[worker.callbackId];
    1717              :           if (callbackHandle == null) {
    1718            0 :             throw StateError(
    1719            0 :               'INTERNAL ERROR: Callback handle not found for "${worker.callbackId}". '
    1720              :               'This should never happen. Please report this bug.',
    1721              :             );
    1722              :           }
    1723              : 
    1724              :           // Convert DartWorker to DartWorkerInternal
    1725            0 :           final convertedWorker = DartWorkerInternal(
    1726            0 :             callbackId: worker.callbackId,
    1727              :             callbackHandle: callbackHandle,
    1728            0 :             input: worker.input,
    1729            0 :             autoDispose: worker.autoDispose,
    1730            0 :             timeoutMs: worker.timeoutMs,
    1731              :           );
    1732              : 
    1733              :           // Return modified task map with converted worker
    1734            0 :           return {
    1735            0 :             'id': task.id,
    1736            0 :             'workerClassName': convertedWorker.workerClassName,
    1737            0 :             'workerConfig': convertedWorker.toMap(),
    1738            0 :             'constraints': task.constraints.toMap(),
    1739              :           };
    1740              :         }
    1741              : 
    1742              :         // For non-DartWorker tasks, use original toMap()
    1743            0 :         return task.toMap();
    1744            0 :       }).toList();
    1745            0 :     }).toList();
    1746              : 
    1747              :     // Build the chain map with converted steps
    1748            0 :     final chainMap = {
    1749            0 :       'name': chain.name,
    1750            0 :       'constraints': chain.constraints.toMap(),
    1751              :       'steps': convertedSteps,
    1752              :     };
    1753              : 
    1754            0 :     return NativeWorkManagerPlatform.instance.enqueueChain(chainMap);
    1755              :   }
    1756              : 
    1757              :   // ═══════════════════════════════════════════════════════════════════════════
    1758              :   // EVENTS & OBSERVABILITY
    1759              :   // ═══════════════════════════════════════════════════════════════════════════
    1760              : 
    1761              :   /// Stream of task completion events.
    1762              :   ///
    1763              :   /// **Recommended:** Use this stream to reactively respond to task completions
    1764              :   /// instead of polling [getTaskStatus].
    1765              :   ///
    1766              :   /// ## Example - Show Notification on Completion
    1767              :   ///
    1768              :   /// ```dart
    1769              :   /// @override
    1770              :   /// void initState() {
    1771              :   ///   super.initState();
    1772              :   ///
    1773              :   ///   // Listen to task events
    1774              :   ///   NativeWorkManager.events.listen((event) {
    1775              :   ///     if (event.success) {
    1776              :   ///       showNotification(
    1777              :   ///         title: 'Task Complete',
    1778              :   ///         body: 'Task ${event.taskId} finished successfully',
    1779              :   ///       );
    1780              :   ///     } else {
    1781              :   ///       showError(
    1782              :   ///         title: 'Task Failed',
    1783              :   ///         body: event.message ?? 'Unknown error',
    1784              :   ///       );
    1785              :   ///     }
    1786              :   ///   });
    1787              :   /// }
    1788              :   /// ```
    1789              :   ///
    1790              :   /// ## Example - Update UI on Upload Complete
    1791              :   ///
    1792              :   /// ```dart
    1793              :   /// class UploadManager {
    1794              :   ///   StreamSubscription? _eventSub;
    1795              :   ///
    1796              :   ///   void startListening() {
    1797              :   ///     _eventSub = NativeWorkManager.events.listen((event) {
    1798              :   ///       if (event.taskId.startsWith('upload-')) {
    1799              :   ///         if (event.success) {
    1800              :   ///           markUploadComplete(event.taskId);
    1801              :   ///           refreshUI();
    1802              :   ///         } else {
    1803              :   ///           showRetryDialog(event.taskId);
    1804              :   ///         }
    1805              :   ///       }
    1806              :   ///     });
    1807              :   ///   }
    1808              :   ///
    1809              :   ///   void dispose() {
    1810              :   ///     _eventSub?.cancel();
    1811              :   ///   }
    1812              :   /// }
    1813              :   /// ```
    1814              :   ///
    1815              :   /// ## Example - Collect Statistics
    1816              :   ///
    1817              :   /// ```dart
    1818              :   /// int successCount = 0;
    1819              :   /// int failureCount = 0;
    1820              :   ///
    1821              :   /// NativeWorkManager.events.listen((event) {
    1822              :   ///   if (event.success) {
    1823              :   ///     successCount++;
    1824              :   ///   } else {
    1825              :   ///     failureCount++;
    1826              :   ///   }
    1827              :   ///   print('Success: $successCount, Failed: $failureCount');
    1828              :   /// });
    1829              :   /// ```
    1830              :   ///
    1831              :   /// ## Event Properties
    1832              :   ///
    1833              :   /// Each [TaskEvent] contains:
    1834              :   /// - `taskId` - ID of the completed task
    1835              :   /// - `success` - true if task succeeded, false if failed
    1836              :   /// - `message` - Optional error message (only present on failure)
    1837              :   ///
    1838              :   /// ## Behavior
    1839              :   ///
    1840              :   /// - Emits an event when ANY task completes (success or failure)
    1841              :   /// - Events are emitted even if app is in background
    1842              :   /// - Stream is broadcast - multiple listeners supported
    1843              :   /// - Events are NOT persisted - you won't receive events for tasks
    1844              :   ///   that completed while app was closed
    1845              :   ///
    1846              :   /// ## Platform Notes
    1847              :   ///
    1848              :   /// **Android:**
    1849              :   /// - Events delivered via EventChannel
    1850              :   /// - Immediate delivery when app is running
    1851              :   ///
    1852              :   /// **iOS:**
    1853              :   /// - Events delivered when app comes to foreground
    1854              :   /// - May be batched if multiple tasks completed while app was suspended
    1855              :   ///
    1856              :   /// ## Common Patterns
    1857              :   ///
    1858              :   /// **Filter by task ID prefix:**
    1859              :   /// ```dart
    1860              :   /// NativeWorkManager.events
    1861              :   ///     .where((event) => event.taskId.startsWith('sync-'))
    1862              :   ///     .listen((event) {
    1863              :   ///   // Only sync tasks
    1864              :   /// });
    1865              :   /// ```
    1866              :   ///
    1867              :   /// **Handle only failures:**
    1868              :   /// ```dart
    1869              :   /// NativeWorkManager.events
    1870              :   ///     .where((event) => !event.success)
    1871              :   ///     .listen((event) {
    1872              :   ///   logError('Task ${event.taskId} failed: ${event.message}');
    1873              :   /// });
    1874              :   /// ```
    1875              :   ///
    1876              :   /// ## Common Pitfalls
    1877              :   ///
    1878              :   /// ❌ **Don't** forget to cancel subscriptions (causes memory leaks)
    1879              :   /// ❌ **Don't** perform heavy work in listener (blocks event stream)
    1880              :   /// ✅ **Do** use StreamBuilder for UI updates
    1881              :   /// ✅ **Do** filter events by taskId prefix for organization
    1882              :   ///
    1883              :   /// ## See Also
    1884              :   ///
    1885              :   /// - [progress] - Stream of progress updates during task execution
    1886              :   /// - [getTaskStatus] - Query task status on-demand
    1887            0 :   static Stream<TaskEvent> get events {
    1888            0 :     _checkInitialized();
    1889            0 :     return NativeWorkManagerPlatform.instance.events;
    1890              :   }
    1891              : 
    1892              :   /// Stream of task progress updates.
    1893              :   ///
    1894              :   /// Get real-time progress updates during task execution. Useful for showing
    1895              :   /// upload/download progress bars in your UI.
    1896              :   ///
    1897              :   /// ## Example - Show Progress Bar
    1898              :   ///
    1899              :   /// ```dart
    1900              :   /// class DownloadProgressWidget extends StatefulWidget {
    1901              :   ///   final String taskId;
    1902              :   ///   const DownloadProgressWidget({required this.taskId});
    1903              :   ///
    1904              :   ///   @override
    1905              :   ///   State createState() => _DownloadProgressWidgetState();
    1906              :   /// }
    1907              :   ///
    1908              :   /// class _DownloadProgressWidgetState extends State<DownloadProgressWidget> {
    1909              :   ///   double _progress = 0.0;
    1910              :   ///   StreamSubscription? _progressSub;
    1911              :   ///
    1912              :   ///   @override
    1913              :   ///   void initState() {
    1914              :   ///     super.initState();
    1915              :   ///     _progressSub = NativeWorkManager.progress
    1916              :   ///         .where((p) => p.taskId == widget.taskId)
    1917              :   ///         .listen((progress) {
    1918              :   ///       setState(() {
    1919              :   ///         _progress = progress.progress / 100.0;
    1920              :   ///       });
    1921              :   ///     });
    1922              :   ///   }
    1923              :   ///
    1924              :   ///   @override
    1925              :   ///   void dispose() {
    1926              :   ///     _progressSub?.cancel();
    1927              :   ///     super.dispose();
    1928              :   ///   }
    1929              :   ///
    1930              :   ///   @override
    1931              :   ///   Widget build(BuildContext context) {
    1932              :   ///     return LinearProgressIndicator(value: _progress);
    1933              :   ///   }
    1934              :   /// }
    1935              :   /// ```
    1936              :   ///
    1937              :   /// ## Example - Show Upload Speed
    1938              :   ///
    1939              :   /// ```dart
    1940              :   /// double? lastProgress;
    1941              :   /// DateTime? lastUpdate;
    1942              :   ///
    1943              :   /// NativeWorkManager.progress.listen((progress) {
    1944              :   ///   if (lastProgress != null && lastUpdate != null) {
    1945              :   ///     final progressDelta = progress.progress - lastProgress!;
    1946              :   ///     final timeDelta = DateTime.now().difference(lastUpdate!).inSeconds;
    1947              :   ///
    1948              :   ///     if (timeDelta > 0) {
    1949              :   ///       final speed = progressDelta / timeDelta;
    1950              :   ///       print('Upload speed: ${speed.toStringAsFixed(1)}%/sec');
    1951              :   ///     }
    1952              :   ///   }
    1953              :   ///
    1954              :   ///   lastProgress = progress.progress;
    1955              :   ///   lastUpdate = DateTime.now();
    1956              :   /// });
    1957              :   /// ```
    1958              :   ///
    1959              :   /// ## Progress Properties
    1960              :   ///
    1961              :   /// Each [TaskProgress] contains:
    1962              :   /// - `taskId` - ID of the task reporting progress
    1963              :   /// - `progress` - Progress value (0-100)
    1964              :   ///
    1965              :   /// ## Supported Workers
    1966              :   ///
    1967              :   /// Progress updates are available for:
    1968              :   /// - ✅ `NativeWorker.httpUpload` - Upload progress
    1969              :   /// - ✅ `NativeWorker.httpDownload` - Download progress
    1970              :   /// - ❌ `NativeWorker.httpRequest` - No progress (too fast)
    1971              :   /// - ❌ `NativeWorker.httpSync` - No progress
    1972              :   /// - ⚠️ `DartWorker` - Only if manually reported in callback
    1973              :   ///
    1974              :   /// ## Behavior
    1975              :   ///
    1976              :   /// - Progress values range from 0 to 100
    1977              :   /// - Updates emitted periodically during task execution
    1978              :   /// - Not all tasks report progress (see Supported Workers above)
    1979              :   /// - Stream is broadcast - multiple listeners supported
    1980              :   ///
    1981              :   /// ## Platform Notes
    1982              :   ///
    1983              :   /// **Android:**
    1984              :   /// - Progress delivered via EventChannel
    1985              :   /// - Update frequency: ~1 update per second
    1986              :   ///
    1987              :   /// **iOS:**
    1988              :   /// - Progress may not be available for all workers
    1989              :   /// - Update frequency varies by worker type
    1990              :   ///
    1991              :   /// ## Common Pitfalls
    1992              :   ///
    1993              :   /// ❌ **Don't** expect progress for all worker types
    1994              :   /// ❌ **Don't** assume linear progress (may jump or pause)
    1995              :   /// ❌ **Don't** forget to cancel subscriptions
    1996              :   /// ✅ **Do** filter by taskId if tracking specific task
    1997              :   /// ✅ **Do** show indeterminate progress for unsupported workers
    1998              :   ///
    1999              :   /// ## See Also
    2000              :   ///
    2001              :   /// - [events] - Stream of task completion events
    2002            0 :   static Stream<TaskProgress> get progress {
    2003            0 :     _checkInitialized();
    2004            0 :     return NativeWorkManagerPlatform.instance.progress;
    2005              :   }
    2006              : 
    2007              :   // ═══════════════════════════════════════════════════════════════════════════
    2008              :   // DART WORKER REGISTRATION
    2009              :   // ═══════════════════════════════════════════════════════════════════════════
    2010              : 
    2011              :   /// Register additional Dart workers after initialization.
    2012              :   ///
    2013              :   /// ```dart
    2014              :   /// NativeWorkManager.registerDartWorker(
    2015              :   ///   'lateWorker',
    2016              :   ///   (input) async {
    2017              :   ///     // Worker logic
    2018              :   ///     return true;
    2019              :   ///   },
    2020              :   /// );
    2021              :   /// ```
    2022            4 :   static void registerDartWorker(String id, DartWorkerCallback callback) {
    2023            8 :     _dartWorkers[id] = callback;
    2024              : 
    2025            4 :     final handle = PluginUtilities.getCallbackHandle(callback);
    2026              :     if (handle == null) {
    2027            2 :       throw StateError(
    2028              :         'Failed to get callback handle for "$id". '
    2029              :         'Ensure the callback is a top-level or static function, '
    2030              :         'NOT an anonymous function or instance method.',
    2031              :       );
    2032              :     }
    2033            9 :     _callbackHandles[id] = handle.toRawHandle();
    2034              :   }
    2035              : 
    2036              :   /// Unregister a Dart worker.
    2037            4 :   static void unregisterDartWorker(String id) {
    2038            8 :     _dartWorkers.remove(id);
    2039            8 :     _callbackHandles.remove(id);
    2040              :   }
    2041              : 
    2042              :   /// Check if a Dart worker is registered.
    2043            3 :   static bool isDartWorkerRegistered(String id) {
    2044            6 :     return _dartWorkers.containsKey(id);
    2045              :   }
    2046              : 
    2047              :   // ═══════════════════════════════════════════════════════════════════════════
    2048              :   // OBSERVABILITY
    2049              :   // ═══════════════════════════════════════════════════════════════════════════
    2050              : 
    2051              :   // ═══════════════════════════════════════════════════════════════════════════
    2052              :   // TASK GRAPH (DAG)
    2053              :   // ═══════════════════════════════════════════════════════════════════════════
    2054              : 
    2055              :   /// Schedule a [TaskGraph] (directed acyclic graph) of background tasks.
    2056              :   ///
    2057              :   /// Nodes with no dependencies run immediately in parallel.  A node starts
    2058              :   /// only when **all** its [TaskNode.dependsOn] nodes have succeeded.  If any
    2059              :   /// node fails, its transitive dependents are cancelled.
    2060              :   ///
    2061              :   /// Returns a [GraphExecution] handle whose [GraphExecution.result] future
    2062              :   /// resolves when the entire graph finishes.
    2063              :   ///
    2064              :   /// ```dart
    2065              :   /// final graph = TaskGraph(id: 'export')
    2066              :   ///   ..add(TaskNode(id: 'dl-a', worker: HttpDownloadWorker(url: urlA, savePath: pathA)))
    2067              :   ///   ..add(TaskNode(id: 'dl-b', worker: HttpDownloadWorker(url: urlB, savePath: pathB)))
    2068              :   ///   ..add(TaskNode(id: 'merge', worker: DartWorker(callbackId: 'merge'),
    2069              :   ///       dependsOn: ['dl-a', 'dl-b']))
    2070              :   ///   ..add(TaskNode(id: 'upload', worker: HttpUploadWorker(url: apiUrl, filePath: merged),
    2071              :   ///       dependsOn: ['merge']));
    2072              :   ///
    2073              :   /// final exec = await NativeWorkManager.enqueueGraph(graph);
    2074              :   /// final result = await exec.result;
    2075              :   /// print(result.success ? 'Done!' : 'Failed: ${result.failedNodes}');
    2076              :   /// ```
    2077              :   ///
    2078              :   /// **Note:** The graph executor uses [NativeWorkManager.events] for
    2079              :   /// fan-in synchronization, so the app must be running while the graph
    2080              :   /// executes. Graphs that must survive app termination should use
    2081              :   /// independent [enqueue] calls with chain logic instead.
    2082            0 :   static Future<GraphExecution> enqueueGraph(TaskGraph graph) {
    2083            0 :     _checkInitialized();
    2084            0 :     return enqueueTaskGraph(graph);
    2085              :   }
    2086              : 
    2087              :   /// Configure observability hooks for all background tasks.
    2088              :   ///
    2089              :   /// Call after [initialize] to receive callbacks whenever a task starts,
    2090              :   /// completes, or fails — without manually subscribing to the [events] and
    2091              :   /// [progress] streams in every widget.
    2092              :   ///
    2093              :   /// Calling [configure] again replaces the previous observability config.
    2094              :   /// Pass `null` to remove the existing config.
    2095              :   ///
    2096              :   /// ```dart
    2097              :   /// NativeWorkManager.configure(
    2098              :   ///   observability: ObservabilityConfig(
    2099              :   ///     onTaskStart: (taskId, workerType) {
    2100              :   ///       analytics.track('bg_task_start', {'type': workerType});
    2101              :   ///     },
    2102              :   ///     onTaskComplete: (event) {
    2103              :   ///       performance.record('task_ok', {'id': event.taskId});
    2104              :   ///     },
    2105              :   ///     onTaskFail: (event) {
    2106              :   ///       crashlytics.log('Task failed: ${event.taskId} — ${event.message}');
    2107              :   ///     },
    2108              :   ///   ),
    2109              :   /// );
    2110              :   /// ```
    2111            1 :   static void configure({ObservabilityConfig? observability}) {
    2112              :     // Cancel existing observability subscriptions.
    2113            1 :     _observabilityEventSub?.cancel();
    2114            1 :     _observabilityProgressSub?.cancel();
    2115              :     _observabilityEventSub = null;
    2116              :     _observabilityProgressSub = null;
    2117              : 
    2118              :     if (observability == null) return;
    2119              : 
    2120            1 :     final dispatcher = ObservabilityDispatcher(observability);
    2121              : 
    2122            2 :     _observabilityEventSub = NativeWorkManagerPlatform.instance.events
    2123            2 :         .listen(dispatcher.dispatchEvent);
    2124            2 :     _observabilityProgressSub = NativeWorkManagerPlatform.instance.progress
    2125            2 :         .listen(dispatcher.dispatchProgress);
    2126              :   }
    2127              : 
    2128              :   /// Register a remote trigger for background task execution.
    2129              :   ///
    2130              :   /// Remote triggers allow your backend to initiate background tasks on
    2131              :   /// user devices via FCM (Android/iOS) or APNs (iOS).
    2132              :   ///
    2133              :   /// Unlike standard tasks, remote triggers are **registered once** and
    2134              :   /// can then be triggered multiple times from the server by sending a
    2135              :   /// data message with the matching payload.
    2136              :   ///
    2137              :   /// **Zero Flutter Engine overhead:** When a remote trigger is received, the
    2138              :   /// plugin enqueues the worker directly on the native side. Flutter is
    2139              :   /// NOT started unless you use a [DartWorker].
    2140              :   ///
    2141              :   /// ## Example - Remote Sync
    2142              :   ///
    2143              :   /// ```dart
    2144              :   /// await NativeWorkManager.registerRemoteTrigger(
    2145              :   ///   source: RemoteTriggerSource.fcm,
    2146              :   ///   rule: RemoteTriggerRule(
    2147              :   ///     payloadKey: 'action',
    2148              :   ///     workerMappings: {
    2149              :   ///       'sync': NativeWorker.httpSync(
    2150              :   ///         url: 'https://api.example.com/sync',
    2151              :   ///         headers: {'Authorization': 'Bearer {{token}}'}, // From payload
    2152              :   ///       ),
    2153              :   ///     },
    2154              :   ///   ),
    2155              :   /// );
    2156              :   /// ```
    2157            0 :   static Future<void> registerRemoteTrigger({
    2158              :     required RemoteTriggerSource source,
    2159              :     required RemoteTriggerRule rule,
    2160              :   }) async {
    2161            0 :     _checkInitialized();
    2162            0 :     return NativeWorkManagerPlatform.instance.registerRemoteTrigger(
    2163              :       source: source,
    2164              :       rule: rule,
    2165              :     );
    2166              :   }
    2167              : 
    2168              :   /// Register a middleware for background tasks.
    2169              :   ///
    2170              :   /// Middleware allows you to intercept and modify tasks globally on the
    2171              :   /// native side.
    2172              :   ///
    2173              :   /// ## Example - Global Auth Header
    2174              :   ///
    2175              :   /// ```dart
    2176              :   /// await NativeWorkManager.registerMiddleware(
    2177              :   ///   HeaderMiddleware(
    2178              :   ///     headers: {'Authorization': 'Bearer my-token'},
    2179              :   ///     urlPattern: 'https://api.myapp.com/.*',
    2180              :   ///   ),
    2181              :   /// );
    2182              :   /// ```
    2183            0 :   static Future<void> registerMiddleware(Middleware middleware) async {
    2184            0 :     _checkInitialized();
    2185            0 :     return NativeWorkManagerPlatform.instance
    2186            0 :         .registerMiddleware(middleware.toMap());
    2187              :   }
    2188              : 
    2189              :   // ═══════════════════════════════════════════════════════════════════════════
    2190              :   // OBSERVABILITY EXTENSIONS
    2191              :   // ═══════════════════════════════════════════════════════════════════════════
    2192              : 
    2193              :   /// Convenience stub for Sentry integration.
    2194              :   ///
    2195              :   /// **⚠️ This method does not call any Sentry SDK.** It only logs to
    2196              :   /// `debugPrint` so it is useful only as a quick smoke-test.
    2197              :   ///
    2198              :   /// For production Sentry integration, implement [WorkManagerLogger] and
    2199              :   /// wire it up via [ObservabilityConfig.fromLogger]:
    2200              :   ///
    2201              :   /// ```dart
    2202              :   /// class SentryWorkManagerLogger implements WorkManagerLogger {
    2203              :   ///   @override
    2204              :   ///   void onTaskStart(String taskId, String workerType) =>
    2205              :   ///       Sentry.addBreadcrumb(Breadcrumb(message: 'Task started: $taskId'));
    2206              :   ///
    2207              :   ///   @override
    2208              :   ///   void onTaskComplete(TaskEvent event) =>
    2209              :   ///       Sentry.addBreadcrumb(Breadcrumb(message: 'Task done: ${event.taskId}'));
    2210              :   ///
    2211              :   ///   @override
    2212              :   ///   void onTaskFail(TaskEvent event) =>
    2213              :   ///       Sentry.captureMessage('Task failed: ${event.taskId}');
    2214              :   /// }
    2215              :   ///
    2216              :   /// NativeWorkManager.configure(
    2217              :   ///   observability: ObservabilityConfig.fromLogger(SentryWorkManagerLogger()),
    2218              :   /// );
    2219              :   /// ```
    2220            1 :   @Deprecated(
    2221              :     'useSentry() does not call the Sentry SDK. '
    2222              :     'Implement WorkManagerLogger and use ObservabilityConfig.fromLogger() instead.',
    2223              :   )
    2224              :   static void useSentry({bool addBreadcrumbs = true, dynamic sentryInstance}) {
    2225            1 :     configure(
    2226            1 :       observability: ObservabilityConfig(
    2227              :         onTaskStart: addBreadcrumbs
    2228            0 :             ? (id, type) =>
    2229            0 :                 debugPrint('[native_workmanager] task started: $id ($type)')
    2230              :             : null,
    2231              :         onTaskComplete: addBreadcrumbs
    2232            0 :             ? (e) => debugPrint('[native_workmanager] task done: ${e.taskId}')
    2233              :             : null,
    2234            0 :         onTaskFail: (e) => debugPrint(
    2235            0 :             '[native_workmanager] task FAILED: ${e.taskId} — ${e.message}'),
    2236              :       ),
    2237              :     );
    2238              :   }
    2239              : 
    2240              :   /// Convenience stub for Firebase Analytics / Crashlytics integration.
    2241              :   ///
    2242              :   /// **⚠️ This method does not call any Firebase SDK.** It only logs to
    2243              :   /// `debugPrint` so it is useful only as a quick smoke-test.
    2244              :   ///
    2245              :   /// For production Firebase integration, implement [WorkManagerLogger] and
    2246              :   /// wire it up via [ObservabilityConfig.fromLogger]:
    2247              :   ///
    2248              :   /// ```dart
    2249              :   /// class FirebaseWorkManagerLogger implements WorkManagerLogger {
    2250              :   ///   @override
    2251              :   ///   void onTaskStart(String taskId, String workerType) =>
    2252              :   ///       FirebaseAnalytics.instance.logEvent(
    2253              :   ///         name: 'bg_task_start',
    2254              :   ///         parameters: {'task_id': taskId, 'worker': workerType},
    2255              :   ///       );
    2256              :   ///
    2257              :   ///   @override
    2258              :   ///   void onTaskComplete(TaskEvent event) =>
    2259              :   ///       FirebaseAnalytics.instance.logEvent(
    2260              :   ///         name: 'bg_task_success',
    2261              :   ///         parameters: {'task_id': event.taskId},
    2262              :   ///       );
    2263              :   ///
    2264              :   ///   @override
    2265              :   ///   void onTaskFail(TaskEvent event) =>
    2266              :   ///       FirebaseCrashlytics.instance.recordError(
    2267              :   ///         event.message ?? 'Unknown',
    2268              :   ///         null,
    2269              :   ///         reason: 'Background task failed: ${event.taskId}',
    2270              :   ///       );
    2271              :   /// }
    2272              :   ///
    2273              :   /// NativeWorkManager.configure(
    2274              :   ///   observability: ObservabilityConfig.fromLogger(FirebaseWorkManagerLogger()),
    2275              :   /// );
    2276              :   /// ```
    2277            1 :   @Deprecated(
    2278              :     'useFirebase() does not call the Firebase SDK. '
    2279              :     'Implement WorkManagerLogger and use ObservabilityConfig.fromLogger() instead.',
    2280              :   )
    2281              :   static void useFirebase({
    2282              :     bool logToAnalytics = true,
    2283              :     bool logToCrashlytics = true,
    2284              :   }) {
    2285            1 :     configure(
    2286            1 :       observability: ObservabilityConfig(
    2287              :         onTaskStart: logToAnalytics
    2288            0 :             ? (id, type) => debugPrint(
    2289            0 :                 '[native_workmanager] Firebase: bg_task_start $id $type')
    2290              :             : null,
    2291              :         onTaskComplete: logToAnalytics
    2292            0 :             ? (e) => debugPrint(
    2293            0 :                 '[native_workmanager] Firebase: bg_task_success ${e.taskId}')
    2294              :             : null,
    2295            0 :         onTaskFail: (e) {
    2296              :           if (logToAnalytics) {
    2297            0 :             debugPrint(
    2298            0 :                 '[native_workmanager] Firebase: bg_task_fail ${e.taskId} — ${e.message}');
    2299              :           }
    2300              :           if (logToCrashlytics) {
    2301            0 :             debugPrint(
    2302            0 :                 '[native_workmanager] Firebase Crashlytics: task failed: ${e.taskId}');
    2303              :           }
    2304              :         },
    2305              :       ),
    2306              :     );
    2307              :   }
    2308              : }
        

Generated by: LCOV version 2.4-0