介绍
DownloadManager 是系统提供的下载服务, 在下载过程中,app 被杀进程也不会影响下载的过程,app 要做的只是把下载任务放到系统下载服务的队列里,下载进度可以通过 DownloadManager.Query() 方法来进行本地查询
代码实现
package com.atoto.ota.download
import android.app.DownloadManager
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.lifecycle.MutableLiveData
import com.atoto.ota.cached.SpUtils
import com.atoto.ota.network.model.GetLastVersionReturn
import com.atoto.ota.ui.state.OTAUiState
import com.atoto.ota.utils.AppUtils
import com.atoto.ota.utils.FileUtils
import com.atoto.ota.utils.LogUtils
import java.io.File
import java.util.Timer
import java.util.TimerTask
/**
* 基于 Android 官方提供的 DownloadManager 实现的下载管理器
* 可实现断点续传
*/
class SystemDownloadManager private constructor() {
companion object {
const val TAG = "SystemDownloadManager="
private val SP_KEY_DOWNLOAD_ID = "downloadId"
val instance: SystemDownloadManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
SystemDownloadManager()
}
}
var url:String = ""
var outputPath:OutputInfo? = null
var title = "通知栏标题"
var desc = "通知栏描述"
val downloadLiveData = MutableLiveData<OTAUiState>()
var newVersion = ""
var downloadId: Long = -1
get() {
return SpUtils.instance.getLong(SP_KEY_DOWNLOAD_ID, -1)?:-1
}
set(value) {
SpUtils.instance.putLong(SP_KEY_DOWNLOAD_ID, value)
field = value
}
private var downloadedBytesCount = 0L
var pause = false
private set
var downloadManagerCallback:DownloadManagerCallback ?= null
var cachedLatestVersion: File?= null
/**
* 用于定时查询下载进度
*/
private var timer: Timer?= null
/**
* 检测本地是否存在更新的版本
*/
fun checkIfExistLatestVersion(callback:(Boolean, String)->Unit) {
val downloadFolderPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
val downloadDir = File(downloadFolderPath)
// 获取下载目录下的所有文件, 并对版本排序
downloadDir.listFiles()?.let { listFiles->
val sortedList = listFiles.filter { it.name.startsWith("atoto_ota") }.sortedWith(compareBy { FileUtils.VersionSort(FileUtils.getVersionStrByUrl(it.absolutePath)) })
cachedLatestVersion = sortedList.lastOrNull()
}
// 缓存里存在最新版本
if (cachedLatestVersion != null) {
// 对比当前版本
FileUtils.compareVersion(version1 = FileUtils.getVersionStrByUrl(cachedLatestVersion?.absolutePath?:""), version2 = FileUtils.getVersionStrByUrl(
GetLastVersionReturn.currentApkName())).let {
if (it < 0) { // 缓存里的版本比当前版本低
callback.invoke(false, "")
} else { // 缓存里的版本比当前版本高
callback.invoke(true, "V${FileUtils.getVersionStrByUrl(cachedLatestVersion?.absolutePath?:"")}")
}
}
LogUtils.d(TAG, "${cachedLatestVersion?.absolutePath} md5 ${FileUtils.getFileMD5(cachedLatestVersion)}")
} else {
callback.invoke(false, "")
}
}
/**
* 开始下载
*/
fun startDownload(downloadedBytes:Long = 0) {
if (url.isBlank()) {
return
}
if (outputPath == null || outputPath?.dirType.isNullOrBlank() || outputPath?.subPath.isNullOrBlank()){
return
}
val dirType = outputPath?.dirType?:""
val subPath = outputPath?.subPath?:""
val downloadManager = downloadManager()
val request =
DownloadManager.Request(Uri.parse(url)).setTitle(title)
.setDescription(desc)
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
.setDestinationInExternalPublicDir(dirType, subPath)
.setVisibleInDownloadsUi(true)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
if (downloadedBytes > 0) {
request.addRequestHeader("Range", "bytes=$downloadedBytes-")
}
// 将下载请求添加到任务队列
downloadId = downloadManager?.enqueue(request)?:-1L
LogUtils.d(TAG, "startDownload downloadId:$downloadId, downloadedBytes:${downloadedBytes}")
startProgressTimer()
}
/**
* 检查下载任务是否存在
*/
fun checkIfDownloadTaskExist() {
LogUtils.d(TAG, "checkIfDownloadTaskExist")
if (downloadId != -1L) {
startProgressTimer()
}
}
/**
* 暂停下载
*/
fun pauseDownload() {
pause = true
LogUtils.d(TAG, "pauseDownload ${downloadId}")
downloadedBytesCount = getDownloadedBytes(downloadId)
val downloadManager = downloadManager()
downloadManager?.remove(downloadId)
downloadManagerCallback?.onDownloadPaused()
disableProgressTimer()
}
/**
* 继续下载
*/
fun resumeDownload() {
pause = false
LogUtils.d(TAG, "resumeDownload ${downloadId}")
// 从本地获取已下载的字节数及其他信息
startDownload(downloadedBytesCount)
downloadManagerCallback?.onDownloadRunning()
startProgressTimer()
}
/**
* 创建 cursor
*/
private fun createCursor(downloadId:Long):Cursor? {
// 获取Query
val query = DownloadManager.Query()
.setFilterById(downloadId) // 根据下载ID过滤
.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL or DownloadManager.STATUS_RUNNING or DownloadManager.STATUS_FAILED or DownloadManager.STATUS_PAUSED or DownloadManager.STATUS_PENDING or DownloadManager.STATUS_RUNNING)
// 获取下载管理器
val downloadManager =
downloadManager()
// 查询-Cursor的使用方法与SEQLite一致
return downloadManager?.query(query)
}
private fun getDownloadedBytes(downloadId:Long):Long {
var retBytesCount = 0L
createCursor(downloadId = downloadId)?.use { cursor ->
if(cursor.moveToFirst()){
retBytesCount = cursor.getLongOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))?:0L
LogUtils.d(TAG, "getDownloadedBytes retBytesCount=$retBytesCount")
}
}
return retBytesCount;
}
/**
* 开启下载进度查询定时器
*/
private fun startProgressTimer() {
LogUtils.d(TAG, "startProgressTimer downloadId:$downloadId")
val downloadManager = downloadManager()
timer?.cancel()
timer?.purge()
timer = Timer()
timer?.schedule(object : TimerTask() {
override fun run() {
createCursor(downloadId = downloadId)?.use { cursor->
if (cursor.moveToFirst()) {
val status = cursor.getIntOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))?:0
when (status) {
DownloadManager.STATUS_SUCCESSFUL-> {
LogUtils.d(TAG, "DownloadManager.STATUS_SUCCESSFUL")
timer?.cancel()
timer?.purge()
}
DownloadManager.STATUS_FAILED-> {
LogUtils.d(TAG, "DownloadManager.STATUS_FAILED")
}
else-> {
val downloadedSoFar = cursor.getLongOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))?:0L
val totalBytes = cursor.getLongOrNull(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))?:0L
if (totalBytes > 0 && downloadedSoFar >= downloadedBytesCount ) {
downloadedBytesCount = downloadedSoFar
downloadManagerCallback?.onDownloadProgress(progress = downloadedBytesCount, total = totalBytes)
}
// 根据状态和下载进度处理任务
LogUtils.d(TAG, "status:$status, ($downloadedBytesCount/$totalBytes)")
}
}
}
}
}
}, 0, 1000)
}
private fun downloadManager() =
AppUtils.context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager?
fun disableProgressTimer() {
timer?.cancel()
timer?.purge()
}
}
/**
*
* @param dirType Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MOVIES, etc.
* @param subPath xxx/xxx.apk
*/
data class OutputInfo(val dirType:String, val subPath:String)
interface DownloadManagerCallback {
fun onDownloadProgress(progress:Long, total:Long)
fun onDownloadFinished()
fun onDownloadPaused()
fun onDownloadRunning()
}
注册广播监听
package com.atoto.ota.receiver
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.atoto.ota.MyApplication
import com.atoto.ota.download.SystemDownloadManager
import com.atoto.ota.ui.state.OTAUiState
import com.atoto.ota.utils.LogUtils
class DownloadManagerReceiver: BroadcastReceiver() {
@SuppressLint("Range")
override fun onReceive(context: Context?, intent: Intent?) {
LogUtils.d("OTAVersionActivityViewModel=", "DownloadManagerReceiver onReceive ${intent?.action}")
intent?.extras?.let {
for (key in it.keySet()) {
LogUtils.d("OTAVersionActivityViewModel=", "DownloadManagerReceiver onReceive key=$key value=${it.get(key)}")
}
}
val downloadId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)?:-1
val downloadManager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager?
downloadManager?.let { downloadManager->
val query = DownloadManager.Query()
query.setFilterById(downloadId)
query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL or DownloadManager.STATUS_RUNNING or DownloadManager.STATUS_FAILED or DownloadManager.STATUS_PAUSED or DownloadManager.STATUS_PENDING or DownloadManager.STATUS_RUNNING)
val cursor = downloadManager.query(query)
val moveToFirst = cursor.moveToFirst()
LogUtils.d("OTAVersionActivityViewModel=", "setFilterById ${downloadId}, moveToFirst:${moveToFirst}")
if (moveToFirst) {
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
if (status == DownloadManager.STATUS_SUCCESSFUL) {
// 下载成功
LogUtils.d("OTAVersionActivityViewModel=", "DownloadManager.STATUS_SUCCESSFUL")
} else if (status == DownloadManager.STATUS_FAILED) {
// 下载失败
LogUtils.d("OTAVersionActivityViewModel=", "DownloadManager.STATUS_FAILED")
} else if (status == DownloadManager.STATUS_PAUSED) {
// 下载暂停
LogUtils.d("OTAVersionActivityViewModel=", "DownloadManager.STATUS_PAUSED")
} else if (status == DownloadManager.STATUS_PENDING || status == DownloadManager.STATUS_RUNNING) {
// 下载进行中
LogUtils.d("OTAVersionActivityViewModel=", "DownloadManager.STATUS_PENDING || status == DownloadManager.STATUS_RUNNING")
}
}
cursor.close()
}
if (SystemDownloadManager.instance.pause) {
return
}
SystemDownloadManager.instance.downloadLiveData.postValue(OTAUiState.DownloadFinished(newVersion = SystemDownloadManager.instance.newVersion))
}
}
AndroidManifest.xml
<receiver android:name=".receiver.DownloadManagerReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
使用方法
缓存版本检测
SystemDownloadManager.instance.checkIfExistLatestVersion { hasNewVersion, version ->
if (hasNewVersion) {
// 本地存在新版本
} else {
// 本地不存在新版本
}
}
SystemDownloadManager.instance.downloadManagerCallback = object : DownloadManagerCallback {
override fun onDownloadProgress(progress: Long, total: Long) {
}
override fun onDownloadFinished() {
}
override fun onDownloadPaused() {
}
override fun onDownloadRunning() {
}
}
SystemDownloadManager.instance.checkIfDownloadTaskExist()
下载方法
SystemDownloadManager.instance.newVersion = "服务端返回的apk版本号,如 V1.0.1"
SystemDownloadManager.instance.url = "apk 文件下载 url"
SystemDownloadManager.instance.outputPath = OutputInfo(Environment.DIRECTORY_DOWNLOADS, "apk 文件名,如 xxx/xxx.apk")
SystemDownloadManager.instance.startDownload()
暂停下载
SystemDownloadManager.instance.pauseDownload()
恢复下载
SystemDownloadManager.instance.resumeDownload()