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

            Line data    Source code
       1              : import 'package:flutter/foundation.dart';
       2              : import '../worker.dart';
       3              : 
       4              : export 'request_signing.dart';
       5              : 
       6              : /// What to do if the destination file already exists.
       7              : enum DuplicatePolicy {
       8              :   /// Overwrite the existing file (default).
       9              :   overwrite,
      10              : 
      11              :   /// Rename the new file to avoid collision (e.g. `file_1.zip`).
      12              :   rename,
      13              : 
      14              :   /// Skip the download entirely and report success.
      15              :   skip,
      16              : }
      17              : 
      18              : /// HTTP download worker configuration.
      19              : ///
      20              : /// Supports automatic resume from last downloaded byte on network failure,
      21              : /// and optional checksum verification for download integrity.
      22              : @immutable
      23              : final class HttpDownloadWorker extends Worker {
      24           14 :   const HttpDownloadWorker({
      25              :     required this.url,
      26              :     required this.savePath,
      27              :     this.headers = const {},
      28              :     this.timeout = const Duration(minutes: 5),
      29              :     this.enableResume = true,
      30              :     this.expectedChecksum,
      31              :     this.checksumAlgorithm = 'SHA-256',
      32              :     this.useBackgroundSession = false,
      33              :     this.showNotification = false,
      34              :     this.notificationTitle,
      35              :     this.notificationBody,
      36              :     this.skipExisting = false,
      37              :     this.allowPause = false,
      38              :     this.cookies,
      39              :     this.authToken,
      40              :     this.authHeaderTemplate = 'Bearer {accessToken}',
      41              :     this.onDuplicate = DuplicatePolicy.overwrite,
      42              :     this.moveToPublicDownloads = false,
      43              :     this.saveToGallery = false,
      44              :     this.extractAfterDownload = false,
      45              :     this.extractPath,
      46              :     this.deleteArchiveAfterExtract = false,
      47              :     this.bandwidthLimitBytesPerSecond,
      48              :     this.requestSigning,
      49              :     this.tokenRefresh,
      50              :   });
      51              : 
      52              :   /// The URL to download from.
      53              :   final String url;
      54              : 
      55              :   /// Where to save the downloaded file (absolute path).
      56              :   final String savePath;
      57              : 
      58              :   /// Optional HTTP headers to include in the request.
      59              :   final Map<String, String> headers;
      60              : 
      61              :   /// Request timeout per attempt (default: 5 minutes).
      62              :   ///
      63              :   /// NET-027: This is a **per-attempt** timeout. When WorkManager retries a
      64              :   /// failed task it resets the clock, so the total time across all retries can
      65              :   /// exceed this value.  To bound the total retry window, configure a deadline
      66              :   /// or max-retry count on the [TaskTrigger] instead.
      67              :   final Duration timeout;
      68              : 
      69              :   /// Enable automatic resume from last downloaded byte on network failure.
      70              :   ///
      71              :   /// When enabled (default), if the download is interrupted, the next attempt
      72              :   /// will resume from the last successfully downloaded byte using HTTP Range
      73              :   /// requests. The server must support Range requests (returns 206 Partial Content).
      74              :   /// Falls back to full download if server doesn't support resume.
      75              :   ///
      76              :   /// Default: `true`
      77              :   final bool enableResume;
      78              : 
      79              :   /// Expected checksum for download verification (optional).
      80              :   ///
      81              :   /// If provided, the downloaded file will be verified against this checksum
      82              :   /// after download completes. The download fails if checksums don't match.
      83              :   ///
      84              :   /// Example: `"a3b2c1d4e5f6..."` (hexadecimal string)
      85              :   ///
      86              :   /// Use with [checksumAlgorithm] to specify the hashing algorithm.
      87              :   final String? expectedChecksum;
      88              : 
      89              :   /// Checksum algorithm for verification.
      90              :   ///
      91              :   /// Supported algorithms:
      92              :   /// - `'MD5'` - Fast but not cryptographically secure
      93              :   /// - `'SHA-1'` - 160-bit hash (deprecated for security)
      94              :   /// - `'SHA-256'` - 256-bit hash (recommended, default)
      95              :   /// - `'SHA-512'` - 512-bit hash (most secure)
      96              :   ///
      97              :   /// Default: `'SHA-256'`
      98              :   final String checksumAlgorithm;
      99              : 
     100              :   /// Show a system notification with download progress.
     101              :   ///
     102              :   /// When `true`, the native side shows a persistent notification while the
     103              :   /// download is in progress and a completion/failure notification when done.
     104              :   ///
     105              :   /// **Android:** Uses a low-priority notification channel with a Cancel button.
     106              :   /// Requires `POST_NOTIFICATIONS` permission on Android 13+ (API 33+).
     107              :   ///
     108              :   /// **iOS:** Uses `UNUserNotificationCenter`. Progress updates are best-effort
     109              :   /// (iOS suspends the app during background downloads). The completion
     110              :   /// notification is always shown.
     111              :   ///
     112              :   /// Default: `false`
     113              :   final bool showNotification;
     114              : 
     115              :   /// Title for the progress notification.
     116              :   ///
     117              :   /// Defaults to the file name derived from [url] on the native side.
     118              :   final String? notificationTitle;
     119              : 
     120              :   /// Body text for the progress notification.
     121              :   ///
     122              :   /// Defaults to the download URL on the native side.
     123              :   final String? notificationBody;
     124              : 
     125              :   /// Use background URLSession for downloads (iOS only).
     126              :   ///
     127              :   /// **v2.3.0+ iOS Feature:**
     128              :   /// When enabled, downloads use `URLSessionConfiguration.background` which:
     129              :   /// - **Survives app termination** - Downloads continue even if app is killed
     130              :   /// - **No time limits** - Can download for hours (vs 30s foreground limit)
     131              :   /// - **System-managed** - OS handles network changes and retries
     132              :   /// - **Battery efficient** - OS schedules transfers optimally
     133              :   ///
     134              :   /// **Android:**
     135              :   /// This parameter has no effect on Android. WorkManager already handles
     136              :   /// background downloads robustly without special configuration.
     137              :   ///
     138              :   /// **When to use:**
     139              :   /// - ✅ Large files (>10MB) that may take minutes to download
     140              :   /// - ✅ Downloads that must complete even if user force-quits app
     141              :   /// - ✅ Downloads on unreliable networks (automatic retry)
     142              :   /// - ❌ Small files (<1MB) - foreground session is faster
     143              :   /// - ❌ Immediate downloads that finish in seconds
     144              :   ///
     145              :   /// Example:
     146              :   /// ```dart
     147              :   /// // Large app update download (survives app termination)
     148              :   /// worker: NativeWorker.httpDownload(
     149              :   ///   url: 'https://cdn.example.com/app-v2.0.0.apk',
     150              :   ///   savePath: '/downloads/update.apk',
     151              :   ///   useBackgroundSession: true,  // 🚀 Survives termination
     152              :   /// ),
     153              :   /// ```
     154              :   ///
     155              :   /// Default: `false` (backward compatible with existing code)
     156              :   final bool useBackgroundSession;
     157              : 
     158              :   /// Skip the download if the destination file already exists.
     159              :   ///
     160              :   /// When `true`, the worker checks whether [savePath] already exists on disk.
     161              :   /// If it does, the task reports success immediately without issuing any HTTP
     162              :   /// request.  This is useful for incremental download managers or caches
     163              :   /// where a previously completed download should not be repeated.
     164              :   ///
     165              :   /// When `false` (default), the download always proceeds and overwrites any
     166              :   /// existing file.
     167              :   ///
     168              :   /// Default: `false`
     169              :   final bool skipExisting;
     170              : 
     171              :   /// Allow the task to be paused via [NativeWorkManager.pause]. When false
     172              :   /// (default), the Pause button is hidden from the download notification.
     173              :   final bool allowPause;
     174              : 
     175              :   /// HTTP cookies to include in the download request. Keys are cookie names,
     176              :   /// values are cookie values.
     177              :   final Map<String, String>? cookies;
     178              : 
     179              :   /// Auth token for the download request. Injected into the header as
     180              :   /// [authHeaderTemplate] with `{accessToken}` replaced by this value.
     181              :   final String? authToken;
     182              : 
     183              :   /// Template for the `Authorization` header value.
     184              :   ///
     185              :   /// `{accessToken}` is replaced with [authToken] at request time.
     186              :   /// Default: `"Bearer {accessToken}"`
     187              :   final String authHeaderTemplate;
     188              : 
     189              :   /// What to do if the destination file already exists.
     190              :   final DuplicatePolicy onDuplicate;
     191              : 
     192              :   /// Move the completed download into the public Downloads folder.
     193              :   ///
     194              :   /// NET-024: Post-processing is **best-effort**. If moving to the public
     195              :   /// Downloads folder fails (e.g. missing permission, storage full), the main
     196              :   /// task still reports success and the file remains in its original location.
     197              :   /// The native log will contain a warning. Check [TaskEvent.resultData] for the
     198              :   /// final `filePath` to know where the file landed.
     199              :   final bool moveToPublicDownloads;
     200              : 
     201              :   /// Save the completed download to the device gallery (images/videos).
     202              :   ///
     203              :   /// NET-024: Same best-effort semantics as [moveToPublicDownloads] — failure
     204              :   /// does not fail the task; the original file is preserved.
     205              :   final bool saveToGallery;
     206              : 
     207              :   /// Automatically extract the downloaded archive after a successful download.
     208              :   final bool extractAfterDownload;
     209              : 
     210              :   /// Directory to extract the archive into. Defaults to the directory
     211              :   /// containing [savePath] when null.
     212              :   final String? extractPath;
     213              : 
     214              :   /// Delete the archive file after successful extraction.
     215              :   final bool deleteArchiveAfterExtract;
     216              : 
     217              :   /// Maximum download speed in bytes per second.
     218              :   ///
     219              :   /// When set, the download stream is throttled to this rate using a token-bucket
     220              :   /// algorithm. Useful for limiting bandwidth consumption on metered connections.
     221              :   ///
     222              :   /// **Android:** applied via OkHttp response-body wrapping (effective immediately).
     223              :   /// **iOS:** applied via streaming download on iOS 15+; ignored on iOS 14 (downloads
     224              :   /// proceed at full speed — no error is raised).
     225              :   ///
     226              :   /// Example: `500 * 1024` for 500 KB/s.
     227              :   ///
     228              :   /// Default: `null` (no limit).
     229              :   final int? bandwidthLimitBytesPerSecond;
     230              : 
     231              :   /// HMAC-SHA256 request signing configuration.
     232              :   ///
     233              :   /// When set, each download request is signed with the specified secret key
     234              :   /// and the signature is injected as a request header (default: `X-Signature`).
     235              :   /// An `X-Timestamp` header is also added when [RequestSigning.includeTimestamp] is true.
     236              :   ///
     237              :   /// Default: `null` (no signing).
     238              :   final RequestSigning? requestSigning;
     239              : 
     240              :   /// Automatic token refresh configuration.
     241              :   final TokenRefreshConfig? tokenRefresh;
     242              : 
     243              :   // ═══════════════════════════════════════════════════════════════════════════
     244              :   // BUILDER-STYLE copyWith — avoids parameter explosion at call sites
     245              :   // ═══════════════════════════════════════════════════════════════════════════
     246              : 
     247              :   /// Returns a copy of this worker with the given fields replaced.
     248              :   ///
     249              :   /// Enables a fluent builder-style API without a separate builder class:
     250              :   ///
     251              :   /// ```dart
     252              :   /// final base = HttpDownloadWorker(url: '...', savePath: '...');
     253              :   ///
     254              :   /// final withNotification = base.copyWith(
     255              :   ///   showNotification: true,
     256              :   ///   notificationTitle: 'Downloading update…',
     257              :   ///   allowPause: true,
     258              :   /// );
     259              :   ///
     260              :   /// final withResume = withNotification.copyWith(
     261              :   ///   enableResume: true,
     262              :   ///   onDuplicate: DuplicatePolicy.rename,
     263              :   /// );
     264              :   /// ```
     265            1 :   HttpDownloadWorker copyWith({
     266              :     String? url,
     267              :     String? savePath,
     268              :     Map<String, String>? headers,
     269              :     Duration? timeout,
     270              :     bool? enableResume,
     271              :     String? expectedChecksum,
     272              :     String? checksumAlgorithm,
     273              :     bool? useBackgroundSession,
     274              :     bool? showNotification,
     275              :     String? notificationTitle,
     276              :     String? notificationBody,
     277              :     bool? skipExisting,
     278              :     bool? allowPause,
     279              :     Map<String, String>? cookies,
     280              :     String? authToken,
     281              :     String? authHeaderTemplate,
     282              :     DuplicatePolicy? onDuplicate,
     283              :     bool? moveToPublicDownloads,
     284              :     bool? saveToGallery,
     285              :     bool? extractAfterDownload,
     286              :     String? extractPath,
     287              :     bool? deleteArchiveAfterExtract,
     288              :     int? bandwidthLimitBytesPerSecond,
     289              :     RequestSigning? requestSigning,
     290              :   }) {
     291            1 :     return HttpDownloadWorker(
     292            1 :       url: url ?? this.url,
     293            1 :       savePath: savePath ?? this.savePath,
     294            1 :       headers: headers ?? this.headers,
     295            1 :       timeout: timeout ?? this.timeout,
     296            1 :       enableResume: enableResume ?? this.enableResume,
     297            1 :       expectedChecksum: expectedChecksum ?? this.expectedChecksum,
     298            1 :       checksumAlgorithm: checksumAlgorithm ?? this.checksumAlgorithm,
     299            1 :       useBackgroundSession: useBackgroundSession ?? this.useBackgroundSession,
     300            1 :       showNotification: showNotification ?? this.showNotification,
     301            1 :       notificationTitle: notificationTitle ?? this.notificationTitle,
     302            1 :       notificationBody: notificationBody ?? this.notificationBody,
     303            1 :       skipExisting: skipExisting ?? this.skipExisting,
     304            1 :       allowPause: allowPause ?? this.allowPause,
     305            1 :       cookies: cookies ?? this.cookies,
     306            1 :       authToken: authToken ?? this.authToken,
     307            1 :       authHeaderTemplate: authHeaderTemplate ?? this.authHeaderTemplate,
     308            1 :       onDuplicate: onDuplicate ?? this.onDuplicate,
     309              :       moveToPublicDownloads:
     310            1 :           moveToPublicDownloads ?? this.moveToPublicDownloads,
     311            1 :       saveToGallery: saveToGallery ?? this.saveToGallery,
     312            1 :       extractAfterDownload: extractAfterDownload ?? this.extractAfterDownload,
     313            1 :       extractPath: extractPath ?? this.extractPath,
     314              :       deleteArchiveAfterExtract:
     315            1 :           deleteArchiveAfterExtract ?? this.deleteArchiveAfterExtract,
     316              :       bandwidthLimitBytesPerSecond:
     317            1 :           bandwidthLimitBytesPerSecond ?? this.bandwidthLimitBytesPerSecond,
     318            1 :       requestSigning: requestSigning ?? this.requestSigning,
     319              :     );
     320              :   }
     321              : 
     322              :   /// Convenience: enable notification with sensible defaults.
     323              :   ///
     324              :   /// ```dart
     325              :   /// worker.withNotification(title: 'Downloading...', allowPause: true)
     326              :   /// ```
     327            1 :   HttpDownloadWorker withNotification({
     328              :     String? title,
     329              :     String? body,
     330              :     bool allowPause = false,
     331              :   }) =>
     332            1 :       copyWith(
     333              :         showNotification: true,
     334              :         notificationTitle: title,
     335              :         notificationBody: body,
     336              :         allowPause: allowPause,
     337              :       );
     338              : 
     339              :   /// Convenience: set authentication token.
     340              :   ///
     341              :   /// ```dart
     342              :   /// worker.withAuth(token: myToken)
     343              :   /// worker.withAuth(token: myApiKey, template: 'ApiKey {accessToken}')
     344              :   /// ```
     345            1 :   HttpDownloadWorker withAuth({
     346              :     required String token,
     347              :     String template = 'Bearer {accessToken}',
     348              :   }) =>
     349            1 :       copyWith(authToken: token, authHeaderTemplate: template);
     350              : 
     351              :   /// Convenience: enable resume + skip-existing policy.
     352            2 :   HttpDownloadWorker withResume({bool skipIfExists = false}) => copyWith(
     353              :         enableResume: true,
     354              :         skipExisting: skipIfExists,
     355              :       );
     356              : 
     357              :   /// Convenience: verify download integrity with a checksum.
     358              :   ///
     359              :   /// ```dart
     360              :   /// worker.withChecksum(expected: sha256Hex)  // defaults to SHA-256
     361              :   /// worker.withChecksum(expected: md5Hex, algorithm: 'MD5')
     362              :   /// ```
     363            1 :   HttpDownloadWorker withChecksum({
     364              :     required String expected,
     365              :     String algorithm = 'SHA-256',
     366              :   }) =>
     367            1 :       copyWith(expectedChecksum: expected, checksumAlgorithm: algorithm);
     368              : 
     369              :   /// Convenience: limit download speed.
     370              :   ///
     371              :   /// ```dart
     372              :   /// worker.withBandwidthLimit(500 * 1024)  // 500 KB/s
     373              :   /// ```
     374            1 :   HttpDownloadWorker withBandwidthLimit(int bytesPerSecond) =>
     375            1 :       copyWith(bandwidthLimitBytesPerSecond: bytesPerSecond);
     376              : 
     377              :   /// Convenience: sign requests with HMAC-SHA256.
     378              :   ///
     379              :   /// ```dart
     380              :   /// worker.withSigning(RequestSigning(secretKey: mySecret))
     381              :   /// ```
     382            1 :   HttpDownloadWorker withSigning(RequestSigning signing) =>
     383            1 :       copyWith(requestSigning: signing);
     384              : 
     385            5 :   @override
     386              :   String get workerClassName => 'HttpDownloadWorker';
     387              : 
     388            7 :   @override
     389            7 :   Map<String, dynamic> toMap() => {
     390            7 :         'workerType': 'httpDownload',
     391           14 :         'url': url,
     392           14 :         'savePath': savePath,
     393           14 :         'headers': headers,
     394           21 :         'timeoutMs': timeout.inMilliseconds,
     395           14 :         'enableResume': enableResume,
     396           11 :         if (expectedChecksum != null) 'expectedChecksum': expectedChecksum,
     397           14 :         'checksumAlgorithm': checksumAlgorithm,
     398           14 :         'useBackgroundSession': useBackgroundSession,
     399           14 :         'skipExisting': skipExisting,
     400           14 :         'showNotification': showNotification,
     401            7 :         if (notificationTitle != null) 'notificationTitle': notificationTitle,
     402            7 :         if (notificationBody != null) 'notificationBody': notificationBody,
     403           14 :         'allowPause': allowPause,
     404            7 :         if (cookies != null) 'cookies': cookies,
     405            7 :         if (authToken != null) 'authToken': authToken,
     406           14 :         'authHeaderTemplate': authHeaderTemplate,
     407           21 :         'onDuplicate': onDuplicate.name,
     408           14 :         'moveToPublicDownloads': moveToPublicDownloads,
     409           14 :         'saveToGallery': saveToGallery,
     410           14 :         'extractAfterDownload': extractAfterDownload,
     411            7 :         if (extractPath != null) 'extractPath': extractPath,
     412           14 :         'deleteArchiveAfterExtract': deleteArchiveAfterExtract,
     413            7 :         if (bandwidthLimitBytesPerSecond != null)
     414            0 :           'bandwidthLimitBytesPerSecond': bandwidthLimitBytesPerSecond,
     415           10 :         if (requestSigning != null) 'requestSigning': requestSigning!.toMap(),
     416              :       };
     417              : }
        

Generated by: LCOV version 2.4-0