diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index 01fd8ef..c89a48a 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -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? = runCatching { + private fun readLicenseFileData(context: Context): Triple? = 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 + method.invoke(instance, context.applicationContext) as? Triple }.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? diff --git a/sdk-license/src/main/java/com/xuqm/sdk/license/LicenseSDK.kt b/sdk-license/src/main/java/com/xuqm/sdk/license/LicenseSDK.kt index 4126b39..e9799ca 100644 --- a/sdk-license/src/main/java/com/xuqm/sdk/license/LicenseSDK.kt +++ b/sdk-license/src/main/java/com/xuqm/sdk/license/LicenseSDK.kt @@ -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? { + fun readLicenseFileData(context: Context): Triple? { 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() } diff --git a/sdk-license/src/main/java/com/xuqm/sdk/license/internal/LicenseFileReader.kt b/sdk-license/src/main/java/com/xuqm/sdk/license/internal/LicenseFileReader.kt index 0a2a73e..e2a5a9d 100644 --- a/sdk-license/src/main/java/com/xuqm/sdk/license/internal/LicenseFileReader.kt +++ b/sdk-license/src/main/java/com/xuqm/sdk/license/internal/LicenseFileReader.kt @@ -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() diff --git a/sdk-license/src/main/java/com/xuqm/sdk/license/model/LicenseFile.kt b/sdk-license/src/main/java/com/xuqm/sdk/license/model/LicenseFile.kt index c4ae8e1..cc1d2ce 100644 --- a/sdk-license/src/main/java/com/xuqm/sdk/license/model/LicenseFile.kt +++ b/sdk-license/src/main/java/com/xuqm/sdk/license/model/LicenseFile.kt @@ -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, diff --git a/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt b/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt index 1f01376..2e464c6 100644 --- a/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt +++ b/sdk-update/src/main/java/com/xuqm/sdk/update/UpdateSDK.kt @@ -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,