使用Android DownloadManager更新APP

10,032 阅读2分钟

具体实现思路

我们通过downloaderManager来下载apk,并且本地保存downManager.enqueue(request)返回的id值,并且通过这个id获取apk的下载文件路径和下载的状态,并且通过状态来更新通知栏的显示。

第一次下载成功,弹出安装界面

如果用户没有点击安装,而是按了返回键,在某个时候,又再次使用了我们的APP

如果下载成功,则判断本地的apk的包名是否和当前程序是相同的,并且本地apk的版本号大于当前程序的版本,如果都满足则直接启动安装程序。

检查软件更新

APP的更新检查时机一般是在APP登陆成功后MainActivity的onResume()执行的时候,APP在冷启动或者从后台返回时可以弹出新版本提示,除此之外还应该在APP特定页面中增加软件更新检查的入口,方便用户手动选择更新。对于某些特定的应用场景,比如APP需要长时间在前台展示,按照上述方法实现的更新检查,如果没有人为干预的话,APP是不会得到更新的,这种场景可通过线程池执行定时任务间隔一段时间向后台轮询当前最新软件包的版本,如果最新软件包的版本号大于当前APP的VersonCode,则通过后台获取到的url下载最新的软件升级包。

mScheduleExecutor = Executors.newSingleThreadScheduledExecutor().apply {
            scheduleAtFixedRate(mCheckVersionTask,1,1,TimeUnit.HOURS)
        }
    private val versionCallback = object : Callback<CheckVersionResponse> {
        override fun onResponse(
            call: Call<CheckVersionResponse>,
            response: Response<CheckVersionResponse>
        ) {
            if (response.isSuccessful) {
                response.body()?.let {
                    if (it.success != false) {
                        it.data?.run {
                            if (code?:0 > BuildConfig.VERSION_CODE) {
                                mDownloadListener?.onDiscoverNewVersion(this)
                            }
                        }
                    }
                }
            }
        }


        override fun onFailure(call: Call<CheckVersionResponse>, t: Throwable) {
            Log.e(TAG,"checkVersion onFailure: $t.message")
        }
    }

查询最新软件包的接口可通过retrofit网络框架进行封装,在这里不再赘述。

使用谷歌推荐的DownloadManager实现下载

Android自带的DownloadManager模块来下载,在api level 9之后,我们通过通知栏知道, 该模块属于系统自带, 它已经帮我们处理了下载失败、重新下载等功能。整个下载 过程全部交给系统负责,不需要我们过多的处理。首先需要在manifest文件中注明APP使用DownloadManager所需的相应权限:

    <!--DownloadManager-->
    <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER"/>

否则不能下载成功,这点需要特别注意。

DownLoadManager.Request:主要用于发起一个下载请求。

先看下简单的实现:

创建Request对象的代码如下:

DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkurl));
   //设置在什么网络情况下进行下载
   request.setAllowedNetworkTypes(Request.NETWORK_WIFI);
   //设置通知栏标题
   request.setNotificationVisibility(Request.VISIBILITY_VISIBLE);
   request.setTitle("下载");
   request.setDescription("apk正在下载");
   request.setAllowedOverRoaming(false);
   //设置文件存放目录
   request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "mydown");

这里我们可以看下request的一些属性:

addRequestHeader(String header,String value):添加网络下载请求的http头信息
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等方法用于设置下载文件的存放路径

取得系统服务后,调用downloadmanager对象的enqueue方法进行下载,此方法返回一个编号用于标示此下载任务:

downManager = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);
id= downManager.enqueue(request);

DownLoadManager.Query:主要用于查询下载信息。

    /*
     * 通过downloadID查询下载的进度信息
     * */
    private fun getBytesAndStatus(downloadId: Long): IntArray? {
        val bytesAndStatus = intArrayOf(-1, -1, 0)
        val query: DownloadManager.Query = DownloadManager.Query().setFilterById(downloadId)
        var cursor: Cursor? = null
        try {
            cursor = mDownloadManager.query(query)
            if (cursor != null && cursor.moveToFirst()) {
                //已经下载文件大小
                bytesAndStatus[0] =
                    cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
                //下载文件的总大小
                bytesAndStatus[1] =
                    cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
                //下载状态
                bytesAndStatus[2] =
                    cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))

                mDownloadListener?.onProgressChange(
                    bytesAndStatus[1],
                    bytesAndStatus[0],
                    bytesAndStatus[2]
                )
            }
        } finally {
            cursor?.close()
        }
        return bytesAndStatus
    }

