From 286a7eada6315292a00a78c452b69777fd9a4263 Mon Sep 17 00:00:00 2001 From: Kesha Antonov Date: Mon, 1 Jan 2024 21:29:30 +0300 Subject: [PATCH] improved progress events on ios/android --- android/src/main/java/com/eko/OnProgress.java | 25 +-- .../main/java/com/eko/RNBGDTaskConfig.java | 4 +- .../com/eko/RNBackgroundDownloaderModule.java | 168 +++++++++++++++--- index.d.ts | 11 +- index.ts | 62 ++++--- ios/RNBackgroundDownloader.m | 107 ++++++++--- 6 files changed, 273 insertions(+), 104 deletions(-) diff --git a/android/src/main/java/com/eko/OnProgress.java b/android/src/main/java/com/eko/OnProgress.java index 65be559..2e3cd8b 100644 --- a/android/src/main/java/com/eko/OnProgress.java +++ b/android/src/main/java/com/eko/OnProgress.java @@ -19,19 +19,20 @@ public class OnProgress extends Thread { private Cursor cursor; private int lastBytesDownloaded; private int bytesTotal; + private ProgressCallback callback; private RNBGDTaskConfig config; - private DeviceEventManagerModule.RCTDeviceEventEmitter ee; public OnProgress(RNBGDTaskConfig config, long downloadId, - DeviceEventManagerModule.RCTDeviceEventEmitter ee, Downloader downloader) { + Downloader downloader, + ProgressCallback callback) { this.config = config; + this.callback = callback; this.downloadId = downloadId; this.query = new DownloadManager.Query(); query.setFilterById(this.downloadId); - this.ee = ee; this.downloader = downloader; } @@ -73,7 +74,7 @@ public class OnProgress extends Thread { Thread.sleep(1000); } else { Log.d("RNBackgroundDownloader", "RNBD: OnProgress-2.3. downloadId " + downloadId); - Thread.sleep(config.progressInterval); + Thread.sleep(250); } // get total bytes of the file @@ -93,21 +94,7 @@ public class OnProgress extends Thread { + bytesDownloaded + " bytesTotal " + bytesTotal); if (lastBytesDownloaded > 0 && bytesTotal > 0) { - WritableMap params = Arguments.createMap(); - params.putString("id", config.id); - params.putInt("bytesDownloaded", (int) lastBytesDownloaded); - params.putInt("bytesTotal", (int) bytesTotal); - - HashMap progressReports = new HashMap<>(); - progressReports.put(config.id, params); - - WritableArray reportsArray = Arguments.createArray(); - for (WritableMap report : progressReports.values()) { - reportsArray.pushMap(report); - } - - Log.d("RNBackgroundDownloader", "RNBD: OnProgress-4. downloadId " + downloadId); - ee.emit("downloadProgress", reportsArray); + callback.onProgress(config.id, lastBytesDownloaded, bytesTotal); } } catch (Exception e) { return; diff --git a/android/src/main/java/com/eko/RNBGDTaskConfig.java b/android/src/main/java/com/eko/RNBGDTaskConfig.java index 9604437..70ca158 100644 --- a/android/src/main/java/com/eko/RNBGDTaskConfig.java +++ b/android/src/main/java/com/eko/RNBGDTaskConfig.java @@ -8,14 +8,12 @@ public class RNBGDTaskConfig implements Serializable { public String destination; public String metadata = "{}"; public boolean reportedBegin; - public int progressInterval; - public RNBGDTaskConfig(String id, String url, String destination, String metadata, int progressInterval) { + public RNBGDTaskConfig(String id, String url, String destination, String metadata) { this.id = id; this.url = url; this.destination = destination; this.metadata = metadata; this.reportedBegin = false; - this.progressInterval = progressInterval; } } diff --git a/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java b/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java index c689fb5..8521b69 100644 --- a/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java +++ b/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java @@ -80,8 +80,12 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { private Downloader downloader; - private Map condigIdToDownloadId = new HashMap<>(); + private Map configIdToDownloadId = new HashMap<>(); private Map downloadIdToConfig = new HashMap<>(); + private Map configIdToPercent = new HashMap<>(); + private Map progressReports = new HashMap<>(); + private Date lastProgressReportedAt = new Date(); + private int progressInterval; private DeviceEventManagerModule.RCTDeviceEventEmitter ee; private Map onProgressThreads = new HashMap<>(); @@ -150,6 +154,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { public RNBackgroundDownloaderModule(ReactApplicationContext reactContext) { super(reactContext); + loadDownloadIdToConfigMap(); loadConfigMap(); downloader = new Downloader(reactContext); @@ -183,6 +188,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { if (onProgressTh != null) { onProgressTh.interrupt(); onProgressThreads.remove(configId); + configIdToPercent.remove(configId); } } @@ -235,19 +241,20 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { synchronized (sharedLock) { RNBGDTaskConfig config = downloadIdToConfig.get(downloadId); if (config != null) { - condigIdToDownloadId.remove(config.id); + configIdToDownloadId.remove(config.id); downloadIdToConfig.remove(downloadId); + configIdToPercent.remove(config.id); - saveConfigMap(); + saveDownloadIdToConfigMap(); } } } - private void saveConfigMap() { - Log.d(getName(), "RNBD: saveConfigMap"); + private void saveDownloadIdToConfigMap() { + Log.d(getName(), "RNBD: saveDownloadIdToConfigMap"); synchronized (sharedLock) { - File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); + File file = new File(this.getReactApplicationContext().getFilesDir(), getName() + "_downloadIdToConfig"); try { ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file)); outputStream.writeObject(downloadIdToConfig); @@ -259,15 +266,74 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { } } - private void loadConfigMap() { + private void loadDownloadIdToConfigMap() { + Log.d(getName(), "RNBD: loadDownloadIdToConfigMap"); + + File file = new File(this.getReactApplicationContext().getFilesDir(), getName() + "_downloadIdToConfig"); + if (file.exists()) { + try { + ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file)); + downloadIdToConfig = (Map) inputStream.readObject(); + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + } + } + + private void saveConfigMap () { + Log.d(getName(), "RNBD: saveConfigMap"); + + synchronized (sharedLock) { + File file = new File(this.getReactApplicationContext().getFilesDir(), getName() + "_config"); + try { + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file)); + + Map config = new HashMap<>(); + config.put("progressInterval", Integer.toString(progressInterval)); + outputStream.writeObject(config); + outputStream.flush(); + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private void loadConfigMap () { Log.d(getName(), "RNBD: loadConfigMap"); - File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); - try { - ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file)); - downloadIdToConfig = (Map) inputStream.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); + File file = new File(this.getReactApplicationContext().getFilesDir(), getName() + "_config"); + if (file.exists()) { + try { + ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file)); + Map config = (HashMap) inputStream.readObject(); + // iterate over config + for(Map.Entry entry : config.entrySet()) { + String key = entry.getKey(); + + Object valueObj = entry.getValue(); + Log.d(getName(), "RNBD: loadConfigMap-1 " + " key " + key + " valueObj " + valueObj); + String value = null; + if (valueObj instanceof Long) { + value = Long.toString((Long) valueObj); + } else if (valueObj instanceof String) { + value = (String) valueObj; + } + + if (key.equals("progressInterval") && value != null) { + try { + int _progressInterval = Integer.parseInt(value); + if (_progressInterval > 0) { + progressInterval = _progressInterval; + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } } } @@ -279,19 +345,23 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { String destination = options.getString("destination"); ReadableMap headers = options.getMap("headers"); String metadata = options.getString("metadata"); - int progressInterval = options.getInt("progressInterval"); + int _progressInterval = options.getInt("progressInterval"); + if (_progressInterval > 0) { + progressInterval = _progressInterval; + saveConfigMap(); + } boolean isAllowedOverRoaming = options.getBoolean("isAllowedOverRoaming"); boolean isAllowedOverMetered = options.getBoolean("isAllowedOverMetered"); - Log.d(getName(), "RNBD: download " + id + " " + url + " " + destination + " " + metadata); + Log.d(getName(), "RNBD: download " + id + " " + url + " " + destination + " " + metadata + " " + progressInterval); if (id == null || url == null || destination == null) { Log.e(getName(), "id, url and destination must be set"); return; } - RNBGDTaskConfig config = new RNBGDTaskConfig(id, url, destination, metadata, progressInterval); + RNBGDTaskConfig config = new RNBGDTaskConfig(id, url, destination, metadata); final Request request = new Request(Uri.parse(url)); request.setAllowedOverRoaming(isAllowedOverRoaming); request.setAllowedOverMetered(isAllowedOverMetered); @@ -320,9 +390,10 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { long downloadId = downloader.queueDownload(request); synchronized (sharedLock) { - condigIdToDownloadId.put(id, downloadId); + configIdToDownloadId.put(id, downloadId); downloadIdToConfig.put(downloadId, config); - saveConfigMap(); + configIdToPercent.put(id, 0.0); + saveDownloadIdToConfigMap(); WritableMap downloadStatus = downloader.checkDownloadStatus(downloadId); int status = downloadStatus.getInt("status"); @@ -341,7 +412,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { Log.d(getName(), "RNBD: startReportingTasks-1 downloadId " + downloadId + " config.id " + config.id); config.reportedBegin = true; downloadIdToConfig.put(downloadId, config); - saveConfigMap(); + saveDownloadIdToConfigMap(); Log.d(getName(), "RNBD: startReportingTasks-2 downloadId: " + downloadId); // report begin & progress @@ -376,7 +447,45 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { onBeginTh.join(); Log.d(getName(), "RNBD: startReportingTasks-4 downloadId: " + downloadId); - OnProgress onProgressTh = new OnProgress(config, downloadId, ee, downloader); + OnProgress onProgressTh = new OnProgress( + config, + downloadId, + downloader, + new ProgressCallback() { + @Override + public void onProgress(String configId, int bytesDownloaded, int bytesTotal) { + Log.d(getName(), "RNBD: onProgress-1 " + configId + " " + bytesDownloaded + " " + bytesTotal); + + double prevPercent = configIdToPercent.get(configId); + double percent = (double) bytesDownloaded / bytesTotal; + if (percent - prevPercent > 0.01) { + Log.d(getName(), "RNBD: onProgress-2 " + configId + " " + bytesDownloaded + " " + bytesTotal); + WritableMap params = Arguments.createMap(); + params.putString("id", configId); + params.putInt("bytesDownloaded", bytesDownloaded); + params.putInt("bytesTotal", bytesTotal); + + progressReports.put(configId, params); + configIdToPercent.put(configId, percent); + } + + Date now = new Date(); + if ( + now.getTime() - lastProgressReportedAt.getTime() > progressInterval && + progressReports.size() > 0 + ) { + Log.d(getName(), "RNBD: onProgress-3 " + configId + " " + bytesDownloaded + " " + bytesTotal); + WritableArray reportsArray = Arguments.createArray(); + for (Object report : progressReports.values()) { + reportsArray.pushMap((WritableMap) report); + } + ee.emit("downloadProgress", reportsArray); + lastProgressReportedAt = now; + progressReports.clear(); + } + } + } + ); onProgressThreads.put(config.id, onProgressTh); onProgressTh.start(); Log.d(getName(), "RNBD: startReportingTasks-5 downloadId: " + downloadId); @@ -394,7 +503,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { Log.d(getName(), "RNBD: pauseTask " + configId); synchronized (sharedLock) { - Long downloadId = condigIdToDownloadId.get(configId); + Long downloadId = configIdToDownloadId.get(configId); if (downloadId != null) { downloader.pauseDownload(downloadId); } @@ -407,7 +516,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { Log.d(getName(), "RNBD: resumeTask " + configId); synchronized (sharedLock) { - Long downloadId = condigIdToDownloadId.get(configId); + Long downloadId = configIdToDownloadId.get(configId); if (downloadId != null) { downloader.resumeDownload(downloadId); } @@ -419,7 +528,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { Log.d(getName(), "RNBD: stopTask-1 " + configId); synchronized (sharedLock) { - Long downloadId = condigIdToDownloadId.get(configId); + Long downloadId = configIdToDownloadId.get(configId); Log.d(getName(), "RNBD: stopTask-2 " + configId + " downloadId " + downloadId); if (downloadId != null) { // DELETES CONFIG HERE SO receiver WILL NOT THROW ERROR DOWNLOAD_FAILED TO THE @@ -437,7 +546,7 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { Log.d(getName(), "RNBD: completeHandler-1 " + configId); synchronized (sharedLock) { - Long downloadId = condigIdToDownloadId.get(configId); + Long downloadId = configIdToDownloadId.get(configId); Log.d(getName(), "RNBD: completeHandler-2 " + configId + " downloadId " + downloadId); if (downloadId != null) { removeFromMaps(downloadId); @@ -470,14 +579,19 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { params.putString("id", config.id); params.putString("metadata", config.metadata); params.putInt("state", stateMap.get(downloadStatus.getInt("status"))); - params.putInt("bytesDownloaded", downloadStatus.getInt("bytesDownloaded")); - params.putInt("bytesTotal", downloadStatus.getInt("bytesTotal")); + + int bytesDownloaded = downloadStatus.getInt("bytesDownloaded"); + params.putInt("bytesDownloaded", bytesDownloaded); + + int bytesTotal = downloadStatus.getInt("bytesTotal"); + params.putInt("bytesTotal", bytesTotal); foundIds.pushMap(params); // TODO: MAYBE ADD headers - condigIdToDownloadId.put(config.id, downloadId); + configIdToDownloadId.put(config.id, downloadId); + configIdToPercent.put(config.id, (double) bytesDownloaded / bytesTotal); // TOREMOVE // config.reportedBegin = true; diff --git a/index.d.ts b/index.d.ts index 2a20a7a..45475eb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,7 +9,11 @@ export interface DownloadHeaders { [key: string]: string | null; } -type SetHeaders = (h: DownloadHeaders) => void; +type SetConfig = ( + headers: DownloadHeaders, + progressInterval: number, + isLogsEnabled: boolean +) => void; export interface BeginHandlerObject { expectedBytes: number; @@ -102,7 +106,6 @@ export interface DownloadOption { metadata?: object; isAllowedOverRoaming?: boolean; isAllowedOverMetered?: boolean; - progressInterval?: number; } export type Download = (options: DownloadOption) => DownloadTask; @@ -112,7 +115,7 @@ export interface Directories { documents: string; } -export const setHeaders: SetHeaders +export const setConfig: SetConfig export const checkForExistingDownloads: CheckForExistingDownloads export const ensureDownloadsAreRunning: EnsureDownloadsAreRunning export const download: Download @@ -120,7 +123,7 @@ export const completeHandler: CompleteHandler export const directories: Directories export interface RNBackgroundDownloader { - setHeaders: SetHeaders; + setConfig: SetConfig; checkForExistingDownloads: CheckForExistingDownloads; ensureDownloadsAreRunning: EnsureDownloadsAreRunning; download: Download; diff --git a/index.ts b/index.ts index b6d92b6..198d90a 100644 --- a/index.ts +++ b/index.ts @@ -5,26 +5,36 @@ const { RNBackgroundDownloader } = NativeModules const RNBackgroundDownloaderEmitter = new NativeEventEmitter(RNBackgroundDownloader) const tasksMap = new Map() -let headers = {} + +const config = { + headers: {}, + progressInterval: 1000, + isLogsEnabled: false, +} + +function log(...args) { + if (config.isLogsEnabled) + console.log('[RNBackgroundDownloader]', ...args) +} RNBackgroundDownloaderEmitter.addListener('downloadBegin', ({ id, ...rest }) => { - console.log('[RNBackgroundDownloader] downloadBegin', id, rest) + log('[RNBackgroundDownloader] downloadBegin', id, rest) const task = tasksMap.get(id) task?.onBegin(rest) }) RNBackgroundDownloaderEmitter.addListener('downloadProgress', events => { - // console.log('[RNBackgroundDownloader] downloadProgress-1', events, tasksMap) + // log('[RNBackgroundDownloader] downloadProgress-1', events, tasksMap) for (const event of events) { const { id, ...rest } = event const task = tasksMap.get(id) - // console.log('[RNBackgroundDownloader] downloadProgress-2', id, task) + // log('[RNBackgroundDownloader] downloadProgress-2', id, task) task?.onProgress(rest) } }) RNBackgroundDownloaderEmitter.addListener('downloadComplete', ({ id, ...rest }) => { - console.log('[RNBackgroundDownloader] downloadComplete', id, rest) + log('[RNBackgroundDownloader] downloadComplete', id, rest) const task = tasksMap.get(id) task?.onDone(rest) @@ -32,29 +42,34 @@ RNBackgroundDownloaderEmitter.addListener('downloadComplete', ({ id, ...rest }) }) RNBackgroundDownloaderEmitter.addListener('downloadFailed', ({ id, ...rest }) => { - console.log('[RNBackgroundDownloader] downloadFailed', id, rest) + log('[RNBackgroundDownloader] downloadFailed', id, rest) const task = tasksMap.get(id) task?.onError(rest) tasksMap.delete(id) }) -export function setHeaders (h = {}) { - if (typeof h !== 'object') - throw new Error('[RNBackgroundDownloader] headers must be an object') +export function setConfig({ headers, progressInterval, isLogsEnabled }) { + if (typeof headers === 'object') config.headers = headers - headers = h + if (progressInterval != null) + if (typeof progressInterval === 'number' && progressInterval >= 200) + config.progressInterval = progressInterval + else + console.warn(`[RNBackgroundDownloader] progressInterval must be a number >= 200. You passed ${progressInterval}`) + + if (typeof isLogsEnabled === 'boolean') config.isLogsEnabled = isLogsEnabled } -export function checkForExistingDownloads () { - console.log('[RNBackgroundDownloader] checkForExistingDownloads-1') +export function checkForExistingDownloads() { + log('[RNBackgroundDownloader] checkForExistingDownloads-1') return RNBackgroundDownloader.checkForExistingDownloads() .then(foundTasks => { - console.log('[RNBackgroundDownloader] checkForExistingDownloads-2', foundTasks) + log('[RNBackgroundDownloader] checkForExistingDownloads-2', foundTasks) return foundTasks.map(taskInfo => { // SECOND ARGUMENT RE-ASSIGNS EVENT HANDLERS const task = new DownloadTask(taskInfo, tasksMap.get(taskInfo.id)) - console.log('[RNBackgroundDownloader] checkForExistingDownloads-3', taskInfo) + log('[RNBackgroundDownloader] checkForExistingDownloads-3', taskInfo) if (taskInfo.state === RNBackgroundDownloader.TaskRunning) { task.state = 'DOWNLOADING' @@ -76,8 +91,8 @@ export function checkForExistingDownloads () { }) } -export function ensureDownloadsAreRunning () { - console.log('[RNBackgroundDownloader] ensureDownloadsAreRunning') +export function ensureDownloadsAreRunning() { + log('[RNBackgroundDownloader] ensureDownloadsAreRunning') return checkForExistingDownloads() .then(tasks => { for (const task of tasks) @@ -88,7 +103,7 @@ export function ensureDownloadsAreRunning () { }) } -export function completeHandler (jobId: string) { +export function completeHandler(jobId: string) { if (jobId == null) { console.warn('[RNBackgroundDownloader] completeHandler: jobId is empty') return @@ -105,15 +120,14 @@ type DownloadOptions = { metadata?: object, isAllowedOverRoaming?: boolean, isAllowedOverMetered?: boolean, - progressInterval?: number, } -export function download (options: DownloadOptions) { - console.log('[RNBackgroundDownloader] download', options) +export function download(options: DownloadOptions) { + log('[RNBackgroundDownloader] download', options) if (!options.id || !options.url || !options.destination) throw new Error('[RNBackgroundDownloader] id, url and destination are required') - options.headers = { ...headers, ...(options.headers || {}) } + options.headers = { ...config.headers, ...options.headers } if (!(options.metadata && typeof options.metadata === 'object')) options.metadata = {} @@ -122,7 +136,6 @@ export function download (options: DownloadOptions) { if (options.isAllowedOverRoaming == null) options.isAllowedOverRoaming = true if (options.isAllowedOverMetered == null) options.isAllowedOverMetered = true - if (options.progressInterval == null) options.progressInterval = 1000 const task = new DownloadTask({ id: options.id, @@ -133,6 +146,7 @@ export function download (options: DownloadOptions) { RNBackgroundDownloader.download({ ...options, metadata: JSON.stringify(options.metadata), + progressInterval: config.progressInterval, }) return task @@ -147,6 +161,8 @@ export default { checkForExistingDownloads, ensureDownloadsAreRunning, completeHandler, - setHeaders, + + setConfig, + directories, } diff --git a/ios/RNBackgroundDownloader.m b/ios/RNBackgroundDownloader.m index 36f00e7..c398081 100644 --- a/ios/RNBackgroundDownloader.m +++ b/ios/RNBackgroundDownloader.m @@ -10,6 +10,7 @@ #import "RNBGDTaskConfig.h" #define ID_TO_CONFIG_MAP_KEY @"com.eko.bgdownloadidmap" +#define CONFIG_MAP_KEY @"com.eko.config_map" static CompletionHandler storedCompletionHandler; @@ -19,9 +20,11 @@ static CompletionHandler storedCompletionHandler; NSMutableDictionary *taskToConfigMap; NSMutableDictionary *idToTaskMap; NSMutableDictionary *idToResumeDataMap; + NSMutableDictionary *idToPercentMap; NSMutableDictionary *progressReports; - NSDate *lastProgressReport; + NSDate *lastProgressReportedAt; NSNumber *sharedLock; + float progressInterval; // IN SECONDS BOOL isNotificationCenterInited; } @@ -65,8 +68,22 @@ RCT_EXPORT_MODULE(); if (taskToConfigMap == nil) { taskToConfigMap = [[NSMutableDictionary alloc] init]; } - idToTaskMap = [[NSMutableDictionary alloc] init]; + + NSDictionary *configMap = [self deserialize:[[NSUserDefaults standardUserDefaults] objectForKey:CONFIG_MAP_KEY]]; + if (configMap != nil) { + for (NSString *key in configMap) { + if ([key isEqual: @"progressInterval"]) { + progressInterval = [configMap[key] intValue]; + } + } + } + if (isnan(progressInterval)) { + progressInterval = 1.0; + } + + self->idToTaskMap = [[NSMutableDictionary alloc] init]; idToResumeDataMap = [[NSMutableDictionary alloc] init]; + idToPercentMap = [[NSMutableDictionary alloc] init]; NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessonIdentifier]; @@ -83,7 +100,7 @@ RCT_EXPORT_MODULE(); } progressReports = [[NSMutableDictionary alloc] init]; - lastProgressReport = [[NSDate alloc] init]; + lastProgressReportedAt = [[NSDate alloc] init]; sharedLock = [NSNumber numberWithInt:1]; } return self; @@ -151,7 +168,8 @@ RCT_EXPORT_MODULE(); [[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY]; if (taskConfig) { - [idToTaskMap removeObjectForKey:taskConfig.id]; + [self->idToTaskMap removeObjectForKey:taskConfig.id]; + [idToPercentMap removeObjectForKey:taskConfig.id]; } // TOREMOVE - GIVES ERROR IN JS ON HOT RELOAD // if (taskToConfigMap.count == 0) { @@ -194,12 +212,24 @@ RCT_EXPORT_MODULE(); #pragma mark - JS exported methods RCT_EXPORT_METHOD(download: (NSDictionary *) options) { - NSLog(@"[RNBackgroundDownloader] - [download]"); + NSLog(@"[RNBackgroundDownloader] - [download] - 1"); NSString *identifier = options[@"id"]; NSString *url = options[@"url"]; NSString *destination = options[@"destination"]; NSString *metadata = options[@"metadata"]; NSDictionary *headers = options[@"headers"]; + + + NSNumber *_progressInterval = options[@"progressInterval"]; + if (_progressInterval) { + progressInterval = [_progressInterval intValue] / 1000; // progressInterval IN options SUPPLIED IN MILLISECONDS + + NSDictionary *configMap = @{@"progressInterval": [NSNumber numberWithFloat:progressInterval]}; + [[NSUserDefaults standardUserDefaults] setObject:[self serialize: configMap] forKey:CONFIG_MAP_KEY]; + } + + + NSLog(@"[RNBackgroundDownloader] - [download] - 1 url %@ destination %@ progressInterval %f", url, destination, progressInterval); if (identifier == nil || url == nil || destination == nil) { NSLog(@"[RNBackgroundDownloader] - [Error] id, url and destination must be set"); return; @@ -225,17 +255,18 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) { taskToConfigMap[@(task.taskIdentifier)] = taskConfig; [[NSUserDefaults standardUserDefaults] setObject:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY]; - idToTaskMap[identifier] = task; + self->idToTaskMap[identifier] = task; + idToPercentMap[identifier] = @0.0; [task resume]; - lastProgressReport = [[NSDate alloc] init]; + lastProgressReportedAt = [[NSDate alloc] init]; } } RCT_EXPORT_METHOD(pauseTask: (NSString *)identifier) { NSLog(@"[RNBackgroundDownloader] - [pauseTask]"); @synchronized (sharedLock) { - NSURLSessionDownloadTask *task = idToTaskMap[identifier]; + NSURLSessionDownloadTask *task = self->idToTaskMap[identifier]; if (task != nil && task.state == NSURLSessionTaskStateRunning) { [task suspend]; } @@ -245,7 +276,7 @@ RCT_EXPORT_METHOD(pauseTask: (NSString *)identifier) { RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) { NSLog(@"[RNBackgroundDownloader] - [resumeTask]"); @synchronized (sharedLock) { - NSURLSessionDownloadTask *task = idToTaskMap[identifier]; + NSURLSessionDownloadTask *task = self->idToTaskMap[identifier]; if (task != nil && task.state == NSURLSessionTaskStateSuspended) { [task resume]; } @@ -255,7 +286,7 @@ RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) { RCT_EXPORT_METHOD(stopTask: (NSString *)identifier) { NSLog(@"[RNBackgroundDownloader] - [stopTask]"); @synchronized (sharedLock) { - NSURLSessionDownloadTask *task = idToTaskMap[identifier]; + NSURLSessionDownloadTask *task = self->idToTaskMap[identifier]; if (task != nil) { [task cancel]; [self removeTaskFromMap:task]; @@ -268,16 +299,16 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej [self lazyInitSession]; [urlSession getTasksWithCompletionHandler:^(NSArray * _Nonnull dataTasks, NSArray * _Nonnull uploadTasks, NSArray * _Nonnull downloadTasks) { NSMutableArray *idsFound = [[NSMutableArray alloc] init]; - @synchronized (sharedLock) { + @synchronized (self->sharedLock) { for (NSURLSessionDownloadTask *foundTask in downloadTasks) { NSURLSessionDownloadTask __strong *task = foundTask; - RNBGDTaskConfig *taskConfig = taskToConfigMap[@(task.taskIdentifier)]; + RNBGDTaskConfig *taskConfig = self->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]]; + task = [self->urlSession downloadTaskWithResumeData:task.error.userInfo[NSURLSessionDownloadTaskResumeData]]; } else { - task = [urlSession downloadTaskWithURL:task.currentRequest.URL]; + task = [self->urlSession downloadTaskWithURL:task.currentRequest.URL]; } [task resume]; } @@ -290,8 +321,11 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej @"bytesTotal": [NSNumber numberWithLongLong:task.countOfBytesExpectedToReceive] }]; taskConfig.reportedBegin = YES; - taskToConfigMap[@(task.taskIdentifier)] = taskConfig; - idToTaskMap[taskConfig.id] = task; + self->taskToConfigMap[@(task.taskIdentifier)] = taskConfig; + self->idToTaskMap[taskConfig.id] = task; + + NSNumber *percent = task.countOfBytesExpectedToReceive > 0 ? [NSNumber numberWithFloat:(float)task.countOfBytesReceived/(float)task.countOfBytesExpectedToReceive] : @0.0; + self->idToPercentMap[taskConfig.id] = percent; } else { [task cancel]; } @@ -378,19 +412,24 @@ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId taskCofig.reportedBegin = YES; } - progressReports[taskCofig.id] = @{ - @"id": taskCofig.id, - @"bytesDownloaded": [NSNumber numberWithLongLong: bytesTotalWritten], - @"bytesTotal": [NSNumber numberWithLongLong: bytesTotalExpectedToWrite] - }; + NSNumber *prevPercent = idToPercentMap[taskCofig.id]; + NSNumber *percent = [NSNumber numberWithFloat:(float)bytesTotalWritten/(float)bytesTotalExpectedToWrite]; + if ([percent floatValue] - [prevPercent floatValue] > 0.01f) { + progressReports[taskCofig.id] = @{ + @"id": taskCofig.id, + @"bytesDownloaded": [NSNumber numberWithLongLong: bytesTotalWritten], + @"bytesTotal": [NSNumber numberWithLongLong: bytesTotalExpectedToWrite] + }; + idToPercentMap[taskCofig.id] = percent; + } + NSDate *now = [[NSDate alloc] init]; - // TODO: PROPOSE OPTION TO SET PROGRESS INTERVAL (ITS COMMON FOR ALL DOWNLOADS. ON ANDROID SIDE ITS PER DOWNLOAD. MAYBE CHANGE ANDROID TO BE COMMON AS WELL AND PREVENT IT SEND TOO OFTEN. ALSO SEND IT ONLY IF PROGRESS CHANGED - SEE PREV IMPLEMENTATION IN 2.10) - if ([now timeIntervalSinceDate:lastProgressReport] > 0.5 && progressReports.count > 0) { + if ([now timeIntervalSinceDate:lastProgressReportedAt] > progressInterval && progressReports.count > 0) { if (self.bridge) { [self sendEventWithName:@"downloadProgress" body:[progressReports allValues]]; } - lastProgressReport = now; + lastProgressReportedAt = now; [progressReports removeAllObjects]; } } @@ -436,15 +475,27 @@ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId #pragma mark - serialization - (NSData *)serialize: (id)obj { - return [NSKeyedArchiver archivedDataWithRootObject:obj]; + NSError *error; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:obj requiringSecureCoding:NO error:&error]; + + if (error) { + // Handle the error + NSLog(@"[RNBackgroundDownloader] Serialization error: %@", error); + } + + return data; } - (id)deserialize: (NSData *)data { - if (data == nil) { - return nil; + NSError *error; + id obj = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSObject class] fromData:data error:&error]; + + if (error) { + // Handle the error + NSLog(@"[RNBackgroundDownloader] Deserialization error: %@", error); } - return [NSKeyedUnarchiver unarchiveObjectWithData:data]; + return obj; } @end