大数跨境
0
0

Android - DownloadManager

Android - DownloadManager lucky出海
2025-10-25
6
导读:在Android应用开发中,实现文件下载功能是常见的需求,Android系统提供了强大的系统服务 —— DownloadManager。它是一个系统级的下载管理器。

Android应用开发中,实现文件下载功能是常见的需求,如下载图片、视频、文档、应用安装包。为了简化开发并确保下载任务在后台稳定运行,Android系统提供了强大的系统服务 —— DownloadManager。它是一个系统级的下载管理器,能够帮助我们轻松实现可靠的后台文件下载,同时处理网络切换、断点续传、状态通知等复杂场景。

今天就跟大家分享 DownloadManager 的使用方法和一些特性。


DownloadManagerAndroid SDKandroid.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 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)


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 DownloadManager                val 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 = 2                            callback?.onSuccess(targetFile)                            cursor.close()                            cleanup()                        }                        DownloadManager.STATUS_FAILED -> {                            val reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))                            downloadType = 3                            callback?.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 = 1                                callback?.onProgress(progress)//                                Log.d("ZQ""1111检测进度2》》》$progress")                                if(progress>=100){                                    downloadType = 2                                    cursor.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 = -1    private var targetFile: File? = null    private var callback: DownloadCallback? = null    private 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 = callback        val 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 DownloadManager                    dm.remove(downloadId)                    downloadType = 3                    callback?.onCancel(null)                } catch (e: Exception) {                    downloadType = 3                    callback?.onFailure("取消下载失败:"+e.message)                } finally {                    cleanup()                }            }        }
        private fun cleanup() {//            Log.d("ZQ", "执行cleanup,资源被释放》》》")            downloadId = -1            if(downloadType != 3){                callback?.onProgress(100)                callback?.onSuccess(targetFile)            }            callback = null            job.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 DownloadManager                val 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 = 2                            callback?.onSuccess(targetFile)                            Log.d("ZQ""111下载成功》")                            cursor.close()                            cleanup()                        }                        DownloadManager.STATUS_FAILED -> {                            val reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))                            downloadType = 3                            callback?.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 = 1                                callback?.onProgress(progress)//                                Log.d("ZQ", "1111检测进度2》》》$progress")                                if(progress>=100){                                    downloadType = 2                                    cursor.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)            }        }    }
    //安装apk    fun 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 = uri        intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION        intent.setDataAndType(uri, "application/vnd.android.package-archive")        activity.startActivity(intent)    }

}


使用方法:

配合我们上期写的下载组件来做这个下载功能


com.cd.lib_widget.btn.MyDownloadProgressButton    android: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平台实现简单稳定后台文件下载的理想选择。它减轻了开发者处理复杂网络状态和生命周期管理的负担,特别适用于下载大文件更新包等场景。通过合理配置请求参数和监听下载状态,可以构建出用户体验良好的下载功能。好了,今天的分享就结束了,我们下一期再见!







About the Author
Joe Zhang (MBaaS Engineer)
Joe Zhang is a technical expert focusing on mobile development, proficient in flutter and android development, and has relatively rich experience in the field of mobile development. He is proficient in java, kotlin and dart programming languages and their ecological frameworks. With rich practical experience, he is more outstanding in cross-platform development, architecture design and other technical aspects.
Copyright
This article was originally written by Klilala Group and its affiliates and is copyrighted by Klilala Group. Anyone wishing to reproduce, abstract or otherwise quote the contents of this article must obtain the express authorization and written permission of Klilala Group, and indicate the source of reproduction when doing so. Any unauthorized reproduction or use of this article will result in legal liability.
For authorization, please contact us at
developer@klilala-inc.com
028-67876790
Thank you for your understanding and support!
Follow us on our official WeChat public account



【声明】内容源于网络
0
0
lucky出海
跨境分享圈 | 每天分享跨境干货
内容 44188
粉丝 1
lucky出海 跨境分享圈 | 每天分享跨境干货
总阅读236.3k
粉丝1
内容44.2k