diff --git a/README.md b/README.md index 59ea7dd..4fa025c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ On iOS, if you want to download big files no matter the state of your app, wethe This API handles your downloads separately from your app and only keeps it informed using delegates (Read: [Downloading Files in the Background](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background)). -On Android we are simulating this process with a wonderful library called [Fetch2](https://github.com/tonyofrancis/Fetch) +On Android we are simulating this process with [DownloadManager](https://developer.android.com/reference/android/app/DownloadManager) The real challenge of using this method is making sure the app's UI is always up-to-date with the downloads that are happening in another process because your app might startup from scratch while the downloads are still running. @@ -118,9 +118,8 @@ let task = download({ // PROCESS YOUR STUFF - // FINISH DOWNLOAD JOB ON IOS - if (Platform.OS === 'ios') - completeHandler(jobId) + // FINISH DOWNLOAD JOB + completeHandler(jobId) }).error(error => { console.log('Download canceled due to error: ', error); }) @@ -205,15 +204,15 @@ Download a file to destination An object containing options properties -| Property | Type | Required | Platforms | Info | -| ------------- | ------------------------------------------------ | :------: | :-------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | String | ✅ | All | A Unique ID to provide for this download. This ID will help to identify the download task when the app re-launches | -| `url` | String | ✅ | All | URL to file you want to download | -| `destination` | String | ✅ | All | Where to copy the file to once the download is done | -| `metadata` | Object | | All | Data to be preserved on reboot. | -| `headers` | Object | | All | Costume headers to add to the download request. These are merged with the headers given in the `setHeaders` function -| `priority` | [Priority (enum)](#priority-enum---android-only) | | Android | The priority of the download. On Android, downloading is limited to 4 simultaneous instances where further downloads are queued. Priority helps in deciding which download to pick next from the queue. **Default:** Priority.MEDIUM | -| `network` | [Network (enum)](#network-enum---android-only) | | Android | Give your the ability to limit the download to WIFI only. **Default:** Network.ALL | +| Property | Type | Required | Platforms | Info | +| ------------- | ------------------------------------------------ | :------: | :-------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `id` | String | ✅ | All | A Unique ID to provide for this download. This ID will help to identify the download task when the app re-launches | +| `url` | String | ✅ | All | URL to file you want to download | +| `destination` | String | ✅ | All | Where to copy the file to once the download is done | +| `metadata` | Object | | All | Data to be preserved on reboot. | +| `headers` | Object | | All | Costume headers to add to the download request. These are merged with the headers given in the `setHeaders` function | +| `isAllowedOverRoaming` | Boolean | | Android | whether this download may proceed over a roaming connection. By default, roaming is allowed | +| `isAllowedOverMetered` | Boolean | | Android | Whether this download may proceed over a metered network connection. By default, metered networks are allowed | **returns** @@ -245,12 +244,12 @@ A class representing a download task created by `RNBackgroundDownloader.download | `id` | String | The id you gave the task when calling `RNBackgroundDownloader.download` | | `metadata` | Object | The metadata you gave the task when calling `RNBackgroundDownloader.download` | | `percent` | Number | The current percent of completion of the task between 0 and 1 | -| `bytesWritten` | Number | The number of bytes currently written by the task | -| `totalBytes` | Number | The number bytes expected to be written by this task or more plainly, the file size being downloaded | +| `bytesDownloaded` | Number | The number of bytes currently written by the task | +| `bytesTotal` | Number | The number bytes expected to be written by this task or more plainly, the file size being downloaded | ### `completeHandler(jobId)` -Finishes download job on iOS and informs OS that app can be closed in background if needed. +Finishes download job and informs OS that app can be closed in background if needed. After finishing download in background you have some time to process your JS logic and finish the job. ### `ensureDownloadsAreRunning` @@ -305,12 +304,12 @@ Use these methods to stay updated on what's happening with the task. All callback methods return the current instance of the `DownloadTask` for chaining. -| Function | Callback Arguments | Info | -| ---------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| Function | Callback Arguments | Info | +| ---------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `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 | +| `progress` | percent, bytesDownloaded, bytesTotal | 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 | ### `pause()` Pauses the download @@ -321,31 +320,6 @@ Resumes a pause download ### `stop()` Stops the download for good and removes the file that was written so far -### `initDownloader(options)` - -Init android downloader with options - -**options** - -An object containing options properties - -| Property | Type | Required | Platforms | Info | -| ------------- | ------------------------------------------------ | :------: | :-------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | String | | Android | Downloader type: 'parallel' or 'sequential'. Default: 'sequential'. 'parallel' seems to cause lots of ANRs, so be careful | - -**Usage** - -```javascript -import { initDownloader } from '@kesha-antonov/react-native-background-downloader' - -... -// SOMEWHERE AT APP STARTUP - -useEffect(() => { - initDownloader({ type: 'parallel' }) -}, []) -``` - ## Constants ### directories @@ -354,20 +328,6 @@ useEffect(() => { An absolute path to the app's documents directory. It is recommended that you use this path as the target of downloaded files. -### Priority (enum) - Android only - -`Priority.HIGH` - -`Priority.MEDIUM` - Default ✅ - -`Priority.LOW` - -### Network (enum) - Android only - -`Network.WIFI_ONLY` - -`Network.ALL` - Default ✅ - ## Authors Developed by [Elad Gil](https://github.com/ptelad) of [Eko](http://www.helloeko.com) diff --git a/android/build.gradle b/android/build.gradle index 61161a8..30b5f60 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,7 +1,5 @@ apply plugin: 'com.android.library' -def useAndroidX = (project.hasProperty('android.useAndroidX')) ? project.property('android.useAndroidX') : false - def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } @@ -26,11 +24,4 @@ android { dependencies { //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' - if (useAndroidX) { - api 'com.github.tonyofrancis.Fetch:xfetch2:3.1.6' - implementation 'com.github.tonyofrancis.Fetch:xfetch2okhttp:3.1.6' - } else { - api 'com.tonyodev.fetch2:fetch2:3.0.12' - implementation 'com.tonyodev.fetch2okhttp:fetch2okhttp:3.0.12' - } } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 357e2e7..aa98e5a 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - diff --git a/android/src/main/java/com/eko/Downloader.java b/android/src/main/java/com/eko/Downloader.java new file mode 100644 index 0000000..8fb55ae --- /dev/null +++ b/android/src/main/java/com/eko/Downloader.java @@ -0,0 +1,157 @@ +package com.eko; + +import android.app.DownloadManager; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; + +import java.util.HashMap; +import android.util.Log; + +import static android.content.Context.DOWNLOAD_SERVICE; + +public class Downloader { + + public DownloadManager downloadManager; + private Context context; + + public Downloader(Context ctx) { + context = ctx; + downloadManager = (DownloadManager) ctx.getSystemService(DOWNLOAD_SERVICE); + } + + public long queueDownload(DownloadManager.Request request) { + return downloadManager.enqueue(request); + } + + public WritableMap checkDownloadStatus(long downloadId) { + DownloadManager.Query downloadQuery = new DownloadManager.Query(); + downloadQuery.setFilterById(downloadId); + Cursor cursor = downloadManager.query(downloadQuery); + + WritableMap result = Arguments.createMap(); + + if (cursor.moveToFirst()) { + result = getDownloadStatus(cursor); + } else { + result.putString("downloadId", String.valueOf(downloadId)); + result.putInt("status", DownloadManager.STATUS_FAILED); + result.putInt("reason", -1); + result.putString("reasonText", "COULD_NOT_FIND"); + } + + return result; + } + + public int cancelDownload(long downloadId) { + Log.d("RNBackgroundDownloader", "Downloader cancelDownload " + downloadId); + return downloadManager.remove(downloadId); + } + + // WAITING FOR THE FIX TO BE MERGED + // https://android-review.googlesource.com/c/platform/packages/providers/DownloadProvider/+/2089866 + public void pauseDownload(long downloadId) { + // ContentValues values = new ContentValues(); + + // values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_PAUSED); + // values.put(Downloads.Impl.COLUMN_STATUS, + // Downloads.Impl.STATUS_PAUSED_BY_APP); + + // downloadManager.mResolver.update(ContentUris.withAppendedId(mBaseUri, + // ids[0]), values, null, null) + } + + public void resumeDownload(long downloadId) { + // ContentValues values = new ContentValues(); + + // values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING); + // values.put(Downloads.Impl.COLUMN_CONTROL, Downloads.Impl.CONTROL_RUN); + } + + public WritableMap getDownloadStatus(Cursor cursor) { + String downloadId = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_ID)); + String localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); + if (localUri != null) { + localUri = localUri.replace("file://", ""); + } + String bytesDownloadedSoFar = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); + String totalSizeBytes = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); + + int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); + int reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON)); + + String reasonText = ""; + + switch (status) { + case DownloadManager.STATUS_FAILED: + switch (reason) { + case DownloadManager.ERROR_CANNOT_RESUME: + reasonText = "ERROR_CANNOT_RESUME"; + break; + case DownloadManager.ERROR_DEVICE_NOT_FOUND: + reasonText = "ERROR_DEVICE_NOT_FOUND"; + break; + case DownloadManager.ERROR_FILE_ALREADY_EXISTS: + reasonText = "ERROR_FILE_ALREADY_EXISTS"; + break; + case DownloadManager.ERROR_FILE_ERROR: + reasonText = "ERROR_FILE_ERROR"; + break; + case DownloadManager.ERROR_HTTP_DATA_ERROR: + reasonText = "ERROR_HTTP_DATA_ERROR"; + break; + case DownloadManager.ERROR_INSUFFICIENT_SPACE: + reasonText = "ERROR_INSUFFICIENT_SPACE"; + break; + case DownloadManager.ERROR_TOO_MANY_REDIRECTS: + reasonText = "ERROR_TOO_MANY_REDIRECTS"; + break; + case DownloadManager.ERROR_UNHANDLED_HTTP_CODE: + reasonText = "ERROR_UNHANDLED_HTTP_CODE"; + break; + default: + reasonText = "ERROR_UNKNOWN"; + break; + } + break; + case DownloadManager.STATUS_PAUSED: + switch (reason) { + case DownloadManager.PAUSED_QUEUED_FOR_WIFI: + reasonText = "PAUSED_QUEUED_FOR_WIFI"; + break; + case DownloadManager.PAUSED_UNKNOWN: + reasonText = "PAUSED_UNKNOWN"; + break; + case DownloadManager.PAUSED_WAITING_FOR_NETWORK: + reasonText = "PAUSED_WAITING_FOR_NETWORK"; + break; + case DownloadManager.PAUSED_WAITING_TO_RETRY: + reasonText = "PAUSED_WAITING_TO_RETRY"; + break; + default: + reasonText = "UNKNOWN"; + } + break; + } + + WritableMap result = Arguments.createMap(); + result.putString("downloadId", downloadId); + + result.putInt("status", status); + result.putInt("reason", reason); + result.putString("reasonText", reasonText); + + result.putInt("bytesDownloaded", Integer.parseInt(bytesDownloadedSoFar)); + result.putInt("bytesTotal", Integer.parseInt(totalSizeBytes)); + result.putString("localUri", localUri); + + return result; + } +} diff --git a/android/src/main/java/com/eko/OnBegin.java b/android/src/main/java/com/eko/OnBegin.java new file mode 100644 index 0000000..dd121e4 --- /dev/null +++ b/android/src/main/java/com/eko/OnBegin.java @@ -0,0 +1,49 @@ +package com.eko; + +import java.net.URL; +import java.net.URLConnection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.eko.RNBGDTaskConfig; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; + +public class OnBegin extends Thread { + private RNBGDTaskConfig config; + private DeviceEventManagerModule.RCTDeviceEventEmitter ee; + + public OnBegin(RNBGDTaskConfig config, + DeviceEventManagerModule.RCTDeviceEventEmitter ee) { + this.config = config; + this.ee = ee; + } + + @Override + public void run() { + try { + WritableMap headersMap = Arguments.createMap(); + + URL urlC = new URL(config.url); + 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); + } + con.getInputStream().close(); + + WritableMap params = Arguments.createMap(); + params.putString("id", config.id); + params.putMap("headers", headersMap); + params.putInt("expectedBytes", Integer.valueOf(headersMap.getString("Content-Length"))); + + ee.emit("downloadBegin", params); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/android/src/main/java/com/eko/OnProgress.java b/android/src/main/java/com/eko/OnProgress.java new file mode 100644 index 0000000..ac995cd --- /dev/null +++ b/android/src/main/java/com/eko/OnProgress.java @@ -0,0 +1,132 @@ +package com.eko; + +import java.util.HashMap; +import android.app.DownloadManager; +import android.util.Log; + +import com.eko.Downloader; +import com.eko.RNBGDTaskConfig; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import android.database.Cursor; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; + +public class OnProgress extends Thread { + private final long downloadId; + private final DownloadManager.Query query; + private final Downloader downloader; + private Cursor cursor; + private int lastBytesDownloaded; + private int bytesTotal; + private int progressInterval = 300; + + private RNBGDTaskConfig config; + private DeviceEventManagerModule.RCTDeviceEventEmitter ee; + + public OnProgress(RNBGDTaskConfig config, long downloadId, + DeviceEventManagerModule.RCTDeviceEventEmitter ee, Downloader downloader, int progressInterval) { + this.config = config; + + this.downloadId = downloadId; + this.query = new DownloadManager.Query(); + query.setFilterById(this.downloadId); + + this.ee = ee; + this.downloader = downloader; + if (progressInterval > 0) { + this.progressInterval = progressInterval; + } + } + + private void handleInterrupt() { + try { + Log.d("RNBackgroundDownloader", "RNBD: OnProgress handleInterrupt. downloadId " + downloadId); + if (cursor != null) { + cursor.close(); + } + } catch (Exception e) { + return; + } + this.interrupt(); + } + + @Override + public void run() { + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-1. downloadId " + downloadId); + while (downloadId > 0) { + try { + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-2. downloadId " + downloadId); + + cursor = downloader.downloadManager.query(query); + + if (!cursor.moveToFirst()) { + this.handleInterrupt(); + } + + int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); + if (status == DownloadManager.STATUS_FAILED || status == DownloadManager.STATUS_SUCCESSFUL) { + this.handleInterrupt(); + } + + if (status == DownloadManager.STATUS_PAUSED) { + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-2.1. downloadId " + downloadId); + Thread.sleep(5000); + } else if (status == DownloadManager.STATUS_PENDING) { + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-2.2. downloadId " + downloadId); + Thread.sleep(1000); + } else { + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-2.3. downloadId " + downloadId); + Thread.sleep(progressInterval); + } + + // get total bytes of the file + if (bytesTotal <= 0) { + bytesTotal = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); + } + + int bytesDownloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); + + if (bytesTotal > 0 && bytesDownloaded == bytesTotal) { + this.handleInterrupt(); + } else { + lastBytesDownloaded = bytesDownloaded; + } + + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-3. downloadId " + downloadId + " bytesDownloaded " + + 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); + params.putDouble("percent", ((double) lastBytesDownloaded / 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); + } + } catch (Exception e) { + return; + } + + try { + if (cursor != null) { + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-5. downloadId " + downloadId); + cursor.close(); + Log.d("RNBackgroundDownloader", "RNBD: OnProgress-6. downloadId " + downloadId); + } + } 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 e0ccec9..70ca158 100644 --- a/android/src/main/java/com/eko/RNBGDTaskConfig.java +++ b/android/src/main/java/com/eko/RNBGDTaskConfig.java @@ -4,12 +4,14 @@ import java.io.Serializable; public class RNBGDTaskConfig implements Serializable { public String id; + public String url; public String destination; public String metadata = "{}"; public boolean reportedBegin; - public RNBGDTaskConfig(String id, String destination, String metadata) { + 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; diff --git a/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java b/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java index 418bb5c..ed317dc 100644 --- a/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java +++ b/android/src/main/java/com/eko/RNBackgroundDownloaderModule.java @@ -8,27 +8,14 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; 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; -import com.tonyodev.fetch2.FetchListener; -import com.tonyodev.fetch2.NetworkType; -import com.tonyodev.fetch2.Priority; -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 android.app.DownloadManager; +import android.app.DownloadManager.Request; import org.jetbrains.annotations.NotNull; @@ -42,16 +29,33 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Scanner; import javax.annotation.Nullable; -import okhttp3.OkHttpClient; - import java.util.Set; import java.net.URL; import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; -public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule implements FetchListener { +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.LongSparseArray; + +import com.facebook.react.bridge.Callback; + +import java.util.ArrayList; +import java.util.Arrays; +import android.net.Uri; +import android.webkit.MimeTypeMap; +import android.database.Cursor; +import android.os.Build; +import android.os.Environment; + +public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule { private static final int TASK_RUNNING = 0; private static final int TASK_SUSPENDED = 1; @@ -64,37 +68,117 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp private static final int ERR_FILE_NOT_FOUND = 3; private static final int ERR_OTHERS = 100; - private static Map stateMap = new HashMap() {{ - put(Status.DOWNLOADING, TASK_RUNNING); - put(Status.COMPLETED, TASK_COMPLETED); - put(Status.PAUSED, TASK_SUSPENDED); - put(Status.QUEUED, TASK_RUNNING); - put(Status.CANCELLED, TASK_CANCELING); - put(Status.FAILED, TASK_CANCELING); - put(Status.REMOVED, TASK_CANCELING); - put(Status.DELETED, TASK_CANCELING); - put(Status.NONE, TASK_CANCELING); - }}; + private static Map stateMap = new HashMap() { + { + put(DownloadManager.STATUS_FAILED, TASK_CANCELING); + put(DownloadManager.STATUS_PAUSED, TASK_SUSPENDED); + put(DownloadManager.STATUS_PENDING, TASK_RUNNING); + put(DownloadManager.STATUS_RUNNING, TASK_RUNNING); + put(DownloadManager.STATUS_SUCCESSFUL, TASK_COMPLETED); + } + }; - private Fetch fetch; - private Map idToRequestId = new HashMap<>(); - @SuppressLint("UseSparseArrays") - private Map requestIdToConfig = new HashMap<>(); + private Downloader downloader; + + private Map condigIdToDownloadId = new HashMap<>(); + private Map downloadIdToConfig = new HashMap<>(); private DeviceEventManagerModule.RCTDeviceEventEmitter ee; - private Date lastProgressReport = new Date(); - private HashMap progressReports = new HashMap<>(); + private Map onProgressThreads = new HashMap<>(); + private static Object sharedLock = new Object(); + BroadcastReceiver downloadReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.d(getName(), "RNBD: onReceive-1"); + try { + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); + Log.d(getName(), "RNBD: onReceive-2 " + downloadId); + + RNBGDTaskConfig config = downloadIdToConfig.get(downloadId); + + if (config != null) { + Log.d(getName(), "RNBD: onReceive-3"); + WritableMap downloadStatus = downloader.checkDownloadStatus(downloadId); + int status = downloadStatus.getInt("status"); + + Log.d(getName(), "RNBD: onReceive: status - " + status); + + stopTrackingProgress(config.id); + + synchronized (sharedLock) { + switch (status) { + case DownloadManager.STATUS_SUCCESSFUL: { + // MOVES FILE TO DESTINATION + String localUri = downloadStatus.getString("localUri"); + Log.d(getName(), "RNBD: onReceive: localUri " + localUri + " destination " + config.destination); + File file = new File(localUri); + Log.d(getName(), "RNBD: onReceive: file exists " + file.exists()); + File dest = new File(config.destination); + Log.d(getName(), "RNBD: onReceive: dest exists " + dest.exists()); + Files.move(file.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + + WritableMap params = Arguments.createMap(); + params.putString("id", config.id); + params.putString("location", config.destination); + + ee.emit("downloadComplete", params); + break; + } + case DownloadManager.STATUS_FAILED: { + Log.e(getName(), "Error in enqueue: " + downloadStatus.getInt("status") + ":" + + downloadStatus.getInt("reason") + ":" + downloadStatus.getString("reasonText")); + + WritableMap params = Arguments.createMap(); + params.putString("id", config.id); + params.putInt("errorCode", downloadStatus.getInt("reason")); + params.putString("error", downloadStatus.getString("reasonText")); + ee.emit("downloadFailed", params); + break; + } + } + } + } + } catch (Exception e) { + Log.e(getName(), "downloadReceiver: onReceive. " + Log.getStackTraceString(e)); + } + } + }; + public RNBackgroundDownloaderModule(ReactApplicationContext reactContext) { super(reactContext); ReadableMap emptyMap = Arguments.createMap(); this.initDownloader(emptyMap); + + downloader = new Downloader(reactContext); + + IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + compatRegisterReceiver(reactContext, downloadReceiver, filter, true); + } + + // TAKEN FROM + // https://github.com/facebook/react-native/pull/38256/files#diff-d5e21477eeadeb0c536d5870f487a8528f9a16ae928c397fec7b255805cc8ad3 + private void compatRegisterReceiver(Context context, BroadcastReceiver receiver, IntentFilter filter, + boolean exported) { + if (Build.VERSION.SDK_INT >= 34 && context.getApplicationInfo().targetSdkVersion >= 34) { + context.registerReceiver( + receiver, filter, exported ? Context.RECEIVER_EXPORTED : Context.RECEIVER_NOT_EXPORTED); + } else { + context.registerReceiver(receiver, filter); + } + } + + private void stopTrackingProgress(String configId) { + OnProgress onProgressTh = onProgressThreads.get(configId); + if (onProgressTh != null) { + onProgressTh.interrupt(); + onProgressThreads.remove(configId); + } } @Override public void onCatalystInstanceDestroy() { - fetch.close(); } @Override @@ -132,46 +216,27 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp constants.put("TaskSuspended", TASK_SUSPENDED); constants.put("TaskCanceling", TASK_CANCELING); constants.put("TaskCompleted", TASK_COMPLETED); - constants.put("PriorityHigh", Priority.HIGH.getValue()); - constants.put("PriorityNormal", Priority.NORMAL.getValue()); - constants.put("PriorityLow", Priority.LOW.getValue()); - constants.put("OnlyWifi", NetworkType.WIFI_ONLY.getValue()); - constants.put("AllNetworks", NetworkType.ALL.getValue()); + return constants; } @ReactMethod public void initDownloader(ReadableMap options) { - if (fetch != null) { - fetch.close(); - fetch = null; - } - - Downloader.FileDownloaderType downloaderType = options.getString("type") == "parallel" - ? Downloader.FileDownloaderType.PARALLEL - : Downloader.FileDownloaderType.SEQUENTIAL; - - OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); - final Downloader okHttpDownloader = new OkHttpDownloader(okHttpClient, - downloaderType); + Log.d(getName(), "RNBD: initDownloader"); loadConfigMap(); - FetchConfiguration fetchConfiguration = new FetchConfiguration.Builder(this.getReactApplicationContext()) - .setDownloadConcurrentLimit(4) - .setHttpDownloader(okHttpDownloader) - .enableRetryOnNetworkGain(true) - .setHttpDownloader(new HttpUrlConnectionDownloader(downloaderType)) - .build(); - fetch = Fetch.Impl.getInstance(fetchConfiguration); - fetch.addListener(this); + + // TODO. MAYBE REINIT DOWNLOADER } - private void removeFromMaps(int requestId) { - synchronized(sharedLock) { - RNBGDTaskConfig config = requestIdToConfig.get(requestId); + private void removeFromMaps(long downloadId) { + Log.d(getName(), "RNBD: removeFromMaps"); + + synchronized (sharedLock) { + RNBGDTaskConfig config = downloadIdToConfig.get(downloadId); if (config != null) { - idToRequestId.remove(config.id); - requestIdToConfig.remove(requestId); + condigIdToDownloadId.remove(config.id); + downloadIdToConfig.remove(downloadId); saveConfigMap(); } @@ -179,11 +244,13 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp } private void saveConfigMap() { - synchronized(sharedLock) { + Log.d(getName(), "RNBD: saveConfigMap"); + + synchronized (sharedLock) { File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); try { ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file)); - outputStream.writeObject(requestIdToConfig); + outputStream.writeObject(downloadIdToConfig); outputStream.flush(); outputStream.close(); } catch (IOException e) { @@ -193,31 +260,17 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp } 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)); - requestIdToConfig = (Map) inputStream.readObject(); + downloadIdToConfig = (Map) inputStream.readObject(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } - private int convertErrorCode(Error error) { - if ((error == Error.FILE_NOT_CREATED) - || (error == Error.WRITE_PERMISSION_DENIED)) { - return ERR_NO_WRITE_PERMISSION; - } else if ((error == Error.CONNECTION_TIMED_OUT) - || (error == Error.NO_NETWORK_CONNECTION)) { - return ERR_NO_INTERNET; - } else if (error == Error.NO_STORAGE_SPACE) { - return ERR_STORAGE_FULL; - } else if (error == Error.FILE_NOT_FOUND) { - return ERR_FILE_NOT_FOUND; - } else { - return ERR_OTHERS; - } - } - // JS Methods @ReactMethod public void download(ReadableMap options) { @@ -226,277 +279,220 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp String destination = options.getString("destination"); ReadableMap headers = options.getMap("headers"); String metadata = options.getString("metadata"); + int progressInterval = options.getInt("progressInterval"); + + boolean isAllowedOverRoaming = options.getBoolean("isAllowedOverRoaming"); + boolean isAllowedOverMetered = options.getBoolean("isAllowedOverMetered"); + + Log.d(getName(), "RNBD: download " + id + " " + url + " " + destination + " " + metadata); if (id == null || url == null || destination == null) { Log.e(getName(), "id, url and destination must be set"); return; } - RNBGDTaskConfig config = new RNBGDTaskConfig(id, destination, metadata); - final Request request = new Request(url, destination); + RNBGDTaskConfig config = new RNBGDTaskConfig(id, url, destination, metadata); + final Request request = new Request(Uri.parse(url)); + request.setAllowedOverRoaming(isAllowedOverRoaming); + request.setAllowedOverMetered(isAllowedOverMetered); + request.setNotificationVisibility(Request.VISIBILITY_HIDDEN); + request.setRequiresCharging(false); + + int uuid = (int) (System.currentTimeMillis() & 0xfffffff); + // GETS THE FILE EXTENSION FROM PATH + String extension = MimeTypeMap.getFileExtensionFromUrl(destination); + + String fileName = uuid + "." + extension; + request.setDestinationInExternalFilesDir(this.getReactApplicationContext(), null, fileName); + + // TOREMOVE + // request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS.toString(), fileName); + // request.setDestinationUri(Uri.parse(destination)); + if (headers != null) { ReadableMapKeySetIterator it = headers.keySetIterator(); while (it.hasNextKey()) { String headerKey = it.nextKey(); - request.addHeader(headerKey, headers.getString(headerKey)); + request.addRequestHeader(headerKey, headers.getString(headerKey)); } } - 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) { - } - }, new Func() { - @Override - public void call(Error error) { - //An error occurred when enqueuing a request. + long downloadId = downloader.queueDownload(request); - WritableMap params = Arguments.createMap(); - params.putString("id", id); - params.putString("error", error.toString()); - - int convertedErrCode = convertErrorCode(error); - params.putInt("errorcode", convertedErrCode); - ee.emit("downloadFailed", params); - - removeFromMaps(request.getId()); - fetch.remove(request.getId()); - - Log.e(getName(), "Error in enqueue: " + error.toString() + ":" + error.getValue()); - } - } - ); - - synchronized(sharedLock) { - lastProgressReport = new Date(); - idToRequestId.put(id, request.getId()); - requestIdToConfig.put(request.getId(), config); + synchronized (sharedLock) { + condigIdToDownloadId.put(id, downloadId); + downloadIdToConfig.put(downloadId, config); saveConfigMap(); + + WritableMap downloadStatus = downloader.checkDownloadStatus(downloadId); + int status = downloadStatus.getInt("status"); + + Log.d(getName(), "RNBD: download-1. status: " + status + " downloadId: " + downloadId); + + if (config.reportedBegin) { + return; + } + + config.reportedBegin = true; + saveConfigMap(); + + Log.d(getName(), "RNBD: download-2 downloadId: " + downloadId); + // report begin & progress + // + // overlaped with thread to not block main thread + new Thread(new Runnable() { + public void run() { + try { + Log.d(getName(), "RNBD: download-3 downloadId: " + downloadId); + + while (true) { + WritableMap downloadStatus = downloader.checkDownloadStatus(downloadId); + int status = downloadStatus.getInt("status"); + + Log.d(getName(), "RNBD: download-3.1 " + status + " downloadId: " + downloadId); + + if (status == DownloadManager.STATUS_RUNNING) { + break; + } + if (status == DownloadManager.STATUS_FAILED || status == DownloadManager.STATUS_SUCCESSFUL) { + Log.d(getName(), "RNBD: download-3.2 " + status + " downloadId: " + downloadId); + Thread.currentThread().interrupt(); + } + + Thread.sleep(500); + } + + // EMIT BEGIN + OnBegin onBeginTh = new OnBegin(config, ee); + onBeginTh.start(); + // wait for onBeginTh to finish + onBeginTh.join(); + + Log.d(getName(), "RNBD: download-4 downloadId: " + downloadId); + OnProgress onProgressTh = new OnProgress(config, downloadId, ee, downloader, progressInterval); + onProgressThreads.put(config.id, onProgressTh); + onProgressTh.start(); + Log.d(getName(), "RNBD: download-5 downloadId: " + downloadId); + } catch (Exception e) { + } + } + }).start(); + Log.d(getName(), "RNBD: download-6 downloadId: " + downloadId); } } + // TODO: NOT WORKING WITH DownloadManager FOR NOW @ReactMethod - public void pauseTask(String identifier) { - synchronized(sharedLock) { - Integer requestId = idToRequestId.get(identifier); - if (requestId != null) { - fetch.pause(requestId); + public void pauseTask(String configId) { + Log.d(getName(), "RNBD: pauseTask " + configId); + + synchronized (sharedLock) { + Long downloadId = condigIdToDownloadId.get(configId); + if (downloadId != null) { + downloader.pauseDownload(downloadId); + } + } + } + + // TODO: NOT WORKING WITH DownloadManager FOR NOW + @ReactMethod + public void resumeTask(String configId) { + Log.d(getName(), "RNBD: resumeTask " + configId); + + synchronized (sharedLock) { + Long downloadId = condigIdToDownloadId.get(configId); + if (downloadId != null) { + downloader.resumeDownload(downloadId); } } } @ReactMethod - public void resumeTask(String identifier) { - synchronized(sharedLock) { - Integer requestId = idToRequestId.get(identifier); - if (requestId != null) { - fetch.resume(requestId); + public void stopTask(String configId) { + Log.d(getName(), "RNBD: stopTask-1 " + configId); + + synchronized (sharedLock) { + Long downloadId = condigIdToDownloadId.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 + // USER + removeFromMaps(downloadId); + stopTrackingProgress(configId); + + downloader.cancelDownload(downloadId); } } } @ReactMethod - public void stopTask(String identifier) { - synchronized(sharedLock) { - Integer requestId = idToRequestId.get(identifier); - if (requestId != null) { - fetch.cancel(requestId); + public void completeHandler(String configId, final Promise promise) { + Log.d(getName(), "RNBD: completeHandler-1 " + configId); + + synchronized (sharedLock) { + Long downloadId = condigIdToDownloadId.get(configId); + Log.d(getName(), "RNBD: completeHandler-2 " + configId + " downloadId " + downloadId); + if (downloadId != null) { + removeFromMaps(downloadId); + // REMOVES DOWNLOAD FROM DownloadManager SO IT WOULD NOT BE RETURNED IN checkForExistingDownloads + downloader.cancelDownload(downloadId); } } } @ReactMethod public void checkForExistingDownloads(final Promise promise) { - fetch.getDownloads(new Func>() { - @Override - public void call(@NotNull List downloads) { - WritableArray foundIds = Arguments.createArray(); + Log.d(getName(), "RNBD: checkForExistingDownloads-1"); - synchronized(sharedLock) { - for (Download download : downloads) { - if (requestIdToConfig.containsKey(download.getId())) { - RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); + WritableArray foundIds = Arguments.createArray(); + + synchronized (sharedLock) { + try { + DownloadManager.Query downloadQuery = new DownloadManager.Query(); + Cursor cursor = downloader.downloadManager.query(downloadQuery); + + if (cursor.moveToFirst()) { + do { + WritableMap result = downloader.getDownloadStatus(cursor); + Long downloadId = Long.parseLong(result.getString("downloadId")); + + if (downloadIdToConfig.containsKey(downloadId)) { + Log.d(getName(), "RNBD: checkForExistingDownloads-2"); + RNBGDTaskConfig config = downloadIdToConfig.get(downloadId); WritableMap params = Arguments.createMap(); params.putString("id", config.id); params.putString("metadata", config.metadata); - params.putInt("state", stateMap.get(download.getStatus())); - params.putInt("bytesWritten", (int)download.getDownloaded()); - params.putInt("totalBytes", (int)download.getTotal()); - params.putDouble("percent", ((double)download.getProgress()) / 100); + params.putInt("state", stateMap.get(result.getInt("status"))); + + int bytesDownloaded = result.getInt("bytesDownloaded"); + params.putInt("bytesDownloaded", bytesDownloaded); + + int bytesTotal = result.getInt("bytesTotal"); + params.putInt("bytesTotal", bytesTotal); + + params.putDouble("percent", ((double) bytesDownloaded / bytesTotal)); foundIds.pushMap(params); // TODO: MAYBE ADD headers - idToRequestId.put(config.id, download.getId()); - config.reportedBegin = true; + condigIdToDownloadId.put(config.id, downloadId); + + // TOREMOVE + // config.reportedBegin = true; } else { - fetch.delete(download.getId()); + Log.d(getName(), "RNBD: checkForExistingDownloads-3"); + downloader.cancelDownload(downloadId); } - } + } while (cursor.moveToNext()); } - promise.resolve(foundIds); - } - }); - } - - // Fetch API - @Override - public void onCompleted(Download download) { - synchronized(sharedLock) { - RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); - if (config != null) { - WritableMap params = Arguments.createMap(); - params.putString("id", config.id); - params.putString("location", config.destination); - - ee.emit("downloadComplete", params); - } - - removeFromMaps(download.getId()); - if (!fetch.isClosed()) { - fetch.remove(download.getId()); + cursor.close(); + } catch (Exception e) { + Log.e(getName(), "Error in checkForExistingDownloads: " + e.getLocalizedMessage()); } } - } - @Override - public void onProgress(Download download, long l, long l1) { - synchronized(sharedLock) { - RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); - if (config == null) { - return; - } - - WritableMap params = Arguments.createMap(); - params.putString("id", config.id); - - if (!config.reportedBegin) { - 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(); - } - }); - new Thread(new Runnable() { - public void run() { - try { - th.start(); - th.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - }).start(); - } 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() > 250) { - WritableArray reportsArray = Arguments.createArray(); - for (WritableMap report : progressReports.values()) { - reportsArray.pushMap(report); - } - ee.emit("downloadProgress", reportsArray); - lastProgressReport = now; - progressReports.clear(); - } - } - } - } - - @Override - public void onPaused(Download download) { - } - - @Override - public void onResumed(Download download) { - } - - @Override - public void onCancelled(Download download) { - synchronized(sharedLock) { - removeFromMaps(download.getId()); - fetch.delete(download.getId()); - } - } - - @Override - public void onRemoved(Download download) { - } - - @Override - public void onDeleted(Download download) { - } - - @Override - public void onAdded(Download download) { - } - - @Override - public void onQueued(Download download, boolean b) { - } - - @Override - public void onWaitingNetwork(Download download) { - } - - @Override - public void onError(Download download, Error error, Throwable throwable) { - synchronized(sharedLock) { - RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); - - if (config != null ) { - WritableMap params = Arguments.createMap(); - params.putString("id", config.id); - - int convertedErrCode = convertErrorCode(error); - params.putInt("errorcode", convertedErrCode); - - if (error == Error.UNKNOWN && throwable != null) { - params.putString("error", throwable.getLocalizedMessage()); - Log.e(getName(), "UNKNOWN Error in download: " + throwable.getLocalizedMessage()); - } else { - params.putString("error", error.toString()); - Log.e(getName(), "Error in download: " + error.toString() + ":" + error.getValue()); - } - ee.emit("downloadFailed", params); - } - - removeFromMaps(download.getId()); - fetch.remove(download.getId()); - } - } - - @Override - public void onDownloadBlockUpdated(Download download, DownloadBlock downloadBlock, int i) { - } - - @Override - public void onStarted(Download download, List list, int i) { + promise.resolve(foundIds); } } diff --git a/index.d.ts b/index.d.ts index 39f5a9b..7c8304b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,8 +16,8 @@ export interface TaskInfoObject { metadata: object | string; percent?: number; - bytesWritten?: number; - totalBytes?: number; + bytesDownloaded?: number; + bytesTotal?: number; beginHandler?: Function; progressHandler?: Function; @@ -37,8 +37,8 @@ export type BeginHandler = ({ }: BeginHandlerObject) => any; export type ProgressHandler = ( percent: number, - bytesWritten: number, - totalBytes: number + bytesDownloaded: number, + bytesTotal: number ) => any; export type DoneHandler = () => any; export type ErrorHandler = (error: any, errorCode: any) => any; @@ -55,8 +55,8 @@ export interface DownloadTask { id: string; state: DownloadTaskState; percent: number; - bytesWritten: number; - totalBytes: number; + bytesDownloaded: number; + bytesTotal: number; begin: (handler: BeginHandler) => DownloadTask; progress: (handler: ProgressHandler) => DownloadTask; @@ -77,7 +77,6 @@ export type CheckForExistingDownloads = () => Promise; export type EnsureDownloadsAreRunning = () => Promise; export interface InitDownloaderOptions { - type?: 'parallel' | 'sequential' | null; } export type InitDownloader = (options: InitDownloaderOptions) => undefined; @@ -87,6 +86,8 @@ export interface DownloadOption { destination: string; headers?: DownloadHeaders | undefined; metadata?: object; + isAllowedOverRoaming?: boolean; + isAllowedOverMetered?: boolean; } export type Download = (options: DownloadOption) => DownloadTask; @@ -96,17 +97,6 @@ export interface Directories { documents: string; } -export interface Network { - WIFI_ONLY: string; - ALL: string; -} - -export interface Priority { - HIGH: string; - MEDIUM: string; - LOW: string; -} - export const setHeaders: SetHeaders; export const checkForExistingDownloads: CheckForExistingDownloads; export const ensureDownloadsAreRunning: EnsureDownloadsAreRunning; @@ -114,8 +104,6 @@ export const initDownloader: InitDownloader; export const download: Download; export const completeHandler: CompleteHandler; export const directories: Directories; -export const Network: Network; -export const Priority: Priority; export interface RNBackgroundDownloader { setHeaders: SetHeaders; @@ -125,8 +113,6 @@ export interface RNBackgroundDownloader { download: Download; completeHandler: CompleteHandler; directories: Directories; - Network: Network; - Priority: Priority; } declare const RNBackgroundDownloader: RNBackgroundDownloader; diff --git a/index.ts b/index.ts index e43292e..a145f9d 100644 --- a/index.ts +++ b/index.ts @@ -7,14 +7,23 @@ const RNBackgroundDownloaderEmitter = new NativeEventEmitter(RNBackgroundDownloa const tasksMap = new Map() let headers = {} +RNBackgroundDownloaderEmitter.addListener('downloadBegin', ({ id, expectedBytes, headers }) => { + console.log('[RNBackgroundDownloader] downloadBegin', id, expectedBytes, headers) + const task = tasksMap.get(id) + task?.onBegin({ expectedBytes, headers }) +}) + RNBackgroundDownloaderEmitter.addListener('downloadProgress', events => { + // console.log('[RNBackgroundDownloader] downloadProgress-1', events, tasksMap) for (const event of events) { const task = tasksMap.get(event.id) - task?.onProgress(event.percent, event.written, event.total) + // console.log('[RNBackgroundDownloader] downloadProgress-2', event.id, task) + task?.onProgress(event.percent, event.bytesDownloaded, event.bytesTotal) } }) RNBackgroundDownloaderEmitter.addListener('downloadComplete', ({ id, location }) => { + console.log('[RNBackgroundDownloader] downloadComplete', id, location) const task = tasksMap.get(id) task?.onDone({ location }) @@ -22,35 +31,34 @@ RNBackgroundDownloaderEmitter.addListener('downloadComplete', ({ id, location }) }) RNBackgroundDownloaderEmitter.addListener('downloadFailed', event => { + console.log('[RNBackgroundDownloader] downloadFailed', event) const task = tasksMap.get(event.id) - task?.onError(event.error, event.errorcode) + task?.onError(event.error, event.errorCode) tasksMap.delete(event.id) }) -RNBackgroundDownloaderEmitter.addListener('downloadBegin', ({ id, expectedBytes, headers }) => { - const task = tasksMap.get(id) - task?.onBegin({ expectedBytes, headers }) -}) - -export function setHeaders (h = {}) { +export function setHeaders(h = {}) { if (typeof h !== 'object') throw new Error('[RNBackgroundDownloader] headers must be an object') headers = h } -export function initDownloader (options = {}) { +export function initDownloader(options = {}) { if (Platform.OS === 'android') RNBackgroundDownloader.initDownloader(options) } -export function checkForExistingDownloads () { +export function checkForExistingDownloads() { + console.log('[RNBackgroundDownloader] checkForExistingDownloads-1') return RNBackgroundDownloader.checkForExistingDownloads() .then(foundTasks => { + console.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) if (taskInfo.state === RNBackgroundDownloader.TaskRunning) { task.state = 'DOWNLOADING' @@ -60,7 +68,7 @@ export function checkForExistingDownloads () { task.stop() return null } else if (taskInfo.state === RNBackgroundDownloader.TaskCompleted) { - if (taskInfo.bytesWritten === taskInfo.totalBytes) + if (taskInfo.bytesDownloaded === taskInfo.bytesTotal) task.state = 'DONE' else // IOS completed the download but it was not done. @@ -68,11 +76,12 @@ export function checkForExistingDownloads () { } tasksMap.set(taskInfo.id, task) return task - }).filter(task => task !== null) + }).filter(task => !!task) }) } -export function ensureDownloadsAreRunning () { +export function ensureDownloadsAreRunning() { + console.log('[RNBackgroundDownloader] ensureDownloadsAreRunning') return checkForExistingDownloads() .then(tasks => { for (const task of tasks) @@ -83,7 +92,12 @@ export function ensureDownloadsAreRunning () { }) } -export function completeHandler (jobId) { +export function completeHandler(jobId: string) { + if (jobId == null) { + console.warn('[RNBackgroundDownloader] completeHandler: jobId is empty') + return + } + return RNBackgroundDownloader.completeHandler(jobId) } @@ -93,9 +107,12 @@ type DownloadOptions = { destination: string, headers?: object, metadata?: object, + isAllowedOverRoaming?: boolean, + isAllowedOverMetered?: boolean, } -export function download (options : DownloadOptions) { +export function download(options: DownloadOptions) { + console.log('[RNBackgroundDownloader] download', options) if (!options.id || !options.url || !options.destination) throw new Error('[RNBackgroundDownloader] id, url and destination are required') @@ -104,6 +121,11 @@ export function download (options : DownloadOptions) { if (!(options.metadata && typeof options.metadata === 'object')) options.metadata = {} + options.destination = options.destination.replace('file://', '') + + if (options.isAllowedOverRoaming == null) options.isAllowedOverRoaming = true + if (options.isAllowedOverMetered == null) options.isAllowedOverMetered = true + const task = new DownloadTask({ id: options.id, metadata: options.metadata, @@ -122,17 +144,6 @@ export const directories = { documents: RNBackgroundDownloader.documents, } -export const Network = { - WIFI_ONLY: RNBackgroundDownloader.OnlyWifi, - ALL: RNBackgroundDownloader.AllNetworks, -} - -export const Priority = { - HIGH: RNBackgroundDownloader.PriorityHigh, - MEDIUM: RNBackgroundDownloader.PriorityNormal, - LOW: RNBackgroundDownloader.PriorityLow, -} - export default { initDownloader, download, @@ -141,6 +152,4 @@ export default { completeHandler, setHeaders, directories, - Network, - Priority, } diff --git a/ios/RNBackgroundDownloader.h b/ios/RNBackgroundDownloader.h index 0e72fac..5017a88 100644 --- a/ios/RNBackgroundDownloader.h +++ b/ios/RNBackgroundDownloader.h @@ -19,6 +19,6 @@ typedef void (^CompletionHandler)(); @interface RNBackgroundDownloader : RCTEventEmitter -+ (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler; ++ (void)setCompletionHandlerWithIdentifier:(NSString *)identifier completionHandler:(CompletionHandler)completionHandler; @end diff --git a/ios/RNBackgroundDownloader.m b/ios/RNBackgroundDownloader.m index d413a10..b05479e 100644 --- a/ios/RNBackgroundDownloader.m +++ b/ios/RNBackgroundDownloader.m @@ -62,7 +62,7 @@ RCT_EXPORT_MODULE(); taskToConfigMap = [[NSMutableDictionary alloc] init]; } idToTaskMap = [[NSMutableDictionary alloc] init]; - idToResumeDataMap= [[NSMutableDictionary alloc] init]; + idToResumeDataMap = [[NSMutableDictionary alloc] init]; idToPercentMap = [[NSMutableDictionary alloc] init]; NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; @@ -286,8 +286,8 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej @"id": taskConfig.id, @"metadata": taskConfig.metadata, @"state": [NSNumber numberWithInt: task.state], - @"bytesWritten": [NSNumber numberWithLongLong:task.countOfBytesReceived], - @"totalBytes": [NSNumber numberWithLongLong:task.countOfBytesExpectedToReceive], + @"bytesDownloaded": [NSNumber numberWithLongLong:task.countOfBytesReceived], + @"bytesTotal": [NSNumber numberWithLongLong:task.countOfBytesExpectedToReceive], @"percent": percent }]; taskConfig.reportedBegin = YES; @@ -341,11 +341,11 @@ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId } } -- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { +- (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)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { +- (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)]; @@ -356,7 +356,7 @@ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId if (self.bridge) { [self sendEventWithName:@"downloadBegin" body:@{ @"id": taskCofig.id, - @"expectedBytes": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], + @"expectedBytes": [NSNumber numberWithLongLong: bytesTotalExpectedToWrite], @"headers": responseHeaders }]; } @@ -364,9 +364,9 @@ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId } NSNumber *prevPercent = idToPercentMap[taskCofig.id]; - NSNumber *percent = [NSNumber numberWithFloat:(float)totalBytesWritten/(float)totalBytesExpectedToWrite]; + NSNumber *percent = [NSNumber numberWithFloat:(float)bytesTotalWritten/(float)bytesTotalExpectedToWrite]; if ([percent floatValue] - [prevPercent floatValue] > 0.01f) { - progressReports[taskCofig.id] = @{@"id": taskCofig.id, @"written": [NSNumber numberWithLongLong: totalBytesWritten], @"total": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], @"percent": percent}; + progressReports[taskCofig.id] = @{@"id": taskCofig.id, @"bytesDownloaded": [NSNumber numberWithLongLong: bytesTotalWritten], @"bytesTotal": [NSNumber numberWithLongLong: bytesTotalExpectedToWrite], @"percent": percent}; idToPercentMap[taskCofig.id] = percent; } diff --git a/lib/DownloadTask.ts b/lib/DownloadTask.ts index 08d53cb..865c6bd 100644 --- a/lib/DownloadTask.ts +++ b/lib/DownloadTask.ts @@ -16,14 +16,14 @@ export default class DownloadTask { metadata = {} percent = 0 - bytesWritten = 0 - totalBytes = 0 + bytesDownloaded = 0 + bytesTotal = 0 constructor (taskInfo: TaskInfo, originalTask?: TaskInfo) { this.id = taskInfo.id this.percent = taskInfo.percent ?? 0 - this.bytesWritten = taskInfo.bytesWritten ?? 0 - this.totalBytes = taskInfo.totalBytes ?? 0 + this.bytesDownloaded = taskInfo.bytesDownloaded ?? 0 + this.bytesTotal = taskInfo.bytesTotal ?? 0 const metadata = this.tryParseJson(taskInfo.metadata) if (metadata) @@ -66,11 +66,11 @@ export default class DownloadTask { this.beginHandler?.({ expectedBytes, headers }) } - onProgress (percent, bytesWritten, totalBytes) { + onProgress (percent, bytesDownloaded, bytesTotal) { this.percent = percent - this.bytesWritten = bytesWritten - this.totalBytes = totalBytes - this.progressHandler?.(percent, bytesWritten, totalBytes) + this.bytesDownloaded = bytesDownloaded + this.bytesTotal = bytesTotal + this.progressHandler?.(percent, bytesDownloaded, bytesTotal) } onDone ({ location }) { diff --git a/package.json b/package.json index 78104be..ca00ad2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kesha-antonov/react-native-background-downloader", - "version": "2.10.0", + "version": "3.0.0-alpha.0", "description": "A library for React-Native to help you download large files on iOS and Android both in the foreground and most importantly in the background.", "keywords": [ "react-native",