mirror of
https://github.com/zoriya/react-native-background-downloader.git
synced 2025-12-06 06:56:10 +00:00
rewritten android side from Fetch to DownloadManager. Everything works except manual pause/resume
This commit is contained in:
80
README.md
80
README.md
@@ -12,7 +12,7 @@ On iOS, if you want to download big files no matter the state of your app, wethe
|
|||||||
|
|
||||||
This API handles your downloads separately from your app and only keeps it informed using delegates (Read: [Downloading Files in the Background](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background)).
|
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)
|
||||||
|
|||||||
@@ -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'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|
||||||
|
|||||||
157
android/src/main/java/com/eko/Downloader.java
Normal file
157
android/src/main/java/com/eko/Downloader.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
android/src/main/java/com/eko/OnBegin.java
Normal file
49
android/src/main/java/com/eko/OnBegin.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
android/src/main/java/com/eko/OnProgress.java
Normal file
132
android/src/main/java/com/eko/OnProgress.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
30
index.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
65
index.ts
65
index.ts
@@ -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,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user