Android应用内下载并更新APK

背景

公司的APP之前一直是放在公司服务器,没有发布到应用市场,每次发布新版本用户更新都很麻烦。前段时间抽时间写了个应用内下载APK并更新的模块。 整个模块主要的几个类:

  • UpgradeManager
  • UpgradeService
  • FileResponseBody
  • ResponseInterceptor

网络请求使用的是Retrofit2+Rxjava2

UpgradeManager

该类主要实现

  • 检查最新版本
  • 升级信息提示
  • 安装APK
  • 删除冗余APK文件

检查最新版本

调用服务器提供的版本比对接口,将本地的版本名称提取出来封装为JSON字符串发送到服务器,根据服务器的返回结果做对应处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 private fun checkUpdate(context: Context, disposableObserver: DisposableObserver<UpdateAck>) {
        val pm = context.packageManager
        val versionName = pm.getPackageInfo(context.applicationInfo.packageName, 0).versionName
        val jsonObject = JSONObject()
        jsonObject.put("versionName", versionName)
        LogUtil.d(tag, jsonObject.toString())
        RetrofitClient.removeMethod(API.METHOD.CHECK_UPDATE)
        val checkUpdateCall = RetrofitClient.apiService
                .checkNewVer(RetrofitClient.body(API.METHOD.CHECK_UPDATE, jsonObject.toString()))
                .compose(RetrofitClient.io2main())
                .subscribeWith(disposableObserver)    
        RetrofitClient.addMethod(javaClass.name, API.METHOD.CHECK_UPDATE, checkUpdateCall)
    }

下面贴出来的是手动检查升级的代码,无论是否需要升级都有相应提示,还需要一个backgroundCheck(context: Context)方法,在APP启动时调用,无需升级时不弹出提示打扰用户,代码就不贴了,只需省略部分提示代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
     fun manualCheck(context: Context) {
        val disposableObserver = object : DisposableObserver<UpdateAck>() {
            override fun onComplete() {}

            override fun onNext(t: UpdateAck) {
                LogUtil.d(tag, t.toString())
                val responseData = t.responseData
                if (t.result == "T" && responseData != null) {
                    //无错误信息
                    if (responseData.errorMessage.isBlank()) {
                        //服务器存在不同版本
                        if (responseData.isNeedUpgrade) {
                            val str = context.getString(R.string.need_upgrade)
                            val msg = String.format(str, responseData.versionName)
                            val upgradeVerName = "yourAppName" + responseData.versionName
                            showDialog(context, msg, responseData.upgradeUrl, upgradeVerName)
                        } else {
                            val msg = context.getString(R.string.no_need_upgrade)
                            showDialog(context, msg)
                        }
                    } else {
                        showDialog(context, responseData.errorMessage)
                    }
                } else {
                    showDialog(context, t.errorMsg)
                }
            }

            override fun onError(e: Throwable) {
                LogUtil.e(tag, e.message.toString())
                showDialog(context, context.getString(R.string.network_state))
            }

        }
        checkUpdate(context, disposableObserver)
    }

升级信息提示

showDialog方法中为urlversionName参数设置了默认值null,当不是提示版本有版本需要更新时可以只传两个参数,这样能使代码看起来更简洁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  private fun showDialog(context: Context?, msg: String, url: String? = null, versionName: String? = null) {
        //防止activity销毁后弹出提示导致程序崩溃
        if (context != null) {
            val builder = AlertDialog.Builder(context)
            builder.setMessage(msg)
                    .setPositiveButton(R.string.ok) { p0, _ ->
                        p0.dismiss()
                        //有新版本时启动更新流程
                        if (url != null) {
                            if (!hasDownLoad(versionName!!)) {
                                LogUtil.d(tag, "未下载$versionName" + "APK")
                                startDownLoadService(context, url, versionName)
                            } else {
                                LogUtil.d(tag, "已下载$versionName" + "APK")
                                installAPK(context, versionName)
                            }
                        }
                    }
                    .setNegativeButton(R.string.cancel) { p0, _ ->
                        p0.dismiss()
                    }.create().show()
            //通过EventBus通知AppInfo页面隐藏网络请求的旋转菊花
            EventBus.getDefault().post(EventMsg.AppInfoDismissProgressBar())
        }
    }

如果当前版本和服务器上的版本不一致时,先调用hasDownload(versionName!!)方法检查指定路径是否存在最新的APK文件

1
2
3
4
5
6
7
fun hasDownLoad(versionName: String): Boolean {
        return apkFile(versionName).exists()
    }
    
fun apkFile(versionName: String): File {
        return File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/" + versionName + ".apk")
    }

如果存在则直接进行安装,如果没有最新版的APK文件,则通过 startDownLoadService(context, url, versionName)来开启UpgradeService进行后台下载。

1
2
3
4
5
6
  private fun startDownLoadService(context: Context, url: String, versionName: String) {
        val intent = Intent(context, UpgradeService::class.java)
        intent.putExtra("url", url)
        intent.putExtra("versionName", versionName)
        context.startService(intent)
    }

安装APK

