背景
公司的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
方法中为url
和versionName
参数设置了默认值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
该类主要实现
onCreate
初始化升级服务
- 注册EventBus并发送粘性广播提示升级服务已启动
- 初始化通知中心对象,设置标题,信息,图标
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
开始下载
通过isDowloading
和needDownLoad
判断是否需要执行下载操作
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
公共文件夹
期间若发生错误
- 通过
UpgradeManager.apkFile(versionName).delete()
清除未下载完的文件 - 通过
updateNotification(R.string.notification_err_retry)
更新通知中心提示 - 通过
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
- 通过
notificationBuilder.setProgress(0, 0, false)
移除通知中的下载进度条 - 通过
notificationBuilder.setContentIntent(pendingIntent)
设置通知的点击响应,以便用户在安装界面点击了取消
还能通过点击通知进行安装 - 通过
updateNotification(R.string.notification_load_complete)
更新通知中心提示 - 执行安装
- 通过
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
- 停止所有当前类名相关网络请求
- 发送粘性广播通知更新服务已停止
- 注销
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
实现
- 自定义
ResponseBody
即下文的FileResponseBody
,重写ResponseBody
的fun source()
方法,在该方法内获取当前下载进度并通过EventBus
广播 - 自定义
Interceptor
即下文的ResponseInterceptor
,将原ResponseBody
替换为FileResponseBody
以获取下载进度 - 在创建
OkHttpClient
的过程中通过addNetworkInterceptor(ResponseInterceptor())
将自定义网络拦截器添加进去 - 在
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)
}
}
|