feat(update): 添加更新检查功能增强和配置文件读取日志

- 新增 UpdateInfo.requiresLogin 字段支持登录后更新检测
- 添加 UpdateSDK.ignoreVersion 和 clearIgnoredVersions 方法实现版本忽略功能
- 扩展 checkAppUpdate 方法支持 userIdOverride 参数用于 H5 登录后重检
- 在 ConfigFileReader 中添加日志输出便于调试配置文件读取问题
- 优化 XuqmInitializerProvider 自动初始化错误处理和日志记录
- 实现非强制更新版本忽略机制,支持下次检测时不弹窗提示
- 添加 15 秒超时等待 SDK 初始化完成的异步处理逻辑
这个提交包含在:
XuqmGroup 2026-06-04 13:14:02 +08:00
父节点 1a6a91e6fe
当前提交 d12c575dd6
共有 5 个文件被更改,包括 75 次插入9 次删除

查看文件

@ -0,0 +1,21 @@
# sdk-update 更新日志
## 1.1.0
### 新增
- `UpdateInfo.requiresLogin: Boolean`:服务端要求先登录再检测更新时为 `true`,客户端可据此静默等待登录后重检。
- `UpdateSDK.ignoreVersion(context, versionCode)`:将指定版本码标记为已忽略,存储于 `SharedPreferences`。非强制更新时,`checkAppUpdate` 检测到被忽略的版本会直接返回 `needsUpdate=false`,不弹窗。
- `UpdateSDK.clearIgnoredVersions(context)`:清除所有已忽略版本记录。
- `UpdateSDK.checkAppUpdate(context, userIdOverride)` 新增可选参数 `userIdOverride: String?`:优先级高于 `XuqmSDK.currentLoginSession?.userId`,供 H5 login 回调触发补充检测时使用。
### 兼容性
- 完全向后兼容 `1.0.x`,现有调用无需修改。
- `updateInfo.requiresLogin` 默认值为 `false`,服务端未升级时行为不变。
---
## 1.0.9
- 历史版本。

查看文件

@ -1,6 +1,7 @@
package com.xuqm.sdk.internal package com.xuqm.sdk.internal
import android.content.Context import android.content.Context
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
/** /**
@ -10,19 +11,25 @@ import com.google.gson.Gson
*/ */
internal object ConfigFileReader { internal object ConfigFileReader {
private const val TAG = "XuqmSDK"
private const val CONFIG_PATH = "xuqm/config.xuqm" private const val CONFIG_PATH = "xuqm/config.xuqm"
private const val CONFIG_DIR = "xuqm" private const val CONFIG_DIR = "xuqm"
private val gson = Gson() private val gson = Gson()
fun read(context: Context): ConfigFile? { fun read(context: Context): ConfigFile? {
return try { return try {
val path = findConfigPath(context) ?: return null val path = findConfigPath(context) ?: run {
Log.w(TAG, "No config file found in assets/$CONFIG_DIR/")
return null
}
Log.d(TAG, "Reading config file: $path")
context.assets.open(path).use { stream -> context.assets.open(path).use { stream ->
val encrypted = stream.bufferedReader().readText() val encrypted = stream.bufferedReader().readText()
val json = ConfigFileCrypto.decrypt(encrypted) val json = ConfigFileCrypto.decrypt(encrypted)
gson.fromJson(json, ConfigFile::class.java) gson.fromJson(json, ConfigFile::class.java)
} }
} catch (_: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to read/decrypt config file", e)
null null
} }
} }

查看文件

@ -4,10 +4,11 @@ import android.content.ContentProvider
import android.content.ContentValues import android.content.ContentValues
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.util.Log
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
/** /**
* Auto-initializes XuqmSDK at app startup when a license file is present. * Auto-initializes XuqmSDK at app startup when a config file is present.
* Registered in sdk-core AndroidManifest so any module (im/update/push/license) * Registered in sdk-core AndroidManifest so any module (im/update/push/license)
* triggers one-shot initialization without explicit init calls from the app. * triggers one-shot initialization without explicit init calls from the app.
*/ */
@ -18,6 +19,8 @@ class XuqmInitializerProvider : ContentProvider() {
if (!XuqmSDK.isInitialized()) { if (!XuqmSDK.isInitialized()) {
XuqmSDK.autoInitialize(ctx) XuqmSDK.autoInitialize(ctx)
} }
}.onFailure { e ->
Log.w("XuqmSDK", "Auto-initialization skipped: ${e.message}")
} }
return true return true
} }

查看文件

@ -32,7 +32,29 @@ object UpdateSDK {
}.getOrDefault(rawUrl) }.getOrDefault(rawUrl)
} }
suspend fun checkAppUpdate(context: Context): UpdateInfo? = withContext(Dispatchers.IO) { 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() awaitInitialization()
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@ -41,16 +63,27 @@ object UpdateSDK {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
packageInfo.versionCode packageInfo.versionCode
} }
val userId = XuqmSDK.currentLoginSession?.userId val userId = userIdOverride ?: XuqmSDK.currentLoginSession?.userId
runCatching { runCatching {
api.checkUpdate(XuqmSDK.appKey, "ANDROID", versionCode, userId).data?.let { api.checkUpdate(XuqmSDK.appKey, "ANDROID", versionCode, userId).data?.let { info ->
it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl) 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() }.getOrNull()
} }
private fun awaitInitialization() { private suspend fun awaitInitialization() {
XuqmSDK.requireInit() 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( suspend fun downloadAndInstall(

查看文件

@ -9,4 +9,6 @@ data class UpdateInfo(
val forceUpdate: Boolean = false, val forceUpdate: Boolean = false,
val appStoreUrl: String = "", val appStoreUrl: String = "",
val marketUrl: String = "", val marketUrl: String = "",
/** 服务端要求先登录再检测更新时为 true,此时 needsUpdate 通常为 false */
val requiresLogin: Boolean = false,
) )