feat(update): 添加更新检查功能增强和配置文件读取日志
- 新增 UpdateInfo.requiresLogin 字段支持登录后更新检测 - 添加 UpdateSDK.ignoreVersion 和 clearIgnoredVersions 方法实现版本忽略功能 - 扩展 checkAppUpdate 方法支持 userIdOverride 参数用于 H5 登录后重检 - 在 ConfigFileReader 中添加日志输出便于调试配置文件读取问题 - 优化 XuqmInitializerProvider 自动初始化错误处理和日志记录 - 实现非强制更新版本忽略机制,支持下次检测时不弹窗提示 - 添加 15 秒超时等待 SDK 初始化完成的异步处理逻辑
这个提交包含在:
父节点
1a6a91e6fe
当前提交
d12c575dd6
21
docs/sdk-update-CHANGELOG.md
普通文件
21
docs/sdk-update-CHANGELOG.md
普通文件
@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户