@kesha-antonov/react-native-background-downloader 4.5.4 → 4.5.6
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.
- package/README.md +45 -0
- package/android/src/main/java/com/eko/DownloadConstants.kt +3 -0
- package/android/src/main/java/com/eko/Downloader.kt +92 -0
- package/android/src/main/java/com/eko/RNBackgroundDownloaderModuleImpl.kt +79 -5
- package/android/src/main/java/com/eko/ResumableDownloader.kt +7 -0
- package/android/src/main/java/com/eko/utils/StorageManager.kt +95 -0
- package/ios/RNBGDTaskConfig.h +3 -0
- package/ios/RNBGDTaskConfig.mm +3 -0
- package/ios/RNBackgroundDownloader.mm +329 -45
- package/lib/DownloadTask.d.ts +39 -0
- package/lib/DownloadTask.js +176 -0
- package/lib/NativeRNBackgroundDownloader.d.ts +127 -0
- package/lib/NativeRNBackgroundDownloader.js +4 -0
- package/lib/UploadTask.d.ts +30 -0
- package/lib/UploadTask.js +174 -0
- package/lib/config.d.ts +33 -0
- package/lib/config.js +79 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +498 -0
- package/lib/types.d.ts +249 -0
- package/lib/types.js +2 -0
- package/package.json +8 -5
- package/plugin/package.json +6 -0
- package/react-native-background-downloader.podspec +8 -2
- package/src/DownloadTask.ts +2 -0
- package/src/NativeRNBackgroundDownloader.ts +2 -0
- package/src/config.ts +6 -1
- package/src/index.ts +4 -0
- package/src/types.ts +27 -0
- package/CHANGELOG.md +0 -294
- package/example/android/app/debug.keystore +0 -0
- package/ios/.DS_Store +0 -0
package/README.md
CHANGED
|
@@ -523,6 +523,39 @@ const task = createDownloadTask({
|
|
|
523
523
|
|
|
524
524
|
</details>
|
|
525
525
|
|
|
526
|
+
<details>
|
|
527
|
+
<summary><strong>iOS Data Protection (downloading while the device is locked)</strong></summary>
|
|
528
|
+
|
|
529
|
+
On iOS, files protected with `NSFileProtectionComplete` cannot be written while the device is locked. Since a background download can finish while the screen is locked, the library saves files with **`completeUntilFirstUserAuthentication`** by default - writable while locked after the first unlock since boot - and, if a save still can't happen because the device is locked, it stages the bytes and finishes the save (emitting `complete`) when the device is next unlocked.
|
|
530
|
+
|
|
531
|
+
You usually don't need to change this. If your app has stricter security requirements you can raise the protection level globally or per task (iOS only - ignored on Android):
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
import { setConfig, createDownloadTask, directories } from '@kesha-antonov/react-native-background-downloader'
|
|
535
|
+
|
|
536
|
+
// Global default for all downloads
|
|
537
|
+
setConfig({
|
|
538
|
+
iosDataProtection: 'completeUntilFirstUserAuthentication', // default
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
// Per-download override
|
|
542
|
+
const task = createDownloadTask({
|
|
543
|
+
id: 'secret-doc',
|
|
544
|
+
url: 'https://example.com/secret.pdf',
|
|
545
|
+
destination: `${directories.documents}/secret.pdf`,
|
|
546
|
+
iosDataProtection: 'complete', // strongest; note: won't be writable while locked before first unlock
|
|
547
|
+
})
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
| Value | NSFileProtection level | Notes |
|
|
551
|
+
|-------|------------------------|-------|
|
|
552
|
+
| `'completeUntilFirstUserAuthentication'` | `NSFileProtectionCompleteUntilFirstUserAuthentication` | **Default.** Accessible after the first unlock since boot - recommended for background downloads |
|
|
553
|
+
| `'complete'` | `NSFileProtectionComplete` | Strongest. File is inaccessible whenever the device is locked |
|
|
554
|
+
| `'completeUnlessOpen'` | `NSFileProtectionCompleteUnlessOpen` | Accessible while open, even if the device locks afterward |
|
|
555
|
+
| `'none'` | `NSFileProtectionNone` | No protection |
|
|
556
|
+
|
|
557
|
+
</details>
|
|
558
|
+
|
|
526
559
|
<details>
|
|
527
560
|
<summary><strong>Enabling debug logs</strong></summary>
|
|
528
561
|
|
|
@@ -865,6 +898,18 @@ If you're using `react-native-mmkv`, you don't need to add the MMKV dependency m
|
|
|
865
898
|
This was fixed in v4.4.0. Update to the latest version. If you're not using `react-native-mmkv`, add `pod 'MMKV', '>= 1.0.0'` to your Podfile.
|
|
866
899
|
</details>
|
|
867
900
|
|
|
901
|
+
<details>
|
|
902
|
+
<summary><strong>Downloads "don't work" when the iOS screen is locked</strong></summary>
|
|
903
|
+
|
|
904
|
+
On iOS the actual transfer is handed to the system `nsurlsessiond` daemon and **keeps running while the app is suspended and the screen is locked** - this library already configures the background session correctly (`discretionary = NO`, `sessionSendsLaunchEvents = YES`). If it looks like downloads stop when locked, check these in order:
|
|
905
|
+
|
|
906
|
+
1. **Did you force-quit the app?** If the user swipes the app away in the app switcher, iOS halts *that app's* background transfers until it's relaunched. This is an OS policy and cannot be worked around by any library.
|
|
907
|
+
2. **You won't get live `progress` events while locked.** Your JavaScript isn't running while the app is suspended, so `begin` / `progress` / `complete` callbacks fire when the app resumes or is relaunched - not in real time. Re-attach with `getExistingDownloadTasks()` on launch.
|
|
908
|
+
3. **AppDelegate wiring.** Make sure `handleEventsForBackgroundURLSession` is implemented (see [Installation](#-installation)) and that you call `completeHandler(jobId)` from JS in your `complete`/`error` handlers, or iOS will throttle future background time.
|
|
909
|
+
4. **Data Protection (the one this library handles).** On a passcoded, locked device, files protected with `NSFileProtectionComplete` can't be written, so a download that *finishes* while locked could previously fail to save. The library now writes the file with `NSFileProtectionCompleteUntilFirstUserAuthentication` by default (writable while locked after the first unlock since boot), and if the move still can't happen because the device is locked it stages the bytes and completes the save automatically when the device is next unlocked - emitting `complete` then. You can change the level with `setConfig({ iosDataProtection })` or per task (see Advanced Configuration).
|
|
910
|
+
5. **Test on a real device.** The iOS Simulator's background-transfer behavior is unreliable; verify on hardware.
|
|
911
|
+
</details>
|
|
912
|
+
|
|
868
913
|
<details>
|
|
869
914
|
<summary><strong>Downloads not resuming after app restart</strong></summary>
|
|
870
915
|
|
|
@@ -34,6 +34,9 @@ object DownloadConstants {
|
|
|
34
34
|
/** Interval for throttling progress log messages (milliseconds) */
|
|
35
35
|
const val PROGRESS_LOG_INTERVAL_MS = 500L
|
|
36
36
|
|
|
37
|
+
/** Interval for persisting in-progress resumable download recovery snapshots (milliseconds) */
|
|
38
|
+
const val RECOVERY_SNAPSHOT_INTERVAL_MS = 2_000L
|
|
39
|
+
|
|
37
40
|
// ========== HTTP Headers ==========
|
|
38
41
|
|
|
39
42
|
/** Keep-Alive header value for connection pooling */
|
|
@@ -95,6 +95,9 @@ class Downloader(private val context: Context, private val storageManager: com.e
|
|
|
95
95
|
pausedDownloads.putAll(loadedPaused)
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
// Recover any resumable downloads that were active when the app was last
|
|
99
|
+
// force-stopped (their foreground service and in-memory state were lost).
|
|
100
|
+
restoreRecoverableDownloads()
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
private fun bindToService() {
|
|
@@ -471,6 +474,95 @@ class Downloader(private val context: Context, private val storageManager: com.e
|
|
|
471
474
|
return pausedDownloads.toMap()
|
|
472
475
|
}
|
|
473
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Get all in-progress resumable downloads tracked by the foreground service.
|
|
479
|
+
* On Android 14+ active downloads run as UIDT jobs instead and are not included here.
|
|
480
|
+
*/
|
|
481
|
+
fun getActiveResumableDownloads(): Map<String, ResumableDownloader.DownloadState> {
|
|
482
|
+
return resumableDownloader.getActiveDownloads()
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Persist a recovery snapshot for an in-progress resumable download so it can be
|
|
487
|
+
* recovered after a force-stop. Called throttled from progress callbacks.
|
|
488
|
+
* The byte counter stored here is advisory only - on recovery the resume offset
|
|
489
|
+
* is recomputed from the actual on-disk file length to avoid corruption.
|
|
490
|
+
*/
|
|
491
|
+
fun saveActiveDownloadSnapshot(
|
|
492
|
+
configId: String,
|
|
493
|
+
url: String,
|
|
494
|
+
destination: String,
|
|
495
|
+
headers: Map<String, String>,
|
|
496
|
+
bytesDownloaded: Long,
|
|
497
|
+
bytesTotal: Long,
|
|
498
|
+
metadata: String = "{}"
|
|
499
|
+
) {
|
|
500
|
+
storageManager?.saveActiveDownload(
|
|
501
|
+
PausedDownloadInfo(
|
|
502
|
+
configId = configId,
|
|
503
|
+
url = url,
|
|
504
|
+
destination = destination,
|
|
505
|
+
headers = headers,
|
|
506
|
+
bytesDownloaded = bytesDownloaded,
|
|
507
|
+
bytesTotal = bytesTotal,
|
|
508
|
+
metadata = metadata
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Remove the recovery snapshot for a download (on complete, error, pause or stop).
|
|
515
|
+
*/
|
|
516
|
+
fun removeActiveDownloadSnapshot(configId: String) {
|
|
517
|
+
storageManager?.removeActiveDownload(configId)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Recover resumable downloads that were active when the app was force-stopped.
|
|
522
|
+
* Moves their snapshots into the paused-downloads map (so getExistingDownloadTasks
|
|
523
|
+
* surfaces them and resumeTask works), recomputing the resume offset from the
|
|
524
|
+
* partial file's actual length. Snapshots without a partial file on disk are dropped.
|
|
525
|
+
*/
|
|
526
|
+
private fun restoreRecoverableDownloads() {
|
|
527
|
+
val sm = storageManager ?: return
|
|
528
|
+
val recoverable = sm.loadActiveDownloads()
|
|
529
|
+
if (recoverable.isEmpty()) {
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
var recoveredCount = 0
|
|
534
|
+
for ((configId, info) in recoverable) {
|
|
535
|
+
// Skip if it is already tracked as paused, or still running as a UIDT job.
|
|
536
|
+
if (pausedDownloads.containsKey(configId)) {
|
|
537
|
+
continue
|
|
538
|
+
}
|
|
539
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
|
|
540
|
+
UIDTDownloadJobService.isActiveJob(configId)
|
|
541
|
+
) {
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
val destFile = File(info.destination)
|
|
546
|
+
val fileLength = if (destFile.exists()) destFile.length() else 0L
|
|
547
|
+
if (fileLength <= 0L) {
|
|
548
|
+
// Nothing downloaded yet - nothing to resume from.
|
|
549
|
+
continue
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Use the on-disk length as the authoritative resume offset.
|
|
553
|
+
pausedDownloads[configId] = info.copy(bytesDownloaded = fileLength)
|
|
554
|
+
recoveredCount++
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (recoveredCount > 0) {
|
|
558
|
+
savePausedDownloads()
|
|
559
|
+
RNBackgroundDownloaderModuleImpl.logD(TAG, "Recovered $recoveredCount interrupted resumable download(s) after force-stop")
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Clear the active store now that recovery has been processed.
|
|
563
|
+
sm.clearActiveDownloads()
|
|
564
|
+
}
|
|
565
|
+
|
|
474
566
|
/**
|
|
475
567
|
* Persist paused downloads to storage.
|
|
476
568
|
*/
|
|
@@ -160,6 +160,10 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
160
160
|
// Map to store metadata for paused downloads
|
|
161
161
|
private val configIdToMetadata = mutableMapOf<String, String>()
|
|
162
162
|
|
|
163
|
+
// Last time (ms) a recovery snapshot was persisted for a resumable download,
|
|
164
|
+
// used to throttle snapshot writes from the frequent progress callbacks.
|
|
165
|
+
private val configIdToLastSnapshotMs = mutableMapOf<String, Long>()
|
|
166
|
+
|
|
163
167
|
/**
|
|
164
168
|
* Get the event emitter, ensuring it's initialized.
|
|
165
169
|
* Returns null if the module hasn't been initialized yet.
|
|
@@ -222,6 +226,9 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
222
226
|
|
|
223
227
|
override fun onProgress(id: String, bytesDownloaded: Long, bytesTotal: Long) {
|
|
224
228
|
onProgressDownload(id, bytesDownloaded, bytesTotal)
|
|
229
|
+
// Persist a recovery snapshot so this download can be recovered if the app
|
|
230
|
+
// is force-stopped (which kills the foreground service and its in-memory state).
|
|
231
|
+
maybeSnapshotActiveResumable(id, bytesDownloaded, bytesTotal)
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
override fun onComplete(id: String, location: String, bytesDownloaded: Long, bytesTotal: Long) {
|
|
@@ -515,7 +522,10 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
515
522
|
configIdToDownloadId.remove(configId)
|
|
516
523
|
configIdToHeaders.remove(configId)
|
|
517
524
|
configIdToMetadata.remove(configId)
|
|
525
|
+
configIdToLastSnapshotMs.remove(configId)
|
|
518
526
|
progressReporter.clearDownloadState(configId)
|
|
527
|
+
// Drop any force-stop recovery snapshot for this download.
|
|
528
|
+
downloader.removeActiveDownloadSnapshot(configId)
|
|
519
529
|
|
|
520
530
|
if (downloadId != null) {
|
|
521
531
|
downloadIdToConfig.remove(downloadId)
|
|
@@ -527,6 +537,42 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
527
537
|
}
|
|
528
538
|
}
|
|
529
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Persist a recovery snapshot for an in-progress resumable download, throttled so
|
|
542
|
+
* the frequent progress callbacks don't write to storage on every buffer read.
|
|
543
|
+
* If the app is force-stopped, restoreRecoverableDownloads() uses this snapshot to
|
|
544
|
+
* surface the download again via getExistingDownloadTasks.
|
|
545
|
+
*/
|
|
546
|
+
private fun maybeSnapshotActiveResumable(configId: String, bytesDownloaded: Long, bytesTotal: Long) {
|
|
547
|
+
val now = System.currentTimeMillis()
|
|
548
|
+
val last = configIdToLastSnapshotMs[configId] ?: 0L
|
|
549
|
+
if (now - last < DownloadConstants.RECOVERY_SNAPSHOT_INTERVAL_MS) {
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
configIdToLastSnapshotMs[configId] = now
|
|
553
|
+
|
|
554
|
+
// The active state lives in the foreground-service ResumableDownloader (Android < 14)
|
|
555
|
+
// or, on Android 14+, in the UIDT job. Check both so force-stop recovery works on
|
|
556
|
+
// every supported version.
|
|
557
|
+
val state = downloader.getActiveResumableDownloads()[configId]
|
|
558
|
+
?: (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
559
|
+
UIDTDownloadJobService.getJobDownloadState(configId)
|
|
560
|
+
else null)
|
|
561
|
+
?: return
|
|
562
|
+
val headers = configIdToHeaders[configId] ?: state.headers
|
|
563
|
+
val metadata = configIdToMetadata[configId] ?: "{}"
|
|
564
|
+
|
|
565
|
+
downloader.saveActiveDownloadSnapshot(
|
|
566
|
+
configId = configId,
|
|
567
|
+
url = state.url,
|
|
568
|
+
destination = state.destination,
|
|
569
|
+
headers = headers,
|
|
570
|
+
bytesDownloaded = bytesDownloaded,
|
|
571
|
+
bytesTotal = bytesTotal,
|
|
572
|
+
metadata = metadata
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
530
576
|
/**
|
|
531
577
|
* Resolve redirects for a URL up to maxRedirects limit
|
|
532
578
|
* @param originalUrl The original URL to follow
|
|
@@ -841,6 +887,10 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
841
887
|
metadata = metadata
|
|
842
888
|
)
|
|
843
889
|
|
|
890
|
+
// Now tracked as an explicit paused download - drop the recovery snapshot.
|
|
891
|
+
downloader.removeActiveDownloadSnapshot(configId)
|
|
892
|
+
configIdToLastSnapshotMs.remove(configId)
|
|
893
|
+
|
|
844
894
|
logD(NAME, "Paused resumable download: $configId (saved state: ${state.bytesDownloaded.get()}/${state.bytesTotal} bytes)")
|
|
845
895
|
}
|
|
846
896
|
return
|
|
@@ -1087,11 +1137,35 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
1087
1137
|
processedIds.add(configId)
|
|
1088
1138
|
}
|
|
1089
1139
|
|
|
1090
|
-
// Phase 4: Add active resumable downloads (in-progress via ResumableDownloader)
|
|
1091
|
-
|
|
1092
|
-
//
|
|
1093
|
-
// (
|
|
1094
|
-
//
|
|
1140
|
+
// Phase 4: Add active resumable downloads (in-progress via ResumableDownloader).
|
|
1141
|
+
// These run in the foreground service (Android < 14) and are invisible to
|
|
1142
|
+
// DownloadManager, so without this phase getExistingDownloadTasks misses them
|
|
1143
|
+
// while they are actively downloading (it only saw them once paused, via Phase 3).
|
|
1144
|
+
// On Android 14+ active downloads run as UIDT jobs and are surfaced in Phase 2.
|
|
1145
|
+
for ((configId, state) in downloader.getActiveResumableDownloads()) {
|
|
1146
|
+
if (processedIds.contains(configId) || state.isCancelled.get()) {
|
|
1147
|
+
continue
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
val params = Arguments.createMap()
|
|
1151
|
+
params.putString("id", configId)
|
|
1152
|
+
params.putString("metadata", configIdToMetadata[configId] ?: "{}")
|
|
1153
|
+
params.putInt(
|
|
1154
|
+
"state",
|
|
1155
|
+
if (state.isPaused.get()) DownloadConstants.TASK_SUSPENDED else DownloadConstants.TASK_RUNNING
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
val bytesDownloaded = state.bytesDownloaded.get()
|
|
1159
|
+
val bytesTotal = state.bytesTotal
|
|
1160
|
+
params.putDouble("bytesDownloaded", bytesDownloaded.toDouble())
|
|
1161
|
+
params.putDouble("bytesTotal", bytesTotal.toDouble())
|
|
1162
|
+
params.putString("destination", state.destination)
|
|
1163
|
+
|
|
1164
|
+
val percent = if (bytesTotal > 0) bytesDownloaded.toDouble() / bytesTotal else 0.0
|
|
1165
|
+
foundTasks.pushMap(params)
|
|
1166
|
+
processedIds.add(configId)
|
|
1167
|
+
progressReporter.setPercent(configId, percent)
|
|
1168
|
+
}
|
|
1095
1169
|
|
|
1096
1170
|
logD(NAME, "getExistingDownloadTasks: found ${foundTasks.size()} tasks")
|
|
1097
1171
|
}
|
|
@@ -218,6 +218,13 @@ class ResumableDownloader {
|
|
|
218
218
|
|
|
219
219
|
fun getState(id: String): DownloadState? = activeDownloads[id]
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Returns a snapshot of all currently tracked download states.
|
|
223
|
+
* Used by getExistingDownloadTasks to surface in-progress resumable downloads
|
|
224
|
+
* (which are not visible to DownloadManager).
|
|
225
|
+
*/
|
|
226
|
+
fun getActiveDownloads(): Map<String, DownloadState> = activeDownloads.toMap()
|
|
227
|
+
|
|
221
228
|
fun isPaused(id: String): Boolean = activeDownloads[id]?.isPaused?.get() ?: false
|
|
222
229
|
|
|
223
230
|
fun getBytesDownloaded(id: String): Long = activeDownloads[id]?.bytesDownloaded?.get() ?: 0
|
|
@@ -20,6 +20,7 @@ class StorageManager(context: Context, private val name: String) {
|
|
|
20
20
|
private const val TAG = "StorageManager"
|
|
21
21
|
private const val KEY_DOWNLOAD_ID_TO_CONFIG = "_downloadIdToConfig"
|
|
22
22
|
private const val KEY_PAUSED_DOWNLOADS = "_pausedDownloads"
|
|
23
|
+
private const val KEY_ACTIVE_DOWNLOADS = "_activeDownloads"
|
|
23
24
|
private const val KEY_UPLOAD_CONFIGS = "_uploadConfigs"
|
|
24
25
|
private const val KEY_PROGRESS_INTERVAL = "_progressInterval"
|
|
25
26
|
private const val KEY_PROGRESS_MIN_BYTES = "_progressMinBytes"
|
|
@@ -280,6 +281,100 @@ class StorageManager(context: Context, private val name: String) {
|
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Save the set of in-progress resumable downloads ("recovery snapshots").
|
|
286
|
+
* These let getExistingDownloadTasks recover an active download after the app
|
|
287
|
+
* is force-stopped (which kills the foreground service and loses in-memory
|
|
288
|
+
* state). Stored separately from paused downloads so it never interferes with
|
|
289
|
+
* the live pause/resume bookkeeping.
|
|
290
|
+
*/
|
|
291
|
+
fun saveActiveDownloads(activeDownloads: Map<String, Downloader.PausedDownloadInfo>) {
|
|
292
|
+
try {
|
|
293
|
+
val gson = Gson()
|
|
294
|
+
val mapCopy = activeDownloads.mapValues { (_, info) ->
|
|
295
|
+
PausedDownloadInfoData(
|
|
296
|
+
configId = info.configId,
|
|
297
|
+
url = info.url,
|
|
298
|
+
destination = info.destination,
|
|
299
|
+
headers = info.headers,
|
|
300
|
+
bytesDownloaded = info.bytesDownloaded,
|
|
301
|
+
bytesTotal = info.bytesTotal,
|
|
302
|
+
metadata = info.metadata
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
val str = gson.toJson(mapCopy)
|
|
306
|
+
|
|
307
|
+
if (isMMKVAvailable && mmkv != null) {
|
|
308
|
+
mmkv!!.encode("$name$KEY_ACTIVE_DOWNLOADS", str)
|
|
309
|
+
} else {
|
|
310
|
+
sharedPreferences.edit()
|
|
311
|
+
.putString("$name$KEY_ACTIVE_DOWNLOADS", str)
|
|
312
|
+
.commit()
|
|
313
|
+
}
|
|
314
|
+
} catch (e: Exception) {
|
|
315
|
+
RNBackgroundDownloaderModuleImpl.logE(TAG, "Failed to save active downloads: ${e.message}")
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Load the in-progress resumable download recovery snapshots.
|
|
321
|
+
*/
|
|
322
|
+
fun loadActiveDownloads(): MutableMap<String, Downloader.PausedDownloadInfo> {
|
|
323
|
+
try {
|
|
324
|
+
val str = if (isMMKVAvailable && mmkv != null) {
|
|
325
|
+
mmkv!!.decodeString("$name$KEY_ACTIVE_DOWNLOADS")
|
|
326
|
+
} else {
|
|
327
|
+
sharedPreferences.getString("$name$KEY_ACTIVE_DOWNLOADS", null)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (str != null && str.isNotEmpty()) {
|
|
331
|
+
val gson = Gson()
|
|
332
|
+
val mapType = object : TypeToken<Map<String, PausedDownloadInfoData>>() {}.type
|
|
333
|
+
val dataMap: Map<String, PausedDownloadInfoData> = gson.fromJson(str, mapType)
|
|
334
|
+
return dataMap.mapValues { (_, data) ->
|
|
335
|
+
Downloader.PausedDownloadInfo(
|
|
336
|
+
configId = data.configId,
|
|
337
|
+
url = data.url,
|
|
338
|
+
destination = data.destination,
|
|
339
|
+
headers = data.headers,
|
|
340
|
+
bytesDownloaded = data.bytesDownloaded,
|
|
341
|
+
bytesTotal = data.bytesTotal,
|
|
342
|
+
metadata = data.metadata
|
|
343
|
+
)
|
|
344
|
+
}.toMutableMap()
|
|
345
|
+
}
|
|
346
|
+
} catch (e: Exception) {
|
|
347
|
+
RNBackgroundDownloaderModuleImpl.logE(TAG, "Failed to load active downloads: ${e.message}")
|
|
348
|
+
}
|
|
349
|
+
return mutableMapOf()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Upsert a single in-progress resumable download recovery snapshot.
|
|
354
|
+
*/
|
|
355
|
+
fun saveActiveDownload(info: Downloader.PausedDownloadInfo) {
|
|
356
|
+
val map = loadActiveDownloads()
|
|
357
|
+
map[info.configId] = info
|
|
358
|
+
saveActiveDownloads(map)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Remove a single recovery snapshot by config ID.
|
|
363
|
+
*/
|
|
364
|
+
fun removeActiveDownload(configId: String) {
|
|
365
|
+
val map = loadActiveDownloads()
|
|
366
|
+
if (map.remove(configId) != null) {
|
|
367
|
+
saveActiveDownloads(map)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Clear all recovery snapshots.
|
|
373
|
+
*/
|
|
374
|
+
fun clearActiveDownloads() {
|
|
375
|
+
saveActiveDownloads(emptyMap())
|
|
376
|
+
}
|
|
377
|
+
|
|
283
378
|
/**
|
|
284
379
|
* Save upload configs map.
|
|
285
380
|
*/
|
package/ios/RNBGDTaskConfig.h
CHANGED
|
@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
14
14
|
@property (nonatomic, assign) NSInteger state;
|
|
15
15
|
@property (nonatomic, assign) NSInteger errorCode;
|
|
16
16
|
@property (nonatomic, assign) BOOL hasResumeData;
|
|
17
|
+
// iOS Data Protection level key for the saved file
|
|
18
|
+
// (e.g. NSFileProtectionCompleteUntilFirstUserAuthentication). nil = library default.
|
|
19
|
+
@property (nonatomic, copy, nullable) NSString *dataProtection;
|
|
17
20
|
|
|
18
21
|
- (instancetype)initWithDictionary:(NSDictionary *)dict;
|
|
19
22
|
|
package/ios/RNBGDTaskConfig.mm
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
self.url = dict[@"url"];
|
|
17
17
|
self.destination = dict[@"destination"];
|
|
18
18
|
self.metadata = dict[@"metadata"];
|
|
19
|
+
self.dataProtection = dict[@"dataProtection"];
|
|
19
20
|
self.reportedBegin = NO;
|
|
20
21
|
self.bytesDownloaded = 0;
|
|
21
22
|
self.bytesTotal = 0;
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
[aCoder encodeObject:self.url forKey:@"url"];
|
|
34
35
|
[aCoder encodeObject:self.destination forKey:@"destination"];
|
|
35
36
|
[aCoder encodeObject:self.metadata forKey:@"metadata"];
|
|
37
|
+
[aCoder encodeObject:self.dataProtection forKey:@"dataProtection"];
|
|
36
38
|
[aCoder encodeBool:self.reportedBegin forKey:@"reportedBegin"];
|
|
37
39
|
[aCoder encodeInt64:self.bytesDownloaded forKey:@"bytesDownloaded"];
|
|
38
40
|
[aCoder encodeInt64:self.bytesTotal forKey:@"bytesTotal"];
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
self.destination = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"destination"];
|
|
53
55
|
NSString *metadata = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"metadata"];
|
|
54
56
|
self.metadata = metadata != nil ? metadata : @"{}";
|
|
57
|
+
self.dataProtection = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"dataProtection"];
|
|
55
58
|
self.reportedBegin = [aDecoder decodeBoolForKey:@"reportedBegin"];
|
|
56
59
|
self.bytesDownloaded = [aDecoder decodeInt64ForKey:@"bytesDownloaded"];
|
|
57
60
|
self.bytesTotal = [aDecoder decodeInt64ForKey:@"bytesTotal"];
|