@kesha-antonov/react-native-background-downloader 4.1.2-alpha.0 → 4.1.3
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.
|
@@ -22,12 +22,14 @@ import com.facebook.react.bridge.Arguments
|
|
|
22
22
|
import com.facebook.react.bridge.Promise
|
|
23
23
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
24
24
|
import com.facebook.react.bridge.ReadableMap
|
|
25
|
+
import com.facebook.react.bridge.ReadableType
|
|
25
26
|
import com.facebook.react.bridge.WritableArray
|
|
26
27
|
import com.facebook.react.bridge.WritableMap
|
|
27
28
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
28
29
|
import com.google.gson.Gson
|
|
29
30
|
import com.google.gson.reflect.TypeToken
|
|
30
31
|
import com.tencent.mmkv.MMKV
|
|
32
|
+
import org.json.JSONObject
|
|
31
33
|
import java.io.File
|
|
32
34
|
import java.net.HttpURLConnection
|
|
33
35
|
import java.net.URL
|
|
@@ -38,829 +40,847 @@ import java.util.concurrent.Future
|
|
|
38
40
|
|
|
39
41
|
class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicationContext) {
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
loadDownloadIdToConfigMap()
|
|
129
|
-
loadConfigMap()
|
|
130
|
-
|
|
131
|
-
downloader = Downloader(reactContext)
|
|
43
|
+
companion object {
|
|
44
|
+
const val NAME = "RNBackgroundDownloader"
|
|
45
|
+
|
|
46
|
+
// Library version
|
|
47
|
+
private const val VERSION = "4.1.3"
|
|
48
|
+
private const val USER_AGENT = "ReactNative-BackgroundDownloader/$VERSION"
|
|
49
|
+
|
|
50
|
+
// Task state constants
|
|
51
|
+
private const val TASK_RUNNING = 0
|
|
52
|
+
private const val TASK_SUSPENDED = 1
|
|
53
|
+
private const val TASK_CANCELING = 2
|
|
54
|
+
private const val TASK_COMPLETED = 3
|
|
55
|
+
|
|
56
|
+
// Error code constants
|
|
57
|
+
private const val ERR_STORAGE_FULL = 0
|
|
58
|
+
private const val ERR_NO_INTERNET = 1
|
|
59
|
+
private const val ERR_NO_WRITE_PERMISSION = 2
|
|
60
|
+
private const val ERR_FILE_NOT_FOUND = 3
|
|
61
|
+
private const val ERR_OTHERS = 100
|
|
62
|
+
|
|
63
|
+
// Network timeout constants (milliseconds)
|
|
64
|
+
private const val REDIRECT_CONNECT_TIMEOUT_MS = 10000 // 10 seconds
|
|
65
|
+
private const val REDIRECT_READ_TIMEOUT_MS = 10000 // 10 seconds
|
|
66
|
+
|
|
67
|
+
// Progress reporting constants
|
|
68
|
+
private const val PROGRESS_REPORT_THRESHOLD = 0.01 // 1% change
|
|
69
|
+
|
|
70
|
+
// HTTP header constants
|
|
71
|
+
private const val KEEP_ALIVE_HEADER_VALUE = "timeout=600, max=1000"
|
|
72
|
+
|
|
73
|
+
private val stateMap = mapOf(
|
|
74
|
+
DownloadManager.STATUS_FAILED to TASK_CANCELING,
|
|
75
|
+
DownloadManager.STATUS_PAUSED to TASK_SUSPENDED,
|
|
76
|
+
DownloadManager.STATUS_PENDING to TASK_RUNNING,
|
|
77
|
+
DownloadManager.STATUS_RUNNING to TASK_RUNNING,
|
|
78
|
+
DownloadManager.STATUS_SUCCESSFUL to TASK_COMPLETED
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
private var mmkv: MMKV? = null
|
|
82
|
+
private lateinit var sharedPreferences: SharedPreferences
|
|
83
|
+
private var isMMKVAvailable = false
|
|
84
|
+
private val sharedLock = Any()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private val cachedExecutorPool: ExecutorService = Executors.newCachedThreadPool()
|
|
88
|
+
private val fixedExecutorPool: ExecutorService = Executors.newFixedThreadPool(1)
|
|
89
|
+
private val downloader: Downloader
|
|
90
|
+
private var downloadReceiver: BroadcastReceiver? = null
|
|
91
|
+
private var downloadIdToConfig = mutableMapOf<Long, RNBGDTaskConfig>()
|
|
92
|
+
private val configIdToDownloadId = mutableMapOf<String, Long>()
|
|
93
|
+
private val configIdToPercent = mutableMapOf<String, Double>()
|
|
94
|
+
private val configIdToLastBytes = mutableMapOf<String, Long>()
|
|
95
|
+
private val configIdToProgressFuture = mutableMapOf<String, Future<OnProgressState?>>()
|
|
96
|
+
private val progressReports = mutableMapOf<String, WritableMap>()
|
|
97
|
+
private var progressInterval = 0
|
|
98
|
+
private var progressMinBytes: Long = 0
|
|
99
|
+
private var lastProgressReportedAt = Date()
|
|
100
|
+
private lateinit var ee: DeviceEventManagerModule.RCTDeviceEventEmitter
|
|
101
|
+
|
|
102
|
+
init {
|
|
103
|
+
// Initialize SharedPreferences as fallback
|
|
104
|
+
sharedPreferences = reactContext.getSharedPreferences(NAME + "_prefs", Context.MODE_PRIVATE)
|
|
105
|
+
|
|
106
|
+
// Try to initialize MMKV with comprehensive error handling
|
|
107
|
+
try {
|
|
108
|
+
MMKV.initialize(reactContext)
|
|
109
|
+
mmkv = MMKV.mmkvWithID(NAME)
|
|
110
|
+
isMMKVAvailable = true
|
|
111
|
+
Log.d(NAME, "MMKV initialized successfully")
|
|
112
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
113
|
+
Log.e(NAME, "Failed to initialize MMKV (libmmkv.so not found): ${e.message}")
|
|
114
|
+
Log.w(NAME, "This may be due to unsupported architecture (x86/ARMv7). Using SharedPreferences fallback.")
|
|
115
|
+
Log.w(NAME, "Download persistence across app restarts will use basic storage.")
|
|
116
|
+
mmkv = null
|
|
117
|
+
isMMKVAvailable = false
|
|
118
|
+
} catch (e: NoClassDefFoundError) {
|
|
119
|
+
Log.e(NAME, "MMKV classes not found: ${e.message}")
|
|
120
|
+
Log.w(NAME, "MMKV library not available on this architecture. Using SharedPreferences fallback.")
|
|
121
|
+
mmkv = null
|
|
122
|
+
isMMKVAvailable = false
|
|
123
|
+
} catch (e: Exception) {
|
|
124
|
+
Log.e(NAME, "Failed to initialize MMKV: ${e.message}")
|
|
125
|
+
Log.w(NAME, "Using SharedPreferences fallback for persistence.")
|
|
126
|
+
mmkv = null
|
|
127
|
+
isMMKVAvailable = false
|
|
132
128
|
}
|
|
133
129
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
val externalDirectory = reactContext.getExternalFilesDir(null)
|
|
138
|
-
constants["documents"] = externalDirectory?.absolutePath ?: reactContext.filesDir.absolutePath
|
|
139
|
-
|
|
140
|
-
constants["TaskRunning"] = TASK_RUNNING
|
|
141
|
-
constants["TaskSuspended"] = TASK_SUSPENDED
|
|
142
|
-
constants["TaskCanceling"] = TASK_CANCELING
|
|
143
|
-
constants["TaskCompleted"] = TASK_COMPLETED
|
|
130
|
+
loadDownloadIdToConfigMap()
|
|
131
|
+
loadConfigMap()
|
|
144
132
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
constants["storageType"] = if (isMMKVAvailable) "MMKV" else "SharedPreferences"
|
|
148
|
-
|
|
149
|
-
return constants
|
|
150
|
-
}
|
|
133
|
+
downloader = Downloader(reactContext)
|
|
134
|
+
}
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
registerDownloadReceiver()
|
|
136
|
+
fun getConstants(): Map<String, Any>? {
|
|
137
|
+
val constants = mutableMapOf<String, Any>()
|
|
155
138
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
}
|
|
139
|
+
val externalDirectory = reactContext.getExternalFilesDir(null)
|
|
140
|
+
constants["documents"] = externalDirectory?.absolutePath ?: reactContext.filesDir.absolutePath
|
|
160
141
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
142
|
+
constants["TaskRunning"] = TASK_RUNNING
|
|
143
|
+
constants["TaskSuspended"] = TASK_SUSPENDED
|
|
144
|
+
constants["TaskCanceling"] = TASK_CANCELING
|
|
145
|
+
constants["TaskCompleted"] = TASK_COMPLETED
|
|
164
146
|
|
|
165
|
-
|
|
166
|
-
|
|
147
|
+
// Expose storage type information for debugging/monitoring
|
|
148
|
+
constants["isMMKVAvailable"] = isMMKVAvailable
|
|
149
|
+
constants["storageType"] = if (isMMKVAvailable) "MMKV" else "SharedPreferences"
|
|
167
150
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
|
171
|
-
val config = downloadIdToConfig[downloadId]
|
|
151
|
+
return constants
|
|
152
|
+
}
|
|
172
153
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
val localUri = downloadStatus.getString("localUri")
|
|
154
|
+
fun initialize() {
|
|
155
|
+
ee = reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
156
|
+
registerDownloadReceiver()
|
|
177
157
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
158
|
+
for ((downloadId, config) in downloadIdToConfig) {
|
|
159
|
+
resumeTasks(downloadId, config)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fun invalidate() {
|
|
164
|
+
unregisterDownloadReceiver()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun registerDownloadReceiver() {
|
|
168
|
+
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
|
169
|
+
|
|
170
|
+
downloadReceiver = object : BroadcastReceiver() {
|
|
171
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
172
|
+
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
|
173
|
+
val config = downloadIdToConfig[downloadId]
|
|
174
|
+
|
|
175
|
+
if (config != null) {
|
|
176
|
+
val downloadStatus = downloader.checkDownloadStatus(downloadId)
|
|
177
|
+
val status = downloadStatus.getInt("status")
|
|
178
|
+
val localUri = downloadStatus.getString("localUri")
|
|
179
|
+
|
|
180
|
+
stopTaskProgress(config.id)
|
|
181
|
+
|
|
182
|
+
synchronized(sharedLock) {
|
|
183
|
+
when (status) {
|
|
184
|
+
DownloadManager.STATUS_SUCCESSFUL -> {
|
|
185
|
+
onSuccessfulDownload(config, downloadStatus)
|
|
186
|
+
}
|
|
187
|
+
DownloadManager.STATUS_FAILED -> {
|
|
188
|
+
onFailedDownload(config, downloadStatus)
|
|
189
|
+
}
|
|
203
190
|
}
|
|
204
|
-
}
|
|
205
191
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
context.registerReceiver(
|
|
219
|
-
receiver,
|
|
220
|
-
filter,
|
|
221
|
-
if (exported) Context.RECEIVER_EXPORTED else Context.RECEIVER_NOT_EXPORTED
|
|
222
|
-
)
|
|
223
|
-
} else {
|
|
224
|
-
context.registerReceiver(receiver, filter)
|
|
192
|
+
if (localUri != null) {
|
|
193
|
+
// Prevent memory leaks from MediaScanner.
|
|
194
|
+
// Download successful, clean task after media scanning.
|
|
195
|
+
val paths = arrayOf(localUri)
|
|
196
|
+
MediaScannerConnection.scanFile(context, paths, null) { _, _ ->
|
|
197
|
+
stopTask(config.id)
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Download failed, clean task.
|
|
201
|
+
stopTask(config.id)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
225
204
|
}
|
|
205
|
+
}
|
|
226
206
|
}
|
|
227
207
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
208
|
+
compatRegisterReceiver(reactContext, downloadReceiver!!, filter, true)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// TAKEN FROM
|
|
212
|
+
// https://github.com/facebook/react-native/pull/38256/files\#diff-d5e21477eeadeb0c536d5870f487a8528f9a16ae928c397fec7b255805cc8ad3
|
|
213
|
+
private fun compatRegisterReceiver(
|
|
214
|
+
context: Context,
|
|
215
|
+
receiver: BroadcastReceiver,
|
|
216
|
+
filter: IntentFilter,
|
|
217
|
+
exported: Boolean
|
|
218
|
+
) {
|
|
219
|
+
if (Build.VERSION.SDK_INT >= 34 && context.applicationInfo.targetSdkVersion >= 34) {
|
|
220
|
+
context.registerReceiver(
|
|
221
|
+
receiver,
|
|
222
|
+
filter,
|
|
223
|
+
if (exported) Context.RECEIVER_EXPORTED else Context.RECEIVER_NOT_EXPORTED
|
|
224
|
+
)
|
|
225
|
+
} else {
|
|
226
|
+
context.registerReceiver(receiver, filter)
|
|
233
227
|
}
|
|
228
|
+
}
|
|
234
229
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
var bytesTotal: Long = 0
|
|
240
|
-
|
|
241
|
-
if (!config.reportedBegin) {
|
|
242
|
-
val onBeginCallable = OnBegin(config, this::onBeginDownload)
|
|
243
|
-
val onBeginFuture = cachedExecutorPool.submit(onBeginCallable)
|
|
244
|
-
val onBeginState = onBeginFuture.get()
|
|
245
|
-
bytesTotal = onBeginState.expectedBytes
|
|
246
|
-
|
|
247
|
-
config.reportedBegin = true
|
|
248
|
-
downloadIdToConfig[downloadId] = config
|
|
249
|
-
saveDownloadIdToConfigMap()
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
val onProgressCallable = OnProgress(
|
|
253
|
-
config,
|
|
254
|
-
downloader,
|
|
255
|
-
downloadId,
|
|
256
|
-
bytesDownloaded,
|
|
257
|
-
bytesTotal,
|
|
258
|
-
this::onProgressDownload
|
|
259
|
-
)
|
|
260
|
-
val onProgressFuture = cachedExecutorPool.submit(onProgressCallable)
|
|
261
|
-
configIdToProgressFuture[config.id] = onProgressFuture
|
|
262
|
-
} catch (e: Exception) {
|
|
263
|
-
Log.e(NAME, "resumeTasks: ${Log.getStackTraceString(e)}")
|
|
264
|
-
}
|
|
265
|
-
}.start()
|
|
230
|
+
private fun unregisterDownloadReceiver() {
|
|
231
|
+
downloadReceiver?.let {
|
|
232
|
+
reactContext.unregisterReceiver(it)
|
|
233
|
+
downloadReceiver = null
|
|
266
234
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun resumeTasks(downloadId: Long, config: RNBGDTaskConfig) {
|
|
238
|
+
Thread {
|
|
239
|
+
try {
|
|
240
|
+
var bytesDownloaded: Long = 0
|
|
241
|
+
var bytesTotal: Long = 0
|
|
242
|
+
|
|
243
|
+
if (!config.reportedBegin) {
|
|
244
|
+
val onBeginCallable = OnBegin(config, this::onBeginDownload)
|
|
245
|
+
val onBeginFuture = cachedExecutorPool.submit(onBeginCallable)
|
|
246
|
+
val onBeginState = onBeginFuture.get()
|
|
247
|
+
bytesTotal = onBeginState.expectedBytes
|
|
248
|
+
|
|
249
|
+
config.reportedBegin = true
|
|
250
|
+
downloadIdToConfig[downloadId] = config
|
|
251
|
+
saveDownloadIdToConfigMap()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
val onProgressCallable = OnProgress(
|
|
255
|
+
config,
|
|
256
|
+
downloader,
|
|
257
|
+
downloadId,
|
|
258
|
+
bytesDownloaded,
|
|
259
|
+
bytesTotal,
|
|
260
|
+
this::onProgressDownload
|
|
261
|
+
)
|
|
262
|
+
val onProgressFuture = cachedExecutorPool.submit(onProgressCallable)
|
|
263
|
+
configIdToProgressFuture[config.id] = onProgressFuture
|
|
264
|
+
} catch (e: Exception) {
|
|
265
|
+
Log.e(NAME, "resumeTasks: ${Log.getStackTraceString(e)}")
|
|
266
|
+
}
|
|
267
|
+
}.start()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun removeTaskFromMap(downloadId: Long) {
|
|
271
|
+
synchronized(sharedLock) {
|
|
272
|
+
val config = downloadIdToConfig[downloadId]
|
|
273
|
+
|
|
274
|
+
if (config != null) {
|
|
275
|
+
configIdToDownloadId.remove(config.id)
|
|
276
|
+
configIdToPercent.remove(config.id)
|
|
277
|
+
configIdToLastBytes.remove(config.id)
|
|
278
|
+
downloadIdToConfig.remove(downloadId)
|
|
279
|
+
saveDownloadIdToConfigMap()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Resolve redirects for a URL up to maxRedirects limit
|
|
286
|
+
* @param originalUrl The original URL to follow
|
|
287
|
+
* @param maxRedirects Maximum number of redirects to follow (0 means no redirect resolution)
|
|
288
|
+
* @param headers Headers to include in redirect resolution requests
|
|
289
|
+
* @return The final resolved URL, or original URL if maxRedirects is 0 or resolution fails
|
|
290
|
+
*/
|
|
291
|
+
private fun resolveRedirects(originalUrl: String, maxRedirects: Int, headers: ReadableMap?): String {
|
|
292
|
+
if (maxRedirects <= 0) {
|
|
293
|
+
return originalUrl
|
|
280
294
|
}
|
|
281
295
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
* @param maxRedirects Maximum number of redirects to follow (0 means no redirect resolution)
|
|
286
|
-
* @param headers Headers to include in redirect resolution requests
|
|
287
|
-
* @return The final resolved URL, or original URL if maxRedirects is 0 or resolution fails
|
|
288
|
-
*/
|
|
289
|
-
private fun resolveRedirects(originalUrl: String, maxRedirects: Int, headers: ReadableMap?): String {
|
|
290
|
-
if (maxRedirects <= 0) {
|
|
291
|
-
return originalUrl
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
var currentUrl = originalUrl
|
|
296
|
-
var redirectCount = 0
|
|
297
|
-
|
|
298
|
-
while (redirectCount < maxRedirects) {
|
|
299
|
-
val url = URL(currentUrl)
|
|
300
|
-
val connection = url.openConnection() as HttpURLConnection
|
|
301
|
-
|
|
302
|
-
// Add headers to the redirect resolution request
|
|
303
|
-
headers?.let {
|
|
304
|
-
val iterator = it.keySetIterator()
|
|
305
|
-
while (iterator.hasNextKey()) {
|
|
306
|
-
val headerKey = iterator.nextKey()
|
|
307
|
-
connection.setRequestProperty(headerKey, it.getString(headerKey))
|
|
308
|
-
}
|
|
309
|
-
}
|
|
296
|
+
try {
|
|
297
|
+
var currentUrl = originalUrl
|
|
298
|
+
var redirectCount = 0
|
|
310
299
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (!hasUserAgentHeader(headers)) {
|
|
315
|
-
connection.setRequestProperty("User-Agent", USER_AGENT)
|
|
316
|
-
}
|
|
300
|
+
while (redirectCount < maxRedirects) {
|
|
301
|
+
val url = URL(currentUrl)
|
|
302
|
+
val connection = url.openConnection() as HttpURLConnection
|
|
317
303
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
304
|
+
// Add headers to the redirect resolution request
|
|
305
|
+
headers?.let {
|
|
306
|
+
val iterator = it.keySetIterator()
|
|
307
|
+
while (iterator.hasNextKey()) {
|
|
308
|
+
val headerKey = iterator.nextKey()
|
|
309
|
+
connection.setRequestProperty(headerKey, it.getString(headerKey))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
322
312
|
|
|
323
|
-
|
|
313
|
+
// Add default headers for consistency with DownloadManager
|
|
314
|
+
connection.setRequestProperty("Connection", "keep-alive")
|
|
315
|
+
connection.setRequestProperty("Keep-Alive", KEEP_ALIVE_HEADER_VALUE)
|
|
316
|
+
if (!hasUserAgentHeader(headers)) {
|
|
317
|
+
connection.setRequestProperty("User-Agent", USER_AGENT)
|
|
318
|
+
}
|
|
324
319
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
Log.w(NAME, "Redirect response without Location header at: $currentUrl")
|
|
330
|
-
break
|
|
331
|
-
}
|
|
320
|
+
connection.instanceFollowRedirects = false
|
|
321
|
+
connection.requestMethod = "HEAD" // Use HEAD to avoid downloading content
|
|
322
|
+
connection.connectTimeout = REDIRECT_CONNECT_TIMEOUT_MS
|
|
323
|
+
connection.readTimeout = REDIRECT_READ_TIMEOUT_MS
|
|
332
324
|
|
|
333
|
-
|
|
334
|
-
currentUrl = when {
|
|
335
|
-
location.startsWith("/") -> {
|
|
336
|
-
val baseUrl = URL(currentUrl)
|
|
337
|
-
"${baseUrl.protocol}://${baseUrl.host}$location"
|
|
338
|
-
}
|
|
339
|
-
!location.startsWith("http") -> {
|
|
340
|
-
val baseUrl = URL(currentUrl)
|
|
341
|
-
"${baseUrl.protocol}://${baseUrl.host}/$location"
|
|
342
|
-
}
|
|
343
|
-
else -> location
|
|
344
|
-
}
|
|
325
|
+
val responseCode = connection.responseCode
|
|
345
326
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
327
|
+
if (responseCode in 300..399) {
|
|
328
|
+
// This is a redirect
|
|
329
|
+
val location = connection.getHeaderField("Location")
|
|
330
|
+
if (location == null) {
|
|
331
|
+
Log.w(NAME, "Redirect response without Location header at: $currentUrl")
|
|
332
|
+
break
|
|
333
|
+
}
|
|
352
334
|
|
|
353
|
-
|
|
335
|
+
// Handle relative URLs
|
|
336
|
+
currentUrl = when {
|
|
337
|
+
location.startsWith("/") -> {
|
|
338
|
+
val baseUrl = URL(currentUrl)
|
|
339
|
+
"${baseUrl.protocol}://${baseUrl.host}$location"
|
|
354
340
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
NAME,
|
|
359
|
-
"Reached maximum redirects ($maxRedirects) for URL: $originalUrl. Final URL: $currentUrl"
|
|
360
|
-
)
|
|
361
|
-
} else {
|
|
362
|
-
Log.d(NAME, "Resolved URL after $redirectCount redirects: $currentUrl")
|
|
341
|
+
!location.startsWith("http") -> {
|
|
342
|
+
val baseUrl = URL(currentUrl)
|
|
343
|
+
"${baseUrl.protocol}://${baseUrl.host}/$location"
|
|
363
344
|
}
|
|
345
|
+
else -> location
|
|
346
|
+
}
|
|
364
347
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return originalUrl
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
fun download(options: ReadableMap) {
|
|
375
|
-
val id = options.getString("id")
|
|
376
|
-
var url = options.getString("url")
|
|
377
|
-
val destination = options.getString("destination")
|
|
378
|
-
val headers = options.getMap("headers")
|
|
379
|
-
val metadata = options.getString("metadata")
|
|
380
|
-
val notificationTitle = options.getString("notificationTitle")
|
|
381
|
-
|
|
382
|
-
val progressIntervalScope = options.getInt("progressInterval")
|
|
383
|
-
if (progressIntervalScope > 0) {
|
|
384
|
-
progressInterval = progressIntervalScope
|
|
385
|
-
saveConfigMap()
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
val progressMinBytesScope = options.getDouble("progressMinBytes")
|
|
389
|
-
if (progressMinBytesScope > 0) {
|
|
390
|
-
progressMinBytes = progressMinBytesScope.toLong()
|
|
391
|
-
saveConfigMap()
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
val isAllowedOverRoaming = options.getBoolean("isAllowedOverRoaming")
|
|
395
|
-
val isAllowedOverMetered = options.getBoolean("isAllowedOverMetered")
|
|
396
|
-
val isNotificationVisible = options.getBoolean("isNotificationVisible")
|
|
397
|
-
|
|
398
|
-
// Get maxRedirects parameter
|
|
399
|
-
var maxRedirects = 0
|
|
400
|
-
if (options.hasKey("maxRedirects")) {
|
|
401
|
-
maxRedirects = options.getInt("maxRedirects")
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (id == null || url == null || destination == null) {
|
|
405
|
-
Log.e(NAME, "download: id, url and destination must be set.")
|
|
406
|
-
return
|
|
348
|
+
Log.d(NAME, "Redirect ${redirectCount + 1}/$maxRedirects: $currentUrl -> $location")
|
|
349
|
+
redirectCount++
|
|
350
|
+
} else {
|
|
351
|
+
// Not a redirect, we've found the final URL
|
|
352
|
+
break
|
|
407
353
|
}
|
|
408
354
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
Log.d(NAME, "Resolving redirects for URL: $url (maxRedirects: $maxRedirects)")
|
|
412
|
-
url = resolveRedirects(url, maxRedirects, headers)
|
|
413
|
-
Log.d(NAME, "Final resolved URL: $url")
|
|
414
|
-
}
|
|
355
|
+
connection.disconnect()
|
|
356
|
+
}
|
|
415
357
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (isNotificationVisible) DownloadManager.Request.VISIBILITY_VISIBLE
|
|
421
|
-
else DownloadManager.Request.VISIBILITY_HIDDEN
|
|
358
|
+
if (redirectCount >= maxRedirects) {
|
|
359
|
+
Log.w(
|
|
360
|
+
NAME,
|
|
361
|
+
"Reached maximum redirects ($maxRedirects) for URL: $originalUrl. Final URL: $currentUrl"
|
|
422
362
|
)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
notificationTitle?.let { request.setTitle(it) }
|
|
363
|
+
} else {
|
|
364
|
+
Log.d(NAME, "Resolved URL after $redirectCount redirects: $currentUrl")
|
|
365
|
+
}
|
|
428
366
|
|
|
429
|
-
|
|
430
|
-
// These headers encourage longer connections and help prevent premature timeouts
|
|
431
|
-
request.addRequestHeader("Connection", "keep-alive")
|
|
432
|
-
request.addRequestHeader("Keep-Alive", KEEP_ALIVE_HEADER_VALUE)
|
|
433
|
-
|
|
434
|
-
// Add a proper User-Agent to improve server compatibility
|
|
435
|
-
if (!hasUserAgentHeader(headers)) {
|
|
436
|
-
request.addRequestHeader("User-Agent", USER_AGENT)
|
|
437
|
-
}
|
|
367
|
+
return currentUrl
|
|
438
368
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
369
|
+
} catch (e: Exception) {
|
|
370
|
+
Log.e(NAME, "Failed to resolve redirects for URL: $originalUrl. Error: ${e.message}")
|
|
371
|
+
// Return original URL if redirect resolution fails
|
|
372
|
+
return originalUrl
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fun download(options: ReadableMap) {
|
|
377
|
+
val id = options.getString("id")
|
|
378
|
+
var url = options.getString("url")
|
|
379
|
+
val destination = options.getString("destination")
|
|
380
|
+
val headers = options.getMap("headers")
|
|
381
|
+
// Handle metadata - it should be a string, but handle object case defensively
|
|
382
|
+
val metadata = if (options.hasKey("metadata")) {
|
|
383
|
+
when (options.getType("metadata")) {
|
|
384
|
+
ReadableType.String -> options.getString("metadata")
|
|
385
|
+
ReadableType.Map -> {
|
|
386
|
+
// If passed as object, convert to JSON string
|
|
387
|
+
try {
|
|
388
|
+
val map = options.getMap("metadata")
|
|
389
|
+
Arguments.toBundle(map)?.let {
|
|
390
|
+
JSONObject(it.toString()).toString()
|
|
391
|
+
} ?: "{}"
|
|
392
|
+
} catch (e: Exception) {
|
|
393
|
+
Log.w(NAME, "Failed to convert metadata map to string: ${e.message}")
|
|
394
|
+
"{}"
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else -> null
|
|
398
|
+
}
|
|
399
|
+
} else null
|
|
400
|
+
val notificationTitle = options.getString("notificationTitle")
|
|
401
|
+
|
|
402
|
+
val progressIntervalScope = options.getInt("progressInterval")
|
|
403
|
+
if (progressIntervalScope > 0) {
|
|
404
|
+
progressInterval = progressIntervalScope
|
|
405
|
+
saveConfigMap()
|
|
406
|
+
}
|
|
446
407
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
408
|
+
val progressMinBytesScope = options.getDouble("progressMinBytes")
|
|
409
|
+
if (progressMinBytesScope > 0) {
|
|
410
|
+
progressMinBytes = progressMinBytesScope.toLong()
|
|
411
|
+
saveConfigMap()
|
|
412
|
+
}
|
|
451
413
|
|
|
452
|
-
|
|
453
|
-
|
|
414
|
+
val isAllowedOverRoaming = options.getBoolean("isAllowedOverRoaming")
|
|
415
|
+
val isAllowedOverMetered = options.getBoolean("isAllowedOverMetered")
|
|
416
|
+
val isNotificationVisible = options.getBoolean("isNotificationVisible")
|
|
454
417
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
saveDownloadIdToConfigMap()
|
|
460
|
-
resumeTasks(downloadId, config)
|
|
461
|
-
}
|
|
418
|
+
// Get maxRedirects parameter
|
|
419
|
+
var maxRedirects = 0
|
|
420
|
+
if (options.hasKey("maxRedirects")) {
|
|
421
|
+
maxRedirects = options.getInt("maxRedirects")
|
|
462
422
|
}
|
|
463
423
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
fun pauseTask(configId: String) {
|
|
468
|
-
synchronized(sharedLock) {
|
|
469
|
-
val downloadId = configIdToDownloadId[configId]
|
|
470
|
-
if (downloadId != null) {
|
|
471
|
-
try {
|
|
472
|
-
downloader.pause(downloadId)
|
|
473
|
-
} catch (e: UnsupportedOperationException) {
|
|
474
|
-
Log.w("RNBackgroundDownloader", "pauseTask: ${e.message}")
|
|
475
|
-
// Note: We don't rethrow the exception to avoid crashing the JS thread.
|
|
476
|
-
// The limitation is already documented and expected.
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
424
|
+
if (id == null || url == null || destination == null) {
|
|
425
|
+
Log.e(NAME, "download: id, url and destination must be set.")
|
|
426
|
+
return
|
|
480
427
|
}
|
|
481
428
|
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
val downloadId = configIdToDownloadId[configId]
|
|
488
|
-
if (downloadId != null) {
|
|
489
|
-
try {
|
|
490
|
-
downloader.resume(downloadId)
|
|
491
|
-
} catch (e: UnsupportedOperationException) {
|
|
492
|
-
Log.w("RNBackgroundDownloader", "resumeTask: ${e.message}")
|
|
493
|
-
// Note: We don't rethrow the exception to avoid crashing the JS thread.
|
|
494
|
-
// The limitation is already documented and expected.
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
429
|
+
// Resolve redirects if maxRedirects is specified
|
|
430
|
+
if (maxRedirects > 0) {
|
|
431
|
+
Log.d(NAME, "Resolving redirects for URL: $url (maxRedirects: $maxRedirects)")
|
|
432
|
+
url = resolveRedirects(url, maxRedirects, headers)
|
|
433
|
+
Log.d(NAME, "Final resolved URL: $url")
|
|
498
434
|
}
|
|
499
435
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
436
|
+
val request = DownloadManager.Request(Uri.parse(url))
|
|
437
|
+
request.setAllowedOverRoaming(isAllowedOverRoaming)
|
|
438
|
+
request.setAllowedOverMetered(isAllowedOverMetered)
|
|
439
|
+
request.setNotificationVisibility(
|
|
440
|
+
if (isNotificationVisible) DownloadManager.Request.VISIBILITY_VISIBLE
|
|
441
|
+
else DownloadManager.Request.VISIBILITY_HIDDEN
|
|
442
|
+
)
|
|
443
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
444
|
+
request.setRequiresCharging(false)
|
|
509
445
|
}
|
|
510
446
|
|
|
511
|
-
|
|
512
|
-
// Firebase Performance compatibility: Add defensive programming to prevent crashes
|
|
513
|
-
// when Firebase Performance SDK is installed and uses bytecode instrumentation
|
|
447
|
+
notificationTitle?.let { request.setTitle(it) }
|
|
514
448
|
|
|
515
|
-
|
|
449
|
+
// Add default headers to improve connection handling for slow-responding URLs
|
|
450
|
+
// These headers encourage longer connections and help prevent premature timeouts
|
|
451
|
+
request.addRequestHeader("Connection", "keep-alive")
|
|
452
|
+
request.addRequestHeader("Keep-Alive", KEEP_ALIVE_HEADER_VALUE)
|
|
516
453
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
try {
|
|
524
|
-
// Currently this method doesn't have any implementation on Android
|
|
525
|
-
// as completion handlers are handled differently than iOS.
|
|
526
|
-
// This defensive structure ensures Firebase Performance compatibility.
|
|
527
|
-
Log.d(NAME, "completeHandler executed successfully for configId: $configId")
|
|
528
|
-
|
|
529
|
-
} catch (e: Exception) {
|
|
530
|
-
// Catch any potential exceptions that might be thrown due to Firebase Performance
|
|
531
|
-
// bytecode instrumentation interfering with method dispatch
|
|
532
|
-
Log.e(NAME, "completeHandler: Exception occurred: ${Log.getStackTraceString(e)}")
|
|
533
|
-
}
|
|
454
|
+
// Add a proper User-Agent to improve server compatibility
|
|
455
|
+
if (!hasUserAgentHeader(headers)) {
|
|
456
|
+
request.addRequestHeader("User-Agent", USER_AGENT)
|
|
534
457
|
}
|
|
535
458
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
downloader.downloadManager.query(query)?.use { cursor ->
|
|
543
|
-
if (cursor.moveToFirst()) {
|
|
544
|
-
do {
|
|
545
|
-
val downloadStatus = downloader.getDownloadStatus(cursor)
|
|
546
|
-
val downloadId = downloadStatus.getString("downloadId")?.toLong()
|
|
547
|
-
|
|
548
|
-
if (downloadId != null && downloadIdToConfig.containsKey(downloadId)) {
|
|
549
|
-
val config = downloadIdToConfig[downloadId]
|
|
550
|
-
|
|
551
|
-
if (config != null) {
|
|
552
|
-
val status = downloadStatus.getInt("status")
|
|
553
|
-
// Handle completed downloads that weren't processed
|
|
554
|
-
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
|
555
|
-
val localUri = downloadStatus.getString("localUri")
|
|
556
|
-
if (localUri != null) {
|
|
557
|
-
try {
|
|
558
|
-
val future = setFileChangesBeforeCompletion(localUri, config.destination)
|
|
559
|
-
future.get()
|
|
560
|
-
} catch (e: Exception) {
|
|
561
|
-
Log.e(NAME, "Error moving completed download file: ${e.message}")
|
|
562
|
-
// Continue with normal processing even if file move fails
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
val params = Arguments.createMap()
|
|
568
|
-
|
|
569
|
-
params.putString("id", config.id)
|
|
570
|
-
params.putString("metadata", config.metadata)
|
|
571
|
-
val state = stateMap[status] ?: 0
|
|
572
|
-
params.putInt("state", state)
|
|
573
|
-
|
|
574
|
-
val bytesDownloaded = downloadStatus.getDouble("bytesDownloaded")
|
|
575
|
-
params.putDouble("bytesDownloaded", bytesDownloaded)
|
|
576
|
-
val bytesTotal = downloadStatus.getDouble("bytesTotal")
|
|
577
|
-
params.putDouble("bytesTotal", bytesTotal)
|
|
578
|
-
val percent = if (bytesTotal > 0) bytesDownloaded / bytesTotal else 0.0
|
|
579
|
-
|
|
580
|
-
foundTasks.pushMap(params)
|
|
581
|
-
configIdToDownloadId[config.id] = downloadId
|
|
582
|
-
configIdToPercent[config.id] = percent
|
|
583
|
-
}
|
|
584
|
-
} else if (downloadId != null) {
|
|
585
|
-
downloader.cancel(downloadId)
|
|
586
|
-
}
|
|
587
|
-
} while (cursor.moveToNext())
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
} catch (e: Exception) {
|
|
591
|
-
Log.e(NAME, "getExistingDownloadTasks: ${Log.getStackTraceString(e)}")
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
promise.resolve(foundTasks)
|
|
459
|
+
headers?.let {
|
|
460
|
+
val iterator = it.keySetIterator()
|
|
461
|
+
while (iterator.hasNextKey()) {
|
|
462
|
+
val headerKey = iterator.nextKey()
|
|
463
|
+
request.addRequestHeader(headerKey, it.getString(headerKey))
|
|
464
|
+
}
|
|
596
465
|
}
|
|
597
466
|
|
|
598
|
-
|
|
599
|
-
|
|
467
|
+
val uuid = (System.currentTimeMillis() and 0xfffffff).toInt()
|
|
468
|
+
val extension = MimeTypeMap.getFileExtensionFromUrl(destination)
|
|
469
|
+
val filename = "$uuid.$extension"
|
|
470
|
+
request.setDestinationInExternalFilesDir(reactContext, null, filename)
|
|
600
471
|
|
|
601
|
-
|
|
602
|
-
}
|
|
472
|
+
val downloadId = downloader.download(request)
|
|
473
|
+
val config = RNBGDTaskConfig(id, url, destination, metadata ?: "{}", notificationTitle)
|
|
603
474
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
475
|
+
synchronized(sharedLock) {
|
|
476
|
+
configIdToDownloadId[id] = downloadId
|
|
477
|
+
configIdToPercent[id] = 0.0
|
|
478
|
+
downloadIdToConfig[downloadId] = config
|
|
479
|
+
saveDownloadIdToConfigMap()
|
|
480
|
+
resumeTasks(downloadId, config)
|
|
610
481
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
val params = Arguments.createMap()
|
|
627
|
-
params.putString("id", configId)
|
|
628
|
-
params.putDouble("bytesDownloaded", bytesDownloaded.toDouble())
|
|
629
|
-
params.putDouble("bytesTotal", bytesTotal.toDouble())
|
|
630
|
-
progressReports[configId] = params
|
|
631
|
-
configIdToPercent[configId] = percent
|
|
632
|
-
configIdToLastBytes[configId] = bytesDownloaded
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Pause functionality is not supported by Android DownloadManager.
|
|
485
|
+
// This method will throw an UnsupportedOperationException to clearly indicate
|
|
486
|
+
// that pause is not available on Android platform.
|
|
487
|
+
fun pauseTask(configId: String) {
|
|
488
|
+
synchronized(sharedLock) {
|
|
489
|
+
val downloadId = configIdToDownloadId[configId]
|
|
490
|
+
if (downloadId != null) {
|
|
491
|
+
try {
|
|
492
|
+
downloader.pause(downloadId)
|
|
493
|
+
} catch (e: UnsupportedOperationException) {
|
|
494
|
+
Log.w("RNBackgroundDownloader", "pauseTask: ${e.message}")
|
|
495
|
+
// Note: We don't rethrow the exception to avoid crashing the JS thread.
|
|
496
|
+
// The limitation is already documented and expected.
|
|
633
497
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Resume functionality is not supported by Android DownloadManager.
|
|
503
|
+
// This method will throw an UnsupportedOperationException to clearly indicate
|
|
504
|
+
// that resume is not available on Android platform.
|
|
505
|
+
fun resumeTask(configId: String) {
|
|
506
|
+
synchronized(sharedLock) {
|
|
507
|
+
val downloadId = configIdToDownloadId[configId]
|
|
508
|
+
if (downloadId != null) {
|
|
509
|
+
try {
|
|
510
|
+
downloader.resume(downloadId)
|
|
511
|
+
} catch (e: UnsupportedOperationException) {
|
|
512
|
+
Log.w("RNBackgroundDownloader", "resumeTask: ${e.message}")
|
|
513
|
+
// Note: We don't rethrow the exception to avoid crashing the JS thread.
|
|
514
|
+
// The limitation is already documented and expected.
|
|
648
515
|
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fun stopTask(configId: String) {
|
|
521
|
+
synchronized(sharedLock) {
|
|
522
|
+
val downloadId = configIdToDownloadId[configId]
|
|
523
|
+
if (downloadId != null) {
|
|
524
|
+
stopTaskProgress(configId)
|
|
525
|
+
removeTaskFromMap(downloadId)
|
|
526
|
+
downloader.cancel(downloadId)
|
|
527
|
+
}
|
|
649
528
|
}
|
|
529
|
+
}
|
|
650
530
|
|
|
651
|
-
|
|
652
|
-
|
|
531
|
+
fun completeHandler(configId: String) {
|
|
532
|
+
// Firebase Performance compatibility: Add defensive programming to prevent crashes
|
|
533
|
+
// when Firebase Performance SDK is installed and uses bytecode instrumentation
|
|
653
534
|
|
|
654
|
-
|
|
655
|
-
// Maybe somewhere in downloadReceiver?
|
|
656
|
-
// Feedback if any error occurs after downloading the file.
|
|
657
|
-
try {
|
|
658
|
-
val future = setFileChangesBeforeCompletion(localUri!!, config.destination)
|
|
659
|
-
future.get()
|
|
660
|
-
} catch (e: Exception) {
|
|
661
|
-
val newDownloadStatus = Arguments.createMap()
|
|
662
|
-
newDownloadStatus.putString("downloadId", downloadStatus.getString("downloadId"))
|
|
663
|
-
newDownloadStatus.putInt("status", DownloadManager.STATUS_FAILED)
|
|
664
|
-
newDownloadStatus.putInt("reason", DownloadManager.ERROR_UNKNOWN)
|
|
665
|
-
newDownloadStatus.putString("reasonText", e.message)
|
|
666
|
-
onFailedDownload(config, newDownloadStatus)
|
|
667
|
-
return
|
|
668
|
-
}
|
|
535
|
+
Log.d(NAME, "completeHandler called with configId: $configId")
|
|
669
536
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
params.putDouble("bytesTotal", downloadStatus.getDouble("bytesTotal"))
|
|
675
|
-
ee.emit("downloadComplete", params)
|
|
537
|
+
// Defensive programming: Validate parameters
|
|
538
|
+
if (configId.isEmpty()) {
|
|
539
|
+
Log.w(NAME, "completeHandler: Invalid configId provided")
|
|
540
|
+
return
|
|
676
541
|
}
|
|
677
542
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
downloadStatus.getString("reasonText")
|
|
684
|
-
)
|
|
543
|
+
try {
|
|
544
|
+
// Currently this method doesn't have any implementation on Android
|
|
545
|
+
// as completion handlers are handled differently than iOS.
|
|
546
|
+
// This defensive structure ensures Firebase Performance compatibility.
|
|
547
|
+
Log.d(NAME, "completeHandler executed successfully for configId: $configId")
|
|
685
548
|
|
|
686
|
-
|
|
687
|
-
|
|
549
|
+
} catch (e: Exception) {
|
|
550
|
+
// Catch any potential exceptions that might be thrown due to Firebase Performance
|
|
551
|
+
// bytecode instrumentation interfering with method dispatch
|
|
552
|
+
Log.e(NAME, "completeHandler: Exception occurred: ${Log.getStackTraceString(e)}")
|
|
553
|
+
}
|
|
554
|
+
}
|
|
688
555
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
Log.w(
|
|
692
|
-
NAME, "ERROR_CANNOT_RESUME detected for download: ${config.id}" +
|
|
693
|
-
". This is a known Android DownloadManager issue with larger files. " +
|
|
694
|
-
"Consider restarting the download or using smaller file segments."
|
|
695
|
-
)
|
|
556
|
+
fun getExistingDownloadTasks(promise: Promise) {
|
|
557
|
+
val foundTasks = Arguments.createArray()
|
|
696
558
|
|
|
697
|
-
|
|
698
|
-
|
|
559
|
+
synchronized(sharedLock) {
|
|
560
|
+
val query = DownloadManager.Query()
|
|
561
|
+
try {
|
|
562
|
+
downloader.downloadManager.query(query)?.use { cursor ->
|
|
563
|
+
if (cursor.moveToFirst()) {
|
|
564
|
+
do {
|
|
565
|
+
val downloadStatus = downloader.getDownloadStatus(cursor)
|
|
566
|
+
val downloadId = downloadStatus.getString("downloadId")?.toLong()
|
|
699
567
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
"ERROR_CANNOT_RESUME - Unable to resume download. This may occur with large files due to Android DownloadManager limitations. Try restarting the download."
|
|
703
|
-
}
|
|
568
|
+
if (downloadId != null && downloadIdToConfig.containsKey(downloadId)) {
|
|
569
|
+
val config = downloadIdToConfig[downloadId]
|
|
704
570
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (isMMKVAvailable && mmkv != null) {
|
|
722
|
-
mmkv!!.encode("${NAME}_downloadIdToConfig", str)
|
|
723
|
-
Log.d(NAME, "Saved download config to MMKV")
|
|
724
|
-
} else {
|
|
725
|
-
sharedPreferences.edit()
|
|
726
|
-
.putString("${NAME}_downloadIdToConfig", str)
|
|
727
|
-
.apply()
|
|
728
|
-
Log.d(NAME, "Saved download config to SharedPreferences fallback")
|
|
729
|
-
}
|
|
730
|
-
} catch (e: Exception) {
|
|
731
|
-
Log.e(NAME, "Failed to save download config: ${e.message}")
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
571
|
+
if (config != null) {
|
|
572
|
+
val status = downloadStatus.getInt("status")
|
|
573
|
+
// Handle completed downloads that weren't processed
|
|
574
|
+
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
|
575
|
+
val localUri = downloadStatus.getString("localUri")
|
|
576
|
+
if (localUri != null) {
|
|
577
|
+
try {
|
|
578
|
+
val future = setFileChangesBeforeCompletion(localUri, config.destination)
|
|
579
|
+
future.get()
|
|
580
|
+
} catch (e: Exception) {
|
|
581
|
+
Log.e(NAME, "Error moving completed download file: ${e.message}")
|
|
582
|
+
// Continue with normal processing even if file move fails
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
735
586
|
|
|
736
|
-
|
|
737
|
-
synchronized(sharedLock) {
|
|
738
|
-
downloadIdToConfig = mutableMapOf()
|
|
587
|
+
val params = Arguments.createMap()
|
|
739
588
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
} else {
|
|
746
|
-
sharedPreferences.getString("${NAME}_downloadIdToConfig", null)?.also {
|
|
747
|
-
Log.d(NAME, "Loaded download config from SharedPreferences fallback")
|
|
748
|
-
}
|
|
749
|
-
}
|
|
589
|
+
params.putString("id", config.id)
|
|
590
|
+
params.putString("metadata", config.metadata)
|
|
591
|
+
val state = stateMap[status] ?: 0
|
|
592
|
+
params.putInt("state", state)
|
|
750
593
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
Log.d(NAME, "No existing download config found, starting with empty map")
|
|
757
|
-
}
|
|
758
|
-
} catch (e: Exception) {
|
|
759
|
-
Log.e(NAME, "Failed to load download config: ${e.message}")
|
|
760
|
-
downloadIdToConfig = mutableMapOf()
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
594
|
+
val bytesDownloaded = downloadStatus.getDouble("bytesDownloaded")
|
|
595
|
+
params.putDouble("bytesDownloaded", bytesDownloaded)
|
|
596
|
+
val bytesTotal = downloadStatus.getDouble("bytesTotal")
|
|
597
|
+
params.putDouble("bytesTotal", bytesTotal)
|
|
598
|
+
val percent = if (bytesTotal > 0) bytesDownloaded / bytesTotal else 0.0
|
|
764
599
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (isMMKVAvailable && mmkv != null) {
|
|
769
|
-
mmkv!!.encode("${NAME}_progressInterval", progressInterval)
|
|
770
|
-
mmkv!!.encode("${NAME}_progressMinBytes", progressMinBytes)
|
|
771
|
-
Log.d(NAME, "Saved config to MMKV")
|
|
772
|
-
} else {
|
|
773
|
-
sharedPreferences.edit()
|
|
774
|
-
.putInt("${NAME}_progressInterval", progressInterval)
|
|
775
|
-
.putLong("${NAME}_progressMinBytes", progressMinBytes)
|
|
776
|
-
.apply()
|
|
777
|
-
Log.d(NAME, "Saved config to SharedPreferences fallback")
|
|
600
|
+
foundTasks.pushMap(params)
|
|
601
|
+
configIdToDownloadId[config.id] = downloadId
|
|
602
|
+
configIdToPercent[config.id] = percent
|
|
778
603
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
604
|
+
} else if (downloadId != null) {
|
|
605
|
+
downloader.cancel(downloadId)
|
|
606
|
+
}
|
|
607
|
+
} while (cursor.moveToNext())
|
|
608
|
+
}
|
|
782
609
|
}
|
|
610
|
+
} catch (e: Exception) {
|
|
611
|
+
Log.e(NAME, "getExistingDownloadTasks: ${Log.getStackTraceString(e)}")
|
|
612
|
+
}
|
|
783
613
|
}
|
|
784
614
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
615
|
+
promise.resolve(foundTasks)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
fun addListener(eventName: String) {
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
fun removeListeners(count: Int) {
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private fun onBeginDownload(configId: String, headers: WritableMap, expectedBytes: Long) {
|
|
625
|
+
val params = Arguments.createMap()
|
|
626
|
+
params.putString("id", configId)
|
|
627
|
+
params.putMap("headers", headers)
|
|
628
|
+
params.putDouble("expectedBytes", expectedBytes.toDouble())
|
|
629
|
+
ee.emit("downloadBegin", params)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private fun onProgressDownload(configId: String, bytesDownloaded: Long, bytesTotal: Long) {
|
|
633
|
+
val existPercent = configIdToPercent[configId]
|
|
634
|
+
val existLastBytes = configIdToLastBytes[configId]
|
|
635
|
+
val prevPercent = existPercent ?: 0.0
|
|
636
|
+
val prevBytes = existLastBytes ?: 0L
|
|
637
|
+
val percent = if (bytesTotal > 0.0) bytesDownloaded.toDouble() / bytesTotal else 0.0
|
|
638
|
+
|
|
639
|
+
// Check if we should report progress based on percentage OR bytes threshold
|
|
640
|
+
val percentThresholdMet = percent - prevPercent > PROGRESS_REPORT_THRESHOLD
|
|
641
|
+
// Only check bytes threshold if progressMinBytes > 0
|
|
642
|
+
val bytesThresholdMet = progressMinBytes > 0 && (bytesDownloaded - prevBytes >= progressMinBytes)
|
|
643
|
+
|
|
644
|
+
// Report progress if either threshold is met, or if total bytes unknown (for realtime streams)
|
|
645
|
+
if (percentThresholdMet || bytesThresholdMet || bytesTotal <= 0) {
|
|
646
|
+
val params = Arguments.createMap()
|
|
647
|
+
params.putString("id", configId)
|
|
648
|
+
params.putDouble("bytesDownloaded", bytesDownloaded.toDouble())
|
|
649
|
+
params.putDouble("bytesTotal", bytesTotal.toDouble())
|
|
650
|
+
progressReports[configId] = params
|
|
651
|
+
configIdToPercent[configId] = percent
|
|
652
|
+
configIdToLastBytes[configId] = bytesDownloaded
|
|
813
653
|
}
|
|
814
654
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
655
|
+
val now = Date()
|
|
656
|
+
val isReportTimeDifference = now.time - lastProgressReportedAt.time > progressInterval
|
|
657
|
+
val isReportNotEmpty = progressReports.isNotEmpty()
|
|
658
|
+
if (isReportTimeDifference && isReportNotEmpty) {
|
|
659
|
+
// Extra steps to avoid map always consumed errors.
|
|
660
|
+
val reportsList = progressReports.values.toList()
|
|
661
|
+
val reportsArray = Arguments.createArray()
|
|
662
|
+
for (report in reportsList) {
|
|
663
|
+
reportsArray.pushMap(report.copy())
|
|
664
|
+
}
|
|
665
|
+
ee.emit("downloadProgress", reportsArray)
|
|
666
|
+
lastProgressReportedAt = now
|
|
667
|
+
progressReports.clear()
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private fun onSuccessfulDownload(config: RNBGDTaskConfig, downloadStatus: WritableMap) {
|
|
672
|
+
val localUri = downloadStatus.getString("localUri")
|
|
673
|
+
|
|
674
|
+
// TODO: We need to move it to a more suitable location.
|
|
675
|
+
// Maybe somewhere in downloadReceiver?
|
|
676
|
+
// Feedback if any error occurs after downloading the file.
|
|
677
|
+
try {
|
|
678
|
+
val future = setFileChangesBeforeCompletion(localUri!!, config.destination)
|
|
679
|
+
future.get()
|
|
680
|
+
} catch (e: Exception) {
|
|
681
|
+
val newDownloadStatus = Arguments.createMap()
|
|
682
|
+
newDownloadStatus.putString("downloadId", downloadStatus.getString("downloadId"))
|
|
683
|
+
newDownloadStatus.putInt("status", DownloadManager.STATUS_FAILED)
|
|
684
|
+
newDownloadStatus.putInt("reason", DownloadManager.ERROR_UNKNOWN)
|
|
685
|
+
newDownloadStatus.putString("reasonText", e.message)
|
|
686
|
+
onFailedDownload(config, newDownloadStatus)
|
|
687
|
+
return
|
|
823
688
|
}
|
|
824
689
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
690
|
+
val params = Arguments.createMap()
|
|
691
|
+
params.putString("id", config.id)
|
|
692
|
+
params.putString("location", config.destination)
|
|
693
|
+
params.putDouble("bytesDownloaded", downloadStatus.getDouble("bytesDownloaded"))
|
|
694
|
+
params.putDouble("bytesTotal", downloadStatus.getDouble("bytesTotal"))
|
|
695
|
+
ee.emit("downloadComplete", params)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private fun onFailedDownload(config: RNBGDTaskConfig, downloadStatus: WritableMap) {
|
|
699
|
+
Log.e(
|
|
700
|
+
NAME, "onFailedDownload: " +
|
|
701
|
+
"${downloadStatus.getInt("status")}:" +
|
|
702
|
+
"${downloadStatus.getInt("reason")}:" +
|
|
703
|
+
downloadStatus.getString("reasonText")
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
val reason = downloadStatus.getInt("reason")
|
|
707
|
+
var reasonText = downloadStatus.getString("reasonText")
|
|
708
|
+
|
|
709
|
+
// Enhanced handling for ERROR_CANNOT_RESUME (1008)
|
|
710
|
+
if (reason == DownloadManager.ERROR_CANNOT_RESUME) {
|
|
711
|
+
Log.w(
|
|
712
|
+
NAME, "ERROR_CANNOT_RESUME detected for download: ${config.id}" +
|
|
713
|
+
". This is a known Android DownloadManager issue with larger files. " +
|
|
714
|
+
"Consider restarting the download or using smaller file segments."
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
// Clean up the failed download entry
|
|
718
|
+
removeTaskFromMap(downloadStatus.getString("downloadId")?.toLong() ?: 0L)
|
|
719
|
+
|
|
720
|
+
// Provide more helpful error message
|
|
721
|
+
reasonText =
|
|
722
|
+
"ERROR_CANNOT_RESUME - Unable to resume download. This may occur with large files due to Android DownloadManager limitations. Try restarting the download."
|
|
723
|
+
}
|
|
842
724
|
|
|
843
|
-
|
|
844
|
-
|
|
725
|
+
val params = Arguments.createMap()
|
|
726
|
+
params.putString("id", config.id)
|
|
727
|
+
params.putInt("errorCode", reason)
|
|
728
|
+
params.putString("error", reasonText)
|
|
729
|
+
ee.emit("downloadFailed", params)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private fun saveDownloadIdToConfigMap() {
|
|
733
|
+
synchronized(sharedLock) {
|
|
734
|
+
try {
|
|
735
|
+
val gson = Gson()
|
|
736
|
+
// Create a defensive copy to prevent ConcurrentModificationException
|
|
737
|
+
// when Gson iterates over the map while another thread modifies it
|
|
738
|
+
val mapCopy = HashMap(downloadIdToConfig)
|
|
739
|
+
val str = gson.toJson(mapCopy)
|
|
740
|
+
|
|
741
|
+
if (isMMKVAvailable && mmkv != null) {
|
|
742
|
+
mmkv!!.encode("${NAME}_downloadIdToConfig", str)
|
|
743
|
+
Log.d(NAME, "Saved download config to MMKV")
|
|
744
|
+
} else {
|
|
745
|
+
sharedPreferences.edit()
|
|
746
|
+
.putString("${NAME}_downloadIdToConfig", str)
|
|
747
|
+
.apply()
|
|
748
|
+
Log.d(NAME, "Saved download config to SharedPreferences fallback")
|
|
749
|
+
}
|
|
750
|
+
} catch (e: Exception) {
|
|
751
|
+
Log.e(NAME, "Failed to save download config: ${e.message}")
|
|
752
|
+
}
|
|
845
753
|
}
|
|
754
|
+
}
|
|
846
755
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
|
|
756
|
+
private fun loadDownloadIdToConfigMap() {
|
|
757
|
+
synchronized(sharedLock) {
|
|
758
|
+
downloadIdToConfig = mutableMapOf()
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
val str = if (isMMKVAvailable && mmkv != null) {
|
|
762
|
+
mmkv!!.decodeString("${NAME}_downloadIdToConfig")?.also {
|
|
763
|
+
Log.d(NAME, "Loaded download config from MMKV")
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
sharedPreferences.getString("${NAME}_downloadIdToConfig", null)?.also {
|
|
767
|
+
Log.d(NAME, "Loaded download config from SharedPreferences fallback")
|
|
768
|
+
}
|
|
854
769
|
}
|
|
855
770
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
771
|
+
if (str != null) {
|
|
772
|
+
val gson = Gson()
|
|
773
|
+
val mapType = object : TypeToken<Map<Long, RNBGDTaskConfig>>() {}.type
|
|
774
|
+
downloadIdToConfig = gson.fromJson(str, mapType)
|
|
775
|
+
} else {
|
|
776
|
+
Log.d(NAME, "No existing download config found, starting with empty map")
|
|
862
777
|
}
|
|
778
|
+
} catch (e: Exception) {
|
|
779
|
+
Log.e(NAME, "Failed to load download config: ${e.message}")
|
|
780
|
+
downloadIdToConfig = mutableMapOf()
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private fun saveConfigMap() {
|
|
786
|
+
synchronized(sharedLock) {
|
|
787
|
+
try {
|
|
788
|
+
if (isMMKVAvailable && mmkv != null) {
|
|
789
|
+
mmkv!!.encode("${NAME}_progressInterval", progressInterval)
|
|
790
|
+
mmkv!!.encode("${NAME}_progressMinBytes", progressMinBytes)
|
|
791
|
+
Log.d(NAME, "Saved config to MMKV")
|
|
792
|
+
} else {
|
|
793
|
+
sharedPreferences.edit()
|
|
794
|
+
.putInt("${NAME}_progressInterval", progressInterval)
|
|
795
|
+
.putLong("${NAME}_progressMinBytes", progressMinBytes)
|
|
796
|
+
.apply()
|
|
797
|
+
Log.d(NAME, "Saved config to SharedPreferences fallback")
|
|
798
|
+
}
|
|
799
|
+
} catch (e: Exception) {
|
|
800
|
+
Log.e(NAME, "Failed to save config: ${e.message}")
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private fun loadConfigMap() {
|
|
806
|
+
synchronized(sharedLock) {
|
|
807
|
+
try {
|
|
808
|
+
if (isMMKVAvailable && mmkv != null) {
|
|
809
|
+
val progressIntervalScope = mmkv!!.decodeInt("${NAME}_progressInterval")
|
|
810
|
+
if (progressIntervalScope > 0) {
|
|
811
|
+
progressInterval = progressIntervalScope
|
|
812
|
+
}
|
|
813
|
+
val progressMinBytesScope = mmkv!!.decodeLong("${NAME}_progressMinBytes")
|
|
814
|
+
if (progressMinBytesScope > 0) {
|
|
815
|
+
progressMinBytes = progressMinBytesScope
|
|
816
|
+
}
|
|
817
|
+
Log.d(NAME, "Loaded config from MMKV")
|
|
818
|
+
} else {
|
|
819
|
+
val progressIntervalScope = sharedPreferences.getInt("${NAME}_progressInterval", 0)
|
|
820
|
+
if (progressIntervalScope > 0) {
|
|
821
|
+
progressInterval = progressIntervalScope
|
|
822
|
+
}
|
|
823
|
+
val progressMinBytesScope = sharedPreferences.getLong("${NAME}_progressMinBytes", 0)
|
|
824
|
+
if (progressMinBytesScope > 0) {
|
|
825
|
+
progressMinBytes = progressMinBytesScope
|
|
826
|
+
}
|
|
827
|
+
Log.d(NAME, "Loaded config from SharedPreferences fallback")
|
|
828
|
+
}
|
|
829
|
+
} catch (e: Exception) {
|
|
830
|
+
Log.e(NAME, "Failed to load config: ${e.message}")
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private fun stopTaskProgress(configId: String) {
|
|
836
|
+
val onProgressFuture = configIdToProgressFuture[configId]
|
|
837
|
+
if (onProgressFuture != null) {
|
|
838
|
+
onProgressFuture.cancel(true)
|
|
839
|
+
configIdToPercent.remove(configId)
|
|
840
|
+
configIdToLastBytes.remove(configId)
|
|
841
|
+
configIdToProgressFuture.remove(configId)
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
private fun setFileChangesBeforeCompletion(targetSrc: String, destinationSrc: String): Future<Boolean> {
|
|
846
|
+
return fixedExecutorPool.submit<Boolean> {
|
|
847
|
+
val file = File(targetSrc)
|
|
848
|
+
val destination = File(destinationSrc)
|
|
849
|
+
var destinationParent: File? = null
|
|
850
|
+
try {
|
|
851
|
+
if (file.exists()) {
|
|
852
|
+
FileUtils.rm(destination)
|
|
853
|
+
destinationParent = FileUtils.mkdirParent(destination)
|
|
854
|
+
FileUtils.mv(file, destination)
|
|
855
|
+
}
|
|
856
|
+
} catch (e: Exception) {
|
|
857
|
+
FileUtils.rm(file)
|
|
858
|
+
FileUtils.rm(destination)
|
|
859
|
+
FileUtils.rm(destinationParent)
|
|
860
|
+
throw Exception(e)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
true
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Check if the provided headers already contain a User-Agent header
|
|
869
|
+
* (case-insensitive)
|
|
870
|
+
*/
|
|
871
|
+
private fun hasUserAgentHeader(headers: ReadableMap?): Boolean {
|
|
872
|
+
if (headers == null) {
|
|
873
|
+
return false
|
|
874
|
+
}
|
|
863
875
|
|
|
864
|
-
|
|
876
|
+
val iterator = headers.keySetIterator()
|
|
877
|
+
while (iterator.hasNextKey()) {
|
|
878
|
+
val headerKey = iterator.nextKey()
|
|
879
|
+
if (headerKey.lowercase() == "user-agent") {
|
|
880
|
+
return true
|
|
881
|
+
}
|
|
865
882
|
}
|
|
883
|
+
|
|
884
|
+
return false
|
|
885
|
+
}
|
|
866
886
|
}
|