sdk: auto-init from license file, cross-module init waiting
- XuqmSDK: autoInitialize() validates license package name; afterInit() callback - LicenseSDK: await XuqmSDK init via reflection with timeout before operations - UpdateSDK: await XuqmSDK init before checkAppUpdate - LicenseFileReader: return Triple(appKey, serverUrl, packageName) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
这个提交包含在:
父节点
7b18d279bf
当前提交
63da94c670
@ -27,6 +27,9 @@ object XuqmSDK {
|
||||
@Volatile
|
||||
private var loginSession: XuqmLoginSession? = null
|
||||
|
||||
private val initLock = Any()
|
||||
private val pendingInitCallbacks = mutableListOf<() -> Unit>()
|
||||
|
||||
/**
|
||||
* Initializes the SDK automatically from the license file embedded in assets/xuqm/.
|
||||
* Place the license.xuqm file downloaded from the tenant platform into your app's
|
||||
@ -35,16 +38,37 @@ object XuqmSDK {
|
||||
* For private deployments the license file contains the server URL and all service
|
||||
* endpoints are configured automatically. For public deployments the default endpoints
|
||||
* (dev.xuqinmin.com) are used.
|
||||
*
|
||||
* The license file's packageName/bundleId is validated against the local app package name.
|
||||
* If they don't match, an exception is thrown.
|
||||
*/
|
||||
fun autoInitialize(context: Context, logLevel: LogLevel = LogLevel.WARN) {
|
||||
val (appKey, serverUrl) = readLicenseFileData(context)
|
||||
val licenseData = readLicenseFileData(context)
|
||||
?: throw IllegalStateException(
|
||||
"No license file found in assets/xuqm/. " +
|
||||
"Download license.xuqm from the tenant platform and place it in src/main/assets/xuqm/."
|
||||
)
|
||||
val appKey = licenseData.first
|
||||
val serverUrl = licenseData.second
|
||||
val licensePackageName = licenseData.third
|
||||
|
||||
val localPackageName = context.packageName
|
||||
if (licensePackageName != null && licensePackageName.isNotBlank() && licensePackageName != localPackageName) {
|
||||
throw IllegalStateException(
|
||||
"License package name mismatch: license=$licensePackageName, local=$localPackageName. " +
|
||||
"Please download the correct license file for this app."
|
||||
)
|
||||
}
|
||||
|
||||
initialize(context, appKey, serverUrl, logLevel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual initialization without license file.
|
||||
* The SDK will validate the appKey against the server by calling /api/sdk/config
|
||||
* with the local package name. If the package name does not match the registered one,
|
||||
* an exception is thrown.
|
||||
*/
|
||||
fun initialize(
|
||||
context: Context,
|
||||
appKey: String,
|
||||
@ -52,7 +76,7 @@ object XuqmSDK {
|
||||
logLevel: LogLevel = LogLevel.WARN,
|
||||
) {
|
||||
val applicationContext = context.applicationContext
|
||||
synchronized(this) {
|
||||
synchronized(initLock) {
|
||||
if (initialized) {
|
||||
check(initializedAppKey == appKey) {
|
||||
"XuqmSDK already initialized with appKey=$initializedAppKey"
|
||||
@ -66,16 +90,33 @@ object XuqmSDK {
|
||||
ApiClient.init(config, tokenStore)
|
||||
initializedAppKey = appKey
|
||||
initialized = true
|
||||
pendingInitCallbacks.forEach { runCatching(it) }
|
||||
pendingInitCallbacks.clear()
|
||||
}
|
||||
serverUrl?.takeIf { it.isNotBlank() }?.let { configurePrivateServer(context, appKey, it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given block after initialization is complete.
|
||||
* If already initialized, executes immediately.
|
||||
* If not initialized, waits until initialization completes.
|
||||
*/
|
||||
fun afterInit(block: () -> Unit) {
|
||||
synchronized(initLock) {
|
||||
if (initialized) {
|
||||
block()
|
||||
} else {
|
||||
pendingInitCallbacks.add(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun readLicenseFileData(context: Context): Pair<String, String?>? = runCatching {
|
||||
private fun readLicenseFileData(context: Context): Triple<String, String?, String?>? = runCatching {
|
||||
val clazz = Class.forName("com.xuqm.sdk.license.LicenseSDK")
|
||||
val instance = clazz.getField("INSTANCE").get(null)
|
||||
val method = clazz.getMethod("readLicenseFileData", Context::class.java)
|
||||
method.invoke(instance, context.applicationContext) as? Pair<String, String?>
|
||||
method.invoke(instance, context.applicationContext) as? Triple<String, String?, String?>
|
||||
}.getOrNull()
|
||||
|
||||
private fun configurePrivateServer(context: Context, appKey: String, serverUrl: String) {
|
||||
@ -124,6 +165,8 @@ object XuqmSDK {
|
||||
check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." }
|
||||
}
|
||||
|
||||
fun isInitialized(): Boolean = synchronized(initLock) { initialized }
|
||||
|
||||
val appKey: String get() = config.appKey
|
||||
|
||||
val currentLoginSession: XuqmLoginSession?
|
||||
|
||||
@ -73,10 +73,8 @@ object LicenseSDK {
|
||||
@JvmOverloads
|
||||
fun checkLicense(userInfo: LicenseUserInfo? = null, callback: LicenseCallback) {
|
||||
sdkScope.launch {
|
||||
val valid = when (checkLicense(userInfo)) {
|
||||
is LicenseResult.Success -> true
|
||||
is LicenseResult.Error -> false
|
||||
}
|
||||
val result = checkLicense(userInfo)
|
||||
val valid = result is LicenseResult.Success
|
||||
withContext(Dispatchers.Main) {
|
||||
callback.onResult(valid)
|
||||
}
|
||||
@ -190,13 +188,13 @@ object LicenseSDK {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns (appKey, serverUrl) from the embedded license file, or null if no file is present.
|
||||
* Returns (appKey, serverUrl, packageName) from the embedded license file, or null if no file is present.
|
||||
* Called by XuqmSDK.autoInitialize() via reflection — sdk-core cannot depend on sdk-license directly.
|
||||
*/
|
||||
fun readLicenseFileData(context: Context): Pair<String, String?>? {
|
||||
fun readLicenseFileData(context: Context): Triple<String, String?, String?>? {
|
||||
val licenseFile = LicenseFileReader.read(context) ?: return null
|
||||
val appKey = licenseFile.appKey.takeIf { it.isNotBlank() } ?: return null
|
||||
return appKey to licenseFile.serverUrl?.takeIf { it.isNotBlank() }
|
||||
return Triple(appKey, licenseFile.serverUrl?.takeIf { it.isNotBlank() }, licenseFile.packageName)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -224,9 +222,25 @@ object LicenseSDK {
|
||||
store.statusTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun ensureInitialized() {
|
||||
private suspend fun ensureInitialized() {
|
||||
if (!initialized) {
|
||||
tryAutoInitialize()
|
||||
// Wait for XuqmSDK initialization via reflection (poll-based)
|
||||
val xuqmClazz = runCatching { Class.forName("com.xuqm.sdk.XuqmSDK") }.getOrNull()
|
||||
if (xuqmClazz != null) {
|
||||
val isInitMethod = xuqmClazz.getMethod("isInitialized")
|
||||
val maxWaitMs = 30000L
|
||||
val start = System.currentTimeMillis()
|
||||
while (!(isInitMethod.invoke(null) as Boolean)) {
|
||||
if (System.currentTimeMillis() - start > maxWaitMs) {
|
||||
throw IllegalStateException("LicenseSDK initialization timed out waiting for XuqmSDK")
|
||||
}
|
||||
kotlinx.coroutines.delay(100)
|
||||
}
|
||||
}
|
||||
// After XuqmSDK is initialized, try to auto-initialize LicenseSDK
|
||||
if (!initialized) {
|
||||
tryAutoInitialize()
|
||||
}
|
||||
}
|
||||
requireInit()
|
||||
}
|
||||
|
||||
@ -29,6 +29,15 @@ internal object LicenseFileReader {
|
||||
return findLicensePath(context) != null
|
||||
}
|
||||
|
||||
fun readRaw(context: Context): String? {
|
||||
return try {
|
||||
val path = findLicensePath(context) ?: return null
|
||||
context.assets.open(path).use { it.bufferedReader().readText().trim() }
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLicensePath(context: Context): String? {
|
||||
try {
|
||||
context.assets.open(LICENSE_PATH).close()
|
||||
|
||||
@ -10,8 +10,10 @@ data class LicenseFile(
|
||||
@SerializedName(value = "appKey", alternate = ["app_key"]) val appKey: String,
|
||||
@SerializedName(value = "appName", alternate = ["app_name"]) val appName: String? = null,
|
||||
@SerializedName(value = "companyName", alternate = ["company_name"]) val companyName: String? = null,
|
||||
@SerializedName(value = "packageName", alternate = ["package_name"]) val packageName: String? = null,
|
||||
@SerializedName(value = "iosBundleId", alternate = ["ios_bundle_id"]) val iosBundleId: String? = null,
|
||||
@SerializedName(value = "harmonyBundleName", alternate = ["harmony_bundle_name"]) val harmonyBundleName: String? = null,
|
||||
@SerializedName(value = "baseUrl", alternate = ["base_url"]) val baseUrl: String? = null,
|
||||
// Set only for private deployments; used by XuqmSDK.autoInitialize() to configure all service endpoints.
|
||||
@SerializedName(value = "serverUrl", alternate = ["server_url"]) val serverUrl: String? = null,
|
||||
@SerializedName(value = "issuedAt", alternate = ["issued_at"]) val issuedAt: String? = null,
|
||||
@SerializedName(value = "expiresAt", alternate = ["expires_at"]) val expiresAt: String? = null,
|
||||
|
||||
@ -33,7 +33,7 @@ object UpdateSDK {
|
||||
}
|
||||
|
||||
suspend fun checkAppUpdate(context: Context): UpdateInfo? = withContext(Dispatchers.IO) {
|
||||
XuqmSDK.requireInit()
|
||||
awaitInitialization()
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
packageInfo.longVersionCode.toInt()
|
||||
@ -49,6 +49,22 @@ object UpdateSDK {
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private suspend fun awaitInitialization() {
|
||||
val xuqmClazz = runCatching { Class.forName("com.xuqm.sdk.XuqmSDK") }.getOrNull()
|
||||
if (xuqmClazz != null) {
|
||||
val isInitMethod = xuqmClazz.getMethod("isInitialized")
|
||||
val maxWaitMs = 30000L
|
||||
val start = System.currentTimeMillis()
|
||||
while (!(isInitMethod.invoke(null) as Boolean)) {
|
||||
if (System.currentTimeMillis() - start > maxWaitMs) {
|
||||
throw IllegalStateException("UpdateSDK initialization timed out waiting for XuqmSDK")
|
||||
}
|
||||
kotlinx.coroutines.delay(100)
|
||||
}
|
||||
}
|
||||
XuqmSDK.requireInit()
|
||||
}
|
||||
|
||||
suspend fun downloadAndInstall(
|
||||
context: Context,
|
||||
downloadUrl: String,
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户