From 481c57c5350217ca78f47442645d89019fda63f5 Mon Sep 17 00:00:00 2001 From: Liuxinyu176 <1041316040@qq.com> Date: Fri, 9 Oct 2026 10:11:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(update):=20=E5=B7=B2=E4=B8=8B=E8=BD=BD=20A?= =?UTF-8?q?PK=20=E7=9B=B4=E6=8E=A5=E5=A4=8D=E7=94=A8=20+=20=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=A4=B1=E8=B4=A5=E5=8F=AF=E9=87=8D=E8=AF=95=E4=B8=8D?= =?UTF-8?q?=E9=87=8D=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - downloadApk 检测同版本已完成文件,存在则跳过下载 - 下载先写 .part 再改名,避免半截文件被误用 - install 失败保留 Ready(file, error),UI 显示重试安装 - NeedInstallPermission 携带文件,开启权限后可继续安装 --- app/build.gradle.kts | 4 ++-- .../token/data/repository/UpdateRepository.kt | 11 +++++++++-- .../rainy/token/ui/settings/SettingsScreen.kt | 19 ++++++++++++++++++- .../token/ui/settings/UpdateViewModel.kt | 10 +++++----- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbe625d..e402f8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/com/rainy/token/data/repository/UpdateRepository.kt b/app/src/main/java/com/rainy/token/data/repository/UpdateRepository.kt index 32b8151..921f23c 100644 --- a/app/src/main/java/com/rainy/token/data/repository/UpdateRepository.kt +++ b/app/src/main/java/com/rainy/token/data/repository/UpdateRepository.kt @@ -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) diff --git a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt index 6344a8a..6b25d18 100644 --- a/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/rainy/token/ui/settings/SettingsScreen.kt @@ -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( diff --git a/app/src/main/java/com/rainy/token/ui/settings/UpdateViewModel.kt b/app/src/main/java/com/rainy/token/ui/settings/UpdateViewModel.kt index 3fc9456..6b56e0e 100644 --- a/app/src/main/java/com/rainy/token/ui/settings/UpdateViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/settings/UpdateViewModel.kt @@ -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 ?: "未知错误"))) } } } }