From a3f67a0a7c18ba7dbece7bcbf36db3c591d35ae0 Mon Sep 17 00:00:00 2001 From: Song <168455@qq.com> Date: Fri, 10 Jul 2026 19:17:53 +0800 Subject: [PATCH] fix: download timeout + file validation + source indicator + stale APK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of "install doesn't update": gradle cached old APK, release uploaded stale version. Fixes: - Dedicated download OkHttpClient with 120s readTimeout (was 30s) - 64KB buffer (was 8KB) for faster large-file download - Post-download file size validation (Content-Length / release declared) - Reject files < 1MB (likely HTML error pages) - downloadSource StateFlow shows "Gitea" or "GitHub" during download - UpdateDialog shows "正在下载更新…(来自 Gitea)" - version 1.9.2 -> 1.9.3 (code 31 -> 32) Co-Authored-By: Claude Opus 4.8 (1M context) --- app/build.gradle.kts | 4 +- .../com/a2i/forwarder/core/UpdateChecker.kt | 50 +++++++++++++++---- .../a2i/forwarder/ui/screens/UpdateDialog.kt | 3 +- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f24c2de..f206dbb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.a2i.forwarder" minSdk = 34 targetSdk = 36 - versionCode = 30 - versionName = "1.9.1" + versionCode = 32 + versionName = "1.9.3" } signingConfigs { diff --git a/app/src/main/java/com/a2i/forwarder/core/UpdateChecker.kt b/app/src/main/java/com/a2i/forwarder/core/UpdateChecker.kt index 5d28f29..8250a90 100644 --- a/app/src/main/java/com/a2i/forwarder/core/UpdateChecker.kt +++ b/app/src/main/java/com/a2i/forwarder/core/UpdateChecker.kt @@ -45,6 +45,7 @@ class UpdateChecker(private val context: Context) { val updateInfo = MutableStateFlow(null) val downloading = MutableStateFlow(false) val downloadProgress = MutableStateFlow(0) + val downloadSource = MutableStateFlow(null) // "Gitea" / "GitHub" / null val lastError = MutableStateFlow(null) private var lastNotifiedVersion: String? = null // 记录已推送的版本,避免重复通知 @@ -184,33 +185,49 @@ class UpdateChecker(private val context: Context) { suspend fun download(info: UpdateInfo): File? = withContext(Dispatchers.IO) { downloading.value = true downloadProgress.value = 0 + downloadSource.value = null lastError.value = null - // 主 URL 失败时自动尝试备选 URL - val urls = listOf(info.apkUrl) + (info.fallbackUrl?.let { listOf(it) } ?: emptyList()) - for ((idx, url) in urls.withIndex()) { - if (idx > 0) downloadProgress.value = 0 // 重试时重置进度 - val file = tryDownload(url, info.latestVersion) + // 主 URL (Gitea) 失败时自动尝试备选 URL (GitHub) + val sources = listOf( + info.apkUrl to sourceLabel(info.apkUrl), + ) + (info.fallbackUrl?.let { listOf(it to sourceLabel(it)) } ?: emptyList()) + for ((idx, pair) in sources.withIndex()) { + val (url, label) = pair + if (idx > 0) { downloadProgress.value = 0; downloadSource.value = null } + downloadSource.value = label + val file = tryDownload(url, info.latestVersion, info.apkSize) if (file != null) { downloading.value = false + downloadSource.value = null return@withContext file } } downloading.value = false + downloadSource.value = null null } - private suspend fun tryDownload(url: String, version: String): File? { + private fun sourceLabel(url: String): String = + if (url.contains("songer.eu.org") || url.contains("sunlunfan")) "Gitea" else "GitHub" + + private suspend fun tryDownload(url: String, version: String, expectedSize: Long): File? { + // 专用下载 client:更长超时(120s),避免大文件慢网超时截断 + val dlClient = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build() return try { val req = Request.Builder().url(url).get().build() - http.newCall(req).execute().use { res -> - if (!res.isSuccessful) { lastError.value = "HTTP ${res.code}"; return@tryDownload null } - val body = res.body ?: run { lastError.value = "空响应"; return@tryDownload null } + dlClient.newCall(req).execute().use { res -> + if (!res.isSuccessful) { lastError.value = "HTTP ${res.code}"; return null } + val body = res.body ?: run { lastError.value = "空响应"; return null } val total = body.contentLength() val dir = context.getExternalFilesDir(null) ?: context.cacheDir val target = File(dir, "gotmsg-${version}.apk") target.outputStream().use { sink -> val input = body.byteStream() - val buf = ByteArray(8 * 1024) + val buf = ByteArray(64 * 1024) // 64KB buffer 提升大文件速度 var read = 0L while (true) { val n = input.read(buf) @@ -220,6 +237,19 @@ class UpdateChecker(private val context: Context) { if (total > 0) downloadProgress.value = (read * 100 / total).toInt() } } + // 校验文件大小:Content-Length 或 release 声明的大小 + val actualSize = target.length() + val refSize = if (total > 0) total else expectedSize + if (refSize > 0 && actualSize < refSize) { + lastError.value = "文件不完整(${actualSize}/${refSize} 字节),请重试" + target.delete() + return null + } + if (actualSize < 1_000_000) { + lastError.value = "文件过小,可能下载了错误页面" + target.delete() + return null + } downloadProgress.value = 100 target } diff --git a/app/src/main/java/com/a2i/forwarder/ui/screens/UpdateDialog.kt b/app/src/main/java/com/a2i/forwarder/ui/screens/UpdateDialog.kt index 31d593c..7b279e5 100644 --- a/app/src/main/java/com/a2i/forwarder/ui/screens/UpdateDialog.kt +++ b/app/src/main/java/com/a2i/forwarder/ui/screens/UpdateDialog.kt @@ -37,6 +37,7 @@ fun UpdateDialog(info: UpdateInfo, onDismiss: () -> Unit) { val app = A2iApp.instance val downloading by app.updateChecker.downloading.collectAsState() val progress by app.updateChecker.downloadProgress.collectAsState() + val downloadSource by app.updateChecker.downloadSource.collectAsState() var downloadedFile by remember { mutableStateOf(null) } AlertDialog( @@ -70,7 +71,7 @@ fun UpdateDialog(info: UpdateInfo, onDismiss: () -> Unit) { ) Spacer(Modifier.size(4.dp)) Text( - "正在下载更新…", + "正在下载更新…${downloadSource?.let { "(来自 $it)" } ?: ""}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, )