fix: startup always checks + dual-source (Gitea+GitHub) take highest

- start(): check on every launch (no 24h throttle) so the banner shows
  after restart even if checked manually moments ago; plus a 24h loop
  while app is resident
- check both Gitea and GitHub release APIs, skip whichever is
  unreachable, notify the higher version
- version 1.7.2 -> 1.7.3 (code 15 -> 16)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 20:48:16 +08:00
parent 94422fbd76
commit 9985719f87
2 changed files with 54 additions and 29 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "com.a2i.forwarder" applicationId = "com.a2i.forwarder"
minSdk = 34 minSdk = 34
targetSdk = 36 targetSdk = 36
versionCode = 15 versionCode = 16
versionName = "1.7.2" versionName = "1.7.3"
} }
signingConfigs { signingConfigs {
@@ -11,6 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -49,51 +50,71 @@ class UpdateChecker(private val context: Context) {
fun start() { fun start() {
scope.launch { scope.launch {
delay(5000) // 启动 5 秒后首检,给网络/应用初始化留时间 delay(5000) // 启动 5 秒后首检,给网络/应用初始化留时间
check(force = false) doCheck(throttle = false, toggle = true) // 启动必查(无视节流,但尊重开关)
while (isActive) {
delay(CHECK_INTERVAL_MS) // 常驻期间每 24h 巡检一次
doCheck(throttle = true, toggle = true)
}
} }
} }
suspend fun check(force: Boolean): Result<UpdateInfo?> = withContext(Dispatchers.IO) { /** 手动检查:无视节流和开关(用户主动)。 */
suspend fun check(force: Boolean): Result<UpdateInfo?> = doCheck(throttle = !force, toggle = !force)
private suspend fun doCheck(throttle: Boolean, toggle: Boolean): Result<UpdateInfo?> = withContext(Dispatchers.IO) {
runCatching { runCatching {
val settings = app.settings val settings = app.settings
if (toggle && !settings.updateCheckEnabled.value) return@runCatching updateInfo.value
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
// 自动检查跳过条件(仅 force=false 时有效) if (throttle && now - settings.lastUpdateCheckTime.value < CHECK_INTERVAL_MS) return@runCatching updateInfo.value
if (!force) {
if (!settings.updateCheckEnabled.value) return@runCatching updateInfo.value // 双源查询(Gitea 国内优先 + GitHub 备用),任一不通跳过,取最高版本
if (now - settings.lastUpdateCheckTime.value < CHECK_INTERVAL_MS) return@runCatching updateInfo.value val infos = RELEASES_APIS.mapNotNull { api ->
} runCatching { fetchRelease(api) }.getOrNull()
val req = Request.Builder()
.url(RELEASES_API)
.header("Accept", "application/json")
.header("User-Agent", "a2i-android")
.get()
.build()
val info = http.newCall(req).execute().use { res ->
if (!res.isSuccessful) error("HTTP ${res.code}")
val body = res.body?.string() ?: error("空响应")
parseRelease(body)
} }
settings.setLastUpdateCheck(now) settings.setLastUpdateCheck(now)
if (infos.isEmpty()) {
lastError.value = "所有更新源不可达"
return@runCatching updateInfo.value
}
val current = currentVersionName() val current = currentVersionName()
val available = info != null && val best = infos
isNewer(info.latestVersion, current) && .filter { isNewer(it.latestVersion, current) && it.latestVersion != settings.ignoredVersion.value }
info.latestVersion != settings.ignoredVersion.value .maxByOrNull { versionKey(it.latestVersion) }
val prev = updateInfo.value val prev = updateInfo.value
updateInfo.value = if (available) info else { if (best == null) {
lastNotifiedVersion = null // 清除记录,等下一个新版 updateInfo.value = null
null lastNotifiedVersion = null
} else {
updateInfo.value = best
} }
lastError.value = null lastError.value = null
// 发现新版且之前没推过通知 → 自动推送到所有已开启通道 // 发现新版且之前没推过通知 → 自动推送到所有已开启通道
if (available && info != null && info.latestVersion != lastNotifiedVersion) { if (best != null && best.latestVersion != lastNotifiedVersion) {
lastNotifiedVersion = info.latestVersion lastNotifiedVersion = best.latestVersion
notifyNewVersion(prev, info) notifyNewVersion(prev, best)
} }
updateInfo.value updateInfo.value
}.onFailure { lastError.value = it.message ?: "检查失败" } }.onFailure { lastError.value = it.message ?: "检查失败" }
} }
private fun fetchRelease(api: String): UpdateInfo? {
val req = Request.Builder()
.url(api)
.header("Accept", "application/json")
.header("User-Agent", "a2i-android")
.get()
.build()
return http.newCall(req).execute().use { res ->
if (!res.isSuccessful) error("HTTP ${res.code}")
parseRelease(res.body?.string() ?: error("空响应"))
}
}
private fun versionKey(v: String): String =
v.split(".").joinToString(".") { (it.substringBefore("-").toIntOrNull() ?: 0).toString().padStart(4, '0') }
/** 发现新版后,自动通过所有已开启的推送通道通知用户 */ /** 发现新版后,自动通过所有已开启的推送通道通知用户 */
private fun notifyNewVersion(prev: UpdateInfo?, info: UpdateInfo) { private fun notifyNewVersion(prev: UpdateInfo?, info: UpdateInfo) {
val s = app.settings val s = app.settings
@@ -225,7 +246,11 @@ class UpdateChecker(private val context: Context) {
} }
companion object { companion object {
const val RELEASES_API = "https://git.sunlunfan.com/api/v1/repos/song/a2i/releases/latest" // 双源:Gitea(国内优先)+ GitHub(备用),任一不通跳过,取最高版本
val RELEASES_APIS = listOf(
"https://git.sunlunfan.com/api/v1/repos/song/a2i/releases/latest",
"https://api.github.com/repos/lsxf/a2i/releases/latest",
)
const val CHECK_INTERVAL_MS = 24L * 60 * 60 * 1000 const val CHECK_INTERVAL_MS = 24L * 60 * 60 * 1000
} }