Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 43 additions & 56 deletions android/src/main/java/com/vydia/RNUploader/EventReporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,51 @@ import android.util.Log
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Response

// Sends events to React Native
class EventReporter {
companion object {
private const val TAG = "UploadReceiver"
fun cancelled(uploadId: String) =
sendEvent("cancelled", Arguments.createMap().apply {
putString("id", uploadId)
})

fun error(uploadId: String, exception: Throwable) =
sendEvent("error", Arguments.createMap().apply {
putString("id", uploadId)
putString("error", exception.message ?: "Unknown exception")
})

fun success(uploadId: String, response: Response) =
CoroutineScope(Dispatchers.IO).launch {
sendEvent("completed", Arguments.createMap().apply {
putString("id", uploadId)
putInt("responseCode", response.code)
putString("responseBody", response.body?.string().let {
if (it.isNullOrBlank()) response.message else it
})
putMap("responseHeaders", Arguments.createMap().apply {
response.headers.names().forEach { name ->
putString(name, response.headers.values(name).joinToString(", "))
}
})
})
}

fun progress(uploadId: String, bytesSentTotal: Long, contentLength: Long) =
sendEvent("progress", Arguments.createMap().apply {
putString("id", uploadId)
putDouble("progress", (bytesSentTotal.toDouble() * 100 / contentLength)) //0-100
})

fun notification() = sendEvent("notification")

/** Sends an event to the JS module */
private fun sendEvent(eventName: String, params: WritableMap = Arguments.createMap()) {
val reactContext = UploaderModule.reactContext ?: return

// Right after JS reloads, react instance might not be available yet
if (!reactContext.hasActiveReactInstance()) return

try {
val jsModule = reactContext.getJSModule(RCTDeviceEventEmitter::class.java)
jsModule.emit("RNFileUploader-$eventName", params)
} catch (exc: Throwable) {
Log.e(TAG, "sendEvent() failed", exc)
}
object EventReporter {

private const val TAG = "UploadReceiver"
fun cancelled(uploadId: String) =
sendEvent("cancelled", Arguments.createMap().apply {
putString("id", uploadId)
})

fun error(uploadId: String, exception: Throwable) =
sendEvent("error", Arguments.createMap().apply {
putString("id", uploadId)
putString("error", exception.message ?: "Unknown exception")
})

fun success(uploadId: String, response: UploadResponse) =
sendEvent("completed", Arguments.createMap().apply {
putString("id", uploadId)
putInt("responseCode", response.code)
putString("responseBody", response.body)
putMap("responseHeaders", Arguments.makeNativeMap(response.headers))
})


fun progress(uploadId: String, bytesSentTotal: Long, contentLength: Long) =
sendEvent("progress", Arguments.createMap().apply {
putString("id", uploadId)
putDouble("progress", (bytesSentTotal.toDouble() * 100 / contentLength)) //0-100
})

fun notification() = sendEvent("notification")

/** Sends an event to the JS module */
private fun sendEvent(eventName: String, params: WritableMap = Arguments.createMap()) {
val reactContext = UploaderModule.reactContext ?: return

// Right after JS reloads, react instance might not be available yet
if (!reactContext.hasActiveReactInstance()) return

try {
val jsModule = reactContext.getJSModule(RCTDeviceEventEmitter::class.java)
jsModule.emit("RNFileUploader-$eventName", params)
} catch (exc: Throwable) {
Log.e(TAG, "sendEvent() failed", exc)
}
}

}
6 changes: 3 additions & 3 deletions android/src/main/java/com/vydia/RNUploader/Upload.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.vydia.RNUploader

import com.facebook.react.bridge.ReadableMap
import java.util.*
import java.util.UUID

// Data model of a single upload
// Can be created from RN's ReadableMap
Expand All @@ -14,7 +14,7 @@ data class Upload(
val maxRetries: Int,
val wifiOnly: Boolean,
val headers: Map<String, String>,
val notificationId: String,
val notificationId: Int,
val notificationTitle: String,
val notificationTitleNoInternet: String,
val notificationTitleNoWifi: String,
Expand All @@ -39,7 +39,7 @@ data class Upload(
}
return@let map
},
notificationId = map.getString(Upload::notificationId.name)
notificationId = map.getString(Upload::notificationId.name)?.hashCode()
?: throw MissingOptionException(Upload::notificationId.name),
notificationTitle = map.getString(Upload::notificationTitle.name)
?: throw MissingOptionException(Upload::notificationTitle.name),
Expand Down
86 changes: 40 additions & 46 deletions android/src/main/java/com/vydia/RNUploader/UploadProgress.kt
Original file line number Diff line number Diff line change
@@ -1,64 +1,58 @@
package com.vydia.RNUploader

import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.work.WorkManager
import com.vydia.RNUploader.UploaderModule.Companion.WORKER_TAG

// Stores and aggregates total progress from all workers
class UploadProgress {

companion object {
private fun storage(context: Context) =
context.getSharedPreferences("RNFileUpload-Progress", Context.MODE_PRIVATE)

@Synchronized
fun set(context: Context, uploadId: String, bytesUploaded: Long, fileSize: Long) =
storage(context).edit()
.putLong("$uploadId-uploaded", bytesUploaded)
.putLong("$uploadId-size", fileSize)
.apply()

@Synchronized
fun remove(context: Context, uploadId: String) =
storage(context).edit()
.remove("$uploadId-uploaded")
.remove("$uploadId-size")
.apply()

@Synchronized
fun total(context: Context): Double {
val storage = storage(context)
object UploadProgress {
private data class Progress(
var bytesUploaded: Long,
val size: Long,
var complete: Boolean = false
) {
fun complete() {
bytesUploaded = size
complete = true
}
}

val totalBytesUploaded = storage.all.keys
.filter { it.endsWith("-uploaded") }
.sumOf { storage.getLong(it, 0L) }
private val map = mutableMapOf<String, Progress>()

val totalFileSize = storage.all.keys
.filter { it.endsWith("-size") }
.sumOf { storage.getLong(it, 0L) }
@Synchronized
fun add(id: String, size: Long) {
map[id] = Progress(bytesUploaded = 0L, size = size)
}

if (totalFileSize == 0L) return 0.0
return (totalBytesUploaded.toDouble() * 100 / totalFileSize)
}
@Synchronized
fun set(uploadId: String, bytesUploaded: Long) {
map[uploadId]?.bytesUploaded = bytesUploaded

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only sets if map[uploadId] exists, which is not conventional in a set function. I would rename this to something like setIfNotNull

}

private val handler = Handler(Looper.getMainLooper())
@Synchronized
fun complete(uploadId: String) {
map[uploadId]?.complete()

// Attempt to clear in 2 seconds. This is the simplest way to let the
// last worker reset the overall progress.
// Clearing progress ensures the notification starts at 0% next time.
fun scheduleClearing(context: Context) =
handler.postDelayed({ clearIfNeeded(context) }, 2000)
Handler(Looper.getMainLooper()).postDelayed({ clearIfCompleted() }, 2000)
}

@Synchronized
fun clearIfNeeded(context: Context) {
val workManager = WorkManager.getInstance(context)
val works = workManager.getWorkInfosByTag(WORKER_TAG).get()
if (works.any { !it.state.isFinished }) return
@Synchronized
fun remove(uploadId: String) {
map.remove(uploadId)
}

val storage = storage(context)
storage.edit().clear().apply()
}
@Synchronized
fun total(): Double {
val totalBytesUploaded = map.values.sumOf { it.bytesUploaded }
val totalFileSize = map.values.sumOf { it.size }
if (totalFileSize == 0L) return 0.0
return (totalBytesUploaded.toDouble() * 100 / totalFileSize)
}

@Synchronized
private fun clearIfCompleted() {
if (map.values.all { it.complete }) map.clear()
}
}
27 changes: 23 additions & 4 deletions android/src/main/java/com/vydia/RNUploader/UploadUtils.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.vydia.RNUploader

import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.Response
import okio.Buffer
import okio.BufferedSink
import okio.ForwardingSink
Expand All @@ -15,6 +20,11 @@ import kotlin.coroutines.resumeWithException
// Throttling interval of progress reports
private const val PROGRESS_INTERVAL = 500 // milliseconds

data class UploadResponse(
val code: Int,
val body: String,
val headers: Map<String, String>
)

// make an upload request using okhttp
suspend fun okhttpUpload(
Expand All @@ -23,7 +33,7 @@ suspend fun okhttpUpload(
file: File,
onProgress: (Long) -> Unit
) =
suspendCancellableCoroutine<Response> { continuation ->
suspendCancellableCoroutine { continuation ->
val requestBody = file.asRequestBody()
var lastProgressReport = 0L
fun throttled(): Boolean {
Expand All @@ -47,8 +57,17 @@ suspend fun okhttpUpload(
override fun onFailure(call: Call, e: IOException) =
continuation.resumeWithException(e)

override fun onResponse(call: Call, response: Response) =
continuation.resumeWith(Result.success(response))
override fun onResponse(call: Call, response: Response) {
val result = response.use { res -> // close the response asap
UploadResponse(
res.code,
res.body?.string()?.takeIf { str -> str.isNotEmpty() } ?: res.message,
res.headers.toMultimap().mapValues { it.value.joinToString(", ") }
)
}

continuation.resumeWith(Result.success(result))
}
})
}

Expand Down
Loading
Loading