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 ?: "未知错误"))) } } } }