package com.example.myapp.utils
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.Environment
import androidx.core.content.getSystemService
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import java.io.File
class DownloadHelper(private val context: Context) {
private val downloadManager = context.getSystemService<DownloadManager>()!!
fun downloadFile(
url: String,
fileName: String,
description: String = "Downloading file",
mimeType: String? = null,
showNotification: Boolean = true
): Long {
val request = DownloadManager.Request(Uri.parse(url))
.setTitle(fileName)
.setDescription(description)
.setNotificationVisibility(
if (showNotification)
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
else
DownloadManager.Request.VISIBILITY_HIDDEN
)
.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
fileName
)
.setAllowedOverMetered(true)
.setAllowedOverRoaming(false)
mimeType?.let { request.setMimeType(it) }
return downloadManager.enqueue(request)
}
fun observeDownload(downloadId: Long): Flow<DownloadStatus> = callbackFlow {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id == downloadId) {
val status = getDownloadStatus(downloadId)
trySend(status)
if (status is DownloadStatus.Completed ||
status is DownloadStatus.Failed) {
close()
}
}
}
}
context.registerReceiver(
receiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
// Send initial status
trySend(getDownloadStatus(downloadId))
awaitClose {
context.unregisterReceiver(receiver)
}
}
fun getDownloadStatus(downloadId: Long): DownloadStatus {
val query = DownloadManager.Query().setFilterById(downloadId)
val cursor: Cursor = downloadManager.query(query) ?:
return DownloadStatus.Failed("Download not found")
if (!cursor.moveToFirst()) {
cursor.close()
return DownloadStatus.Failed("Download not found")
}
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = cursor.getInt(statusIndex)
val result = when (status) {
DownloadManager.STATUS_PENDING ->
DownloadStatus.Pending
DownloadManager.STATUS_RUNNING -> {
val bytesDownloaded = cursor.getLong(
cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
)
val bytesTotal = cursor.getLong(
cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
)
val progress = if (bytesTotal > 0) {
(bytesDownloaded * 100 / bytesTotal).toInt()
} else 0
DownloadStatus.Running(progress, bytesDownloaded, bytesTotal)
}
DownloadManager.STATUS_SUCCESSFUL -> {
val uriIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
val uri = cursor.getString(uriIndex)
DownloadStatus.Completed(uri)
}
DownloadManager.STATUS_FAILED -> {
val reasonIndex = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
val reason = cursor.getInt(reasonIndex)
DownloadStatus.Failed(getFailureReason(reason))
}
else -> DownloadStatus.Failed("Unknown status")
}
cursor.close()
return result
}
private fun getFailureReason(reason: Int): String = when (reason) {
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
DownloadManager.ERROR_DEVICE_NOT_FOUND -> "No external storage"
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
DownloadManager.ERROR_FILE_ERROR -> "File error"
DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP data error"
DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space"
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP code"
else -> "Unknown error"
}
fun cancelDownload(downloadId: Long) {
downloadManager.remove(downloadId)
}
}
sealed class DownloadStatus {
object Pending : DownloadStatus()
data class Running(
val progress: Int,
val bytesDownloaded: Long,
val bytesTotal: Long
) : DownloadStatus()
data class Completed(val uri: String) : DownloadStatus()
data class Failed(val error: String) : DownloadStatus()
}
DownloadManager handles long-running downloads with notification progress. I create DownloadManager.Request with URI and destination. setNotificationVisibility() controls progress notifications. Headers customize HTTP requests. Network type requirements prevent downloads on metered connections. The manager returns a download ID for tracking. BroadcastReceiver listens for ACTIONDOWNLOADCOMPLETE. Queries check download status—pending, running, successful, failed. The system handles retries, resumption, and network changes. Downloads persist across app restarts. DownloadManager simplifies complex download scenarios, offloading work to the system while providing fine-grained control when needed.