fix: download timeout + file validation + source indicator + stale APK
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) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ class UpdateChecker(private val context: Context) {
|
||||
val updateInfo = MutableStateFlow<UpdateInfo?>(null)
|
||||
val downloading = MutableStateFlow(false)
|
||||
val downloadProgress = MutableStateFlow(0)
|
||||
val downloadSource = MutableStateFlow<String?>(null) // "Gitea" / "GitHub" / null
|
||||
val lastError = MutableStateFlow<String?>(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
|
||||
}
|
||||
|
||||
@@ -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<File?>(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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user