feat(update): 已下载 APK 直接复用 + 安装失败可重试不重下
Some checks are pending
Release / build (push) Waiting to run
Some checks are pending
Release / build (push) Waiting to run
- downloadApk 检测同版本已完成文件,存在则跳过下载 - 下载先写 .part 再改名,避免半截文件被误用 - install 失败保留 Ready(file, error),UI 显示重试安装 - NeedInstallPermission 携带文件,开启权限后可继续安装
This commit is contained in:
parent
571b270cb6
commit
481c57c535
@ -16,8 +16,8 @@ android {
|
||||
applicationId = "com.rainy.token"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 20
|
||||
versionName = "1.7.0"
|
||||
versionCode = 21
|
||||
versionName = "1.7.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@ -56,7 +56,7 @@ class UpdateRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载 APK 到缓存目录。onProgress 回调下载字节数/总字节数(total 可能为 -1)。 */
|
||||
/** 下载 APK 到缓存目录;同版本已下载完成时直接复用,不重复下载。 */
|
||||
suspend fun downloadApk(
|
||||
info: UpdateInfo,
|
||||
onProgress: (downloaded: Long, total: Long) -> Unit
|
||||
@ -64,6 +64,11 @@ class UpdateRepository @Inject constructor(
|
||||
try {
|
||||
val dir = File(appContext.cacheDir, "apk_downloads").apply { mkdirs() }
|
||||
val file = File(dir, "rainytoken-" + info.versionName + ".apk")
|
||||
if (file.exists() && file.length() > 0L) {
|
||||
return@withContext Result.success(file)
|
||||
}
|
||||
// 先写 .part,下载完整后再替换正式文件名,避免半截文件被当成完整包
|
||||
val part = File(dir, file.name + ".part")
|
||||
val request = Request.Builder().url(info.apkUrl).get().build()
|
||||
okHttpClient.newCall(request).execute().use { resp ->
|
||||
if (!resp.isSuccessful) {
|
||||
@ -71,7 +76,7 @@ class UpdateRepository @Inject constructor(
|
||||
}
|
||||
val body = resp.body ?: return@withContext Result.failure(IOException("empty body"))
|
||||
val total = body.contentLength()
|
||||
file.outputStream().buffered().use { output ->
|
||||
part.outputStream().buffered().use { output ->
|
||||
body.byteStream().use { input ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var downloaded = 0L
|
||||
@ -84,6 +89,8 @@ class UpdateRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file.exists()) file.delete()
|
||||
part.renameTo(file)
|
||||
Result.success(file)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
|
||||
@ -508,9 +508,23 @@ private fun UpdateCard(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = InkMuted
|
||||
)
|
||||
if (phase.error != null) {
|
||||
Spacer(modifier = Modifier.padding(top = 4.dp))
|
||||
Text(
|
||||
text = phase.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.padding(top = 8.dp))
|
||||
Button(onClick = { viewModel.install(phase.file) }) {
|
||||
Text(stringResource(R.string.update_install))
|
||||
Text(
|
||||
if (phase.error == null) {
|
||||
stringResource(R.string.update_install)
|
||||
} else {
|
||||
"重试安装"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is UpdateViewModel.Phase.NeedInstallPermission -> {
|
||||
@ -529,6 +543,9 @@ private fun UpdateCard(
|
||||
}) {
|
||||
Text(stringResource(R.string.update_enable_permission))
|
||||
}
|
||||
TextButton(onClick = { viewModel.install(phase.file) }) {
|
||||
Text("已开启,继续安装")
|
||||
}
|
||||
}
|
||||
is UpdateViewModel.Phase.Error -> {
|
||||
Text(
|
||||
|
||||
@ -30,8 +30,8 @@ class UpdateViewModel @Inject constructor(
|
||||
object Latest : Phase
|
||||
data class Available(val info: UpdateRepository.UpdateInfo) : Phase
|
||||
data class Downloading(val progress: Float) : Phase
|
||||
data class Ready(val file: File) : Phase
|
||||
object NeedInstallPermission : Phase
|
||||
data class Ready(val file: File, val error: String? = null) : Phase
|
||||
data class NeedInstallPermission(val file: File) : Phase
|
||||
data class Error(val message: String) : Phase
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ class UpdateViewModel @Inject constructor(
|
||||
val canInstall = runCatching { context.packageManager.canRequestPackageInstalls() }
|
||||
.getOrDefault(false)
|
||||
if (!canInstall) {
|
||||
_uiState.update { it.copy(phase = Phase.NeedInstallPermission) }
|
||||
_uiState.update { it.copy(phase = Phase.NeedInstallPermission(file)) }
|
||||
return
|
||||
}
|
||||
_uiState.update { it.copy(phase = Phase.Ready(file)) }
|
||||
@ -91,7 +91,7 @@ class UpdateViewModel @Inject constructor(
|
||||
val uri = try {
|
||||
FileProvider.getUriForFile(context, authority, file)
|
||||
} catch (e: Exception) {
|
||||
_uiState.update { it.copy(phase = Phase.Error("安装文件无效:" + (e.message ?: "未知错误"))) }
|
||||
_uiState.update { it.copy(phase = Phase.Ready(file, "安装文件无效:" + (e.message ?: "未知错误"))) }
|
||||
return
|
||||
}
|
||||
val actionView = Intent(Intent.ACTION_VIEW).apply {
|
||||
@ -105,7 +105,7 @@ class UpdateViewModel @Inject constructor(
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
runCatching { context.startActivity(actionInstall) }.onFailure { e ->
|
||||
_uiState.update { it.copy(phase = Phase.Error(e.message ?: "无法启动安装器")) }
|
||||
_uiState.update { it.copy(phase = Phase.Ready(file, "无法启动安装器:" + (e.message ?: "未知错误"))) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user