安装代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    private fun installAPK(context: Context, versionName: String) {
        val install = installIntent(context, versionName)
        context.startActivity(install)
    }

  fun installIntent(context: Context, versionName: String): Intent {
        val file = apkFile(versionName)
        //7.0关于读写文件有更严格的要求,需要配置FileProvider
        val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            FileProvider.getUriForFile(context, "com.xxxxx.fileprovider", file)
        } else {
            Uri.fromFile(file)
        }
        val install = Intent(Intent.ACTION_VIEW)
        install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)//为7.0添加
        install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        install.setDataAndType(uri, "application/vnd.android.package-archive")
        return install
    }

删除冗余APK文件

在启动页面调用 UpgradeManager.cleanOldApk(ctx)检查当前运行的APP版本和指定路径的APK versionName是否相同,相同则删除

1
2
3
4
5
6
7
8
fun cleanOldApk(context: Context) {
        val pm = context.packageManager
        val locVersionName = pm.getPackageInfo(context.applicationInfo.packageName, 0).versionName
        //已下载的APK和当前安装的版本相同,删除下载的APK
        if (hasDownLoad("yourAppName$locVersionName")) {
            apkFile("yourAppName$locVersionName").delete()
        }
    }

UpgradeService

该类主要实现

  • APK下载 保存 安装
  • 通知中心更新下载进度

onCreate

初始化升级服务

  1. 注册EventBus并发送粘性广播提示升级服务已启动
  2. 初始化通知中心对象,设置标题,信息,图标
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
override fun onCreate() {
        super.onCreate()
        LogUtil.d(tag, "onCreate")
        EventBus.getDefault().register(this)
        EventBus.getDefault().postSticky(EventMsg.UpgradeServiceState(true))
        notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationBuilder = NotificationCompat.Builder(this, "upgrade")
        notificationBuilder.setContentTitle(getString(R.string.notification_upgrade_title))
                .setContentText(getString(R.string.notification_loading))
                .setSmallIcon(R.mipmap.mainlancher)
        notificationBuilder.priority = PRIORITY_HIGH
        //点击后自动移除通知
        notificationBuilder.setAutoCancel(true)
    }

onStartCommand

开始下载 通过isDowloadingneedDownLoad判断是否需要执行下载操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        LogUtil.d(tag, "onStartCommand:startId$startId")
        url = intent.getStringExtra("url")
        versionName = intent.getStringExtra("versionName")
        val needDownLoad = !UpgradeManager.hasDownLoad(versionName)
        if (!isDowloading && needDownLoad) {
            retryCount = 0
            downLoadAPK()
        } else {
            LogUtil.d(tag, "已有下载任务在执行!!!!")
        }
        return super.onStartCommand(intent, flags, startId)
    }

APK下载并保存并安装

downLoadAPK

retryCount重试次数,目前设置的重试次数为7次。 下载开始后将isDowloading置为true 请求成功后调用saveFile(fileResponseBody: ResponseBody)读取数据写入本地

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 private fun downLoadAPK() {
        //下载次数
        retryCount++
        LogUtil.d(tag, "开始下载")
        RetrofitClient.removeMethod("downloadAPK")
        val downLoadCall = RetrofitClient.apiService.getApk(url)
                .subscribeOn(Schedulers.io())
                .observeOn(Schedulers.io())
                .subscribeWith(object : DisposableObserver<ResponseBody>() {
                    override fun onStart() {
                        isDowloading = true
                    }

                    override fun onComplete() {
                        LogUtil.d(tag, "onComplete")
                    }

                    override fun onNext(t: ResponseBody) {
                        LogUtil.d(tag, "onNext")
                        saveFile(t)
                    }

                    override fun onError(e: Throwable) {
                        isDowloading = false
                        LogUtil.e(tag, e.message.toString())
                    }

                })

        RetrofitClient.addMethod(javaClass.name, "downloadAPK", downLoadCall)
    }

saveFile

写入文件到本地Download公共文件夹 期间若发生错误

  1. 通过UpgradeManager.apkFile(versionName).delete()清除未下载完的文件
  2. 通过 updateNotification(R.string.notification_err_retry)更新通知中心提示
  3. 通过retryCount判断是否需要重试,超过重试上限则更新通知中心并调用stopSelf()停止服务

顺利保存到本地则调用installAPK()安装APK,并将isDowloading置为false

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private fun saveFile(fileResponseBody: ResponseBody) {
        Observable.create<File> { e ->
            var inputStream: InputStream? = null
            var fileOutputStream: FileOutputStream? = null
            val buffer = ByteArray(2048)
            val apkFile = UpgradeManager.apkFile(versionName)
            LogUtil.d(tag, "保存文件到" + apkFile.absolutePath)
            try {
                inputStream = fileResponseBody.byteStream()
                fileOutputStream = FileOutputStream(apkFile)
                var len = inputStream!!.read(buffer)
                while (len != -1) {
                    fileOutputStream.write(buffer, 0, len)
                    len = inputStream.read(buffer)
                }
                fileOutputStream.flush()
                e.onNext(apkFile)
            } catch (err: IOException) {
                e.onError(err)
            } finally {
                inputStream?.close()
                fileOutputStream?.close()
                e.onComplete()
            }
        }.subscribeOn(Schedulers.io())
                .observeOn(Schedulers.io())
                .subscribe({
                    isDowloading = false
                    installAPK()
                }, { e ->
                    isDowloading = false
                    LogUtil.e(tag, e.message.toString())
                    UpgradeManager.apkFile(versionName).delete()
                    updateNotification(R.string.notification_err_retry)
                    if (retryCount < 7) {
                        downLoadAPK()
                    } else {
                        updateNotification(R.string.notification_err_retry_later)
                        stopSelf()
                    }
                })
    }

