package com.xuqm.sdk.update import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import androidx.core.content.FileProvider import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.file.FileSDK import com.xuqm.sdk.core.ServiceEndpointRegistry 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 { private val api: UpdateApi get() = ApiClient.create(UpdateApi::class.java, ServiceEndpointRegistry.updateBaseUrl) 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) } 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) /** * 检测应用更新。 * * @param bypassIgnore 是否绕过"忽略版本"过滤。 * - `false`(默认,静默检查):用户已忽略的版本不再弹窗,适合启动时后台检查。 * - `true`(主动检查):忽略记录不生效,始终弹出更新对话框;无更新时由调用方显示提示。 * * userId 通过 [XuqmSDK.login] 设置会话后自动传递,无需外部覆盖。 */ suspend fun checkAppUpdate(context: Context, bypassIgnore: Boolean = false): UpdateInfo? = withContext(Dispatchers.IO) { awaitInitialization() 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 } val userId = XuqmSDK.currentLoginSession?.userId runCatching { api.checkUpdate(XuqmSDK.appKey, "ANDROID", versionCode, userId).data?.let { info -> val normalized = info.copy(downloadUrl = normalizeDownloadUrl(info.downloadUrl) ?: info.downloadUrl) // 静默检查时跳过已忽略版本;主动检查(bypassIgnore=true)始终展示 if (!bypassIgnore && normalized.needsUpdate && !normalized.forceUpdate && isVersionIgnored(context, normalized.versionCode) ) { normalized.copy(needsUpdate = false) } else { normalized } } }.getOrNull() } 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.") } suspend fun downloadAndInstall( context: Context, downloadUrl: String, onProgress: (Int) -> Unit = {}, ) = withContext(Dispatchers.IO) { val apkFile = File(context.getExternalFilesDir(null), "update.apk") FileSDK.downloadToFile(downloadUrl, apkFile, onProgress) withContext(Dispatchers.Main) { installApk(context, apkFile) } } 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) } }