@kesha-antonov/react-native-background-downloader 4.1.2 → 4.2.0

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## v4.2.0
4
+
5
+ > 📖 **Upgrading from v4.1.x?** See the [Migration Guide](./MIGRATION.md) for details on the new Android pause/resume functionality.
6
+
7
+ ### ✨ New Features
8
+
9
+ - **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.
10
+ - **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.
11
+ - **WakeLock Support:** Downloads maintain a partial wake lock to prevent the device from sleeping during active downloads.
12
+ - **`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".
13
+
14
+ ### 🐛 Bug Fixes
15
+
16
+ - **Android Pause Error:** Fixed `COULD_NOT_FIND` error when pausing downloads on Android by properly tracking pausing state
17
+ - **Temp File Cleanup:** Fixed temp files (`.tmp`) not being deleted when stopping or deleting paused downloads
18
+ - **Content-Length Handling:** Fixed progress percentage calculation when server doesn't provide Content-Length header
19
+
20
+ ### 📦 Dependencies & Infrastructure
21
+
22
+ - **New Android Permissions:** Added `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_DATA_SYNC`, and `WAKE_LOCK` permissions for background download support
23
+ - **New Service:** Added `ResumableDownloadService` with `dataSync` foreground service type
24
+
25
+ ### 📚 Documentation
26
+
27
+ - Updated README to reflect Android pause/resume support
28
+ - Removed "iOS only" notes from pause/resume documentation
29
+ - Added documentation about `bytesTotal` returning `-1` for unknown sizes
30
+ - Updated Android DownloadManager Limitations section with new pause/resume implementation details
31
+
32
+ ---
33
+
3
34
  ## v4.1.0
4
35
 
5
36
  > 📖 **Upgrading from v4.0.x?** See the [Migration Guide](./MIGRATION.md) for the MMKV dependency change.
package/README.md CHANGED
@@ -279,61 +279,16 @@ task.start()
279
279
 
280
280
  // ...later
281
281
 
282
- // Pause the task (iOS only)
283
- // Note: On Android, pause/resume is not supported by DownloadManager
282
+ // Pause the task
284
283
  await task.pause()
285
284
 
286
- // Resume after pause (iOS only)
287
- // Note: On Android, pause/resume is not supported by DownloadManager
285
+ // Resume after pause
288
286
  await task.resume()
289
287
 
290
288
  // Cancel the task
291
289
  await task.stop()
292
290
  ```
293
291
 
294
- ### Platform-Aware Pause/Resume
295
-
296
- ```javascript
297
- import { Platform } from 'react-native'
298
- import { createDownloadTask, directories } from '@kesha-antonov/react-native-background-downloader'
299
-
300
- const task = createDownloadTask({
301
- id: 'file123',
302
- url: 'https://link-to-very.large/file.zip',
303
- destination: `${directories.documents}/file.zip`,
304
- metadata: {}
305
- }).begin(({ expectedBytes, headers }) => {
306
- console.log(`Going to download ${expectedBytes} bytes!`)
307
- }).progress(({ bytesDownloaded, bytesTotal }) => {
308
- console.log(`Downloaded: ${bytesDownloaded / bytesTotal * 100}%`)
309
- }).done(({ bytesDownloaded, bytesTotal }) => {
310
- console.log('Download is done!', { bytesDownloaded, bytesTotal })
311
- }).error(({ error, errorCode }) => {
312
- console.log('Download canceled due to error: ', { error, errorCode });
313
- })
314
-
315
- task.start()
316
-
317
- // Platform-aware pause/resume handling
318
- async function pauseDownloadTask() {
319
- if (Platform.OS === 'ios') {
320
- await task.pause()
321
- console.log('Download paused')
322
- } else {
323
- console.log('Pause not supported on Android. Consider using stop() instead.')
324
- }
325
- }
326
-
327
- async function resumeDownloadTask() {
328
- if (Platform.OS === 'ios') {
329
- await task.resume()
330
- console.log('Download resumed')
331
- } else {
332
- console.log('Resume not supported on Android. You may need to restart the download.')
333
- }
334
- }
335
- ```
336
-
337
292
  ### Re-Attaching to background downloads
338
293
 