使用ContentObserver监听APK的下载进度

DownloadManager在Android系统内部有固定的资源URI,可以很方便的通过该URI注册一个ContentObserver:

mDownObserver = DownloadChangeObserver(null).also {
            contentResolver.registerContentObserver(
                Uri.parse("content://downloads/my_downloads"),
                true,
                it
            )
        }

当ContentObserver构造函数中传入的Handler对象为空时,它的onChange()回调方法是在UI线程中执行的,所以我们通过线程池去查询下载进度以防止UI阻塞:

    inner class DownloadChangeObserver(handler: Handler?) : ContentObserver(handler) {
        override fun onChange(selfChange: Boolean) {
            //设置查询进度的线程每隔两秒查询一下
            mProgressFuture = mScheduleExecutor.scheduleAtFixedRate(mProgressThread, 0, 2, TimeUnit.SECONDS)
        }
    }

    inner class ProgressThread : Runnable {
        override fun run() {
            getBytesAndStatus(mDownLoadId)
        }
    }

使用Android系统提供的方法安装APK

DownloadManager下载完成后会向外发出ACTION_DOWNLOAD_COMPLETE的广播,在程序中通过动态注册广播接收者监听该广播:

        mDownReceiver = CompleteReceiver().also {
            registerReceiver(it, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
        }
        
    inner class CompleteReceiver : BroadcastReceiver() {
        override fun onReceive(
            context: Context,
            intent: Intent
        ) {
            val completeDownloadId =
                intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
            if (completeDownloadId == mDownLoadId) {
                mProgressFuture?.cancel(false)
                val myDownloadQuery = DownloadManager.Query()
                myDownloadQuery.setFilterById(mDownLoadId)
                mDownloadManager.query(myDownloadQuery)?.let {
                    if (it.moveToFirst()) {
                        val sizeTotal: String =
                            it.getString(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
                        if (sizeTotal.toLong() < 0 || mFileName == null) {
                            return
                        }
                    }
                    it.close()
                    mFileName?.let {name ->
                        installAPK(context,name)
                    }
                }
            }
        }
    }

检查下载的APK是版本是否大于当前APP版本号:

    private fun checkDownLoadAPK(versionCode: Int, versionName: String) {
        val file = File(baseContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
            "my.apk")
        if (file.exists() && file.isFile) {
            val pm: PackageManager = packageManager
            pm.getPackageArchiveInfo(
                file.path,
                PackageManager.GET_ACTIVITIES
            )?.also {
                val appInfo: ApplicationInfo = it.applicationInfo
                if (appInfo.packageName == baseContext.packageName
                    && it.versionCode >= versionCode){
                    installAPK()
                }
            }
            file.delete()
        }

    }

跳转Android系统APK安装界面:

fun installAPK(context: Context, path: String) {
    setPermission(path)
    val intent =
        Intent(Intent.ACTION_VIEW)
    // 由于没有在Activity环境下启动Activity,设置下面的标签
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    //Android 7.0以上要使用FileProvider
    val file = File(path)
    if (Build.VERSION.SDK_INT >= 24) {
        //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
        val apkUri =
            FileProvider.getUriForFile(context, "com.android.file.provider", file)
        //添加这一句表示对目标应用临时授权该Uri所代表的文件
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
    } else {
        intent.setDataAndType(
            Uri.fromFile(
                file
            ), "application/vnd.android.package-archive"
        )
    }
    context.startActivity(intent)
}

如果使用系统秘钥对APK签名后可使用静默安装的方式:

private  fun installApkSilently(apkPathName: String) {
    val cmd = "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm install -r $apkPathName"
    val install = arrayOfNulls<String>(3)
    install[0] = "su"
    install[1] = "-c"
    install[2] = cmd
    try {
        val p = Runtime.getRuntime().exec(install)
        p.waitFor()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}