Skip to content

Commit a4c733f

Browse files
committed
2 parents b87e2fa + 4c143f2 commit a4c733f

5 files changed

Lines changed: 183 additions & 49 deletions

File tree

android/src/main/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
<uses-permission android:name="android.permission.WAKE_LOCK" />
1818

1919
<application>
20+
<!-- ContentProvider for automatic context initialization -->
21+
<provider
22+
android:name="com.margelo.nitro.nitroclouduploader.ContextProvider"
23+
android:authorities="${applicationId}.nitroclouduploader.contextprovider"
24+
android:exported="false"
25+
android:initOrder="100" />
26+
2027
<!-- Foreground service for background uploads -->
2128
<service
2229
android:name="com.margelo.nitro.nitroclouduploader.UploadForegroundService"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.margelo.nitro.nitroclouduploader
2+
3+
import android.content.ContentProvider
4+
import android.content.ContentValues
5+
import android.content.Context
6+
import android.database.Cursor
7+
import android.net.Uri
8+
9+
/**
10+
* ContentProvider to automatically get application context on app startup
11+
* This is called before any Activity or Application.onCreate()
12+
*/
13+
class ContextProvider : ContentProvider() {
14+
override fun onCreate(): Boolean {
15+
// Store application context globally
16+
appContext = context?.applicationContext
17+
println("✅ ContextProvider initialized with context: ${appContext != null}")
18+
return true
19+
}
20+
21+
override fun query(
22+
uri: Uri,
23+
projection: Array<out String>?,
24+
selection: String?,
25+
selectionArgs: Array<out String>?,
26+
sortOrder: String?
27+
): Cursor? = null
28+
29+
override fun getType(uri: Uri): String? = null
30+
31+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
32+
33+
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
34+
35+
override fun update(
36+
uri: Uri,
37+
values: ContentValues?,
38+
selection: String?,
39+
selectionArgs: Array<out String>?
40+
): Int = 0
41+
42+
companion object {
43+
@Volatile
44+
var appContext: Context? = null
45+
private set
46+
}
47+
}
48+

android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt

Lines changed: 120 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ import java.util.Collections
3434

