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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user