From a15c88570c58928abbc3b448fa2ab765bfccb00c Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 16 Oct 2025 15:33:02 -0700 Subject: [PATCH 1/8] fix --- .../com/vydia/RNUploader/EventReporter.kt | 27 ++---- .../main/java/com/vydia/RNUploader/Upload.kt | 6 +- .../com/vydia/RNUploader/UploadProgress.kt | 84 +++++++++---------- .../java/com/vydia/RNUploader/UploadUtils.kt | 23 ++++- .../java/com/vydia/RNUploader/UploadWorker.kt | 60 ++++++------- .../com/vydia/RNUploader/UploaderModule.kt | 9 +- example/RNBGUExample/android/build.gradle | 2 +- 7 files changed, 105 insertions(+), 106 deletions(-) diff --git a/android/src/main/java/com/vydia/RNUploader/EventReporter.kt b/android/src/main/java/com/vydia/RNUploader/EventReporter.kt index 32daf999..0ded9ba0 100644 --- a/android/src/main/java/com/vydia/RNUploader/EventReporter.kt +++ b/android/src/main/java/com/vydia/RNUploader/EventReporter.kt @@ -4,10 +4,6 @@ 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 { @@ -24,21 +20,14 @@ class EventReporter { 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 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 { diff --git a/android/src/main/java/com/vydia/RNUploader/Upload.kt b/android/src/main/java/com/vydia/RNUploader/Upload.kt index 5afdac69..db7b83d5 100644 --- a/android/src/main/java/com/vydia/RNUploader/Upload.kt +++ b/android/src/main/java/com/vydia/RNUploader/Upload.kt @@ -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 @@ -14,7 +14,7 @@ data class Upload( val maxRetries: Int, val wifiOnly: Boolean, val headers: Map, - val notificationId: String, + val notificationId: Int, val notificationTitle: String, val notificationTitleNoInternet: String, val notificationTitleNoWifi: String, @@ -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), diff --git a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt index ee1bd78a..7a7c9b88 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt @@ -1,64 +1,56 @@ 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 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch // Stores and aggregates total progress from all workers -class UploadProgress { +object UploadProgress { + private data class Progress(var bytesUploaded: Long, val size: Long) - companion object { - private fun storage(context: Context) = - context.getSharedPreferences("RNFileUpload-Progress", Context.MODE_PRIVATE) + private val map = mutableMapOf() - @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) - - val totalBytesUploaded = storage.all.keys - .filter { it.endsWith("-uploaded") } - .sumOf { storage.getLong(it, 0L) } + @Synchronized + fun add(id: String, size: Long) { + map[id] = Progress(bytesUploaded = 0L, size = size) + } - val totalFileSize = storage.all.keys - .filter { it.endsWith("-size") } - .sumOf { storage.getLong(it, 0L) } + @Synchronized + fun set(uploadId: String, bytesUploaded: Long) { + map[uploadId]?.bytesUploaded = bytesUploaded + } - if (totalFileSize == 0L) return 0.0 - return (totalBytesUploaded.toDouble() * 100 / totalFileSize) - } + @Synchronized + fun remove(uploadId: String) { + map.remove(uploadId) + } - private val handler = Handler(Looper.getMainLooper()) + @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) + } - // 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) + @Synchronized + private fun clearIfNeeded(context: Context) { + val workManager = WorkManager.getInstance(context) + val works = workManager.getWorkInfosByTag(WORKER_TAG).get() - @Synchronized - fun clearIfNeeded(context: Context) { - val workManager = WorkManager.getInstance(context) - val works = workManager.getWorkInfosByTag(WORKER_TAG).get() - if (works.any { !it.state.isFinished }) return + if (works.all { it.state.isFinished }) map.clear() + } - val storage = storage(context) - storage.edit().clear().apply() + init { + CoroutineScope(Dispatchers.IO).launch { + while (true) { + delay(5000L) + clearIfNeeded(UploaderModule.reactContext ?: continue) + } } } } \ No newline at end of file diff --git a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt index 57da5aac..aadb415b 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt @@ -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 @@ -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 +) // make an upload request using okhttp suspend fun okhttpUpload( @@ -23,7 +33,7 @@ suspend fun okhttpUpload( file: File, onProgress: (Long) -> Unit ) = - suspendCancellableCoroutine { continuation -> + suspendCancellableCoroutine { continuation -> val requestBody = file.asRequestBody() var lastProgressReport = 0L fun throttled(): Boolean { @@ -48,7 +58,14 @@ suspend fun okhttpUpload( continuation.resumeWithException(e) override fun onResponse(call: Call, response: Response) = - continuation.resumeWith(Result.success(response)) + response.use { + val result = UploadResponse( + response.code, + response.body?.string().takeIf { !it.isNullOrBlank() } ?: response.message, + response.headers.toMultimap().mapValues { it.value.joinToString(", ") } + ) + continuation.resumeWith(Result.success(result)) + } }) } diff --git a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt index a3fc701b..cebf9edf 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt @@ -1,6 +1,7 @@ package com.vydia.RNUploader import android.app.Notification +import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -17,11 +18,9 @@ import androidx.work.WorkerParameters import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.withContext import okhttp3.OkHttpClient -import okhttp3.Response import java.io.File import java.io.IOException import java.net.UnknownHostException @@ -59,6 +58,8 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : private lateinit var upload: Upload private var retries = 0 private var connectivity = Connectivity.Ok + private val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager override suspend fun doWork(): Result = withContext(Dispatchers.IO) { // Retrieve the upload. If this throws errors, error reporting won't work. @@ -115,48 +116,46 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : return@withContext Result.failure() } - private suspend fun upload(): Response? = withContext(Dispatchers.IO) { + private suspend fun upload(): UploadResponse? { val file = File(upload.path) val size = file.length() // Register progress asap so the total progress is accurate // This needs to happen before the semaphore wait - handleProgress(0, size) + UploadProgress.add(upload.id, size) // Don't bother to run on an invalid network - if (!validateAndReportConnectivity()) return@withContext null + if (!validateAndReportConnectivity()) return null // wait for its turn to run semaphore.acquire() try { - val response = okhttpUpload(client, upload, file) { progress -> - launch { handleProgress(progress, size) } + return okhttpUpload(client, upload, file) { progress -> + handleProgress(progress, size) } - - handleProgress(size, size) - return@withContext response - } - // don't catch, propagate error up - finally { + } catch (error: Throwable) { + // reset progress on error + UploadProgress.set(upload.id, 0L) + // pass the error to upper layer for retry decision + throw error + } finally { semaphore.release() } } - private suspend fun handleProgress(bytesSentTotal: Long, fileSize: Long) { - UploadProgress.set(context, upload.id, bytesSentTotal, fileSize) + private fun handleProgress(bytesSentTotal: Long, fileSize: Long) { + UploadProgress.set(upload.id, bytesSentTotal) EventReporter.progress(upload.id, bytesSentTotal, fileSize) - setForeground(getForegroundInfo()) + notificationManager.notify(upload.notificationId, buildNotification()) } - private fun handleSuccess(response: Response) { - UploadProgress.scheduleClearing(context) + private fun handleSuccess(response: UploadResponse) { EventReporter.success(upload.id, response) } private fun handleError(error: Throwable) { - UploadProgress.remove(context, upload.id) - UploadProgress.scheduleClearing(context) + UploadProgress.remove(upload.id) EventReporter.error(upload.id, error) } @@ -165,14 +164,13 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : private fun checkAndHandleCancellation(): Boolean { if (!isStopped) return false - UploadProgress.remove(context, upload.id) - UploadProgress.scheduleClearing(context) + UploadProgress.remove(upload.id) EventReporter.cancelled(upload.id) return true } /** @return whether to retry */ - private suspend fun checkRetry(error: Throwable): Boolean { + private fun checkRetry(error: Throwable): Boolean { var unlimitedRetry = false // Error was thrown due to unmet network preferences. @@ -202,19 +200,17 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : } // Checks connection and alerts connection issues - private suspend fun validateAndReportConnectivity(): Boolean { + private fun validateAndReportConnectivity(): Boolean { this.connectivity = validateConnectivity(context, upload.wifiOnly) // alert connectivity mode - setForeground(getForegroundInfo()) + notificationManager.notify(upload.notificationId, buildNotification()) return this.connectivity == Connectivity.Ok } // builds the notification required to enable Foreground mode - override suspend fun getForegroundInfo(): ForegroundInfo { - // All workers share the same notification that shows the total progress - val id = upload.notificationId.hashCode() + fun buildNotification(): Notification { val channel = upload.notificationChannel - val progress = UploadProgress.total(context) + val progress = UploadProgress.total() val progress2Decimals = "%.2f".format(progress) val title = when (connectivity) { Connectivity.NoWifi -> upload.notificationTitleNoWifi @@ -230,7 +226,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : content.setTextViewText(R.id.notification_progress, "${progress2Decimals}%") content.setProgressBar(R.id.notification_progress_bar, 100, progress.toInt(), false) - val notification = NotificationCompat.Builder(context, channel).run { + return NotificationCompat.Builder(context, channel).run { // Starting Android 12, the notification shows up with a confusing delay of 10s. // This fixes that delay. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) @@ -248,7 +244,11 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : setContentIntent(openAppIntent(context)) build() } + } + override suspend fun getForegroundInfo(): ForegroundInfo { + val notification = buildNotification() + val id = upload.notificationId // Starting Android 14, FOREGROUND_SERVICE_TYPE_DATA_SYNC is mandatory, otherwise app will crash return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) ForegroundInfo(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) diff --git a/android/src/main/java/com/vydia/RNUploader/UploaderModule.kt b/android/src/main/java/com/vydia/RNUploader/UploaderModule.kt index 0b6db02b..0d833fc3 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploaderModule.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploaderModule.kt @@ -5,7 +5,11 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf -import com.facebook.react.bridge.* +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap import com.google.gson.Gson @@ -23,9 +27,6 @@ class UploaderModule(context: ReactApplicationContext) : init { reactContext = context - // workers may be killed abruptly for whatever reasons, - // so they might not have had a chance to clear the progress data. - UploadProgress.clearIfNeeded(context) } diff --git a/example/RNBGUExample/android/build.gradle b/example/RNBGUExample/android/build.gradle index c83c9a07..ca04cb46 100644 --- a/example/RNBGUExample/android/build.gradle +++ b/example/RNBGUExample/android/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { buildToolsVersion = "34.0.0" - minSdkVersion = 24 + minSdkVersion = 29 compileSdkVersion = 34 targetSdkVersion = 34 ndkVersion = "26.1.10909125" From 8d3d07415dd10e6624adb168f6d963b4d9c08f3a Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 16 Oct 2025 15:41:20 -0700 Subject: [PATCH 2/8] doc --- android/src/main/java/com/vydia/RNUploader/UploadUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt index aadb415b..1df757b9 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt @@ -58,7 +58,7 @@ suspend fun okhttpUpload( continuation.resumeWithException(e) override fun onResponse(call: Call, response: Response) = - response.use { + response.use { // close the response asap val result = UploadResponse( response.code, response.body?.string().takeIf { !it.isNullOrBlank() } ?: response.message, From 710a9caf8415542e587b73027d950bfe125cfb70 Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 16 Oct 2025 15:50:45 -0700 Subject: [PATCH 3/8] fix --- .../com/vydia/RNUploader/UploadProgress.kt | 44 +++++++++---------- .../java/com/vydia/RNUploader/UploadWorker.kt | 1 + 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt index 7a7c9b88..412f7073 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt @@ -1,16 +1,15 @@ package com.vydia.RNUploader -import android.content.Context -import androidx.work.WorkManager -import com.vydia.RNUploader.UploaderModule.Companion.WORKER_TAG -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import android.os.Handler +import android.os.Looper // Stores and aggregates total progress from all workers object UploadProgress { - private data class Progress(var bytesUploaded: Long, val size: Long) + private data class Progress( + var bytesUploaded: Long, + val size: Long, + var complete: Boolean = false + ) private val map = mutableMapOf() @@ -24,6 +23,19 @@ object UploadProgress { map[uploadId]?.bytesUploaded = bytesUploaded } + @Synchronized + fun complete(uploadId: String) { + map[uploadId]?.let { + it.bytesUploaded = it.size + it.complete = true + } + + // 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. + Handler(Looper.getMainLooper()).postDelayed({ clearIfNeeded() }, 2000) + } + @Synchronized fun remove(uploadId: String) { map.remove(uploadId) @@ -38,19 +50,7 @@ object UploadProgress { } @Synchronized - private fun clearIfNeeded(context: Context) { - val workManager = WorkManager.getInstance(context) - val works = workManager.getWorkInfosByTag(WORKER_TAG).get() - - if (works.all { it.state.isFinished }) map.clear() - } - - init { - CoroutineScope(Dispatchers.IO).launch { - while (true) { - delay(5000L) - clearIfNeeded(UploaderModule.reactContext ?: continue) - } - } + private fun clearIfNeeded() { + if (map.values.all { it.complete }) map.clear() } } \ No newline at end of file diff --git a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt index cebf9edf..a3bbec7c 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt @@ -151,6 +151,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : } private fun handleSuccess(response: UploadResponse) { + UploadProgress.complete(upload.id) EventReporter.success(upload.id, response) } From a3ac03ba0fbbb2a20c84fee5ff5b198c638bbcad Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 16 Oct 2025 15:51:56 -0700 Subject: [PATCH 4/8] simpler --- .../com/vydia/RNUploader/EventReporter.kt | 88 +++++++++---------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/android/src/main/java/com/vydia/RNUploader/EventReporter.kt b/android/src/main/java/com/vydia/RNUploader/EventReporter.kt index 0ded9ba0..3604a137 100644 --- a/android/src/main/java/com/vydia/RNUploader/EventReporter.kt +++ b/android/src/main/java/com/vydia/RNUploader/EventReporter.kt @@ -6,51 +6,49 @@ import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter // 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: 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) - } +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) } } - } From e3c135ce217faef9c69bcc6f0345ad305f51509a Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 16 Oct 2025 16:11:48 -0700 Subject: [PATCH 5/8] fix --- .../src/main/java/com/vydia/RNUploader/UploadUtils.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt index 1df757b9..38d4f8b1 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt @@ -57,15 +57,17 @@ suspend fun okhttpUpload( override fun onFailure(call: Call, e: IOException) = continuation.resumeWithException(e) - override fun onResponse(call: Call, response: Response) = - response.use { // close the response asap - val result = UploadResponse( + override fun onResponse(call: Call, response: Response) { + val result = response.use { // close the response asap + UploadResponse( response.code, response.body?.string().takeIf { !it.isNullOrBlank() } ?: response.message, response.headers.toMultimap().mapValues { it.value.joinToString(", ") } ) - continuation.resumeWith(Result.success(result)) } + + continuation.resumeWith(Result.success(result)) + } }) } From 976621f3ab2600126c0a084f75b867a229c155fa Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 11 Dec 2025 16:42:42 -0800 Subject: [PATCH 6/8] Address Daniel's PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Name response variable in use block (UploadUtils.kt) - Fix shadowed it in takeIf lambda (UploadUtils.kt) - Rename set() to setIfNotNull() (UploadProgress.kt) - Add complete() method to Progress class (UploadProgress.kt) - Rename clearIfNeeded() to clearIfCompleted() (UploadProgress.kt) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../com/vydia/RNUploader/UploadProgress.kt | 18 ++++++++++-------- .../java/com/vydia/RNUploader/UploadUtils.kt | 8 ++++---- .../java/com/vydia/RNUploader/UploadWorker.kt | 4 ++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt index 412f7073..fc9d1e60 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt @@ -9,7 +9,12 @@ object UploadProgress { var bytesUploaded: Long, val size: Long, var complete: Boolean = false - ) + ) { + fun complete() { + bytesUploaded = size + complete = true + } + } private val map = mutableMapOf() @@ -19,21 +24,18 @@ object UploadProgress { } @Synchronized - fun set(uploadId: String, bytesUploaded: Long) { + fun setIfNotNull(uploadId: String, bytesUploaded: Long) { map[uploadId]?.bytesUploaded = bytesUploaded } @Synchronized fun complete(uploadId: String) { - map[uploadId]?.let { - it.bytesUploaded = it.size - it.complete = true - } + 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. - Handler(Looper.getMainLooper()).postDelayed({ clearIfNeeded() }, 2000) + Handler(Looper.getMainLooper()).postDelayed({ clearIfCompleted() }, 2000) } @Synchronized @@ -50,7 +52,7 @@ object UploadProgress { } @Synchronized - private fun clearIfNeeded() { + private fun clearIfCompleted() { if (map.values.all { it.complete }) map.clear() } } \ No newline at end of file diff --git a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt index 38d4f8b1..e26e2ec1 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadUtils.kt @@ -58,11 +58,11 @@ suspend fun okhttpUpload( continuation.resumeWithException(e) override fun onResponse(call: Call, response: Response) { - val result = response.use { // close the response asap + val result = response.use { res -> // close the response asap UploadResponse( - response.code, - response.body?.string().takeIf { !it.isNullOrBlank() } ?: response.message, - response.headers.toMultimap().mapValues { it.value.joinToString(", ") } + res.code, + res.body?.string()?.takeIf { str -> str.isNotEmpty() } ?: res.message, + res.headers.toMultimap().mapValues { it.value.joinToString(", ") } ) } diff --git a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt index a3bbec7c..58f1ec0f 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt @@ -136,7 +136,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : } } catch (error: Throwable) { // reset progress on error - UploadProgress.set(upload.id, 0L) + UploadProgress.setIfNotNull(upload.id, 0L) // pass the error to upper layer for retry decision throw error } finally { @@ -145,7 +145,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : } private fun handleProgress(bytesSentTotal: Long, fileSize: Long) { - UploadProgress.set(upload.id, bytesSentTotal) + UploadProgress.setIfNotNull(upload.id, bytesSentTotal) EventReporter.progress(upload.id, bytesSentTotal, fileSize) notificationManager.notify(upload.notificationId, buildNotification()) } From 28382b4c9dab0ce4fde2fec4fa4dd033efc496bf Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 11 Dec 2025 19:00:02 -0800 Subject: [PATCH 7/8] set --- android/src/main/java/com/vydia/RNUploader/UploadProgress.kt | 2 +- android/src/main/java/com/vydia/RNUploader/UploadWorker.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt index fc9d1e60..03b9455b 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadProgress.kt @@ -24,7 +24,7 @@ object UploadProgress { } @Synchronized - fun setIfNotNull(uploadId: String, bytesUploaded: Long) { + fun set(uploadId: String, bytesUploaded: Long) { map[uploadId]?.bytesUploaded = bytesUploaded } diff --git a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt index 58f1ec0f..a3bbec7c 100644 --- a/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt +++ b/android/src/main/java/com/vydia/RNUploader/UploadWorker.kt @@ -136,7 +136,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : } } catch (error: Throwable) { // reset progress on error - UploadProgress.setIfNotNull(upload.id, 0L) + UploadProgress.set(upload.id, 0L) // pass the error to upper layer for retry decision throw error } finally { @@ -145,7 +145,7 @@ class UploadWorker(private val context: Context, params: WorkerParameters) : } private fun handleProgress(bytesSentTotal: Long, fileSize: Long) { - UploadProgress.setIfNotNull(upload.id, bytesSentTotal) + UploadProgress.set(upload.id, bytesSentTotal) EventReporter.progress(upload.id, bytesSentTotal, fileSize) notificationManager.notify(upload.notificationId, buildNotification()) } From ca7748e9bde9fe943e59f35840ea1344f6fe7976 Mon Sep 17 00:00:00 2001 From: thomasvo Date: Thu, 11 Dec 2025 19:01:44 -0800 Subject: [PATCH 8/8] package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d66fa806..a32a3e60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-background-upload", - "version": "7.5.2", + "version": "7.5.3", "description": "Cross platform http post file uploader with android and iOS background support", "main": "src/index", "typings": "lib/index.d.ts",