3535
/**
3636
* Cloud uploader using Coroutines - No WorkManager dependency
37+
* Note: Nitro uses no-arg constructor via JNI, so context must be nullable
3738
*/
3839
@DoNotStrip
3940
@Keep
4041
class NitroCloudUploader(
41-
private val appContext: Context
42+
private val injectedContext: Context? = null
4243
) : HybridNitroCloudUploaderSpec() {
4344

4445
companion object {
@@ -48,6 +49,10 @@ class NitroCloudUploader(
4849
private const val MAX_RETRIES = 3
4950
}
5051

52+
// ✅ Get context from injected param or ContentProvider (auto-initialized on app start)
53+
private val appContext: Context?
54+
get() = injectedContext ?: ContextProvider.appContext
55+
5156
// ✅ Nullable with lazy initialization (Nitro JNI bridge bypasses normal initialization)
5257
private var _activeUploads: ConcurrentHashMap<String, UploadJob>? = null
5358
private var _uploadStates: ConcurrentHashMap<String, UploadStateData>? = null
@@ -130,9 +135,12 @@ class NitroCloudUploader(
130135
private val notificationManager: NotificationManager
131136
get() {
132137
if (_notificationManager == null) {
133-
synchronized(this) {
134-
if (_notificationManager == null) {
135-
_notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
138+
val ctx = appContext // Cache to avoid smart cast issues
139+
if (ctx != null) {
140+
synchronized(this) {
141+
if (_notificationManager == null) {
142+
_notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
143+
}
136144
}
137145
}
138146
}
@@ -142,9 +150,12 @@ class NitroCloudUploader(
142150
private val connectivityManager: ConnectivityManager
143151
get() {
144152
if (_connectivityManager == null) {
145-
synchronized(this) {
146-
if (_connectivityManager == null) {
147-
_connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
153+
val ctx = appContext // Cache to avoid smart cast issues
154+
if (ctx != null) {
155+
synchronized(this) {
156+
if (_connectivityManager == null) {
157+
_connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
158+
}
148159
}
149160
}
150161
}
@@ -218,6 +229,12 @@ class NitroCloudUploader(
218229

219230
private fun setupNotifications() {
220231
try {
232+
val ctx = appContext // Cache to avoid smart cast issues
233+
if (ctx == null) {
234+
println("⚠️ appContext is null, skipping notification setup")
235+
return
236+
}
237+
221238
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
222239
val channel = NotificationChannel(
223240
NOTIFICATION_CHANNEL_ID,
@@ -236,6 +253,12 @@ class NitroCloudUploader(
236253

237254
private fun setupNetworkMonitoring() {
238255
try {
256+
val ctx = appContext // Cache to avoid smart cast issues
257+
if (ctx == null) {
258+
println("⚠️ appContext is null, skipping network monitoring setup")
259+
return
260+
}
261+
239262
val request = NetworkRequest.Builder()
240263
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
241264
.build()
@@ -369,13 +392,24 @@ class NitroCloudUploader(
369392
}
370393

371394
// ✅ Stop foreground service on success
372-
try {
373-
UploadForegroundService.stopUploadService(appContext)
374-
} catch (e: Exception) {
375-
println("⚠️ Failed to stop foreground service: ${e.message}")
395+
val ctx = appContext // Cache to avoid smart cast issues
396+
if (ctx != null) {
397+
try {
398+
UploadForegroundService.stopUploadService(ctx)
399+
} catch (e: Exception) {
400+
println("⚠️ Failed to stop foreground service: ${e.message}")
401+
}
376402
}
377403

378404
promise.resolve(result)
405+
} catch (e: CancellationException) {
406+
// ✅ Cancellation is intentional, resolve with cancelled result
407+
println("⏸️ Upload cancelled: ${e.message}")
408+
promise.resolve(UploadResult(
409+
uploadId = uploadId,
410+
success = false,
411+
etags = emptyArray()
412+
))
379413
} catch (e: Exception) {
380414
println("❌ Upload failed: ${e.message}")
381415

@@ -384,10 +418,13 @@ class NitroCloudUploader(
384418
}
385419

386420
// ✅ Stop foreground service on failure
387-
try {
388-
UploadForegroundService.stopUploadService(appContext)
389-
} catch (serviceError: Exception) {
390-
println("⚠️ Failed to stop foreground service: ${serviceError.message}")
421+
val ctx = appContext // Cache to avoid smart cast issues
422+
if (ctx != null) {
423+
try {
424+
UploadForegroundService.stopUploadService(ctx)
425+
} catch (serviceError: Exception) {
426+
println("⚠️ Failed to stop foreground service: ${serviceError.message}")
427+
}
391428
}
392429

393430
promise.reject(e)
@@ -399,10 +436,13 @@ class NitroCloudUploader(
399436
activeUploads[uploadId] = UploadJob(uploadId, job)
400437

401438
// ✅ Start foreground service for background upload support
402-
try {
403-
UploadForegroundService.startUploadService(appContext, uploadId)
404-
} catch (e: Exception) {
405-
println("⚠️ Failed to start foreground service: ${e.message}")
439+
val ctx = appContext // Cache to avoid smart cast issues
440+
if (shouldNotify && ctx != null) {
441+
try {
442+
UploadForegroundService.startUploadService(ctx, uploadId)
443+
} catch (e: Exception) {
444+
println("⚠️ Failed to start foreground service: ${e.message}")
445+
}
406446
}
407447

408448
emitEvent(
@@ -586,14 +626,19 @@ class NitroCloudUploader(
586626

587627
if (etag.isEmpty()) {
588628
println("⚠️ No ETag received for part ${part.partNumber}")
629+
throw Exception("No ETag in response for part ${part.partNumber}")
589630
}
590631

591-
// ✅ Update state
632+
// ✅ Update state - double-check not already counted
592633
synchronized(state) {
593-
state.partETags[part.partNumber] = etag
594-
state.completedChunks++
595-
state.bytesUploaded += part.size
596-
state.failedChunks.remove(part.partNumber)
634+
if (!state.partETags.containsKey(part.partNumber)) {
635+
state.partETags[part.partNumber] = etag
636+
state.completedChunks++
637+
state.bytesUploaded += part.size
638+
state.failedChunks.remove(part.partNumber)
639+
} else {
640+
println("⚠️ Part ${part.partNumber} was already marked as completed, skipping state update")
641+
}
597642
}
598643

599644
val progress = state.bytesUploaded.toDouble() / state.totalBytes
@@ -630,15 +675,18 @@ class NitroCloudUploader(
630675
showNotification(uploadId, progressPercent, "Uploading... ${progressPercent}%")
631676

632677
// ✅ Update foreground service notification
633-
try {
634-
UploadForegroundService.updateProgress(
635-
appContext,
636-
uploadId,
637-
progressPercent,
638-
"Uploading... ${progressPercent}%"
639-
)
640-
} catch (e: Exception) {
641-
println("⚠️ Failed to update foreground service: ${e.message}")
678+
val ctx = appContext // Cache to avoid smart cast issues
679+
if (ctx != null) {
680+
try {
681+
UploadForegroundService.updateProgress(
682+
ctx,
683+
uploadId,
684+
progressPercent,
685+
"Uploading... ${progressPercent}%"
686+
)
687+
} catch (e: Exception) {
688+
println("⚠️ Failed to update foreground service: ${e.message}")
689+
}
642690
}
643691
}
644692

@@ -769,10 +817,37 @@ class NitroCloudUploader(
769817
println("🛑 Cancelling upload: $uploadId")
770818
val uploadJob = activeUploads[uploadId]
771819

820+
// ✅ Cancel job safely
772821
if (uploadJob != null) {
773-
uploadJob.job.cancel()
822+
try {
823+
if (uploadJob.job.isActive) {
824+
uploadJob.job.cancel() // Don't pass exception, just cancel
825+
println("✅ Upload job cancelled")
826+
} else {
827+
println("⚠️ Upload job is not active, skipping cancellation")
828+
}
829+
} catch (e: Exception) {
830+
println("⚠️ Error cancelling job: ${e.message}")
831+
}
832+
} else {
833+
println("⚠️ No active upload found for: $uploadId")
774834
}
775835

836+
// ✅ Cleanup before emitting events (prevent accessing disposed resources)
837+
cleanup(uploadId)
838+
cancelNotification()
839+
840+
// ✅ Stop foreground service on cancel
841+
val ctx = appContext // Cache to avoid smart cast issues
842+
if (ctx != null) {
843+
try {
844+
UploadForegroundService.stopUploadService(ctx)
845+
} catch (e: Exception) {
846+
println("⚠️ Failed to stop foreground service: ${e.message}")
847+
}
848+
}
849+
850+
// ✅ Emit event after cleanup
776851
emitEvent(
777852
UploadProgressEvent(
778853
type = "upload-cancelled",
@@ -785,16 +860,6 @@ class NitroCloudUploader(
785860
)
786861
)
787862

788-
cleanup(uploadId)
789-
cancelNotification()
790-
791-
// ✅ Stop foreground service on cancel
792-
try {
793-
UploadForegroundService.stopUploadService(appContext)
794-
} catch (e: Exception) {
795-
println("⚠️ Failed to stop foreground service: ${e.message}")
796-
}
797-
798863
promise.resolve(Unit)
799864
} catch (e: Exception) {
800865
println("❌ Cancel error: ${e.message}")
@@ -886,10 +951,17 @@ class NitroCloudUploader(
886951

887952
private fun showNotification(uploadId: String, progress: Int, message: String, isComplete: Boolean = false) {
888953
try {
954+
// ✅ Check if context is available and cache it
955+
val ctx = appContext
956+
if (ctx == null) {
957+
println("⚠️ appContext is null, cannot show notification")
958+
return
959+
}
960+
889961
// ✅ Check notification permission for Android 13+ (API 33+)
890962
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
891963
val hasPermission = ContextCompat.checkSelfPermission(
892-
appContext,
964+
ctx,
893965
android.Manifest.permission.POST_NOTIFICATIONS
894966
) == PackageManager.PERMISSION_GRANTED
895967

@@ -901,13 +973,13 @@ class NitroCloudUploader(
901973
}
902974

903975
// ✅ Create intent to open app when notification is tapped
904-
val launchIntent = appContext.packageManager.getLaunchIntentForPackage(appContext.packageName)?.apply {
976+
val launchIntent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)?.apply {
905977
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
906978
}
907979

908980
val pendingIntent = if (launchIntent != null) {
909981
PendingIntent.getActivity(
910-
appContext,
982+
ctx,
911983
0,
912984
launchIntent,
913985
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
@@ -916,7 +988,7 @@ class NitroCloudUploader(
916988
null
917989
}
918990

919-
val notification = NotificationCompat.Builder(appContext, NOTIFICATION_CHANNEL_ID)
991+
val notification = NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID)
920992
.setContentTitle("Uploading File")
921993
.setContentText(message)
922994
.setSmallIcon(android.R.drawable.stat_sys_upload)

android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploaderPackage.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.facebook.react.module.model.ReactModuleInfoProvider
77

88
class NitroCloudUploaderPackage : BaseReactPackage() {
99
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10+
// Store context for Nitro modules to access
11+
appContext = reactContext.applicationContext
1012
return null
1113
}
1214

@@ -15,6 +17,11 @@ class NitroCloudUploaderPackage : BaseReactPackage() {
1517
}
1618

1719
companion object {
20+
// Static context for Nitro modules (since Nitro uses no-arg constructor)
21+
@JvmStatic
22+
var appContext: android.content.Context? = null
23+
internal set
24+
1825
init {
1926
System.loadLibrary("nitroclouduploader")
2027
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-nitro-cloud-uploader",
3-
"version": "1.0.6",
3+
"version": "1.0.8",
44
"description": "Mutipart File, Images, Audios upload to AWS S3, Cloudflare R2, Backblaze storage solutions with Nitro Modules",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

0 commit comments

Comments
 (0)