@ammarahmed/react-native-upload 6.16.0 → 6.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/appfolio/extensions/ContextExtensions.kt +63 -0
- package/android/src/main/java/com/appfolio/extensions/UploadExtensions.kt +57 -0
- package/android/src/main/java/com/appfolio/uploader/GlobalRequestObserverDelegate.kt +62 -0
- package/android/src/main/java/com/appfolio/uploader/ModifiedBinaryUploadRequest.kt +29 -0
- package/android/src/main/java/com/appfolio/uploader/ModifiedHttpUploadRequest.kt +57 -0
- package/android/src/main/java/com/appfolio/uploader/ModifiedMultipartUploadRequest.kt +60 -0
- package/android/src/main/java/com/appfolio/uploader/UploaderModule.kt +384 -0
- package/android/src/main/java/com/appfolio/uploader/UploaderReactPackage.java +32 -0
- package/android/src/main/java/com/appfolio/work/TaskCompletionNotifier.kt +47 -0
- package/android/src/main/java/com/appfolio/work/UploadManager.kt +109 -0
- package/android/src/main/java/com/appfolio/work/UploadWorker.kt +20 -0
- package/package.json +1 -1
- package/src/index.ts +549 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
package com.appfolio.uploader
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import android.webkit.MimeTypeMap
|
|
10
|
+
import androidx.work.WorkInfo
|
|
11
|
+
import androidx.work.WorkManager
|
|
12
|
+
import com.appfolio.work.UploadManager
|
|
13
|
+
import com.appfolio.work.UploadWorker
|
|
14
|
+
import com.facebook.react.BuildConfig
|
|
15
|
+
import com.facebook.react.bridge.*
|
|
16
|
+
import net.gotev.uploadservice.UploadServiceConfig.httpStack
|
|
17
|
+
import net.gotev.uploadservice.UploadServiceConfig.initialize
|
|
18
|
+
import net.gotev.uploadservice.data.UploadNotificationConfig
|
|
19
|
+
import net.gotev.uploadservice.data.UploadNotificationStatusConfig
|
|
20
|
+
import net.gotev.uploadservice.observer.request.GlobalRequestObserver
|
|
21
|
+
import net.gotev.uploadservice.okhttp.OkHttpStack
|
|
22
|
+
import okhttp3.OkHttpClient
|
|
23
|
+
import java.io.File
|
|
24
|
+
import java.util.concurrent.TimeUnit
|
|
25
|
+
|
|
26
|
+
class UploaderModule(val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
|
|
27
|
+
private val TAG = "UploaderBridge"
|
|
28
|
+
private var notificationChannelID = "BackgroundUploadChannel"
|
|
29
|
+
private var isGlobalRequestObserver = false
|
|
30
|
+
private var limitNetwork = false
|
|
31
|
+
|
|
32
|
+
override fun getName(): String {
|
|
33
|
+
return "RNFileUploader"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
Sets uploading network limit to unmetered network.
|
|
38
|
+
*/
|
|
39
|
+
@ReactMethod
|
|
40
|
+
fun shouldLimitNetwork(limit: Boolean) {
|
|
41
|
+
limitNetwork = limit;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
Gets file information for the path specified. Example valid path is: /storage/extSdCard/DCIM/Camera/20161116_074726.mp4
|
|
46
|
+
Returns an object such as: {extension: "mp4", size: "3804316", exists: true, mimeType: "video/mp4", name: "20161116_074726.mp4"}
|
|
47
|
+
*/
|
|
48
|
+
@ReactMethod
|
|
49
|
+
fun getFileInfo(path: String?, promise: Promise) {
|
|
50
|
+
try {
|
|
51
|
+
val params = Arguments.createMap()
|
|
52
|
+
val fileInfo = File(path)
|
|
53
|
+
params.putString("name", fileInfo.name)
|
|
54
|
+
if (!fileInfo.exists() || !fileInfo.isFile) {
|
|
55
|
+
params.putBoolean("exists", false)
|
|
56
|
+
} else {
|
|
57
|
+
params.putBoolean("exists", true)
|
|
58
|
+
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
|
|
59
|
+
val extension = MimeTypeMap.getFileExtensionFromUrl(path)
|
|
60
|
+
params.putString("extension", extension)
|
|
61
|
+
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase())
|
|
62
|
+
params.putString("mimeType", mimeType)
|
|
63
|
+
}
|
|
64
|
+
promise.resolve(params)
|
|
65
|
+
} catch (exc: Exception) {
|
|
66
|
+
exc.printStackTrace()
|
|
67
|
+
Log.e(TAG, exc.message, exc)
|
|
68
|
+
promise.reject(exc)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun configureUploadServiceHTTPStack(options: ReadableMap, promise: Promise) {
|
|
73
|
+
var followRedirects = true
|
|
74
|
+
var followSslRedirects = true
|
|
75
|
+
var retryOnConnectionFailure = true
|
|
76
|
+
var connectTimeout = 45
|
|
77
|
+
var writeTimeout = 90
|
|
78
|
+
var readTimeout = 90
|
|
79
|
+
//TODO: make 'cache' customizable
|
|
80
|
+
if (options.hasKey("followRedirects")) {
|
|
81
|
+
if (options.getType("followRedirects") != ReadableType.Boolean) {
|
|
82
|
+
promise.reject(IllegalArgumentException("followRedirects must be a boolean."))
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
followRedirects = options.getBoolean("followRedirects")
|
|
86
|
+
}
|
|
87
|
+
if (options.hasKey("followSslRedirects")) {
|
|
88
|
+
if (options.getType("followSslRedirects") != ReadableType.Boolean) {
|
|
89
|
+
promise.reject(IllegalArgumentException("followSslRedirects must be a boolean."))
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
followSslRedirects = options.getBoolean("followSslRedirects")
|
|
93
|
+
}
|
|
94
|
+
if (options.hasKey("retryOnConnectionFailure")) {
|
|
95
|
+
if (options.getType("retryOnConnectionFailure") != ReadableType.Boolean) {
|
|
96
|
+
promise.reject(IllegalArgumentException("retryOnConnectionFailure must be a boolean."))
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
retryOnConnectionFailure = options.getBoolean("retryOnConnectionFailure")
|
|
100
|
+
}
|
|
101
|
+
if (options.hasKey("connectTimeout")) {
|
|
102
|
+
if (options.getType("connectTimeout") != ReadableType.Number) {
|
|
103
|
+
promise.reject(IllegalArgumentException("connectTimeout must be a number."))
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
connectTimeout = options.getInt("connectTimeout")
|
|
107
|
+
}
|
|
108
|
+
if (options.hasKey("writeTimeout")) {
|
|
109
|
+
if (options.getType("writeTimeout") != ReadableType.Number) {
|
|
110
|
+
promise.reject(IllegalArgumentException("writeTimeout must be a number."))
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
writeTimeout = options.getInt("writeTimeout")
|
|
114
|
+
}
|
|
115
|
+
if (options.hasKey("readTimeout")) {
|
|
116
|
+
if (options.getType("readTimeout") != ReadableType.Number) {
|
|
117
|
+
promise.reject(IllegalArgumentException("readTimeout must be a number."))
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
readTimeout = options.getInt("readTimeout")
|
|
121
|
+
}
|
|
122
|
+
httpStack = OkHttpStack(OkHttpClient().newBuilder()
|
|
123
|
+
.followRedirects(followRedirects)
|
|
124
|
+
.followSslRedirects(followSslRedirects)
|
|
125
|
+
.retryOnConnectionFailure(retryOnConnectionFailure)
|
|
126
|
+
.connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
|
|
127
|
+
.writeTimeout(writeTimeout.toLong(), TimeUnit.SECONDS)
|
|
128
|
+
.readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
|
|
129
|
+
.cache(null)
|
|
130
|
+
.build())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* Starts a file upload.
|
|
135
|
+
* Returns a promise with the string ID of the upload.
|
|
136
|
+
*/
|
|
137
|
+
@ReactMethod
|
|
138
|
+
fun startUpload(options: ReadableMap, promise: Promise) {
|
|
139
|
+
for (key in arrayOf("url", "path")) {
|
|
140
|
+
if (!options.hasKey(key)) {
|
|
141
|
+
promise.reject(java.lang.IllegalArgumentException("Missing '$key' field."))
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
if (options.getType(key) != ReadableType.String) {
|
|
145
|
+
promise.reject(java.lang.IllegalArgumentException("$key must be a string."))
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (options.hasKey("headers") && options.getType("headers") != ReadableType.Map) {
|
|
150
|
+
promise.reject(java.lang.IllegalArgumentException("headers must be a hash."))
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
if (options.hasKey("notification") && options.getType("notification") != ReadableType.Map) {
|
|
154
|
+
promise.reject(java.lang.IllegalArgumentException("notification must be a hash."))
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
configureUploadServiceHTTPStack(options, promise)
|
|
158
|
+
var requestType: String? = "raw"
|
|
159
|
+
if (options.hasKey("type")) {
|
|
160
|
+
requestType = options.getString("type")
|
|
161
|
+
if (requestType == null) {
|
|
162
|
+
promise.reject(java.lang.IllegalArgumentException("type must be string."))
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (requestType != "raw" && requestType != "multipart") {
|
|
166
|
+
promise.reject(java.lang.IllegalArgumentException("type should be string: raw or multipart."))
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
val notification: WritableMap = WritableNativeMap()
|
|
171
|
+
notification.putBoolean("enabled", true)
|
|
172
|
+
if (options.hasKey("notification")) {
|
|
173
|
+
notification.merge(options.getMap("notification")!!)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
val application = reactContext.applicationContext as Application
|
|
177
|
+
|
|
178
|
+
reactContext.addLifecycleEventListener(this)
|
|
179
|
+
|
|
180
|
+
if (notification.hasKey("notificationChannel")) {
|
|
181
|
+
notificationChannelID = notification.getString("notificationChannel")!!
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
createNotificationChannel()
|
|
185
|
+
|
|
186
|
+
initialize(application, notificationChannelID, BuildConfig.DEBUG)
|
|
187
|
+
|
|
188
|
+
if(!isGlobalRequestObserver) {
|
|
189
|
+
isGlobalRequestObserver = true
|
|
190
|
+
GlobalRequestObserver(application, GlobalRequestObserverDelegate(reactContext))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
val url = options.getString("url")
|
|
194
|
+
val filePath = options.getString("path")
|
|
195
|
+
val method = if (options.hasKey("method") && options.getType("method") == ReadableType.String) options.getString("method") else "POST"
|
|
196
|
+
val maxRetries = if (options.hasKey("maxRetries") && options.getType("maxRetries") == ReadableType.Number) options.getInt("maxRetries") else 2
|
|
197
|
+
val customUploadId = if (options.hasKey("customUploadId") && options.getType("method") == ReadableType.String) options.getString("customUploadId") else null
|
|
198
|
+
try {
|
|
199
|
+
val request = if (requestType == "raw") {
|
|
200
|
+
ModifiedBinaryUploadRequest(this.reactApplicationContext, url!!, limitNetwork)
|
|
201
|
+
.setFileToUpload(filePath!!)
|
|
202
|
+
} else {
|
|
203
|
+
if (!options.hasKey("field")) {
|
|
204
|
+
promise.reject(java.lang.IllegalArgumentException("field is required field for multipart type."))
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (options.getType("field") != ReadableType.String) {
|
|
208
|
+
promise.reject(java.lang.IllegalArgumentException("field must be string."))
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
ModifiedMultipartUploadRequest(this.reactApplicationContext, url!!, limitNetwork)
|
|
212
|
+
.addFileToUpload(filePath!!, options.getString("field")!!)
|
|
213
|
+
}
|
|
214
|
+
request.setMethod(method!!)
|
|
215
|
+
.setMaxRetries(maxRetries)
|
|
216
|
+
if (notification.getBoolean("enabled")) {
|
|
217
|
+
val notificationConfig = UploadNotificationConfig(
|
|
218
|
+
notificationChannelId = notificationChannelID,
|
|
219
|
+
isRingToneEnabled = notification.hasKey("enableRingTone") && notification.getBoolean("enableRingTone"),
|
|
220
|
+
progress = UploadNotificationStatusConfig(
|
|
221
|
+
title = if (notification.hasKey("onProgressTitle")) notification.getString("onProgressTitle")!! else "",
|
|
222
|
+
message = if (notification.hasKey("onProgressMessage")) notification.getString("onProgressMessage")!! else ""
|
|
223
|
+
),
|
|
224
|
+
success = UploadNotificationStatusConfig(
|
|
225
|
+
title = if (notification.hasKey("onCompleteTitle")) notification.getString("onCompleteTitle")!! else "",
|
|
226
|
+
message = if (notification.hasKey("onCompleteMessage")) notification.getString("onCompleteMessage")!! else "",
|
|
227
|
+
autoClear = notification.hasKey("autoClear") && notification.getBoolean("autoClear")
|
|
228
|
+
),
|
|
229
|
+
error = UploadNotificationStatusConfig(
|
|
230
|
+
title = if (notification.hasKey("onErrorTitle")) notification.getString("onErrorTitle")!! else "",
|
|
231
|
+
message = if (notification.hasKey("onErrorMessage")) notification.getString("onErrorMessage")!! else ""
|
|
232
|
+
),
|
|
233
|
+
cancelled = UploadNotificationStatusConfig(
|
|
234
|
+
title = if (notification.hasKey("onCancelledTitle")) notification.getString("onCancelledTitle")!! else "",
|
|
235
|
+
message = if (notification.hasKey("onCancelledMessage")) notification.getString("onCancelledMessage")!! else ""
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
request.setNotificationConfig { _, _ ->
|
|
239
|
+
notificationConfig
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (options.hasKey("parameters")) {
|
|
243
|
+
if (requestType == "raw") {
|
|
244
|
+
promise.reject(java.lang.IllegalArgumentException("Parameters supported only in multipart type"))
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
val parameters = options.getMap("parameters")
|
|
248
|
+
val keys = parameters!!.keySetIterator()
|
|
249
|
+
while (keys.hasNextKey()) {
|
|
250
|
+
val key = keys.nextKey()
|
|
251
|
+
if (parameters.getType(key) != ReadableType.String) {
|
|
252
|
+
promise.reject(java.lang.IllegalArgumentException("Parameters must be string key/values. Value was invalid for '$key'"))
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
request.addParameter(key, parameters.getString(key)!!)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (options.hasKey("headers")) {
|
|
259
|
+
val headers = options.getMap("headers")
|
|
260
|
+
val keys = headers!!.keySetIterator()
|
|
261
|
+
while (keys.hasNextKey()) {
|
|
262
|
+
val key = keys.nextKey()
|
|
263
|
+
if (headers.getType(key) != ReadableType.String) {
|
|
264
|
+
promise.reject(java.lang.IllegalArgumentException("Headers must be string key/values. Value was invalid for '$key'"))
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
request.addHeader(key, headers.getString(key)!!)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (customUploadId != null)
|
|
271
|
+
request.setCustomUploadID(customUploadId)
|
|
272
|
+
|
|
273
|
+
val uploadId = request.startUpload()
|
|
274
|
+
promise.resolve(uploadId)
|
|
275
|
+
} catch (exc: java.lang.Exception) {
|
|
276
|
+
exc.printStackTrace()
|
|
277
|
+
Log.e(TAG, exc.message, exc)
|
|
278
|
+
promise.reject(exc)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/*
|
|
283
|
+
* Cancels file upload
|
|
284
|
+
* Accepts upload ID as a first argument, this upload will be cancelled
|
|
285
|
+
* Event "cancelled" will be fired when upload is cancelled.
|
|
286
|
+
*/
|
|
287
|
+
@ReactMethod
|
|
288
|
+
fun cancelUpload(cancelUploadId: String?, promise: Promise) {
|
|
289
|
+
if (cancelUploadId !is String) {
|
|
290
|
+
promise.reject(java.lang.IllegalArgumentException("Upload ID must be a string"))
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
UploadManager.stopUpload(cancelUploadId)
|
|
295
|
+
promise.resolve(true)
|
|
296
|
+
} catch (exc: java.lang.Exception) {
|
|
297
|
+
exc.printStackTrace()
|
|
298
|
+
Log.e(TAG, exc.message, exc)
|
|
299
|
+
promise.reject(exc)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/*
|
|
304
|
+
* Cancels all file uploads
|
|
305
|
+
*/
|
|
306
|
+
@ReactMethod
|
|
307
|
+
fun stopAllUploads(promise: Promise) {
|
|
308
|
+
try {
|
|
309
|
+
UploadManager.stopAllUploads()
|
|
310
|
+
promise.resolve(true)
|
|
311
|
+
} catch (exc: java.lang.Exception) {
|
|
312
|
+
exc.printStackTrace()
|
|
313
|
+
Log.e(TAG, exc.message, exc)
|
|
314
|
+
promise.reject(exc)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/*
|
|
319
|
+
* Gets all file uploads with their state
|
|
320
|
+
*/
|
|
321
|
+
@ReactMethod
|
|
322
|
+
fun getAllUploads(promise: Promise) {
|
|
323
|
+
val workManager: WorkManager = WorkManager.getInstance(reactContext)
|
|
324
|
+
val workInfos = workManager.getWorkInfosByTag(UploadWorker::class.java.name).get()
|
|
325
|
+
val uploads = Arguments.createArray()
|
|
326
|
+
|
|
327
|
+
for (info in workInfos) {
|
|
328
|
+
// Ignore 'SUCCEEDED' state from WorkManager since that only means that the uploads have started
|
|
329
|
+
if (info.state === WorkInfo.State.SUCCEEDED) {
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
val upload = Arguments.createMap()
|
|
334
|
+
|
|
335
|
+
val idTag = info.tags.toTypedArray().find { it.startsWith(UploadWorker::class.java.simpleName) }
|
|
336
|
+
upload.putString("id", idTag?.removePrefix("${UploadWorker::class.java.simpleName}-"))
|
|
337
|
+
|
|
338
|
+
when(info.state) {
|
|
339
|
+
WorkInfo.State.RUNNING ->
|
|
340
|
+
upload.putString("state", "running")
|
|
341
|
+
WorkInfo.State.CANCELLED, WorkInfo.State.FAILED ->
|
|
342
|
+
upload.putString("state", "cancelled")
|
|
343
|
+
WorkInfo.State.BLOCKED, WorkInfo.State.ENQUEUED ->
|
|
344
|
+
upload.putString("state", "pending")
|
|
345
|
+
else ->
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
uploads.pushMap(upload)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
val uploadTaskList = UploadManager.taskList
|
|
353
|
+
for (task in uploadTaskList) {
|
|
354
|
+
val upload = Arguments.createMap()
|
|
355
|
+
upload.putString("id", task)
|
|
356
|
+
upload.putString("state", "running")
|
|
357
|
+
uploads.pushMap(upload)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
promise.resolve(uploads)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Customize the notification channel as you wish. This is only for a bare minimum example
|
|
364
|
+
private fun createNotificationChannel() {
|
|
365
|
+
if (Build.VERSION.SDK_INT >= 26) {
|
|
366
|
+
val channel = NotificationChannel(
|
|
367
|
+
notificationChannelID,
|
|
368
|
+
"Background Upload Channel",
|
|
369
|
+
NotificationManager.IMPORTANCE_LOW
|
|
370
|
+
)
|
|
371
|
+
val manager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
372
|
+
manager.createNotificationChannel(channel)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
override fun onHostResume() {
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
override fun onHostPause() {
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
override fun onHostDestroy() {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.appfolio.uploader;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage;
|
|
4
|
+
import com.facebook.react.bridge.JavaScriptModule;
|
|
5
|
+
import com.facebook.react.bridge.NativeModule;
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
7
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
8
|
+
|
|
9
|
+
import java.util.ArrayList;
|
|
10
|
+
import java.util.Collections;
|
|
11
|
+
import java.util.List;
|
|
12
|
+
|
|
13
|
+
public class UploaderReactPackage implements ReactPackage {
|
|
14
|
+
|
|
15
|
+
// Deprecated in RN 0.47, @todo remove after < 0.47 support remove
|
|
16
|
+
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
|
17
|
+
return Collections.emptyList();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Override
|
|
21
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
22
|
+
return Collections.emptyList();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
public List<NativeModule> createNativeModules(
|
|
27
|
+
ReactApplicationContext reactContext) {
|
|
28
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
29
|
+
modules.add(new UploaderModule(reactContext));
|
|
30
|
+
return modules;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.appfolio.work
|
|
2
|
+
|
|
3
|
+
import net.gotev.uploadservice.data.UploadInfo
|
|
4
|
+
import net.gotev.uploadservice.data.UploadNotificationConfig
|
|
5
|
+
import net.gotev.uploadservice.network.ServerResponse
|
|
6
|
+
import net.gotev.uploadservice.observer.task.UploadTaskObserver
|
|
7
|
+
|
|
8
|
+
class TaskCompletionNotifier : UploadTaskObserver {
|
|
9
|
+
override fun onStart(
|
|
10
|
+
info: UploadInfo,
|
|
11
|
+
notificationId: Int,
|
|
12
|
+
notificationConfig: UploadNotificationConfig
|
|
13
|
+
) {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override fun onProgress(
|
|
17
|
+
info: UploadInfo,
|
|
18
|
+
notificationId: Int,
|
|
19
|
+
notificationConfig: UploadNotificationConfig
|
|
20
|
+
) {
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun onSuccess(
|
|
24
|
+
info: UploadInfo,
|
|
25
|
+
notificationId: Int,
|
|
26
|
+
notificationConfig: UploadNotificationConfig,
|
|
27
|
+
response: ServerResponse
|
|
28
|
+
) {
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun onError(
|
|
32
|
+
info: UploadInfo,
|
|
33
|
+
notificationId: Int,
|
|
34
|
+
notificationConfig: UploadNotificationConfig,
|
|
35
|
+
exception: Throwable
|
|
36
|
+
) {
|
|
37
|
+
UploadManager.taskFailed(info.uploadId)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun onCompleted(
|
|
41
|
+
info: UploadInfo,
|
|
42
|
+
notificationId: Int,
|
|
43
|
+
notificationConfig: UploadNotificationConfig
|
|
44
|
+
) {
|
|
45
|
+
UploadManager.taskCompleted(info.uploadId)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
package com.appfolio.work
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.appfolio.extensions.UploadTaskCreationParameters
|
|
5
|
+
import com.appfolio.extensions.getUploadTask
|
|
6
|
+
import net.gotev.uploadservice.UploadTask
|
|
7
|
+
import net.gotev.uploadservice.data.UploadNotificationConfig
|
|
8
|
+
import net.gotev.uploadservice.data.UploadTaskParameters
|
|
9
|
+
import net.gotev.uploadservice.logger.UploadServiceLogger
|
|
10
|
+
import java.util.concurrent.*
|
|
11
|
+
|
|
12
|
+
class UploadManager {
|
|
13
|
+
|
|
14
|
+
companion object {
|
|
15
|
+
/**
|
|
16
|
+
* Sets the Thread Pool to use for upload operations.
|
|
17
|
+
* By default a thread pool with size equal to the number of processors is created.
|
|
18
|
+
*/
|
|
19
|
+
@JvmStatic
|
|
20
|
+
var threadPool: AbstractExecutorService = ThreadPoolExecutor(
|
|
21
|
+
Runtime.getRuntime().availableProcessors(), // Initial pool size
|
|
22
|
+
Runtime.getRuntime().availableProcessors(), // Max pool size
|
|
23
|
+
5.toLong(), // Keep Alive Time
|
|
24
|
+
TimeUnit.SECONDS,
|
|
25
|
+
LinkedBlockingQueue<Runnable>()
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
val TAG: String = UploadManager::class.java.simpleName
|
|
29
|
+
|
|
30
|
+
private const val UPLOAD_NOTIFICATION_BASE_ID = 1234 // Something unique
|
|
31
|
+
|
|
32
|
+
private var notificationIncrementalId = 0
|
|
33
|
+
private val uploadTasksMap = ConcurrentHashMap<String, UploadTask>()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Stops the upload task with the given uploadId.
|
|
37
|
+
* @param uploadId The unique upload id
|
|
38
|
+
*/
|
|
39
|
+
@Synchronized
|
|
40
|
+
@JvmStatic
|
|
41
|
+
fun stopUpload(uploadId: String) {
|
|
42
|
+
uploadTasksMap[uploadId]?.cancel()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gets the list of the currently active upload tasks.
|
|
47
|
+
* @return list of uploadIDs or an empty list if no tasks are currently running
|
|
48
|
+
*/
|
|
49
|
+
@JvmStatic
|
|
50
|
+
val taskList: List<String>
|
|
51
|
+
@Synchronized get() = if (uploadTasksMap.isEmpty()) {
|
|
52
|
+
emptyList()
|
|
53
|
+
} else {
|
|
54
|
+
uploadTasksMap.keys().toList()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Stop all the active uploads.
|
|
59
|
+
*/
|
|
60
|
+
@Synchronized
|
|
61
|
+
@JvmStatic
|
|
62
|
+
fun stopAllUploads() {
|
|
63
|
+
val iterator = uploadTasksMap.keys.iterator()
|
|
64
|
+
|
|
65
|
+
while (iterator.hasNext()) {
|
|
66
|
+
uploadTasksMap[iterator.next()]?.cancel()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun startUpload(context: Context, params: UploadTaskParameters, notificationConfig: UploadNotificationConfig) {
|
|
71
|
+
val taskCreationParameters = UploadTaskCreationParameters(params, notificationConfig)
|
|
72
|
+
|
|
73
|
+
if (uploadTasksMap.containsKey(taskCreationParameters.params.id)) {
|
|
74
|
+
UploadServiceLogger.error(TAG, taskCreationParameters.params.id) {
|
|
75
|
+
"Preventing upload! An upload with the same ID is already in progress. " +
|
|
76
|
+
"Every upload must have unique ID. Please check your code and fix it!"
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
val currentTask = context.getUploadTask(
|
|
82
|
+
creationParameters = taskCreationParameters,
|
|
83
|
+
notificationId = UPLOAD_NOTIFICATION_BASE_ID + notificationIncrementalId,
|
|
84
|
+
) ?: return
|
|
85
|
+
|
|
86
|
+
uploadTasksMap[currentTask.params.id] = currentTask
|
|
87
|
+
threadPool.execute(currentTask)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Called by each task when it errors out.
|
|
92
|
+
* @param uploadId the uploadID of the task with error
|
|
93
|
+
*/
|
|
94
|
+
@Synchronized
|
|
95
|
+
fun taskFailed(uploadId: String) {
|
|
96
|
+
uploadTasksMap.remove(uploadId)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Called by each task when it is completed (either successfully, with an error or due to
|
|
101
|
+
* user cancellation).
|
|
102
|
+
* @param uploadId the uploadID of the finished task
|
|
103
|
+
*/
|
|
104
|
+
@Synchronized
|
|
105
|
+
fun taskCompleted(uploadId: String) {
|
|
106
|
+
uploadTasksMap.remove(uploadId)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.appfolio.work
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.work.Worker
|
|
5
|
+
import androidx.work.WorkerParameters
|
|
6
|
+
import com.appfolio.extensions.PARAM_KEY_NOTIF_CONFIG
|
|
7
|
+
import com.appfolio.extensions.PARAM_KEY_TASK_PARAMS
|
|
8
|
+
import com.appfolio.extensions.toUploadNotificationConfig
|
|
9
|
+
import com.appfolio.extensions.toUploadTaskParameters
|
|
10
|
+
|
|
11
|
+
class UploadWorker(val context: Context, params: WorkerParameters): Worker(context, params) {
|
|
12
|
+
|
|
13
|
+
override fun doWork(): Result {
|
|
14
|
+
val taskParamsStr = inputData.getString(PARAM_KEY_TASK_PARAMS) ?: return Result.failure()
|
|
15
|
+
val notifConfigStr = inputData.getString(PARAM_KEY_NOTIF_CONFIG) ?: return Result.failure()
|
|
16
|
+
|
|
17
|
+
UploadManager.startUpload(context, taskParamsStr.toUploadTaskParameters(), notifConfigStr.toUploadNotificationConfig())
|
|
18
|
+
return Result.success()
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
CHANGED