339
294
  This is the main selling point of this library (but it's free!).
@@ -527,7 +482,7 @@ A class representing a download task created by `createDownloadTask()`. Note: Yo
527
482
  | `metadata` | Record<string, unknown> | The metadata you gave the task when calling `createDownloadTask` |
528
483
  | `state` | 'PENDING' \| 'DOWNLOADING' \| 'PAUSED' \| 'DONE' \| 'FAILED' \| 'STOPPED' | Current state of the download task |
529
484
  | `bytesDownloaded` | Number | The number of bytes currently written by the task |
530
- | `bytesTotal` | Number | The number bytes expected to be written by this task or more plainly, the file size being downloaded |
485
+ | `bytesTotal` | Number | The number bytes expected to be written by this task or more plainly, the file size being downloaded. **Note:** This value will be `-1` if the server does not provide a `Content-Length` header |
531
486
  | `downloadParams` | DownloadParams | The download parameters set for this task |
532
487
 
533
488
  ### `completeHandler(jobId: string)`
@@ -548,19 +503,19 @@ All callback methods return the current instance of the `DownloadTask` for chain
548
503
  | Function | Callback Arguments | Info|
549
504
  | ---------- | --------------------------------- | ---- |
550
505
  | `begin` | `{ expectedBytes: number, headers: Record<string, string \| null> }` | Called when the first byte is received. 💡: this is good place to check if the device has enough storage space for this download |
551
- | `progress` | `{ bytesDownloaded: number, bytesTotal: number }` | Called based on progressInterval (default: every 1000ms) so you can update your progress bar accordingly |
552
- | `done` | `{ bytesDownloaded: number, bytesTotal: number }` | Called when the download is done, the file is at the destination you've set |
506
+ | `progress` | `{ bytesDownloaded: number, bytesTotal: number }` | Called based on progressInterval (default: every 1000ms) so you can update your progress bar accordingly. **Note:** `bytesTotal` will be `-1` if the server does not provide a `Content-Length` header |
507
+ | `done` | `{ bytesDownloaded: number, bytesTotal: number }` | Called when the download is done, the file is at the destination you've set. **Note:** `bytesTotal` will be `-1` if the server did not provide a `Content-Length` header |
553
508
  | `error` | `{ error: string, errorCode: number }` | Called when the download stops due to an error |
554
509
 
555
- ### `pause(): Promise<void>` (iOS only)
510
+ ### `pause(): Promise<void>`
556
511
  Pauses the download. Returns a promise that resolves when the pause operation is complete.
557
512
 
558
- **Note:** This functionality is not supported on Android due to limitations in the DownloadManager API. On Android, calling this method will log a warning but will not crash the application.
513
+ **Note:** On Android, pause/resume is implemented using HTTP Range headers, which requires server support. The download progress is saved and resumed from where it left off.
559
514
 
560
- ### `resume(): Promise<void>` (iOS only)
515
+ ### `resume(): Promise<void>`
561
516
  Resumes a paused download. Returns a promise that resolves when the resume operation is complete.
562
517
 
563
- **Note:** This functionality is not supported on Android due to limitations in the DownloadManager API. On Android, calling this method will log a warning but will not crash the application.
518
+ **Note:** On Android, this uses HTTP Range headers to resume from the last downloaded byte position. If the server doesn't support range requests, the download will restart from the beginning.
564
519
 
565
520
  ### `stop(): Promise<void>`
566
521
  Stops the download for good and removes the file that was written so far. Returns a promise that resolves when the stop operation is complete.
@@ -594,19 +549,13 @@ dependencies {
594
549
 
595
550
  ### Android DownloadManager Limitations
596
551
 
597
- The Android implementation uses the system's `DownloadManager` service, which has some limitations compared to iOS:
598
-
599
- #### Pause/Resume Not Supported
600
- - **Issue**: Android's DownloadManager does not provide a public API for pausing and resuming downloads
601
- - **Impact**: Calling `task.pause()` or `task.resume()` on Android will log a warning but not perform any action
602
- - **Workaround**: If you need to stop a download, use `task.stop()` and restart it later with a new download request
603
- - **Technical Details**: The private APIs needed for pause/resume functionality are not accessible to third-party applications
552
+ The Android implementation uses the system's `DownloadManager` service for downloads, with custom pause/resume support:
604
553
 
605
- #### Alternative Approaches for Android
606
- If pause/resume functionality is critical for your application, consider:
607
- 1. Using `task.stop()` and tracking progress to restart downloads from where they left off (if the server supports range requests)
608
- 2. Implementing a custom download solution for Android that doesn't use DownloadManager
609
- 3. Designing your app flow to minimize the need for pause/resume functionality
554
+ #### Pause/Resume Support
555
+ - **Implementation**: Pause/resume on Android is implemented using HTTP Range headers
556
+ - **How it works**: When you pause a download, the current progress is saved. When resumed, a new download starts from where it left off using the `Range` header
557
+ - **Server requirement**: The server must support HTTP Range requests for resume to work correctly. If the server doesn't support range requests, the download will restart from the beginning
558
+ - **Temp files**: During pause/resume, progress is stored in a `.tmp` file which is renamed to the final destination upon completion
610
559
 
611
560
  ## Rules for proguard-rules.pro
612
561
 
@@ -2,5 +2,16 @@
2
2
  package="com.eko">
3
3
 
4
4
  <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
5
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
6
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
7
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
8
+
9
+ <application>
10
+ <service
11
+ android:name=".ResumableDownloadService"
12
+ android:enabled="true"
13
+ android:exported="false"
14
+ android:foregroundServiceType="dataSync" />
15
+ </application>
5
16
 
6
17
  </manifest>
@@ -1,133 +1,386 @@
1
1
  package com.eko
2
2
 
3
3
  import android.app.DownloadManager
4
+ import android.content.ComponentName
4
5
  import android.content.Context
5
6
  import android.content.Intent
7
+ import android.content.ServiceConnection
6
8
  import android.database.Cursor
9
+ import android.os.Build
10
+ import android.os.IBinder
7
11
  import android.util.Log
8
12
  import com.facebook.react.bridge.Arguments
9
13
  import com.facebook.react.bridge.WritableMap
14
+ import java.io.File
15
+ import java.util.concurrent.ConcurrentHashMap
10
16
 
11
17
  /**
12
18
  * Wrapper around Android's DownloadManager for managing file downloads.
13
19
  * Provides methods for downloading, canceling, and querying download status.
20
+ *
21
+ * For pause/resume functionality, this class integrates with ResumableDownloadService
22
+ * which uses HTTP Range headers to support true pause/resume, even in the background.
14
23
  */
15
24
  class Downloader(private val context: Context) {
16
25
 
17
- companion object {
18
- private const val TAG = "RNBackgroundDownloader"
26
+ companion object {
27
+ private const val TAG = "RNBackgroundDownloader"
28
+ }
29
+
30
+ val downloadManager: DownloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
31
+
32
+ // Service for background resumable downloads
33
+ private var downloadService: ResumableDownloadService? = null
34
+ private var serviceBound = false
35
+ private var pendingServiceOperations = mutableListOf<() -> Unit>()
36
+
37
+ // Track which config IDs are using resumable downloads (paused state)
38
+ private val pausedDownloads = ConcurrentHashMap<String, PausedDownloadInfo>()
39
+
40
+ // Track download IDs that are being intentionally paused (to ignore broadcast events)
41
+ private val pausingDownloadIds = ConcurrentHashMap.newKeySet<Long>()
42
+
43
+ // Service connection
44
+ private val serviceConnection = object : ServiceConnection {
45
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
46
+ val binder = service as ResumableDownloadService.LocalBinder
47
+ downloadService = binder.getService()
48
+ serviceBound = true
49
+ Log.d(TAG, "ResumableDownloadService connected")
50
+
51
+ // Execute any pending operations
52
+ synchronized(pendingServiceOperations) {
53
+ pendingServiceOperations.forEach { it.invoke() }
54
+ pendingServiceOperations.clear()
55
+ }
19
56
  }
20
57
 
21
- val downloadManager: DownloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
58
+ override fun onServiceDisconnected(name: ComponentName?) {
59
+ downloadService = null
60
+ serviceBound = false
61
+ Log.d(TAG, "ResumableDownloadService disconnected")
62
+ }
63
+ }
64
+
65
+ // Expose resumableDownloader for compatibility (delegates to service)
66
+ val resumableDownloader: ResumableDownloader
67
+ get() = downloadService?.resumableDownloader ?: ResumableDownloader()
68
+
69
+ data class PausedDownloadInfo(
70
+ val configId: String,
71
+ val url: String,
72
+ val destination: String,
73
+ val headers: Map<String, String>,
74
+ val bytesDownloaded: Long,
75
+ val bytesTotal: Long
76
+ )
77
+
78
+ init {
79
+ bindToService()
80
+ }
81
+
82
+ private fun bindToService() {
83
+ val intent = Intent(context, ResumableDownloadService::class.java)
84
+ context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
85
+ }
86
+
87
+ fun unbindService() {
88
+ if (serviceBound) {
89
+ context.unbindService(serviceConnection)
90
+ serviceBound = false
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Set the download listener for resumable downloads.
96
+ */
97
+ fun setResumableDownloadListener(listener: ResumableDownloader.DownloadListener) {
98
+ executeWhenServiceReady {
99
+ downloadService?.setDownloadListener(listener)
100
+ }
101
+ }
102
+
103
+ private fun executeWhenServiceReady(operation: () -> Unit) {
104
+ if (serviceBound && downloadService != null) {
105
+ operation()
106
+ } else {
107
+ synchronized(pendingServiceOperations) {
108
+ pendingServiceOperations.add(operation)
109
+ }
110
+ // Ensure service is started
111
+ bindToService()
112
+ }
113
+ }
114
+
115
+ fun download(request: DownloadManager.Request): Long {
116
+ return downloadManager.enqueue(request)
117
+ }
118
+
119
+ fun cancel(downloadId: Long): Int {
120
+ return downloadManager.remove(downloadId)
121
+ }
122
+
123
+ /**
124
+ * Pause a download. This cancels the DownloadManager download and saves state
125
+ * so it can be resumed later using HTTP Range headers.
126
+ */
127
+ fun pause(downloadId: Long, configId: String, url: String, destination: String, headers: Map<String, String>): Boolean {
128
+ // Mark this download as being paused to ignore broadcast events
129
+ pausingDownloadIds.add(downloadId)
130
+
131
+ // Query current progress before cancelling
132
+ val status = checkDownloadStatus(downloadId)
133
+ val bytesDownloaded = status.getDouble("bytesDownloaded").toLong()
134
+ val bytesTotal = status.getDouble("bytesTotal").toLong()
22
135
 
23
- fun download(request: DownloadManager.Request): Long {
24
- return downloadManager.enqueue(request)
136
+ // Cancel the DownloadManager download
137
+ downloadManager.remove(downloadId)
138
+
139
+ // Save paused state for later resume
140
+ pausedDownloads[configId] = PausedDownloadInfo(
141
+ configId = configId,
142
+ url = url,
143
+ destination = destination,
144
+ headers = headers,
145
+ bytesDownloaded = bytesDownloaded,
146
+ bytesTotal = bytesTotal
147
+ )
148
+
149
+ Log.d(TAG, "Paused download $configId at $bytesDownloaded/$bytesTotal bytes")
150
+ return true
151
+ }
152
+
153
+ /**
154
+ * Check if a download ID is being intentionally paused (to ignore broadcast events).
155
+ */
156
+ fun isBeingPaused(downloadId: Long): Boolean {
157
+ return pausingDownloadIds.contains(downloadId)
158
+ }
159
+
160
+ /**
161
+ * Clear the pausing state for a download ID after pause is complete.
162
+ */
163
+ fun clearPausingState(downloadId: Long) {
164
+ pausingDownloadIds.remove(downloadId)
165
+ }
166
+
167
+ /**
168
+ * Resume a paused download using the background service with HTTP Range headers.
169
+ */
170
+ fun resume(configId: String, listener: ResumableDownloader.DownloadListener): Boolean {
171
+ val pausedInfo = pausedDownloads[configId]
172
+ if (pausedInfo == null) {
173
+ Log.w(TAG, "No paused download found for configId: $configId")
174
+ return false
25
175
  }
26
176
 
27
- fun cancel(downloadId: Long): Int {
28
- return downloadManager.remove(downloadId)
177
+ // Start the foreground service for background download
178
+ startDownloadService(
179
+ configId,
180
+ pausedInfo.url,
181
+ pausedInfo.destination,
182
+ pausedInfo.headers,
183
+ pausedInfo.bytesDownloaded,
184
+ pausedInfo.bytesTotal,
185
+ listener
186
+ )
187
+
188
+ Log.d(TAG, "Resuming download $configId from ${pausedInfo.bytesDownloaded} bytes via service")
189
+ return true
190
+ }
191
+
192
+ /**
193
+ * Start a download via the foreground service for background support.
194
+ */
195
+ private fun startDownloadService(
196
+ configId: String,
197
+ url: String,
198
+ destination: String,
199
+ headers: Map<String, String>,
200
+ startByte: Long,
201
+ totalBytes: Long,
202
+ listener: ResumableDownloader.DownloadListener
203
+ ) {
204
+ executeWhenServiceReady {
205
+ downloadService?.setDownloadListener(listener)
206
+ downloadService?.startDownload(configId, url, destination, headers, startByte, totalBytes)
29
207
  }
30
208
 
31
- fun pause(downloadId: Long) {
32
- // Android DownloadManager does not provide a public API for pausing downloads.
33
- // The Downloads.Impl.* constants and private fields like mResolver are not accessible.
34
- // See: https://android-review.googlesource.com/c/platform/packages/providers/DownloadProvider/+/2089866
35
- throw UnsupportedOperationException(
36
- "Pause functionality is not supported by Android DownloadManager. " +
37
- "Consider using stop() and restart the download if needed."
38
- )
209
+ // Also start the service explicitly to ensure it runs in foreground
210
+ val intent = Intent(context, ResumableDownloadService::class.java).apply {
211
+ action = ResumableDownloadService.ACTION_START_DOWNLOAD
212
+ putExtra(ResumableDownloadService.EXTRA_DOWNLOAD_ID, configId)
213
+ putExtra(ResumableDownloadService.EXTRA_URL, url)
214
+ putExtra(ResumableDownloadService.EXTRA_DESTINATION, destination)
215
+ putExtra(ResumableDownloadService.EXTRA_HEADERS, HashMap(headers))
216
+ putExtra(ResumableDownloadService.EXTRA_START_BYTE, startByte)
217
+ putExtra(ResumableDownloadService.EXTRA_TOTAL_BYTES, totalBytes)
39
218
  }
40
219
 
41
- fun resume(downloadId: Long) {
42
- // Android DownloadManager does not provide a public API for resuming paused downloads.
43
- // The Downloads.Impl.* constants and private fields like mResolver are not accessible.
44
- // See: https://android-review.googlesource.com/c/platform/packages/providers/DownloadProvider/+/2089866
45
- throw UnsupportedOperationException(
46
- "Resume functionality is not supported by Android DownloadManager. " +
47
- "Downloads that were stopped need to be restarted from the beginning."
48
- )
220
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
221
+ context.startForegroundService(intent)
222
+ } else {
223
+ context.startService(intent)
49
224
  }
225
+ }
226
+
227
+ /**
228
+ * Pause a resumable download that's currently in progress.
229
+ */
230
+ fun pauseResumable(configId: String): Boolean {
231
+ return downloadService?.pauseDownload(configId) ?: false
232
+ }
50
233
 
51
- // Manually trigger the receiver from anywhere.
52
- fun broadcast(downloadId: Long) {
53
- val intent = Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
54
- intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId)
55
- context.sendBroadcast(intent)
234
+ /**
235
+ * Resume a paused resumable download.
236
+ */
237
+ fun resumeResumable(configId: String, listener: ResumableDownloader.DownloadListener): Boolean {
238
+ executeWhenServiceReady {
239
+ downloadService?.setDownloadListener(listener)
56
240
  }
241
+ return downloadService?.resumeDownload(configId) ?: false
242
+ }
57
243
 
58
- fun checkDownloadStatus(downloadId: Long): WritableMap {
59
- val query = DownloadManager.Query()
60
- query.setFilterById(downloadId)
61
-
62
- var result = Arguments.createMap()
63
-
64
- try {
65
- downloadManager.query(query)?.use { cursor ->
66
- if (cursor.moveToFirst()) {
67
- result = getDownloadStatus(cursor)
68
- } else {
69
- result.putString("downloadId", downloadId.toString())
70
- result.putInt("status", DownloadManager.STATUS_FAILED)
71
- result.putInt("reason", -1)
72
- result.putString("reasonText", "COULD_NOT_FIND")
73
- }
74
- }
75
- } catch (e: Exception) {
76
- Log.e(TAG, "Downloader: ${Log.getStackTraceString(e)}")
244
+ /**
245
+ * Cancel a resumable download and clean up all temp files.
246
+ */
247
+ fun cancelResumable(configId: String): Boolean {
248
+ // Clean up paused download state and temp file
249
+ val pausedInfo = pausedDownloads.remove(configId)
250
+ if (pausedInfo != null) {
251
+ // Delete the temp file for resumable downloads
252
+ try {
253
+ val tempFile = File(pausedInfo.destination + ".tmp")
254
+ if (tempFile.exists()) {
255
+ tempFile.delete()
256
+ Log.d(TAG, "Deleted temp file for paused download: ${tempFile.absolutePath}")
77
257
  }
258
+ } catch (e: Exception) {
259
+ Log.w(TAG, "Failed to delete temp file for paused download: ${e.message}")
260
+ }
261
+ }
262
+
263
+ return downloadService?.cancelDownload(configId) ?: false
264
+ }
265
+
266
+ /**
267
+ * Check if a download is paused.
268
+ */
269
+ fun isPaused(configId: String): Boolean {
270
+ return pausedDownloads.containsKey(configId) || (downloadService?.isPaused(configId) ?: false)
271
+ }
78
272
 
79
- return result
273
+ /**
274
+ * Get paused download info if available.
275
+ */
276
+ fun getPausedInfo(configId: String): PausedDownloadInfo? = pausedDownloads[configId]
277
+
278
+ /**
279
+ * Check if download is using resumable downloader.
280
+ */
281
+ fun isResumableDownload(configId: String): Boolean {
282
+ return downloadService?.getState(configId) != null
283
+ }
284
+
285
+ /**
286
+ * Remove paused state for a config ID and clean up temp files.
287
+ */
288
+ fun removePausedState(configId: String) {
289
+ val pausedInfo = pausedDownloads.remove(configId)
290
+ if (pausedInfo != null) {
291
+ // Delete the temp file for resumable downloads
292
+ try {
293
+ val tempFile = File(pausedInfo.destination + ".tmp")
294
+ if (tempFile.exists()) {
295
+ tempFile.delete()
296
+ Log.d(TAG, "Deleted temp file when removing paused state: ${tempFile.absolutePath}")
297
+ }
298
+ } catch (e: Exception) {
299
+ Log.w(TAG, "Failed to delete temp file when removing paused state: ${e.message}")
300
+ }
80
301
  }
302
+ }
81
303
 
82
- fun getDownloadStatus(cursor: Cursor): WritableMap {
83
- val downloadId = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID))
84
- var localUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI))
85
- val bytesDownloadedSoFar = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
86
- val totalSizeBytes = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
87
- val status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
88
- val reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))
304
+ // Manually trigger the receiver from anywhere.
305
+ fun broadcast(downloadId: Long) {
306
+ val intent = Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
307
+ intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId)
308
+ context.sendBroadcast(intent)
309
+ }
89
310
 
90
- localUri = localUri?.replace("file://", "")
311
+ fun checkDownloadStatus(downloadId: Long): WritableMap {
312
+ val query = DownloadManager.Query()
313
+ query.setFilterById(downloadId)
91
314
 
92
- val reasonText = if (status == DownloadManager.STATUS_PAUSED || status == DownloadManager.STATUS_FAILED) {
93
- getReasonText(status, reason)
315
+ var result = Arguments.createMap()
316
+
317
+ try {
318
+ downloadManager.query(query)?.use { cursor ->
319
+ if (cursor.moveToFirst()) {
320
+ result = getDownloadStatus(cursor)
94
321
  } else {
95
- ""
322
+ result.putString("downloadId", downloadId.toString())
323
+ result.putInt("status", DownloadManager.STATUS_FAILED)
324
+ result.putInt("reason", -1)
325
+ result.putString("reasonText", "COULD_NOT_FIND")
96
326
  }
327
+ }
328
+ } catch (e: Exception) {
329
+ Log.e(TAG, "Downloader: ${Log.getStackTraceString(e)}")
330
+ }
97
331
 
98
- val result = Arguments.createMap()
99
- result.putString("downloadId", downloadId)
100
- result.putInt("status", status)
101
- result.putInt("reason", reason)
102
- result.putString("reasonText", reasonText)
103
- result.putDouble("bytesDownloaded", bytesDownloadedSoFar.toLong().toDouble())
104
- result.putDouble("bytesTotal", totalSizeBytes.toLong().toDouble())
105
- result.putString("localUri", localUri)
332
+ return result
333
+ }
106
334
 
107
- return result
335
+ fun getDownloadStatus(cursor: Cursor): WritableMap {
336
+ val downloadId = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID))
337
+ var localUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI))
338
+ val bytesDownloadedSoFar = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
339
+ val totalSizeBytes = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
340
+ val status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
341
+ val reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))
342
+
343
+ localUri = localUri?.replace("file://", "")
344
+
345
+ val reasonText = if (status == DownloadManager.STATUS_PAUSED || status == DownloadManager.STATUS_FAILED) {
346
+ getReasonText(status, reason)
347
+ } else {
348
+ ""
108
349
  }
