@kesha-antonov/react-native-background-downloader 4.5.3 → 4.5.4
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/CHANGELOG.md +294 -0
- package/README.md +29 -8
- package/android/build.gradle +4 -2
- package/android/src/main/java/com/eko/Downloader.kt +10 -0
- package/android/src/main/java/com/eko/RNBackgroundDownloaderModuleImpl.kt +7 -0
- package/android/src/main/java/com/eko/UIDTDownloadJobService.kt +42 -4
- package/android/src/main/java/com/eko/uidt/UIDTJobManager.kt +11 -1
- package/android/src/main/java/com/eko/uidt/UIDTJobState.kt +62 -0
- package/example/android/app/debug.keystore +0 -0
- package/ios/.DS_Store +0 -0
- package/ios/RNBackgroundDownloader.mm +53 -9
- package/package.json +1 -1
- package/plugin/build/index.d.ts +2 -2
- package/plugin/build/index.js +1 -1
- package/plugin/package.json +0 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v4.5.3
|
|
4
|
+
|
|
5
|
+
### ✨ New Features
|
|
6
|
+
|
|
7
|
+
- **Android: Notification Grouping Mode (`summaryOnly`):** Added `mode` option to `NotificationsGroupingConfig`. Set `mode: 'summaryOnly'` to show only the summary notification for a group while individual download notifications are minimized (ultra-silent, no alert). Useful for keeping the notification shade clean during large batch downloads.
|
|
8
|
+
- `'individual'` (default) — all notifications shown, current behavior unchanged
|
|
9
|
+
- `'summaryOnly'` — only the group summary notification is shown with aggregate progress; individual notifications are invisible/silent
|
|
10
|
+
- **Android: Progress-Based Summary Notification:** In `summaryOnly` mode, the group summary notification now displays aggregate progress (total bytes downloaded / total bytes) across all downloads in the group.
|
|
11
|
+
- **Android: Auto-Remove Completed Downloads from Group:** Completed downloads are now automatically removed from notification groups, keeping the summary accurate.
|
|
12
|
+
|
|
13
|
+
### 🏗️ Architecture Changes
|
|
14
|
+
|
|
15
|
+
- **JS: `NotificationGroupingMode` Type:** New exported type `'individual' | 'summaryOnly'` for the `mode` field in `NotificationsGroupingConfig`.
|
|
16
|
+
- **Android: Ultra-Silent Notification Channel:** Added `NOTIFICATION_CHANNEL_ULTRA_SILENT_ID` (`IMPORTANCE_MIN`) channel used for individual notifications in `summaryOnly` mode.
|
|
17
|
+
- **Android: `updateSummaryNotificationForGroup()`:** New method that dispatches to the correct summary update strategy based on grouping mode.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## v4.5.2
|
|
22
|
+
|
|
23
|
+
### ✨ New Features
|
|
24
|
+
|
|
25
|
+
- **Update Headers on Paused Downloads:** Added ability to update headers (e.g., refresh auth tokens) on paused download tasks before resuming. Use `task.setDownloadParams()` to update headers while paused, then `task.resume()` to continue the download with new headers.
|
|
26
|
+
- **Use case:** User pauses a large download, returns hours/days later when auth token has expired. Now you can refresh the token and resume without restarting the download.
|
|
27
|
+
- **iOS:** Creates a fresh request with HTTP Range header and updated headers on resume
|
|
28
|
+
- **Android:** Updates both in-memory headers and persisted paused download state
|
|
29
|
+
|
|
30
|
+
### 🏗️ Architecture Changes
|
|
31
|
+
|
|
32
|
+
- **JS: `setDownloadParams()` Now Async:** The `DownloadTask.setDownloadParams()` method is now async and returns `Promise<boolean>` indicating whether native headers were updated (true when task is paused).
|
|
33
|
+
- **Native: Added `updateTaskHeaders` Method:** New native method on iOS and Android to update headers for paused tasks.
|
|
34
|
+
|
|
35
|
+
### 📚 Documentation
|
|
36
|
+
|
|
37
|
+
- Added "Updating headers on paused downloads" section to README
|
|
38
|
+
- Added `setDownloadParams()` method documentation to API.md
|
|
39
|
+
- Added iOS "Updating Headers on Paused Downloads" section to PLATFORM_NOTES.md
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## v4.5.1
|
|
44
|
+
|
|
45
|
+
### 🏗️ Architecture Changes
|
|
46
|
+
|
|
47
|
+
- **Android: UIDT Code Refactoring:** Extracted 980-line monolithic `UIDTDownloadJobService.kt` into modular components:
|
|
48
|
+
- `uidt/UIDTJobState.kt` - Data classes, constants, job registry
|
|
49
|
+
- `uidt/UIDTNotificationManager.kt` - All notification logic
|
|
50
|
+
- `uidt/UIDTJobManager.kt` - Job scheduling, cancel, pause, resume
|
|
51
|
+
- `utils/ProgressUtils.kt` - Progress calculation utilities
|
|
52
|
+
- Backward compatibility maintained via companion object delegates
|
|
53
|
+
- **Android: Removed Redundant jobScheduler.cancel():** In `pauseJob()`, removed unnecessary `jobScheduler.cancel()` after `jobFinished(params, false)` since `wantsReschedule=false` already tells the system the job is complete.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## v4.5.0
|
|
58
|
+
|
|
59
|
+
### ✨ New Features
|
|
60
|
+
|
|
61
|
+
- **Android: Global Notification Configuration:** Added `showNotificationsEnabled` and `notificationsGrouping` config options for controlling UIDT notifications globally via `setConfig()`.
|
|
62
|
+
- **Android: Customizable Paused Notification Text:** Added `downloadPaused` to `NotificationTexts` interface for customizing the "Paused" notification text.
|
|
63
|
+
- **Android: Notification Update Throttling:** Notification updates are now synced with `progressInterval` for consistent UI/notification progress display.
|
|
64
|
+
|
|
65
|
+
### 🐛 Bug Fixes
|
|
66
|
+
|
|
67
|
+
- **Android: Paused Downloads Continuing in Background:** Fixed paused UIDT downloads continuing to download in background after app restart. Now UIDT job is fully cancelled on pause, with a detached "Paused" notification shown.
|
|
68
|
+
- **Android: Duplicate Notifications After Resume:** Fixed duplicate notifications appearing when resuming downloads after app restart. Now uses stable notification IDs based on `configId.hashCode()`.
|
|
69
|
+
- **Android: Notification Not Updating After Resume:** Fixed notification stuck on old progress after resuming. Now resets notification timing on resume and shows correct progress immediately.
|
|
70
|
+
- **Android: Notification Updating While Paused:** Fixed notification progress updating even when download is paused.
|
|
71
|
+
- **Android: Stale Notifications on App Close:** All download notifications are now cancelled when the app closes via `invalidate()`.
|
|
72
|
+
|
|
73
|
+
### 💥 Breaking Changes
|
|
74
|
+
|
|
75
|
+
- **Removed Per-Task Notification Options:** `isNotificationVisible` and `notificationTitle` removed from `DownloadParams` and `UploadParams`. Use global `setConfig({ showNotificationsEnabled, notificationsGrouping })` instead.
|
|
76
|
+
|
|
77
|
+
### 🏗️ Architecture Changes
|
|
78
|
+
|
|
79
|
+
- **Android: Pause Behavior on Android 14+:** Complete redesign of pause/resume for User-Initiated Data Transfer (UIDT) jobs:
|
|
80
|
+
- **Problem:** UIDT jobs continue running in background even after app closes, causing "paused" downloads to secretly continue downloading.
|
|
81
|
+
- **Solution:** On pause, the UIDT job is properly terminated via `jobFinished(params, false)`. Download state is persisted to disk for resumption via HTTP Range headers.
|
|
82
|
+
- **UX:** A detached "Paused" notification (using `JOB_END_NOTIFICATION_POLICY_DETACH`) remains visible after job termination. On resume, a new UIDT job is created.
|
|
83
|
+
- **Follows Google's UIDT best practices:** State saved even without `onStopJob`, `jobFinished()` called on completion, notifications updated periodically with throttling.
|
|
84
|
+
- **Android: Separate Notification Channels:** Added separate channels for visible (`IMPORTANCE_LOW`) and silent (`IMPORTANCE_MIN`) notifications.
|
|
85
|
+
|
|
86
|
+
### ✨ Improvements
|
|
87
|
+
|
|
88
|
+
- **Android: Cleaner Notifications:** Added `setOnlyAlertOnce(true)` and `setShowWhen(false)` to all notifications for less intrusive updates.
|
|
89
|
+
- **Example App: Persistent Notification Settings:** Show Notifications and Notification Grouping toggles are now persisted with MMKV.
|
|
90
|
+
- **Example App: Android 13+ Permission Request:** Added POST_NOTIFICATIONS permission request when enabling notifications.
|
|
91
|
+
|
|
92
|
+
### 📚 Documentation
|
|
93
|
+
|
|
94
|
+
- Updated README with notification behavior during pause/resume
|
|
95
|
+
- Updated API.md with new notification configuration options
|
|
96
|
+
- Documented that notifications are removed when app closes
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## v4.4.5
|
|
101
|
+
|
|
102
|
+
### 🐛 Bug Fixes
|
|
103
|
+
|
|
104
|
+
- **Android: Stop Task Not Working on Android 14+:** Fixed `stopTask()` not actually stopping UIDT downloads on Android 14+. The JobScheduler job was cancelled but the underlying HTTP download continued. Now properly calls `resumableDownloader.cancel()` before removing from active jobs.
|
|
105
|
+
- **Android: Paused Tasks Not Persisting Across App Restarts:** Fixed paused UIDT downloads losing their state when the app was restarted. Added `getJobDownloadState()` to retrieve download state from active UIDT jobs and `savePausedDownloadState()` to properly persist pause state.
|
|
106
|
+
- **Android: ACCESS_NETWORK_STATE Permission:** Added missing permission required for JobScheduler network connectivity constraints on Android 14+.
|
|
107
|
+
- **Android: Downloaded Files List Showing Incomplete Files:** The "Downloaded Files" section in the example app now correctly filters out files that have active (non-DONE) download tasks, preventing incomplete files from appearing in the list.
|
|
108
|
+
|
|
109
|
+
### 🧹 Code Cleanup
|
|
110
|
+
|
|
111
|
+
- **Removed Verbose Debug Logs:** Cleaned up extensive debug logging in `StorageManager`, `Downloader`, and `RNBackgroundDownloaderModuleImpl` that was cluttering production logs. Removed serialization/deserialization logs, verification reads, and per-item iteration logs while keeping error logging.
|
|
112
|
+
- **Simplified Kotlin Code:** Removed unnecessary `else` blocks containing only debug/warning logs from `pauseTask()` and `resumeTask()` methods for cleaner code.
|
|
113
|
+
|
|
114
|
+
### ✨ Improvements
|
|
115
|
+
|
|
116
|
+
- **TypeScript: Added `destination` to Task Info:** The `destination` field is now returned from `getExistingDownloadTasks()` for paused downloads, allowing the app to know where the file will be saved.
|
|
117
|
+
|
|
118
|
+
### 📚 Documentation
|
|
119
|
+
|
|
120
|
+
- Added `skipMmkvDependency` option documentation to README for Expo plugin
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## v4.4.4
|
|
125
|
+
|
|
126
|
+
### 🐛 Bug Fixes
|
|
127
|
+
|
|
128
|
+
- **Expo Plugin: Fixed TypeScript Types:** Corrected TypeScript type definitions in the Expo config plugin.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## v4.4.3
|
|
133
|
+
|
|
134
|
+
### ✨ New Features
|
|
135
|
+
|
|
136
|
+
- **Expo Plugin: Auto-detect react-native-mmkv:** The Expo config plugin now automatically detects if `react-native-mmkv` is installed and skips adding the MMKV dependency to avoid duplicate class errors. Use `skipMmkvDependency: true` option to manually skip if needed.
|
|
137
|
+
- **Android: Version from package.json:** Android native code now reads the library version from `package.json` instead of hardcoding it.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## v4.4.2
|
|
142
|
+
|
|
143
|
+
### 🐛 Bug Fixes
|
|
144
|
+
|
|
145
|
+
- **Kotlin 2.0 Compatibility:** Fixed compilation error with Kotlin 2.0 (React Native 0.77+) by updating `progressReporter` to use named parameter syntax. This ensures compatibility with both Kotlin 1.9 (RN 0.76) and Kotlin 2.x (RN 0.77+).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## v4.4.1
|
|
150
|
+
|
|
151
|
+
### 🐛 Bug Fixes
|
|
152
|
+
|
|
153
|
+
- **Android: Paused Tasks Persistence:** Fixed paused downloads not being restored after app restart on Android. Added persistent storage for paused download state using MMKV/SharedPreferences.
|
|
154
|
+
- **iOS: Improved Pause/Resume Handling:** Better handling of pause/resume operations on app restarts for iOS.
|
|
155
|
+
- **Upload Task App Restart Recovery:** Fixed upload tasks not being recoverable after app restart (#143). Added persistent storage for upload task configurations.
|
|
156
|
+
|
|
157
|
+
### ✨ Improvements
|
|
158
|
+
|
|
159
|
+
- **Example App:** Added task list display with animations and improved UI for managing downloads.
|
|
160
|
+
|
|
161
|
+
### 📚 Documentation
|
|
162
|
+
|
|
163
|
+
- Updated README with clearer MMKV dependency instructions
|
|
164
|
+
- Added information about resuming tasks after app restarts
|
|
165
|
+
- Updated authors section
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## v4.4.0
|
|
170
|
+
|
|
171
|
+
### ✨ New Features
|
|
172
|
+
|
|
173
|
+
- **Android 16 UIDT Support:** Downloads are now automatically marked as User-Initiated Data Transfers on Android 16+ (API 36) to prevent thermal throttling and job quota restrictions. Downloads will continue reliably even under moderate thermal conditions (~40°C).
|
|
174
|
+
|
|
175
|
+
### 🐛 Bug Fixes
|
|
176
|
+
|
|
177
|
+
- **iOS MMKV Conflict Fix:** Removed hard MMKV dependency from iOS podspec to prevent symbol conflicts with `react-native-mmkv`. Apps using `react-native-mmkv` no longer experience crashes (EXC_BAD_ACCESS) on iOS.
|
|
178
|
+
|
|
179
|
+
### 📦 Dependencies & Infrastructure
|
|
180
|
+
|
|
181
|
+
- **New Android Permission:** Added `RUN_USER_INITIATED_JOBS` permission for Android 16+ UIDT support
|
|
182
|
+
- **iOS MMKV Dependency:** MMKV is no longer a hard dependency in the podspec. Apps not using `react-native-mmkv` must add `pod 'MMKV', '>= 1.0.0'` to their Podfile.
|
|
183
|
+
|
|
184
|
+
### 📚 Documentation
|
|
185
|
+
|
|
186
|
+
- Added documentation about Android 16+ UIDT support in README
|
|
187
|
+
- Added iOS MMKV dependency section in README (similar to Android section)
|
|
188
|
+
- Added migration guide for iOS MMKV dependency change
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## v4.2.0
|
|
193
|
+
|
|
194
|
+
> 📖 **Upgrading from v4.1.x?** See the [Migration Guide](./MIGRATION.md) for details on the new Android pause/resume functionality.
|
|
195
|
+
|
|
196
|
+
### ✨ New Features
|
|
197
|
+
|
|
198
|
+
- **Android Pause/Resume Support:** Android now fully supports `task.pause()` and `task.resume()` methods using HTTP Range headers. Downloads can be paused and resumed just like on iOS.
|
|
199
|
+
- **Background Download Service:** Added `ResumableDownloadService` - a foreground service that ensures downloads continue even when the app is in the background or the screen is off.
|
|
200
|
+
- **WakeLock Support:** Downloads maintain a partial wake lock to prevent the device from sleeping during active downloads.
|
|
201
|
+
- **`bytesTotal` Unknown Size Handling:** When the server doesn't provide a `Content-Length` header, `bytesTotal` now returns `-1` instead of `0` to distinguish "unknown size" from "zero bytes".
|
|
202
|
+
|
|
203
|
+
### 🐛 Bug Fixes
|
|
204
|
+
|
|
205
|
+
- **Android Pause Error:** Fixed `COULD_NOT_FIND` error when pausing downloads on Android by properly tracking pausing state
|
|
206
|
+
- **Temp File Cleanup:** Fixed temp files (`.tmp`) not being deleted when stopping or deleting paused downloads
|
|
207
|
+
- **Content-Length Handling:** Fixed progress percentage calculation when server doesn't provide Content-Length header
|
|
208
|
+
|
|
209
|
+
### 📦 Dependencies & Infrastructure
|
|
210
|
+
|
|
211
|
+
- **New Android Permissions:** Added `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_DATA_SYNC`, and `WAKE_LOCK` permissions for background download support
|
|
212
|
+
- **New Service:** Added `ResumableDownloadService` with `dataSync` foreground service type
|
|
213
|
+
|
|
214
|
+
### 📚 Documentation
|
|
215
|
+
|
|
216
|
+
- Updated README to reflect Android pause/resume support
|
|
217
|
+
- Removed "iOS only" notes from pause/resume documentation
|
|
218
|
+
- Added documentation about `bytesTotal` returning `-1` for unknown sizes
|
|
219
|
+
- Updated Android DownloadManager Limitations section with new pause/resume implementation details
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## v4.1.0
|
|
224
|
+
|
|
225
|
+
> 📖 **Upgrading from v4.0.x?** See the [Migration Guide](./MIGRATION.md) for the MMKV dependency change.
|
|
226
|
+
|
|
227
|
+
### ⚠️ Breaking Changes
|
|
228
|
+
|
|
229
|
+
- **MMKV Dependency Changed to `compileOnly`:** The MMKV dependency is now `compileOnly` instead of `implementation` to avoid duplicate class errors when the app also uses `react-native-mmkv`. Apps not using `react-native-mmkv` must now explicitly add the MMKV dependency.
|
|
230
|
+
|
|
231
|
+
### ✨ New Features
|
|
232
|
+
|
|
233
|
+
- **Expo Plugin Android Support:** The Expo config plugin now automatically adds the MMKV dependency on Android. Use `addMmkvDependency: false` option if you're already using `react-native-mmkv`.
|
|
234
|
+
|
|
235
|
+
### 🐛 Bug Fixes
|
|
236
|
+
|
|
237
|
+
- **Duplicate Class Errors:** Fixed potential duplicate class errors when app uses both this library and `react-native-mmkv` by changing MMKV to `compileOnly` dependency
|
|
238
|
+
|
|
239
|
+
### 📚 Documentation
|
|
240
|
+
|
|
241
|
+
- Added documentation for MMKV dependency requirements in README
|
|
242
|
+
- Updated Platform-Specific Limitations section with MMKV setup instructions
|
|
243
|
+
- Added Expo plugin options documentation
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## v4.0.0
|
|
248
|
+
|
|
249
|
+
> 📖 **Upgrading from v3.x?** See the [Migration Guide](./MIGRATION.md) for detailed instructions.
|
|
250
|
+
|
|
251
|
+
### ⚠️ Breaking Changes
|
|
252
|
+
|
|
253
|
+
- **API Renamed:** `checkForExistingDownloads()` → `getExistingDownloadTasks()` - Now returns a Promise with better naming
|
|
254
|
+
- **API Renamed:** `download()` → `createDownloadTask()` - Downloads now require explicit `.start()` call
|
|
255
|
+
- **Download Tasks Start Explicitly:** Tasks created with `createDownloadTask()` are now in `PENDING` state and must call `.start()` to begin downloading
|
|
256
|
+
- **New Config Option:** Added `progressMinBytes` to `setConfig()` - controls minimum bytes change before progress callback fires (default: 1MB)
|
|
257
|
+
- **Source Structure Changed:** Code moved from `lib/` to `src/` directory with proper TypeScript types
|
|
258
|
+
|
|
259
|
+
### ✨ New Features
|
|
260
|
+
|
|
261
|
+
- **React Native New Architecture Support:** Full TurboModules support for both iOS and Android
|
|
262
|
+
- **Expo Config Plugin:** Added automatic iOS native code integration for Expo projects via `app.plugin.js`
|
|
263
|
+
- **Android Kotlin Migration:** All Java code converted to Kotlin
|
|
264
|
+
- **`maxRedirects` Option:** Configure maximum redirects for Android downloads (resolves #15)
|
|
265
|
+
- **`progressMinBytes` Option:** Hybrid progress reporting - callbacks fire based on time interval OR bytes downloaded
|
|
266
|
+
- **Android 15+ Support:** Added support for 16KB memory page sizes
|
|
267
|
+
- **Architecture Fallback:** Comprehensive x86/ARMv7 support with SharedPreferences fallback
|
|
268
|
+
|
|
269
|
+
### 🐛 Bug Fixes
|
|
270
|
+
|
|
271
|
+
- **iOS Pause/Resume:** Fixed pause and resume functionality on iOS
|
|
272
|
+
- **RN 0.78+ Compatibility:** Fixed bridge checks with safe emitter checks
|
|
273
|
+
- **New Architecture Events:** Fixed `downloadBegin` and `downloadProgress` events emission
|
|
274
|
+
- **Android Background Downloads:** Fixed completed files not moving to destination
|
|
275
|
+
- **Progress Callback Unknown Total:** Fixed progress callback not firing when total bytes unknown
|
|
276
|
+
- **Android 12 MMKV Crash:** Added robust error handling
|
|
277
|
+
- **`checkForExistingDownloads` TypeError:** Fixed TypeError on Android with architecture fallback
|
|
278
|
+
- **Firebase Performance Compatibility:** Fixed `completeHandler` method compatibility on Android
|
|
279
|
+
- **Slow Connection Handling:** Better handling of slow-responding URLs with timeouts
|
|
280
|
+
- **Android OldArch Export:** Fixed module method export issue (#79)
|
|
281
|
+
- **MMKV Compatibility:** Support for react-native-mmkv 4+ with mmkv-shared dependency
|
|
282
|
+
|
|
283
|
+
### 📦 Dependencies & Infrastructure
|
|
284
|
+
|
|
285
|
+
- **React Native:** Updated example app to RN 0.81.4
|
|
286
|
+
- **TypeScript:** Full TypeScript types in `src/types.ts`
|
|
287
|
+
- **iOS Native:** Converted from `.m` to `.mm` (Objective-C++)
|
|
288
|
+
- **Package Manager:** Switched to yarn as preferred package manager
|
|
289
|
+
|
|
290
|
+
### 📚 Documentation
|
|
291
|
+
|
|
292
|
+
- Added documentation for `progressMinBytes` option
|
|
293
|
+
- Updated README for React Native 0.77+ instructions
|
|
294
|
+
- Improved Expo config plugin examples
|
package/README.md
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
<h1 align="center">React Native Background Downloader</h1>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
Download and upload large files on iOS & Android — even when your app is in the background or terminated.
|
|
18
|
+
Download and upload large files on iOS & Android — even when your app is in the background or terminated by the OS.
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
23
|
## ✨ Features
|
|
24
24
|
|
|
25
|
-
- 📥 **Background Downloads** - Downloads continue even when app is in background or terminated
|
|
25
|
+
- 📥 **Background Downloads** - Downloads continue even when app is in background or terminated by the OS
|
|
26
26
|
- 📤 **Background Uploads** - Upload files reliably in the background
|
|
27
27
|
- ⏸️ **Pause/Resume** - Full pause and resume support on both iOS and Android
|
|
28
28
|
- 🔄 **Re-attach to Downloads** - Reconnect to ongoing downloads after app restart
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
**The Problem:** Standard network requests in React Native are tied to your app's lifecycle. When the user switches to another app or the OS terminates your app to free memory, your downloads stop. For small files this is fine, but for large files (videos, podcasts, documents) this creates a frustrating user experience.
|
|
38
38
|
|
|
39
39
|
**The Solution:** Both iOS and Android provide system-level APIs for background file transfers:
|
|
40
|
-
- **iOS:** [`NSURLSession`](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background) - handles downloads in a separate process, continuing even after your app is terminated
|
|
40
|
+
- **iOS:** [`NSURLSession`](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background) - handles downloads in a separate process, continuing even after your app is terminated by the OS. Note: if the user explicitly force-kills the app via the App Switcher, iOS cancels all background tasks — this is an iOS system limitation that cannot be overridden
|
|
41
41
|
- **Android:** A combination of [`DownloadManager`](https://developer.android.com/reference/android/app/DownloadManager) for system-managed downloads, [Foreground Services](https://developer.android.com/develop/background-work/services/foreground-services) for pause/resume support, and [MMKV](https://github.com/Tencent/MMKV) for persistent state storage
|
|
42
42
|
|
|
43
43
|
**The Challenge:** These APIs are powerful but complex. Downloads run in a separate process, so your app might restart from scratch while downloads are still in progress. Keeping your UI in sync with background downloads requires careful state management.
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
- [📦 Installation](#-installation)
|
|
54
54
|
- [Expo Projects](#expo-projects)
|
|
55
55
|
- [Bare React Native Projects](#bare-react-native-projects)
|
|
56
|
+
- [MMKV version comparison](#mmkv-version-comparison)
|
|
56
57
|
- [🚀 Usage](#-usage)
|
|
57
58
|
- [Downloading a file](#downloading-a-file)
|
|
58
59
|
- [Re-Attaching to background tasks](#re-attaching-to-background-tasks)
|
|
@@ -113,7 +114,7 @@ export default {
|
|
|
113
114
|
expo: {
|
|
114
115
|
plugins: [
|
|
115
116
|
["@kesha-antonov/react-native-background-downloader", {
|
|
116
|
-
mmkvVersion: "
|
|
117
|
+
mmkvVersion: "1.3.16", // Customize MMKV version on Android
|
|
117
118
|
skipMmkvDependency: true // Skip if you want to add MMKV manually
|
|
118
119
|
}]
|
|
119
120
|
]
|
|
@@ -123,8 +124,8 @@ export default {
|
|
|
123
124
|
|
|
124
125
|
| Option | Type | Default | Description |
|
|
125
126
|
|--------|------|---------|-------------|
|
|
126
|
-
| `mmkvVersion` | string | `'
|
|
127
|
-
| `skipMmkvDependency` | boolean | `false` | Skip adding MMKV dependency. Set to `true` if you're using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) to avoid duplicate class errors. The plugin auto-detects `react-native-mmkv` but you can use this option to explicitly skip. |
|
|
127
|
+
| `mmkvVersion` | string | `'1.3.16'` | The version of [MMKV](https://github.com/Tencent/MMKV/releases) to use on Android. See [MMKV version comparison](#mmkv-version-comparison) for details. |
|
|
128
|
+
| `skipMmkvDependency` | boolean | `false` | Skip adding MMKV dependency. Set to `true` if you're using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) to avoid duplicate class errors. The plugin auto-detects `react-native-mmkv` but you can use this option to explicitly skip. See [MMKV version comparison](#mmkv-version-comparison). |
|
|
128
129
|
|
|
129
130
|
</details>
|
|
130
131
|
|
|
@@ -214,11 +215,29 @@ Add MMKV to your `android/app/build.gradle`:
|
|
|
214
215
|
|
|
215
216
|
```gradle
|
|
216
217
|
dependencies {
|
|
217
|
-
implementation 'com.tencent:mmkv-shared:
|
|
218
|
+
implementation 'com.tencent:mmkv-shared:1.3.16'
|
|
218
219
|
}
|
|
219
220
|
```
|
|
220
221
|
|
|
221
|
-
> **Note:** If you're already using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) in your project, skip this step — it already includes MMKV.
|
|
222
|
+
> **Note:** If you're already using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) in your project, skip this step — it already includes MMKV. Note that `react-native-mmkv` v4.x uses [Margelo's fork of MMKV](https://github.com/margelo/MMKV) (`io.github.zhongwuzw:mmkv`) which re-adds armeabi-v7a (32-bit ARM) support that was dropped in the official MMKV 2.x release.
|
|
223
|
+
|
|
224
|
+
> **⚠️ armeabi-v7a (32-bit ARM) users:** MMKV 2.x dropped 32-bit ABI support (since v2.0.0). If you need armeabi-v7a support and get a CMake error like `No compatible library found for //mmkv/mmkv`, use the MMKV 1.3.x LTS series instead — it supports both armeabi-v7a **and** 16KB page sizes (since v1.3.14):
|
|
225
|
+
> ```gradle
|
|
226
|
+
> dependencies {
|
|
227
|
+
> implementation 'com.tencent:mmkv-shared:1.3.16'
|
|
228
|
+
> }
|
|
229
|
+
> ```
|
|
230
|
+
|
|
231
|
+
#### MMKV version comparison
|
|
232
|
+
|
|
233
|
+
| Dependency | armeabi-v7a (32-bit) | arm64-v8a | 16KB page size | Recommended for |
|
|
234
|
+
|---|:---:|:---:|:---:|---|
|
|
235
|
+
| `com.tencent:mmkv-shared:1.3.16` (**default**) | ✅ | ✅ | ✅ (since 1.3.14) | Most apps — broadest device coverage |
|
|
236
|
+
| `com.tencent:mmkv-shared:2.x` | ❌ | ✅ | ✅ | 64-bit only apps (no legacy devices) |
|
|
237
|
+
| `io.github.zhongwuzw:mmkv:2.3.0` ([Margelo fork](https://github.com/margelo/MMKV)) | ✅ | ✅ | ✅ | Used automatically by `react-native-mmkv` v4.x — skip manual dependency |
|
|
238
|
+
| `react-native-mmkv` (already in project) | ✅ | ✅ | ✅ | If you already use `react-native-mmkv` — skip Step 4 entirely |
|
|
239
|
+
|
|
240
|
+
**TL;DR:** Use the default `1.3.16`. If you already have `react-native-mmkv` in your project, skip Step 4.
|
|
222
241
|
|
|
223
242
|
## 🚀 Usage
|
|
224
243
|
|
|
@@ -836,6 +855,8 @@ This can happen with slow-responding servers. The library automatically adds kee
|
|
|
836
855
|
<summary><strong>Duplicate class errors with react-native-mmkv (Android)</strong></summary>
|
|
837
856
|
|
|
838
857
|
If you're using `react-native-mmkv`, you don't need to add the MMKV dependency manually - it's already included. The library uses `compileOnly` to avoid conflicts.
|
|
858
|
+
|
|
859
|
+
`react-native-mmkv` v4.x uses [Margelo's fork of MMKV](https://github.com/margelo/MMKV) (`io.github.zhongwuzw:mmkv`) which re-adds armeabi-v7a (32-bit ARM) support, so you have full ABI coverage including 32-bit devices when using `react-native-mmkv`.
|
|
839
860
|
</details>
|
|
840
861
|
|
|
841
862
|
<details>
|
package/android/build.gradle
CHANGED
|
@@ -36,6 +36,8 @@ android {
|
|
|
36
36
|
versionName '1.0'
|
|
37
37
|
|
|
38
38
|
// Support for 16KB memory page sizes (Android 15+)
|
|
39
|
+
// Note: MMKV 2.x dropped armeabi-v7a support. If your app targets armeabi-v7a,
|
|
40
|
+
// downgrade to MMKV 1.x in your app's build.gradle (see README for details).
|
|
39
41
|
ndk {
|
|
40
42
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
|
41
43
|
}
|
|
@@ -78,8 +80,8 @@ dependencies {
|
|
|
78
80
|
// MMKV dependency for persistent download state storage
|
|
79
81
|
// Uses compileOnly to avoid duplicate class errors when app also uses react-native-mmkv
|
|
80
82
|
// The app must provide MMKV dependency (either directly or via react-native-mmkv)
|
|
81
|
-
// MMKV
|
|
82
|
-
compileOnly 'com.tencent:mmkv-shared:
|
|
83
|
+
// MMKV 1.3.14+ supports both armeabi-v7a (32-bit) and 16KB page sizes (Android 15+)
|
|
84
|
+
compileOnly 'com.tencent:mmkv-shared:1.3.16'
|
|
83
85
|
|
|
84
86
|
implementation 'com.google.code.gson:gson:2.12.1'
|
|
85
87
|
}
|
|
@@ -111,8 +111,18 @@ class Downloader(private val context: Context, private val storageManager: com.e
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Set the download listener for resumable downloads.
|
|
114
|
+
* Also sets the UIDT listener on Android 14+ so that ongoing UIDT jobs
|
|
115
|
+
* (e.g. ones that survived app termination) can forward events to JS after
|
|
116
|
+
* the app is reopened.
|
|
114
117
|
*/
|
|
115
118
|
fun setResumableDownloadListener(listener: ResumableDownloader.DownloadListener) {
|
|
119
|
+
// For UIDT jobs (Android 14+), set the listener directly on the registry.
|
|
120
|
+
// This reconnects event forwarding after app restart when UIDT jobs are
|
|
121
|
+
// already running but UIDTJobRegistry.downloadListener was null in the
|
|
122
|
+
// fresh process.
|
|
123
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
124
|
+
UIDTDownloadJobService.downloadListener = listener
|
|
125
|
+
}
|
|
116
126
|
executeWhenServiceReady {
|
|
117
127
|
downloadService?.setDownloadListener(listener)
|
|
118
128
|
}
|
|
@@ -225,6 +225,8 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
override fun onComplete(id: String, location: String, bytesDownloaded: Long, bytesTotal: Long) {
|
|
228
|
+
// Drop any buffered progress for this task so it cannot arrive in JS after downloadComplete
|
|
229
|
+
progressReporter.clearPendingReport(id)
|
|
228
230
|
eventEmitter.emitComplete(id, location, bytesDownloaded, bytesTotal)
|
|
229
231
|
|
|
230
232
|
// Clean up all download state
|
|
@@ -234,6 +236,8 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
override fun onError(id: String, error: String, errorCode: Int) {
|
|
239
|
+
// Drop any buffered progress for this task so it cannot arrive in JS after downloadFailed
|
|
240
|
+
progressReporter.clearPendingReport(id)
|
|
237
241
|
eventEmitter.emitFailed(id, error, errorCode)
|
|
238
242
|
|
|
239
243
|
// Clean up all download state
|
|
@@ -385,6 +389,9 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
|
|
|
385
389
|
stopTaskProgress(config.id)
|
|
386
390
|
|
|
387
391
|
synchronized(sharedLock) {
|
|
392
|
+
// Drop any buffered progress that slipped past stopTaskProgress's clearPendingReport
|
|
393
|
+
// (the polling thread may have re-added it between clearPendingReport and this lock)
|
|
394
|
+
progressReporter.clearPendingReport(config.id)
|
|
388
395
|
when (status) {
|
|
389
396
|
DownloadManager.STATUS_SUCCESSFUL -> {
|
|
390
397
|
onSuccessfulDownload(config, downloadStatus)
|
|
@@ -148,7 +148,7 @@ class UIDTDownloadJobService : JobService() {
|
|
|
148
148
|
}
|
|
149
149
|
val url = extras.getString(UIDTConstants.KEY_URL) ?: return false
|
|
150
150
|
val destination = extras.getString(UIDTConstants.KEY_DESTINATION) ?: return false
|
|
151
|
-
val
|
|
151
|
+
val startByteFromExtras = extras.getLong(UIDTConstants.KEY_START_BYTE, 0)
|
|
152
152
|
val totalBytes = extras.getLong(UIDTConstants.KEY_TOTAL_BYTES, -1)
|
|
153
153
|
|
|
154
154
|
// Extract group info from metadata (for notification grouping)
|
|
@@ -165,8 +165,36 @@ class UIDTDownloadJobService : JobService() {
|
|
|
165
165
|
RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to parse metadata: ${e.message}")
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
//
|
|
169
|
-
|
|
168
|
+
// Resolve headers and start byte.
|
|
169
|
+
// In-memory pendingHeaders is populated in the same process (scheduleDownload / onStopJob).
|
|
170
|
+
// The disk-persisted resume state is the fallback for a fresh process after a restart.
|
|
171
|
+
val inMemoryHeaders = UIDTJobRegistry.pendingHeaders.remove(configId)
|
|
172
|
+
val diskResumeState = UIDTJobRegistry.loadResumeState(this, configId)
|
|
173
|
+
// Clear disk state now that we've consumed it.
|
|
174
|
+
UIDTJobRegistry.clearResumeState(this, configId)
|
|
175
|
+
|
|
176
|
+
val headers: Map<String, String>
|
|
177
|
+
val startByte: Long
|
|
178
|
+
when {
|
|
179
|
+
inMemoryHeaders != null -> {
|
|
180
|
+
// Same-process restart: use in-memory headers.
|
|
181
|
+
// Prefer the disk byte position if it is more advanced than the extras
|
|
182
|
+
// (written by onStopJob with the actual download offset).
|
|
183
|
+
headers = inMemoryHeaders
|
|
184
|
+
startByte = if (diskResumeState != null && diskResumeState.second > startByteFromExtras)
|
|
185
|
+
diskResumeState.second else startByteFromExtras
|
|
186
|
+
}
|
|
187
|
+
diskResumeState != null -> {
|
|
188
|
+
// Cross-process restart: use the persisted state.
|
|
189
|
+
headers = diskResumeState.first
|
|
190
|
+
startByte = diskResumeState.second
|
|
191
|
+
}
|
|
192
|
+
else -> {
|
|
193
|
+
headers = emptyMap()
|
|
194
|
+
startByte = startByteFromExtras
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "onStartJob: configId=$configId, startByte=$startByte, headers=${headers.size}")
|
|
170
198
|
|
|
171
199
|
// Create notification for UIDT job (required)
|
|
172
200
|
val notificationId = UIDTNotificationManager.getNotificationIdForConfig(configId)
|
|
@@ -233,8 +261,12 @@ class UIDTDownloadJobService : JobService() {
|
|
|
233
261
|
// Save state for potential resume
|
|
234
262
|
val state = jobState.resumableDownloader.getState(configId)
|
|
235
263
|
if (state != null) {
|
|
236
|
-
|
|
264
|
+
val bytesDownloaded = state.bytesDownloaded.get()
|
|
265
|
+
// Keep in-memory headers so the same-process reschedule works.
|
|
237
266
|
UIDTJobRegistry.pendingHeaders[configId] = state.headers
|
|
267
|
+
// Persist to disk so a fresh process can resume from the right position.
|
|
268
|
+
UIDTJobRegistry.saveResumeState(this, configId, state.headers, bytesDownloaded)
|
|
269
|
+
RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "onStopJob: saved resume state for $configId at $bytesDownloaded bytes")
|
|
238
270
|
}
|
|
239
271
|
}
|
|
240
272
|
UIDTJobRegistry.activeJobs.remove(configId)
|
|
@@ -364,6 +396,9 @@ class UIDTDownloadJobService : JobService() {
|
|
|
364
396
|
// Signal job completion - this triggers the REMOVE policy
|
|
365
397
|
jobFinished(params, false)
|
|
366
398
|
|
|
399
|
+
// Clear persisted resume state - no longer needed after successful completion
|
|
400
|
+
UIDTJobRegistry.clearResumeState(this@UIDTDownloadJobService, id)
|
|
401
|
+
|
|
367
402
|
// Notify external listener
|
|
368
403
|
UIDTJobRegistry.downloadListener?.onComplete(id, location, bytesDownloaded, bytesTotal)
|
|
369
404
|
}
|
|
@@ -402,6 +437,9 @@ class UIDTDownloadJobService : JobService() {
|
|
|
402
437
|
// Signal job completion with no reschedule - this triggers the REMOVE policy
|
|
403
438
|
jobFinished(params, false)
|
|
404
439
|
|
|
440
|
+
// Clear persisted resume state - no longer needed after failure
|
|
441
|
+
UIDTJobRegistry.clearResumeState(this@UIDTDownloadJobService, id)
|
|
442
|
+
|
|
405
443
|
// Notify external listener
|
|
406
444
|
UIDTJobRegistry.downloadListener?.onError(id, error, errorCode)
|
|
407
445
|
}
|
|
@@ -66,6 +66,9 @@ object UIDTJobManager {
|
|
|
66
66
|
|
|
67
67
|
// Store headers for later retrieval (PersistableBundle can't store Map<String, String>)
|
|
68
68
|
UIDTJobRegistry.pendingHeaders[configId] = headers
|
|
69
|
+
// Also persist to disk so headers survive process death and are available
|
|
70
|
+
// in onStartJob even when the process is restarted by the JobScheduler.
|
|
71
|
+
UIDTJobRegistry.saveResumeState(context, configId, headers, startByte)
|
|
69
72
|
|
|
70
73
|
// Create extras bundle
|
|
71
74
|
val extras = PersistableBundle().apply {
|
|
@@ -77,9 +80,14 @@ object UIDTJobManager {
|
|
|
77
80
|
putString(UIDTConstants.KEY_METADATA, metadata)
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
// Build network request - require internet connectivity
|
|
83
|
+
// Build network request - require internet connectivity.
|
|
84
|
+
// Remove NET_CAPABILITY_NOT_VPN (added by Builder default) so that VPN networks
|
|
85
|
+
// (e.g. Proton VPN, full-tunnel VPNs) are accepted. Without this, the JobScheduler
|
|
86
|
+
// only considers non-VPN networks; a kill-switch VPN blocks that traffic and the
|
|
87
|
+
// job never starts, causing callbacks to never fire.
|
|
81
88
|
val networkRequest = NetworkRequest.Builder()
|
|
82
89
|
.addCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
90
|
+
.removeCapability(android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
|
83
91
|
.build()
|
|
84
92
|
|
|
85
93
|
// Build the job with UIDT flag
|
|
@@ -150,6 +158,8 @@ object UIDTJobManager {
|
|
|
150
158
|
jobScheduler.cancel(jobId)
|
|
151
159
|
UIDTJobRegistry.pendingHeaders.remove(configId)
|
|
152
160
|
UIDTJobRegistry.activeJobs.remove(configId)
|
|
161
|
+
// Clear persisted resume state so stale headers/bytes don't affect future downloads
|
|
162
|
+
UIDTJobRegistry.clearResumeState(context, configId)
|
|
153
163
|
|
|
154
164
|
// Update summary notification if grouping was enabled
|
|
155
165
|
if (jobState != null && config.groupingEnabled && jobState.groupId.isNotEmpty()) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
package com.eko.uidt
|
|
2
2
|
|
|
3
3
|
import android.app.job.JobParameters
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import com.eko.RNBackgroundDownloaderModuleImpl
|
|
4
6
|
import com.eko.ResumableDownloader
|
|
7
|
+
import org.json.JSONObject
|
|
5
8
|
import java.util.concurrent.ConcurrentHashMap
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -127,6 +130,65 @@ data class UIDTJobInfo(
|
|
|
127
130
|
* Singleton for managing active UIDT jobs state.
|
|
128
131
|
*/
|
|
129
132
|
object UIDTJobRegistry {
|
|
133
|
+
|
|
134
|
+
private const val PREFS_NAME = "rnbd_uidt_resume"
|
|
135
|
+
private const val KEY_BYTES_PREFIX = "bytes_"
|
|
136
|
+
private const val KEY_HEADERS_PREFIX = "headers_"
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Persist UIDT resume state (headers + byte position) to disk so it
|
|
140
|
+
* survives process death and can be used when the job is rescheduled in a
|
|
141
|
+
* new process.
|
|
142
|
+
*/
|
|
143
|
+
fun saveResumeState(context: Context, configId: String, headers: Map<String, String>, bytesDownloaded: Long) {
|
|
144
|
+
try {
|
|
145
|
+
val headersJson = JSONObject(headers as Map<*, *>).toString()
|
|
146
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
|
|
147
|
+
.putLong("$KEY_BYTES_PREFIX$configId", bytesDownloaded)
|
|
148
|
+
.putString("$KEY_HEADERS_PREFIX$configId", headersJson)
|
|
149
|
+
.apply()
|
|
150
|
+
RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "Saved UIDT resume state for $configId: bytes=$bytesDownloaded")
|
|
151
|
+
} catch (e: Exception) {
|
|
152
|
+
RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to save UIDT resume state for $configId: ${e.message}")
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Load persisted UIDT resume state for a download.
|
|
158
|
+
* Returns (headers, bytesDownloaded) or null if no state was saved.
|
|
159
|
+
*/
|
|
160
|
+
fun loadResumeState(context: Context, configId: String): Pair<Map<String, String>, Long>? {
|
|
161
|
+
try {
|
|
162
|
+
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
163
|
+
val bytesDownloaded = prefs.getLong("$KEY_BYTES_PREFIX$configId", -1L)
|
|
164
|
+
val headersJson = prefs.getString("$KEY_HEADERS_PREFIX$configId", null)
|
|
165
|
+
if (bytesDownloaded < 0 || headersJson == null) return null
|
|
166
|
+
val json = JSONObject(headersJson)
|
|
167
|
+
val headers = mutableMapOf<String, String>()
|
|
168
|
+
for (key in json.keys()) headers[key] = json.getString(key)
|
|
169
|
+
RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "Loaded UIDT resume state for $configId: bytes=$bytesDownloaded, headers=${headers.size}")
|
|
170
|
+
return Pair(headers, bytesDownloaded)
|
|
171
|
+
} catch (e: Exception) {
|
|
172
|
+
RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to load UIDT resume state for $configId: ${e.message}")
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Clear persisted UIDT resume state for a download once it is no longer needed.
|
|
179
|
+
*/
|
|
180
|
+
fun clearResumeState(context: Context, configId: String) {
|
|
181
|
+
try {
|
|
182
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
|
|
183
|
+
.remove("$KEY_BYTES_PREFIX$configId")
|
|
184
|
+
.remove("$KEY_HEADERS_PREFIX$configId")
|
|
185
|
+
.apply()
|
|
186
|
+
RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "Cleared UIDT resume state for $configId")
|
|
187
|
+
} catch (e: Exception) {
|
|
188
|
+
RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to clear UIDT resume state for $configId: ${e.message}")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
130
192
|
// Track active jobs for pause/resume
|
|
131
193
|
val activeJobs = ConcurrentHashMap<String, JobState>()
|
|
132
194
|
|
|
Binary file
|
package/ios/.DS_Store
ADDED
|
Binary file
|
|
@@ -66,6 +66,13 @@ static CompletionHandler storedCompletionHandler;
|
|
|
66
66
|
// Controls whether debug logs are sent to JS
|
|
67
67
|
BOOL isLogsEnabled;
|
|
68
68
|
|
|
69
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
70
|
+
// Queue of events that arrived before the TurboModule event emitter callback was set.
|
|
71
|
+
// This prevents crashes (std::bad_function_call / SIGABRT) when NSURLSession delegate
|
|
72
|
+
// callbacks fire before JS has registered event listeners.
|
|
73
|
+
NSMutableArray<NSDictionary *> *pendingEmitEvents;
|
|
74
|
+
#endif
|
|
75
|
+
|
|
69
76
|
// Upload-specific instance variables
|
|
70
77
|
NSMutableDictionary<NSNumber *, RNBGDUploadTaskConfig *> *uploadTaskToConfigMap;
|
|
71
78
|
NSMutableDictionary<NSString *, NSURLSessionUploadTask *> *idToUploadTaskMap;
|
|
@@ -233,6 +240,10 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
233
240
|
isSessionActivated = NO;
|
|
234
241
|
pendingDownloads = [[NSMutableArray alloc] init];
|
|
235
242
|
|
|
243
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
244
|
+
pendingEmitEvents = [[NSMutableArray alloc] init];
|
|
245
|
+
#endif
|
|
246
|
+
|
|
236
247
|
// Initialize upload-specific data structures
|
|
237
248
|
NSData *uploadTaskToConfigMapData = [mmkv getDataForKey:ID_TO_UPLOAD_CONFIG_MAP_KEY];
|
|
238
249
|
NSMutableDictionary *uploadTaskToConfigMapDataDefault = [[NSMutableDictionary alloc] init];
|
|
@@ -1140,6 +1151,36 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1140
1151
|
}
|
|
1141
1152
|
}
|
|
1142
1153
|
|
|
1154
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1155
|
+
#pragma mark - Safe event emission (New Architecture)
|
|
1156
|
+
|
|
1157
|
+
// Safely emit an event, queuing it if the TurboModule event emitter callback
|
|
1158
|
+
// has not been registered yet. This prevents std::bad_function_call crashes
|
|
1159
|
+
// when NSURLSession delegate callbacks fire before JS has registered listeners
|
|
1160
|
+
// (e.g., background session delivering completions from a prior app session).
|
|
1161
|
+
- (void)safeEmitEvent:(NSString *)eventName value:(id)value {
|
|
1162
|
+
@synchronized (pendingEmitEvents) {
|
|
1163
|
+
if (_eventEmitterCallback) {
|
|
1164
|
+
_eventEmitterCallback(std::string([eventName UTF8String]), value);
|
|
1165
|
+
} else {
|
|
1166
|
+
[pendingEmitEvents addObject:@{@"name": eventName, @"value": value}];
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper {
|
|
1172
|
+
@synchronized (pendingEmitEvents) {
|
|
1173
|
+
[super setEventEmitterCallback:eventEmitterCallbackWrapper];
|
|
1174
|
+
|
|
1175
|
+
// Flush any events that arrived before the callback was set
|
|
1176
|
+
for (NSDictionary *event in pendingEmitEvents) {
|
|
1177
|
+
_eventEmitterCallback(std::string([event[@"name"] UTF8String]), event[@"value"]);
|
|
1178
|
+
}
|
|
1179
|
+
[pendingEmitEvents removeAllObjects];
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
#endif
|
|
1183
|
+
|
|
1143
1184
|
#pragma mark - NSURLSessionDownloadDelegate methods
|
|
1144
1185
|
- (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location {
|
|
1145
1186
|
@synchronized (sharedLock) {
|
|
@@ -1161,6 +1202,9 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1161
1202
|
[self sendDebugLog:[NSString stringWithFormat:@"didFinishDownloadingToURL: error - %@", error.localizedDescription] taskId:taskConfig.id];
|
|
1162
1203
|
}
|
|
1163
1204
|
|
|
1205
|
+
// Drop any buffered progress for this task so it cannot arrive in JS after downloadComplete
|
|
1206
|
+
[progressReports removeObjectForKey:taskConfig.id];
|
|
1207
|
+
|
|
1164
1208
|
[self sendDownloadCompletionEvent:taskConfig task:downloadTask error:error];
|
|
1165
1209
|
|
|
1166
1210
|
[self removeTaskFromMap:downloadTask];
|
|
@@ -1170,7 +1214,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1170
1214
|
- (void)sendDownloadCompletionEvent:(RNBGDTaskConfig *)taskConfig task:(NSURLSessionDownloadTask *)task error:(NSError *)error {
|
|
1171
1215
|
if (error) {
|
|
1172
1216
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1173
|
-
[self
|
|
1217
|
+
[self safeEmitEvent:@"onDownloadFailed" value:@{
|
|
1174
1218
|
@"id": taskConfig.id,
|
|
1175
1219
|
@"error": [error localizedDescription],
|
|
1176
1220
|
@"errorCode": @(error.code)
|
|
@@ -1184,7 +1228,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1184
1228
|
#endif
|
|
1185
1229
|
} else {
|
|
1186
1230
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1187
|
-
[self
|
|
1231
|
+
[self safeEmitEvent:@"onDownloadComplete" value:@{
|
|
1188
1232
|
@"id": taskConfig.id,
|
|
1189
1233
|
@"location": taskConfig.destination,
|
|
1190
1234
|
@"bytesDownloaded": @(task.countOfBytesReceived),
|
|
@@ -1243,7 +1287,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1243
1287
|
responseHeaders = @{};
|
|
1244
1288
|
}
|
|
1245
1289
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1246
|
-
[self
|
|
1290
|
+
[self safeEmitEvent:@"onDownloadBegin" value:@{
|
|
1247
1291
|
@"id": taskConfig.id,
|
|
1248
1292
|
@"expectedBytes": @(expectedBytes),
|
|
1249
1293
|
@"headers": responseHeaders
|
|
@@ -1290,7 +1334,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1290
1334
|
NSDate *now = [NSDate date];
|
|
1291
1335
|
if ([now timeIntervalSinceDate:lastProgressReportedAt] > progressInterval) {
|
|
1292
1336
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1293
|
-
[self
|
|
1337
|
+
[self safeEmitEvent:@"onDownloadProgress" value:[progressReports allValues]];
|
|
1294
1338
|
#else
|
|
1295
1339
|
[self sendEventWithName:@"downloadProgress" body:[progressReports allValues]];
|
|
1296
1340
|
#endif
|
|
@@ -1385,7 +1429,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1385
1429
|
|
|
1386
1430
|
// Handle failure
|
|
1387
1431
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1388
|
-
[self
|
|
1432
|
+
[self safeEmitEvent:@"onDownloadFailed" value:@{
|
|
1389
1433
|
@"id": taskConfig.id,
|
|
1390
1434
|
@"error": [error localizedDescription],
|
|
1391
1435
|
@"errorCode": @(error.code)
|
|
@@ -1764,7 +1808,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1764
1808
|
if (!taskConfig.reportedBegin) {
|
|
1765
1809
|
taskConfig.reportedBegin = YES;
|
|
1766
1810
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1767
|
-
[self
|
|
1811
|
+
[self safeEmitEvent:@"onUploadBegin" value:@{
|
|
1768
1812
|
@"id": taskConfig.id,
|
|
1769
1813
|
@"expectedBytes": @(totalBytesExpectedToSend)
|
|
1770
1814
|
}];
|
|
@@ -1801,7 +1845,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1801
1845
|
NSDate *now = [NSDate date];
|
|
1802
1846
|
if ([now timeIntervalSinceDate:lastUploadProgressReportedAt] > progressInterval) {
|
|
1803
1847
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1804
|
-
[self
|
|
1848
|
+
[self safeEmitEvent:@"onUploadProgress" value:[uploadProgressReports allValues]];
|
|
1805
1849
|
#else
|
|
1806
1850
|
[self sendEventWithName:@"uploadProgress" body:[uploadProgressReports allValues]];
|
|
1807
1851
|
#endif
|
|
@@ -1847,7 +1891,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1847
1891
|
|
|
1848
1892
|
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [handleUploadCompletion] error: %@", error);
|
|
1849
1893
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1850
|
-
[self
|
|
1894
|
+
[self safeEmitEvent:@"onUploadFailed" value:@{
|
|
1851
1895
|
@"id": taskConfig.id,
|
|
1852
1896
|
@"error": [error localizedDescription],
|
|
1853
1897
|
@"errorCode": @(error.code)
|
|
@@ -1873,7 +1917,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1873
1917
|
|
|
1874
1918
|
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [handleUploadCompletion] success, responseCode: %ld", (long)responseCode);
|
|
1875
1919
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1876
|
-
[self
|
|
1920
|
+
[self safeEmitEvent:@"onUploadComplete" value:@{
|
|
1877
1921
|
@"id": taskConfig.id,
|
|
1878
1922
|
@"responseCode": @(responseCode),
|
|
1879
1923
|
@"responseBody": responseBody,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kesha-antonov/react-native-background-downloader",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.4",
|
|
4
4
|
"description": "A library for React-Native to help you download large files on iOS and Android both in the foreground and most importantly in the background.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
package/plugin/build/index.d.ts
CHANGED
|
@@ -3,12 +3,12 @@ interface PluginOptions {
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for the MMKV dependency on Android.
|
|
5
5
|
* Pass a string to specify the version, or an object with version property.
|
|
6
|
-
* @default '
|
|
6
|
+
* @default '1.3.16'
|
|
7
7
|
* @example
|
|
8
8
|
* // Use default version
|
|
9
9
|
* ["@kesha-antonov/react-native-background-downloader"]
|
|
10
10
|
* // Specify version
|
|
11
|
-
* ["@kesha-antonov/react-native-background-downloader", { mmkvVersion: "
|
|
11
|
+
* ["@kesha-antonov/react-native-background-downloader", { mmkvVersion: "1.3.16" }]
|
|
12
12
|
*/
|
|
13
13
|
mmkvVersion?: string;
|
|
14
14
|
/**
|
package/plugin/build/index.js
CHANGED
|
@@ -37,7 +37,7 @@ const config_plugins_1 = require("@expo/config-plugins");
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const withRNBackgroundDownloader = (config, options) => {
|
|
40
|
-
const { mmkvVersion = '
|
|
40
|
+
const { mmkvVersion = '1.3.16', skipMmkvDependency = false } = options || {};
|
|
41
41
|
// Auto-detect react-native-mmkv in dependencies
|
|
42
42
|
const hasReactNativeMmkv = checkForReactNativeMmkv(config);
|
|
43
43
|
const shouldSkipMmkv = skipMmkvDependency || hasReactNativeMmkv;
|