在 Android 开发中,许多时候都会用到大文件下载功能,例如 app 更新、缓存视频文件等。之前我们开发团队里布置了一次作业,写的是关于利用 RandomAcceseeFile 和 service 来实现一个下载器,需要考虑到例如异步、后台下载、暴露回调接口、自动安装等策略,对于初级 Android 工程师来说,项目比较赶的时候可能就会影响效率。那么,有没有一个库能够简单粗暴地完成这些任务呢?
当然,那就是 Google 官方的 DownloadManager

介绍

DownloadManager

我们看看官方对于两个主要内部类的介绍:

Nested classes description
DownloadManager.Query This class may be used to filter download manager queries.
DownloadManager.Request This class contains all the information necessary to request a new download.

顾名思义 DownloadManager.Query 主要用于查询下载的信息,DownloadManager.Request 主要用于发起一个下载请求(其中可以添加下载的配置,例如 Header 等信息)

基本使用

1. 设置权限

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
复制代码

2. 构造 Request 对象

        val url = "http://hongyan.cqupt.edu.cn/app/com.mredrock.cyxbs.apk?version=44.0"//下载地址
        val uri: Uri = Uri.parse(url)//转变为Uri
        val request:DownloadManager.Request = DownloadManager.Request(uri)//构造request实例
复制代码

3. 配置 Request 的信息

        request.setTitle("下载任务")
        request.setDescription("下载掌上重邮 app 中...")
        request.allowScanningByMediaScanner()
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "cyxbs")
        //...
复制代码

部分配置:

  • addRequestHeader(String header,String value) 添加请求头
  • allowScanningByMediaScanner() 表示允许 MediaScanner 扫描到这个文件, 默认不允许
  • setAllowedNetworkTypes(int flags) 设置下载时的网络条件,默认任何网络都可以下载,可选配置:NETWORK_BLUETOOTH、NETWORK_MOBILE、NETWORK_WIFI。
  • setAllowedOverRoaming(Boolean allowed) 漫游状态下是否可以下载
  • setNotificationVisibility(int visibility) 设置下载完成或下载时是否发布通知
  • setTitle(CharSequence): 设置 Notification 的 title
  • setDescription(CharSequence): 设置 Notification 的 message
  • setDestinationInExternalFilesDir 设置路径为应用程序外部文件目录
  • setDestinationInExternalPublicDir 设置路径为外部存储目录
  • setDestinationUri 设置路径
  • setMimeType(String mimeType) 设置 MIME 内容类型

4. 获取 DownloadManager 实例

val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
复制代码

5. 加入下载队列

val downloadId = downloadManager.enqueue(request)
复制代码

6. 其他操作

//remove方法可以用来取消一个准备进行的下载,中止一个正在进行的下载,或者删除一个已经完成的下载。
downloadManager.remove(downloadId)//移除请求队列,取消下载
复制代码

监听 DownloadManager 的广播

DownloadManager 会在完成时发送一条 ACTION_DOWNLOAD_COMPLETE 的广播,这时我们只需要创建一个 BroadCastReceiver 就能接收到下载完成的信息

创建 BroadCastReceiver 的子类来接收广播

class DownLoadFinishReceiver : BroadcastReceiver(){
        override fun onReceive(context: Context?, intent: Intent?) {
            intent?.let {
                val action = intent.action
                if (action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
                    //下载完成操作
                } else if (action == DownloadManager.ACTION_NOTIFICATION_CLICKED) {
                    //点击Notification的操作 例如暂停操作
                }
            }
        }
    }
复制代码

动态注册广播接收器

        val downLoadFinishReceiver = DownLoadFinishReceiver()
        val intentFilter = IntentFilter()
        intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
        intentFilter.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
        registerReceiver(downLoadFinishReceiver, intentFilter)
复制代码

别忘了在 onDestroy 里解除

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(downLoadFinishReceiver)
    }
复制代码

Query 的使用

query 主要用于查询下载的信息,DownloadManager 类中内置了很多的字段可以供 Query 来查询,例如状态、文件下载路径等。

Public method description
setFilterById(long... ids) 仅包括给定 id 的下载文件
setFilterByStatus(int flags) 仅包含状态与给定状态匹配的下载文件

结合 Cursor,我们可以查询到 DownloadManager 里有的信息,包括下载文件的 Uri,下载状态等。具体字段可以进入 DownloadManager 源码中查看。

查询下载状态

private fun queryStatus(){
        val query = DownloadManager.Query().setFilterById(downloadId)
        val cursor = manager.query(query)
        if(cursor!=null){
            if(cursor.moveToFirst()){
                val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
                when(status){
                    DownloadManager.STATUS_RUNNING->{
                        //下载中
                    }
                    DownloadManager.STATUS_FAILED->{
                        //下载失败
                    }
                    DownloadManager.STATUS_PAUSED->{
                        //下载暂停
                    }
                    DownloadManager.STATUS_PENDING->{
                        //下载延迟
                    }
                    DownloadManager.STATUS_SUCCESSFUL->{
                        //下载成功
                    }
                }
            }
        }
        cursor.close()
    }
