diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 57f2a25..0a5e24a 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -17,6 +17,13 @@
+
+
+
?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?
+ ): Cursor? = null
+
+ override fun getType(uri: Uri): String? = null
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?
+ ): Int = 0
+
+ companion object {
+ @Volatile
+ var appContext: Context? = null
+ private set
+ }
+}
+
diff --git a/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt b/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt
index 83eee71..f93d4dc 100644
--- a/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt
+++ b/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt
@@ -34,11 +34,12 @@ import java.util.Collections
/**
* Cloud uploader using Coroutines - No WorkManager dependency
+ * Note: Nitro uses no-arg constructor via JNI, so context must be nullable
*/
@DoNotStrip
@Keep
class NitroCloudUploader(
- private val appContext: Context
+ private val injectedContext: Context? = null
) : HybridNitroCloudUploaderSpec() {
companion object {
@@ -48,6 +49,10 @@ class NitroCloudUploader(
private const val MAX_RETRIES = 3
}
+ // ✅ Get context from injected param or ContentProvider (auto-initialized on app start)
+ private val appContext: Context?
+ get() = injectedContext ?: ContextProvider.appContext
+
// ✅ Nullable with lazy initialization (Nitro JNI bridge bypasses normal initialization)
private var _activeUploads: ConcurrentHashMap? = null
private var _uploadStates: ConcurrentHashMap? = null
@@ -130,9 +135,12 @@ class NitroCloudUploader(
private val notificationManager: NotificationManager
get() {
if (_notificationManager == null) {
- synchronized(this) {
- if (_notificationManager == null) {
- _notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx != null) {
+ synchronized(this) {
+ if (_notificationManager == null) {
+ _notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ }
}
}
}
@@ -142,9 +150,12 @@ class NitroCloudUploader(
private val connectivityManager: ConnectivityManager
get() {
if (_connectivityManager == null) {
- synchronized(this) {
- if (_connectivityManager == null) {
- _connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx != null) {
+ synchronized(this) {
+ if (_connectivityManager == null) {
+ _connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ }
}
}
}
@@ -218,6 +229,12 @@ class NitroCloudUploader(
private fun setupNotifications() {
try {
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx == null) {
+ println("⚠️ appContext is null, skipping notification setup")
+ return
+ }
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
@@ -236,6 +253,12 @@ class NitroCloudUploader(
private fun setupNetworkMonitoring() {
try {
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx == null) {
+ println("⚠️ appContext is null, skipping network monitoring setup")
+ return
+ }
+
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
@@ -369,13 +392,24 @@ class NitroCloudUploader(
}
// ✅ Stop foreground service on success
- try {
- UploadForegroundService.stopUploadService(appContext)
- } catch (e: Exception) {
- println("⚠️ Failed to stop foreground service: ${e.message}")
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx != null) {
+ try {
+ UploadForegroundService.stopUploadService(ctx)
+ } catch (e: Exception) {
+ println("⚠️ Failed to stop foreground service: ${e.message}")
+ }
}
promise.resolve(result)
+ } catch (e: CancellationException) {
+ // ✅ Cancellation is intentional, resolve with cancelled result
+ println("⏸️ Upload cancelled: ${e.message}")
+ promise.resolve(UploadResult(
+ uploadId = uploadId,
+ success = false,
+ etags = emptyArray()
+ ))
} catch (e: Exception) {
println("❌ Upload failed: ${e.message}")
@@ -384,10 +418,13 @@ class NitroCloudUploader(
}
// ✅ Stop foreground service on failure
- try {
- UploadForegroundService.stopUploadService(appContext)
- } catch (serviceError: Exception) {
- println("⚠️ Failed to stop foreground service: ${serviceError.message}")
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx != null) {
+ try {
+ UploadForegroundService.stopUploadService(ctx)
+ } catch (serviceError: Exception) {
+ println("⚠️ Failed to stop foreground service: ${serviceError.message}")
+ }
}
promise.reject(e)
@@ -399,10 +436,13 @@ class NitroCloudUploader(
activeUploads[uploadId] = UploadJob(uploadId, job)
// ✅ Start foreground service for background upload support
- try {
- UploadForegroundService.startUploadService(appContext, uploadId)
- } catch (e: Exception) {
- println("⚠️ Failed to start foreground service: ${e.message}")
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (shouldNotify && ctx != null) {
+ try {
+ UploadForegroundService.startUploadService(ctx, uploadId)
+ } catch (e: Exception) {
+ println("⚠️ Failed to start foreground service: ${e.message}")
+ }
}
emitEvent(
@@ -586,14 +626,19 @@ class NitroCloudUploader(
if (etag.isEmpty()) {
println("⚠️ No ETag received for part ${part.partNumber}")
+ throw Exception("No ETag in response for part ${part.partNumber}")
}
- // ✅ Update state
+ // ✅ Update state - double-check not already counted
synchronized(state) {
- state.partETags[part.partNumber] = etag
- state.completedChunks++
- state.bytesUploaded += part.size
- state.failedChunks.remove(part.partNumber)
+ if (!state.partETags.containsKey(part.partNumber)) {
+ state.partETags[part.partNumber] = etag
+ state.completedChunks++
+ state.bytesUploaded += part.size
+ state.failedChunks.remove(part.partNumber)
+ } else {
+ println("⚠️ Part ${part.partNumber} was already marked as completed, skipping state update")
+ }
}
val progress = state.bytesUploaded.toDouble() / state.totalBytes
@@ -630,15 +675,18 @@ class NitroCloudUploader(
showNotification(uploadId, progressPercent, "Uploading... ${progressPercent}%")
// ✅ Update foreground service notification
- try {
- UploadForegroundService.updateProgress(
- appContext,
- uploadId,
- progressPercent,
- "Uploading... ${progressPercent}%"
- )
- } catch (e: Exception) {
- println("⚠️ Failed to update foreground service: ${e.message}")
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx != null) {
+ try {
+ UploadForegroundService.updateProgress(
+ ctx,
+ uploadId,
+ progressPercent,
+ "Uploading... ${progressPercent}%"
+ )
+ } catch (e: Exception) {
+ println("⚠️ Failed to update foreground service: ${e.message}")
+ }
}
}
@@ -769,10 +817,37 @@ class NitroCloudUploader(
println("🛑 Cancelling upload: $uploadId")
val uploadJob = activeUploads[uploadId]
+ // ✅ Cancel job safely
if (uploadJob != null) {
- uploadJob.job.cancel()
+ try {
+ if (uploadJob.job.isActive) {
+ uploadJob.job.cancel() // Don't pass exception, just cancel
+ println("✅ Upload job cancelled")
+ } else {
+ println("⚠️ Upload job is not active, skipping cancellation")
+ }
+ } catch (e: Exception) {
+ println("⚠️ Error cancelling job: ${e.message}")
+ }
+ } else {
+ println("⚠️ No active upload found for: $uploadId")
}
+ // ✅ Cleanup before emitting events (prevent accessing disposed resources)
+ cleanup(uploadId)
+ cancelNotification()
+
+ // ✅ Stop foreground service on cancel
+ val ctx = appContext // Cache to avoid smart cast issues
+ if (ctx != null) {
+ try {
+ UploadForegroundService.stopUploadService(ctx)
+ } catch (e: Exception) {
+ println("⚠️ Failed to stop foreground service: ${e.message}")
+ }
+ }
+
+ // ✅ Emit event after cleanup
emitEvent(
UploadProgressEvent(
type = "upload-cancelled",
@@ -785,16 +860,6 @@ class NitroCloudUploader(
)
)
- cleanup(uploadId)
- cancelNotification()
-
- // ✅ Stop foreground service on cancel
- try {
- UploadForegroundService.stopUploadService(appContext)
- } catch (e: Exception) {
- println("⚠️ Failed to stop foreground service: ${e.message}")
- }
-
promise.resolve(Unit)
} catch (e: Exception) {
println("❌ Cancel error: ${e.message}")
@@ -886,10 +951,17 @@ class NitroCloudUploader(
private fun showNotification(uploadId: String, progress: Int, message: String, isComplete: Boolean = false) {
try {
+ // ✅ Check if context is available and cache it
+ val ctx = appContext
+ if (ctx == null) {
+ println("⚠️ appContext is null, cannot show notification")
+ return
+ }
+
// ✅ Check notification permission for Android 13+ (API 33+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val hasPermission = ContextCompat.checkSelfPermission(
- appContext,
+ ctx,
android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
@@ -901,13 +973,13 @@ class NitroCloudUploader(
}
// ✅ Create intent to open app when notification is tapped
- val launchIntent = appContext.packageManager.getLaunchIntentForPackage(appContext.packageName)?.apply {
+ val launchIntent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = if (launchIntent != null) {
PendingIntent.getActivity(
- appContext,
+ ctx,
0,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
@@ -916,7 +988,7 @@ class NitroCloudUploader(
null
}
- val notification = NotificationCompat.Builder(appContext, NOTIFICATION_CHANNEL_ID)
+ val notification = NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Uploading File")
.setContentText(message)
.setSmallIcon(android.R.drawable.stat_sys_upload)
diff --git a/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploaderPackage.kt b/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploaderPackage.kt
index 9ea16be..ee4f810 100644
--- a/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploaderPackage.kt
+++ b/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploaderPackage.kt
@@ -7,6 +7,8 @@ import com.facebook.react.module.model.ReactModuleInfoProvider
class NitroCloudUploaderPackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
+ // Store context for Nitro modules to access
+ appContext = reactContext.applicationContext
return null
}
@@ -15,6 +17,11 @@ class NitroCloudUploaderPackage : BaseReactPackage() {
}
companion object {
+ // Static context for Nitro modules (since Nitro uses no-arg constructor)
+ @JvmStatic
+ var appContext: android.content.Context? = null
+ internal set
+
init {
System.loadLibrary("nitroclouduploader")
}