diff --git a/README.md b/README.md index adf604b..dd45a51 100644 --- a/README.md +++ b/README.md @@ -92,13 +92,13 @@ let task = RNBackgroundDownloader.download({ id: 'file123', url: 'https://link-to-very.large/file.zip' destination: `${RNBackgroundDownloader.directories.documents}/file.zip` -}).begin((expectedBytes) => { +}).begin(({ expectedBytes, headers }) => { console.log(`Going to download ${expectedBytes} bytes!`); -}).progress((percent) => { +}).progress(percent => { console.log(`Downloaded: ${percent * 100}%`); }).done(() => { console.log('Download is done!'); -}).error((error) => { +}).error(error => { console.log('Download canceled due to error: ', error); }); @@ -126,7 +126,7 @@ import RNBackgroundDownloader from 'react-native-background-downloader'; let lostTasks = await RNBackgroundDownloader.checkForExistingDownloads(); for (let task of lostTasks) { console.log(`Task ${task.id} was found!`); - task.progress((percent) => { + task.progress(percent => { console.log(`Downloaded: ${percent * 100}%`); }).done(() => { console.log('Downlaod is done!'); @@ -158,9 +158,9 @@ let task = RNBackgroundDownloader.download({ headers: { Authorization: 'Bearer 2we$@$@Ddd223' } -}).begin((expectedBytes) => { +}).begin(({ expectedBytes, headers }) => { console.log(`Going to download ${expectedBytes} bytes!`); -}).progress((percent) => { +}).progress(percent => { console.log(`Downloaded: ${percent * 100}%`); }).done(() => { console.log('Download is done!'); @@ -228,7 +228,7 @@ All callback methods return the current instance of the `DownloadTask` for chain | Function | Callback Arguments | Info | | ---------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `begin` | expectedBytes | Called when the first byte is received. 💡: this is good place to check if the device has enough storage space for this download | +| `begin` | { expectedBytes, headers } | Called when the first byte is received. 💡: this is good place to check if the device has enough storage space for this download | | `progress` | percent, bytesWritten, totalBytes | Called at max every 1.5s so you can update your progress bar accordingly | | `done` | | Called when the download is done, the file is at the destination you've set | | `error` | error | Called when the download stops due to an error | diff --git a/android/build.gradle b/android/build.gradle index 3018efe..c650bd4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,11 +5,11 @@ def safeExtGet(prop, fallback) { } android { - compileSdkVersion safeExtGet("compileSdkVersion", 28) + compileSdkVersion safeExtGet("compileSdkVersion", 30) defaultConfig { - minSdkVersion safeExtGet('minSdkVersion', 16) - targetSdkVersion safeExtGet('targetSdkVersion', 28) + minSdkVersion safeExtGet('minSdkVersion', 21) + targetSdkVersion safeExtGet('targetSdkVersion', 30) versionCode 1 versionName "1.0" ndk { @@ -22,8 +22,10 @@ dependencies { //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' if (project.properties['android.useAndroidX'] == 'true' || project.properties['android.useAndroidX'] == true) { - api "androidx.tonyodev.fetch2:xfetch2:3.1.4" + api "androidx.tonyodev.fetch2:xfetch2:3.1.6" + implementation "androidx.tonyodev.fetch2okhttp:xfetch2okhttp:3.1.6" } else { - api "com.tonyodev.fetch2:fetch2:3.0.10" + api "com.tonyodev.fetch2:fetch2:3.0.12" + implementation "com.tonyodev.fetch2okhttp:fetch2okhttp:3.0.12" } } diff --git a/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java b/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java index 0c976a1..8888b53 100644 --- a/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java +++ b/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java @@ -14,6 +14,8 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.tonyodev.fetch2.Download; +import com.tonyodev.fetch2core.Downloader; +import com.tonyodev.fetch2okhttp.OkHttpDownloader; import com.tonyodev.fetch2.Error; import com.tonyodev.fetch2.Fetch; import com.tonyodev.fetch2.FetchConfiguration; @@ -24,6 +26,8 @@ import com.tonyodev.fetch2.Request; import com.tonyodev.fetch2.Status; import com.tonyodev.fetch2core.DownloadBlock; import com.tonyodev.fetch2core.Func; +import com.tonyodev.fetch2.HttpUrlConnectionDownloader; +import com.tonyodev.fetch2core.Downloader; import org.jetbrains.annotations.NotNull; @@ -40,6 +44,12 @@ import java.util.Map; import javax.annotation.Nullable; +import okhttp3.OkHttpClient; + +import java.util.Set; +import java.net.URL; +import java.net.URLConnection; + public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule implements FetchListener { private static final int TASK_RUNNING = 0; @@ -72,15 +82,21 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp private DeviceEventManagerModule.RCTDeviceEventEmitter ee; private Date lastProgressReport = new Date(); private HashMap progressReports = new HashMap<>(); - private static Object sharedLock = new Object(); + private static Object sharedLock = new Object(); public RNBackgroundDownloaderModule(ReactApplicationContext reactContext) { super(reactContext); + OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); + final Downloader okHttpDownloader = new OkHttpDownloader(okHttpClient, + Downloader.FileDownloaderType.PARALLEL); + loadConfigMap(); FetchConfiguration fetchConfiguration = new FetchConfiguration.Builder(this.getReactApplicationContext()) .setDownloadConcurrentLimit(4) - .setNamespace("RNBackgroundDownloader") + .setHttpDownloader(okHttpDownloader) + .enableRetryOnNetworkGain(true) + .setHttpDownloader(new HttpUrlConnectionDownloader(Downloader.FileDownloaderType.PARALLEL)) .build(); fetch = Fetch.Impl.getInstance(fetchConfiguration); fetch.addListener(this); @@ -164,7 +180,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp e.printStackTrace(); } } - + private int convertErrorCode(Error error) { if ((error == Error.FILE_NOT_CREATED) || (error == Error.WRITE_PERMISSION_DENIED)) { @@ -205,7 +221,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp } request.setPriority(options.hasKey("priority") ? Priority.valueOf(options.getInt("priority")) : Priority.NORMAL); request.setNetworkType(options.hasKey("network") ? NetworkType.valueOf(options.getInt("network")) : NetworkType.ALL); - + fetch.enqueue(request, new Func() { @Override public void call(Request download) { @@ -214,7 +230,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp @Override public void call(Error error) { //An error occurred when enqueuing a request. - + WritableMap params = Arguments.createMap(); params.putString("id", id); params.putString("error", error.toString()); @@ -232,6 +248,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp ); synchronized(sharedLock) { + lastProgressReport = new Date(); idToRequestId.put(id, request.getId()); requestIdToConfig.put(request.getId(), config); saveConfigMap(); @@ -288,6 +305,8 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp foundIds.pushMap(params); + // TODO: MAYBE ADD headers + idToRequestId.put(config.id, download.getId()); config.reportedBegin = true; } else { @@ -309,6 +328,9 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp if (config != null) { WritableMap params = Arguments.createMap(); params.putString("id", config.id); + + // TODO: add location + ee.emit("downloadComplete", params); } @@ -331,16 +353,44 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp params.putString("id", config.id); if (!config.reportedBegin) { - params.putInt("expectedBytes", (int)download.getTotal()); - ee.emit("downloadBegin", params); config.reportedBegin = true; + + params.putInt("expectedBytes", (int)download.getTotal()); + + // TODO: MAKE IT IN CUSTOM DOWNLOADER + // https://github.com/tonyofrancis/Fetch/issues/347#issuecomment-478349299 + Thread th = new Thread(() -> { + try { + WritableMap headersMap = Arguments.createMap(); + + URL urlC = new URL(download.getUrl()); + URLConnection con = urlC.openConnection(); + Map> headers = con.getHeaderFields(); + Set keys = headers.keySet(); + for (String key : keys) { + String val = con.getHeaderField(key); + headersMap.putString(key, val); + } + params.putMap("headers", headersMap); + + ee.emit("downloadBegin", params); + } catch (IOException e) { + e.printStackTrace(); + } + }); + try { + th.start(); + th.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } } else { params.putInt("written", (int)download.getDownloaded()); params.putInt("total", (int)download.getTotal()); params.putDouble("percent", ((double)download.getProgress()) / 100); progressReports.put(config.id, params); Date now = new Date(); - if (now.getTime() - lastProgressReport.getTime() > 1500) { + if (now.getTime() - lastProgressReport.getTime() > 250) { WritableArray reportsArray = Arguments.createArray(); for (WritableMap report : progressReports.values()) { reportsArray.pushMap(report); diff --git a/index.js b/index.js index 0ce997c..8ef577d 100644 --- a/index.js +++ b/index.js @@ -15,12 +15,12 @@ RNBackgroundDownloaderEmitter.addListener('downloadProgress', events => { } }); -RNBackgroundDownloaderEmitter.addListener('downloadComplete', event => { - let task = tasksMap.get(event.id); +RNBackgroundDownloaderEmitter.addListener('downloadComplete', ({ id, location }) => { + let task = tasksMap.get(id); if (task) { - task._onDone(event.location); + task._onDone({ location }); } - tasksMap.delete(event.id); + tasksMap.delete(id); }); RNBackgroundDownloaderEmitter.addListener('downloadFailed', event => { @@ -31,10 +31,10 @@ RNBackgroundDownloaderEmitter.addListener('downloadFailed', event => { tasksMap.delete(event.id); }); -RNBackgroundDownloaderEmitter.addListener('downloadBegin', event => { - let task = tasksMap.get(event.id); +RNBackgroundDownloaderEmitter.addListener('downloadBegin', ({ id, expectedBytes, headers }) => { + let task = tasksMap.get(id); if (task) { - task._onBegin(event.expectedBytes); + task._onBegin({ expectedBytes, headers }); } }); @@ -49,7 +49,7 @@ export function checkForExistingDownloads() { return RNBackgroundDownloader.checkForExistingDownloads() .then(foundTasks => { return foundTasks.map(taskInfo => { - let task = new DownloadTask(taskInfo); + let task = new DownloadTask(taskInfo,tasksMap.get(taskInfo.id)); if (taskInfo.state === RNBackgroundDownloader.TaskRunning) { task.state = 'DOWNLOADING'; } else if (taskInfo.state === RNBackgroundDownloader.TaskSuspended) { @@ -71,6 +71,10 @@ export function checkForExistingDownloads() { }); } +export function completeHandler (jobId) { + return RNBackgroundDownloader.completeHandler(jobId) +} + export function download(options) { if (!options.id || !options.url || !options.destination) { throw new Error('[RNBackgroundDownloader] id, url and destination are required'); @@ -107,6 +111,7 @@ export const Priority = { export default { download, checkForExistingDownloads, + completeHandler, setHeaders, directories, Network, diff --git a/ios/RNBackgroundDownloader.m b/ios/RNBackgroundDownloader.m index 7fd7912..1346f0a 100644 --- a/ios/RNBackgroundDownloader.m +++ b/ios/RNBackgroundDownloader.m @@ -23,6 +23,7 @@ static CompletionHandler storedCompletionHandler; NSMutableDictionary *progressReports; NSDate *lastProgressReport; NSNumber *sharedLock; + BOOL isNotificationCenterInited; } RCT_EXPORT_MODULE(); @@ -42,7 +43,7 @@ RCT_EXPORT_MODULE(); - (NSDictionary *)constantsToExport { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - + return @{ @"documents": [paths firstObject], @"TaskRunning": @(NSURLSessionTaskStateRunning), @@ -53,6 +54,7 @@ RCT_EXPORT_MODULE(); } - (id) init { + NSLog(@"[RNBackgroundDownloader] - [init]"); self = [super init]; if (self) { taskToConfigMap = [self deserialize:[[NSUserDefaults standardUserDefaults] objectForKey:ID_TO_CONFIG_MAP_KEY]]; @@ -65,6 +67,18 @@ RCT_EXPORT_MODULE(); 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]; @@ -73,12 +87,63 @@ RCT_EXPORT_MODULE(); } - (void)lazyInitSession { - if (urlSession == nil) { - urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; + 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] 1"); + @synchronized (self->sharedLock) { + [urlSession getTasksWithCompletionHandler:^(NSArray * _Nonnull dataTasks, NSArray * _Nonnull uploadTasks, NSArray * _Nonnull downloadTasks) { + NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 2"); + for (NSURLSessionDownloadTask *task in downloadTasks) { + NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 3 - state - %@", [NSNumber numberWithInt:task.state]); + // running - 0 + // suspended - 1 + // canceling - 2 + // completed - 3 + NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 4 - totalBytes - %@", [NSNumber numberWithInt:task.countOfBytesExpectedToReceive]); + NSLog(@"[RNBackgroundDownloader] - [resumeTasks] 5 - bytesWritten - %@", [NSNumber numberWithInt:task.countOfBytesReceived]); + + 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]; @@ -98,6 +163,7 @@ RCT_EXPORT_MODULE(); } + (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler { + NSLog(@"[RNBackgroundDownloader] - [setCompletionHandlerWithIdentifier]"); NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; if ([sessonIdentifier isEqualToString:identifier]) { @@ -105,9 +171,31 @@ RCT_EXPORT_MODULE(); } } +- (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"]; @@ -116,17 +204,22 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) { NSLog(@"[RNBackgroundDownloader] - [Error] id, url and destination must be set"); return; } - [self lazyInitSession]; - + 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}]; taskToConfigMap[@(task.taskIdentifier)] = taskConfig; @@ -134,12 +227,14 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) { idToTaskMap[identifier] = task; idToPercentMap[identifier] = @0.0; - + [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) { @@ -149,6 +244,7 @@ RCT_EXPORT_METHOD(pauseTask: (NSString *)identifier) { } RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) { + NSLog(@"[RNBackgroundDownloader] - [resumeTask]"); @synchronized (sharedLock) { NSURLSessionDownloadTask *task = idToTaskMap[identifier]; if (task != nil && task.state == NSURLSessionTaskStateSuspended) { @@ -158,6 +254,7 @@ RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) { } RCT_EXPORT_METHOD(stopTask: (NSString *)identifier) { + NSLog(@"[RNBackgroundDownloader] - [stopTask]"); @synchronized (sharedLock) { NSURLSessionDownloadTask *task = idToTaskMap[identifier]; if (task != nil) { @@ -168,6 +265,7 @@ RCT_EXPORT_METHOD(stopTask: (NSString *)identifier) { } RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + NSLog(@"[RNBackgroundDownloader] - [checkForExistingDownloads]"); [self lazyInitSession]; [urlSession getTasksWithCompletionHandler:^(NSArray * _Nonnull dataTasks, NSArray * _Nonnull uploadTasks, NSArray * _Nonnull downloadTasks) { NSMutableArray *idsFound = [[NSMutableArray alloc] init]; @@ -205,22 +303,37 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej }]; } +RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSLog(@"[RNBackgroundDownloader] - [completeHandlerIOS]"); + if (storedCompletionHandler) { + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + 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 *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)]; - if (taskCofig != nil) { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *destURL = [NSURL fileURLWithPath:taskCofig.destination]; - [fileManager createDirectoryAtURL:[destURL URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; - [fileManager removeItemAtURL:destURL error:nil]; - NSError *moveError; - BOOL moved = [fileManager moveItemAtURL:location toURL:destURL error:&moveError]; + 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 (moved) { - [self sendEventWithName:@"downloadComplete" body:@{@"id": taskCofig.id}]; + if (error == nil) { + NSDictionary *responseHeaders = ((NSHTTPURLResponse *)downloadTask.response).allHeaderFields; + [self sendEventWithName:@"downloadComplete" body:@{@"id": taskConfig.id, @"headers": responseHeaders, @"location": taskConfig.destination}]; } else { - [self sendEventWithName:@"downloadFailed" body:@{@"id": taskCofig.id, @"error": [moveError localizedDescription]}]; + [self sendEventWithName:@"downloadFailed" body:@{@"id": taskConfig.id, @"error": [error localizedDescription]}]; } } [self removeTaskFromMap:downloadTask]; @@ -229,26 +342,33 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { + NSLog(@"[RNBackgroundDownloader] - [didResumeAtOffset]"); } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { + NSLog(@"[RNBackgroundDownloader] - [didWriteData]"); @synchronized (sharedLock) { RNBGDTaskConfig *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)]; if (taskCofig != nil) { if (!taskCofig.reportedBegin) { - [self sendEventWithName:@"downloadBegin" body:@{@"id": taskCofig.id, @"expectedBytes": [NSNumber numberWithLongLong: totalBytesExpectedToWrite]}]; + NSDictionary *responseHeaders = ((NSHTTPURLResponse *)downloadTask.response).allHeaderFields; + [self sendEventWithName:@"downloadBegin" body:@{ + @"id": taskCofig.id, + @"expectedBytes": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], + @"headers": responseHeaders + }]; taskCofig.reportedBegin = YES; } - + NSNumber *prevPercent = idToPercentMap[taskCofig.id]; NSNumber *percent = [NSNumber numberWithFloat:(float)totalBytesWritten/(float)totalBytesExpectedToWrite]; if ([percent floatValue] - [prevPercent floatValue] > 0.01f) { progressReports[taskCofig.id] = @{@"id": taskCofig.id, @"written": [NSNumber numberWithLongLong: totalBytesWritten], @"total": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], @"percent": percent}; idToPercentMap[taskCofig.id] = percent; } - + NSDate *now = [[NSDate alloc] init]; - if ([now timeIntervalSinceDate:lastProgressReport] > 1.5 && progressReports.count > 0) { + if ([now timeIntervalSinceDate:lastProgressReport] > 0.25 && progressReports.count > 0) { if (self.bridge) { [self sendEventWithName:@"downloadProgress" body:[progressReports allValues]]; } @@ -260,6 +380,7 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { + NSLog(@"[RNBackgroundDownloader] - [didCompleteWithError]"); @synchronized (sharedLock) { RNBGDTaskConfig *taskCofig = taskToConfigMap[@(task.taskIdentifier)]; if (error != nil && error.code != -999 && taskCofig != nil) { @@ -272,12 +393,15 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej } - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { - if (storedCompletionHandler) { - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - storedCompletionHandler(); - storedCompletionHandler = nil; - }]; - } + NSLog(@"[RNBackgroundDownloader] - [URLSessionDidFinishEventsForBackgroundURLSession]"); + // USE completionHandler FROM JS INSTEAD OF THIS + // TOREMOVE + // if (storedCompletionHandler) { + // [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + // storedCompletionHandler(); + // storedCompletionHandler = nil; + // }]; + // } } #pragma mark - serialization @@ -289,7 +413,7 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej if (data == nil) { return nil; } - + return [NSKeyedUnarchiver unarchiveObjectWithData:data]; } diff --git a/lib/downloadTask.js b/lib/downloadTask.js index 5de948d..747205a 100644 --- a/lib/downloadTask.js +++ b/lib/downloadTask.js @@ -12,7 +12,7 @@ export default class DownloadTask { bytesWritten = 0 totalBytes = 0 - constructor(taskInfo) { + constructor (taskInfo, originalTask) { if (typeof taskInfo === 'string') { this.id = taskInfo; } else { @@ -21,40 +21,47 @@ export default class DownloadTask { this.bytesWritten = taskInfo.bytesWritten; this.totalBytes = taskInfo.totalBytes; } + + if (originalTask) { + this._beginHandler = originalTask._beginHandler + this._progressHandler = originalTask._progressHandler + this._doneHandler = originalTask._doneHandler + this._errorHandler = originalTask._errorHandler + } } - begin(handler) { + begin (handler) { validateHandler(handler); this._beginHandler = handler; return this; } - progress(handler) { + progress (handler) { validateHandler(handler); this._progressHandler = handler; return this; } - done(handler) { + done (handler) { validateHandler(handler); this._doneHandler = handler; return this; } - error(handler) { + error (handler) { validateHandler(handler); this._errorHandler = handler; return this; } - _onBegin(expectedBytes) { + _onBegin ({ expectedBytes, headers }) { this.state = 'DOWNLOADING'; if (this._beginHandler) { - this._beginHandler(expectedBytes); + this._beginHandler({ expectedBytes, headers }); } } - _onProgress(percent, bytesWritten, totalBytes) { + _onProgress (percent, bytesWritten, totalBytes) { this.percent = percent; this.bytesWritten = bytesWritten; this.totalBytes = totalBytes; @@ -63,31 +70,31 @@ export default class DownloadTask { } } - _onDone() { + _onDone ({ location }) { this.state = 'DONE'; if (this._doneHandler) { - this._doneHandler(); + this._doneHandler({ location }); } } - _onError(error, errorCode) { + _onError (error, errorCode) { this.state = 'FAILED'; if (this._errorHandler) { this._errorHandler(error, errorCode); } } - pause() { + pause () { this.state = 'PAUSED'; RNBackgroundDownloader.pauseTask(this.id); } - resume() { + resume () { this.state = 'DOWNLOADING'; RNBackgroundDownloader.resumeTask(this.id); } - stop() { + stop () { this.state = 'STOPPED'; RNBackgroundDownloader.stopTask(this.id); }