updateNotification(resId: Int)

1
2
3
4
5
 private fun updateNotification(resId: Int) {
        val string = getString(resId)
        notificationBuilder.setContentText(string)
        notificationManager.notify(0, notificationBuilder.build())
    }

installAPK

  1. 通过notificationBuilder.setProgress(0, 0, false)移除通知中的下载进度条
  2. 通过notificationBuilder.setContentIntent(pendingIntent)设置通知的点击响应,以便用户在安装界面点击了取消还能通过点击通知进行安装
  3. 通过 updateNotification(R.string.notification_load_complete)更新通知中心提示
  4. 执行安装
  5. 通过stopSelf()停止服务
1
2
3
4
5
6
7
8
9
 private fun installAPK() {
        notificationBuilder.setProgress(0, 0, false)
        val install = UpgradeManager.installIntent(applicationContext, versionName)
        val pendingIntent = PendingIntent.getActivity(this, 0, install, PendingIntent.FLAG_UPDATE_CURRENT)
        notificationBuilder.setContentIntent(pendingIntent)
        updateNotification(R.string.notification_load_complete)
        startActivity(install)
        stopSelf()
    }

onDestroy

  1. 停止所有当前类名相关网络请求
  2. 发送粘性广播通知更新服务已停止
  3. 注销EventBus
1
2
3
4
5
6
7
override fun onDestroy() {
        super.onDestroy()
        LogUtil.d(tag, "onDestroy")
        RetrofitClient.removeAll(javaClass.name)
        EventBus.getDefault().postSticky(EventMsg.UpgradeServiceState(false))
        EventBus.getDefault().unregister(this)
    }

下载进度百分比

下载进度更新通过EventBus+ResponseInterceptor+FileResponseBody实现

  1. 自定义ResponseBody即下文的FileResponseBody,重写ResponseBodyfun source()方法,在该方法内获取当前下载进度并通过EventBus广播
  2. 自定义Interceptor即下文的ResponseInterceptor,将原ResponseBody替换为FileResponseBody以获取下载进度
  3. 在创建OkHttpClient的过程中通过addNetworkInterceptor(ResponseInterceptor())将自定义网络拦截器添加进去
  4. UpgradeService接收下载进度并更新到通知中心
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    fun downloadProgress(event: EventMsg.FileLoading) {
        val progress = (event.bytesRead / event.contentLength) * 100
        val progressStr = String.format("%.2f", progress)
        //以1%为步进,大量更新会导致通知中心下拉卡死
        if (progressStr.endsWith("00")) {
            notificationBuilder.setProgress(100, progress.toInt(), false)
            notificationManager.notify(0, notificationBuilder.build())
        }
    }

FileResponseBody

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class FileResponseBody(private val originalResponse: Response) : ResponseBody() {

    private var bufferedSource: BufferedSource? = null

    init {
        LogUtil.d("FileResponseBody init")
    }

    override fun contentType(): MediaType? {
        return originalResponse.body()?.contentType()
    }

    override fun contentLength(): Long {// 返回文件的总长度,也就是进度条的max
        return originalResponse.body()!!.contentLength()
    }

    override fun source(): BufferedSource {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(source(originalResponse.body()!!.source()))
        }
        return bufferedSource!!
    }

    private fun source(source: Source): Source {
        return object : ForwardingSource(source) {
            var totalBytesRead = 0L
            override fun read(sink: Buffer, byteCount: Long): Long {
                val bytesRead = super.read(sink, byteCount)
                // read() returns the number of bytes read, or -1 if this source is exhausted.
                totalBytesRead += if (bytesRead == -1L) 0 else bytesRead
                LogUtil.d("readed:", totalBytesRead.toString())
                EventBus.getDefault().post(EventMsg.FileLoading(contentLength().toFloat(), totalBytesRead.toFloat()))
                return bytesRead
            }
        }
    }

}

ResponseInterceptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 private class ResponseInterceptor : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            val request = chain.request()
            LogUtil.d("request.url", request.url().toString())
            //地址为下载APK的地址时替换为FileResponseBody
            if (request.url().toString().startsWith("http://www.******.com")) {
                LogUtil.d("new Response")
                val response = chain.proceed(request)
                return response.newBuilder().body(FileResponseBody(response)).build()
            }
            return chain.proceed(request)
        }
    }
comments powered by Disqus