feat(agent): add authenticated in-app updates (#144)
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "cn.ilapage.goauto.agent"
|
||||
minSdk = 23
|
||||
targetSdk = 34
|
||||
versionCode = 40
|
||||
versionName = "0.9.27"
|
||||
versionCode = 41
|
||||
versionName = "0.9.28"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<queries>
|
||||
<package android:name="com.xunmeng.pinduoduo" />
|
||||
@@ -36,6 +37,15 @@
|
||||
android:name=".service.AgentForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".automation.GoAutoAccessibilityService"
|
||||
android:exported="true"
|
||||
|
||||
@@ -15,6 +15,7 @@ import cn.ilapage.goauto.agent.service.AgentSettingsStore
|
||||
import cn.ilapage.goauto.agent.ui.AgentSettingsFragment
|
||||
import cn.ilapage.goauto.agent.ui.AgentStatusFragment
|
||||
import cn.ilapage.goauto.agent.ui.TaskHistoryFragment
|
||||
import cn.ilapage.goauto.agent.update.AgentAppUpdateManager
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -38,6 +39,7 @@ class MainActivity : AppCompatActivity() {
|
||||
setContentView(buildContent())
|
||||
requestNotificationPermission()
|
||||
if (AgentSettingsStore(this).serverUrl().isNotBlank()) AgentForegroundService.start(this)
|
||||
AgentAppUpdateManager.checkOnceAtStartup(this)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: android.content.Intent) {
|
||||
|
||||
@@ -8,6 +8,10 @@ import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import java.util.UUID
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
data class DeviceInfo(
|
||||
val installId: String,
|
||||
@@ -33,6 +37,16 @@ data class HeartbeatResult(
|
||||
val heartbeatIntervalSeconds: Int,
|
||||
)
|
||||
|
||||
data class AgentAppRelease(
|
||||
val id: Long,
|
||||
val versionCode: Long,
|
||||
val versionName: String,
|
||||
val sha256: String,
|
||||
val byteSize: Long,
|
||||
val releaseNotes: String,
|
||||
val downloadUrl: String,
|
||||
)
|
||||
|
||||
data class AgentTask(
|
||||
val taskId: Long,
|
||||
val attemptNumber: Int,
|
||||
@@ -219,6 +233,72 @@ class AgentApiClient(private val serverUrl: String) {
|
||||
)
|
||||
}
|
||||
|
||||
fun latestAgentAppRelease(token: String): AgentAppRelease? {
|
||||
val response = request("GET", "/api/agent/v1/app/latest", null, token) ?: return null
|
||||
if (response.isNull("data")) return null
|
||||
val data = response.getJSONObject("data")
|
||||
return AgentAppRelease(
|
||||
id = data.getLong("id"), versionCode = data.getLong("versionCode"),
|
||||
versionName = data.getString("versionName"), sha256 = data.getString("sha256"),
|
||||
byteSize = data.getLong("byteSize"), releaseNotes = data.optString("releaseNotes"),
|
||||
downloadUrl = data.getString("downloadUrl"),
|
||||
)
|
||||
}
|
||||
|
||||
fun downloadAgentAppRelease(
|
||||
release: AgentAppRelease,
|
||||
token: String,
|
||||
target: File,
|
||||
cancelled: AtomicBoolean,
|
||||
onProgress: (Int) -> Unit,
|
||||
) {
|
||||
val connection = (URL(serverUrl + release.downloadUrl).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 10_000
|
||||
readTimeout = 30_000
|
||||
useCaches = false
|
||||
setRequestProperty("Authorization", "Bearer $token")
|
||||
setRequestProperty("Accept", "application/vnd.android.package-archive")
|
||||
}
|
||||
try {
|
||||
val status = connection.responseCode
|
||||
if (status !in 200..299) {
|
||||
val body = connection.errorStream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() }.orEmpty()
|
||||
val json = runCatching { JSONObject(body) }.getOrElse { JSONObject() }
|
||||
throw AgentApiException(status, json.optString("code", "HTTP_$status"), json.optString("message", "Agent APK 下载失败"), status >= 500)
|
||||
}
|
||||
target.parentFile?.mkdirs()
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
var written = 0L
|
||||
connection.inputStream.use { input ->
|
||||
FileOutputStream(target).use { output ->
|
||||
val buffer = ByteArray(64 * 1024)
|
||||
while (true) {
|
||||
if (cancelled.get()) throw InterruptedException("下载已取消")
|
||||
val count = input.read(buffer)
|
||||
if (count < 0) break
|
||||
output.write(buffer, 0, count)
|
||||
digest.update(buffer, 0, count)
|
||||
written += count
|
||||
if (release.byteSize > 0) onProgress((written * 100 / release.byteSize).coerceIn(0, 100).toInt())
|
||||
}
|
||||
output.fd.sync()
|
||||
}
|
||||
}
|
||||
val actual = digest.digest().joinToString("") { "%02x".format(it) }
|
||||
if (written != release.byteSize || !actual.equals(release.sha256, ignoreCase = true)) {
|
||||
target.delete()
|
||||
error("APK 完整性校验失败,已删除下载文件")
|
||||
}
|
||||
onProgress(100)
|
||||
} catch (error: Throwable) {
|
||||
target.delete()
|
||||
throw error
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun nextTask(token: String): AgentTask? {
|
||||
val response = request("GET", "/api/agent/v1/tasks/next", null, token) ?: return null
|
||||
return task(response.getJSONObject("data"))
|
||||
|
||||
@@ -19,6 +19,7 @@ import cn.ilapage.goauto.agent.BuildConfig
|
||||
import cn.ilapage.goauto.agent.R
|
||||
import cn.ilapage.goauto.agent.identity.SecureDeviceStore
|
||||
import cn.ilapage.goauto.agent.network.AgentApiClient
|
||||
import cn.ilapage.goauto.agent.network.AgentAppRelease
|
||||
import cn.ilapage.goauto.agent.network.CollectionHistoryItem
|
||||
import cn.ilapage.goauto.agent.network.PurchaseHistoryItem
|
||||
import cn.ilapage.goauto.agent.network.ServerUrlPolicy
|
||||
@@ -29,11 +30,15 @@ import cn.ilapage.goauto.agent.service.AgentStateStore
|
||||
import cn.ilapage.goauto.agent.service.CollectionIntervalRange
|
||||
import cn.ilapage.goauto.agent.service.CollectionIntervalPolicy
|
||||
import cn.ilapage.goauto.agent.service.HistoryRangePolicy
|
||||
import cn.ilapage.goauto.agent.update.AgentAppUpdateManager
|
||||
import cn.ilapage.goauto.agent.update.UpdateCheckResult
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
@@ -63,6 +68,13 @@ class AgentSettingsFragment : Fragment() {
|
||||
private lateinit var historyDaysInput: TextInputEditText
|
||||
private lateinit var historySyncButton: MaterialButton
|
||||
private lateinit var historySyncFeedback: TextView
|
||||
private lateinit var updateButton: MaterialButton
|
||||
private lateinit var updateFeedback: TextView
|
||||
private var availableRelease: AgentAppRelease? = null
|
||||
private var downloadedAPK: File? = null
|
||||
private var updateBusy = false
|
||||
private var updateDownloading = false
|
||||
private var updateCancelled = AtomicBoolean(false)
|
||||
private var testing = false
|
||||
private var syncingHistory = false
|
||||
private val refresh = object : Runnable {
|
||||
@@ -296,6 +308,19 @@ class AgentSettingsFragment : Fragment() {
|
||||
historySyncFeedback.setPadding(0, context.dp(8), 0, 0)
|
||||
addView(historySyncFeedback)
|
||||
}))
|
||||
addView(context.card(context.cardColumn().apply {
|
||||
addView(context.label("Agent 应用更新", 18f, context.getColor(R.color.agent_text), true))
|
||||
updateFeedback = context.label("当前版本 ${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})", 14f, context.getColor(R.color.agent_text_muted))
|
||||
updateFeedback.setPadding(0, context.dp(8), 0, 0)
|
||||
addView(updateFeedback)
|
||||
updateButton = MaterialButton(context).apply {
|
||||
text = "检查更新"
|
||||
minHeight = context.dp(48)
|
||||
contentDescription = "检查 Agent 应用更新"
|
||||
setOnClickListener { handleUpdateAction() }
|
||||
}
|
||||
addView(updateButton, fullWidth(12))
|
||||
}))
|
||||
addView(context.card(context.cardColumn().apply {
|
||||
addView(context.label("设备与诊断", 18f, context.getColor(R.color.agent_text), true))
|
||||
diagnostics = context.label("—", 14f, context.getColor(R.color.agent_text))
|
||||
@@ -308,6 +333,13 @@ class AgentSettingsFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (::updateFeedback.isInitialized && !updateBusy) {
|
||||
AgentAppUpdateManager(requireContext()).cachedAvailable()?.let {
|
||||
availableRelease = it
|
||||
updateFeedback.text = "发现新版本 ${it.versionName}(${it.versionCode}),可手动下载并安装。"
|
||||
}
|
||||
refreshUpdateButton()
|
||||
}
|
||||
handler.removeCallbacks(refresh)
|
||||
handler.post(refresh)
|
||||
}
|
||||
@@ -323,6 +355,7 @@ class AgentSettingsFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
updateCancelled.set(true)
|
||||
executor.shutdownNow()
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -350,6 +383,106 @@ class AgentSettingsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateAction() {
|
||||
if (updateDownloading) {
|
||||
updateCancelled.set(true)
|
||||
updateFeedback.text = "正在取消下载…"
|
||||
return
|
||||
}
|
||||
if (updateBusy) return
|
||||
val apk = downloadedAPK
|
||||
if (apk?.isFile == true) {
|
||||
val manager = AgentAppUpdateManager(requireContext())
|
||||
if (!manager.canInstallPackages()) {
|
||||
updateFeedback.text = "请在系统页面允许此来源安装应用,返回后点击“安装已下载版本”。"
|
||||
manager.openInstallPermission()
|
||||
} else {
|
||||
runCatching { manager.install(apk) }.onFailure { showUpdateError(it) }
|
||||
}
|
||||
return
|
||||
}
|
||||
val release = availableRelease
|
||||
if (release != null) downloadUpdate(release) else checkUpdate()
|
||||
}
|
||||
|
||||
private fun checkUpdate() {
|
||||
val state = stateStore.read()
|
||||
if (state.code == "BUSY" || state.currentTaskId != null) {
|
||||
updateFeedback.text = "任务执行中,暂时不能检查或安装更新。"
|
||||
updateFeedback.setTextColor(requireContext().getColor(R.color.agent_warning))
|
||||
return
|
||||
}
|
||||
updateBusy = true
|
||||
updateDownloading = true
|
||||
refreshUpdateButton()
|
||||
updateFeedback.text = "正在检查更新…"
|
||||
executor.execute {
|
||||
val result = runCatching { AgentAppUpdateManager(requireContext()).check() }
|
||||
activity?.runOnUiThread {
|
||||
if (!isAdded || view == null) return@runOnUiThread
|
||||
updateBusy = false
|
||||
updateDownloading = false
|
||||
result.onSuccess { checked ->
|
||||
when (checked) {
|
||||
UpdateCheckResult.Busy -> updateFeedback.text = "任务执行中,暂时不能更新 Agent。"
|
||||
UpdateCheckResult.NotRegistered -> updateFeedback.text = "设备尚未注册或服务器地址未配置。"
|
||||
UpdateCheckResult.UpToDate -> updateFeedback.text = "当前已是最新版本 ${BuildConfig.VERSION_NAME}。"
|
||||
is UpdateCheckResult.Available -> {
|
||||
availableRelease = checked.release
|
||||
updateFeedback.text = "发现新版本 ${checked.release.versionName}(${checked.release.versionCode})。\n${checked.release.releaseNotes}"
|
||||
MaterialAlertDialogBuilder(requireContext()).setTitle("发现 Agent 新版本").setMessage(updateFeedback.text).setNegativeButton("稍后", null).setPositiveButton("下载") { _, _ -> downloadUpdate(checked.release) }.show()
|
||||
}
|
||||
}
|
||||
}.onFailure(::showUpdateError)
|
||||
refreshUpdateButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadUpdate(release: AgentAppRelease) {
|
||||
val state = stateStore.read()
|
||||
if (state.code == "BUSY" || state.currentTaskId != null) {
|
||||
updateFeedback.text = "任务执行中,下载已阻止。"
|
||||
return
|
||||
}
|
||||
updateBusy = true
|
||||
updateCancelled = AtomicBoolean(false)
|
||||
refreshUpdateButton()
|
||||
executor.execute {
|
||||
val result = runCatching {
|
||||
AgentAppUpdateManager(requireContext()).download(release, updateCancelled) { progress ->
|
||||
handler.post { if (isAdded && view != null) updateFeedback.text = "正在下载 ${release.versionName}:$progress%" }
|
||||
}
|
||||
}
|
||||
activity?.runOnUiThread {
|
||||
if (!isAdded || view == null) return@runOnUiThread
|
||||
updateBusy = false
|
||||
result.onSuccess { apk ->
|
||||
downloadedAPK = apk
|
||||
updateFeedback.text = "下载及 SHA-256 校验完成。安装需要在系统确认页面手动确认。"
|
||||
}.onFailure(::showUpdateError)
|
||||
refreshUpdateButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshUpdateButton() {
|
||||
if (!::updateButton.isInitialized) return
|
||||
updateButton.isEnabled = !updateBusy || updateDownloading
|
||||
updateButton.text = when {
|
||||
updateDownloading -> "取消下载"
|
||||
updateBusy -> "处理中…"
|
||||
downloadedAPK?.isFile == true -> "安装已下载版本"
|
||||
availableRelease != null -> "下载新版本"
|
||||
else -> "检查更新"
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUpdateError(error: Throwable) {
|
||||
updateFeedback.text = error.message?.takeIf { it.isNotBlank() } ?: "更新失败,请稍后重试。"
|
||||
updateFeedback.setTextColor(requireContext().getColor(R.color.agent_error))
|
||||
}
|
||||
|
||||
private fun confirmSave() {
|
||||
val normalizedUrl = validateInputs(requireName = true) ?: return
|
||||
val deviceName = nameInput.text?.toString()?.trim().orEmpty()
|
||||
@@ -526,6 +659,7 @@ class AgentSettingsFragment : Fragment() {
|
||||
val context = requireContext()
|
||||
val state = stateStore.read()
|
||||
val busy = state.code == "BUSY" || state.currentTaskId != null
|
||||
if (busy && updateDownloading) updateCancelled.set(true)
|
||||
val editable = SettingsAvailabilityResolver.editable(state.code, state.currentTaskId, testing)
|
||||
serverInput.isEnabled = editable
|
||||
nameInput.isEnabled = editable
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package cn.ilapage.goauto.agent.update
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.FileProvider
|
||||
import cn.ilapage.goauto.agent.BuildConfig
|
||||
import cn.ilapage.goauto.agent.identity.SecureDeviceStore
|
||||
import cn.ilapage.goauto.agent.network.AgentApiClient
|
||||
import cn.ilapage.goauto.agent.network.AgentAppRelease
|
||||
import cn.ilapage.goauto.agent.service.AgentSettingsStore
|
||||
import cn.ilapage.goauto.agent.service.AgentStateStore
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
sealed class UpdateCheckResult {
|
||||
data object NotRegistered : UpdateCheckResult()
|
||||
data object Busy : UpdateCheckResult()
|
||||
data object UpToDate : UpdateCheckResult()
|
||||
data class Available(val release: AgentAppRelease) : UpdateCheckResult()
|
||||
}
|
||||
|
||||
class AgentAppUpdateManager(private val context: Context) {
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
fun check(): UpdateCheckResult {
|
||||
val state = AgentStateStore(appContext).read()
|
||||
if (AgentUpdatePolicy.blockedByTask(state.code, state.currentTaskId)) return UpdateCheckResult.Busy
|
||||
val credentials = SecureDeviceStore(appContext).credentials() ?: return UpdateCheckResult.NotRegistered
|
||||
val serverUrl = AgentSettingsStore(appContext).serverUrl()
|
||||
if (serverUrl.isBlank()) return UpdateCheckResult.NotRegistered
|
||||
val release = AgentApiClient(serverUrl).latestAgentAppRelease(credentials.token)
|
||||
cacheLatest(release)
|
||||
return if (release == null || !AgentUpdatePolicy.updateAvailable(BuildConfig.VERSION_CODE.toLong(), release.versionCode)) UpdateCheckResult.UpToDate else UpdateCheckResult.Available(release)
|
||||
}
|
||||
|
||||
fun cachedAvailable(): AgentAppRelease? = preferences().getString(CACHED_RELEASE, null)?.let { raw ->
|
||||
runCatching {
|
||||
val data = JSONObject(raw)
|
||||
AgentAppRelease(data.getLong("id"), data.getLong("versionCode"), data.getString("versionName"), data.getString("sha256"), data.getLong("byteSize"), data.optString("releaseNotes"), data.getString("downloadUrl"))
|
||||
}.getOrNull()?.takeIf { it.versionCode > BuildConfig.VERSION_CODE }
|
||||
}
|
||||
|
||||
fun download(release: AgentAppRelease, cancelled: AtomicBoolean, onProgress: (Int) -> Unit): File {
|
||||
check(!AgentStateStore(appContext).read().let { AgentUpdatePolicy.blockedByTask(it.code, it.currentTaskId) }) { "任务执行中,不能更新 Agent" }
|
||||
val credentials = checkNotNull(SecureDeviceStore(appContext).credentials()) { "设备尚未注册" }
|
||||
val serverUrl = AgentSettingsStore(appContext).serverUrl()
|
||||
check(serverUrl.isNotBlank()) { "服务器地址未配置" }
|
||||
val target = File(File(appContext.cacheDir, "agent-updates"), "agent-${release.versionCode}.apk")
|
||||
AgentApiClient(serverUrl).downloadAgentAppRelease(release, credentials.token, target, cancelled, onProgress)
|
||||
return target
|
||||
}
|
||||
|
||||
fun canInstallPackages(): Boolean = Build.VERSION.SDK_INT < 26 || appContext.packageManager.canRequestPackageInstalls()
|
||||
|
||||
fun openInstallPermission() {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${appContext.packageName}")))
|
||||
}
|
||||
}
|
||||
|
||||
fun install(apk: File) {
|
||||
check(!AgentStateStore(appContext).read().let { AgentUpdatePolicy.blockedByTask(it.code, it.currentTaskId) }) { "任务执行中,不能安装更新" }
|
||||
check(apk.isFile) { "已下载的 APK 不存在,请重新下载" }
|
||||
check(canInstallPackages()) { "请先允许此来源安装应用" }
|
||||
val uri = FileProvider.getUriForFile(appContext, "${appContext.packageName}.fileprovider", apk)
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
}
|
||||
|
||||
private fun cacheLatest(release: AgentAppRelease?) {
|
||||
val value = release?.let { JSONObject().put("id", it.id).put("versionCode", it.versionCode).put("versionName", it.versionName).put("sha256", it.sha256).put("byteSize", it.byteSize).put("releaseNotes", it.releaseNotes).put("downloadUrl", it.downloadUrl).toString() }
|
||||
preferences().edit().putString(CACHED_RELEASE, value).apply()
|
||||
}
|
||||
|
||||
private fun preferences() = appContext.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES = "goauto_agent_update"
|
||||
private const val CACHED_RELEASE = "cached_release"
|
||||
private val startupExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
fun checkOnceAtStartup(context: Context) {
|
||||
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
|
||||
if (preferences.getBoolean("startup_checked_${BuildConfig.VERSION_CODE}", false)) return
|
||||
preferences.edit().putBoolean("startup_checked_${BuildConfig.VERSION_CODE}", true).apply()
|
||||
startupExecutor.execute { runCatching { AgentAppUpdateManager(context).check() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package cn.ilapage.goauto.agent.update
|
||||
|
||||
object AgentUpdatePolicy {
|
||||
fun blockedByTask(stateCode: String, currentTaskId: Long?): Boolean = stateCode == "BUSY" || currentTaskId != null
|
||||
fun updateAvailable(currentVersionCode: Long, latestVersionCode: Long): Boolean = latestVersionCode > currentVersionCode
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="agent_updates" path="agent-updates/" />
|
||||
</paths>
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.ilapage.goauto.agent.update
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AgentUpdatePolicyTest {
|
||||
@Test fun onlyHigherIntegerVersionCodeIsAnUpdate() {
|
||||
assertTrue(AgentUpdatePolicy.updateAvailable(40, 41))
|
||||
assertFalse(AgentUpdatePolicy.updateAvailable(41, 41))
|
||||
assertFalse(AgentUpdatePolicy.updateAvailable(41, 40))
|
||||
}
|
||||
|
||||
@Test fun anyActiveTaskBlocksUpdate() {
|
||||
assertTrue(AgentUpdatePolicy.blockedByTask("BUSY", null))
|
||||
assertTrue(AgentUpdatePolicy.blockedByTask("ONLINE", 99))
|
||||
assertFalse(AgentUpdatePolicy.blockedByTask("ONLINE", null))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user