rewritten android side from Fetch to DownloadManager. Everything works except manual pause/resume

This commit is contained in:
Kesha Antonov
2023-12-22 15:16:46 +03:00
parent 401d31ce43
commit bd1e4adfc6
14 changed files with 727 additions and 446 deletions

View File

@@ -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)

View File

@@ -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'
}
}

View File

@@ -1,4 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.eko">

View File

@@ -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;
}
}

View File

@@ -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<String, List<String>> headers = con.getHeaderFields();
Set<String> 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();
}
}
}

View File

@@ -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<String, WritableMap> 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;
}
}
}
}

View File

@@ -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;

View File

@@ -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<Status, Integer> stateMap = new HashMap<Status, Integer>() {{
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<Integer, Integer> stateMap = new HashMap<Integer, Integer>() {
{
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<String, Integer> idToRequestId = new HashMap<>();
@SuppressLint("UseSparseArrays")
private Map<Integer, RNBGDTaskConfig> requestIdToConfig = new HashMap<>();
private Downloader downloader;
private Map<String, Long> condigIdToDownloadId = new HashMap<>();
private Map<Long, RNBGDTaskConfig> downloadIdToConfig = new HashMap<>();
private DeviceEventManagerModule.RCTDeviceEventEmitter ee;
private Date lastProgressReport = new Date();
private HashMap<String, WritableMap> progressReports = new HashMap<>();
private Map<String, OnProgress> 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<Integer, RNBGDTaskConfig>) inputStream.readObject();
downloadIdToConfig = (Map<Long, RNBGDTaskConfig>) 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<Request>() {
@Override
public void call(Request download) {
}
}, new Func<Error>() {
@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<List<Download>>() {
@Override
public void call(@NotNull List<Download> 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<String,List<String>> headers = con.getHeaderFields();
Set<String> 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<? extends DownloadBlock> list, int i) {
promise.resolve(foundIds);
}
}

30
index.d.ts vendored
View File

@@ -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<DownloadTask[]>;
export type EnsureDownloadsAreRunning = () => Promise<void>;
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;

View File

@@ -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,
}

View File

@@ -19,6 +19,6 @@ typedef void (^CompletionHandler)();
@interface RNBackgroundDownloader : RCTEventEmitter <RCTBridgeModule, NSURLSessionDelegate, NSURLSessionDownloadDelegate>
+ (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler;
+ (void)setCompletionHandlerWithIdentifier:(NSString *)identifier completionHandler:(CompletionHandler)completionHandler;
@end

View File

@@ -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;
}

View File

@@ -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 }) {

View File

@@ -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",