jetpack加入WorkManager后,后台的实现基本上实现起来非常方便。
首先,我们需要获取到新版本信息,然后如果需要更新就下载新版本的apk,2个串行的worker。
app客户端部分
核验版本信息的worker示例:
class VerifyVersionWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
override suspend fun doWork(): Result {
return try {
//根据官方推荐的mvvm架构,这里应该调用Repository发起network,然后返回livedata
//worker本身运行于子线程,方便就直接用了
val versionInfo = NetWork.getVersionInfo()
// AppUtils.compareVersion()算法可以查询老的下载示例
val outputData = if (versionInfo.versionCode > AppUtils.getAppVersionCode() ||
AppUtils.compareVersion(versionInfo.versionName, AppUtils.getAppVersionName()) == 1
) {
workDataOf(KEY_URL to versionInfo.url)
} else {
//公司工控屏是安卓6.0,所以还用getExternalStoragePublicDirectory
val file =
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path)
//如果最新版本 清空下载目录,url传null给下一个(手机app别这么干)
val walk = file.walk()
walk.iterator().forEach { file ->
if (file.isFile) {
file.delete()
}
}
workDataOf(KEY_URL to null)
}
Result.success(outputData)
} catch (e: Exception) {
e.printStackTrace()
//失败会启动WorkManager重试策略
Result.failure()
}
}
}
下载文件worker示例:
class DownloadWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
override suspend fun doWork(): Result {
//url null直接成功
val srcUrl = inputData.getString(KEY_URL)
?: return Result.success()
val fileName = srcUrl.substring(srcUrl.lastIndexOf("/"))
val outputFileUri = """${
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
).path}$fileName"""
val file = File(outputFileUri)
if (file.exists()){
//用户未安装文件而已
Log.i("DownloadWorker", "文件已下载成功")
//这是一个全局的通知消息队列,继承自LiveData<T>,重写postValue入队
//出队是一个可挂起的协程,延时递归清空队列显示消息。$NEW_VERSION_TIP固定字段会被监控。
Application.msg.postValue("$NEW_VERSION_TIP$outputFileUri")
return Result.success()
}
LogUtils.i("DownloadWorker", "$srcUrl to $outputFileUri")
// Mark the Worker as important
val progress = "Starting Download"
setForeground(createForegroundInfo(progress))
return withContext(Dispatchers.IO){download(srcUrl, outputFileUri)}
}
private suspend fun download(srcUrl: String, outputFileUri: String): Result {
val partNameUri = outputFileUri.replace(".apk",".park")
//.part 保存未下载完成的文件
val file = File(partNameUri)
val downloadedLength = if (file.exists()) {
file.length()
} else 0L
//val contentLength = getContentLength(srcUrl)
val client = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.addHeader("RANGE", "bytes=$downloadedLength-")
.url(srcUrl) //
.build()
//val response = client.newCall(request).execute()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
return Result.retry()
}
response.body?.let { responseBody ->
val contentLength = responseBody.contentLength()
LogUtils.i("DownloadWorker", "$contentLength")
responseBody.byteStream().use { inputStream ->
RandomAccessFile(partNameUri, "rw").use { randomAccessFile ->
randomAccessFile.seek(downloadedLength)
val buf = ByteArray(REQUEST_BUFFER_LEN)
var rLen = 0
var total = downloadedLength
// kotlin IO有更优雅的写法,这里为了满足断点续传
while (inputStream.read(buf).also { rLen = it } != -1){
total += rLen;
randomAccessFile.write(buf, 0, rLen)
// setForeground 调用台频繁会导致ui卡顿
//setForeground(createForegroundInfo((total * 100 / contentLength).toString()))
}
file.renameTo(File(outputFileUri))
Application.msg.postValue("$NEW_VERSION_TIP$outputFileUri")
}
}
}
// Downloads a file and updates bytes read
// Calls setForegroundInfo() periodically when it needs to update
// the ongoing Notification
return Result.success()
}
// Creates an instance of ForegroundInfo which can be used to update the
// ongoing notification.
private fun createForegroundInfo(progress: String): ForegroundInfo {
LogUtils.i("DownloadWorker", progress)
val id = applicationContext.getString(R.string.notification_channel_id)
val title = applicationContext.getString(R.string.notification_title)
val cancel = applicationContext.getString(R.string.cancel_download)
// This PendingIntent can be used to cancel the worker
val intent = WorkManager.getInstance(applicationContext)
.createCancelPendingIntent(getId())
// Create a Notification channel if necessary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel()
}
val notification = NotificationCompat.Builder(applicationContext, id)
.setContentTitle(title)
.setTicker(title)
.setContentText(progress)
.setSmallIcon(R.drawable.ic_work_notification)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build()
return ForegroundInfo(22, notification)
}
//8.0以上需要适配渠道
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
// Create a Notification channel
}
}
附带一句 kotlin IO 中的copyTo() 可以实现非常优雅的复制文件
baseActivity观察到$NEW_VERSION_TIP时:
else if(it.contains(NEW_VERSION_TIP)) {
val filePath = it.replace(NEW_VERSION_TIP, "")
GlobalScope.launch(Dispatchers.IO) {
// 没有系统签名的,需要弹窗申请安装app
val res = async { Jni.exec("pm install -r -d $filePath") }
if (!res.await()) {
withContext(Dispatchers.Main) {
installApk(filePath)
}
}
}
}
Jni 直接调用system执行命令即可,你也可以用java执行命令
worker 发起
private val constraintsWifi = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()
public fun verifyVersion() {
WorkManager.getInstance(CpnirApplication.context).cancelAllWorkByTag("verify_version_worker")
WorkManager.getInstance(CpnirApplication.context).cancelAllWorkByTag("download")
val verifyVersion = OneTimeWorkRequestBuilder<VerifyVersionWorker>()
.addTag("verify_version_worker")
.setConstraints(constraintsWifi)
.setInitialDelay(15, TimeUnit.SECONDS)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
val download = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(constraintsWifi)
.setInputMerger(OverwritingInputMerger::class.java)
.setInitialDelay(1, TimeUnit.SECONDS)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
WorkManager.getInstance(CpnirApplication.context).beginWith(verifyVersion).then(download).enqueue()
}
服务端部分
旧版本采用spring boot 返回一个字符串,然后从字符串文件名读取版本信息,这样每次的文件名都很重要。新版本我们直接返回一个json,并且采用的spring cloud的项目(毕竟把公司的服务端升级成cloud分布式了)
json对象大致(你可以携带更多比如分支等信息):
/**
* @author markrenChina
* @param versionName 版本号
* @param versionCode 版本码
* @param url 静态资源地址
*/
data class ResponseVersionInfo (
val versionName : String,
val versionCode : Int,
val url: String
)
在cloud 聚合项目中整加一个微服务项目,省略一些基础步骤(记得用netty)。接着建一个RestController提供PostMapping,大致如下:
@RestController
@RefreshScope
@RequestMapping(path = ["/demo"],consumes = [APPLICATION_JSON_VALUE],produces = [APPLICATION_JSON_VALUE])
public class Demo{
@Value("\${demot.versionName}")
private val versionName: String = ""
@Value("\${demot.versionCode}")
private val versionCode: Int = 0
@Value("\${demo.url}")
private val url: String = ""
@PostMapping("version")
fun versionInfo(@RequestBody request: Mono<RequestVersionInfo>): Mono<ResponseVersionInfo> {
return request.filter {
//过滤条件自己设置,这里只是简单示例
abs(System.currentTimeMillis() - it.Time) < 60000
&& it.AppId == "XXXXXXXXXXXXXXX"
&& it.Key == "XXXXXXXXXXXXX"
&& it.Sign == SignUtils.encode("XXXXXXXXX")
}.map {
ResponseVersionInfo(
versionName,
versionCode,
url
)
}
}
}
配置nacos,在配置中心加入demo下的versionName,versionCode,url。配置信息多了可以上数据库,推荐nosql类型。
nacos 有个好处就是发布新版本非常方便。
配置gateway网关,在网关负载均衡并且重写url。
ok,一个自动更新项目就这样完成了!终于把以前写的代码优雅了起来!还有优化的地方,请留言!