mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-21 00:36:24 +00:00
wip: android support
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Background Downloader Module
|
||||
|
||||
A native iOS module for downloading large files in the background using `NSURLSession` with background configuration.
|
||||
A native iOS and Android module for downloading large files in the background using `NSURLSession` (iOS) and `DownloadManager` (Android).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -10,6 +10,7 @@ A native iOS module for downloading large files in the background using `NSURLSe
|
||||
- **Cancellation**: Cancel individual or all downloads
|
||||
- **Custom Destination**: Optionally specify custom file paths
|
||||
- **Error Handling**: Comprehensive error reporting
|
||||
- **Cross-Platform**: Works on both iOS and Android
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -201,35 +202,57 @@ interface ActiveDownload {
|
||||
- Downloads continue when app is backgrounded or suspended
|
||||
- System may terminate downloads if app is force-quit
|
||||
|
||||
### Android Background Downloads
|
||||
|
||||
- Uses Android's `DownloadManager` API
|
||||
- Downloads are managed by the system and continue in the background
|
||||
- Shows download notification in the notification tray
|
||||
- Downloads continue even if the app is closed
|
||||
- Requires `INTERNET` permission (automatically added by Expo)
|
||||
|
||||
### Background Modes
|
||||
|
||||
The app's `Info.plist` already includes the required background mode:
|
||||
The app's `Info.plist` already includes the required background mode for iOS:
|
||||
|
||||
- `UIBackgroundModes`: `["audio", "fetch"]`
|
||||
|
||||
### File Storage
|
||||
|
||||
By default, downloaded files are saved to the app's Documents directory. You can specify a custom path using the `destinationPath` parameter.
|
||||
**iOS:** By default, downloaded files are saved to the app's Documents directory.
|
||||
|
||||
**Android:** By default, files are saved to the app's external files directory (accessible via `FileSystem.documentDirectory` in Expo).
|
||||
|
||||
You can specify a custom path using the `destinationPath` parameter on both platforms.
|
||||
|
||||
## Building
|
||||
|
||||
After adding this module, rebuild the iOS app:
|
||||
After adding this module, rebuild the app:
|
||||
|
||||
```bash
|
||||
# iOS
|
||||
npx expo prebuild -p ios
|
||||
npx expo run:ios
|
||||
|
||||
# Android
|
||||
npx expo prebuild -p android
|
||||
npx expo run:android
|
||||
```
|
||||
|
||||
Or install pods manually:
|
||||
Or install manually:
|
||||
|
||||
```bash
|
||||
# iOS
|
||||
cd ios
|
||||
pod install
|
||||
cd ..
|
||||
|
||||
# Android - prebuild handles everything
|
||||
npx expo prebuild -p android
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Background downloads may be cancelled if the user force-quits the app
|
||||
- Background downloads may be cancelled if the user force-quits the app (iOS)
|
||||
- The OS manages download priority and may pause downloads to save battery
|
||||
- Downloads over cellular can be disabled in the module configuration if needed
|
||||
- Android shows a system notification for ongoing downloads
|
||||
- Downloads over cellular are allowed by default on both platforms
|
||||
|
||||
46
modules/background-downloader/android/build.gradle
Normal file
46
modules/background-downloader/android/build.gradle
Normal file
@@ -0,0 +1,46 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
group = 'expo.modules.backgrounddownloader'
|
||||
version = '1.0.0'
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
|
||||
|
||||
apply from: expoModulesCorePlugin
|
||||
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
useDefaultAndroidSdkVersions()
|
||||
useCoreDependencies()
|
||||
useExpoPublishing()
|
||||
|
||||
android {
|
||||
namespace "expo.modules.backgrounddownloader"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ["-Xshow-kotlin-compiler-errors"]
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Automatically generated file. DO NOT MODIFY
|
||||
*/
|
||||
package expo.modules.backgrounddownloader;
|
||||
|
||||
public final class BuildConfig {
|
||||
public static final boolean DEBUG = Boolean.parseBoolean("true");
|
||||
public static final String LIBRARY_PACKAGE_NAME = "expo.modules.backgrounddownloader";
|
||||
public static final String BUILD_TYPE = "debug";
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="expo.modules.backgrounddownloader" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="24" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "AAPT_FRIENDLY_MERGED_MANIFESTS",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "expo.modules.backgrounddownloader",
|
||||
"variantName": "debug",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"outputFile": "AndroidManifest.xml"
|
||||
}
|
||||
],
|
||||
"elementType": "File"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
aarFormatVersion=1.0
|
||||
aarMetadataVersion=1.0
|
||||
minCompileSdk=1
|
||||
minCompileSdkExtension=0
|
||||
minAndroidGradlePluginVersion=1.0.0
|
||||
coreLibraryDesugaringEnabled=false
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
#Fri Oct 03 11:14:29 CEST 2025
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merger version="3"><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="main$Generated" generated="true" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/res"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="main" generated-set="main$Generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/res"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="debug$Generated" generated="true" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/debug/res"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="debug" generated-set="debug$Generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/debug/res"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="generated$Generated" generated="true" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/build/generated/res/resValues/debug"/></dataSet><dataSet aapt-namespace="http://schemas.android.com/apk/res-auto" config="generated" generated-set="generated$Generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/build/generated/res/resValues/debug"/></dataSet><mergedItems/></merger>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/assets"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/debug/assets"/></dataSet><dataSet config="generated" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/build/intermediates/shader_assets/debug/compileDebugShaders/out"/></dataSet></merger>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/jniLibs"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/debug/jniLibs"/></dataSet></merger>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merger version="3"><dataSet config="main" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/shaders"/></dataSet><dataSet config="debug" ignore_pattern="!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"><source path="/Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/debug/shaders"/></dataSet></merger>
|
||||
@@ -0,0 +1,2 @@
|
||||
R_DEF: Internal format may change without notice
|
||||
local
|
||||
@@ -0,0 +1,7 @@
|
||||
1<?xml version="1.0" encoding="utf-8"?>
|
||||
2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
3 package="expo.modules.backgrounddownloader" >
|
||||
4
|
||||
5 <uses-sdk android:minSdkVersion="24" />
|
||||
6
|
||||
7</manifest>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="expo.modules.backgrounddownloader" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="24" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
0 Warning/Error
|
||||
@@ -0,0 +1 @@
|
||||
expo.modules.backgrounddownloader
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Merging decision tree log ---
|
||||
manifest
|
||||
ADDED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml:1:1-2:12
|
||||
INJECTED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml:1:1-2:12
|
||||
package
|
||||
INJECTED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml
|
||||
uses-sdk
|
||||
INJECTED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml reason: use-sdk injection requested
|
||||
INJECTED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml
|
||||
INJECTED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml
|
||||
android:targetSdkVersion
|
||||
INJECTED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml
|
||||
android:minSdkVersion
|
||||
INJECTED from /Users/fredrikburmester/Documents/GitHub/streamyfin/modules/background-downloader/android/src/main/AndroidManifest.xml
|
||||
@@ -0,0 +1,3 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
package expo.modules.backgrounddownloader
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.content.ContextCompat
|
||||
import expo.modules.kotlin.Promise
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import java.io.File
|
||||
|
||||
class BackgroundDownloaderModule : Module() {
|
||||
private val context
|
||||
get() = requireNotNull(appContext.reactContext)
|
||||
|
||||
private val downloadManager: DownloadManager by lazy {
|
||||
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
}
|
||||
|
||||
private val downloadTasks = mutableMapOf<Long, DownloadTaskInfo>()
|
||||
private val progressHandler = Handler(Looper.getMainLooper())
|
||||
private val progressRunnables = mutableMapOf<Long, Runnable>()
|
||||
|
||||
private val downloadCompleteReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val downloadId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
|
||||
if (downloadId != -1L && downloadTasks.containsKey(downloadId)) {
|
||||
handleDownloadComplete(downloadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("BackgroundDownloader")
|
||||
|
||||
Events(
|
||||
"onDownloadProgress",
|
||||
"onDownloadComplete",
|
||||
"onDownloadError",
|
||||
"onDownloadStarted"
|
||||
)
|
||||
|
||||
OnCreate {
|
||||
registerDownloadReceiver()
|
||||
}
|
||||
|
||||
OnDestroy {
|
||||
unregisterDownloadReceiver()
|
||||
progressRunnables.values.forEach { progressHandler.removeCallbacks(it) }
|
||||
progressRunnables.clear()
|
||||
}
|
||||
|
||||
AsyncFunction("startDownload") { urlString: String, destinationPath: String?, promise: Promise ->
|
||||
try {
|
||||
val uri = Uri.parse(urlString)
|
||||
val request = DownloadManager.Request(uri).apply {
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||
setAllowedNetworkTypes(
|
||||
DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE
|
||||
)
|
||||
setAllowedOverMetered(true)
|
||||
setAllowedOverRoaming(true)
|
||||
|
||||
if (destinationPath != null) {
|
||||
val file = File(destinationPath)
|
||||
val directory = file.parentFile
|
||||
if (directory != null && !directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
setDestinationUri(Uri.fromFile(file))
|
||||
} else {
|
||||
val fileName = uri.lastPathSegment ?: "download_${System.currentTimeMillis()}"
|
||||
setDestinationInExternalFilesDir(
|
||||
context,
|
||||
null,
|
||||
fileName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
|
||||
downloadTasks[downloadId] = DownloadTaskInfo(
|
||||
url = urlString,
|
||||
destinationPath = destinationPath
|
||||
)
|
||||
|
||||
startProgressTracking(downloadId)
|
||||
|
||||
sendEvent("onDownloadStarted", mapOf(
|
||||
"taskId" to downloadId.toInt(),
|
||||
"url" to urlString
|
||||
))
|
||||
|
||||
promise.resolve(downloadId.toInt())
|
||||
} catch (e: Exception) {
|
||||
promise.reject("DOWNLOAD_ERROR", "Failed to start download: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
Function("cancelDownload") { taskId: Int ->
|
||||
val downloadId = taskId.toLong()
|
||||
if (downloadTasks.containsKey(downloadId)) {
|
||||
downloadManager.remove(downloadId)
|
||||
stopProgressTracking(downloadId)
|
||||
downloadTasks.remove(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
Function("cancelAllDownloads") {
|
||||
val downloadIds = downloadTasks.keys.toList()
|
||||
downloadIds.forEach { downloadId ->
|
||||
downloadManager.remove(downloadId)
|
||||
stopProgressTracking(downloadId)
|
||||
}
|
||||
downloadTasks.clear()
|
||||
}
|
||||
|
||||
AsyncFunction("getActiveDownloads") { promise: Promise ->
|
||||
try {
|
||||
val activeDownloads = mutableListOf<Map<String, Any>>()
|
||||
|
||||
downloadTasks.forEach { (downloadId, taskInfo) ->
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
val cursor = downloadManager.query(query)
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
val status = if (statusIndex >= 0) cursor.getInt(statusIndex) else -1
|
||||
|
||||
activeDownloads.add(mapOf(
|
||||
"taskId" to downloadId.toInt(),
|
||||
"url" to taskInfo.url,
|
||||
"state" to getStateString(status)
|
||||
))
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
promise.resolve(activeDownloads)
|
||||
} catch (e: Exception) {
|
||||
promise.reject("GET_DOWNLOADS_ERROR", "Failed to get active downloads: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDownloadReceiver() {
|
||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
downloadCompleteReceiver,
|
||||
filter,
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
} else {
|
||||
context.registerReceiver(downloadCompleteReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterDownloadReceiver() {
|
||||
try {
|
||||
context.unregisterReceiver(downloadCompleteReceiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Receiver not registered, ignore
|
||||
}
|
||||
}
|
||||
|
||||
private fun startProgressTracking(downloadId: Long) {
|
||||
val runnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!downloadTasks.containsKey(downloadId)) {
|
||||
return
|
||||
}
|
||||
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
val cursor = downloadManager.query(query)
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
val status = if (statusIndex >= 0) cursor.getInt(statusIndex) else -1
|
||||
|
||||
val bytesDownloadedIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
||||
val bytesDownloaded = if (bytesDownloadedIndex >= 0) cursor.getLong(bytesDownloadedIndex) else 0L
|
||||
|
||||
val totalBytesIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||
val totalBytes = if (totalBytesIndex >= 0) cursor.getLong(totalBytesIndex) else 0L
|
||||
|
||||
if (status == DownloadManager.STATUS_RUNNING && totalBytes > 0) {
|
||||
val progress = bytesDownloaded.toDouble() / totalBytes.toDouble()
|
||||
|
||||
sendEvent("onDownloadProgress", mapOf(
|
||||
"taskId" to downloadId.toInt(),
|
||||
"bytesWritten" to bytesDownloaded,
|
||||
"totalBytes" to totalBytes,
|
||||
"progress" to progress
|
||||
))
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (status == DownloadManager.STATUS_FAILED) {
|
||||
val reasonIndex = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
|
||||
val reason = if (reasonIndex >= 0) cursor.getInt(reasonIndex) else -1
|
||||
|
||||
cursor.close()
|
||||
stopProgressTracking(downloadId)
|
||||
|
||||
sendEvent("onDownloadError", mapOf(
|
||||
"taskId" to downloadId.toInt(),
|
||||
"error" to getErrorString(reason)
|
||||
))
|
||||
|
||||
downloadTasks.remove(downloadId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
|
||||
// Continue tracking if still in progress
|
||||
if (downloadTasks.containsKey(downloadId)) {
|
||||
progressHandler.postDelayed(this, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progressRunnables[downloadId] = runnable
|
||||
progressHandler.post(runnable)
|
||||
}
|
||||
|
||||
private fun stopProgressTracking(downloadId: Long) {
|
||||
progressRunnables[downloadId]?.let { runnable ->
|
||||
progressHandler.removeCallbacks(runnable)
|
||||
progressRunnables.remove(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDownloadComplete(downloadId: Long) {
|
||||
stopProgressTracking(downloadId)
|
||||
|
||||
val taskInfo = downloadTasks[downloadId]
|
||||
if (taskInfo == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
val cursor = downloadManager.query(query)
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
val status = if (statusIndex >= 0) cursor.getInt(statusIndex) else -1
|
||||
|
||||
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
val uriIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||
val localUri = if (uriIndex >= 0) cursor.getString(uriIndex) else null
|
||||
|
||||
if (localUri != null) {
|
||||
val filePath = Uri.parse(localUri).path ?: localUri
|
||||
|
||||
sendEvent("onDownloadComplete", mapOf(
|
||||
"taskId" to downloadId.toInt(),
|
||||
"filePath" to filePath,
|
||||
"url" to taskInfo.url
|
||||
))
|
||||
} else {
|
||||
sendEvent("onDownloadError", mapOf(
|
||||
"taskId" to downloadId.toInt(),
|
||||
"error" to "Could not retrieve downloaded file path"
|
||||
))
|
||||
}
|
||||
} else if (status == DownloadManager.STATUS_FAILED) {
|
||||
val reasonIndex = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
|
||||
val reason = if (reasonIndex >= 0) cursor.getInt(reasonIndex) else -1
|
||||
|
||||
sendEvent("onDownloadError", mapOf(
|
||||
"taskId" to downloadId.toInt(),
|
||||
"error" to getErrorString(reason)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
downloadTasks.remove(downloadId)
|
||||
}
|
||||
|
||||
private fun getStateString(status: Int): String {
|
||||
return when (status) {
|
||||
DownloadManager.STATUS_RUNNING -> "running"
|
||||
DownloadManager.STATUS_PAUSED -> "suspended"
|
||||
DownloadManager.STATUS_PENDING -> "suspended"
|
||||
DownloadManager.STATUS_SUCCESSFUL -> "completed"
|
||||
DownloadManager.STATUS_FAILED -> "completed"
|
||||
else -> "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getErrorString(reason: Int): String {
|
||||
return when (reason) {
|
||||
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
|
||||
DownloadManager.ERROR_DEVICE_NOT_FOUND -> "No external storage device found"
|
||||
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
|
||||
DownloadManager.ERROR_FILE_ERROR -> "Storage error"
|
||||
DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP data error"
|
||||
DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient storage space"
|
||||
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
|
||||
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP response code"
|
||||
DownloadManager.ERROR_UNKNOWN -> "Unknown error"
|
||||
else -> "Download failed (code: $reason)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadTaskInfo(
|
||||
val url: String,
|
||||
val destinationPath: String?
|
||||
)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"name": "background-downloader",
|
||||
"version": "1.0.0",
|
||||
"platforms": ["ios"],
|
||||
"platforms": ["ios", "android"],
|
||||
"ios": {
|
||||
"modules": ["BackgroundDownloaderModule"],
|
||||
"appDelegateSubscribers": ["BackgroundDownloaderAppDelegate"]
|
||||
},
|
||||
"android": {
|
||||
"modules": ["expo.modules.backgrounddownloader.BackgroundDownloaderModule"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user