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 userIdOverride 显式传入用户 ID,优先级高于 [XuqmSDK.currentLoginSession]。 * 用于登录后补充检测的场景:当服务端返回 [UpdateInfo.requiresLogin] 时,H5 侧完成登录并传入 userId, * 再调用此方法重新检测。 */ suspend fun checkAppUpdate(context: Context, userIdOverride: String? = null): 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 = userIdOverride ?: XuqmSDK.currentLoginSession?.userId runCatching { api.checkUpdate(XuqmSDK.appKey, "ANDROID", versionCode, userId).data?.let { info -> val normalized = info.copy(downloadUrl = normalizeDownloadUrl(info.downloadUrl) ?: info.downloadUrl) // 非强制更新且已被用户忽略时,压制弹窗 if (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) } }