@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 +31 -0
- package/README.md +15 -66
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/com/eko/Downloader.kt +343 -90
- package/android/src/main/java/com/eko/RNBackgroundDownloaderModuleImpl.kt +856 -722
- package/android/src/main/java/com/eko/ResumableDownloadService.kt +348 -0
- package/android/src/main/java/com/eko/ResumableDownloader.kt +348 -0
- package/android/src/main/java/com/eko/handlers/OnBegin.kt +2 -2
- package/android/src/main/java/com/eko/handlers/OnProgress.kt +15 -1
- package/package.json +1 -1
- package/src/index.ts +9 -8
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
|
|
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
|
|
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>`
|
|
510
|
+
### `pause(): Promise<void>`
|
|
556
511
|
Pauses the download. Returns a promise that resolves when the pause operation is complete.
|
|
557
512
|
|
|
558
|
-
**Note:**
|
|
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>`
|
|
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:**
|
|
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
|
|
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
|
-
####
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
311
|
+
fun checkDownloadStatus(downloadId: Long): WritableMap {
|
|
312
|
+
val query = DownloadManager.Query()
|
|
313
|
+
query.setFilterById(downloadId)
|
|
91
314
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
}
|