@kesha-antonov/react-native-background-downloader 4.5.3 → 4.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -8
- package/android/build.gradle +4 -2
- package/android/src/main/java/com/eko/DownloadConstants.kt +3 -0
- package/android/src/main/java/com/eko/Downloader.kt +102 -0
- package/android/src/main/java/com/eko/RNBackgroundDownloaderModuleImpl.kt +86 -5
- package/android/src/main/java/com/eko/ResumableDownloader.kt +7 -0
- package/android/src/main/java/com/eko/UIDTDownloadJobService.kt +42 -4
- package/android/src/main/java/com/eko/uidt/UIDTJobManager.kt +11 -1
- package/android/src/main/java/com/eko/uidt/UIDTJobState.kt +62 -0
- package/android/src/main/java/com/eko/utils/StorageManager.kt +95 -0
- package/ios/RNBGDTaskConfig.h +3 -0
- package/ios/RNBGDTaskConfig.mm +3 -0
- package/ios/RNBackgroundDownloader.mm +375 -48
- package/lib/DownloadTask.d.ts +39 -0
- package/lib/DownloadTask.js +176 -0
- package/lib/NativeRNBackgroundDownloader.d.ts +127 -0
- package/lib/NativeRNBackgroundDownloader.js +4 -0
- package/lib/UploadTask.d.ts +30 -0
- package/lib/UploadTask.js +174 -0
- package/lib/config.d.ts +33 -0
- package/lib/config.js +79 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +498 -0
- package/lib/types.d.ts +249 -0
- package/lib/types.js +2 -0
- package/package.json +7 -4
- package/plugin/build/index.d.ts +2 -2
- package/plugin/build/index.js +1 -1
- package/react-native-background-downloader.podspec +8 -2
- package/src/DownloadTask.ts +2 -0
- package/src/NativeRNBackgroundDownloader.ts +2 -0
- package/src/config.ts +6 -1
- package/src/index.ts +4 -0
- package/src/types.ts +27 -0
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
#define ID_TO_UPLOAD_CONFIG_MAP_KEY @"com.eko.bguploadidmap"
|
|
14
14
|
#define PROGRESS_INTERVAL_KEY @"progressInterval"
|
|
15
15
|
#define PROGRESS_MIN_BYTES_KEY @"progressMinBytes"
|
|
16
|
+
// Persisted map of downloads whose finished file could not be moved to its destination
|
|
17
|
+
// yet because the device was locked (Data Protection). The move is retried when protected
|
|
18
|
+
// data becomes available / on next launch. See issue #101.
|
|
19
|
+
#define PENDING_MOVES_MAP_KEY @"com.eko.bgdpendingmoves"
|
|
16
20
|
|
|
17
21
|
// Session configuration constants
|
|
18
22
|
static const NSInteger kMaxConnectionsPerHost = 4;
|
|
@@ -66,6 +70,18 @@ static CompletionHandler storedCompletionHandler;
|
|
|
66
70
|
// Controls whether debug logs are sent to JS
|
|
67
71
|
BOOL isLogsEnabled;
|
|
68
72
|
|
|
73
|
+
// Downloads that finished but whose file couldn't be moved to its destination because
|
|
74
|
+
// the device was locked. Keyed by configId -> staging/destination info. Retried on
|
|
75
|
+
// UIApplicationProtectedDataDidBecomeAvailable, app foreground, and launch. See #101.
|
|
76
|
+
NSMutableDictionary<NSString *, NSDictionary *> *pendingMoves;
|
|
77
|
+
|
|
78
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
79
|
+
// Queue of events that arrived before the TurboModule event emitter callback was set.
|
|
80
|
+
// This prevents crashes (std::bad_function_call / SIGABRT) when NSURLSession delegate
|
|
81
|
+
// callbacks fire before JS has registered event listeners.
|
|
82
|
+
NSMutableArray<NSDictionary *> *pendingEmitEvents;
|
|
83
|
+
#endif
|
|
84
|
+
|
|
69
85
|
// Upload-specific instance variables
|
|
70
86
|
NSMutableDictionary<NSNumber *, RNBGDUploadTaskConfig *> *uploadTaskToConfigMap;
|
|
71
87
|
NSMutableDictionary<NSString *, NSURLSessionUploadTask *> *idToUploadTaskMap;
|
|
@@ -233,6 +249,15 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
233
249
|
isSessionActivated = NO;
|
|
234
250
|
pendingDownloads = [[NSMutableArray alloc] init];
|
|
235
251
|
|
|
252
|
+
// Restore any downloads whose file move was deferred because the device was locked
|
|
253
|
+
NSData *pendingMovesData = [mmkv getDataForKey:PENDING_MOVES_MAP_KEY];
|
|
254
|
+
NSMutableDictionary *pendingMovesDecoded = pendingMovesData != nil ? [self deserializePendingMoves:pendingMovesData] : nil;
|
|
255
|
+
pendingMoves = pendingMovesDecoded != nil ? pendingMovesDecoded : [[NSMutableDictionary alloc] init];
|
|
256
|
+
|
|
257
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
258
|
+
pendingEmitEvents = [[NSMutableArray alloc] init];
|
|
259
|
+
#endif
|
|
260
|
+
|
|
236
261
|
// Initialize upload-specific data structures
|
|
237
262
|
NSData *uploadTaskToConfigMapData = [mmkv getDataForKey:ID_TO_UPLOAD_CONFIG_MAP_KEY];
|
|
238
263
|
NSMutableDictionary *uploadTaskToConfigMapDataDefault = [[NSMutableDictionary alloc] init];
|
|
@@ -249,6 +274,10 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
249
274
|
|
|
250
275
|
// Initialize session early to receive background events on app relaunch
|
|
251
276
|
[self lazyRegisterSession];
|
|
277
|
+
|
|
278
|
+
// Retry any moves that were deferred while the device was locked (e.g. a download
|
|
279
|
+
// that finished in the background before the first unlock). See #101.
|
|
280
|
+
[self processPendingMoves];
|
|
252
281
|
}
|
|
253
282
|
|
|
254
283
|
return self;
|
|
@@ -330,10 +359,23 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
330
359
|
selector:@selector(handleBridgeHotReload:)
|
|
331
360
|
name:RCTJavaScriptWillStartLoadingNotification
|
|
332
361
|
object:nil];
|
|
362
|
+
|
|
363
|
+
// Finish any moves deferred while the device was locked, as soon as
|
|
364
|
+
// protected data becomes available again (device unlocked). See #101.
|
|
365
|
+
// Note: the ObjC constant has no "Notification" suffix (unlike most UIApplication ones).
|
|
366
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
367
|
+
selector:@selector(handleProtectedDataAvailable:)
|
|
368
|
+
name:UIApplicationProtectedDataDidBecomeAvailable
|
|
369
|
+
object:nil];
|
|
333
370
|
}
|
|
334
371
|
}
|
|
335
372
|
}
|
|
336
373
|
|
|
374
|
+
- (void)handleProtectedDataAvailable:(NSNotification *) note {
|
|
375
|
+
DLog(nil, @"[RNBackgroundDownloader] - [handleProtectedDataAvailable]");
|
|
376
|
+
[self processPendingMoves];
|
|
377
|
+
}
|
|
378
|
+
|
|
337
379
|
- (void)unregisterBridgeListener {
|
|
338
380
|
DLog(nil, @"[RNBackgroundDownloader] - [unregisterBridgeListener]");
|
|
339
381
|
if (isBridgeListenerInited == YES) {
|
|
@@ -345,6 +387,8 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
345
387
|
- (void)handleBridgeAppEnterForeground:(NSNotification *) note {
|
|
346
388
|
DLog(nil, @"[RNBackgroundDownloader] - [handleBridgeAppEnterForeground]");
|
|
347
389
|
[self resumeTasks];
|
|
390
|
+
// Device is unlocked when foregrounded, so finish any locked-deferred file moves.
|
|
391
|
+
[self processPendingMoves];
|
|
348
392
|
}
|
|
349
393
|
|
|
350
394
|
- (void)resumeTasks {
|
|
@@ -403,15 +447,24 @@ RCT_EXPORT_METHOD(setLogsEnabled:(BOOL)enabled) {
|
|
|
403
447
|
|
|
404
448
|
- (void)_setMaxParallelDownloadsInternal:(NSInteger)max {
|
|
405
449
|
DLog(nil, @"[RNBackgroundDownloader] - [setMaxParallelDownloads:%ld]", (long)max);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
450
|
+
// Never let an NSException escape this void method. methodQueue is a custom
|
|
451
|
+
// background queue, and on the new architecture the TurboModule bridge would
|
|
452
|
+
// convert an escaping exception into a JS error via Hermes JSI on this thread
|
|
453
|
+
// (not the JS thread). Hermes is not thread-safe, so that crashes with SIGSEGV.
|
|
454
|
+
// See https://github.com/kesha-antonov/react-native-background-downloader/issues/161
|
|
455
|
+
@try {
|
|
456
|
+
@synchronized (sharedLock) {
|
|
457
|
+
if (max >= 1) {
|
|
458
|
+
sessionConfig.HTTPMaximumConnectionsPerHost = max;
|
|
459
|
+
// Recreate session with new config if it's already initialized
|
|
460
|
+
if (urlSession != nil) {
|
|
461
|
+
[self unregisterSession];
|
|
462
|
+
[self lazyRegisterSession];
|
|
463
|
+
}
|
|
413
464
|
}
|
|
414
465
|
}
|
|
466
|
+
} @catch (NSException *exception) {
|
|
467
|
+
DLog(nil, @"[RNBackgroundDownloader] - [setMaxParallelDownloads] error: %@", exception.reason);
|
|
415
468
|
}
|
|
416
469
|
}
|
|
417
470
|
|
|
@@ -427,13 +480,20 @@ RCT_EXPORT_METHOD(setMaxParallelDownloads:(NSInteger)max) {
|
|
|
427
480
|
|
|
428
481
|
- (void)_setAllowsCellularAccessInternal:(BOOL)allows {
|
|
429
482
|
DLog(nil, @"[RNBackgroundDownloader] - [setAllowsCellularAccess:%@]", allows ? @"YES" : @"NO");
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
483
|
+
// Never let an NSException escape this void method - see the note on
|
|
484
|
+
// -_setMaxParallelDownloadsInternal: and issue #161. Recreating the session
|
|
485
|
+
// here can throw, and an escaping exception crashes Hermes off the JS thread.
|
|
486
|
+
@try {
|
|
487
|
+
@synchronized (sharedLock) {
|
|
488
|
+
sessionConfig.allowsCellularAccess = allows;
|
|
489
|
+
// Recreate session with new config if it's already initialized
|
|
490
|
+
if (urlSession != nil) {
|
|
491
|
+
[self unregisterSession];
|
|
492
|
+
[self lazyRegisterSession];
|
|
493
|
+
}
|
|
436
494
|
}
|
|
495
|
+
} @catch (NSException *exception) {
|
|
496
|
+
DLog(nil, @"[RNBackgroundDownloader] - [setAllowsCellularAccess] error: %@", exception.reason);
|
|
437
497
|
}
|
|
438
498
|
}
|
|
439
499
|
|
|
@@ -455,6 +515,7 @@ RCT_EXPORT_METHOD(setAllowsCellularAccess:(BOOL)allows) {
|
|
|
455
515
|
NSString *destination = options.destination();
|
|
456
516
|
NSString *metadata = options.metadata() ? options.metadata() : @"";
|
|
457
517
|
NSDictionary *headers = options.headers() ? (NSDictionary *)options.headers() : nil;
|
|
518
|
+
NSString *dataProtection = options.iosDataProtection() ? options.iosDataProtection() : nil;
|
|
458
519
|
|
|
459
520
|
if (options.progressInterval().has_value()) {
|
|
460
521
|
progressInterval = options.progressInterval().value() / 1000.0;
|
|
@@ -475,6 +536,7 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
475
536
|
NSString *destination = options[@"destination"];
|
|
476
537
|
NSString *metadata = options[@"metadata"] ?: @"";
|
|
477
538
|
NSDictionary *headers = options[@"headers"];
|
|
539
|
+
NSString *dataProtection = options[@"iosDataProtection"];
|
|
478
540
|
|
|
479
541
|
NSNumber *progressIntervalScope = options[@"progressInterval"];
|
|
480
542
|
if (progressIntervalScope) {
|
|
@@ -511,38 +573,47 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
511
573
|
}
|
|
512
574
|
}
|
|
513
575
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
576
|
+
// download is a void method dispatched on the custom background methodQueue.
|
|
577
|
+
// Any NSException that escapes would be converted to a JS error via Hermes JSI
|
|
578
|
+
// on this thread (not the JS thread) and crash with SIGSEGV (issue #161), so we
|
|
579
|
+
// never let one propagate out of this method.
|
|
580
|
+
@try {
|
|
581
|
+
@synchronized (sharedLock) {
|
|
582
|
+
[self sendDebugLog:@"download: calling lazyRegisterSession" taskId:identifier];
|
|
583
|
+
[self lazyRegisterSession];
|
|
517
584
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
585
|
+
// If session is not yet activated, queue the download to be executed after activation
|
|
586
|
+
// This fixes the issue where downloads don't start on fresh app installs
|
|
587
|
+
if (!isSessionActivated) {
|
|
588
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [download] session not activated, queueing download");
|
|
589
|
+
[self sendDebugLog:@"download: session not activated, queueing download" taskId:identifier];
|
|
590
|
+
__weak RNBackgroundDownloader *weakSelf = self;
|
|
591
|
+
dispatch_block_t downloadBlock = ^{
|
|
592
|
+
RNBackgroundDownloader *strongSelf = weakSelf;
|
|
593
|
+
if (strongSelf) {
|
|
594
|
+
[strongSelf executeDownloadWithRequest:request identifier:identifier url:url destination:destination metadata:metadata dataProtection:dataProtection];
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
[pendingDownloads addObject:downloadBlock];
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
533
600
|
|
|
534
|
-
|
|
535
|
-
|
|
601
|
+
[self sendDebugLog:@"download: session activated, executing download" taskId:identifier];
|
|
602
|
+
[self executeDownloadWithRequest:request identifier:identifier url:url destination:destination metadata:metadata dataProtection:dataProtection];
|
|
603
|
+
}
|
|
604
|
+
} @catch (NSException *exception) {
|
|
605
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [download] error: %@", exception.reason);
|
|
606
|
+
[self sendDebugLog:[NSString stringWithFormat:@"download: ERROR - %@", exception.reason] taskId:identifier];
|
|
536
607
|
}
|
|
537
608
|
}
|
|
538
609
|
|
|
539
610
|
// Internal method to execute the download after session is activated
|
|
540
|
-
- (void)executeDownloadWithRequest:(NSMutableURLRequest *)request identifier:(NSString *)identifier url:(NSString *)url destination:(NSString *)destination metadata:(NSString *)metadata {
|
|
611
|
+
- (void)executeDownloadWithRequest:(NSMutableURLRequest *)request identifier:(NSString *)identifier url:(NSString *)url destination:(NSString *)destination metadata:(NSString *)metadata dataProtection:(NSString *)dataProtection {
|
|
541
612
|
@synchronized (sharedLock) {
|
|
542
613
|
DLog(identifier, @"[RNBackgroundDownloader] - [executeDownloadWithRequest]");
|
|
543
614
|
[self sendDebugLog:@"executeDownloadWithRequest: creating download task" taskId:identifier];
|
|
544
615
|
|
|
545
|
-
NSURLSessionDownloadTask __strong *task = [
|
|
616
|
+
NSURLSessionDownloadTask __strong *task = [self createDownloadTaskWithRequest:request identifier:identifier];
|
|
546
617
|
if (task == nil) {
|
|
547
618
|
DLog(identifier, @"[RNBackgroundDownloader] - [Error] failed to create download task");
|
|
548
619
|
[self sendDebugLog:@"executeDownloadWithRequest: ERROR - failed to create download task" taskId:identifier];
|
|
@@ -557,6 +628,8 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
557
628
|
@"destination": destination,
|
|
558
629
|
@"metadata": metadata
|
|
559
630
|
}];
|
|
631
|
+
// nil means "use the library default" when saving the file (see -nsFileProtectionFromKey:)
|
|
632
|
+
taskConfig.dataProtection = dataProtection;
|
|
560
633
|
|
|
561
634
|
taskToConfigMap[@(task.taskIdentifier)] = taskConfig;
|
|
562
635
|
[mmkv setData:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY];
|
|
@@ -571,6 +644,36 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
571
644
|
}
|
|
572
645
|
}
|
|
573
646
|
|
|
647
|
+
// Creates a download task on the current session. If the session has been
|
|
648
|
+
// invalidated (e.g. after a hot reload or a prior invalidateAndCancel),
|
|
649
|
+
// -downloadTaskWithRequest: raises:
|
|
650
|
+
// "attempted to create a NSURLSessionDownloadTask in a session that has
|
|
651
|
+
// been invalidated"
|
|
652
|
+
// In that case we recreate the background session and retry once.
|
|
653
|
+
// Must be called while holding sharedLock.
|
|
654
|
+
// See https://github.com/kesha-antonov/react-native-background-downloader/issues/157
|
|
655
|
+
- (NSURLSessionDownloadTask *)createDownloadTaskWithRequest:(NSMutableURLRequest *)request identifier:(NSString *)identifier {
|
|
656
|
+
@try {
|
|
657
|
+
return [urlSession downloadTaskWithRequest:request];
|
|
658
|
+
} @catch (NSException *exception) {
|
|
659
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [createDownloadTask] session invalidated, recreating: %@", exception.reason);
|
|
660
|
+
[self sendDebugLog:[NSString stringWithFormat:@"createDownloadTask: session invalidated, recreating (%@)", exception.reason] taskId:identifier];
|
|
661
|
+
|
|
662
|
+
// Recreate the background session directly (do not call unregisterSession,
|
|
663
|
+
// which would invalidateAndCancel and drop queued downloads).
|
|
664
|
+
urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
|
|
665
|
+
isSessionActivated = YES;
|
|
666
|
+
|
|
667
|
+
@try {
|
|
668
|
+
return [urlSession downloadTaskWithRequest:request];
|
|
669
|
+
} @catch (NSException *retryException) {
|
|
670
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [createDownloadTask] retry failed: %@", retryException.reason);
|
|
671
|
+
[self sendDebugLog:[NSString stringWithFormat:@"createDownloadTask: retry failed (%@)", retryException.reason] taskId:identifier];
|
|
672
|
+
return nil;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
574
677
|
- (void)pauseTaskInternal:(NSString *)identifier resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
575
678
|
DLog(identifier, @"[RNBackgroundDownloader] - [pauseTask]");
|
|
576
679
|
@try {
|
|
@@ -765,6 +868,15 @@ RCT_EXPORT_METHOD(resumeTask:(NSString *)id resolver:(RCTPromiseResolveBlock)res
|
|
|
765
868
|
[idsToPauseSet removeObject:identifier];
|
|
766
869
|
// Clean up any updated headers
|
|
767
870
|
[idToUpdatedHeadersMap removeObjectForKey:identifier];
|
|
871
|
+
// Drop any locked-deferred move staged for this task (#101)
|
|
872
|
+
NSDictionary *pendingMove = pendingMoves[identifier];
|
|
873
|
+
if (pendingMove != nil) {
|
|
874
|
+
NSString *stagingPath = pendingMove[@"staging"];
|
|
875
|
+
if (stagingPath != nil)
|
|
876
|
+
[[NSFileManager defaultManager] removeItemAtPath:stagingPath error:nil];
|
|
877
|
+
[pendingMoves removeObjectForKey:identifier];
|
|
878
|
+
[self persistPendingMoves];
|
|
879
|
+
}
|
|
768
880
|
}
|
|
769
881
|
resolve(nil);
|
|
770
882
|
} @catch (NSException *exception) {
|
|
@@ -1140,6 +1252,36 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1140
1252
|
}
|
|
1141
1253
|
}
|
|
1142
1254
|
|
|
1255
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1256
|
+
#pragma mark - Safe event emission (New Architecture)
|
|
1257
|
+
|
|
1258
|
+
// Safely emit an event, queuing it if the TurboModule event emitter callback
|
|
1259
|
+
// has not been registered yet. This prevents std::bad_function_call crashes
|
|
1260
|
+
// when NSURLSession delegate callbacks fire before JS has registered listeners
|
|
1261
|
+
// (e.g., background session delivering completions from a prior app session).
|
|
1262
|
+
- (void)safeEmitEvent:(NSString *)eventName value:(id)value {
|
|
1263
|
+
@synchronized (pendingEmitEvents) {
|
|
1264
|
+
if (_eventEmitterCallback) {
|
|
1265
|
+
_eventEmitterCallback(std::string([eventName UTF8String]), value);
|
|
1266
|
+
} else {
|
|
1267
|
+
[pendingEmitEvents addObject:@{@"name": eventName, @"value": value}];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper {
|
|
1273
|
+
@synchronized (pendingEmitEvents) {
|
|
1274
|
+
[super setEventEmitterCallback:eventEmitterCallbackWrapper];
|
|
1275
|
+
|
|
1276
|
+
// Flush any events that arrived before the callback was set
|
|
1277
|
+
for (NSDictionary *event in pendingEmitEvents) {
|
|
1278
|
+
_eventEmitterCallback(std::string([event[@"name"] UTF8String]), event[@"value"]);
|
|
1279
|
+
}
|
|
1280
|
+
[pendingEmitEvents removeAllObjects];
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
#endif
|
|
1284
|
+
|
|
1143
1285
|
#pragma mark - NSURLSessionDownloadDelegate methods
|
|
1144
1286
|
- (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location {
|
|
1145
1287
|
@synchronized (sharedLock) {
|
|
@@ -1153,8 +1295,21 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1153
1295
|
[self sendDebugLog:@"didFinishDownloadingToURL: download finished" taskId:taskConfig.id];
|
|
1154
1296
|
|
|
1155
1297
|
NSError *error = [self getServerError:downloadTask];
|
|
1298
|
+
BOOL deferred = NO;
|
|
1156
1299
|
if (!error) {
|
|
1157
|
-
[self saveFile:taskConfig downloadURL:location error:&error];
|
|
1300
|
+
[self saveFile:taskConfig downloadURL:location bytesDownloaded:downloadTask.countOfBytesReceived bytesTotal:downloadTask.countOfBytesExpectedToReceive error:&error deferred:&deferred];
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Drop any buffered progress for this task so it cannot arrive in JS after downloadComplete
|
|
1304
|
+
[progressReports removeObjectForKey:taskConfig.id];
|
|
1305
|
+
|
|
1306
|
+
if (deferred) {
|
|
1307
|
+
// The device was locked and the file couldn't be moved to its destination yet.
|
|
1308
|
+
// The bytes are staged and the move (and the downloadComplete event) will happen
|
|
1309
|
+
// once protected data becomes available. Do not emit complete/failed now. See #101.
|
|
1310
|
+
[self sendDebugLog:@"didFinishDownloadingToURL: move deferred until device unlock" taskId:taskConfig.id];
|
|
1311
|
+
[self removeTaskFromMap:downloadTask];
|
|
1312
|
+
return;
|
|
1158
1313
|
}
|
|
1159
1314
|
|
|
1160
1315
|
if (error) {
|
|
@@ -1170,7 +1325,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1170
1325
|
- (void)sendDownloadCompletionEvent:(RNBGDTaskConfig *)taskConfig task:(NSURLSessionDownloadTask *)task error:(NSError *)error {
|
|
1171
1326
|
if (error) {
|
|
1172
1327
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1173
|
-
[self
|
|
1328
|
+
[self safeEmitEvent:@"onDownloadFailed" value:@{
|
|
1174
1329
|
@"id": taskConfig.id,
|
|
1175
1330
|
@"error": [error localizedDescription],
|
|
1176
1331
|
@"errorCode": @(error.code)
|
|
@@ -1184,7 +1339,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1184
1339
|
#endif
|
|
1185
1340
|
} else {
|
|
1186
1341
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1187
|
-
[self
|
|
1342
|
+
[self safeEmitEvent:@"onDownloadComplete" value:@{
|
|
1188
1343
|
@"id": taskConfig.id,
|
|
1189
1344
|
@"location": taskConfig.destination,
|
|
1190
1345
|
@"bytesDownloaded": @(task.countOfBytesReceived),
|
|
@@ -1243,7 +1398,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1243
1398
|
responseHeaders = @{};
|
|
1244
1399
|
}
|
|
1245
1400
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1246
|
-
[self
|
|
1401
|
+
[self safeEmitEvent:@"onDownloadBegin" value:@{
|
|
1247
1402
|
@"id": taskConfig.id,
|
|
1248
1403
|
@"expectedBytes": @(expectedBytes),
|
|
1249
1404
|
@"headers": responseHeaders
|
|
@@ -1290,7 +1445,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1290
1445
|
NSDate *now = [NSDate date];
|
|
1291
1446
|
if ([now timeIntervalSinceDate:lastProgressReportedAt] > progressInterval) {
|
|
1292
1447
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1293
|
-
[self
|
|
1448
|
+
[self safeEmitEvent:@"onDownloadProgress" value:[progressReports allValues]];
|
|
1294
1449
|
#else
|
|
1295
1450
|
[self sendEventWithName:@"downloadProgress" body:[progressReports allValues]];
|
|
1296
1451
|
#endif
|
|
@@ -1385,7 +1540,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1385
1540
|
|
|
1386
1541
|
// Handle failure
|
|
1387
1542
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1388
|
-
[self
|
|
1543
|
+
[self safeEmitEvent:@"onDownloadFailed" value:@{
|
|
1389
1544
|
@"id": taskConfig.id,
|
|
1390
1545
|
@"error": [error localizedDescription],
|
|
1391
1546
|
@"errorCode": @(error.code)
|
|
@@ -1764,7 +1919,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1764
1919
|
if (!taskConfig.reportedBegin) {
|
|
1765
1920
|
taskConfig.reportedBegin = YES;
|
|
1766
1921
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1767
|
-
[self
|
|
1922
|
+
[self safeEmitEvent:@"onUploadBegin" value:@{
|
|
1768
1923
|
@"id": taskConfig.id,
|
|
1769
1924
|
@"expectedBytes": @(totalBytesExpectedToSend)
|
|
1770
1925
|
}];
|
|
@@ -1801,7 +1956,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1801
1956
|
NSDate *now = [NSDate date];
|
|
1802
1957
|
if ([now timeIntervalSinceDate:lastUploadProgressReportedAt] > progressInterval) {
|
|
1803
1958
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1804
|
-
[self
|
|
1959
|
+
[self safeEmitEvent:@"onUploadProgress" value:[uploadProgressReports allValues]];
|
|
1805
1960
|
#else
|
|
1806
1961
|
[self sendEventWithName:@"uploadProgress" body:[uploadProgressReports allValues]];
|
|
1807
1962
|
#endif
|
|
@@ -1847,7 +2002,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1847
2002
|
|
|
1848
2003
|
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [handleUploadCompletion] error: %@", error);
|
|
1849
2004
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1850
|
-
[self
|
|
2005
|
+
[self safeEmitEvent:@"onUploadFailed" value:@{
|
|
1851
2006
|
@"id": taskConfig.id,
|
|
1852
2007
|
@"error": [error localizedDescription],
|
|
1853
2008
|
@"errorCode": @(error.code)
|
|
@@ -1873,7 +2028,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1873
2028
|
|
|
1874
2029
|
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [handleUploadCompletion] success, responseCode: %ld", (long)responseCode);
|
|
1875
2030
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1876
|
-
[self
|
|
2031
|
+
[self safeEmitEvent:@"onUploadComplete" value:@{
|
|
1877
2032
|
@"id": taskConfig.id,
|
|
1878
2033
|
@"responseCode": @(responseCode),
|
|
1879
2034
|
@"responseBody": responseBody,
|
|
@@ -1937,8 +2092,20 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1937
2092
|
userInfo:@{NSLocalizedDescriptionKey: [NSHTTPURLResponse localizedStringForStatusCode:statusCode]}];
|
|
1938
2093
|
}
|
|
1939
2094
|
|
|
1940
|
-
|
|
2095
|
+
// Maps the JS data-protection key to an NSFileProtection* constant.
|
|
2096
|
+
// nil / unknown -> NSFileProtectionCompleteUntilFirstUserAuthentication, which lets a
|
|
2097
|
+
// background download save its file even while the device is locked (after first unlock).
|
|
2098
|
+
// See https://github.com/kesha-antonov/react-native-background-downloader/issues/101
|
|
2099
|
+
- (NSString *)nsFileProtectionFromKey:(NSString *)key {
|
|
2100
|
+
if ([key isEqualToString:@"complete"]) return NSFileProtectionComplete;
|
|
2101
|
+
if ([key isEqualToString:@"completeUnlessOpen"]) return NSFileProtectionCompleteUnlessOpen;
|
|
2102
|
+
if ([key isEqualToString:@"none"]) return NSFileProtectionNone;
|
|
2103
|
+
return NSFileProtectionCompleteUntilFirstUserAuthentication;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
- (BOOL)saveFile: (nonnull RNBGDTaskConfig *) taskConfig downloadURL:(nonnull NSURL *)location bytesDownloaded:(int64_t)bytesDownloaded bytesTotal:(int64_t)bytesTotal error:(NSError **)saveError deferred:(BOOL *)deferred {
|
|
1941
2107
|
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [saveFile]");
|
|
2108
|
+
if (deferred) *deferred = NO;
|
|
1942
2109
|
// taskConfig.destination is absolute path.
|
|
1943
2110
|
// The absolute path may change when the application is restarted.
|
|
1944
2111
|
// But the relative path remains the same.
|
|
@@ -1960,10 +2127,170 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1960
2127
|
NSURL *destinationURL = [NSURL fileURLWithPath:fileAbsolutePath];
|
|
1961
2128
|
|
|
1962
2129
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
1963
|
-
|
|
2130
|
+
// Create the destination directory with a protection class that allows writing while the
|
|
2131
|
+
// device is locked, so a background download can save when it finishes on a locked device.
|
|
2132
|
+
[fileManager createDirectoryAtURL:[destinationURL URLByDeletingLastPathComponent]
|
|
2133
|
+
withIntermediateDirectories:YES
|
|
2134
|
+
attributes:@{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication}
|
|
2135
|
+
error:nil];
|
|
1964
2136
|
[fileManager removeItemAtURL:destinationURL error:nil];
|
|
1965
2137
|
|
|
1966
|
-
|
|
2138
|
+
NSError *moveError = nil;
|
|
2139
|
+
BOOL moved = [fileManager moveItemAtURL:location toURL:destinationURL error:&moveError];
|
|
2140
|
+
if (moved) {
|
|
2141
|
+
// Apply the requested protection level to the final file (best-effort).
|
|
2142
|
+
NSString *protection = [self nsFileProtectionFromKey:taskConfig.dataProtection];
|
|
2143
|
+
[fileManager setAttributes:@{NSFileProtectionKey: protection} ofItemAtPath:destinationURL.path error:nil];
|
|
2144
|
+
return YES;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// The move failed - most commonly because the device is locked and the destination is
|
|
2148
|
+
// protected (Data Protection). Stage the downloaded bytes to an unprotected location and
|
|
2149
|
+
// finish the move once the device is unlocked, instead of losing the download. See #101.
|
|
2150
|
+
if ([self stageAndDeferMoveForConfig:taskConfig location:location destination:destinationURL bytesDownloaded:bytesDownloaded bytesTotal:bytesTotal]) {
|
|
2151
|
+
if (deferred) *deferred = YES;
|
|
2152
|
+
return NO;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// Couldn't even stage the file - report the original move error.
|
|
2156
|
+
if (saveError) *saveError = moveError;
|
|
2157
|
+
return NO;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
#pragma mark - Locked-device deferred move (issue #101)
|
|
2161
|
+
|
|
2162
|
+
// Directory (with NSFileProtectionNone) used to hold finished downloads whose move to the
|
|
2163
|
+
// final destination is deferred because the device is locked.
|
|
2164
|
+
- (NSURL *)pendingMovesDirectory {
|
|
2165
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
2166
|
+
NSURL *cachesDir = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] firstObject];
|
|
2167
|
+
NSURL *dir = [cachesDir URLByAppendingPathComponent:@"RNBGDPendingMoves" isDirectory:YES];
|
|
2168
|
+
if (![fileManager fileExistsAtPath:dir.path]) {
|
|
2169
|
+
// NSFileProtectionNone so we can write here even while the device is locked.
|
|
2170
|
+
[fileManager createDirectoryAtURL:dir
|
|
2171
|
+
withIntermediateDirectories:YES
|
|
2172
|
+
attributes:@{NSFileProtectionKey: NSFileProtectionNone}
|
|
2173
|
+
error:nil];
|
|
2174
|
+
}
|
|
2175
|
+
return dir;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Moves the just-finished download out of the system temp into an unprotected staging file and
|
|
2179
|
+
// records the pending move. Returns YES if the bytes were safely staged. Must hold sharedLock.
|
|
2180
|
+
- (BOOL)stageAndDeferMoveForConfig:(RNBGDTaskConfig *)taskConfig location:(NSURL *)location destination:(NSURL *)destinationURL bytesDownloaded:(int64_t)bytesDownloaded bytesTotal:(int64_t)bytesTotal {
|
|
2181
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
2182
|
+
NSURL *stagingURL = [[self pendingMovesDirectory] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.staged", taskConfig.id]];
|
|
2183
|
+
|
|
2184
|
+
[fileManager removeItemAtURL:stagingURL error:nil];
|
|
2185
|
+
NSError *stageError = nil;
|
|
2186
|
+
if (![fileManager moveItemAtURL:location toURL:stagingURL error:&stageError]) {
|
|
2187
|
+
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [stageAndDeferMove] staging failed: %@", stageError.localizedDescription);
|
|
2188
|
+
return NO;
|
|
2189
|
+
}
|
|
2190
|
+
// Make sure the staged file is readable while locked.
|
|
2191
|
+
[fileManager setAttributes:@{NSFileProtectionKey: NSFileProtectionNone} ofItemAtPath:stagingURL.path error:nil];
|
|
2192
|
+
|
|
2193
|
+
pendingMoves[taskConfig.id] = @{
|
|
2194
|
+
@"id": taskConfig.id,
|
|
2195
|
+
@"staging": stagingURL.path,
|
|
2196
|
+
@"destination": destinationURL.path,
|
|
2197
|
+
@"dataProtection": taskConfig.dataProtection ?: @"",
|
|
2198
|
+
@"bytesDownloaded": @(bytesDownloaded),
|
|
2199
|
+
@"bytesTotal": @(bytesTotal)
|
|
2200
|
+
};
|
|
2201
|
+
[self persistPendingMoves];
|
|
2202
|
+
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [stageAndDeferMove] staged %lld bytes, move deferred until unlock", (long long)bytesDownloaded);
|
|
2203
|
+
return YES;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// Attempts to complete any deferred moves. Safe to call repeatedly; only succeeds for items
|
|
2207
|
+
// whose destination is currently writable (i.e. the device is unlocked enough). Emits
|
|
2208
|
+
// downloadComplete for each move that finishes.
|
|
2209
|
+
- (void)processPendingMoves {
|
|
2210
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
2211
|
+
@synchronized (self->sharedLock) {
|
|
2212
|
+
if (self->pendingMoves.count == 0) return;
|
|
2213
|
+
|
|
2214
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
2215
|
+
NSArray<NSString *> *ids = [self->pendingMoves allKeys];
|
|
2216
|
+
BOOL changed = NO;
|
|
2217
|
+
|
|
2218
|
+
for (NSString *taskId in ids) {
|
|
2219
|
+
NSDictionary *info = self->pendingMoves[taskId];
|
|
2220
|
+
NSString *stagingPath = info[@"staging"];
|
|
2221
|
+
NSString *destinationPath = info[@"destination"];
|
|
2222
|
+
if (stagingPath == nil || destinationPath == nil) {
|
|
2223
|
+
[self->pendingMoves removeObjectForKey:taskId];
|
|
2224
|
+
changed = YES;
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
if (![fileManager fileExistsAtPath:stagingPath]) {
|
|
2229
|
+
// Staged file is gone - nothing we can do, drop the record.
|
|
2230
|
+
[self->pendingMoves removeObjectForKey:taskId];
|
|
2231
|
+
changed = YES;
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
NSURL *destinationURL = [NSURL fileURLWithPath:destinationPath];
|
|
2236
|
+
[fileManager createDirectoryAtURL:[destinationURL URLByDeletingLastPathComponent]
|
|
2237
|
+
withIntermediateDirectories:YES
|
|
2238
|
+
attributes:@{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication}
|
|
2239
|
+
error:nil];
|
|
2240
|
+
[fileManager removeItemAtURL:destinationURL error:nil];
|
|
2241
|
+
|
|
2242
|
+
NSError *moveError = nil;
|
|
2243
|
+
BOOL moved = [fileManager moveItemAtURL:[NSURL fileURLWithPath:stagingPath] toURL:destinationURL error:&moveError];
|
|
2244
|
+
if (!moved) {
|
|
2245
|
+
// Still can't write (device likely still locked) - keep it for the next attempt.
|
|
2246
|
+
DLog(taskId, @"[RNBackgroundDownloader] - [processPendingMoves] still deferred: %@", moveError.localizedDescription);
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
NSString *protection = [self nsFileProtectionFromKey:(info[@"dataProtection"] != nil && [info[@"dataProtection"] length] > 0 ? info[@"dataProtection"] : nil)];
|
|
2251
|
+
[fileManager setAttributes:@{NSFileProtectionKey: protection} ofItemAtPath:destinationURL.path error:nil];
|
|
2252
|
+
|
|
2253
|
+
[self->pendingMoves removeObjectForKey:taskId];
|
|
2254
|
+
changed = YES;
|
|
2255
|
+
|
|
2256
|
+
DLog(taskId, @"[RNBackgroundDownloader] - [processPendingMoves] completed deferred move");
|
|
2257
|
+
NSDictionary *body = @{
|
|
2258
|
+
@"id": taskId,
|
|
2259
|
+
@"location": destinationPath,
|
|
2260
|
+
@"bytesDownloaded": info[@"bytesDownloaded"] ?: @0,
|
|
2261
|
+
@"bytesTotal": info[@"bytesTotal"] ?: @0
|
|
2262
|
+
};
|
|
2263
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2264
|
+
[self safeEmitEvent:@"onDownloadComplete" value:body];
|
|
2265
|
+
#else
|
|
2266
|
+
[self sendEventWithName:@"downloadComplete" body:body];
|
|
2267
|
+
#endif
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
if (changed) [self persistPendingMoves];
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
- (void)persistPendingMoves {
|
|
2276
|
+
NSError *error = nil;
|
|
2277
|
+
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:pendingMoves requiringSecureCoding:YES error:&error];
|
|
2278
|
+
if (error) {
|
|
2279
|
+
DLog(nil, @"[RNBackgroundDownloader] - [persistPendingMoves] serialization error: %@", error);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
[mmkv setData:data forKey:PENDING_MOVES_MAP_KEY];
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
- (NSMutableDictionary *)deserializePendingMoves:(NSData *)data {
|
|
2286
|
+
NSError *error = nil;
|
|
2287
|
+
NSSet *classes = [NSSet setWithObjects:[NSMutableDictionary class], [NSDictionary class], [NSString class], [NSNumber class], nil];
|
|
2288
|
+
NSDictionary *decoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:data error:&error];
|
|
2289
|
+
if (error || decoded == nil) {
|
|
2290
|
+
DLog(nil, @"[RNBackgroundDownloader] - [deserializePendingMoves] error: %@", error);
|
|
2291
|
+
return nil;
|
|
2292
|
+
}
|
|
2293
|
+
return [decoded mutableCopy];
|
|
1967
2294
|
}
|
|
1968
2295
|
|
|
1969
2296
|
#pragma mark - serialization
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TaskInfo, DownloadTask as DownloadTaskType, BeginHandler, ProgressHandler, DoneHandler, ErrorHandler, BeginHandlerParams, ProgressHandlerParams, DoneHandlerParams, ErrorHandlerParams, TaskInfoNative, DownloadParams, DownloadTaskState, Metadata } from './types';
|
|
2
|
+
export declare class DownloadTask {
|
|
3
|
+
id: string;
|
|
4
|
+
metadata: Metadata;
|
|
5
|
+
destination?: string;
|
|
6
|
+
state: DownloadTaskState;
|
|
7
|
+
errorCode: number;
|
|
8
|
+
bytesDownloaded: number;
|
|
9
|
+
bytesTotal: number;
|
|
10
|
+
downloadParams?: DownloadParams;
|
|
11
|
+
beginHandler?: BeginHandler;
|
|
12
|
+
progressHandler?: ProgressHandler;
|
|
13
|
+
doneHandler?: DoneHandler;
|
|
14
|
+
errorHandler?: ErrorHandler;
|
|
15
|
+
constructor(taskParams: TaskInfo | TaskInfoNative, originalTask?: DownloadTaskType);
|
|
16
|
+
begin(handler: BeginHandler): this;
|
|
17
|
+
progress(handler: ProgressHandler): this;
|
|
18
|
+
done(handler: DoneHandler): this;
|
|
19
|
+
error(handler: ErrorHandler): this;
|
|
20
|
+
onBegin(params: BeginHandlerParams): void;
|
|
21
|
+
onProgress(params: ProgressHandlerParams): void;
|
|
22
|
+
onDone(params: DoneHandlerParams): void;
|
|
23
|
+
onError(params: ErrorHandlerParams): void;
|
|
24
|
+
/**
|
|
25
|
+
* Update download parameters.
|
|
26
|
+
* If the task is paused, this will also update headers in the native layer.
|
|
27
|
+
* If the task is in-progress or completed, only the local JS object is updated.
|
|
28
|
+
*
|
|
29
|
+
* @param downloadParams - The new download parameters
|
|
30
|
+
* @returns Promise<boolean> - true if native headers were updated, false otherwise
|
|
31
|
+
*/
|
|
32
|
+
setDownloadParams(downloadParams: DownloadParams): Promise<boolean>;
|
|
33
|
+
pause(): Promise<void>;
|
|
34
|
+
resume(): Promise<void>;
|
|
35
|
+
start(): void;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
tryParseJson(metadata?: string | Metadata | object): Metadata | null;
|
|
38
|
+
private headersToUnsafeObject;
|
|
39
|
+
}
|