diff --git a/README.md b/README.md index 8475adb3..20ddec9c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # react-native-background-upload [![npm version](https://badge.fury.io/js/react-native-background-upload.svg)](https://badge.fury.io/js/react-native-background-upload) ![GitHub Actions status](https://github.com/Vydia/react-native-background-upload/workflows/Test%20iOS%20Example%20App/badge.svg) ![GitHub Actions status](https://github.com/Vydia/react-native-background-upload/workflows/Test%20Android%20Example%20App/badge.svg) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) -The only React Native http post file uploader with android and iOS background support. If you are uploading large files like videos, use this so your users can background your app during a long upload. +The only React Native http post file uploader with android and iOS background support. If you are uploading large files like videos, use this so your users can background your app during a long upload. -NOTE: Use major version 4 with RN 47.0 and greater. If you have RN less than 47, use 3.0. To view all available versions: +NOTE: Use major version 4 with RN 47.0 and greater. If you have RN less than 47, use 3.0. To view all available versions: `npm show react-native-background-upload versions` - # Installation ## 1. Install package @@ -29,6 +28,7 @@ Note: if you are installing on React Native < 0.47, use `react-native-background ##### Android ###### ProGuard + Add this to your ProGuard configuration `(proguard-rules.pro)` : `-keep class net.gotev.uploadservice.** { *; }` @@ -46,40 +46,41 @@ Add this to your ProGuard configuration `(proguard-rules.pro)` : 3. Add `VydiaRNFileUploader.a` to `Build Phases -> Link Binary With Libraries` #### Android + 1. Add the following lines to `android/settings.gradle`: - ```gradle - include ':react-native-background-upload' - project(':react-native-background-upload').projectDir = new File(settingsDir, '../node_modules/react-native-background-upload/android') - ``` + ```gradle + include ':react-native-background-upload' + project(':react-native-background-upload').projectDir = new File(settingsDir, '../node_modules/react-native-background-upload/android') + ``` + 2. Add the compile and resolutionStrategy line to the dependencies in `android/app/build.gradle`: - ```gradle - configurations.all { resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.4.1' } // required by react-native-background-upload until React Native supports okhttp >= okhttp 3.5 + ```gradle + configurations.all { resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.4.1' } // required by react-native-background-upload until React Native supports okhttp >= okhttp 3.5 - dependencies { - compile project(':react-native-background-upload') - } - ``` + dependencies { + compile project(':react-native-background-upload') + } + ``` +3) Add the import and link the package in `MainApplication.java`: -3. Add the import and link the package in `MainApplication.java`: + ```java + import com.sitemate.UploaderReactPackage; <-- add this import - ```java - import com.vydia.RNUploader.UploaderReactPackage; <-- add this import - - public class MainApplication extends Application implements ReactApplication { - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new UploaderReactPackage() // <-- add this line - ); - } - } - ``` + public class MainApplication extends Application implements ReactApplication { + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new UploaderReactPackage() // <-- add this line + ); + } + } + ``` -4. Ensure Android SDK versions. Open your app's `android/app/build.gradle` file. Ensure `compileSdkVersion` and `targetSdkVersion` are 25. Otherwise you'll get compilation errors. +4) Ensure Android SDK versions. Open your app's `android/app/build.gradle` file. Ensure `compileSdkVersion` and `targetSdkVersion` are 25. Otherwise you'll get compilation errors. ## 3. Expo @@ -88,7 +89,7 @@ To use this library with [Expo](https://expo.io) one must first detach (eject) t # Usage ```js -import Upload from 'react-native-background-upload' +import Upload from 'react-native-background-upload'; const options = { url: 'https://myservice.com/path/to/post', @@ -98,38 +99,40 @@ const options = { maxRetries: 2, // set retry count (Android only). Default 2 headers: { 'content-type': 'application/octet-stream', // Customize content-type - 'my-custom-header': 's3headervalueorwhateveryouneed' + 'my-custom-header': 's3headervalueorwhateveryouneed', }, // Below are options only supported on Android notification: { - enabled: true + enabled: true, }, - useUtf8Charset: true -} - -Upload.startUpload(options).then((uploadId) => { - console.log('Upload started') - Upload.addListener('progress', uploadId, (data) => { - console.log(`Progress: ${data.progress}%`) - }) - Upload.addListener('error', uploadId, (data) => { - console.log(`Error: ${data.error}%`) + useUtf8Charset: true, +}; + +Upload.startUpload(options) + .then(uploadId => { + console.log('Upload started'); + Upload.addListener('progress', uploadId, data => { + console.log(`Progress: ${data.progress}%`); + }); + Upload.addListener('error', uploadId, data => { + console.log(`Error: ${data.error}%`); + }); + Upload.addListener('cancelled', uploadId, data => { + console.log(`Cancelled!`); + }); + Upload.addListener('completed', uploadId, data => { + // data includes responseCode: number and responseBody: Object + console.log('Completed!'); + }); }) - Upload.addListener('cancelled', uploadId, (data) => { - console.log(`Cancelled!`) - }) - Upload.addListener('completed', uploadId, (data) => { - // data includes responseCode: number and responseBody: Object - console.log('Completed!') - }) -}).catch((err) => { - console.log('Upload error!', err) -}) + .catch(err => { + console.log('Upload error!', err); + }); ``` ## Multipart Uploads -Just set the `type` option to `multipart` and set the `field` option. Example: +Just set the `type` option to `multipart` and set the `field` option. Example: ``` const options = { @@ -153,58 +156,58 @@ All top-level methods are available as named exports or methods on the default e The primary method you will use, this starts the upload process. -Returns a promise with the string ID of the upload. Will reject if there is a connection problem, the file doesn't exist, or there is some other problem. +Returns a promise with the string ID of the upload. Will reject if there is a connection problem, the file doesn't exist, or there is some other problem. `options` is an object with following values: -*Note: You must provide valid URIs. react-native-background-upload does not escape the values you provide.* - -|Name|Type|Required|Default|Description|Example| -|---|---|---|---|---|---| -|`url`|string|Required||URL to upload to|`https://myservice.com/path/to/post`| -|`path`|string|Required||File path on device|`file://something/coming/from%20the%20device.png`| -|`type`|'raw' or 'multipart'|Optional|`raw`|Primary upload type.|| -|`method`|string|Optional|`POST`|HTTP method|| -|`customUploadId`|string|Optional||`startUpload` returns a Promise that includes the upload ID, which can be used for future status checks. By default, the upload ID is automatically generated. This parameter allows a custom ID to use instead of the default.|| -|`headers`|object|Optional||HTTP headers|`{ 'Accept': 'application/json' }`| -|`field`|string|Required if `type: 'multipart'`||The form field name for the file. Only used when `type: 'multipart`|`uploaded-file`| -|`parameters`|object|Optional||Additional form fields to include in the HTTP request. Only used when `type: 'multipart`|| -|`notification`|Notification object (see below)|Optional||Android only. |`{ enabled: true, onProgressTitle: "Uploading...", autoClear: true }`| -|`useUtf8Charset`|boolean|Optional||Android only. Set to true to use `utf-8` as charset. || -|`appGroup`|string|Optional|iOS only. App group ID needed for share extensions to be able to properly call the library. See: https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati +_Note: You must provide valid URIs. react-native-background-upload does not escape the values you provide._ + +| Name | Type | Required | Default | Description | Example | +| ---------------- | ------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| `url` | string | Required | | URL to upload to | `https://myservice.com/path/to/post` | +| `path` | string | Required | | File path on device | `file://something/coming/from%20the%20device.png` | +| `type` | 'raw' or 'multipart' | Optional | `raw` | Primary upload type. | | +| `method` | string | Optional | `POST` | HTTP method | | +| `customUploadId` | string | Optional | | `startUpload` returns a Promise that includes the upload ID, which can be used for future status checks. By default, the upload ID is automatically generated. This parameter allows a custom ID to use instead of the default. | | +| `headers` | object | Optional | | HTTP headers | `{ 'Accept': 'application/json' }` | +| `field` | string | Required if `type: 'multipart'` | | The form field name for the file. Only used when `type: 'multipart` | `uploaded-file` | +| `parameters` | object | Optional | | Additional form fields to include in the HTTP request. Only used when `type: 'multipart` | | +| `notification` | Notification object (see below) | Optional | | Android only. | `{ enabled: true, onProgressTitle: "Uploading...", autoClear: true }` | +| `useUtf8Charset` | boolean | Optional | | Android only. Set to true to use `utf-8` as charset. | | +| `appGroup` | string | Optional | iOS only. App group ID needed for share extensions to be able to properly call the library. See: https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati | ### Notification Object (Android Only) -|Name|Type|Required|Description|Example| -|---|---|---|---|---| -|`enabled`|boolean|Optional|Enable or diasable notifications. Works only on Android version < 8.0 Oreo. On Android versions >= 8.0 Oreo is required by Google's policy to display a notification when a background service run|`{ enabled: true }`| -|`autoClear`|boolean|Optional|Autoclear notification on complete|`{ autoclear: true }`| -|`notificationChannel`|string|Optional|Sets android notificaion channel|`{ notificationChannel: "My-Upload-Service" }`| -|`enableRingTone`|boolean|Optional|Sets whether or not to enable the notification sound when the upload gets completed with success or error|`{ enableRingTone: true }`| -|`onProgressTitle`|string|Optional|Sets notification progress title|`{ onProgressTitle: "Uploading" }`| -|`onProgressMessage`|string|Optional|Sets notification progress message|`{ onProgressMessage: "Uploading new video" }`| -|`onCompleteTitle`|string|Optional|Sets notification complete title|`{ onCompleteTitle: "Upload finished" }`| -|`onCompleteMessage`|string|Optional|Sets notification complete message|`{ onCompleteMessage: "Your video has been uploaded" }`| -|`onErrorTitle`|string|Optional|Sets notification error title|`{ onErrorTitle: "Upload error" }`| -|`onErrorMessage`|string|Optional|Sets notification error message|`{ onErrorMessage: "An error occured while uploading a video" }`| -|`onCancelledTitle`|string|Optional|Sets notification cancelled title|`{ onCancelledTitle: "Upload cancelled" }`| -|`onCancelledMessage`|string|Optional|Sets notification cancelled message|`{ onCancelledMessage: "Video upload was cancelled" }`| +| Name | Type | Required | Description | Example | +| --------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `enabled` | boolean | Optional | Enable or diasable notifications. Works only on Android version < 8.0 Oreo. On Android versions >= 8.0 Oreo is required by Google's policy to display a notification when a background service run | `{ enabled: true }` | +| `autoClear` | boolean | Optional | Autoclear notification on complete | `{ autoclear: true }` | +| `notificationChannel` | string | Optional | Sets android notificaion channel | `{ notificationChannel: "My-Upload-Service" }` | +| `enableRingTone` | boolean | Optional | Sets whether or not to enable the notification sound when the upload gets completed with success or error | `{ enableRingTone: true }` | +| `onProgressTitle` | string | Optional | Sets notification progress title | `{ onProgressTitle: "Uploading" }` | +| `onProgressMessage` | string | Optional | Sets notification progress message | `{ onProgressMessage: "Uploading new video" }` | +| `onCompleteTitle` | string | Optional | Sets notification complete title | `{ onCompleteTitle: "Upload finished" }` | +| `onCompleteMessage` | string | Optional | Sets notification complete message | `{ onCompleteMessage: "Your video has been uploaded" }` | +| `onErrorTitle` | string | Optional | Sets notification error title | `{ onErrorTitle: "Upload error" }` | +| `onErrorMessage` | string | Optional | Sets notification error message | `{ onErrorMessage: "An error occured while uploading a video" }` | +| `onCancelledTitle` | string | Optional | Sets notification cancelled title | `{ onCancelledTitle: "Upload cancelled" }` | +| `onCancelledMessage` | string | Optional | Sets notification cancelled message | `{ onCancelledMessage: "Video upload was cancelled" }` | ### getFileInfo(path) -Returns some useful information about the file in question. Useful if you want to set a MIME type header. +Returns some useful information about the file in question. Useful if you want to set a MIME type header. `path` is a string, such as `file://path.to.the.file.png` Returns a Promise that resolves to an object containing: -|Name|Type|Required|Description|Example| -|---|---|---|---|---| -|`name`|string|Required|The file name within its directory.|`image2.png`| -|`exists`|boolean|Required|Is there a file matching this path?|| -|`size`|number|If `exists`|File size, in bytes|| -|`extension`|string|If `exists`|File extension|`mov`| -|`mimeType`|string|If `exists`|The MIME type for the file.|`video/mp4`| +| Name | Type | Required | Description | Example | +| ----------- | ------- | ----------- | ----------------------------------- | ------------ | +| `name` | string | Required | The file name within its directory. | `image2.png` | +| `exists` | boolean | Required | Is there a file matching this path? | | +| `size` | number | If `exists` | File size, in bytes | | +| `extension` | string | If `exists` | File extension | `mov` | +| `mimeType` | string | If `exists` | The MIME type for the file. | `video/mp4` | ### cancelUpload(uploadId) @@ -220,7 +223,7 @@ Adds an event listener, possibly confined to a single upload. `eventType` Event to listen for. Values: 'progress' | 'error' | 'completed' | 'cancelled' -`uploadId` The upload ID from `startUpload` to filter events for. If null, this will include all uploads. +`uploadId` The upload ID from `startUpload` to filter events for. If null, this will include all uploads. `listener` Function to call when the event occurs. @@ -232,41 +235,42 @@ Returns an [EventSubscription](https://github.com/facebook/react-native/blob/mas Event Data -|Name|Type|Required|Description| -|---|---|---|---| -|`id`|string|Required|The ID of the upload.| -|`progress`|0-100|Required|Percentage completed.| +| Name | Type | Required | Description | +| ---------- | ------ | -------- | --------------------- | +| `id` | string | Required | The ID of the upload. | +| `progress` | 0-100 | Required | Percentage completed. | ### error Event Data -|Name|Type|Required|Description| -|---|---|---|---| -|`id`|string|Required|The ID of the upload.| -|`error`|string|Required|Error message.| +| Name | Type | Required | Description | +| ------- | ------ | -------- | --------------------- | +| `id` | string | Required | The ID of the upload. | +| `error` | string | Required | Error message. | ### completed Event Data -|Name|Type|Required|Description| -|---|---|---|---| -|`id`|string|Required|The ID of the upload.| -|`responseCode`|string|Required|HTTP status code received| -|`responseBody`|string|Required|HTTP response body| -|`responseHeaders`|string|Required|HTTP response headers (Android)| +| Name | Type | Required | Description | +| ----------------- | ------ | -------- | ------------------------------- | +| `id` | string | Required | The ID of the upload. | +| `responseCode` | string | Required | HTTP status code received | +| `responseBody` | string | Required | HTTP response body | +| `responseHeaders` | string | Required | HTTP response headers (Android) | ### cancelled Event Data -|Name|Type|Required|Description| -|---|---|---|---| -|`id`|string|Required|The ID of the upload.| +| Name | Type | Required | Description | +| ---- | ------ | -------- | --------------------- | +| `id` | string | Required | The ID of the upload. | # Customizing Android Build Properties -You may want to customize the `compileSdk, buildToolsVersion, and targetSdkVersion` versions used by this package. For that, add this to `android/build.gradle`: + +You may want to customize the `compileSdk, buildToolsVersion, and targetSdkVersion` versions used by this package. For that, add this to `android/build.gradle`: ``` ext { @@ -276,7 +280,8 @@ ext { } ``` -Add it above `allProjects` and you're good. Your `android/build.gradle` might then resemble: +Add it above `allProjects` and you're good. Your `android/build.gradle` might then resemble: + ``` buildscript { repositories { @@ -308,12 +313,11 @@ Does it support iOS camera roll assets? Does it support multiple file uploads? -> Yes and No. It supports multiple concurrent uploads, but only a single upload per request. That should be fine for 90%+ of cases. +> Yes and No. It supports multiple concurrent uploads, but only a single upload per request. That should be fine for 90%+ of cases. Why should I use this file uploader instead of others that I've Googled like [react-native-uploader](https://github.com/aroth/react-native-uploader)? -> This package has two killer features not found anywhere else (as of 12/16/2016). First, it works on both iOS and Android. Others are iOS only. Second, it supports background uploading. This means that users can background your app and the upload will continue. This does not happen with other uploaders. - +> This package has two killer features not found anywhere else (as of 12/16/2016). First, it works on both iOS and Android. Others are iOS only. Second, it supports background uploading. This means that users can background your app and the upload will continue. This does not happen with other uploaders. # Contributing @@ -322,19 +326,22 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md). # Common Issues ## BREAKING CHANGE IN 3.0 -This is for 3.0 only. This does NOT apply to 4.0, as recent React Native versions have upgraded the `okhttp` dependencies. Anyway... + +This is for 3.0 only. This does NOT apply to 4.0, as recent React Native versions have upgraded the `okhttp` dependencies. Anyway... In 3.0, you need to add + ```gradle configurations.all { resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.4.1' } ``` + to your app's app's `android/app/build.gradle` file. Just add it above (not within) `dependencies` and you'll be fine. - ## BREAKING CHANGE IN 2.0 -Two big things happened in version 2.0. First, the Android package name had to be changed, as it conflicted with our own internal app. My bad. Second, we updated the android upload service dependency to the latest, but that requires the app have a compileSdkVersion and targetSdkVersion or 25. + +Two big things happened in version 2.0. First, the Android package name had to be changed, as it conflicted with our own internal app. My bad. Second, we updated the android upload service dependency to the latest, but that requires the app have a compileSdkVersion and targetSdkVersion or 25. To upgrade: In `MainApplication.java`: @@ -342,13 +349,13 @@ In `MainApplication.java`: Change ```java - import com.vydia.UploaderReactPackage; + import com.sitemate.UploaderReactPackage; ``` to ```java - import com.vydia.RNUploader.UploaderReactPackage; + import com.sitemate.uploader.UploaderReactPackage; ``` Then open your app's `android/app/build.gradle` file. @@ -356,10 +363,10 @@ Ensure `compileSdkVersion` and `targetSdkVersion` are 25. Done! - ## Gratitude Thanks to: -- [android-upload-service](https://github.com/gotev/android-upload-service) It made Android dead simple to support. -- [MIME type from path on iOS](http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database) Thanks for the answer! +- [android-upload-service](https://github.com/gotev/android-upload-service) It made Android dead simple to support. + +- [MIME type from path on iOS](http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database) Thanks for the answer! diff --git a/android/build.gradle b/android/build.gradle index f3855a1a..74823105 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,9 +4,10 @@ buildscript { } ext { - ext.kotlinVersion = safeExtGet('kotlinVersion', '1.6.0') - buildToolsVersion = '29.0.2' - compileSdkVersion = 29 + agpVersion = '7.4.0' + ext.kotlinVersion = safeExtGet('kotlinVersion', '1.9.22') + buildToolsVersion = '34.0.0' + compileSdkVersion = 34 targetSdkVersion = 29 minSdkVersion = 18 } @@ -17,7 +18,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath "com.android.tools.build:gradle:$agpVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } @@ -25,9 +26,9 @@ buildscript { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -def DEFAULT_COMPILE_SDK_VERSION = 28 -def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" -def DEFAULT_TARGET_SDK_VERSION = 28 +def DEFAULT_COMPILE_SDK_VERSION = 31 +def DEFAULT_BUILD_TOOLS_VERSION = "30.0.2" +def DEFAULT_TARGET_SDK_VERSION = 29 def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback @@ -59,15 +60,26 @@ def _ext = ext def _kotlinVersion = _ext.has('detoxKotlinVersion') ? _ext.detoxKotlinVersion : '1.3.10' def _kotlinStdlib = _ext.has('detoxKotlinStdlib') ? _ext.detoxKotlinStdlib : 'kotlin-stdlib-jdk8' +def _workVersion = "2.9.0" +def _uploadServiceVersion = "4.9.2" dependencies { - implementation "androidx.core:core-ktx:1.0.1" + + implementation "androidx.work:work-runtime:$_workVersion" + implementation "androidx.work:work-runtime-ktx:$_workVersion" + + //noinspection GradleDynamicVersion + implementation "androidx.core:core-ktx:1.12.0" implementation 'com.facebook.react:react-native:+' implementation "org.jetbrains.kotlin:$_kotlinStdlib:$_kotlinVersion" - implementation 'net.gotev:uploadservice-okhttp:4.9.2' + //noinspection GradleDependency + implementation "net.gotev:uploadservice:$_uploadServiceVersion" + //noinspection GradleDependency + implementation "net.gotev:uploadservice-okhttp:$_uploadServiceVersion" - implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'com.google.code.gson:gson:2.10.1' } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 4d977ff9..ac228f0c 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="com.sitemate.uploader"> diff --git a/android/src/main/java/com/sitemate/extensions/ContextExtensions.kt b/android/src/main/java/com/sitemate/extensions/ContextExtensions.kt new file mode 100644 index 00000000..7683cee9 --- /dev/null +++ b/android/src/main/java/com/sitemate/extensions/ContextExtensions.kt @@ -0,0 +1,63 @@ +package com.sitemate.extensions + +import android.content.Context +import com.sitemate.work.TaskCompletionNotifier +import com.sitemate.work.UploadManager +import net.gotev.uploadservice.UploadTask +import net.gotev.uploadservice.data.UploadNotificationConfig +import net.gotev.uploadservice.data.UploadTaskParameters +import net.gotev.uploadservice.logger.UploadServiceLogger +import net.gotev.uploadservice.observer.task.BroadcastEmitter + +data class UploadTaskCreationParameters( + val params: UploadTaskParameters, + val notificationConfig: UploadNotificationConfig +) + +/** + * Creates a new task instance based on the requested task class in the intent. + * @param creationParameters task creation params + * @return task instance or null if the task class is not supported or invalid + */ +@Suppress("UNCHECKED_CAST") +fun Context.getUploadTask( + creationParameters: UploadTaskCreationParameters, + notificationId: Int +): UploadTask? { + return try { + val observers = arrayOf( + BroadcastEmitter(this), + TaskCompletionNotifier() + ) + + val taskClass = Class.forName(creationParameters.params.taskClass) as Class + val uploadTask = taskClass.newInstance().apply { + init( + context = this@getUploadTask, + taskParams = creationParameters.params, + notificationConfig = creationParameters.notificationConfig, + notificationId = notificationId, + taskObservers = observers + ) + } + + UploadServiceLogger.debug( + component = UploadManager.TAG, + uploadId = creationParameters.params.id, + message = { + "Successfully created new task with class: ${taskClass.name}" + } + ) + uploadTask + } catch (exc: Throwable) { + UploadServiceLogger.error( + component = UploadManager.TAG, + uploadId = "", + exception = exc, + message = { + "Error while instantiating new task" + } + ) + null + } +} diff --git a/android/src/main/java/com/sitemate/extensions/UploadExtensions.kt b/android/src/main/java/com/sitemate/extensions/UploadExtensions.kt new file mode 100644 index 00000000..33162c81 --- /dev/null +++ b/android/src/main/java/com/sitemate/extensions/UploadExtensions.kt @@ -0,0 +1,57 @@ +package com.sitemate.extensions + +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import com.google.gson.Gson +import net.gotev.uploadservice.data.UploadFile +import net.gotev.uploadservice.data.UploadNotificationConfig +import net.gotev.uploadservice.data.UploadTaskParameters +import net.gotev.uploadservice.extensions.setOrRemove +import net.gotev.uploadservice.persistence.PersistableData + +private const val PROPERTY_PARAM_NAME = "multipartParamName" +private const val PROPERTY_REMOTE_FILE_NAME = "multipartRemoteFileName" +private const val PROPERTY_CONTENT_TYPE = "multipartContentType" +const val PARAM_KEY_TASK_PARAMS = "PARAM_KEY_TASK_PARAMS" +const val PARAM_KEY_NOTIF_CONFIG = "PARAM_KEY_NOTIF_CONFIG" + +internal var UploadFile.parameterName: String? + get() = properties[PROPERTY_PARAM_NAME] + set(value) { + properties.setOrRemove(PROPERTY_PARAM_NAME, value) + } + +internal var UploadFile.remoteFileName: String? + get() = properties[PROPERTY_REMOTE_FILE_NAME] + set(value) { + properties.setOrRemove(PROPERTY_REMOTE_FILE_NAME, value) + } + +internal var UploadFile.contentType: String? + get() = properties[PROPERTY_CONTENT_TYPE] + set(value) { + properties.setOrRemove(PROPERTY_CONTENT_TYPE, value) + } + +internal fun UploadTaskParameters.toJson(): String = toPersistableData().toJson() + +internal fun String.toUploadTaskParameters(): UploadTaskParameters = UploadTaskParameters.createFromPersistableData(PersistableData.fromJson(this)) + +internal fun UploadNotificationConfig.toJson(): String = Gson().toJson(this) + +internal fun String.toUploadNotificationConfig() = Gson().fromJson(this, UploadNotificationConfig::class.java) + +internal fun OneTimeWorkRequest.Builder.setData(uploadTaskParameters: UploadTaskParameters, notificationConfig: UploadNotificationConfig) { + val data = Data.Builder() + data.putString(PARAM_KEY_TASK_PARAMS, uploadTaskParameters.toJson()) + data.putString(PARAM_KEY_NOTIF_CONFIG, notificationConfig.toJson()) + setInputData(data.build()) +} + +internal fun OneTimeWorkRequest.Builder.shouldLimitNetwork(limit: Boolean) { + val network = if (limit) NetworkType.UNMETERED else NetworkType.CONNECTED + val constraints = Constraints.Builder().setRequiredNetworkType(network).build() + setConstraints(constraints) +} diff --git a/android/src/main/java/com/vydia/RNUploader/GlobalRequestObserverDelegate.kt b/android/src/main/java/com/sitemate/uploader/GlobalRequestObserverDelegate.kt similarity index 98% rename from android/src/main/java/com/vydia/RNUploader/GlobalRequestObserverDelegate.kt rename to android/src/main/java/com/sitemate/uploader/GlobalRequestObserverDelegate.kt index cc16de19..7ba69df1 100644 --- a/android/src/main/java/com/vydia/RNUploader/GlobalRequestObserverDelegate.kt +++ b/android/src/main/java/com/sitemate/uploader/GlobalRequestObserverDelegate.kt @@ -1,4 +1,4 @@ -package com.vydia.RNUploader +package com.sitemate.uploader import android.content.Context import android.util.Log diff --git a/android/src/main/java/com/sitemate/uploader/ModifiedBinaryUploadRequest.kt b/android/src/main/java/com/sitemate/uploader/ModifiedBinaryUploadRequest.kt new file mode 100644 index 00000000..9c59950a --- /dev/null +++ b/android/src/main/java/com/sitemate/uploader/ModifiedBinaryUploadRequest.kt @@ -0,0 +1,30 @@ +package com.sitemate.uploader + +import android.content.Context +import net.gotev.uploadservice.UploadTask +import net.gotev.uploadservice.data.UploadFile +import net.gotev.uploadservice.protocols.binary.BinaryUploadRequest +import net.gotev.uploadservice.protocols.binary.BinaryUploadTask +import java.io.FileNotFoundException +import java.io.IOException + +class ModifiedBinaryUploadRequest(context: Context, serverUrl: String, limitNetwork: Boolean) : + ModifiedHttpUploadRequest(context, serverUrl, limitNetwork) { + + override val taskClass: Class + get() = BinaryUploadTask::class.java + + /** + * Sets the file used as raw body of the upload request. + * + * @param path path to the file that you want to upload + * @throws FileNotFoundException if the file to upload does not exist + * @return [BinaryUploadRequest] + */ + @Throws(IOException::class) + fun setFileToUpload(path: String): ModifiedBinaryUploadRequest { + files.clear() + files.add(UploadFile(path)) + return this + } +} diff --git a/android/src/main/java/com/sitemate/uploader/ModifiedHttpUploadRequest.kt b/android/src/main/java/com/sitemate/uploader/ModifiedHttpUploadRequest.kt new file mode 100644 index 00000000..656e2612 --- /dev/null +++ b/android/src/main/java/com/sitemate/uploader/ModifiedHttpUploadRequest.kt @@ -0,0 +1,57 @@ +package com.sitemate.uploader + +import android.content.Context +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import com.sitemate.extensions.setData +import com.sitemate.extensions.shouldLimitNetwork +import com.sitemate.work.UploadManager +import com.sitemate.work.UploadWorker +import net.gotev.uploadservice.HttpUploadRequest +import net.gotev.uploadservice.data.UploadTaskParameters +import java.util.* + +abstract class ModifiedHttpUploadRequest>(context: Context, serverUrl: String, private val limitNetwork: Boolean = false) : + HttpUploadRequest(context, serverUrl) { + + private var started: Boolean = false + private var uploadId = UUID.randomUUID().toString() + private val uploadTaskParameters: UploadTaskParameters + get() = UploadTaskParameters( + taskClass = taskClass.name, + id = uploadId, + serverUrl = serverUrl, + maxRetries = maxRetries, + autoDeleteSuccessfullyUploadedFiles = autoDeleteSuccessfullyUploadedFiles, + files = files, + additionalParameters = getAdditionalParameters() + ) + + override fun startUpload(): String { + require(files.isNotEmpty()) { "Set the file to be used in the request body first!" } + check(!started) { + "You have already called startUpload() on this Upload request instance once and you " + + "cannot call it multiple times. Check your code." + } + + check(!UploadManager.taskList.contains(uploadTaskParameters.id)) { + "You have tried to perform startUpload() using the same uploadID of an " + + "already running task. You're trying to use the same ID for multiple uploads." + } + + started = true + val workManager: WorkManager = WorkManager.getInstance(context) + val uploadRequest = OneTimeWorkRequest.Builder(UploadWorker::class.java) + uploadRequest.shouldLimitNetwork(limitNetwork) + uploadRequest.addTag("${UploadWorker::class.java.simpleName}-$uploadId") + uploadRequest.setData(uploadTaskParameters, notificationConfig(context, uploadId)) + workManager.enqueue(uploadRequest.build()) + + return uploadTaskParameters.id; + } + + fun setCustomUploadID(uploadID: String) { + this.uploadId = uploadID + setUploadID(uploadID) + } +} diff --git a/android/src/main/java/com/sitemate/uploader/ModifiedMultipartUploadRequest.kt b/android/src/main/java/com/sitemate/uploader/ModifiedMultipartUploadRequest.kt new file mode 100644 index 00000000..d1a2d329 --- /dev/null +++ b/android/src/main/java/com/sitemate/uploader/ModifiedMultipartUploadRequest.kt @@ -0,0 +1,60 @@ +package com.sitemate.uploader + +import android.content.Context +import com.sitemate.extensions.contentType +import com.sitemate.extensions.parameterName +import com.sitemate.extensions.remoteFileName +import net.gotev.uploadservice.UploadTask +import net.gotev.uploadservice.data.UploadFile +import net.gotev.uploadservice.protocols.multipart.MultipartUploadTask +import java.io.FileNotFoundException + +class ModifiedMultipartUploadRequest(context: Context, serverUrl: String, limitNetwork: Boolean) : + ModifiedHttpUploadRequest(context, serverUrl, limitNetwork) { + + override val taskClass: Class + get() = MultipartUploadTask::class.java + + /** + * Adds a file to this upload request. + * + * @param filePath path to the file that you want to upload + * @param parameterName Name of the form parameter that will contain file's data + * @param fileName File name seen by the server side script. If null, the original file name + * will be used + * @param contentType Content type of the file. If null or empty, the mime type will be + * automatically detected. If fore some reasons autodetection fails, + * `application/octet-stream` will be used by default + * @return [ModifiedMultipartUploadRequest] + */ + @Throws(FileNotFoundException::class) + @JvmOverloads + fun addFileToUpload( + filePath: String, + parameterName: String, + fileName: String? = null, + contentType: String? = null + ): ModifiedMultipartUploadRequest { + require(filePath.isNotBlank() && parameterName.isNotBlank()) { + "Please specify valid filePath and parameterName. They cannot be blank." + } + + files.add(UploadFile(filePath).apply { + this.parameterName = parameterName + + this.contentType = if (contentType.isNullOrBlank()) { + handler.contentType(context) + } else { + contentType + } + + remoteFileName = if (fileName.isNullOrBlank()) { + handler.name(context) + } else { + fileName + } + }) + + return this + } +} diff --git a/android/src/main/java/com/vydia/RNUploader/UploaderModule.kt b/android/src/main/java/com/sitemate/uploader/UploaderModule.kt similarity index 97% rename from android/src/main/java/com/vydia/RNUploader/UploaderModule.kt rename to android/src/main/java/com/sitemate/uploader/UploaderModule.kt index cf5af25a..0ee97ce3 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploaderModule.kt +++ b/android/src/main/java/com/sitemate/uploader/UploaderModule.kt @@ -1,4 +1,4 @@ -package com.vydia.RNUploader +package com.sitemate.uploader import android.app.Application import android.app.NotificationChannel @@ -7,6 +7,10 @@ import android.content.Context import android.os.Build import android.util.Log import android.webkit.MimeTypeMap +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.sitemate.work.UploadManager +import com.sitemate.work.UploadWorker import com.facebook.react.BuildConfig import com.facebook.react.bridge.* import net.gotev.uploadservice.UploadService @@ -37,7 +41,7 @@ class UploaderModule(val reactContext: ReactApplicationContext) : ReactContextBa Returns an object such as: {extension: "mp4", size: "3804316", exists: true, mimeType: "video/mp4", name: "20161116_074726.mp4"} */ @ReactMethod - fun getFileInfo(path: String, promise: Promise) { + fun getFileInfo(path: String?, promise: Promise) { try { val params = Arguments.createMap() val fileInfo = File(path) @@ -49,7 +53,7 @@ class UploaderModule(val reactContext: ReactApplicationContext) : ReactContextBa params.putString("size", fileInfo.length().toString()) //use string form of long because there is no putLong and converting to int results in a max size of 17.2 gb, which could happen. Javascript will need to convert it to a number val extension = MimeTypeMap.getFileExtensionFromUrl(path) params.putString("extension", extension) - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.lowercase()) + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()) params.putString("mimeType", mimeType) } promise.resolve(params) @@ -64,9 +68,9 @@ class UploaderModule(val reactContext: ReactApplicationContext) : ReactContextBa var followRedirects = true var followSslRedirects = true var retryOnConnectionFailure = true - var connectTimeout = 15 - var writeTimeout = 30 - var readTimeout = 30 + var connectTimeout = 45 + var writeTimeout = 90 + var readTimeout = 90 //TODO: make 'cache' customizable if (options.hasKey("followRedirects")) { if (options.getType("followRedirects") != ReadableType.Boolean) { diff --git a/android/src/main/java/com/sitemate/uploader/UploaderReactPackage.java b/android/src/main/java/com/sitemate/uploader/UploaderReactPackage.java new file mode 100644 index 00000000..faba730f --- /dev/null +++ b/android/src/main/java/com/sitemate/uploader/UploaderReactPackage.java @@ -0,0 +1,32 @@ +package com.sitemate.uploader; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class UploaderReactPackage implements ReactPackage { + + // Deprecated in RN 0.47, @todo remove after < 0.47 support remove + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new UploaderModule(reactContext)); + return modules; + } +} diff --git a/android/src/main/java/com/sitemate/work/TaskCompletionNotifier.kt b/android/src/main/java/com/sitemate/work/TaskCompletionNotifier.kt new file mode 100644 index 00000000..d8c4431c --- /dev/null +++ b/android/src/main/java/com/sitemate/work/TaskCompletionNotifier.kt @@ -0,0 +1,47 @@ +package com.sitemate.work + +import net.gotev.uploadservice.data.UploadInfo +import net.gotev.uploadservice.data.UploadNotificationConfig +import net.gotev.uploadservice.network.ServerResponse +import net.gotev.uploadservice.observer.task.UploadTaskObserver + +class TaskCompletionNotifier : UploadTaskObserver { + override fun onStart( + info: UploadInfo, + notificationId: Int, + notificationConfig: UploadNotificationConfig + ) { + } + + override fun onProgress( + info: UploadInfo, + notificationId: Int, + notificationConfig: UploadNotificationConfig + ) { + } + + override fun onSuccess( + info: UploadInfo, + notificationId: Int, + notificationConfig: UploadNotificationConfig, + response: ServerResponse + ) { + } + + override fun onError( + info: UploadInfo, + notificationId: Int, + notificationConfig: UploadNotificationConfig, + exception: Throwable + ) { + UploadManager.taskFailed(info.uploadId) + } + + override fun onCompleted( + info: UploadInfo, + notificationId: Int, + notificationConfig: UploadNotificationConfig + ) { + UploadManager.taskCompleted(info.uploadId) + } +} diff --git a/android/src/main/java/com/sitemate/work/UploadManager.kt b/android/src/main/java/com/sitemate/work/UploadManager.kt new file mode 100644 index 00000000..e95423c4 --- /dev/null +++ b/android/src/main/java/com/sitemate/work/UploadManager.kt @@ -0,0 +1,109 @@ +package com.sitemate.work + +import android.content.Context +import com.sitemate.extensions.UploadTaskCreationParameters +import com.sitemate.extensions.getUploadTask +import net.gotev.uploadservice.UploadTask +import net.gotev.uploadservice.data.UploadNotificationConfig +import net.gotev.uploadservice.data.UploadTaskParameters +import net.gotev.uploadservice.logger.UploadServiceLogger +import java.util.concurrent.* + +class UploadManager { + + companion object { + /** + * Sets the Thread Pool to use for upload operations. + * By default a thread pool with size equal to the number of processors is created. + */ + @JvmStatic + var threadPool: AbstractExecutorService = ThreadPoolExecutor( + Runtime.getRuntime().availableProcessors(), // Initial pool size + Runtime.getRuntime().availableProcessors(), // Max pool size + 5.toLong(), // Keep Alive Time + TimeUnit.SECONDS, + LinkedBlockingQueue() + ) + + val TAG: String = UploadManager::class.java.simpleName + + private const val UPLOAD_NOTIFICATION_BASE_ID = 1234 // Something unique + + private var notificationIncrementalId = 0 + private val uploadTasksMap = ConcurrentHashMap() + + /** + * Stops the upload task with the given uploadId. + * @param uploadId The unique upload id + */ + @Synchronized + @JvmStatic + fun stopUpload(uploadId: String) { + uploadTasksMap[uploadId]?.cancel() + } + + /** + * Gets the list of the currently active upload tasks. + * @return list of uploadIDs or an empty list if no tasks are currently running + */ + @JvmStatic + val taskList: List + @Synchronized get() = if (uploadTasksMap.isEmpty()) { + emptyList() + } else { + uploadTasksMap.keys().toList() + } + + /** + * Stop all the active uploads. + */ + @Synchronized + @JvmStatic + fun stopAllUploads() { + val iterator = uploadTasksMap.keys.iterator() + + while (iterator.hasNext()) { + uploadTasksMap[iterator.next()]?.cancel() + } + } + + fun startUpload(context: Context, params: UploadTaskParameters, notificationConfig: UploadNotificationConfig) { + val taskCreationParameters = UploadTaskCreationParameters(params, notificationConfig) + + if (uploadTasksMap.containsKey(taskCreationParameters.params.id)) { + UploadServiceLogger.error(TAG, taskCreationParameters.params.id) { + "Preventing upload! An upload with the same ID is already in progress. " + + "Every upload must have unique ID. Please check your code and fix it!" + } + return + } + + val currentTask = context.getUploadTask( + creationParameters = taskCreationParameters, + notificationId = UPLOAD_NOTIFICATION_BASE_ID + notificationIncrementalId, + ) ?: return + + uploadTasksMap[currentTask.params.id] = currentTask + threadPool.execute(currentTask) + } + + /** + * Called by each task when it errors out. + * @param uploadId the uploadID of the task with error + */ + @Synchronized + fun taskFailed(uploadId: String) { + uploadTasksMap.remove(uploadId) + } + + /** + * Called by each task when it is completed (either successfully, with an error or due to + * user cancellation). + * @param uploadId the uploadID of the finished task + */ + @Synchronized + fun taskCompleted(uploadId: String) { + uploadTasksMap.remove(uploadId) + } + } +} diff --git a/android/src/main/java/com/sitemate/work/UploadWorker.kt b/android/src/main/java/com/sitemate/work/UploadWorker.kt new file mode 100644 index 00000000..87772183 --- /dev/null +++ b/android/src/main/java/com/sitemate/work/UploadWorker.kt @@ -0,0 +1,20 @@ +package com.sitemate.work + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.sitemate.extensions.PARAM_KEY_NOTIF_CONFIG +import com.sitemate.extensions.PARAM_KEY_TASK_PARAMS +import com.sitemate.extensions.toUploadNotificationConfig +import com.sitemate.extensions.toUploadTaskParameters + +class UploadWorker(val context: Context, params: WorkerParameters): Worker(context, params) { + + override fun doWork(): Result { + val taskParamsStr = inputData.getString(PARAM_KEY_TASK_PARAMS) ?: return Result.failure() + val notifConfigStr = inputData.getString(PARAM_KEY_NOTIF_CONFIG) ?: return Result.failure() + + UploadManager.startUpload(context, taskParamsStr.toUploadTaskParameters(), notifConfigStr.toUploadNotificationConfig()) + return Result.success() + } +} diff --git a/android/src/main/java/com/vydia/RNUploader/NotificationActions.kt b/android/src/main/java/com/vydia/RNUploader/NotificationActions.kt deleted file mode 100644 index 0aed437d..00000000 --- a/android/src/main/java/com/vydia/RNUploader/NotificationActions.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.vydia.RNUploader - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent - -class NotificationActions { - var INTENT_ACTION = "com.vydia.RNUploader.notification.action" - - val PARAM_ACTION = "action" - val PARAM_UPLOAD_ID = "uploadId" - - val ACTION_CANCEL_UPLOAD = "cancelUpload" - - - fun getCancelUploadAction(context: Context?, - requestCode: Int, - uploadID: String?): PendingIntent? { - val intent = Intent(INTENT_ACTION) - intent.putExtra(PARAM_ACTION, ACTION_CANCEL_UPLOAD) - intent.putExtra(PARAM_UPLOAD_ID, uploadID) - return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT) - } -} diff --git a/android/src/main/java/com/vydia/RNUploader/NotificationActionsReceiver.kt b/android/src/main/java/com/vydia/RNUploader/NotificationActionsReceiver.kt deleted file mode 100644 index 8d7752ce..00000000 --- a/android/src/main/java/com/vydia/RNUploader/NotificationActionsReceiver.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.vydia.RNUploader - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.util.Log -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.WritableMap -import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter -import net.gotev.uploadservice.UploadService - -class NotificationActionsReceiver : BroadcastReceiver() { - - private val TAG = "NotificationActReceiver" - - private val reactContext: ReactApplicationContext? = null - override fun onReceive(context: Context?, intent: Intent?) { - if (intent == null || NotificationActions().INTENT_ACTION == intent.action) { - return - } - - if (NotificationActions().ACTION_CANCEL_UPLOAD == intent.getStringExtra(NotificationActions().PARAM_ACTION)) { - onUserRequestedUploadCancellation(context!!, intent.getStringExtra(NotificationActions().PARAM_UPLOAD_ID)!!) - } - } - - private fun onUserRequestedUploadCancellation(context: Context, uploadId: String) { - Log.e("CANCEL_UPLOAD", "User requested cancellation of upload with ID: $uploadId") - UploadService.stopUpload(uploadId) - val params = Arguments.createMap() - params.putString("id", uploadId) - sendEvent("cancelled", params, context) - } - - /** - * Sends an event to the JS module. - */ - private fun sendEvent(eventName: String, params: WritableMap?, context: Context) { - reactContext?.getJSModule(RCTDeviceEventEmitter::class.java)?.emit("RNFileUploader-$eventName", params) - ?: Log.e(TAG, "sendEvent() failed due reactContext == null!") - } - -} diff --git a/android/src/main/java/com/vydia/RNUploader/UploaderReactPackage.kt b/android/src/main/java/com/vydia/RNUploader/UploaderReactPackage.kt deleted file mode 100644 index d07faae4..00000000 --- a/android/src/main/java/com/vydia/RNUploader/UploaderReactPackage.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.vydia.RNUploader; - -import android.view.View -import com.facebook.react.ReactPackage -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.ReactShadowNode -import com.facebook.react.uimanager.ViewManager - -class UploaderReactPackage : ReactPackage { - override fun createViewManagers( - reactContext: ReactApplicationContext - ): MutableList>> = mutableListOf() - - override fun createNativeModules( - reactContext: ReactApplicationContext - ): MutableList = listOf(UploaderModule(reactContext)).toMutableList() -} \ No newline at end of file diff --git a/example/RNBackgroundExample/App.js b/example/RNBackgroundExample/App.js index 6c5620c1..57ab8e6d 100644 --- a/example/RNBackgroundExample/App.js +++ b/example/RNBackgroundExample/App.js @@ -7,24 +7,19 @@ */ import React, {useState} from 'react'; + import { - Alert, SafeAreaView, StyleSheet, - ScrollView, View, Text, - StatusBar, Button, Platform, - TouchableOpacity, } from 'react-native'; -import {Header, Colors} from 'react-native/Libraries/NewAppScreen'; - import Upload from 'react-native-background-upload'; -import ImagePicker from 'react-native-image-picker'; +import ImagePicker from 'react-native-image-crop-picker'; import RNFS from 'react-native-fs'; @@ -47,7 +42,63 @@ const commonOptions = { RNFS.writeFile(path, ''); -const App: () => React$Node = () => { +async function upload(params, setUploadId, setProgress) { + const {url, path, method, type, field, headers = {}, ...rest} = params; + const uploadId = await Upload.startUpload({ + url, + path, + method, + type, + headers, + field, + ...rest, + // (Android only) + notification: { + enabled: true, + autoClear: true, + }, + }); + setUploadId(uploadId); + setProgress(0); + return new Promise(resolve => { + Upload.addListener('error', uploadId, data => { + console.log(`NetworkService.bgUpload error ${data.error}`, { + data, + uploadId, + }); + resolve({ + rawResponse: data.error, + responseBody: this.getBgUploadErrorMessage(data.error), + responseCode: 400, + }); + }); + Upload.addListener('cancelled', uploadId, data => { + console.log('NetworkService.bgUpload cancelled', {data, uploadId}); + resolve({ + responseBody: 'Request cancelled', + responseCode: 400, + }); + }); + Upload.addListener('completed', uploadId, data => { + console.log('NetworkService.bgUpload completed', {data, uploadId}); + resolve({ + responseBody: data.responseBody, + responseCode: data.responseCode, + responseHeaders: data.responseHeaders, + }); + }); + if (__DEV__) { + Upload.addListener('progress', uploadId, data => { + if (data.progress % 5 === 0) { + setProgress(+data.progress); + } + console.log(`Progress: ${data.progress}%`); + }); + } + }); +} + +const App = () => { const [delay10Completed, set10SecDelayCompleted] = useState(false); const [delay5Completed, set5SecDelayCompleted] = useState(false); @@ -55,274 +106,189 @@ const App: () => React$Node = () => { const [uploadId, setUploadId] = useState(null); const [progress, setProgress] = useState(null); - const onPressUpload = options => { + const onPressUpload = async options => { if (isImagePickerShowing) { return; } setIsImagePickerShowing(true); - const imagePickerOptions = { - takePhotoButtonTitle: null, - title: 'Upload Media', - chooseFromLibraryButtonTitle: 'Choose From Library', - }; - - ImagePicker.showImagePicker(imagePickerOptions, response => { - let didChooseVideo = true; - - console.log('ImagePicker response: ', response); - const {customButton, didCancel, error, path, uri} = response; - - if (didCancel) { - didChooseVideo = false; - } - - if (error) { - console.warn('ImagePicker error:', response); - didChooseVideo = false; - } - - // TODO: Should this happen higher? + const files = await ImagePicker.openPicker({ + multiple: true, + includeExif: true, + maxFiles: 1, + writeTempFile: false, // note: this would be handy, but is iOS only :-( + }).catch(e => { + console.warn(e); setIsImagePickerShowing(false); + }); - if (!didChooseVideo) { - return; - } - - let finalPath = Platform.OS === 'android' ? path : uri; - - if (finalPath) { - // Video is stored locally on the device - Upload.getFileInfo(finalPath).then(metadata => { - const uploadOpts = Object.assign( - { - path: finalPath, - method: 'POST', - headers: { - 'content-type': metadata.mimeType, // server requires a content-type header - }, - }, - options, - ); - - Upload.startUpload(uploadOpts) - .then(uploadId => { - console.log( - `Upload started with options: ${JSON.stringify(uploadOpts)}`, - ); - setUploadId(uploadId); - setProgress(0); - Upload.addListener('progress', uploadId, data => { - if (data.progress % 5 === 0) { - setProgress(+data.progress); - } - console.log(`Progress: ${data.progress}%`); - }); - Upload.addListener('error', uploadId, data => { - console.log(`Error: ${data.error}%`); - }); - Upload.addListener('completed', uploadId, data => { - console.log('Completed!'); - }); - }) - .catch(function(err) { - setUploadId(null); - setProgress(null); - console.log('Upload error!', err); - }); - }); - } else { - // Video is stored in google cloud - Alert.alert('Video not found'); + setIsImagePickerShowing(false); + + for await (const galleryMedia of files) { + const finalPath = (galleryMedia.path || galleryMedia.uri).replace( + 'file://', + '', + ); + + const metadata = await Upload.getFileInfo(finalPath); + + const uploadOpts = Object.assign( + { + path: finalPath, + method: 'POST', + headers: { + 'content-type': metadata.mimeType, // server requires a content-type header + }, + }, + options, + ); + + try { + await upload(uploadOpts, setUploadId, setProgress); + } catch (e) { + setUploadId(null); + setProgress(null); + console.log('Upload error!', e); } - }); + } }; return ( <> - - - -
- {global.HermesInternal == null ? null : ( - - Engine: Hermes - - )} - - - { - const options = { - ...commonOptions, - url: url10SecDelayPut, - }; - - Upload.startUpload(options) - .then(uploadId => { - console.warn(uploadId); - setUploadId(uploadId); - - Upload.addListener( - 'completed', - uploadId, - ({responseCode}) => { - console.warn({responseCode}); - - if (responseCode <= 299) { - set10SecDelayCompleted(true); - } - }, - ); - }) - .catch(err => { - console.warn(err.message); - }); - }} - > - 10 Sec Delay Success - - - {delay10Completed && ( - - Finished!!! - - )} - - - { - const options = { - ...commonOptions, - url: url5secDelayFail, - }; - - Upload.startUpload(options) - .then(uploadId => { - console.warn(uploadId); - setUploadId(uploadId); - - Upload.addListener( - 'completed', - uploadId, - ({responseCode}) => { - console.warn(responseCode); - if (responseCode === 502) { - set5SecDelayCompleted(true); - } - }, - ); - - Upload.addListener( - 'error', - uploadId, - ({responseCode}) => { + + + +