commit ac1fe56e3f6539d8b337828799cf73f5c54281b0 Author: Elad Gil Date: Tue Apr 24 11:52:58 2018 +0300 first commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..eb39591 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cadbfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ + +# OSX +# +.DS_Store + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# BUCK +buck-out/ +\.buckd/ +*.keystore + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..923a203 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ + +# react-native-background-download + +## Getting started + +`$ npm install react-native-background-download --save` + +### Mostly automatic installation + +`$ react-native link react-native-background-download` + +### Manual installation + + +#### iOS + +1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]` +2. Go to `node_modules` ➜ `react-native-background-download` and add `RNBackgroundDownload.xcodeproj` +3. In XCode, in the project navigator, select your project. Add `libRNBackgroundDownload.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` +4. Run your project (`Cmd+R`)< + +#### Android + +1. Open up `android/app/src/main/java/[...]/MainActivity.java` + - Add `import com.eko.RNBackgroundDownloadPackage;` to the imports at the top of the file + - Add `new RNBackgroundDownloadPackage()` to the list returned by the `getPackages()` method +2. Append the following lines to `android/settings.gradle`: + ``` + include ':react-native-background-download' + project(':react-native-background-download').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-download/android') + ``` +3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: + ``` + compile project(':react-native-background-download') + ``` + + +## Usage +```javascript +import RNBackgroundDownload from 'react-native-background-download'; + +// TODO: What to do with the module? +RNBackgroundDownload; +``` + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..ce880ce --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,37 @@ + +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + } + lintOptions { + abortOnError false + } +} + +repositories { + mavenCentral() +} + +dependencies { + compile "com.tonyodev.fetch2:fetch2:2.0.0-RC12" + compile 'com.facebook.react:react-native:+' +} + \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7a3265e Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0f6a4f8 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Apr 24 11:44:55 IDT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..005b242 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/src/main/java/com/eko/RNBackgroundDownloadModule.java b/android/src/main/java/com/eko/RNBackgroundDownloadModule.java new file mode 100644 index 0000000..e6dffb8 --- /dev/null +++ b/android/src/main/java/com/eko/RNBackgroundDownloadModule.java @@ -0,0 +1,309 @@ + +package com.eko; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.ekoapp.BuildConfig; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.tonyodev.fetch2.Download; +import com.tonyodev.fetch2.Error; +import com.tonyodev.fetch2.Fetch; +import com.tonyodev.fetch2.FetchListener; +import com.tonyodev.fetch2.Func; +import com.tonyodev.fetch2.NetworkType; +import com.tonyodev.fetch2.Priority; +import com.tonyodev.fetch2.Request; +import com.tonyodev.fetch2.Status; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RNBackgroundDownloadModule extends ReactContextBaseJavaModule { + + private static final int TASK_RUNNING = 0; + private static final int TASK_SUSPENDED = 1; + private static final int TASK_CANCELING = 2; + private static final int TASK_COMPLETED = 3; + + private static Map stateMap = new HashMap() {{ + put(Status.DOWNLOADING, TASK_RUNNING); + put(Status.COMPLETED, TASK_COMPLETED); + put(Status.PAUSED, TASK_SUSPENDED); + put(Status.QUEUED, TASK_RUNNING); + put(Status.CANCELLED, TASK_CANCELING); + put(Status.FAILED, TASK_CANCELING); + put(Status.REMOVED, TASK_CANCELING); + put(Status.DELETED, TASK_CANCELING); + put(Status.NONE, TASK_CANCELING); + }}; + + private Fetch fetch; + private Map idToRequestId = new HashMap<>(); + @SuppressLint("UseSparseArrays") + private Map requestIdToConfig = new HashMap<>(); + private DeviceEventManagerModule.RCTDeviceEventEmitter ee; + private Date lastProgressReport = new Date(); + private WritableArray progressReports = Arguments.createArray(); + + public RNFBGDModule(ReactApplicationContext reactContext) { + super(reactContext); + + loadConfigMap(); + fetch = new Fetch.Builder(this.getReactApplicationContext(), "RNBackgroundDownload") + .setDownloadConcurrentLimit(4) + .build(); + fetch.addListener(this); + } + + @Override + public void onCatalystInstanceDestroy() { + fetch.close(); + } + + @Override + public String getName() { + return "RNBackgroundDownload"; + } + + @Override + public void initialize() { + ee = getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + } + + @Override + public boolean hasConstants() { + return true; + } + + @Nullable + @Override + public Map getConstants() { + Map constants = new HashMap<>(); + constants.put("documents", this.getReactApplicationContext().getFilesDir().getAbsolutePath()); + constants.put("TaskRunning", TASK_RUNNING); + constants.put("TaskSuspended", TASK_SUSPENDED); + constants.put("TaskCanceling", TASK_CANCELING); + constants.put("TaskCompleted", TASK_COMPLETED); + constants.put("PriorityHigh", Priority.HIGH.getValue()); + constants.put("PriorityNormal", Priority.NORMAL.getValue()); + constants.put("PriorityLow", Priority.LOW.getValue()); + constants.put("OnlyWifi", NetworkType.WIFI_ONLY.getValue()); + constants.put("AllNetworks", NetworkType.ALL.getValue()); + return constants; + } + + private void removeFromMaps(int requestId) { + TaskConfig config = requestIdToConfig.get(requestId); + if (config != null) { + idToRequestId.remove(config.id); + requestIdToConfig.remove(requestId); + + saveConfigMap(); + } + } + + private void saveConfigMap() { + File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); + try { + ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file)); + outputStream.writeObject(requestIdToConfig); + outputStream.flush(); + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void loadConfigMap() { + File file = new File(this.getReactApplicationContext().getFilesDir(), "RNFileBackgroundDownload_configMap"); + try { + ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file)); + requestIdToConfig = (Map) inputStream.readObject(); + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + } + + // JS Methods + @ReactMethod + public void download(ReadableMap options) { + String id = options.getString("id"); + String url = options.getString("url"); + String destination = options.getString("destination"); + + if (id == null || url == null || destination == null) { + Log.e(getName(), "id, url and destination must be set"); + return; + } + + TaskConfig config = new TaskConfig(id); + Request request = new Request(url, destination); + 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, null, null); + + idToRequestId.put(id, request.getId()); + requestIdToConfig.put(request.getId(), config); + saveConfigMap(); + } + + @ReactMethod + public void puaseTask(String identifier) { + Integer requestId = idToRequestId.get(identifier); + if (requestId != null) { + fetch.pause(requestId); + } + } + + @ReactMethod + public void resumeTask(String identifier) { + Integer requestId = idToRequestId.get(identifier); + if (requestId != null) { + fetch.resume(requestId); + } + } + + @ReactMethod + public void stopTask(String identifier) { + Integer requestId = idToRequestId.get(identifier); + if (requestId != null) { + fetch.cancel(requestId); + } + } + + @ReactMethod + public void checkForExistingDownloads(final Promise promise) { + fetch.getDownloads(new Func>() { + @Override + public void call(List downloads) { + WritableArray foundIds = Arguments.createArray(); + + for (Download download : downloads) { + if (requestIdToConfig.containsKey(download.getId())) { + TaskConfig config = requestIdToConfig.get(download.getId()); + WritableMap params = Arguments.createMap(); + params.putString("id", config.id); + params.putInt("state", stateMap.get(download.getStatus())); + params.putInt("bytesWritten", (int)download.getDownloaded()); + params.putInt("totalBytes", (int)download.getTotal()); + params.putDouble("percent", ((double)download.getProgress()) / 100); + + foundIds.pushMap(params); + + idToRequestId.put(config.id, download.getId()); + config.reportedBegin = true; + } else { + fetch.delete(download.getId()); + } + } + + promise.resolve(foundIds); + } + }); + } + + // Fetch API + @Override + public void onQueued(Download download) { + + } + + @Override + public void onCompleted(Download download) { + WritableMap params = Arguments.createMap(); + params.putString("id", requestIdToConfig.get(download.getId()).id); + ee.emit("downloadComplete", params); + + removeFromMaps(download.getId()); + fetch.remove(download.getId()); + } + + @Override + public void onError(Download download) { + Error error = download.getError(); + Throwable throwable = error.getThrowable(); + + Log.e(getName(), error.toString()); + WritableMap params = Arguments.createMap(); + params.putString("id", requestIdToConfig.get(download.getId()).id); + if (error == Error.UNKNOWN && throwable != null) { + params.putString("error", throwable.getLocalizedMessage()); + } else { + params.putString("error", error.toString()); + } + ee.emit("downloadFailed", params); + + removeFromMaps(download.getId()); + fetch.remove(download.getId()); + } + + @Override + public void onProgress(Download download, long l, long l1) { + TaskConfig config = requestIdToConfig.get(download.getId()); + WritableMap params = Arguments.createMap(); + params.putString("id", config.id); + + if (!config.reportedBegin) { + params.putInt("expctedBytes", (int)download.getTotal()); + ee.emit("downloadBegin", params); + config.reportedBegin = true; + } else { + params.putInt("written", (int)download.getDownloaded()); + params.putInt("total", (int)download.getTotal()); + params.putDouble("percent", ((double)download.getProgress()) / 100); + progressReports.pushMap(params); + Date now = new Date(); + if (now.getTime() - lastProgressReport.getTime() > 1500) { + ee.emit("downloadProgress", progressReports); + lastProgressReport = now; + progressReports = Arguments.createArray(); + } + } + + } + + @Override + public void onPaused(Download download) { + + } + + @Override + public void onResumed(Download download) { + + } + + @Override + public void onCancelled(Download download) { + removeFromMaps(download.getId()); + fetch.delete(download.getId()); + } + + @Override + public void onRemoved(Download download) { + + } + + @Override + public void onDeleted(Download download) { + + } +} \ No newline at end of file diff --git a/android/src/main/java/com/eko/RNBackgroundDownloadPackage.java b/android/src/main/java/com/eko/RNBackgroundDownloadPackage.java new file mode 100644 index 0000000..b2a02e9 --- /dev/null +++ b/android/src/main/java/com/eko/RNBackgroundDownloadPackage.java @@ -0,0 +1,28 @@ + +package com.eko; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.bridge.JavaScriptModule; +public class RNBackgroundDownloadPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new RNBackgroundDownloadModule(reactContext)); + } + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..ffa6abc --- /dev/null +++ b/index.js @@ -0,0 +1,90 @@ +import { NativeModules, NativeEventEmitter } from 'react-native'; +const { RNBackgroundDownload } = NativeModules; +const RNBackgroundDownloadEmitter = new NativeEventEmitter(RNBackgroundDownload); +import DownloadTask from './lib/downloadTask'; + +const tasksMap = new Map(); + +RNBackgroundDownloadEmitter.addListener('downloadProgress', events => { + for (let event of events) { + let task = tasksMap.get(event.id); + task && task._onProgress(event.percent, event.written, event.total); + } +}); + +RNBackgroundDownloadEmitter.addListener('downloadComplete', event => { + let task = tasksMap.get(event.id); + task && task._onDone(event.location); + tasksMap.delete(event.id); +}); + +RNBackgroundDownloadEmitter.addListener('downloadFailed', event => { + let task = tasksMap.get(event.id); + task && task._onError(event.error); + tasksMap.delete(event.id); +}); + +RNBackgroundDownloadEmitter.addListener('downloadBegin', event => { + console.log('GOT downloadBegin', event); + let task = tasksMap.get(event.id); + task && task._onBegin(event.expctedBytes); +}); + +export function checkForExistingDownloads() { + return RNBackgroundDownload.checkForExistingDownloads() + .then(foundTasks => { + console.log('Fond lost downloads!!: ', foundTasks); + return foundTasks.map(taskInfo => { + let task = new DownloadTask(taskInfo); + if (taskInfo.state === RNBackgroundDownload.TaskRunning) { + task.state = 'DOWNLOADING'; + } else if (taskInfo.state === RNBackgroundDownload.TaskSuspended) { + task.state = 'PAUSED'; + } else if (taskInfo.state === RNBackgroundDownload.TaskCanceling) { + task.stop(); + return null; + } else if (taskInfo.state === RNBackgroundDownload.TaskCompleted) { + if (taskInfo.bytesWritten === taskInfo.totalBytes) { + task.state = 'DONE'; + } else { + return null; + } + } + tasksMap.set(taskInfo.id, task); + return task; + }).filter(task => task !== null); + }); +} + +export function download(options) { + if (!options.id || !options.url || !options.destination) { + throw new Error('[RNBackgroundDownload] id, url and destination are required'); + } + RNBackgroundDownload.download(options); + let task = new DownloadTask(options.id); + tasksMap.set(options.id, task); + return task; +} + +export const directories = { + documents: RNBackgroundDownload.documents +}; + +export const Network = { + WIFI_ONLY: RNBackgroundDownload.OnlyWifi, + ALL: RNBackgroundDownload.AllNetworks +}; + +export const Priority = { + HIGH: RNBackgroundDownload.PriorityHigh, + MEDIUM: RNBackgroundDownload.PriorityNormal, + LOW: RNBackgroundDownload.PriorityLow +}; + +export default { + download, + checkForExistingDownloads, + directories, + Network, + Priority +}; \ No newline at end of file diff --git a/ios/RNBackgroundDownload.h b/ios/RNBackgroundDownload.h new file mode 100644 index 0000000..0e39f78 --- /dev/null +++ b/ios/RNBackgroundDownload.h @@ -0,0 +1,24 @@ +// +// RNFileBackgroundDownload.h +// EkoApp +// +// Created by Elad Gil on 20/11/2017. +// Copyright © 2017 Eko. All rights reserved. +// +// +#import +#if __has_include() +#import +#import +#elif __has_include("RCTBridgeModule.h") +#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" +#endif + +typedef void (^CompletionHandler)(); + +@interface RNFileBackgroundDownload : RCTEventEmitter + ++ (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler; + +@end diff --git a/ios/RNBackgroundDownload.m b/ios/RNBackgroundDownload.m new file mode 100644 index 0000000..1f0e470 --- /dev/null +++ b/ios/RNBackgroundDownload.m @@ -0,0 +1,271 @@ +// +// RNFileBackgroundDownload.m +// EkoApp +// +// Created by Elad Gil on 20/11/2017. +// Copyright © 2017 Eko. All rights reserved. +// +// +#import "RNBackgroundDownload.h" +#import "TaskConfig.h" + +#define URL_TO_CONFIG_MAP_KEY @"com.eko.bgdownloadmap" + +static CompletionHandler storedCompletionHandler; + +@implementation RNBackgroundDownload { + NSURLSession *urlSession; + NSURLSessionConfiguration *sessionConfig; + NSMutableDictionary *urlToConfigMap; + NSMutableDictionary *taskToConfigMap; + NSMutableDictionary *idToTaskMap; + NSMutableDictionary *idToResumeDataMap; + NSMutableDictionary *idToPercentMap; + NSOperationQueue *downloadOperationsQueue; + NSDate *lastProgressReport; + NSMutableArray *progressReports; +} + +RCT_EXPORT_MODULE(); + +- (dispatch_queue_t)methodQueue +{ + return dispatch_queue_create("com.eko.backgrounddownload", DISPATCH_QUEUE_SERIAL); +} + +- (BOOL)requiresMainQueueSetup { + return YES; +} + +- (NSArray *)supportedEvents { + return @[@"downloadComplete", @"downloadProgress", @"downloadFailed", @"downloadBegin"]; +} + +- (NSDictionary *)constantsToExport { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + + return @{ + @"documents": [paths firstObject], + @"TaskRunning": @(NSURLSessionTaskStateRunning), + @"TaskSuspended": @(NSURLSessionTaskStateSuspended), + @"TaskCanceling": @(NSURLSessionTaskStateCanceling), + @"TaskCompleted": @(NSURLSessionTaskStateCompleted) + }; +} + +- (id) init { + self = [super init]; + if (self) { + urlToConfigMap = [self deserialize:[[NSUserDefaults standardUserDefaults] objectForKey:URL_TO_CONFIG_MAP_KEY]]; + if (urlToConfigMap == nil) { + urlToConfigMap = [[NSMutableDictionary alloc] init]; + } + taskToConfigMap = [[NSMutableDictionary alloc] init]; + idToTaskMap = [[NSMutableDictionary alloc] init]; + idToResumeDataMap= [[NSMutableDictionary alloc] init]; + idToPercentMap = [[NSMutableDictionary alloc] init]; + NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; + sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessonIdentifier]; + downloadOperationsQueue = [[NSOperationQueue alloc] init]; + progressReports = [[NSMutableArray alloc] init]; + lastProgressReport = [[NSDate alloc] init]; + } + return self; +} + +- (void)lazyInitSession { + if (urlSession == nil) { + urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:downloadOperationsQueue]; + } +} + +- (void)removeTaskFromMap: (NSURLSessionTask *)task { + TaskConfig *taskConfig = taskToConfigMap[task]; + [taskToConfigMap removeObjectForKey:task]; + [urlToConfigMap removeObjectForKey:task.currentRequest.URL.absoluteString]; + [[NSUserDefaults standardUserDefaults] setObject:[self serialize: urlToConfigMap] forKey:URL_TO_CONFIG_MAP_KEY]; + if (taskConfig) { + [idToTaskMap removeObjectForKey:taskConfig.id]; + [idToPercentMap removeObjectForKey:taskConfig.id]; + } + if (taskToConfigMap.count == 0) { + [urlSession invalidateAndCancel]; + urlSession = nil; + } +} + ++ (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler { + NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + NSString *sessonIdentifier = [bundleIdentifier stringByAppendingString:@".backgrounddownloadtask"]; + if ([sessonIdentifier isEqualToString:identifier]) { + storedCompletionHandler = completionHandler; + } +} + + +#pragma mark - JS exported methods +RCT_EXPORT_METHOD(download: (NSDictionary *) options) { + NSString *identifier = options[@"id"]; + NSString *url = options[@"url"]; + NSString *destination = options[@"destination"]; + if (identifier == nil || url == nil || destination == nil) { + NSLog(@"[RNFileBackgroundDownload] [Error] id, url and destination must be set"); + return; + } + + [self lazyInitSession]; + NSURLSessionDownloadTask *task = [urlSession downloadTaskWithURL:[NSURL URLWithString:url]]; + TaskConfig *taskConfig = [[TaskConfig alloc] initWithDictionary: @{@"id": identifier, @"destination": destination}]; + taskToConfigMap[task] = taskConfig; + idToTaskMap[identifier] = task; + idToPercentMap[identifier] = @0.0; + + [task resume]; +} + +RCT_EXPORT_METHOD(puaseTask: (NSString *)identifier) { + NSURLSessionDownloadTask *task = idToTaskMap[identifier]; + if (task != nil && task.state == NSURLSessionTaskStateRunning) { + [task suspend]; + } +} + +RCT_EXPORT_METHOD(resumeTask: (NSString *)identifier) { + NSURLSessionDownloadTask *task = idToTaskMap[identifier]; + if (task != nil && task.state == NSURLSessionTaskStateSuspended) { + [task resume]; + } +} + +RCT_EXPORT_METHOD(stopTask: (NSString *)identifier) { + NSURLSessionDownloadTask *task = idToTaskMap[identifier]; + if (task != nil) { + [task cancel]; + [self removeTaskFromMap:task]; + } +} + +RCT_EXPORT_METHOD(checkForExistingDownloads: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self lazyInitSession]; + [urlSession getTasksWithCompletionHandler:^(NSArray * _Nonnull dataTasks, NSArray * _Nonnull uploadTasks, NSArray * _Nonnull downloadTasks) { + NSMutableArray *idsFound = [[NSMutableArray alloc] init]; + for (NSURLSessionDownloadTask *foundTask in downloadTasks) { + NSURLSessionDownloadTask __strong *task = foundTask; + NSLog(@"Found task with url: %@", task.currentRequest.URL.absoluteString); + TaskConfig *taskConfig = urlToConfigMap[task.currentRequest.URL.absoluteString]; + if (taskConfig) { + if (task.state == NSURLSessionTaskStateCompleted && task.countOfBytesReceived < task.countOfBytesExpectedToReceive) { + if (task.error && task.error.code == -999 && task.error.userInfo[NSURLSessionDownloadTaskResumeData] != nil) { + task = [urlSession downloadTaskWithResumeData:task.error.userInfo[NSURLSessionDownloadTaskResumeData]]; + } else { + task = [urlSession downloadTaskWithURL:foundTask.currentRequest.URL]; + } + [task resume]; + } + NSNumber *percent = foundTask.countOfBytesExpectedToReceive > 0 ? [NSNumber numberWithFloat:(float)task.countOfBytesReceived/(float)foundTask.countOfBytesExpectedToReceive] : @0.0; + [idsFound addObject:@{ + @"id": taskConfig.id, + @"state": [NSNumber numberWithInt: task.state], + @"bytesWritten": [NSNumber numberWithLongLong:task.countOfBytesReceived], + @"totalBytes": [NSNumber numberWithLongLong:foundTask.countOfBytesExpectedToReceive], + @"percent": percent + }]; + taskConfig.reportedBegin = YES; + taskToConfigMap[task] = taskConfig; + idToTaskMap[taskConfig.id] = task; + idToPercentMap[taskConfig.id] = percent; + } else { + [task cancel]; + } + } + resolve(idsFound); + }]; +} + +#pragma mark - NSURLSessionDownloadDelegate methods +- (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location { + TaskConfig *taskCofig = taskToConfigMap[downloadTask]; + if (taskCofig != nil) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *destURL = [NSURL fileURLWithPath:taskCofig.destination]; + [fileManager createDirectoryAtURL:[destURL URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; + [fileManager removeItemAtURL:destURL error:nil]; + NSError *moveError; + BOOL moved = [fileManager moveItemAtURL:location toURL:destURL error:&moveError]; + if (self.bridge) { + if (moved) { + [self sendEventWithName:@"downloadComplete" body:@{@"id": taskCofig.id}]; + } else { + [self sendEventWithName:@"downloadFailed" body:@{@"id": taskCofig.id, @"error": [moveError localizedDescription]}]; + } + } + [self removeTaskFromMap:downloadTask]; + } +} + +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { +} + +- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { + TaskConfig *taskCofig = taskToConfigMap[downloadTask]; + if (taskCofig != nil) { + if (!taskCofig.reportedBegin) { + [self sendEventWithName:@"downloadBegin" body:@{@"id": taskCofig.id, @"expctedBytes": [NSNumber numberWithLongLong: totalBytesExpectedToWrite]}]; + urlToConfigMap[downloadTask.currentRequest.URL.absoluteString] = taskCofig; + [[NSUserDefaults standardUserDefaults] setObject:[self serialize: urlToConfigMap] forKey:URL_TO_CONFIG_MAP_KEY]; + taskCofig.reportedBegin = YES; + } + + NSNumber *prevPercent = idToPercentMap[taskCofig.id]; + NSNumber *percent = [NSNumber numberWithFloat:(float)totalBytesWritten/(float)totalBytesExpectedToWrite]; + if ([percent floatValue] - [prevPercent floatValue] > 0.01f) { + [progressReports addObject:@{@"id": taskCofig.id, @"written": [NSNumber numberWithLongLong: totalBytesWritten], @"total": [NSNumber numberWithLongLong: totalBytesExpectedToWrite], @"percent": percent}]; + idToPercentMap[taskCofig.id] = percent; + } + + NSDate *now = [[NSDate alloc] init]; + if ([now timeIntervalSinceDate:lastProgressReport] > 1.5 && progressReports.count > 0) { + if (self.bridge) { + [self sendEventWithName:@"downloadProgress" body:progressReports]; + } + lastProgressReport = now; + [progressReports removeAllObjects]; + } + } +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { + TaskConfig *taskCofig = taskToConfigMap[task]; + if (error != nil && error.code != -999 && taskCofig != nil) { + if (self.bridge) { + [self sendEventWithName:@"downloadFailed" body:@{@"id": taskCofig.id, @"error": [error localizedDescription]}]; + } + [self removeTaskFromMap:task]; + } +} + +- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { + NSLog(@"in URLSessionDidFinishEventsForBackgroundURLSession"); + if (storedCompletionHandler) { + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + storedCompletionHandler(); + storedCompletionHandler = nil; + }]; + } +} + +#pragma mark - serialization +- (NSData *)serialize: (id)obj { + return [NSKeyedArchiver archivedDataWithRootObject:obj]; +} + +- (id)deserialize: (NSData *)data { + if (data == nil) { + return nil; + } + + return [NSKeyedUnarchiver unarchiveObjectWithData:data]; +} + +@end diff --git a/ios/RNBackgroundDownload.podspec b/ios/RNBackgroundDownload.podspec new file mode 100644 index 0000000..2a206b5 --- /dev/null +++ b/ios/RNBackgroundDownload.podspec @@ -0,0 +1,24 @@ + +Pod::Spec.new do |s| + s.name = "RNBackgroundDownload" + s.version = "1.0.0" + s.summary = "RNBackgroundDownload" + s.description = <<-DESC + RNBackgroundDownload + DESC + s.homepage = "" + s.license = "MIT" + # s.license = { :type => "MIT", :file => "FILE_LICENSE" } + s.author = { "author" => "author@domain.cn" } + s.platform = :ios, "7.0" + s.source = { :git => "https://github.com/author/RNBackgroundDownload.git", :tag => "master" } + s.source_files = "RNBackgroundDownload/**/*.{h,m}" + s.requires_arc = true + + + s.dependency "React" + #s.dependency "others" + +end + + \ No newline at end of file diff --git a/ios/RNBackgroundDownload.xcodeproj/project.pbxproj b/ios/RNBackgroundDownload.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bb11963 --- /dev/null +++ b/ios/RNBackgroundDownload.xcodeproj/project.pbxproj @@ -0,0 +1,252 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + B3E7B58A1CC2AC0600A0062D /* RNBackgroundDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNBackgroundDownload.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libRNBackgroundDownload.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNBackgroundDownload.a; sourceTree = BUILT_PRODUCTS_DIR; }; + B3E7B5881CC2AC0600A0062D /* RNBackgroundDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNBackgroundDownload.h; sourceTree = ""; }; + B3E7B5891CC2AC0600A0062D /* RNBackgroundDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNBackgroundDownload.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRNBackgroundDownload.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + B3E7B5881CC2AC0600A0062D /* RNBackgroundDownload.h */, + B3E7B5891CC2AC0600A0062D /* RNBackgroundDownload.m */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RNBackgroundDownload */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNBackgroundDownload" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RNBackgroundDownload; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRNBackgroundDownload.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNBackgroundDownload" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RNBackgroundDownload */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3E7B58A1CC2AC0600A0062D /* RNBackgroundDownload.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RNBackgroundDownload; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + "$(SRCROOT)/../../react-native/React/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RNBackgroundDownload; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNBackgroundDownload" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNBackgroundDownload" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/lib/downloadTask.js b/lib/downloadTask.js new file mode 100644 index 0000000..5ee9cec --- /dev/null +++ b/lib/downloadTask.js @@ -0,0 +1,77 @@ +import { NativeModules } from 'react-native'; +const { RNBackgroundDownload } = NativeModules; + +export default class DownloadTask { + state = 'PENDING'; + percent = 0; + bytesWritten = 0; + totalBytes = 0; + + constructor(taskInfo) { + if (typeof taskInfo === 'string') { + this.id = taskInfo; + } else { + this.id = taskInfo.id; + this.percent = taskInfo.percent; + this.bytesWritten = taskInfo.bytesWritten; + this.totalBytes = taskInfo.totalBytes; + } + } + + begin(handler) { + this._beginHandler = handler; + return this; + } + + progress(handler) { + this._progressHandler = handler; + return this; + } + + done(handler) { + this._doneHandler = handler; + return this; + } + + error(handler) { + this._errorHandler = handler; + return this; + } + + _onBegin(expectedBytes) { + this.state = 'DOWNLOADING'; + this._beginHandler && this._beginHandler(expectedBytes); + } + + _onProgress(percent, bytesWritten, totalBytes) { + this.percent = percent; + this.bytesWritten = bytesWritten; + this.totalBytes = totalBytes; + this._progressHandler && this._progressHandler(percent, bytesWritten, totalBytes); + } + + _onDone() { + this.state = 'DONE'; + this._doneHandler && this._doneHandler(); + } + + _onError(error) { + this.state = 'FAILED'; + this._errorHandler && this._errorHandler(error); + } + + pause() { + this.state = 'PAUSED'; + RNBackgroundDownload.puaseTask(this.id); + } + + resume() { + this.state = 'DOWNLOADING'; + RNBackgroundDownload.resumeTask(this.id); + } + + stop() { + this.state = 'STOPPED'; + RNBackgroundDownload.stopTask(this.id); + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4233a13 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ + +{ + "name": "react-native-background-download", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "react-native" + ], + "author": "", + "license": "", + "peerDependencies": { + "react-native": "^0.41.2" + } +}