@kesha-antonov/react-native-background-downloader 2.6.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.
@@ -0,0 +1,24 @@
1
+ //
2
+ // RNFileBackgroundDownload.h
3
+ // EkoApp
4
+ //
5
+ // Created by Elad Gil on 20/11/2017.
6
+ // Copyright © 2017 Eko. All rights reserved.
7
+ //
8
+ //
9
+ #import <Foundation/Foundation.h>
10
+ #if __has_include(<React/RCTBridgeModule.h>)
11
+ #import <React/RCTBridgeModule.h>
12
+ #import <React/RCTEventEmitter.h>
13
+ #elif __has_include("RCTBridgeModule.h")
14
+ #import "RCTBridgeModule.h"
15
+ #import "RCTEventEmitter.h"
16
+ #endif
17
+
18
+ typedef void (^CompletionHandler)();
19
+
20
+ @interface RNBackgroundDownloader : RCTEventEmitter <RCTBridgeModule, NSURLSessionDelegate, NSURLSessionDownloadDelegate>
21
+
22
+ + (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler;
23
+
24
+ @end
@@ -0,0 +1,425 @@
1
+ //
2
+ // RNFileBackgroundDownload.m
3
+ // EkoApp
4
+ //
5
+ // Created by Elad Gil on 20/11/2017.
6
+ // Copyright © 2017 Eko. All rights reserved.
7
+ //
8
+ //
9
+ #import "RNBackgroundDownloader.h"
10
+ #import "RNBGDTaskConfig.h"
11
+
12
+ #define ID_TO_CONFIG_MAP_KEY @"com.eko.bgdownloadidmap"
13
+
14
+ static CompletionHandler storedCompletionHandler;
15
+
16
+ @implementation RNBackgroundDownloader {
17
+ NSURLSession *urlSession;
18
+ NSURLSessionConfiguration *sessionConfig;
19
+ NSMutableDictionary<NSNumber *, RNBGDTaskConfig *> *taskToConfigMap;
20
+ NSMutableDictionary<NSString *, NSURLSessionDownloadTask *> *idToTaskMap;
21
+ NSMutableDictionary<NSString *, NSData *> *idToResumeDataMap;
22
+ NSMutableDictionary<NSString *, NSNumber *> *idToPercentMap;
23
+ NSMutableDictionary<NSString *, NSDictionary *> *progressReports;
24
+ NSDate *lastProgressReport;
25
+ NSNumber *sharedLock;
26
+ BOOL isNotificationCenterInited;
27
+ }
28
+
29
+ RCT_EXPORT_MODULE();
30
+
31
+ - (dispatch_queue_t)methodQueue
32
+ {
33
+ return dispatch_queue_create("com.eko.backgrounddownloader", DISPATCH_QUEUE_SERIAL);
34
+ }
35
+
36
+ + (BOOL)requiresMainQueueSetup {
37
+ return YES;
38
+ }
39
+
40
+ - (NSArray<NSString *> *)supportedEvents {
41
+ return @[@"downloadComplete", @"downloadProgress", @"downloadFailed", @"downloadBegin"];
42
+ }
43
+
44
+ - (NSDictionary *)constantsToExport {
45
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
46
+
47
+ return @{
48
+ @"documents": [paths firstObject],
49
+ @"TaskRunning": @(NSURLSessionTaskStateRunning),
50
+ @"TaskSuspended": @(NSURLSessionTaskStateSuspended),
51
+ @"TaskCanceling": @(NSURLSessionTaskStateCanceling),
52
+ @"TaskCompleted": @(NSURLSessionTaskStateCompleted)
53
+ };
54
+ }
55
+
56
+ - (id) init {
57
+ NSLog(@"[RNBackgroundDownloader] - [init]");
58
+ self = [super init];
59
+ if (self) {
60
+ taskToConfigMap = [self deserialize:[[NSUserDefaults standardUserDefaults] objectForKey:ID_TO_CONFIG_MAP_KEY]];
61
+ if (taskToConfigMap == nil) {
62
+ taskToConfigMap = [[NSMutableDictionary alloc] init];
63
+ }
64
+ idToTaskMap = [[NSMutableDictionary alloc] init];
65
+ idToResumeDataMap= [[NSMutableDictionary alloc] init];
66
+ idToPercentMap = [[NSMutableDictionary alloc] init];
67
+ NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
68
+ NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"];
69
+ sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessonIdentifier];
70
+ sessionConfig.HTTPMaximumConnectionsPerHost = 4;
71
+ sessionConfig.timeoutIntervalForRequest = 60 * 60; // MAX TIME TO GET NEW DATA IN REQUEST - 1 HOUR
72
+ sessionConfig.timeoutIntervalForResource = 60 * 60 * 24; // MAX TIME TO DOWNLOAD RESOURCE - 1 DAY
73
+ sessionConfig.discretionary = NO;
74
+ sessionConfig.sessionSendsLaunchEvents = YES;
75
+ if (@available(iOS 9.0, *)) {
76
+ sessionConfig.shouldUseExtendedBackgroundIdleMode = YES;
77
+ }
78
+ if (@available(iOS 13.0, *)) {
79
+ sessionConfig.allowsExpensiveNetworkAccess = YES;
80
+ }
81
+
82
+ progressReports = [[NSMutableDictionary alloc] init];
83
+ lastProgressReport = [[NSDate alloc] init];
84
+ sharedLock = [NSNumber numberWithInt:1];
85
+ }
86
+ return self;
87
+ }
88
+
89
+ - (void)lazyInitSession {
90
+ NSLog(@"[RNBackgroundDownloader] - [lazyInitSession]");
91
+ @synchronized (sharedLock) {
92
+ if (urlSession == nil) {
93
+ urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
94
+ }
95
+ if (isNotificationCenterInited != YES) {
96
+ isNotificationCenterInited = YES;
97
+ [[NSNotificationCenter defaultCenter] addObserver:self
98
+ selector:@selector(resumeTasks:)
99
+ name:UIApplicationWillEnterForegroundNotification
100
+ object:nil];
101
+ }
102
+ }
103
+ }
104
+
105
+ - (void) dealloc {
106
+ NSLog(@"[RNBackgroundDownloader] - [dealloc]");
107
+ [urlSession invalidateAndCancel];
108
+ urlSession = nil;
109
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
110
+ }
111
+
112
+ // NOTE: FIXES HANGING DOWNLOADS WHEN GOING TO BG
113
+ - (void) resumeTasks:(NSNotification *) note {
114
+ NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 1");
115
+ @synchronized (self->sharedLock) {
116
+ [urlSession getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
117
+ NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 2");
118
+ for (NSURLSessionDownloadTask *task in downloadTasks) {
119
+ NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 3 - state - %@", [NSNumber numberWithInt:task.state]);
120
+ // running - 0
121
+ // suspended - 1
122
+ // canceling - 2
123
+ // completed - 3
124
+ NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 4 - totalBytes - %@", [NSNumber numberWithInt:task.countOfBytesExpectedToReceive]);
125
+ NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 5 - bytesWritten - %@", [NSNumber numberWithInt:task.countOfBytesReceived]);
126
+
127
+ if (task.state == NSURLSessionTaskStateRunning) {
128
+ [task suspend]; // PAUSE
129
+ [task resume];
130
+ }
131
+ }
132
+ // TODO: MAYBE ADD FOR OTHER TASKS TYPES
133
+ // for (NSURLSessionDataTask *task in dataTasks) {
134
+ // NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 5");
135
+ // [task resume];
136
+ // }
137
+ // for (NSURLSessionUploadTask *task in dataTasks) {
138
+ // NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 6");
139
+ // [task resume];
140
+ // }
141
+ }];
142
+ }
143
+ }
144
+
145
+ - (void)removeTaskFromMap: (NSURLSessionTask *)task {
146
+ NSLog(@"[RNBackgroundDownloader] - [removeTaskFromMap]");
147
+ @synchronized (sharedLock) {
148
+ NSNumber *taskId = @(task.taskIdentifier);
149
+ RNBGDTaskConfig *taskConfig = taskToConfigMap[taskId];
150
+
151
+ [taskToConfigMap removeObjectForKey:taskId];
152
+ [[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY];
153
+
154
+ if (taskConfig) {
155
+ [idToTaskMap removeObjectForKey:taskConfig.id];
156
+ [idToPercentMap removeObjectForKey:taskConfig.id];
157
+ }
158
+ // TOREMOVE - GIVES ERROR IN JS ON HOT RELOAD
159
+ // if (taskToConfigMap.count == 0) {
160
+ // [urlSession invalidateAndCancel];
161
+ // urlSession = nil;
162
+ // }
163
+ }
164
+ }
165
+
166
+ + (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler {
167
+ NSLog(@"[RNBackgroundDownloader] - [setCompletionHandlerWithIdentifier]");
168
+ NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
169
+ NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"];
170
+ if ([sessonIdentifier isEqualToString:identifier]) {
171
+ storedCompletionHandler = completionHandler;
172
+ }
173
+ }
174
+
175
+ - (NSError *)getServerError: (nonnull NSURLSessionDownloadTask *)downloadTask {
176
+ NSLog(@"[RNBackgroundDownloader] - [getServerError]");
177
+ NSError *serverError;
178
+ NSInteger httpStatusCode = [((NSHTTPURLResponse *)downloadTask.response) statusCode];
179
+ if(httpStatusCode != 200) {
180
+ serverError = [NSError errorWithDomain:NSURLErrorDomain
181
+ code:httpStatusCode
182
+ userInfo:@{NSLocalizedDescriptionKey: [NSHTTPURLResponse localizedStringForStatusCode: httpStatusCode]}];
183
+ }
184
+ return serverError;
185
+ }
186
+
187
+ - (BOOL)saveDownloadedFile: (nonnull RNBGDTaskConfig *) taskConfig downloadURL:(nonnull NSURL *)location error:(NSError **)saveError {
188
+ NSLog(@"[RNBackgroundDownloader] - [saveDownloadedFile]");
189
+ NSFileManager *fileManager = [NSFileManager defaultManager];
190
+ NSURL *destURL = [NSURL fileURLWithPath:taskConfig.destination];
191
+ [fileManager createDirectoryAtURL:[destURL URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
192
+ [fileManager removeItemAtURL:destURL error:nil];
193
+
194
+ return [fileManager moveItemAtURL:location toURL:destURL error:saveError];
195
+ }
196
+
197
+ #pragma mark - JS exported methods
198
+ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
199
+ NSLog(@"[RNBackgroundDownloader] - [download]");
200
+ NSString *identifier = options[@"id"];
201
+ NSString *url = options[@"url"];
202
+ NSString *destination = options[@"destination"];
203
+ NSString *metadata = options[@"metadata"];
204
+ NSDictionary *headers = options[@"headers"];
205
+ if (identifier == nil || url == nil || destination == nil) {
206
+ NSLog(@"[RNBackgroundDownloader] - [Error] id, url and destination must be set");
207
+ return;
208
+ }
209
+
210
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
211
+ if (headers != nil) {
212
+ for (NSString *headerKey in headers) {
213
+ [request setValue:[headers valueForKey:headerKey] forHTTPHeaderField:headerKey];
214
+ }
215
+ }
216
+
217
+ @synchronized (sharedLock) {
218
+ [self lazyInitSession];
219
+ NSURLSessionDownloadTask __strong *task = [urlSession downloadTaskWithRequest:request];
220
+ if (task == nil) {
221
+ NSLog(@"[RNBackgroundDownloader] - [Error] failed to create download task");
222
+ return;
223
+ }
224
+
225
+ RNBGDTaskConfig *taskConfig = [[RNBGDTaskConfig alloc] initWithDictionary: @{@"id": identifier, @"destination": destination, @"metadata": metadata}];
226
+
227
+ taskToConfigMap[@(task.taskIdentifier)] = taskConfig;
228
+ [[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY];
229
+
230
+ idToTaskMap[identifier] = task;
231
+ idToPercentMap[identifier] = @0.0;
232
+
233
+ [task resume];
234
+ lastProgressReport = [[NSDate alloc] init];
235
+ }
236
+ }
237
+
238
+ RCT_EXPORT_METHOD(pauseTask: (NSString *)identifier) {
239
+ NSLog(@"[RNBackgroundDownloader] - [pauseTask]");
240
+ @synchronized (sharedLock) {
241
+ NSURLSessionDownloadTask *task = idToTaskMap[identifier];
242
+ if (task != nil && task.state == NSURLSessionTaskStateRunning) {
243
+ [task suspend];
244
+ }
245
+ }
246
+ }
247
+
248
+ RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) {
249
+ NSLog(@"[RNBackgroundDownloader] - [resumeTask]");
250
+ @synchronized (sharedLock) {
251
+ NSURLSessionDownloadTask *task = idToTaskMap[identifier];
252
+ if (task != nil && task.state == NSURLSessionTaskStateSuspended) {
253
+ [task resume];
254
+ }
255
+ }
256
+ }
257
+
258
+ RCT_EXPORT_METHOD(stopTask: (NSString *)identifier) {
259
+ NSLog(@"[RNBackgroundDownloader] - [stopTask]");
260
+ @synchronized (sharedLock) {
261
+ NSURLSessionDownloadTask *task = idToTaskMap[identifier];
262
+ if (task != nil) {
263
+ [task cancel];
264
+ [self removeTaskFromMap:task];
265
+ }
266
+ }
267
+ }
268
+
269
+ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
270
+ NSLog(@"[RNBackgroundDownloader] - [checkForExistingDownloads]");
271
+ [self lazyInitSession];
272
+ [urlSession getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
273
+ NSMutableArray *idsFound = [[NSMutableArray alloc] init];
274
+ @synchronized (sharedLock) {
275
+ for (NSURLSessionDownloadTask *foundTask in downloadTasks) {
276
+ NSURLSessionDownloadTask __strong *task = foundTask;
277
+ RNBGDTaskConfig *taskConfig = taskToConfigMap[@(task.taskIdentifier)];
278
+ if (taskConfig) {
279
+ if (task.state == NSURLSessionTaskStateCompleted && task.countOfBytesReceived < task.countOfBytesExpectedToReceive) {
280
+ if (task.error && task.error.code == -999 && task.error.userInfo[NSURLSessionDownloadTaskResumeData] != nil) {
281
+ task = [urlSession downloadTaskWithResumeData:task.error.userInfo[NSURLSessionDownloadTaskResumeData]];
282
+ } else {
283
+ task = [urlSession downloadTaskWithURL:foundTask.currentRequest.URL];
284
+ }
285
+ [task resume];
286
+ }
287
+ NSNumber *percent = foundTask.countOfBytesExpectedToReceive > 0 ? [NSNumber numberWithFloat:(float)task.countOfBytesReceived/(float)foundTask.countOfBytesExpectedToReceive] : @0.0;
288
+ [idsFound addObject:@{
289
+ @"id": taskConfig.id,
290
+ @"metadata": taskConfig.metadata,
291
+ @"state": [NSNumber numberWithInt: task.state],
292
+ @"bytesWritten": [NSNumber numberWithLongLong:task.countOfBytesReceived],
293
+ @"totalBytes": [NSNumber numberWithLongLong:foundTask.countOfBytesExpectedToReceive],
294
+ @"percent": percent
295
+ }];
296
+ taskConfig.reportedBegin = YES;
297
+ taskToConfigMap[@(task.taskIdentifier)] = taskConfig;
298
+ idToTaskMap[taskConfig.id] = task;
299
+ idToPercentMap[taskConfig.id] = percent;
300
+ } else {
301
+ [task cancel];
302
+ }
303
+ }
304
+ resolve(idsFound);
305
+ }
306
+ }];
307
+ }
308
+
309
+ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId
310
+ resolver:(RCTPromiseResolveBlock)resolve
311
+ rejecter:(RCTPromiseRejectBlock)reject)
312
+ {
313
+ NSLog(@"[RNBackgroundDownloader] - [completeHandlerIOS]");
314
+ if (storedCompletionHandler) {
315
+ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
316
+ storedCompletionHandler();
317
+ storedCompletionHandler = nil;
318
+ }];
319
+ }
320
+ resolve(nil);
321
+ }
322
+
323
+
324
+ #pragma mark - NSURLSessionDownloadDelegate methods
325
+ - (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location {
326
+ NSLog(@"[RNBackgroundDownloader] - [didFinishDownloadingToURL]");
327
+ @synchronized (sharedLock) {
328
+ RNBGDTaskConfig *taskConfig = taskToConfigMap[@(downloadTask.taskIdentifier)];
329
+ if (taskConfig != nil) {
330
+ NSError *error = [self getServerError:downloadTask];
331
+ if (error == nil) {
332
+ [self saveDownloadedFile:taskConfig downloadURL:location error:&error];
333
+ }
334
+ if (self.bridge) {
335
+ if (error == nil) {
336
+ NSDictionary *responseHeaders = ((NSHTTPURLResponse *)downloadTask.response).allHeaderFields;
337
+ [self sendEventWithName:@"downloadComplete" body:@{@"id": taskConfig.id, @"headers": responseHeaders, @"location": taskConfig.destination}];
338
+ } else {
339
+ [self sendEventWithName:@"downloadFailed" body:@{@"id": taskConfig.id, @"error": [error localizedDescription]}];
340
+ }
341
+ }
342
+ [self removeTaskFromMap:downloadTask];
343
+ }
344
+ }
345
+ }
346
+
347
+ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
348
+ NSLog(@"[RNBackgroundDownloader] - [didResumeAtOffset]");
349
+ }
350
+
351
+ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
352
+ NSLog(@"[RNBackgroundDownloader] - [didWriteData]");
353
+ @synchronized (sharedLock) {
354
+ RNBGDTaskConfig *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)];
355
+ if (taskCofig != nil) {
356
+ if (!taskCofig.reportedBegin) {
357
+ NSDictionary *responseHeaders = ((NSHTTPURLResponse *)downloadTask.response).allHeaderFields;
358
+ if (self.bridge) {
359
+ [self sendEventWithName:@"downloadBegin" body:@{
360
+ @"id": taskCofig.id,
361
+ @"expectedBytes": [NSNumber numberWithLongLong: totalBytesExpectedToWrite],
362
+ @"headers": responseHeaders
363
+ }];
364
+ }
365
+ taskCofig.reportedBegin = YES;
366
+ }
367
+
368
+ NSNumber *prevPercent = idToPercentMap[taskCofig.id];
369
+ NSNumber *percent = [NSNumber numberWithFloat:(float)totalBytesWritten/(float)totalBytesExpectedToWrite];
370
+ if ([percent floatValue] - [prevPercent floatValue] > 0.01f) {
371
+ progressReports[taskCofig.id] = @{@"id": taskCofig.id, @"written": [NSNumber numberWithLongLong: totalBytesWritten], @"total": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], @"percent": percent};
372
+ idToPercentMap[taskCofig.id] = percent;
373
+ }
374
+
375
+ NSDate *now = [[NSDate alloc] init];
376
+ if ([now timeIntervalSinceDate:lastProgressReport] > 0.25 && progressReports.count > 0) {
377
+ if (self.bridge) {
378
+ [self sendEventWithName:@"downloadProgress" body:[progressReports allValues]];
379
+ }
380
+ lastProgressReport = now;
381
+ [progressReports removeAllObjects];
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
388
+ NSLog(@"[RNBackgroundDownloader] - [didCompleteWithError]");
389
+ @synchronized (sharedLock) {
390
+ RNBGDTaskConfig *taskCofig = taskToConfigMap[@(task.taskIdentifier)];
391
+ if (error != nil && error.code != -999 && taskCofig != nil) {
392
+ if (self.bridge) {
393
+ [self sendEventWithName:@"downloadFailed" body:@{@"id": taskCofig.id, @"error": [error localizedDescription]}];
394
+ }
395
+ [self removeTaskFromMap:task];
396
+ }
397
+ }
398
+ }
399
+
400
+ - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
401
+ NSLog(@"[RNBackgroundDownloader] - [URLSessionDidFinishEventsForBackgroundURLSession]");
402
+ // USE completionHandler FROM JS INSTEAD OF THIS
403
+ // TOREMOVE
404
+ // if (storedCompletionHandler) {
405
+ // [[NSOperationQueue mainQueue] addOperationWithBlock:^{
406
+ // storedCompletionHandler();
407
+ // storedCompletionHandler = nil;
408
+ // }];
409
+ // }
410
+ }
411
+
412
+ #pragma mark - serialization
413
+ - (NSData *)serialize: (id)obj {
414
+ return [NSKeyedArchiver archivedDataWithRootObject:obj];
415
+ }
416
+
417
+ - (id)deserialize: (NSData *)data {
418
+ if (data == nil) {
419
+ return nil;
420
+ }
421
+
422
+ return [NSKeyedUnarchiver unarchiveObjectWithData:data];
423
+ }
424
+
425
+ @end