diff --git a/modules/background-downloader/README.md b/modules/background-downloader/README.md
index c3b10ff2..53e419b5 100644
--- a/modules/background-downloader/README.md
+++ b/modules/background-downloader/README.md
@@ -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
diff --git a/modules/background-downloader/android/build.gradle b/modules/background-downloader/android/build.gradle
new file mode 100644
index 00000000..4f1d78fb
--- /dev/null
+++ b/modules/background-downloader/android/build.gradle
@@ -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"
+ }
+}
+
diff --git a/modules/background-downloader/android/build/generated/source/buildConfig/debug/expo/modules/backgrounddownloader/BuildConfig.java b/modules/background-downloader/android/build/generated/source/buildConfig/debug/expo/modules/backgrounddownloader/BuildConfig.java
new file mode 100644
index 00000000..1074b8a6
--- /dev/null
+++ b/modules/background-downloader/android/build/generated/source/buildConfig/debug/expo/modules/backgrounddownloader/BuildConfig.java
@@ -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";
+}
diff --git a/modules/background-downloader/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml b/modules/background-downloader/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml
new file mode 100644
index 00000000..7a835763
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/background-downloader/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json b/modules/background-downloader/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json
new file mode 100644
index 00000000..0addb52c
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json
@@ -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"
+}
diff --git a/modules/background-downloader/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties b/modules/background-downloader/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
new file mode 100644
index 00000000..1211b1ef
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
@@ -0,0 +1,6 @@
+aarFormatVersion=1.0
+aarMetadataVersion=1.0
+minCompileSdk=1
+minCompileSdkExtension=0
+minAndroidGradlePluginVersion=1.0.0
+coreLibraryDesugaringEnabled=false
diff --git a/modules/background-downloader/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json b/modules/background-downloader/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
new file mode 100644
index 00000000..0967ef42
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
@@ -0,0 +1 @@
+{}
diff --git a/modules/background-downloader/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar b/modules/background-downloader/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar
new file mode 100644
index 00000000..08e669c5
Binary files /dev/null and b/modules/background-downloader/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar differ
diff --git a/modules/background-downloader/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt b/modules/background-downloader/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/background-downloader/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/modules/background-downloader/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
new file mode 100644
index 00000000..baf0a22e
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
@@ -0,0 +1 @@
+#Fri Oct 03 11:14:29 CEST 2025
diff --git a/modules/background-downloader/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml b/modules/background-downloader/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
new file mode 100644
index 00000000..9e84793f
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/modules/background-downloader/android/build/intermediates/incremental/mergeDebugAssets/merger.xml b/modules/background-downloader/android/build/intermediates/incremental/mergeDebugAssets/merger.xml
new file mode 100644
index 00000000..a6cb5856
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/incremental/mergeDebugAssets/merger.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/modules/background-downloader/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml b/modules/background-downloader/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml
new file mode 100644
index 00000000..9c8e5811
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/modules/background-downloader/android/build/intermediates/incremental/mergeDebugShaders/merger.xml b/modules/background-downloader/android/build/intermediates/incremental/mergeDebugShaders/merger.xml
new file mode 100644
index 00000000..6659d342
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/incremental/mergeDebugShaders/merger.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/modules/background-downloader/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt b/modules/background-downloader/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt
new file mode 100644
index 00000000..78ac5b8b
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt
@@ -0,0 +1,2 @@
+R_DEF: Internal format may change without notice
+local
diff --git a/modules/background-downloader/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt b/modules/background-downloader/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt
new file mode 100644
index 00000000..442ccc37
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt
@@ -0,0 +1,7 @@
+1
+2
+4
+5
+6
+7
diff --git a/modules/background-downloader/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml b/modules/background-downloader/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml
new file mode 100644
index 00000000..7a835763
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/background-downloader/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json b/modules/background-downloader/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json
new file mode 100644
index 00000000..fe51488c
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json
@@ -0,0 +1 @@
+[]
diff --git a/modules/background-downloader/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt b/modules/background-downloader/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt
new file mode 100644
index 00000000..08f4ebea
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt
@@ -0,0 +1 @@
+0 Warning/Error
\ No newline at end of file
diff --git a/modules/background-downloader/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt b/modules/background-downloader/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt
new file mode 100644
index 00000000..bcf442d8
--- /dev/null
+++ b/modules/background-downloader/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt
@@ -0,0 +1 @@
+expo.modules.backgrounddownloader
diff --git a/modules/background-downloader/android/build/outputs/logs/manifest-merger-debug-report.txt b/modules/background-downloader/android/build/outputs/logs/manifest-merger-debug-report.txt
new file mode 100644
index 00000000..2c3c07f4
--- /dev/null
+++ b/modules/background-downloader/android/build/outputs/logs/manifest-merger-debug-report.txt
@@ -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
diff --git a/modules/background-downloader/android/src/main/AndroidManifest.xml b/modules/background-downloader/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..703e8232
--- /dev/null
+++ b/modules/background-downloader/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/BackgroundDownloaderModule.kt b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/BackgroundDownloaderModule.kt
new file mode 100644
index 00000000..2dae8803
--- /dev/null
+++ b/modules/background-downloader/android/src/main/java/expo/modules/backgrounddownloader/BackgroundDownloaderModule.kt
@@ -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()
+ private val progressHandler = Handler(Looper.getMainLooper())
+ private val progressRunnables = mutableMapOf()
+
+ 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