Files
react-native-background-dow…/ios/RNBackgroundDownloader.m

427 lines
18 KiB
Objective-C

//
// RNFileBackgroundDownload.m
// EkoApp
//
// Created by Elad Gil on 20/11/2017.
// Copyright © 2017 Eko. All rights reserved.
//
//
#import "RNBackgroundDownloader.h"
#import "RNBGDTaskConfig.h"
#define ID_TO_CONFIG_MAP_KEY @"com.eko.bgdownloadidmap"
static CompletionHandler storedCompletionHandler;
@implementation RNBackgroundDownloader {
NSURLSession *urlSession;
NSURLSessionConfiguration *sessionConfig;
NSMutableDictionary<NSNumber *, RNBGDTaskConfig *> *taskToConfigMap;
NSMutableDictionary<NSString *, NSURLSessionDownloadTask *> *idToTaskMap;
NSMutableDictionary<NSString *, NSData *> *idToResumeDataMap;
NSMutableDictionary<NSString *, NSDictionary *> *progressReports;
NSDate *lastProgressReport;
NSNumber *sharedLock;
BOOL isNotificationCenterInited;
}
RCT_EXPORT_MODULE();
- (dispatch_queue_t)methodQueue
{
return dispatch_queue_create("com.eko.backgrounddownloader", DISPATCH_QUEUE_SERIAL);
}
+ (BOOL)requiresMainQueueSetup {
return YES;
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"downloadComplete", @"downloadProgress", @"downloadFailed", @"downloadBegin"];
}
- (NSDictionary *)constantsToExport {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
return @{
@"documents": [paths firstObject],
@"TaskRunning": @(NSURLSessionTaskStateRunning),
@"TaskSuspended": @(NSURLSessionTaskStateSuspended),
@"TaskCanceling": @(NSURLSessionTaskStateCanceling),
@"TaskCompleted": @(NSURLSessionTaskStateCompleted)
};
}
- (id) init {
NSLog(@"[RNBackgroundDownloader] - [init]");
self = [super init];
if (self) {
taskToConfigMap = [self deserialize:[[NSUserDefaults standardUserDefaults] objectForKey:ID_TO_CONFIG_MAP_KEY]];
if (taskToConfigMap == nil) {
taskToConfigMap = [[NSMutableDictionary alloc] init];
}
idToTaskMap = [[NSMutableDictionary alloc] init];
idToResumeDataMap = [[NSMutableDictionary alloc] init];
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"];
sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessonIdentifier];
sessionConfig.HTTPMaximumConnectionsPerHost = 4;
sessionConfig.timeoutIntervalForRequest = 60 * 60; // MAX TIME TO GET NEW DATA IN REQUEST - 1 HOUR
sessionConfig.timeoutIntervalForResource = 60 * 60 * 24; // MAX TIME TO DOWNLOAD RESOURCE - 1 DAY
sessionConfig.discretionary = NO;
sessionConfig.sessionSendsLaunchEvents = YES;
if (@available(iOS 9.0, *)) {
sessionConfig.shouldUseExtendedBackgroundIdleMode = YES;
}
if (@available(iOS 13.0, *)) {
sessionConfig.allowsExpensiveNetworkAccess = YES;
}
progressReports = [[NSMutableDictionary alloc] init];
lastProgressReport = [[NSDate alloc] init];
sharedLock = [NSNumber numberWithInt:1];
}
return self;
}
- (void)lazyInitSession {
NSLog(@"[RNBackgroundDownloader] - [lazyInitSession]");
@synchronized (sharedLock) {
if (urlSession == nil) {
urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
}
if (isNotificationCenterInited != YES) {
isNotificationCenterInited = YES;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(resumeTasks:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
}
}
}
- (void) dealloc {
NSLog(@"[RNBackgroundDownloader] - [dealloc]");
[urlSession invalidateAndCancel];
urlSession = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// NOTE: FIXES HANGING DOWNLOADS WHEN GOING TO BG
- (void) resumeTasks:(NSNotification *) note {
NSLog(@"[RNBackgroundDownloader] - [resumeTasks]");
@synchronized (sharedLock) {
[urlSession getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
for (NSURLSessionDownloadTask *task in downloadTasks) {
// running - 0
// suspended - 1
// canceling - 2
// completed - 3
if (task.state == NSURLSessionTaskStateRunning) {
[task suspend]; // PAUSE
[task resume];
}
}
// TODO: MAYBE ADD FOR OTHER TASKS TYPES
// for (NSURLSessionDataTask *task in dataTasks) {
// NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 5");
// [task resume];
// }
// for (NSURLSessionUploadTask *task in dataTasks) {
// NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 6");
// [task resume];
// }
}];
}
}
- (void)removeTaskFromMap: (NSURLSessionTask *)task {
NSLog(@"[RNBackgroundDownloader] - [removeTaskFromMap]");
@synchronized (sharedLock) {
NSNumber *taskId = @(task.taskIdentifier);
RNBGDTaskConfig *taskConfig = taskToConfigMap[taskId];
[taskToConfigMap removeObjectForKey:taskId];
[[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY];
if (taskConfig) {
[idToTaskMap removeObjectForKey:taskConfig.id];
}
// TOREMOVE - GIVES ERROR IN JS ON HOT RELOAD
// if (taskToConfigMap.count == 0) {
// [urlSession invalidateAndCancel];
// urlSession = nil;
// }
}
}
+ (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler {
NSLog(@"[RNBackgroundDownloader] - [setCompletionHandlerWithIdentifier]");
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"];
if ([sessonIdentifier isEqualToString:identifier]) {
storedCompletionHandler = completionHandler;
}
}
- (NSError *)getServerError: (nonnull NSURLSessionDownloadTask *)downloadTask {
NSLog(@"[RNBackgroundDownloader] - [getServerError]");
NSError *serverError;
NSInteger httpStatusCode = [((NSHTTPURLResponse *)downloadTask.response) statusCode];
if(httpStatusCode != 200) {
serverError = [NSError errorWithDomain:NSURLErrorDomain
code:httpStatusCode
userInfo:@{NSLocalizedDescriptionKey: [NSHTTPURLResponse localizedStringForStatusCode: httpStatusCode]}];
}
return serverError;
}
- (BOOL)saveDownloadedFile: (nonnull RNBGDTaskConfig *) taskConfig downloadURL:(nonnull NSURL *)location error:(NSError **)saveError {
NSLog(@"[RNBackgroundDownloader] - [saveDownloadedFile]");
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *destURL = [NSURL fileURLWithPath:taskConfig.destination];
[fileManager createDirectoryAtURL:[destURL URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
[fileManager removeItemAtURL:destURL error:nil];
return [fileManager moveItemAtURL:location toURL:destURL error:saveError];
}
#pragma mark - JS exported methods
RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
NSLog(@"[RNBackgroundDownloader] - [download]");
NSString *identifier = options[@"id"];
NSString *url = options[@"url"];
NSString *destination = options[@"destination"];
NSString *metadata = options[@"metadata"];
NSDictionary *headers = options[@"headers"];
if (identifier == nil || url == nil || destination == nil) {
NSLog(@"[RNBackgroundDownloader] - [Error] id, url and destination must be set");
return;
}
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
if (headers != nil) {
for (NSString *headerKey in headers) {
[request setValue:[headers valueForKey:headerKey] forHTTPHeaderField:headerKey];
}
}
@synchronized (sharedLock) {
[self lazyInitSession];
NSURLSessionDownloadTask __strong *task = [urlSession downloadTaskWithRequest:request];
if (task == nil) {
NSLog(@"[RNBackgroundDownloader] - [Error] failed to create download task");
return;
}
RNBGDTaskConfig *taskConfig = [[RNBGDTaskConfig alloc] initWithDictionary: @{@"id": identifier, @"destination": destination, @"metadata": metadata}];
taskToConfigMap[@(task.taskIdentifier)] = taskConfig;
[[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY];
idToTaskMap[identifier] = task;
[task resume];
lastProgressReport = [[NSDate alloc] init];
}
}
RCT_EXPORT_METHOD(pauseTask: (NSString *)identifier) {
NSLog(@"[RNBackgroundDownloader] - [pauseTask]");
@synchronized (sharedLock) {
NSURLSessionDownloadTask *task = idToTaskMap[identifier];
if (task != nil && task.state == NSURLSessionTaskStateRunning) {
[task suspend];
}
}
}
RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) {
NSLog(@"[RNBackgroundDownloader] - [resumeTask]");
@synchronized (sharedLock) {
NSURLSessionDownloadTask *task = idToTaskMap[identifier];
if (task != nil && task.state == NSURLSessionTaskStateSuspended) {
[task resume];
}
}
}
RCT_EXPORT_METHOD(stopTask: (NSString *)identifier) {
NSLog(@"[RNBackgroundDownloader] - [stopTask]");
@synchronized (sharedLock) {
NSURLSessionDownloadTask *task = idToTaskMap[identifier];
if (task != nil) {
[task cancel];
[self removeTaskFromMap:task];
}
}
}
RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
NSLog(@"[RNBackgroundDownloader] - [checkForExistingDownloads]");
[self lazyInitSession];
[urlSession getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
NSMutableArray *idsFound = [[NSMutableArray alloc] init];
@synchronized (sharedLock) {
for (NSURLSessionDownloadTask *foundTask in downloadTasks) {
NSURLSessionDownloadTask __strong *task = foundTask;
RNBGDTaskConfig *taskConfig = taskToConfigMap[@(task.taskIdentifier)];
if (taskConfig) {
if ((task.state == NSURLSessionTaskStateCompleted || task.state == NSURLSessionTaskStateSuspended) && task.countOfBytesReceived < task.countOfBytesExpectedToReceive) {
if (task.error && task.error.userInfo[NSURLSessionDownloadTaskResumeData] != nil) {
task = [urlSession downloadTaskWithResumeData:task.error.userInfo[NSURLSessionDownloadTaskResumeData]];
} else {
task = [urlSession downloadTaskWithURL:task.currentRequest.URL];
}
[task resume];
}
[idsFound addObject:@{
@"id": taskConfig.id,
@"metadata": taskConfig.metadata,
@"state": [NSNumber numberWithInt: task.state],
@"bytesDownloaded": [NSNumber numberWithLongLong:task.countOfBytesReceived],
@"bytesTotal": [NSNumber numberWithLongLong:task.countOfBytesExpectedToReceive]
}];
taskConfig.reportedBegin = YES;
taskToConfigMap[@(task.taskIdentifier)] = taskConfig;
idToTaskMap[taskConfig.id] = task;
} else {
[task cancel];
}
}
resolve(idsFound);
}
}];
}
RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSLog(@"[RNBackgroundDownloader] - [completeHandlerIOS]");
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (storedCompletionHandler) {
storedCompletionHandler();
storedCompletionHandler = nil;
}
}];
resolve(nil);
}
#pragma mark - NSURLSessionDownloadDelegate methods
- (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location {
NSLog(@"[RNBackgroundDownloader] - [didFinishDownloadingToURL]");
@synchronized (sharedLock) {
RNBGDTaskConfig *taskConfig = taskToConfigMap[@(downloadTask.taskIdentifier)];
if (taskConfig != nil) {
NSError *error = [self getServerError:downloadTask];
if (error == nil) {
[self saveDownloadedFile:taskConfig downloadURL:location error:&error];
}
if (self.bridge) {
if (error == nil) {
NSDictionary *responseHeaders = ((NSHTTPURLResponse *)downloadTask.response).allHeaderFields;
// TODO: SEND bytesDownloaded AND bytesTotal
[self sendEventWithName:@"downloadComplete" body:@{@"id": taskConfig.id, @"headers": responseHeaders, @"location": taskConfig.destination}];
} else {
[self sendEventWithName:@"downloadFailed" body:@{@"id": taskConfig.id, @"error": [error localizedDescription]}];
}
}
[self removeTaskFromMap:downloadTask];
}
}
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedbytesTotal:(int64_t)expectedbytesTotal {
NSLog(@"[RNBackgroundDownloader] - [didResumeAtOffset]");
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesDownloaded bytesTotalWritten:(int64_t)bytesTotalWritten bytesTotalExpectedToWrite:(int64_t)bytesTotalExpectedToWrite {
NSLog(@"[RNBackgroundDownloader] - [didWriteData]");
@synchronized (sharedLock) {
RNBGDTaskConfig *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)];
if (taskCofig != nil) {
// NSLog(@"[RNBackgroundDownloader] - [didWriteData] destination - %@", taskCofig.destination);
if (!taskCofig.reportedBegin) {
NSDictionary *responseHeaders = ((NSHTTPURLResponse *)downloadTask.response).allHeaderFields;
if (self.bridge) {
[self sendEventWithName:@"downloadBegin" body:@{
@"id": taskCofig.id,
@"expectedBytes": [NSNumber numberWithLongLong: bytesTotalExpectedToWrite],
@"headers": responseHeaders
}];
}
taskCofig.reportedBegin = YES;
}
progressReports[taskCofig.id] = @{
@"id": taskCofig.id,
@"bytesDownloaded": [NSNumber numberWithLongLong: bytesTotalWritten],
@"bytesTotal": [NSNumber numberWithLongLong: bytesTotalExpectedToWrite]
};
NSDate *now = [[NSDate alloc] init];
if ([now timeIntervalSinceDate:lastProgressReport] > 0.25 && progressReports.count > 0) {
if (self.bridge) {
[self sendEventWithName:@"downloadProgress" body:[progressReports allValues]];
}
lastProgressReport = now;
// TODO: SHOULD REMOVE ALL progressReports ?
// IS IT ALL SENT?
[progressReports removeAllObjects];
}
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"[RNBackgroundDownloader] - [didCompleteWithError]");
@synchronized (sharedLock) {
if (error == nil)
return;
RNBGDTaskConfig *taskCofig = taskToConfigMap[@(task.taskIdentifier)];
if (taskCofig == nil)
return;
if (self.bridge) {
// TODO: SEND error AS IN OBJECT
[self sendEventWithName:@"downloadFailed" body:@{@"id": taskCofig.id, @"error": [error localizedDescription]}];
}
// IF WE CAN'T RESUME TO DOWNLOAD LATER
if (error.userInfo[NSURLSessionDownloadTaskResumeData] == nil) {
[self removeTaskFromMap:task];
}
}
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(@"[RNBackgroundDownloader] - [URLSessionDidFinishEventsForBackgroundURLSession]");
// USE completionHandler FROM JS INSTEAD OF THIS
// TOREMOVE
// if (storedCompletionHandler) {
// [[NSOperationQueue mainQueue] addOperationWithBlock:^{
// storedCompletionHandler();
// storedCompletionHandler = nil;
// }];
// }
}
#pragma mark - serialization
- (NSData *)serialize: (id)obj {
return [NSKeyedArchiver archivedDataWithRootObject:obj];
}
- (id)deserialize: (NSData *)data {
if (data == nil) {
return nil;
}
return [NSKeyedUnarchiver unarchiveObjectWithData:data];
}
@end