复制代码

查询下载的文件的部分信息

这里 DownloadManager 封装好了两种查询的方法,其内部都是使用 Query+Cursor 的组合实现的。

        manager.getUriForDownloadedFile(downloadId)
        //下载完成的文件的Uri,你可以拿到这个uri去操作他,例如apk安装
        manager.getMimeTypeForDownloadedFile(downloadId)
        //下载完成的文件的media type
复制代码

你也可以查询其他信息

DownloadManager.COLUMN_ID//下载id
DownloadManager.COLUMN_TITLE//下载文件的题目
DownloadManager.COLUMN_LOCAL_URI//下载文件的uri
DownloadManager.COLUMN_STATUS//下载文件的状态
DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR//目前文件的大小
DownloadManager.COLUMN_TOTAL_SIZE_BYTES//文件的总大小
复制代码

多说一句,若想回调进度,你可以根据上面的 文件目前大小/文件总大小 来得到,刷新进度可以使用 Hanlder+Timer(仅供参考)

自动安装 Apk

首先列出兼容 7.0 的方法,因为 7.0 时引入了 "StrictMode Api" 政策,禁止向你的应用外公开 uri, 如果 Intent 跳转到应用外,则会出现 FileUriExposedException,所以说我们要添加一个 flag 去获得临时授权

    private fun installApk(downloadId: Long) {
        val uri = manager.getUriForDownloadedFile(downloadId)
        val intent = Intent(Intent.ACTION_VIEW)
        if (uri != null) {
            intent.setDataAndType(uri,"application/vnd.android.package-archive")
            if ((Build.VERSION.SDK_INT >= 24)) {//版本是否在7.0以上
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                //对目标apk的uri临时授权 使得有权限打开该Uri指向的Apk
            }
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        <span class="hljs-keyword">if</span> (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        } <span class="hljs-keyword">else</span> {
            Log.e(<span class="hljs-string">"DownloadManager"</span>,<span class="hljs-string">"自动安装失败"</span>)
        }
    }<span class="hljs-keyword">else</span>{
        Log.e(<span class="hljs-string">"DownloadManager"</span>,<span class="hljs-string">"下载失败"</span>)
    }
}
复制代码

其次,8.0 时禁止安装未知来源的 apk,直接安装会闪退,所以说我们加入这条权限,就能跳转到一个让用户手动允许安装未知来源的界面。

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
复制代码

自动更新

(流程图练手)

image

后话

因为 Request 要设置的参数比较多,适合 Builder 模式,所以说大家可以写一个 RequsetBuilder 来配置 Request 对象,比较美观,简单修改如下。

class RequestBuilder {
    private lateinit var request:DownloadManager.Request
    private lateinit var uri: Uri
    private lateinit var context:Context
fun with(context: Context):RequestBuilder{
    this.context = context
    <span class="hljs-built_in">return</span> this
}

fun downloadUrl(url:String):RequestBuilder{
    this.uri = Uri.parse(url)
    this.request = DownloadManager.Request(uri)
    <span class="hljs-built_in">return</span> this
}


fun <span class="hljs-built_in">set</span>Title(title: String):RequestBuilder{
    request.setTitle(title)
    <span class="hljs-built_in">return</span> this
}

fun <span class="hljs-built_in">set</span>Description(description: String):RequestBuilder{
    request.setDescription(description)
    <span class="hljs-built_in">return</span> this
}

fun allowScanningByMediaScanner():RequestBuilder{
    request.allowScanningByMediaScanner()
    <span class="hljs-built_in">return</span> this
}

fun <span class="hljs-built_in">set</span>NetworkType(networkType:Int):RequestBuilder{
    request.setAllowedNetworkTypes(networkType)
    <span class="hljs-built_in">return</span> this
}

fun <span class="hljs-built_in">set</span>NotificationVisibility(visibility:Int):RequestBuilder{
    request.setNotificationVisibility(visibility)
    <span class="hljs-built_in">return</span> this
}

fun <span class="hljs-built_in">set</span>DefaultDestination(subPath:String):RequestBuilder{
    request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, subPath)
    <span class="hljs-built_in">return</span> this
}

fun build():DownloadManager.Request{
    <span class="hljs-built_in">return</span> request
}

}

复制代码

此外,记得缓存 downloadId,否则退出界面之后 id 就丢失了。

最后

基本内容就这么多了,可能有些地方写得不好,有不正确的地方欢迎大家指出

  • Android

    开放手机联盟(一个由 30 多家科技公司和手机公司组成的团体)已开发出 Android,Android 是第一个完整、开放、免费的手机平台。

    293 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...