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
|
@Volatile
|
||||||
private var loginSession: XuqmLoginSession? = null
|
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/.
|
* 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
|
* 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
|
* For private deployments the license file contains the server URL and all service
|
||||||
* endpoints are configured automatically. For public deployments the default endpoints
|
* endpoints are configured automatically. For public deployments the default endpoints
|
||||||
* (dev.xuqinmin.com) are used.
|
* (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) {
|
fun autoInitialize(context: Context, logLevel: LogLevel = LogLevel.WARN) {
|
||||||
val (appKey, serverUrl) = readLicenseFileData(context)
|
val licenseData = readLicenseFileData(context)
|
||||||
?: throw IllegalStateException(
|
?: throw IllegalStateException(
|
||||||
"No license file found in assets/xuqm/. " +
|
"No license file found in assets/xuqm/. " +
|
||||||
"Download license.xuqm from the tenant platform and place it in src/main/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)
|
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(
|
fun initialize(
|
||||||
context: Context,
|
context: Context,
|
||||||
appKey: String,
|
appKey: String,
|
||||||
@ -52,7 +76,7 @@ object XuqmSDK {
|
|||||||
logLevel: LogLevel = LogLevel.WARN,
|
logLevel: LogLevel = LogLevel.WARN,
|
||||||
) {
|
) {
|
||||||
val applicationContext = context.applicationContext
|
val applicationContext = context.applicationContext
|
||||||
synchronized(this) {
|
synchronized(initLock) {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
check(initializedAppKey == appKey) {
|
check(initializedAppKey == appKey) {
|
||||||
"XuqmSDK already initialized with appKey=$initializedAppKey"
|
"XuqmSDK already initialized with appKey=$initializedAppKey"
|
||||||
@ -66,16 +90,33 @@ object XuqmSDK {
|
|||||||
ApiClient.init(config, tokenStore)
|
ApiClient.init(config, tokenStore)
|
||||||
initializedAppKey = appKey
|
initializedAppKey = appKey
|
||||||
initialized = true
|
initialized = true
|
||||||
|
pendingInitCallbacks.forEach { runCatching(it) }
|
||||||
|
pendingInitCallbacks.clear()
|
||||||
}
|
}
|
||||||
serverUrl?.takeIf { it.isNotBlank() }?.let { configurePrivateServer(context, appKey, it) }
|
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")
|
@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 clazz = Class.forName("com.xuqm.sdk.license.LicenseSDK")
|
||||||
val instance = clazz.getField("INSTANCE").get(null)
|
val instance = clazz.getField("INSTANCE").get(null)
|
||||||
val method = clazz.getMethod("readLicenseFileData", Context::class.java)
|
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()
|
}.getOrNull()
|
||||||
|
|
||||||
private fun configurePrivateServer(context: Context, appKey: String, serverUrl: String) {
|
private fun configurePrivateServer(context: Context, appKey: String, serverUrl: String) {
|
||||||
@ -124,6 +165,8 @@ object XuqmSDK {
|
|||||||
check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." }
|
check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isInitialized(): Boolean = synchronized(initLock) { initialized }
|
||||||
|
|
||||||
val appKey: String get() = config.appKey
|
val appKey: String get() = config.appKey
|
||||||
|
|
||||||
val currentLoginSession: XuqmLoginSession?
|
val currentLoginSession: XuqmLoginSession?
|
||||||
|
|||||||
@ -73,10 +73,8 @@ object LicenseSDK {
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun checkLicense(userInfo: LicenseUserInfo? = null, callback: LicenseCallback) {
|
fun checkLicense(userInfo: LicenseUserInfo? = null, callback: LicenseCallback) {
|
||||||
sdkScope.launch {
|
sdkScope.launch {
|
||||||
val valid = when (checkLicense(userInfo)) {
|
val result = checkLicense(userInfo)
|
||||||
is LicenseResult.Success -> true
|
val valid = result is LicenseResult.Success
|
||||||
is LicenseResult.Error -> false
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
callback.onResult(valid)
|
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.
|
* 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 licenseFile = LicenseFileReader.read(context) ?: return null
|
||||||
val appKey = licenseFile.appKey.takeIf { it.isNotBlank() } ?: 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,10 +222,26 @@ object LicenseSDK {
|
|||||||
store.statusTime = System.currentTimeMillis()
|
store.statusTime = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureInitialized() {
|
private suspend fun ensureInitialized() {
|
||||||
|
if (!initialized) {
|
||||||
|
// 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) {
|
if (!initialized) {
|
||||||
tryAutoInitialize()
|
tryAutoInitialize()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
requireInit()
|
requireInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,15 @@ internal object LicenseFileReader {
|
|||||||
return findLicensePath(context) != null
|
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? {
|
private fun findLicensePath(context: Context): String? {
|
||||||
try {
|
try {
|
||||||
context.assets.open(LICENSE_PATH).close()
|
context.assets.open(LICENSE_PATH).close()
|
||||||
|
|||||||
@ -10,8 +10,10 @@ data class LicenseFile(
|
|||||||
@SerializedName(value = "appKey", alternate = ["app_key"]) val appKey: String,
|
@SerializedName(value = "appKey", alternate = ["app_key"]) val appKey: String,
|
||||||
@SerializedName(value = "appName", alternate = ["app_name"]) val appName: String? = null,
|
@SerializedName(value = "appName", alternate = ["app_name"]) val appName: String? = null,
|
||||||
@SerializedName(value = "companyName", alternate = ["company_name"]) val companyName: 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,
|
@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 = "serverUrl", alternate = ["server_url"]) val serverUrl: String? = null,
|
||||||
@SerializedName(value = "issuedAt", alternate = ["issued_at"]) val issuedAt: String? = null,
|
@SerializedName(value = "issuedAt", alternate = ["issued_at"]) val issuedAt: String? = null,
|
||||||
@SerializedName(value = "expiresAt", alternate = ["expires_at"]) val expiresAt: 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) {
|
suspend fun checkAppUpdate(context: Context): UpdateInfo? = withContext(Dispatchers.IO) {
|
||||||
XuqmSDK.requireInit()
|
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) {
|
||||||
packageInfo.longVersionCode.toInt()
|
packageInfo.longVersionCode.toInt()
|
||||||
@ -49,6 +49,22 @@ object UpdateSDK {
|
|||||||
}.getOrNull()
|
}.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(
|
suspend fun downloadAndInstall(
|
||||||
context: Context,
|
context: Context,
|
||||||
downloadUrl: String,
|
downloadUrl: String,
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户