LCOV - code coverage report
Current view: top level - src/workers - native_worker_http.dart Coverage Total Hit
Test: lcov.info Lines: 83.6 % 67 56
Test Date: 2026-04-30 18:23:23 Functions: - 0 0

            Line data    Source code
       1              : part of '../worker.dart';
       2              : 
       3              : /// HTTP request worker (GET, POST, PUT, DELETE).
       4              : ///
       5              : /// Executes an HTTP request in the background **without** starting the Flutter Engine.
       6              : /// This is the most lightweight option for simple API calls, analytics, or ping requests.
       7              : ///
       8              : /// ## Basic GET Request
       9              : ///
      10              : /// ```dart
      11              : /// await NativeWorkManager.enqueue(
      12              : ///   taskId: 'fetch-status',
      13              : ///   trigger: TaskTrigger.oneTime(),
      14              : ///   worker: NativeWorker.httpRequest(
      15              : ///     url: 'https://api.example.com/status',
      16              : ///     method: HttpMethod.get,
      17              : ///   ),
      18              : /// );
      19              : /// ```
      20              : ///
      21              : /// ## POST with JSON Body
      22              : ///
      23              : /// ```dart
      24              : /// await NativeWorkManager.enqueue(
      25              : ///   taskId: 'send-analytics',
      26              : ///   trigger: TaskTrigger.oneTime(),
      27              : ///   worker: NativeWorker.httpRequest(
      28              : ///     url: 'https://analytics.example.com/event',
      29              : ///     method: HttpMethod.post,
      30              : ///     headers: {
      31              : ///       'Content-Type': 'application/json',
      32              : ///       'Authorization': 'Bearer $token',
      33              : ///     },
      34              : ///     body: '{"event": "app_opened", "timestamp": 1234567890}',
      35              : ///   ),
      36              : /// );
      37              : /// ```
      38              : ///
      39              : /// ## DELETE Request
      40              : ///
      41              : /// ```dart
      42              : /// await NativeWorkManager.enqueue(
      43              : ///   taskId: 'delete-account',
      44              : ///   trigger: TaskTrigger.oneTime(),
      45              : ///   worker: NativeWorker.httpRequest(
      46              : ///     url: 'https://api.example.com/users/123',
      47              : ///     method: HttpMethod.delete,
      48              : ///     headers: {'Authorization': 'Bearer $token'},
      49              : ///   ),
      50              : /// );
      51              : /// ```
      52              : ///
      53              : /// ## Parameters
      54              : ///
      55              : /// **[url]** *(required)* - The HTTP/HTTPS endpoint URL.
      56              : /// - Must start with `http://` or `https://`
      57              : /// - Throws `ArgumentError` if empty or invalid format
      58              : ///
      59              : /// **[method]** *(optional)* - HTTP method (default: GET).
      60              : /// - `HttpMethod.get` - Retrieve data
      61              : /// - `HttpMethod.post` - Send data
      62              : /// - `HttpMethod.put` - Update data
      63              : /// - `HttpMethod.delete` - Delete data
      64              : /// - `HttpMethod.patch` - Partial update
      65              : ///
      66              : /// **[headers]** *(optional)* - HTTP headers (default: empty).
      67              : /// - Use for authentication, content type, etc.
      68              : /// - Example: `{'Authorization': 'Bearer token'}`
      69              : ///
      70              : /// **[body]** *(optional)* - Request body for POST/PUT/PATCH.
      71              : /// - Must be a String (JSON encode if needed)
      72              : /// - Ignored for GET/DELETE requests
      73              : ///
      74              : /// **[timeout]** *(optional)* - Request timeout (default: 30 seconds).
      75              : /// - Maximum time to wait for response
      76              : /// - Request fails if timeout exceeded
      77              : ///
      78              : /// ## Behavior
      79              : ///
      80              : /// - Executes in native code (Kotlin on Android, Swift on iOS)
      81              : /// - **No Flutter Engine overhead** (~2MB vs ~50MB RAM)
      82              : /// - Response is not returned (fire-and-forget)
      83              : /// - Task succeeds if HTTP status 200-299
      84              : /// - Task fails on network error or non-2xx status
      85              : ///
      86              : /// ## When to Use
      87              : ///
      88              : /// ✅ **Use httpRequest when:**
      89              : /// - Sending analytics events
      90              : /// - Pinging health check endpoints
      91              : /// - Simple API calls with no response processing
      92              : /// - You need maximum performance (no Flutter Engine)
      93              : ///
      94              : /// ❌ **Don't use httpRequest when:**
      95              : /// - You need to process the response → Use `httpSync` instead
      96              : /// - You're uploading files → Use `httpUpload` instead
      97              : /// - You're downloading files → Use `httpDownload` instead
      98              : ///
      99              : /// ## Common Pitfalls
     100              : ///
     101              : /// ❌ **Don't** expect to receive the response (use `httpSync` for that)
     102              : /// ❌ **Don't** forget to set Content-Type header for POST/PUT
     103              : /// ❌ **Don't** use this for large payloads (use `httpUpload` instead)
     104              : /// ✅ **Do** use for simple fire-and-forget requests
     105              : /// ✅ **Do** set appropriate timeout for your use case
     106              : ///
     107              : /// ## Platform Notes
     108              : ///
     109              : /// **Android:** Uses OkHttp under the hood
     110              : /// **iOS:** Uses URLSession
     111              : ///
     112              : /// ## See Also
     113              : ///
     114              : /// - [NativeWorker.httpSync] - POST JSON and receive JSON response
     115              : /// - [NativeWorker.httpUpload] - Upload files (multipart)
     116              : /// - [NativeWorker.httpDownload] - Download files
     117           13 : Worker _buildHttpRequest({
     118              :   required String url,
     119              :   HttpMethod method = HttpMethod.get,
     120              :   Map<String, String> headers = const {},
     121              :   String? body,
     122              :   Duration timeout = const Duration(seconds: 30),
     123              :   TokenRefreshConfig? tokenRefresh,
     124              : }) {
     125           13 :   NativeWorker._validateUrl(url);
     126              : 
     127              :   // Validate timeout
     128           26 :   if (timeout.inMilliseconds <= 0) {
     129            1 :     throw ArgumentError(
     130            2 :       'Timeout must be positive: ${timeout.inMilliseconds}ms',
     131              :     );
     132              :   }
     133           26 :   if (timeout.inMinutes > 5) {
     134            4 :     throw ArgumentError(
     135            2 :       'Timeout too long: ${timeout.inMinutes} minutes\n'
     136              :       'iOS limits background tasks to 30 seconds\n'
     137              :       'Android may defer long tasks in Doze mode\n'
     138              :       'Recommended: Keep under 5 minutes for reliability\n'
     139            2 :       'Current timeout: ${timeout.inSeconds} seconds',
     140              :     );
     141              :   }
     142              : 
     143           12 :   return HttpRequestWorker(
     144              :     url: url,
     145              :     method: method,
     146              :     headers: headers,
     147              :     body: body,
     148              :     timeout: timeout,
     149              :     tokenRefresh: tokenRefresh,
     150              :   );
     151              : }
     152              : 
     153              : /// HTTP file upload worker (multipart).
     154              : ///
     155              : /// Uploads a file to a server using multipart/form-data encoding.
     156              : /// Runs in native code **without** Flutter Engine for maximum efficiency.
     157              : /// Ideal for uploading photos, videos, documents, or any binary files.
     158              : ///
     159              : /// ## Basic Upload
     160              : ///
     161              : /// ```dart
     162              : /// await NativeWorkManager.enqueue(
     163              : ///   taskId: 'upload-photo-${DateTime.now().millisecondsSinceEpoch}',
     164              : ///   trigger: TaskTrigger.oneTime(),
     165              : ///   worker: NativeWorker.httpUpload(
     166              : ///     url: 'https://api.example.com/upload',
     167              : ///     filePath: '/storage/emulated/0/DCIM/photo.jpg',
     168              : ///   ),
     169              : ///   constraints: Constraints.networkRequired,
     170              : /// );
     171              : /// ```
     172              : ///
     173              : /// ## Upload with Authentication
     174              : ///
     175              : /// ```dart
     176              : /// await NativeWorkManager.enqueue(
     177              : ///   taskId: 'upload-document',
     178              : ///   trigger: TaskTrigger.oneTime(),
     179              : ///   worker: NativeWorker.httpUpload(
     180              : ///     url: 'https://api.example.com/documents',
     181              : ///     filePath: '/data/user/0/com.app/files/document.pdf',
     182              : ///     headers: {
     183              : ///       'Authorization': 'Bearer $accessToken',
     184              : ///     },
     185              : ///   ),
     186              : /// );
     187              : /// ```
     188              : ///
     189              : /// ## Upload with Additional Form Fields
     190              : ///
     191              : /// ```dart
     192              : /// await NativeWorkManager.enqueue(
     193              : ///   taskId: 'upload-avatar',
     194              : ///   trigger: TaskTrigger.oneTime(),
     195              : ///   worker: NativeWorker.httpUpload(
     196              : ///     url: 'https://api.example.com/users/123/avatar',
     197              : ///     filePath: '/cache/cropped_avatar.jpg',
     198              : ///     fileFieldName: 'avatar',
     199              : ///     additionalFields: {
     200              : ///       'user_id': '123',
     201              : ///       'crop_coordinates': '0,0,500,500',
     202              : ///     },
     203              : ///     headers: {'Authorization': 'Bearer $token'},
     204              : ///   ),
     205              : /// );
     206              : /// ```
     207              : ///
     208              : /// ## Upload with Constraints (WiFi + Charging)
     209              : ///
     210              : /// ```dart
     211              : /// // Large video upload - only when charging and on WiFi
     212              : /// await NativeWorkManager.enqueue(
     213              : ///   taskId: 'upload-video',
     214              : ///   trigger: TaskTrigger.oneTime(),
     215              : ///   worker: NativeWorker.httpUpload(
     216              : ///     url: 'https://cdn.example.com/videos',
     217              : ///     filePath: '/storage/videos/recording.mp4',
     218              : ///     timeout: Duration(minutes: 30),
     219              : ///   ),
     220              : ///   constraints: Constraints(
     221              : ///     requiresCharging: true,
     222              : ///     requiresWifi: true,
     223              : ///   ),
     224              : /// );
     225              : /// ```
     226              : ///
     227              : /// ## Upload with Custom Filename and MIME Type
     228              : ///
     229              : /// ```dart
     230              : /// // Upload iOS HEIC photo with custom name and explicit MIME type
     231              : /// final tempPath = '/cache/photo_a1b2c3d4.heic'; // Auto-generated cache file
     232              : ///
     233              : /// await NativeWorkManager.enqueue(
     234              : ///   taskId: 'upload-profile-photo',
     235              : ///   trigger: TaskTrigger.oneTime(),
     236              : ///   worker: NativeWorker.httpUpload(
     237              : ///     url: 'https://api.example.com/photos',
     238              : ///     filePath: tempPath,
     239              : ///     fileName: 'profile_${DateTime.now().millisecondsSinceEpoch}.jpg',
     240              : ///     mimeType: 'image/heic', // Explicit MIME type for iOS HEIC format
     241              : ///     headers: {'Authorization': 'Bearer $token'},
     242              : ///   ),
     243              : /// );
     244              : /// ```
     245              : ///
     246              : /// ## Parameters
     247              : ///
     248              : /// **[url]** *(required)* - The upload endpoint URL.
     249              : /// - Must start with `http://` or `https://`
     250              : /// - Throws `ArgumentError` if empty or invalid
     251              : ///
     252              : /// **[filePath]** *(required)* - Absolute path to file to upload.
     253              : /// - Must be absolute path, not relative
     254              : /// - Throws `ArgumentError` if empty
     255              : /// - File must exist at execution time (not validated at schedule time)
     256              : ///
     257              : /// **[fileFieldName]** *(optional)* - Form field name for file (default: "file").
     258              : /// - Server expects file in this field
     259              : /// - Common values: "file", "image", "avatar", "attachment"
     260              : /// - Throws `ArgumentError` if empty
     261              : ///
     262              : /// **[fileName]** *(optional)* - Override the uploaded filename.
     263              : /// - By default, uses the basename of filePath
     264              : /// - Useful when uploading temp files with meaningful names
     265              : /// - Example: Upload `/cache/temp_123.jpg` as `profile.jpg`
     266              : ///
     267              : /// **[mimeType]** *(optional)* - Override the MIME type.
     268              : /// - By default, auto-detected from file extension
     269              : /// - Required for unusual formats (HEIC, WebP, AVIF)
     270              : /// - Example: `image/heic`, `image/webp`, `application/octet-stream`
     271              : ///
     272              : /// **[headers]** *(optional)* - HTTP headers (default: empty).
     273              : /// - Commonly used for authentication
     274              : /// - Content-Type is set automatically to multipart/form-data
     275              : ///
     276              : /// **[additionalFields]** *(optional)* - Extra form fields (default: empty).
     277              : /// - Send metadata along with file
     278              : /// - All values must be strings
     279              : ///
     280              : /// **[timeout]** *(optional)* - Upload timeout (default: 5 minutes).
     281              : /// - Increase for large files or slow networks
     282              : /// - Upload fails if timeout exceeded
     283              : ///
     284              : /// **[useBackgroundSession]** *(optional, iOS only)* - Use background URLSession (default: false).
     285              : /// - **v2.3.0+ iOS Feature** - Uploads survive app termination
     286              : /// - No time limits (vs 30s foreground limit)
     287              : /// - System-managed retry on network changes
     288              : /// - Battery-efficient scheduling
     289              : /// - Android: No effect (WorkManager already handles this)
     290              : /// - Use for large files (>10MB) or unreliable networks
     291              : /// - Example: Video uploads, large file backups
     292              : ///
     293              : /// ## Behavior
     294              : ///
     295              : /// - Uploads using multipart/form-data encoding
     296              : /// - Content-Type header set automatically
     297              : /// - Reports progress via [NativeWorkManager.progress] stream
     298              : /// - Task succeeds if HTTP status 200-299
     299              : /// - Task fails on network error, file not found, or non-2xx status
     300              : ///
     301              : /// ## Progress Tracking
     302              : ///
     303              : /// ```dart
     304              : /// // Listen to upload progress
     305              : /// NativeWorkManager.progress
     306              : ///     .where((p) => p.taskId == 'my-upload')
     307              : ///     .listen((progress) {
     308              : ///   print('Uploaded: ${progress.progress}%');
     309              : /// });
     310              : /// ```
     311              : ///
     312              : /// ## Progress Tracking (v1.0.0+)
     313              : ///
     314              : /// **NEW:** Upload progress is now automatically reported:
     315              : /// ```dart
     316              : /// // Listen to upload progress
     317              : /// NativeWorkManager.progress
     318              : ///     .where((p) => p.taskId == 'my-upload')
     319              : ///     .listen((progress) {
     320              : ///   print('Uploaded: ${progress.progress}% - ${progress.message}');
     321              : /// });
     322              : /// ```
     323              : ///
     324              : /// Progress updates include:
     325              : /// - Percentage (0-100%)
     326              : /// - Human-readable message (e.g., "Uploading photo.jpg... (2.5MB/10MB)")
     327              : /// - Real-time updates every 1% increment
     328              : ///
     329              : /// ## When to Use
     330              : ///
     331              : /// ✅ **Use httpUpload when:**
     332              : /// - Uploading photos, videos, or documents
     333              : /// - You need progress tracking
     334              : /// - File is already saved to disk
     335              : /// - You want optimal battery usage (native execution)
     336              : ///
     337              : /// ❌ **Don't use httpUpload when:**
     338              : /// - Sending small JSON data → Use `httpRequest` or `httpSync`
     339              : /// - You need to process file before upload → Use `DartWorker`
     340              : ///
     341              : /// ## Storage Validation (v1.0.0+)
     342              : ///
     343              : /// **NEW:** Automatic storage checks before upload:
     344              : /// - Validates minimum 100MB free space
     345              : /// - Prevents uploads when storage is critically low
     346              : /// - Clear error messages if validation fails
     347              : ///
     348              : /// ## Common Pitfalls
     349              : ///
     350              : /// ❌ **Don't** use relative file paths (must be absolute)
     351              : /// ❌ **Don't** assume file still exists at execution time
     352              : /// ❌ **Don't** forget network constraints for large uploads
     353              : /// ❌ **Don't** use short timeout for large files
     354              : /// ✅ **Do** verify file exists before scheduling
     355              : /// ✅ **Do** use WiFi constraint for large uploads
     356              : /// ✅ **Do** handle task failure (file may be deleted)
     357              : ///
     358              : /// ## Platform Notes
     359              : ///
     360              : /// **Android:**
     361              : /// - Uses OkHttp MultipartBody
     362              : /// - Progress reported via WorkManager setProgress
     363              : /// - File must be accessible to app (check permissions)
     364              : ///
     365              : /// **iOS:**
     366              : /// - Uses URLSession uploadTask
     367              : /// - Progress reported via URLSessionTaskDelegate
     368              : /// - File must be in app's sandbox or shared container
     369              : ///
     370              : /// ## See Also
     371              : ///
     372              : /// - [NativeWorker.httpDownload] - Download files
     373              : /// - [NativeWorker.httpRequest] - Simple HTTP requests
     374              : /// - [NativeWorkManager.progress] - Track upload progress
     375            9 : Worker _buildHttpUpload({
     376              :   required String url,
     377              :   required String filePath,
     378              :   String fileFieldName = 'file',
     379              :   String? fileName,
     380              :   String? mimeType,
     381              :   Map<String, String> headers = const {},
     382              :   Map<String, String> additionalFields = const {},
     383              :   Duration timeout = const Duration(minutes: 5),
     384              :   bool useBackgroundSession = false,
     385              : }) {
     386            9 :   NativeWorker._validateUrl(url);
     387            9 :   NativeWorker._validateFilePath(filePath, 'filePath');
     388              : 
     389            9 :   if (fileFieldName.isEmpty) {
     390            1 :     throw ArgumentError(
     391              :       'fileFieldName cannot be empty.\n'
     392              :       'Use a field name like "file" or "image"',
     393              :     );
     394              :   }
     395              : 
     396           18 :   if (timeout.inMinutes > 10) {
     397            2 :     throw ArgumentError(
     398            1 :       'Upload timeout too long: ${timeout.inMinutes} minutes\n'
     399              :       'iOS may terminate tasks after 30 seconds\n'
     400              :       'Android may defer long uploads in Doze mode\n'
     401              :       'Recommended: Keep under 10 minutes, use WiFi constraints for large files\n'
     402            1 :       'Current timeout: ${timeout.inSeconds} seconds',
     403              :     );
     404              :   }
     405              : 
     406              :   // Validate field limits
     407           18 :   if (additionalFields.length > 50) {
     408            2 :     throw ArgumentError(
     409            1 :       'Too many form fields: ${additionalFields.length}\n'
     410              :       'Maximum allowed: 50 fields\n'
     411            1 :       'Current count: ${additionalFields.length}\n'
     412              :       'Consider sending large data as JSON in request body instead',
     413              :     );
     414              :   }
     415              : 
     416              :   // Validate field names are not empty
     417           12 :   for (final key in additionalFields.keys) {
     418            3 :     if (key.isEmpty) {
     419            1 :       throw ArgumentError(
     420              :         'Empty field name in additionalFields\n'
     421              :         'All field names must be non-empty strings',
     422              :       );
     423              :     }
     424              :   }
     425              : 
     426            9 :   return HttpUploadWorker(
     427              :     url: url,
     428              :     filePath: filePath,
     429              :     fileFieldName: fileFieldName,
     430              :     fileName: fileName,
     431              :     mimeType: mimeType,
     432              :     headers: headers,
     433              :     additionalFields: additionalFields,
     434              :     timeout: timeout,
     435              :     useBackgroundSession: useBackgroundSession,
     436              :   );
     437              : }
     438              : 
     439              : /// Upload multiple files in a single multipart/form-data HTTP request.
     440              : ///
     441              : /// Throws [ArgumentError] if [files] is empty or exceeds 50 files.
     442              : ///
     443              : /// Example:
     444              : /// ```dart
     445              : /// NativeWorker.multiUpload(
     446              : ///   url: 'https://api.example.com/batch',
     447              : ///   files: [
     448              : ///     const UploadFile(filePath: '/path/photo1.jpg', fieldName: 'photos'),
     449              : ///     const UploadFile(filePath: '/path/photo2.jpg', fieldName: 'photos'),
     450              : ///   ],
     451              : ///   additionalFields: {'albumId': '42'},
     452              : /// )
     453              : /// ```
     454            1 : MultiUploadWorker _buildMultiUpload({
     455              :   required String url,
     456              :   required List<UploadFile> files,
     457              :   Map<String, String> headers = const {},
     458              :   Map<String, String> additionalFields = const {},
     459              :   Duration timeout = const Duration(minutes: 10),
     460              :   bool useBackgroundSession = false,
     461              : }) {
     462            1 :   if (files.isEmpty) {
     463            1 :     throw ArgumentError('files must not be empty');
     464              :   }
     465            2 :   if (files.length > 50) {
     466            0 :     throw ArgumentError('Maximum 50 files per upload request');
     467              :   }
     468            1 :   return MultiUploadWorker(
     469              :     url: url,
     470              :     files: files,
     471              :     headers: headers,
     472              :     additionalFields: additionalFields,
     473              :     timeout: timeout,
     474              :     useBackgroundSession: useBackgroundSession,
     475              :   );
     476              : }
     477              : 
     478              : /// Move a file from app-private storage to a shared / public location.
     479              : ///
     480              : /// On Android uses `MediaStore` (API 29+) or
     481              : /// `Environment.getExternalStoragePublicDirectory` (API 28−).
     482              : /// On iOS saves to the `PHPhotoLibrary` (for `photos`/`video`) or the app's
     483              : /// `Documents` directory (Files app, for `downloads`/`music`).
     484              : ///
     485              : /// Example — save downloaded photo to camera roll:
     486              : /// ```dart
     487              : /// NativeWorker.moveToSharedStorage(
     488              : ///   sourcePath: cacheFile.path,
     489              : ///   storageType: SharedStorageType.photos,
     490              : /// )
     491              : /// ```
     492            1 : MoveToSharedStorageWorker _buildMoveToSharedStorage({
     493              :   required String sourcePath,
     494              :   required SharedStorageType storageType,
     495              :   String? fileName,
     496              :   String? mimeType,
     497              :   String? subDir,
     498              : }) {
     499            1 :   return MoveToSharedStorageWorker(
     500              :     sourcePath: sourcePath,
     501              :     storageType: storageType,
     502              :     fileName: fileName,
     503              :     mimeType: mimeType,
     504              :     subDir: subDir,
     505              :   );
     506              : }
     507              : 
     508              : /// HTTP file download worker.
     509              : ///
     510              : /// Downloads a file from a URL and saves it to local storage.
     511              : /// Runs in native code **without** Flutter Engine for optimal performance.
     512              : /// Perfect for downloading images, videos, PDFs, or data files.
     513              : ///
     514              : /// ## Basic Download
     515              : ///
     516              : /// ```dart
     517              : /// await NativeWorkManager.enqueue(
     518              : ///   taskId: 'download-update',
     519              : ///   trigger: TaskTrigger.oneTime(),
     520              : ///   worker: NativeWorker.httpDownload(
     521              : ///     url: 'https://cdn.example.com/app-update.apk',
     522              : ///     savePath: '/storage/emulated/0/Download/update.apk',
     523              : ///   ),
     524              : ///   constraints: Constraints.networkRequired,
     525              : /// );
     526              : /// ```
     527              : ///
     528              : /// ## Download with WiFi Constraint
     529              : ///
     530              : /// ```dart
     531              : /// // Large file - only download on WiFi
     532              : /// await NativeWorkManager.enqueue(
     533              : ///   taskId: 'download-video',
     534              : ///   trigger: TaskTrigger.oneTime(),
     535              : ///   worker: NativeWorker.httpDownload(
     536              : ///     url: 'https://cdn.example.com/video.mp4',
     537              : ///     savePath: '/data/user/0/com.app/files/videos/movie.mp4',
     538              : ///     timeout: Duration(minutes: 30),
     539              : ///   ),
     540              : ///   constraints: Constraints(
     541              : ///     requiresWifi: true,
     542              : ///     requiresStorageNotLow: true,
     543              : ///   ),
     544              : /// );
     545              : /// ```
     546              : ///
     547              : /// ## Download with Authentication
     548              : ///
     549              : /// ```dart
     550              : /// await NativeWorkManager.enqueue(
     551              : ///   taskId: 'download-report',
     552              : ///   trigger: TaskTrigger.oneTime(),
     553              : ///   worker: NativeWorker.httpDownload(
     554              : ///     url: 'https://api.example.com/reports/2024.pdf',
     555              : ///     savePath: '/data/user/0/com.app/files/reports/2024.pdf',
     556              : ///     headers: {
     557              : ///       'Authorization': 'Bearer $token',
     558              : ///     },
     559              : ///   ),
     560              : /// );
     561              : /// ```
     562              : ///
     563              : /// ## Background Content Update
     564              : ///
     565              : /// ```dart
     566              : /// // Periodic content sync - download new data every 6 hours
     567              : /// await NativeWorkManager.enqueue(
     568              : ///   taskId: 'sync-content',
     569              : ///   trigger: TaskTrigger.periodic(Duration(hours: 6)),
     570              : ///   worker: NativeWorker.httpDownload(
     571              : ///     url: 'https://api.example.com/content/latest.json',
     572              : ///     savePath: '/data/user/0/com.app/cache/content.json',
     573              : ///   ),
     574              : ///   constraints: Constraints.networkRequired,
     575              : /// );
     576              : /// ```
     577              : ///
     578              : /// ## Resume Support (v1.0.0+)
     579              : ///
     580              : /// Downloads automatically resume from the last byte on network failure:
     581              : /// ```dart
     582              : /// await NativeWorkManager.enqueue(
     583              : ///   taskId: 'download-large-file',
     584              : ///   trigger: TaskTrigger.oneTime(),
     585              : ///   worker: NativeWorker.httpDownload(
     586              : ///     url: 'https://cdn.example.com/app-update.apk',  // 100MB file
     587              : ///     savePath: '/downloads/update.apk',
     588              : ///     enableResume: true,  // Resume from last byte (default)
     589              : ///   ),
     590              : ///   constraints: Constraints.networkRequired,
     591              : /// );
     592              : /// ```
     593              : ///
     594              : /// **How Resume Works:**
     595              : /// - Downloads to temp file (`.tmp` extension)
     596              : /// - On network failure, temp file is preserved
     597              : /// - Next attempt sends `Range: bytes=N-` header
     598              : /// - Server returns `206 Partial Content` with remaining data
     599              : /// - Falls back to full download if server doesn't support Range
     600              : ///
     601              : /// ## Checksum Verification (v1.0.0+)
     602              : ///
     603              : /// Verify download integrity with checksum:
     604              : /// ```dart
     605              : /// await NativeWorkManager.enqueue(
     606              : ///   taskId: 'download-verified',
     607              : ///   trigger: TaskTrigger.oneTime(),
     608              : ///   worker: NativeWorker.httpDownload(
     609              : ///     url: 'https://cdn.example.com/update.apk',
     610              : ///     savePath: '/downloads/update.apk',
     611              : ///     expectedChecksum: 'a3b2c1d4e5f6...',  // Hex string
     612              : ///     checksumAlgorithm: 'SHA-256',  // MD5, SHA-1, SHA-256, SHA-512
     613              : ///   ),
     614              : /// );
     615              : /// ```
     616              : ///
     617              : /// ## Parameters
     618              : ///
     619              : /// **[url]** *(required)* - The file URL to download.
     620              : /// - Must start with `http://` or `https://`
     621              : /// - Throws `ArgumentError` if empty or invalid
     622              : ///
     623              : /// **[savePath]** *(required)* - Where to save the downloaded file.
     624              : /// - Must be absolute path, not relative
     625              : /// - Throws `ArgumentError` if empty
     626              : /// - Directory must exist (not auto-created)
     627              : /// - Existing file will be overwritten
     628              : ///
     629              : /// **[headers]** *(optional)* - HTTP headers (default: empty).
     630              : /// - Use for authentication or custom headers
     631              : /// - Example: `{'Authorization': 'Bearer token'}`
     632              : ///
     633              : /// **[timeout]** *(optional)* - Download timeout (default: 5 minutes).
     634              : /// - Increase for large files or slow networks
     635              : /// - Download fails if timeout exceeded
     636              : ///
     637              : /// **[enableResume]** *(optional)* - Enable automatic resume (default: true).
     638              : /// - When enabled, interrupted downloads resume from last byte
     639              : /// - Uses HTTP Range requests (RFC 7233)
     640              : /// - Falls back to full download if server doesn't support Range
     641              : ///
     642              : /// **[expectedChecksum]** *(optional)* - Expected checksum for verification.
     643              : /// - Hexadecimal string (e.g., "a3b2c1d4e5f6...")
     644              : /// - Download fails if actual checksum doesn't match
     645              : /// - Use with [checksumAlgorithm] to specify algorithm
     646              : ///
     647              : /// **[checksumAlgorithm]** *(optional)* - Hash algorithm (default: 'SHA-256').
     648              : /// - Supported: 'MD5', 'SHA-1', 'SHA-256', 'SHA-512'
     649              : /// - Only used when [expectedChecksum] is provided
     650              : ///
     651              : /// **[useBackgroundSession]** *(optional, iOS only)* - Use background URLSession (default: false).
     652              : /// - **v2.3.0+ iOS Feature** - Downloads survive app termination
     653              : /// - No time limits (vs 30s foreground limit)
     654              : /// - System-managed retry on network changes
     655              : /// - Battery-efficient scheduling
     656              : /// - Android: No effect (WorkManager already handles this)
     657              : /// - Use for large files (>10MB) or unreliable networks
     658              : /// - Example: App updates, media downloads
     659              : ///
     660              : /// ## Behavior
     661              : ///
     662              : /// - Downloads file to specified path
     663              : /// - Reports progress via [NativeWorkManager.progress] stream
     664              : /// - Overwrites existing file at savePath
     665              : /// - Task succeeds if HTTP status 200-299 and file saved
     666              : /// - Task fails on network error, disk full, or non-2xx status
     667              : ///
     668              : /// ## Progress Tracking
     669              : ///
     670              : /// ```dart
     671              : /// // Show download progress in UI
     672              : /// NativeWorkManager.progress
     673              : ///     .where((p) => p.taskId == 'my-download')
     674              : ///     .listen((progress) {
     675              : ///   setState(() {
     676              : ///     downloadProgress = progress.progress / 100.0;
     677              : ///   });
     678              : /// });
     679              : /// ```
     680              : ///
     681              : /// ## Progress Tracking (v1.0.0+)
     682              : ///
     683              : /// **NEW:** Download progress is now automatically reported:
     684              : /// ```dart
     685              : /// // Show download progress in UI
     686              : /// NativeWorkManager.progress
     687              : ///     .where((p) => p.taskId == 'my-download')
     688              : ///     .listen((progress) {
     689              : ///   setState(() {
     690              : ///     downloadProgress = progress.progress / 100.0;
     691              : ///   });
     692              : ///   print(progress.message); // "Downloading file.zip... (45MB/100MB)"
     693              : /// });
     694              : /// ```
     695              : ///
     696              : /// Progress updates include:
     697              : /// - Percentage (0-100%)
     698              : /// - Human-readable message with bytes transferred
     699              : /// - Real-time updates every 1% increment
     700              : ///
     701              : /// ## When to Use
     702              : ///
     703              : /// ✅ **Use httpDownload when:**
     704              : /// - Downloading files, images, videos, or documents
     705              : /// - You need progress tracking
     706              : /// - You want to save result to specific location
     707              : /// - You need optimal battery usage (native execution)
     708              : ///
     709              : /// ❌ **Don't use httpDownload when:**
     710              : /// - Downloading small JSON data → Use `httpSync` instead
     711              : /// - You need to process data before saving → Use `DartWorker`
     712              : ///
     713              : /// ## Storage Validation (v1.0.0+)
     714              : ///
     715              : /// **NEW:** Automatic storage checks before download:
     716              : /// - Validates file size + 20% buffer + 50MB minimum free space
     717              : /// - Prevents downloads when storage is insufficient
     718              : /// - Clear error messages showing required vs available space
     719              : /// - Saves bandwidth by failing early
     720              : ///
     721              : /// ## Common Pitfalls
     722              : ///
     723              : /// ❌ **Don't** use relative paths for savePath (must be absolute)
     724              : /// ❌ **Don't** assume directory exists (create it first)
     725              : /// ❌ **Don't** download large files without WiFi constraint
     726              : /// ❌ **Don't** disable resume for large files (wastes bandwidth)
     727              : /// ✅ **Do** create parent directory before scheduling
     728              : /// ✅ **Do** use WiFi constraint for large downloads
     729              : /// ✅ **Do** handle task failure gracefully
     730              : /// ✅ **Do** listen to progress updates for better UX
     731              : /// ✅ **Do** use checksum verification for critical downloads
     732              : /// ✅ **Do** enable resume for large/slow downloads (default: enabled)
     733              : ///
     734              : /// ## Platform Notes
     735              : ///
     736              : /// **Android:**
     737              : /// - Uses OkHttp for downloading
     738              : /// - Progress reported via WorkManager setProgress
     739              : /// - Requires WRITE_EXTERNAL_STORAGE permission for external storage
     740              : /// - Resume support via HTTP Range requests (RFC 7233)
     741              : /// - Checksum verification using java.security.MessageDigest
     742              : ///
     743              : /// **iOS:**
     744              : /// - Uses URLSession downloadTask
     745              : /// - Progress reported via URLSessionTaskDelegate
     746              : /// - File saved to app sandbox by default
     747              : /// - Resume support via HTTP Range requests (RFC 7233)
     748              : /// - Checksum verification using CryptoKit (iOS 13+)
     749              : ///
     750              : /// ## See Also
     751              : ///
     752              : /// - [NativeWorker.httpUpload] - Upload files
     753              : /// - [NativeWorker.httpRequest] - Simple HTTP requests
     754              : /// - [NativeWorkManager.progress] - Track download progress
     755           11 : Worker _buildHttpDownload({
     756              :   required String url,
     757              :   required String savePath,
     758              :   Map<String, String> headers = const {},
     759              :   Duration timeout = const Duration(minutes: 5),
     760              :   bool enableResume = true,
     761              :   String? expectedChecksum,
     762              :   String checksumAlgorithm = 'SHA-256',
     763              :   bool useBackgroundSession = false,
     764              :   bool skipExisting = false,
     765              :   bool allowPause = false,
     766              :   Map<String, String>? cookies,
     767              :   String? authToken,
     768              :   String authHeaderTemplate = 'Bearer {accessToken}',
     769              :   DuplicatePolicy onDuplicate = DuplicatePolicy.overwrite,
     770              :   bool moveToPublicDownloads = false,
     771              :   bool saveToGallery = false,
     772              :   bool extractAfterDownload = false,
     773              :   String? extractPath,
     774              :   bool deleteArchiveAfterExtract = false,
     775              : }) {
     776           11 :   NativeWorker._validateUrl(url);
     777           11 :   NativeWorker._validateFilePath(savePath, 'savePath');
     778              : 
     779           22 :   if (timeout.inMinutes > 10) {
     780            0 :     throw ArgumentError(
     781            0 :       'Download timeout too long: ${timeout.inMinutes} minutes\n'
     782              :       'iOS may terminate tasks after 30 seconds\n'
     783              :       'Android may defer long downloads in Doze mode\n'
     784              :       'Recommended: Keep under 10 minutes, use WiFi constraints for large files\n'
     785            0 :       'Current timeout: ${timeout.inSeconds} seconds',
     786              :     );
     787              :   }
     788              : 
     789              :   // Validate checksum algorithm if checksum is provided
     790              :   if (expectedChecksum != null) {
     791            2 :     final validAlgorithms = [
     792              :       'MD5',
     793              :       'SHA-1',
     794              :       'SHA1',
     795              :       'SHA-256',
     796              :       'SHA256',
     797              :       'SHA-512',
     798              :       'SHA512',
     799              :     ];
     800            2 :     if (!validAlgorithms.contains(
     801            4 :       checksumAlgorithm.toUpperCase().replaceAll('-', ''),
     802              :     )) {
     803            2 :       throw ArgumentError(
     804              :         'Invalid checksumAlgorithm: "$checksumAlgorithm"\n'
     805              :         'Supported algorithms: MD5, SHA-1, SHA-256, SHA-512',
     806              :       );
     807              :     }
     808              : 
     809              :     // Validate checksum format (must be hex string)
     810            4 :     if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(expectedChecksum)) {
     811            0 :       throw ArgumentError(
     812              :         'Invalid expectedChecksum format: must be hexadecimal string\n'
     813              :         'Example: "a3b2c1d4e5f6789..."',
     814              :       );
     815              :     }
     816              :   }
     817              : 
     818           11 :   return HttpDownloadWorker(
     819              :     url: url,
     820              :     savePath: savePath,
     821              :     headers: headers,
     822              :     timeout: timeout,
     823              :     enableResume: enableResume,
     824              :     expectedChecksum: expectedChecksum,
     825              :     checksumAlgorithm: checksumAlgorithm,
     826              :     useBackgroundSession: useBackgroundSession,
     827              :     skipExisting: skipExisting,
     828              :     allowPause: allowPause,
     829              :     cookies: cookies,
     830              :     authToken: authToken,
     831              :     authHeaderTemplate: authHeaderTemplate,
     832              :     onDuplicate: onDuplicate,
     833              :     moveToPublicDownloads: moveToPublicDownloads,
     834              :     saveToGallery: saveToGallery,
     835              :     extractAfterDownload: extractAfterDownload,
     836              :     extractPath: extractPath,
     837              :     deleteArchiveAfterExtract: deleteArchiveAfterExtract,
     838              :   );
     839              : }
     840              : 
     841              : /// Parallel chunked HTTP download worker.
     842              : ///
     843              : /// Splits a single file into [numChunks] parallel byte-range requests and
     844              : /// downloads them concurrently, then merges into a single output file.
     845              : /// Delivers noticeably faster downloads for large files on servers that
     846              : /// support `Accept-Ranges: bytes`.
     847              : ///
     848              : /// **Automatic fallback:** If the server does not support range requests
     849              : /// or does not return a `Content-Length`, the worker falls back to a
     850              : /// normal sequential download automatically.
     851              : ///
     852              : /// ## Example
     853              : ///
     854              : /// ```dart
     855              : /// await NativeWorkManager.enqueue(
     856              : ///   taskId: 'big-video',
     857              : ///   trigger: TaskTrigger.oneTime(),
     858              : ///   worker: NativeWorker.parallelHttpDownload(
     859              : ///     url: 'https://cdn.example.com/movie.mp4',
     860              : ///     savePath: '/data/user/0/com.example/files/movie.mp4',
     861              : ///     numChunks: 4,
     862              : ///   ),
     863              : ///   constraints: Constraints.networkRequired,
     864              : /// );
     865              : /// ```
     866              : ///
     867              : /// See also: [NativeWorker.httpDownload] for simpler single-connection downloads.
     868            1 : Worker _buildParallelHttpDownload({
     869              :   required String url,
     870              :   required String savePath,
     871              :   int numChunks = 4,
     872              :   Map<String, String> headers = const {},
     873              :   Duration timeout = const Duration(minutes: 10),
     874              :   String? expectedChecksum,
     875              :   String checksumAlgorithm = 'SHA-256',
     876              :   bool showNotification = false,
     877              :   String? notificationTitle,
     878              :   String? notificationBody,
     879              :   bool skipExisting = false,
     880              : }) {
     881            1 :   NativeWorker._validateUrl(url);
     882            1 :   NativeWorker._validateFilePath(savePath, 'savePath');
     883              : 
     884            2 :   if (numChunks < 1 || numChunks > 16) {
     885            0 :     throw ArgumentError('numChunks must be between 1 and 16, got $numChunks');
     886              :   }
     887              : 
     888              :   if (expectedChecksum != null) {
     889            0 :     final validAlgorithms = [
     890              :       'MD5',
     891              :       'SHA-1',
     892              :       'SHA1',
     893              :       'SHA-256',
     894              :       'SHA256',
     895              :       'SHA-512',
     896              :       'SHA512'
     897              :     ];
     898              :     if (!validAlgorithms
     899            0 :         .contains(checksumAlgorithm.toUpperCase().replaceAll('-', ''))) {
     900            0 :       throw ArgumentError(
     901              :         'Invalid checksumAlgorithm: "$checksumAlgorithm"\n'
     902              :         'Supported algorithms: MD5, SHA-1, SHA-256, SHA-512',
     903              :       );
     904              :     }
     905            0 :     if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(expectedChecksum)) {
     906            0 :       throw ArgumentError(
     907              :         'Invalid expectedChecksum format: must be hexadecimal string',
     908              :       );
     909              :     }
     910              :   }
     911              : 
     912            1 :   return ParallelHttpDownloadWorker(
     913              :     url: url,
     914              :     savePath: savePath,
     915              :     numChunks: numChunks,
     916              :     headers: headers,
     917              :     timeout: timeout,
     918              :     expectedChecksum: expectedChecksum,
     919              :     checksumAlgorithm: checksumAlgorithm,
     920              :     showNotification: showNotification,
     921              :     notificationTitle: notificationTitle,
     922              :     notificationBody: notificationBody,
     923              :     skipExisting: skipExisting,
     924              :   );
     925              : }
     926              : 
     927              : /// Data sync worker (POST JSON, receive JSON).
     928              : ///
     929              : /// Sends JSON data to server and receives JSON response. Designed for
     930              : /// data synchronization, API calls that return data, or two-way communication.
     931              : /// Runs in native code **without** Flutter Engine.
     932              : ///
     933              : /// **Note:** Response is NOT returned to Dart code. This is fire-and-forget.
     934              : /// Use `DartWorker` if you need to process the response.
     935              : ///
     936              : /// ## Basic Sync
     937              : ///
     938              : /// ```dart
     939              : /// await NativeWorkManager.enqueue(
     940              : ///   taskId: 'sync-data',
     941              : ///   trigger: TaskTrigger.periodic(Duration(hours: 1)),
     942              : ///   worker: NativeWorker.httpSync(
     943              : ///     url: 'https://api.example.com/sync',
     944              : ///     method: HttpMethod.post,
     945              : ///     requestBody: {
     946              : ///       'lastSyncTime': DateTime.now().millisecondsSinceEpoch,
     947              : ///       'deviceId': 'device123',
     948              : ///     },
     949              : ///   ),
     950              : ///   constraints: Constraints.networkRequired,
     951              : /// );
     952              : /// ```
     953              : ///
     954              : /// ## Sync with Authentication
     955              : ///
     956              : /// ```dart
     957              : /// await NativeWorkManager.enqueue(
     958              : ///   taskId: 'sync-user-data',
     959              : ///   trigger: TaskTrigger.periodic(Duration(hours: 6)),
     960              : ///   worker: NativeWorker.httpSync(
     961              : ///     url: 'https://api.example.com/users/sync',
     962              : ///     method: HttpMethod.post,
     963              : ///     headers: {
     964              : ///       'Authorization': 'Bearer $accessToken',
     965              : ///       'Content-Type': 'application/json',
     966              : ///     },
     967              : ///     requestBody: {
     968              : ///       'settings': {'theme': 'dark', 'notifications': true},
     969              : ///       'timestamp': DateTime.now().toIso8601String(),
     970              : ///     },
     971              : ///   ),
     972              : /// );
     973              : /// ```
     974              : ///
     975              : /// ## Batch Data Upload
     976              : ///
     977              : /// ```dart
     978              : /// await NativeWorkManager.enqueue(
     979              : ///   taskId: 'upload-analytics',
     980              : ///   trigger: TaskTrigger.periodic(Duration(hours: 24)),
     981              : ///   worker: NativeWorker.httpSync(
     982              : ///     url: 'https://analytics.example.com/batch',
     983              : ///     method: HttpMethod.post,
     984              : ///     requestBody: {
     985              : ///       'events': [
     986              : ///         {'type': 'page_view', 'page': '/home', 'timestamp': 1234567890},
     987              : ///         {'type': 'click', 'element': 'button', 'timestamp': 1234567891},
     988              : ///       ],
     989              : ///     },
     990              : ///   ),
     991              : ///   constraints: Constraints(requiresWifi: true),
     992              : /// );
     993              : /// ```
     994              : ///
     995              : /// ## GET Request for Data
     996              : ///
     997              : /// ```dart
     998              : /// // Fetch configuration from server
     999              : /// await NativeWorkManager.enqueue(
    1000              : ///   taskId: 'fetch-config',
    1001              : ///   trigger: TaskTrigger.periodic(Duration(hours: 12)),
    1002              : ///   worker: NativeWorker.httpSync(
    1003              : ///     url: 'https://api.example.com/config',
    1004              : ///     method: HttpMethod.get,
    1005              : ///     headers: {'Authorization': 'Bearer $token'},
    1006              : ///   ),
    1007              : /// );
    1008              : /// ```
    1009              : ///
    1010              : /// ## Parameters
    1011              : ///
    1012              : /// **[url]** *(required)* - The API endpoint URL.
    1013              : /// - Must start with `http://` or `https://`
    1014              : /// - Throws `ArgumentError` if empty or invalid
    1015              : ///
    1016              : /// **[method]** *(optional)* - HTTP method (default: POST).
    1017              : /// - `HttpMethod.post` - Most common for syncing
    1018              : /// - `HttpMethod.get` - Fetch data from server
    1019              : /// - `HttpMethod.put` - Update existing data
    1020              : /// - `HttpMethod.patch` - Partial update
    1021              : ///
    1022              : /// **[headers]** *(optional)* - HTTP headers (default: empty).
    1023              : /// - Content-Type automatically set to application/json
    1024              : /// - Add Authorization header for auth
    1025              : ///
    1026              : /// **[requestBody]** *(optional)* - JSON data to send (default: null).
    1027              : /// - Automatically JSON encoded
    1028              : /// - Can be Map or any JSON-serializable data
    1029              : /// - Null for GET requests
    1030              : ///
    1031              : /// **[timeout]** *(optional)* - Request timeout (default: 60 seconds).
    1032              : /// - Increase for slow APIs or large payloads
    1033              : /// - Request fails if timeout exceeded
    1034              : ///
    1035              : /// ## Behavior
    1036              : ///
    1037              : /// - Automatically JSON encodes requestBody
    1038              : /// - Sets Content-Type to application/json
    1039              : /// - Expects JSON response from server
    1040              : /// - **Response is NOT returned** (fire-and-forget)
    1041              : /// - Task succeeds if HTTP status 200-299
    1042              : /// - Task fails on network error or non-2xx status
    1043              : ///
    1044              : /// ## When to Use
    1045              : ///
    1046              : /// ✅ **Use httpSync when:**
    1047              : /// - Syncing local data to server
    1048              : /// - Sending batch analytics events
    1049              : /// - Periodic data uploads
    1050              : /// - Fire-and-forget API calls with JSON
    1051              : ///
    1052              : /// ❌ **Don't use httpSync when:**
    1053              : /// - You need to process the response → Use `DartWorker`
    1054              : /// - Uploading files → Use `httpUpload`
    1055              : /// - Simple ping without body → Use `httpRequest`
    1056              : ///
    1057              : /// ## Important Limitation
    1058              : ///
    1059              : /// **The response is NOT available in Dart code.** This worker is designed
    1060              : /// for fire-and-forget operations. If you need the response data:
    1061              : ///
    1062              : /// ```dart
    1063              : /// // ❌ Won't work - response is not returned
    1064              : /// NativeWorker.httpSync(url: '...');
    1065              : ///
    1066              : /// // ✅ Use DartWorker instead
    1067              : /// DartWorker(
    1068              : ///   callbackId: 'processSync',
    1069              : ///   // In callback: make HTTP call, process response, save to DB
    1070              : /// );
    1071              : /// ```
    1072              : ///
    1073              : /// ## Common Pitfalls
    1074              : ///
    1075              : /// ❌ **Don't** expect to receive the response
    1076              : /// ❌ **Don't** use for uploading files (use `httpUpload`)
    1077              : /// ❌ **Don't** forget to set Authorization header
    1078              : /// ✅ **Do** use for periodic data syncing
    1079              : /// ✅ **Do** use network constraints
    1080              : /// ✅ **Do** handle task failure gracefully
    1081              : ///
    1082              : /// ## Platform Notes
    1083              : ///
    1084              : /// **Android:** Uses OkHttp with JSON request/response
    1085              : /// **iOS:** Uses URLSession with JSONSerialization
    1086              : ///
    1087              : /// ## See Also
    1088              : ///
    1089              : /// - [NativeWorker.httpRequest] - Simple HTTP requests (no JSON encoding)
    1090              : /// - [NativeWorker.httpUpload] - Upload files
    1091              : /// - [DartWorker] - For processing responses
    1092            7 : Worker _buildHttpSync({
    1093              :   required String url,
    1094              :   HttpMethod method = HttpMethod.post,
    1095              :   Map<String, String> headers = const {},
    1096              :   Map<String, dynamic>? requestBody,
    1097              :   Duration timeout = const Duration(seconds: 60),
    1098              :   TokenRefreshConfig? tokenRefresh,
    1099              :   RequestSigning? requestSigning,
    1100              : }) {
    1101            7 :   NativeWorker._validateUrl(url);
    1102              : 
    1103           14 :   if (timeout.inMinutes > 5) {
    1104            2 :     throw ArgumentError(
    1105            1 :       'Sync timeout too long: ${timeout.inMinutes} minutes\n'
    1106              :       'iOS limits background tasks to 30 seconds\n'
    1107              :       'Android may defer long requests in Doze mode\n'
    1108              :       'Recommended: Keep under 5 minutes for API sync operations\n'
    1109            1 :       'Current timeout: ${timeout.inSeconds} seconds',
    1110              :     );
    1111              :   }
    1112              : 
    1113            7 :   return HttpSyncWorker(
    1114              :     url: url,
    1115              :     method: method,
    1116              :     headers: headers,
    1117              :     requestBody: requestBody,
    1118              :     timeout: timeout,
    1119              :     tokenRefresh: tokenRefresh,
    1120              :     requestSigning: requestSigning,
    1121              :   );
    1122              : }
        

Generated by: LCOV version 2.4-0