在Android应用开发中,实现文件下载功能是常见的需求,如下载图片、视频、文档、应用安装包。为了简化开发并确保下载任务在后台稳定运行,Android系统提供了强大的系统服务 —— DownloadManager。它是一个系统级的下载管理器,能够帮助我们轻松实现可靠的后台文件下载,同时处理网络切换、断点续传、状态通知等复杂场景。
今天就跟大家分享 DownloadManager 的使用方法和一些特性。
DownloadManager是Android SDK中android.app.DownloadManager 类提供的系统服务,自 API Level 9(Android 2.3)起就可以使用。它通过系统级服务管理下载任务,即使应用退出或设备重启,下载任务扔可继续执行(当然,这是需要配置持久化的)。
核心优势:
通过以上的优势可以看出 DownloadManager 的稳定和可靠性,如果我们自己封装不出来可靠的下载器,建议大家试试 DOwnloadManager。
我们继续介绍它的使用步骤:
首先需要声明必要的权限:在 AndroidManifest.xml 中配置以下信息:
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"android:maxSdkVersion="28" />
然后,获取 DownloadManager 实例
val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
构建下载请求
val request = DownloadManager.Request(Uri.parse(targetUrl)).setTitle("正在下载 $targetName").setDescription("请稍后...")//设置下载完成前和完成后是否显示通知,这里设置的是下载中和下载后都显示.setNotificationVisibility(if(onlyShowDownloadResult)DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION elseDownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)//指定文件存储位置.setDestinationUri(Uri.fromFile(targetFile))//true 表示可以用流量下载,false表示只能wifi下载.setAllowedOverMetered(true)//禁止漫游时下载.setAllowedOverRoaming(false)//非充电时也能下载.setRequiresCharging(false)//指定下载时的可用网络.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI orDownloadManager.Request.NETWORK_MOBILE)// 提交下载任务downloadId = dm.enqueue(request)
DownloadManager 不会返回下载进度,所以我们写一个轮巡检查下载进度的方法:
rivate fun start(context: Context, downloadId: Long){// Log.d("ZQ", "启动轮询检查下载:$downloadId")if (downloadId == -1L) {cleanup()return}// Log.d("ZQ", "scope.isActive==${job.isActive}")if(!job.isActive){job.cancel()job = Job()scope = CoroutineScope(Dispatchers.Default + job)}scope.launch {while (isActive){val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManagerval query = DownloadManager.Query().setFilterById(downloadId)val cursor = dm.query(query)// Log.d("ZQ", "111检测进度cursor》》》$cursor")// Log.d("ZQ", "111检测进度cursor.moveToFirst()》》》${cursor.moveToFirst()}")if (cursor.moveToFirst()) {val status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))// Log.d("ZQ", "111检测进度status》》》$status")when (status) {DownloadManager.STATUS_SUCCESSFUL -> {downloadType = 2callback?.onSuccess(targetFile)cursor.close()cleanup()}DownloadManager.STATUS_FAILED -> {val reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))downloadType = 3callback?.onFailure("下载失败: $reason")cursor.close()// Log.d("ZQ", "111下载失败:$reason》")cleanup()}DownloadManager.STATUS_RUNNING -> {val total = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))val downloaded = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))Log.d("ZQ", "111检测进度》》》$total")if (total > 0) {val progress = (downloaded * 100 / total).toInt()downloadType = 1callback?.onProgress(progress)// Log.d("ZQ", "1111检测进度2》》》$progress")if(progress>=100){downloadType = 2cursor.close()cleanup()callback?.onSuccess(targetFile)}}}DownloadManager.STATUS_PENDING -> {//准备中// Log.d("ZQ", "准备中。。。")}DownloadManager.STATUS_PAUSED -> {//暂停// Log.d("ZQ", "暂停下载。。。")cleanup()}else -> {cursor.close()cleanup()// Log.d("ZQ", "111结束》》")}}}delay(500)}}}
我这里封装了一个 FileDownloader 的工具类方便调用,代码如下:
class FileDownloader(private val context: Context) {private var downloadId: Long = -1private var targetFile: File? = nullprivate var callback: DownloadCallback? = nullprivate var job = Job()private var scope: CoroutineScope = CoroutineScope(Dispatchers.Default + job)//0:未开始 1:下载中 2:下载成功 3:下载失败private var downloadType: Int = 0//下载//deleteExistsFile 删除已存在文件,默认false//hideNotification 是否隐藏下载通知,有些机型可能无效fun downloadFile(context: Context, targetUrl: String, targetName: String,deleteExistsFile:Boolean = true,onlyShowDownloadResult: Boolean = true,callback: DownloadCallback) : Long{this.callback = callbackval dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager//存储路径: 应用自己的目录,不需要申请权限targetFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), targetName)//如果文件已存在,先删除if(targetFile!!.exists()){if(deleteExistsFile){targetFile!!.delete()}else{//文件存在的话就直接返回成功callback.onSuccess(targetFile)return -1}}val request = DownloadManager.Request(Uri.parse(targetUrl)).setTitle("正在下载 $targetName").setDescription("请稍后...")//设置下载完成前和完成后是否显示通知,这里设置的是下载中和下载后都显示.setNotificationVisibility(if(onlyShowDownloadResult) DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION else DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)//指定文件存储位置.setDestinationUri(Uri.fromFile(targetFile))//true 表示可以用流量下载,false表示只能wifi下载.setAllowedOverMetered(true)//禁止漫游时下载.setAllowedOverRoaming(false)//非充电时也能下载.setRequiresCharging(false)//指定下载时的可用网络.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)downloadId = dm.enqueue(request)// 启动轮询监听进度start(context, downloadId)return downloadId}// 取消下载fun cancel() {if (downloadId != -1L) {try {val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManagerdm.remove(downloadId)downloadType = 3callback?.onCancel(null)} catch (e: Exception) {downloadType = 3callback?.onFailure("取消下载失败:"+e.message)} finally {cleanup()}}}private fun cleanup() {// Log.d("ZQ", "执行cleanup,资源被释放》》》")downloadId = -1if(downloadType != 3){callback?.onProgress(100)callback?.onSuccess(targetFile)}callback = nulljob.cancel()}private fun start(context: Context, downloadId: Long){// Log.d("ZQ", "启动轮询检查下载:$downloadId")if (downloadId == -1L) {cleanup()return}// Log.d("ZQ", "scope.isActive==${job.isActive}")if(!job.isActive){job.cancel()job = Job()scope = CoroutineScope(Dispatchers.Default + job)}scope.launch {while (isActive){val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManagerval query = DownloadManager.Query().setFilterById(downloadId)val cursor = dm.query(query)if (cursor.moveToFirst()) {val status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))// Log.d("ZQ", "111检测进度status》》》$status")when (status) {DownloadManager.STATUS_SUCCESSFUL -> {downloadType = 2callback?.onSuccess(targetFile)Log.d("ZQ", "111下载成功》")cursor.close()cleanup()}DownloadManager.STATUS_FAILED -> {val reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))downloadType = 3callback?.onFailure("下载失败: $reason")cursor.close()// Log.d("ZQ", "111下载失败:$reason》")cleanup()}DownloadManager.STATUS_RUNNING -> {val total = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))val downloaded = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))Log.d("ZQ", "111检测进度》》》$total")if (total > 0) {val progress = (downloaded * 100 / total).toInt()downloadType = 1callback?.onProgress(progress)// Log.d("ZQ", "1111检测进度2》》》$progress")if(progress>=100){downloadType = 2cursor.close()cleanup()callback?.onSuccess(targetFile)}}}DownloadManager.STATUS_PENDING -> {//准备中// Log.d("ZQ", "准备中。。。")}DownloadManager.STATUS_PAUSED -> {//暂停// Log.d("ZQ", "暂停下载。。。")cleanup()}else -> {cursor.close()cleanup()// Log.d("ZQ", "111结束》》")}}}delay(500)}}}//安装apkfun installApk(activity: Activity, authority: String, apkFile: File? = null) {if(apkFile == null && targetFile ==null){Toast.makeText(activity, "找不到安装的apk文件!", Toast.LENGTH_SHORT).show()return}val uri = FileProvider.getUriForFile(activity,authority,apkFile ?: targetFile!!)val intent = Intent(Intent.ACTION_INSTALL_PACKAGE)intent.data = uriintent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSIONintent.setDataAndType(uri, "application/vnd.android.package-archive")activity.startActivity(intent)}}
使用方法:
配合我们上期写的下载组件来做这个下载功能:
com.cd.lib_widget.btn.MyDownloadProgressButtonandroid:id="@+id/btn"android:layout_width="match_parent"android:layout_height="@dimen/dp_44"android:layout_marginStart="@dimen/dp_15"android:layout_marginTop="@dimen/dp_21"android:layout_marginEnd="@dimen/dp_15"app:progressBgcolor="@color/purple_200"android:textSize="@dimen/sp_16"app:progressRadius="@dimen/dp_16"app:progressText="点击下载" />
在按钮的点击事件里调用我们的工具类:
al fileDownloader = FileDownloader(this)val btn = findViewById<MyDownloadProgressButton>(R.id.btn)btn.setSuccessStr("立即安装")btn.setBtnClickListener(object : DownloadButtonListener{override fun onClick(view: MyDownloadProgressButton, status: DownloadButtonEnum) {when(status){DownloadButtonEnum.DOWNLOADING -> {}DownloadButtonEnum.SUCCESS -> {fileDownloader.installApk(this@TestActivity, "${packageName}.fileprovider")}DownloadButtonEnum.WAITING,DownloadButtonEnum.FAILED,DownloadButtonEnum.CANCEL -> {val url = "https://prod-tf.kfs.aalws.com/gc/d3fe98e89aec9a6e499a5bf3fa4eedf7.apk"val name = "d3fe98e89aec9a6e499a5bf3fa4eedf7.apk"fileDownloader.downloadFile(this@TestActivity, url, name,deleteExistsFile = true,onlyShowDownloadResult = false,callback = object : DownloadCallback {override fun onProgress(progress: Int) {Log.d("ZQ", "下载进度>>$progress")if(progress == 0){btn.setDownloadStatus(DownloadButtonEnum.WAITING)}else{btn.setDownloadStatus(DownloadButtonEnum.DOWNLOADING)}btn.setProgress(progress.toFloat())}override fun onSuccess(file: File?) {Log.d("ZQ", "下载成功>")btn.setDownloadStatus(DownloadButtonEnum.SUCCESS)}override fun onFailure(error: String?) {Log.d("ZQ", "下载失败!!")btn.setDownloadStatus(DownloadButtonEnum.FAILED)}override fun onCancel(error: String?) {btn.setDownloadStatus(DownloadButtonEnum.CANCEL)}})}}}})
也可以取消下载:
ileDownloader.cancel()btn.setDownloadStatus(DownloadButtonEnum.CANCEL)
到这里调用系统的下载功能就算完成了。
DownloadManager 是Android平台上实现简单、稳定、后台文件下载的理想选择。它减轻了开发者处理复杂网络状态和生命周期管理的负担,特别适用于下载大文件、更新包等场景。通过合理配置请求参数和监听下载状态,可以构建出用户体验良好的下载功能。好了,今天的分享就结束了,我们下一期再见!

