package com.xuqm.sdk.license import android.content.Context import com.xuqm.sdk.license.api.LicenseApiService import com.xuqm.sdk.license.internal.DeviceInfoProvider import com.xuqm.sdk.license.internal.LicenseContextHolder import com.xuqm.sdk.license.internal.LicenseFileReader import com.xuqm.sdk.license.internal.LicenseHttpClient import com.xuqm.sdk.license.model.RegisterRequest import com.xuqm.sdk.license.model.LicenseUserInfo import com.xuqm.sdk.license.model.VerifyRequest import com.xuqm.sdk.license.store.LicenseStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext object LicenseSDK { private const val STATUS_OK = "ok" private const val STATUS_DENIED = "denied" private const val DEFAULT_BASE_URL = "https://auth.dev.xuqinmin.com/" private var initialized = false private lateinit var appContext: Context private lateinit var config: LicenseConfig private lateinit var store: LicenseStore private lateinit var apiService: LicenseApiService private val sdkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) /** * Optional manual initialization. Normal apps only need to place the encrypted * license file under assets/xuqm/license.xuqm and call checkLicense(). * * @param context Application context * @param appKey The company appKey (e.g., "f713d051-0fbe-4f2d-bec9-bf7b96fc9ce4") * @param deviceName Optional device name for identification * @param baseUrl Optional custom license server base URL. Defaults to https://auth.dev.xuqinmin.com/ */ @JvmStatic @JvmOverloads fun initialize( context: Context, appKey: String, deviceName: String? = null, baseUrl: String? = null, ) { synchronized(this) { val ctx = context.applicationContext appContext = ctx store = LicenseStore(ctx) // If appKey changed, clear old data to force re-registration if (store.appKey != null && store.appKey != appKey) { store.clear() } store.appKey = appKey config = LicenseConfig( appKey = appKey, baseUrl = normalizeBaseUrl(baseUrl ?: DEFAULT_BASE_URL), deviceName = deviceName ?: DeviceInfoProvider.getDeviceName(), ) apiService = LicenseHttpClient.create(LicenseApiService::class.java, config.baseUrl) initialized = true } } @JvmStatic @JvmOverloads fun checkLicense(userInfo: LicenseUserInfo? = null, callback: LicenseCallback) { sdkScope.launch { val valid = when (checkLicense(userInfo)) { is LicenseResult.Success -> true is LicenseResult.Error -> false } withContext(Dispatchers.Main) { callback.onResult(valid) } } } /** * Check license status. This will: * 1. Return cached OK status if within cache window * 2. Try to verify existing token * 3. Register new device if no token exists * * @return LicenseResult.Success if license is valid, LicenseResult.Error otherwise */ @JvmStatic suspend fun checkLicense(userInfo: LicenseUserInfo? = null): LicenseResult = withContext(Dispatchers.IO) { ensureInitialized() // Check cached status val cachedStatus = store.status val cachedTime = store.statusTime if (cachedStatus == STATUS_OK && System.currentTimeMillis() - cachedTime < config.cacheWindowMs) { return@withContext LicenseResult.Success("Cached") } val deviceId = getOrCreateDeviceId() val token = store.token try { if (token != null) { // Try to verify existing token val result = apiService.verify( VerifyRequest( companyId = config.appKey, deviceId = deviceId, token = token, userInfo = userInfo, ) ) if (result.data?.valid == true) { persistStatus(STATUS_OK) return@withContext LicenseResult.Success("Verified") } // Token invalid, clear and will try register below store.token = null } // No token or token invalid, register new device val result = apiService.register( RegisterRequest( companyId = config.appKey, deviceId = deviceId, deviceName = config.deviceName, deviceModel = DeviceInfoProvider.getDeviceModel(), deviceVendor = DeviceInfoProvider.getDeviceVendor(), osVersion = DeviceInfoProvider.getOsVersion(), userInfo = userInfo, ) ) if (result.data?.success == true && result.data.token != null) { store.token = result.data.token persistStatus(STATUS_OK) return@withContext LicenseResult.Success("Registered") } persistStatus(STATUS_DENIED) return@withContext LicenseResult.Error(result.data?.message ?: "Registration denied") } catch (e: Exception) { // Network error: use cached status if available if (cachedStatus == STATUS_OK) { return@withContext LicenseResult.Success("Offline - using cached status") } return@withContext LicenseResult.Error(e.message ?: "Network error") } } /** * Force re-register the device (e.g., after token revocation). */ suspend fun reRegister(userInfo: LicenseUserInfo? = null): LicenseResult = withContext(Dispatchers.IO) { requireInit() store.token = null store.status = null store.statusTime = 0 checkLicense(userInfo) } /** * Get current license status without network call. */ fun getStatus(): LicenseStatus { if (!initialized && !tryAutoInitialize()) return LicenseStatus.UNKNOWN return when (store.status) { STATUS_OK -> LicenseStatus.OK STATUS_DENIED -> LicenseStatus.DENIED else -> LicenseStatus.UNKNOWN } } /** * Get the stable device ID used for registration. */ fun getDeviceId(): String? { if (!initialized && !tryAutoInitialize()) return null return store.deviceId } /** * Clear all license data (token, device ID, status). */ fun clear() { if (::store.isInitialized) { store.clear() } } private fun requireInit() { check(initialized) { "LicenseSDK not initialized. Call LicenseSDK.initialize() first." } } private fun getOrCreateDeviceId(): String { store.deviceId?.let { return it } val newId = DeviceInfoProvider.getDeviceId(appContext) store.deviceId = newId return newId } private fun persistStatus(status: String) { store.status = status store.statusTime = System.currentTimeMillis() } private fun ensureInitialized() { if (!initialized) { tryAutoInitialize() } requireInit() } private fun tryAutoInitialize(): Boolean { synchronized(this) { if (initialized) return true val ctx = LicenseContextHolder.appContext ?: return false val licenseFile = LicenseFileReader.read(ctx) ?: return false val appKey = licenseFile.appKey.takeIf { it.isNotBlank() } ?: return false appContext = ctx store = LicenseStore(ctx) if (store.appKey != null && store.appKey != appKey) { store.clear() } store.appKey = appKey config = LicenseConfig( appKey = appKey, baseUrl = normalizeBaseUrl(licenseFile.baseUrl ?: DEFAULT_BASE_URL), deviceName = DeviceInfoProvider.getDeviceName(), ) apiService = LicenseHttpClient.create(LicenseApiService::class.java, config.baseUrl) initialized = true return true } } private fun normalizeBaseUrl(value: String): String { val trimmed = value.trim().ifBlank { DEFAULT_BASE_URL } return if (trimmed.endsWith("/")) trimmed else "$trimmed/" } }