2026-04-21 22:07:29 +08:00
|
|
|
|
package com.xuqm.sdk.update
|
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
|
import android.content.Intent
|
|
|
|
|
|
import android.net.Uri
|
2026-04-30 16:54:10 +08:00
|
|
|
|
import android.os.Build
|
2026-04-21 22:07:29 +08:00
|
|
|
|
import androidx.core.content.FileProvider
|
|
|
|
|
|
import com.xuqm.sdk.XuqmSDK
|
2026-04-28 20:11:38 +08:00
|
|
|
|
import com.xuqm.sdk.file.FileSDK
|
2026-04-27 19:30:06 +08:00
|
|
|
|
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
2026-04-21 22:07:29 +08:00
|
|
|
|
import com.xuqm.sdk.network.ApiClient
|
|
|
|
|
|
import com.xuqm.sdk.update.api.UpdateApi
|
|
|
|
|
|
import com.xuqm.sdk.update.model.UpdateInfo
|
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
|
import java.io.File
|
|
|
|
|
|
|
|
|
|
|
|
object UpdateSDK {
|
|
|
|
|
|
|
2026-04-27 19:30:06 +08:00
|
|
|
|
private val api: UpdateApi get() = ApiClient.create(UpdateApi::class.java, ServiceEndpointRegistry.updateBaseUrl)
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
2026-04-27 19:00:54 +08:00
|
|
|
|
private fun normalizeDownloadUrl(rawUrl: String?): String? {
|
|
|
|
|
|
if (rawUrl.isNullOrBlank()) return rawUrl
|
|
|
|
|
|
|
|
|
|
|
|
return runCatching {
|
|
|
|
|
|
val uri = Uri.parse(rawUrl)
|
|
|
|
|
|
if (uri.path?.startsWith("/files/apk/") == true) {
|
|
|
|
|
|
"${uri.scheme}://${uri.authority}/api/v1/updates${uri.path}"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
rawUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
}.getOrDefault(rawUrl)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 13:14:02 +08:00
|
|
|
|
private fun prefs(context: Context) =
|
|
|
|
|
|
context.applicationContext.getSharedPreferences("xuqm_update_prefs", android.content.Context.MODE_PRIVATE)
|
|
|
|
|
|
|
|
|
|
|
|
/** 忽略指定版本,下次检测到该版本时不再提示(强制更新版本不受此影响) */
|
|
|
|
|
|
fun ignoreVersion(context: Context, versionCode: Int) {
|
|
|
|
|
|
prefs(context).edit().putBoolean("ignored_v$versionCode", true).apply()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 清除所有已忽略版本记录 */
|
|
|
|
|
|
fun clearIgnoredVersions(context: Context) {
|
|
|
|
|
|
prefs(context).edit().clear().apply()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun isVersionIgnored(context: Context, versionCode: Int): Boolean =
|
|
|
|
|
|
prefs(context).getBoolean("ignored_v$versionCode", false)
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检测应用更新。
|
2026-06-04 13:35:59 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @param bypassIgnore 是否绕过"忽略版本"过滤。
|
|
|
|
|
|
* - `false`(默认,静默检查):用户已忽略的版本不再弹窗,适合启动时后台检查。
|
|
|
|
|
|
* - `true`(主动检查):忽略记录不生效,始终弹出更新对话框;无更新时由调用方显示提示。
|
|
|
|
|
|
*
|
|
|
|
|
|
* userId 通过 [XuqmSDK.login] 设置会话后自动传递,无需外部覆盖。
|
2026-06-04 13:14:02 +08:00
|
|
|
|
*/
|
2026-06-04 13:35:59 +08:00
|
|
|
|
suspend fun checkAppUpdate(context: Context, bypassIgnore: Boolean = false): UpdateInfo? = withContext(Dispatchers.IO) {
|
2026-05-22 17:56:42 +08:00
|
|
|
|
awaitInitialization()
|
2026-04-30 16:54:10 +08:00
|
|
|
|
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
|
|
|
|
|
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
|
|
|
|
packageInfo.longVersionCode.toInt()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
|
packageInfo.versionCode
|
|
|
|
|
|
}
|
2026-06-04 13:35:59 +08:00
|
|
|
|
val userId = XuqmSDK.currentLoginSession?.userId
|
2026-04-27 17:18:55 +08:00
|
|
|
|
runCatching {
|
2026-06-04 13:14:02 +08:00
|
|
|
|
api.checkUpdate(XuqmSDK.appKey, "ANDROID", versionCode, userId).data?.let { info ->
|
|
|
|
|
|
val normalized = info.copy(downloadUrl = normalizeDownloadUrl(info.downloadUrl) ?: info.downloadUrl)
|
2026-06-04 13:35:59 +08:00
|
|
|
|
// 静默检查时跳过已忽略版本;主动检查(bypassIgnore=true)始终展示
|
|
|
|
|
|
if (!bypassIgnore && normalized.needsUpdate && !normalized.forceUpdate
|
|
|
|
|
|
&& isVersionIgnored(context, normalized.versionCode)
|
|
|
|
|
|
) {
|
2026-06-04 13:14:02 +08:00
|
|
|
|
normalized.copy(needsUpdate = false)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
normalized
|
|
|
|
|
|
}
|
2026-04-27 19:00:54 +08:00
|
|
|
|
}
|
2026-04-27 17:18:55 +08:00
|
|
|
|
}.getOrNull()
|
2026-04-21 22:07:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 13:14:02 +08:00
|
|
|
|
private suspend fun awaitInitialization() {
|
|
|
|
|
|
if (XuqmSDK.isInitialized()) return
|
|
|
|
|
|
kotlinx.coroutines.withTimeoutOrNull(15_000L) {
|
|
|
|
|
|
while (!XuqmSDK.isInitialized()) {
|
|
|
|
|
|
kotlinx.coroutines.delay(100)
|
|
|
|
|
|
}
|
|
|
|
|
|
} ?: throw IllegalStateException("XuqmSDK not initialized. Check that config.xuqm is present and valid.")
|
2026-05-22 17:56:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
|
suspend fun downloadAndInstall(
|
|
|
|
|
|
context: Context,
|
|
|
|
|
|
downloadUrl: String,
|
|
|
|
|
|
onProgress: (Int) -> Unit = {},
|
|
|
|
|
|
) = withContext(Dispatchers.IO) {
|
|
|
|
|
|
val apkFile = File(context.getExternalFilesDir(null), "update.apk")
|
2026-04-28 20:11:38 +08:00
|
|
|
|
FileSDK.downloadToFile(downloadUrl, apkFile, onProgress)
|
2026-04-27 17:18:55 +08:00
|
|
|
|
withContext(Dispatchers.Main) { installApk(context, apkFile) }
|
2026-04-21 22:07:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun installApk(context: Context, apkFile: File) {
|
|
|
|
|
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
|
|
|
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
|
|
|
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apkFile)
|
|
|
|
|
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
|
|
|
|
|
}
|
|
|
|
|
|
context.startActivity(intent)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|