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)). 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. 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 // PROCESS YOUR STUFF
// FINISH DOWNLOAD JOB ON IOS // FINISH DOWNLOAD JOB
if (Platform.OS === 'ios') completeHandler(jobId)
completeHandler(jobId)
}).error(error => { }).error(error => {
console.log('Download canceled due to error: ', error); console.log('Download canceled due to error: ', error);
}) })
@@ -205,15 +204,15 @@ Download a file to destination
An object containing options properties An object containing options properties
| Property | Type | Required | Platforms | Info | | 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 | | `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 | | `url` | String || All | URL to file you want to download |
| `destination` | String | ✅ | All | Where to copy the file to once the download is done | | `destination` | String | ✅ | All | Where to copy the file to once the download is done |
| `metadata` | Object | | All | Data to be preserved on reboot. | | `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 | `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 | | `isAllowedOverRoaming` | Boolean | | Android | whether this download may proceed over a roaming connection. By default, roaming is allowed |
| `network` | [Network (enum)](#network-enum---android-only) | | Android | Give your the ability to limit the download to WIFI only. **Default:** Network.ALL | | `isAllowedOverMetered` | Boolean | | Android | Whether this download may proceed over a metered network connection. By default, metered networks are allowed |
**returns** **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` | | `id` | String | The id you gave the task when calling `RNBackgroundDownloader.download` |
| `metadata` | Object | The metadata 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 | | `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 | | `bytesDownloaded` | 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 | | `bytesTotal` | Number | The number bytes expected to be written by this task or more plainly, the file size being downloaded |
### `completeHandler(jobId)` ### `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. After finishing download in background you have some time to process your JS logic and finish the job.
### `ensureDownloadsAreRunning` ### `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. 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 | | `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 | | `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 | | `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 | | `error` | error | Called when the download stops due to an error |
### `pause()` ### `pause()`
Pauses the download Pauses the download
@@ -321,31 +320,6 @@ Resumes a pause download
### `stop()` ### `stop()`
Stops the download for good and removes the file that was written so far 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 ## Constants
### directories ### 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. 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 ## Authors
Developed by [Elad Gil](https://github.com/ptelad) of [Eko](http://www.helloeko.com) 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' apply plugin: 'com.android.library'
def useAndroidX = (project.hasProperty('android.useAndroidX')) ? project.property('android.useAndroidX') : false
def safeExtGet(prop, fallback) { def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
} }
@@ -26,11 +24,4 @@ android {
dependencies { dependencies {
//noinspection GradleDynamicVersion //noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.eko"> 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 class RNBGDTaskConfig implements Serializable {
public String id; public String id;
public String url;
public String destination; public String destination;
public String metadata = "{}"; public String metadata = "{}";
public boolean reportedBegin; 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.id = id;
this.url = url;
this.destination = destination; this.destination = destination;
this.metadata = metadata; this.metadata = metadata;
this.reportedBegin = false; 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.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.tonyodev.fetch2.Download;
import com.tonyodev.fetch2core.Downloader; import android.app.DownloadManager;
import com.tonyodev.fetch2okhttp.OkHttpDownloader; import android.app.DownloadManager.Request;
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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -42,16 +29,33 @@ import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Scanner;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import okhttp3.OkHttpClient;
import java.util.Set; import java.util.Set;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; 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_RUNNING = 0;
private static final int TASK_SUSPENDED = 1; 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_FILE_NOT_FOUND = 3;
private static final int ERR_OTHERS = 100; private static final int ERR_OTHERS = 100;
private static Map<Status, Integer> stateMap = new HashMap<Status, Integer>() {{ private static Map<Integer, Integer> stateMap = new HashMap<Integer, Integer>() {
put(Status.DOWNLOADING, TASK_RUNNING); {
put(Status.COMPLETED, TASK_COMPLETED); put(DownloadManager.STATUS_FAILED, TASK_CANCELING);
put(Status.PAUSED, TASK_SUSPENDED); put(DownloadManager.STATUS_PAUSED, TASK_SUSPENDED);
put(Status.QUEUED, TASK_RUNNING); put(DownloadManager.STATUS_PENDING, TASK_RUNNING);
put(Status.CANCELLED, TASK_CANCELING); put(DownloadManager.STATUS_RUNNING, TASK_RUNNING);
put(Status.FAILED, TASK_CANCELING); put(DownloadManager.STATUS_SUCCESSFUL, TASK_COMPLETED);
put(Status.REMOVED, TASK_CANCELING); }
put(Status.DELETED, TASK_CANCELING); };
put(Status.NONE, TASK_CANCELING);
}};
private Fetch fetch; private Downloader downloader;
private Map<String, Integer> idToRequestId = new HashMap<>();
@SuppressLint("UseSparseArrays") private Map<String, Long> condigIdToDownloadId = new HashMap<>();
private Map<Integer, RNBGDTaskConfig> requestIdToConfig = new HashMap<>(); private Map<Long, RNBGDTaskConfig> downloadIdToConfig = new HashMap<>();
private DeviceEventManagerModule.RCTDeviceEventEmitter ee; private DeviceEventManagerModule.RCTDeviceEventEmitter ee;
private Date lastProgressReport = new Date(); private Map<String, OnProgress> onProgressThreads = new HashMap<>();
private HashMap<String, WritableMap> progressReports = new HashMap<>();
private static Object sharedLock = new Object(); 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) { public RNBackgroundDownloaderModule(ReactApplicationContext reactContext) {
super(reactContext); super(reactContext);
ReadableMap emptyMap = Arguments.createMap(); ReadableMap emptyMap = Arguments.createMap();
this.initDownloader(emptyMap); 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 @Override
public void onCatalystInstanceDestroy() { public void onCatalystInstanceDestroy() {
fetch.close();
} }
@Override @Override
@@ -132,46 +216,27 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp
constants.put("TaskSuspended", TASK_SUSPENDED); constants.put("TaskSuspended", TASK_SUSPENDED);
constants.put("TaskCanceling", TASK_CANCELING); constants.put("TaskCanceling", TASK_CANCELING);
constants.put("TaskCompleted", TASK_COMPLETED); 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; return constants;
} }
@ReactMethod @ReactMethod
public void initDownloader(ReadableMap options) { public void initDownloader(ReadableMap options) {
if (fetch != null) { Log.d(getName(), "RNBD: initDownloader");
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);
loadConfigMap(); loadConfigMap();
FetchConfiguration fetchConfiguration = new FetchConfiguration.Builder(this.getReactApplicationContext())
.setDownloadConcurrentLimit(4) // TODO. MAYBE REINIT DOWNLOADER
.setHttpDownloader(okHttpDownloader)
.enableRetryOnNetworkGain(true)
.setHttpDownloader(new HttpUrlConnectionDownloader(downloaderType))
.build();
fetch = Fetch.Impl.getInstance(fetchConfiguration);
fetch.addListener(this);
} }
private void removeFromMaps(int requestId) { private void removeFromMaps(long downloadId) {
synchronized(sharedLock) { Log.d(getName(), "RNBD: removeFromMaps");
RNBGDTaskConfig config = requestIdToConfig.get(requestId);
synchronized (sharedLock) {
RNBGDTaskConfig config = downloadIdToConfig.get(downloadId);
if (config != null) { if (config != null) {
idToRequestId.remove(config.id); condigIdToDownloadId.remove(config.id);
requestIdToConfig.remove(requestId); downloadIdToConfig.remove(downloadId);
saveConfigMap(); saveConfigMap();
} }
@@ -179,11 +244,13 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp
} }
private void saveConfigMap() { private void saveConfigMap() {
synchronized(sharedLock) { Log.d(getName(), "RNBD: saveConfigMap");
synchronized (sharedLock) {
File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap");
try { try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file)); ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file));
outputStream.writeObject(requestIdToConfig); outputStream.writeObject(downloadIdToConfig);
outputStream.flush(); outputStream.flush();
outputStream.close(); outputStream.close();
} catch (IOException e) { } catch (IOException e) {
@@ -193,31 +260,17 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp
} }
private void loadConfigMap() { private void loadConfigMap() {
Log.d(getName(), "RNBD: loadConfigMap");
File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap");
try { try {
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file)); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
requestIdToConfig = (Map<Integer, RNBGDTaskConfig>) inputStream.readObject(); downloadIdToConfig = (Map<Long, RNBGDTaskConfig>) inputStream.readObject();
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
e.printStackTrace(); 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 // JS Methods
@ReactMethod @ReactMethod
public void download(ReadableMap options) { public void download(ReadableMap options) {
@@ -226,277 +279,220 @@ public class RNBackgroundDownloaderModule extends ReactContextBaseJavaModule imp
String destination = options.getString("destination"); String destination = options.getString("destination");
ReadableMap headers = options.getMap("headers"); ReadableMap headers = options.getMap("headers");
String metadata = options.getString("metadata"); 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) { if (id == null || url == null || destination == null) {
Log.e(getName(), "id, url and destination must be set"); Log.e(getName(), "id, url and destination must be set");
return; return;
} }
RNBGDTaskConfig config = new RNBGDTaskConfig(id, destination, metadata); RNBGDTaskConfig config = new RNBGDTaskConfig(id, url, destination, metadata);
final Request request = new Request(url, destination); 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) { if (headers != null) {
ReadableMapKeySetIterator it = headers.keySetIterator(); ReadableMapKeySetIterator it = headers.keySetIterator();
while (it.hasNextKey()) { while (it.hasNextKey()) {
String headerKey = it.nextKey(); 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>() { long downloadId = downloader.queueDownload(request);
@Override
public void call(Request download) {
}
}, new Func<Error>() {
@Override
public void call(Error error) {
//An error occurred when enqueuing a request.
WritableMap params = Arguments.createMap(); synchronized (sharedLock) {
params.putString("id", id); condigIdToDownloadId.put(id, downloadId);
params.putString("error", error.toString()); downloadIdToConfig.put(downloadId, config);
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);
saveConfigMap(); 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 @ReactMethod
public void pauseTask(String identifier) { public void pauseTask(String configId) {
synchronized(sharedLock) { Log.d(getName(), "RNBD: pauseTask " + configId);
Integer requestId = idToRequestId.get(identifier);
if (requestId != null) { synchronized (sharedLock) {
fetch.pause(requestId); 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 @ReactMethod
public void resumeTask(String identifier) { public void stopTask(String configId) {
synchronized(sharedLock) { Log.d(getName(), "RNBD: stopTask-1 " + configId);
Integer requestId = idToRequestId.get(identifier);
if (requestId != null) { synchronized (sharedLock) {
fetch.resume(requestId); 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 @ReactMethod
public void stopTask(String identifier) { public void completeHandler(String configId, final Promise promise) {
synchronized(sharedLock) { Log.d(getName(), "RNBD: completeHandler-1 " + configId);
Integer requestId = idToRequestId.get(identifier);
if (requestId != null) { synchronized (sharedLock) {
fetch.cancel(requestId); 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 @ReactMethod
public void checkForExistingDownloads(final Promise promise) { public void checkForExistingDownloads(final Promise promise) {
fetch.getDownloads(new Func<List<Download>>() { Log.d(getName(), "RNBD: checkForExistingDownloads-1");
@Override
public void call(@NotNull List<Download> downloads) {
WritableArray foundIds = Arguments.createArray();
synchronized(sharedLock) { WritableArray foundIds = Arguments.createArray();
for (Download download : downloads) {
if (requestIdToConfig.containsKey(download.getId())) { synchronized (sharedLock) {
RNBGDTaskConfig config = requestIdToConfig.get(download.getId()); 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(); WritableMap params = Arguments.createMap();
params.putString("id", config.id); params.putString("id", config.id);
params.putString("metadata", config.metadata); params.putString("metadata", config.metadata);
params.putInt("state", stateMap.get(download.getStatus())); params.putInt("state", stateMap.get(result.getInt("status")));
params.putInt("bytesWritten", (int)download.getDownloaded());
params.putInt("totalBytes", (int)download.getTotal()); int bytesDownloaded = result.getInt("bytesDownloaded");
params.putDouble("percent", ((double)download.getProgress()) / 100); params.putInt("bytesDownloaded", bytesDownloaded);
int bytesTotal = result.getInt("bytesTotal");
params.putInt("bytesTotal", bytesTotal);
params.putDouble("percent", ((double) bytesDownloaded / bytesTotal));
foundIds.pushMap(params); foundIds.pushMap(params);
// TODO: MAYBE ADD headers // TODO: MAYBE ADD headers
idToRequestId.put(config.id, download.getId()); condigIdToDownloadId.put(config.id, downloadId);
config.reportedBegin = true;
// TOREMOVE
// config.reportedBegin = true;
} else { } else {
fetch.delete(download.getId()); Log.d(getName(), "RNBD: checkForExistingDownloads-3");
downloader.cancelDownload(downloadId);
} }
} } while (cursor.moveToNext());
} }
promise.resolve(foundIds); cursor.close();
} } catch (Exception e) {
}); Log.e(getName(), "Error in checkForExistingDownloads: " + e.getLocalizedMessage());
}
// 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());
} }
} }
}
@Override promise.resolve(foundIds);
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) {
} }
} }

30
index.d.ts vendored
View File

@@ -16,8 +16,8 @@ export interface TaskInfoObject {
metadata: object | string; metadata: object | string;
percent?: number; percent?: number;
bytesWritten?: number; bytesDownloaded?: number;
totalBytes?: number; bytesTotal?: number;
beginHandler?: Function; beginHandler?: Function;
progressHandler?: Function; progressHandler?: Function;
@@ -37,8 +37,8 @@ export type BeginHandler = ({
}: BeginHandlerObject) => any; }: BeginHandlerObject) => any;
export type ProgressHandler = ( export type ProgressHandler = (
percent: number, percent: number,
bytesWritten: number, bytesDownloaded: number,
totalBytes: number bytesTotal: number
) => any; ) => any;
export type DoneHandler = () => any; export type DoneHandler = () => any;
export type ErrorHandler = (error: any, errorCode: any) => any; export type ErrorHandler = (error: any, errorCode: any) => any;
@@ -55,8 +55,8 @@ export interface DownloadTask {
id: string; id: string;
state: DownloadTaskState; state: DownloadTaskState;
percent: number; percent: number;
bytesWritten: number; bytesDownloaded: number;
totalBytes: number; bytesTotal: number;
begin: (handler: BeginHandler) => DownloadTask; begin: (handler: BeginHandler) => DownloadTask;
progress: (handler: ProgressHandler) => DownloadTask; progress: (handler: ProgressHandler) => DownloadTask;
@@ -77,7 +77,6 @@ export type CheckForExistingDownloads = () => Promise<DownloadTask[]>;
export type EnsureDownloadsAreRunning = () => Promise<void>; export type EnsureDownloadsAreRunning = () => Promise<void>;
export interface InitDownloaderOptions { export interface InitDownloaderOptions {
type?: 'parallel' | 'sequential' | null;
} }
export type InitDownloader = (options: InitDownloaderOptions) => undefined; export type InitDownloader = (options: InitDownloaderOptions) => undefined;
@@ -87,6 +86,8 @@ export interface DownloadOption {
destination: string; destination: string;
headers?: DownloadHeaders | undefined; headers?: DownloadHeaders | undefined;
metadata?: object; metadata?: object;
isAllowedOverRoaming?: boolean;
isAllowedOverMetered?: boolean;
} }
export type Download = (options: DownloadOption) => DownloadTask; export type Download = (options: DownloadOption) => DownloadTask;
@@ -96,17 +97,6 @@ export interface Directories {
documents: string; 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 setHeaders: SetHeaders;
export const checkForExistingDownloads: CheckForExistingDownloads; export const checkForExistingDownloads: CheckForExistingDownloads;
export const ensureDownloadsAreRunning: EnsureDownloadsAreRunning; export const ensureDownloadsAreRunning: EnsureDownloadsAreRunning;
@@ -114,8 +104,6 @@ export const initDownloader: InitDownloader;
export const download: Download; export const download: Download;
export const completeHandler: CompleteHandler; export const completeHandler: CompleteHandler;
export const directories: Directories; export const directories: Directories;
export const Network: Network;
export const Priority: Priority;
export interface RNBackgroundDownloader { export interface RNBackgroundDownloader {
setHeaders: SetHeaders; setHeaders: SetHeaders;
@@ -125,8 +113,6 @@ export interface RNBackgroundDownloader {
download: Download; download: Download;
completeHandler: CompleteHandler; completeHandler: CompleteHandler;
directories: Directories; directories: Directories;
Network: Network;
Priority: Priority;
} }
declare const RNBackgroundDownloader: RNBackgroundDownloader; declare const RNBackgroundDownloader: RNBackgroundDownloader;

View File

@@ -7,14 +7,23 @@ const RNBackgroundDownloaderEmitter = new NativeEventEmitter(RNBackgroundDownloa
const tasksMap = new Map() const tasksMap = new Map()
let headers = {} 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 => { RNBackgroundDownloaderEmitter.addListener('downloadProgress', events => {
// console.log('[RNBackgroundDownloader] downloadProgress-1', events, tasksMap)
for (const event of events) { for (const event of events) {
const task = tasksMap.get(event.id) 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 }) => { RNBackgroundDownloaderEmitter.addListener('downloadComplete', ({ id, location }) => {
console.log('[RNBackgroundDownloader] downloadComplete', id, location)
const task = tasksMap.get(id) const task = tasksMap.get(id)
task?.onDone({ location }) task?.onDone({ location })
@@ -22,35 +31,34 @@ RNBackgroundDownloaderEmitter.addListener('downloadComplete', ({ id, location })
}) })
RNBackgroundDownloaderEmitter.addListener('downloadFailed', event => { RNBackgroundDownloaderEmitter.addListener('downloadFailed', event => {
console.log('[RNBackgroundDownloader] downloadFailed', event)
const task = tasksMap.get(event.id) const task = tasksMap.get(event.id)
task?.onError(event.error, event.errorcode) task?.onError(event.error, event.errorCode)
tasksMap.delete(event.id) tasksMap.delete(event.id)
}) })
RNBackgroundDownloaderEmitter.addListener('downloadBegin', ({ id, expectedBytes, headers }) => { export function setHeaders(h = {}) {
const task = tasksMap.get(id)
task?.onBegin({ expectedBytes, headers })
})
export function setHeaders (h = {}) {
if (typeof h !== 'object') if (typeof h !== 'object')
throw new Error('[RNBackgroundDownloader] headers must be an object') throw new Error('[RNBackgroundDownloader] headers must be an object')
headers = h headers = h
} }
export function initDownloader (options = {}) { export function initDownloader(options = {}) {
if (Platform.OS === 'android') if (Platform.OS === 'android')
RNBackgroundDownloader.initDownloader(options) RNBackgroundDownloader.initDownloader(options)
} }
export function checkForExistingDownloads () { export function checkForExistingDownloads() {
console.log('[RNBackgroundDownloader] checkForExistingDownloads-1')
return RNBackgroundDownloader.checkForExistingDownloads() return RNBackgroundDownloader.checkForExistingDownloads()
.then(foundTasks => { .then(foundTasks => {
console.log('[RNBackgroundDownloader] checkForExistingDownloads-2', foundTasks)
return foundTasks.map(taskInfo => { return foundTasks.map(taskInfo => {
// SECOND ARGUMENT RE-ASSIGNS EVENT HANDLERS // SECOND ARGUMENT RE-ASSIGNS EVENT HANDLERS
const task = new DownloadTask(taskInfo, tasksMap.get(taskInfo.id)) const task = new DownloadTask(taskInfo, tasksMap.get(taskInfo.id))
console.log('[RNBackgroundDownloader] checkForExistingDownloads-3', taskInfo)
if (taskInfo.state === RNBackgroundDownloader.TaskRunning) { if (taskInfo.state === RNBackgroundDownloader.TaskRunning) {
task.state = 'DOWNLOADING' task.state = 'DOWNLOADING'
@@ -60,7 +68,7 @@ export function checkForExistingDownloads () {
task.stop() task.stop()
return null return null
} else if (taskInfo.state === RNBackgroundDownloader.TaskCompleted) { } else if (taskInfo.state === RNBackgroundDownloader.TaskCompleted) {
if (taskInfo.bytesWritten === taskInfo.totalBytes) if (taskInfo.bytesDownloaded === taskInfo.bytesTotal)
task.state = 'DONE' task.state = 'DONE'
else else
// IOS completed the download but it was not done. // IOS completed the download but it was not done.
@@ -68,11 +76,12 @@ export function checkForExistingDownloads () {
} }
tasksMap.set(taskInfo.id, task) tasksMap.set(taskInfo.id, task)
return task return task
}).filter(task => task !== null) }).filter(task => !!task)
}) })
} }
export function ensureDownloadsAreRunning () { export function ensureDownloadsAreRunning() {
console.log('[RNBackgroundDownloader] ensureDownloadsAreRunning')
return checkForExistingDownloads() return checkForExistingDownloads()
.then(tasks => { .then(tasks => {
for (const task of 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) return RNBackgroundDownloader.completeHandler(jobId)
} }
@@ -93,9 +107,12 @@ type DownloadOptions = {
destination: string, destination: string,
headers?: object, headers?: object,
metadata?: 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) if (!options.id || !options.url || !options.destination)
throw new Error('[RNBackgroundDownloader] id, url and destination are required') 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')) if (!(options.metadata && typeof options.metadata === 'object'))
options.metadata = {} 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({ const task = new DownloadTask({
id: options.id, id: options.id,
metadata: options.metadata, metadata: options.metadata,
@@ -122,17 +144,6 @@ export const directories = {
documents: RNBackgroundDownloader.documents, 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 { export default {
initDownloader, initDownloader,
download, download,
@@ -141,6 +152,4 @@ export default {
completeHandler, completeHandler,
setHeaders, setHeaders,
directories, directories,
Network,
Priority,
} }

View File

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

View File

@@ -62,7 +62,7 @@ RCT_EXPORT_MODULE();
taskToConfigMap = [[NSMutableDictionary alloc] init]; taskToConfigMap = [[NSMutableDictionary alloc] init];
} }
idToTaskMap = [[NSMutableDictionary alloc] init]; idToTaskMap = [[NSMutableDictionary alloc] init];
idToResumeDataMap= [[NSMutableDictionary alloc] init]; idToResumeDataMap = [[NSMutableDictionary alloc] init];
idToPercentMap = [[NSMutableDictionary alloc] init]; idToPercentMap = [[NSMutableDictionary alloc] init];
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"];
@@ -286,8 +286,8 @@ RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rej
@"id": taskConfig.id, @"id": taskConfig.id,
@"metadata": taskConfig.metadata, @"metadata": taskConfig.metadata,
@"state": [NSNumber numberWithInt: task.state], @"state": [NSNumber numberWithInt: task.state],
@"bytesWritten": [NSNumber numberWithLongLong:task.countOfBytesReceived], @"bytesDownloaded": [NSNumber numberWithLongLong:task.countOfBytesReceived],
@"totalBytes": [NSNumber numberWithLongLong:task.countOfBytesExpectedToReceive], @"bytesTotal": [NSNumber numberWithLongLong:task.countOfBytesExpectedToReceive],
@"percent": percent @"percent": percent
}]; }];
taskConfig.reportedBegin = YES; 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]"); 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]"); NSLog(@"[RNBackgroundDownloader] - [didWriteData]");
@synchronized (sharedLock) { @synchronized (sharedLock) {
RNBGDTaskConfig *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)]; RNBGDTaskConfig *taskCofig = taskToConfigMap[@(downloadTask.taskIdentifier)];
@@ -356,7 +356,7 @@ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId
if (self.bridge) { if (self.bridge) {
[self sendEventWithName:@"downloadBegin" body:@{ [self sendEventWithName:@"downloadBegin" body:@{
@"id": taskCofig.id, @"id": taskCofig.id,
@"expectedBytes": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], @"expectedBytes": [NSNumber numberWithLongLong: bytesTotalExpectedToWrite],
@"headers": responseHeaders @"headers": responseHeaders
}]; }];
} }
@@ -364,9 +364,9 @@ RCT_EXPORT_METHOD(completeHandler:(nonnull NSString *)jobId
} }
NSNumber *prevPercent = idToPercentMap[taskCofig.id]; 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) { 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; idToPercentMap[taskCofig.id] = percent;
} }

View File

@@ -16,14 +16,14 @@ export default class DownloadTask {
metadata = {} metadata = {}
percent = 0 percent = 0
bytesWritten = 0 bytesDownloaded = 0
totalBytes = 0 bytesTotal = 0
constructor (taskInfo: TaskInfo, originalTask?: TaskInfo) { constructor (taskInfo: TaskInfo, originalTask?: TaskInfo) {
this.id = taskInfo.id this.id = taskInfo.id
this.percent = taskInfo.percent ?? 0 this.percent = taskInfo.percent ?? 0
this.bytesWritten = taskInfo.bytesWritten ?? 0 this.bytesDownloaded = taskInfo.bytesDownloaded ?? 0
this.totalBytes = taskInfo.totalBytes ?? 0 this.bytesTotal = taskInfo.bytesTotal ?? 0
const metadata = this.tryParseJson(taskInfo.metadata) const metadata = this.tryParseJson(taskInfo.metadata)
if (metadata) if (metadata)
@@ -66,11 +66,11 @@ export default class DownloadTask {
this.beginHandler?.({ expectedBytes, headers }) this.beginHandler?.({ expectedBytes, headers })
} }
onProgress (percent, bytesWritten, totalBytes) { onProgress (percent, bytesDownloaded, bytesTotal) {
this.percent = percent this.percent = percent
this.bytesWritten = bytesWritten this.bytesDownloaded = bytesDownloaded
this.totalBytes = totalBytes this.bytesTotal = bytesTotal
this.progressHandler?.(percent, bytesWritten, totalBytes) this.progressHandler?.(percent, bytesDownloaded, bytesTotal)
} }
onDone ({ location }) { onDone ({ location }) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@kesha-antonov/react-native-background-downloader", "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.", "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": [ "keywords": [
"react-native", "react-native",