109
350
 
110
- fun getReasonText(status: Int, reason: Int): String {
111
- return when (status) {
112
- DownloadManager.STATUS_FAILED -> when (reason) {
113
- DownloadManager.ERROR_CANNOT_RESUME -> "ERROR_CANNOT_RESUME"
114
- DownloadManager.ERROR_DEVICE_NOT_FOUND -> "ERROR_DEVICE_NOT_FOUND"
115
- DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "ERROR_FILE_ALREADY_EXISTS"
116
- DownloadManager.ERROR_FILE_ERROR -> "ERROR_FILE_ERROR"
117
- DownloadManager.ERROR_HTTP_DATA_ERROR -> "ERROR_HTTP_DATA_ERROR"
118
- DownloadManager.ERROR_INSUFFICIENT_SPACE -> "ERROR_INSUFFICIENT_SPACE"
119
- DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "ERROR_TOO_MANY_REDIRECTS"
120
- DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "ERROR_UNHANDLED_HTTP_CODE"
121
- else -> "ERROR_UNKNOWN"
122
- }
123
- DownloadManager.STATUS_PAUSED -> when (reason) {
124
- DownloadManager.PAUSED_QUEUED_FOR_WIFI -> "PAUSED_QUEUED_FOR_WIFI"
125
- DownloadManager.PAUSED_UNKNOWN -> "PAUSED_UNKNOWN"
126
- DownloadManager.PAUSED_WAITING_FOR_NETWORK -> "PAUSED_WAITING_FOR_NETWORK"
127
- DownloadManager.PAUSED_WAITING_TO_RETRY -> "PAUSED_WAITING_TO_RETRY"
128
- else -> "UNKNOWN"
129
- }
130
- else -> "UNKNOWN"
131
- }
351
+ val result = Arguments.createMap()
352
+ result.putString("downloadId", downloadId)
353
+ result.putInt("status", status)
354
+ result.putInt("reason", reason)
355
+ result.putString("reasonText", reasonText)
356
+ result.putDouble("bytesDownloaded", bytesDownloadedSoFar.toLong().toDouble())
357
+ result.putDouble("bytesTotal", totalSizeBytes.toLong().toDouble())
358
+ result.putString("localUri", localUri)
359
+
360
+ return result
361
+ }
362
+
363
+ fun getReasonText(status: Int, reason: Int): String {
364
+ return when (status) {
365
+ DownloadManager.STATUS_FAILED -> when (reason) {
366
+ DownloadManager.ERROR_CANNOT_RESUME -> "ERROR_CANNOT_RESUME"
367
+ DownloadManager.ERROR_DEVICE_NOT_FOUND -> "ERROR_DEVICE_NOT_FOUND"
368
+ DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "ERROR_FILE_ALREADY_EXISTS"
369
+ DownloadManager.ERROR_FILE_ERROR -> "ERROR_FILE_ERROR"
370
+ DownloadManager.ERROR_HTTP_DATA_ERROR -> "ERROR_HTTP_DATA_ERROR"
371
+ DownloadManager.ERROR_INSUFFICIENT_SPACE -> "ERROR_INSUFFICIENT_SPACE"
372
+ DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "ERROR_TOO_MANY_REDIRECTS"
373
+ DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "ERROR_UNHANDLED_HTTP_CODE"
374
+ else -> "ERROR_UNKNOWN"
375
+ }
376
+ DownloadManager.STATUS_PAUSED -> when (reason) {
377
+ DownloadManager.PAUSED_QUEUED_FOR_WIFI -> "PAUSED_QUEUED_FOR_WIFI"
378
+ DownloadManager.PAUSED_UNKNOWN -> "PAUSED_UNKNOWN"
379
+ DownloadManager.PAUSED_WAITING_FOR_NETWORK -> "PAUSED_WAITING_FOR_NETWORK"
380
+ DownloadManager.PAUSED_WAITING_TO_RETRY -> "PAUSED_WAITING_TO_RETRY"
381
+ else -> "UNKNOWN"
382
+ }
383
+ else -> "UNKNOWN"
132
384
  }
385
+ }
133
386
  }