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 : }
|