使用Android DownloadManager更新APP

具体实现思路

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

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

若是用户没有点击安装,而是按了返回键,在某个时候,又再次使用了咱们的APPbash

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

检查软件更新

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

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所需的相应权限:ide

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

不然不能下载成功,这点须要特别注意。函数

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

先看下简单的实现:this

建立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()
    }
}
复制代码
相关文章
相关标签/搜索