Code
Helper code related to downloading files.
- DownloadModel.kt
- DownloadUtil.kt
DownloadModel.kt
package com.hanmajid.androidnotebook
import android.app.DownloadManager
import android.database.Cursor
import android.os.Parcelable
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.text.SimpleDateFormat
import java.util.Calendar
/**
* Custom download data model retrieved from [DownloadManager.Query].
*
* Add this to your app/build.gradle.kts file to use `@Parcelize`:
* ```
* plugins {
* id("kotlin-parcelize")
* }
* ```
*/
@Parcelize
data class DownloadModel(
val id: Long?,
val bytesDownloadedSoFar: Long?,
val description: String?,
val localUri: String?,
val mediaProviderUri: String?,
val mediaType: String?,
val reason: Int?,
val status: Int?,
val title: String?,
val totalSizeBytes: Long?,
val uri: String?,
val lastModifiedTimestamp: Long?,
) : Parcelable {
@IgnoredOnParcel
val reasonStr = when (reason) {
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
DownloadManager.ERROR_DEVICE_NOT_FOUND -> "No external storage device was found"
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
DownloadManager.ERROR_FILE_ERROR -> "A storage issue arises which doesn't fit under any other error code"
DownloadManager.ERROR_HTTP_DATA_ERROR -> "An error receiving or processing data occurred at the HTTP level"
DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient storage space"
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "An HTTP code was received that download manager can't handle"
DownloadManager.ERROR_UNKNOWN -> "The download has completed with an error that doesn't fit under any other error code"
DownloadManager.PAUSED_QUEUED_FOR_WIFI -> "The download exceeds a size limit for downloads over the mobile network and the download manager is waiting for a Wi-Fi connection to proceed"
DownloadManager.PAUSED_UNKNOWN -> "The download is paused for some other reason"
DownloadManager.PAUSED_WAITING_FOR_NETWORK -> "The download is paused for some other reason"
DownloadManager.PAUSED_WAITING_TO_RETRY -> "The download is paused because some network error occurred and the download manager is waiting before retrying the request"
else -> {
"HTTP code: $reason"
}
}
@IgnoredOnParcel
val statusStr = when (status) {
DownloadManager.STATUS_FAILED -> "Failed"
DownloadManager.STATUS_PAUSED -> "Paused"
DownloadManager.STATUS_PENDING -> "Pending"
DownloadManager.STATUS_RUNNING -> "Running"
DownloadManager.STATUS_SUCCESSFUL -> "Successful"
else -> "Unknown"
}
val lastModifiedTimestampStr: String
get() {
if (lastModifiedTimestamp == null) return ""
val cal = Calendar.getInstance().apply {
timeInMillis = lastModifiedTimestamp
}
val formatter = SimpleDateFormat("MMMM d, yyyy h:mm")
return formatter.format(cal.time)
}
@IgnoredOnParcel
val isRunning = status == DownloadManager.STATUS_RUNNING
@IgnoredOnParcel
val isSuccessful = status == DownloadManager.STATUS_SUCCESSFUL
val progress: Float
get() {
return if (isRunning) {
val downloaded = bytesDownloadedSoFar ?: -1
val total = totalSizeBytes ?: -1
if (downloaded > 0 && total > 0) {
downloaded.toFloat() / total
} else {
0f
}
} else {
0f
}
}
companion object {
/**
* Create [DownloadModel] from [cursor].
*/
fun fromCursor(cursor: Cursor): DownloadModel {
return DownloadModel(
bytesDownloadedSoFar = cursor.getLongOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)),
description = cursor.getStringOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION)),
id = cursor.getLongOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_ID)),
lastModifiedTimestamp = cursor.getLongOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)),
localUri = cursor.getStringOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)),
mediaProviderUri = cursor.getStringOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)),
mediaType = cursor.getStringOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE)),
reason = cursor.getIntOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_REASON)),
status = cursor.getIntOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)),
title = cursor.getStringOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE)),
totalSizeBytes = cursor.getLongOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)),
uri = cursor.getStringOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_URI)),
)
}
}
}
DownloadUtil.kt
package com.hanmajid.androidnotebook
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import androidx.core.content.ContextCompat.startActivity
/**
* Download-related utility object.
*/
object DownloadUtil {
/**
* Launch an Activity to displays all downloads.
*/
fun launchDownloadsActivity(context: Context) {
startActivity(
context,
Intent(DownloadManager.ACTION_VIEW_DOWNLOADS).apply {
putExtra(DownloadManager.INTENT_EXTRAS_SORT_BY_SIZE, true)
},
null
)
}
/**
* Get application's download in [DownloadManager].
*/
fun getDownloads(
context: Context,
filterStatus: Int? = null,
filterId: Array<Long>? = null,
): MutableList<DownloadModel> {
val downloadManager = context.getSystemService(DownloadManager::class.java)
?: throw Exception("DownloadManager not found")
val query = DownloadManager.Query().apply {
if (filterStatus != null) {
setFilterByStatus(filterStatus)
}
if (filterId?.isNotEmpty() == true) {
setFilterById(*filterId.toLongArray())
}
}
val cursor = downloadManager.query(query)
val downloads = mutableListOf<DownloadModel>()
while (cursor.moveToNext()) {
downloads.add(DownloadModel.fromCursor(cursor))
}
return downloads
}
/**
* Remove [download] from [DownloadManager].
*/
fun removeDownload(context: Context, download: DownloadModel): Int {
val downloadManager = context.getSystemService(DownloadManager::class.java)
?: throw Exception("DownloadManager not found")
val id = download.id ?: throw Exception("Download id is null")
return downloadManager.remove(id)
}
/**
* Enqueue new download to public external directory.
*/
fun enqueuePublic(
context: Context,
uri: Uri,
filename: String,
title: String? = null,
description: String? = null,
mimeType: String? = null,
dirType: String = Environment.DIRECTORY_DOWNLOADS,
notificationVisibility: Int = DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
requiresCharging: Boolean = false,
requiresDeviceIdle: Boolean = false,
allowedOverRoaming: Boolean = true,
allowedOverMetered: Boolean = true,
): Long {
val downloadManager = context.getSystemService(DownloadManager::class.java)
?: throw Exception("DownloadManager not found")
return downloadManager.enqueue(
DownloadManager.Request(uri)
.setDestinationInExternalPublicDir(
dirType,
filename,
)
.setTitle(title)
.setDescription(description)
.setRequiresCharging(requiresCharging)
.setRequiresDeviceIdle(requiresDeviceIdle)
.setAllowedOverRoaming(allowedOverRoaming)
.setAllowedOverMetered(allowedOverMetered)
.setMimeType(mimeType)
.setNotificationVisibility(notificationVisibility)
)
}
/**
* Enqueue new download to application's external file directory.
*/
fun enqueueApplication(
context: Context,
uri: Uri,
dirType: String,
filename: String,
title: String? = null,
description: String? = null,
mimeType: String? = null,
notificationVisibility: Int = DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
requiresCharging: Boolean = false,
requiresDeviceIdle: Boolean = false,
allowedOverRoaming: Boolean = true,
allowedOverMetered: Boolean = true,
): Long {
val downloadManager = context.getSystemService(DownloadManager::class.java)
?: throw Exception("DownloadManager not found")
return downloadManager.enqueue(
DownloadManager.Request(uri)
.setDestinationInExternalFilesDir(
context,
dirType,
filename,
)
.setTitle(title)
.setDescription(description)
.setRequiresCharging(requiresCharging)
.setRequiresDeviceIdle(requiresDeviceIdle)
.setAllowedOverRoaming(allowedOverRoaming)
.setAllowedOverMetered(allowedOverMetered)
.setMimeType(mimeType)
.setNotificationVisibility(notificationVisibility)
)
}
}