XuqmGroup-AndroidSDK/sdk-license/src/main/java/com/xuqm/sdk/license/LicenseSDK.kt

249 行
8.5 KiB
Kotlin

2026-05-15 21:00:24 +08:00
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
2026-05-15 21:29:58 +08:00
import com.xuqm.sdk.license.model.LicenseUserInfo
2026-05-15 21:00:24 +08:00
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"
2026-05-15 21:09:32 +08:00
private const val DEFAULT_BASE_URL = "https://auth.dev.xuqinmin.com/"
2026-05-15 21:00:24 +08:00
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
2026-05-15 21:09:32 +08:00
* @param baseUrl Optional custom license server base URL. Defaults to https://auth.dev.xuqinmin.com/
2026-05-15 21:00:24 +08:00
*/
@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
2026-05-15 21:29:58 +08:00
@JvmOverloads
fun checkLicense(userInfo: LicenseUserInfo? = null, callback: LicenseCallback) {
2026-05-15 21:00:24 +08:00
sdkScope.launch {
2026-05-15 21:29:58 +08:00
val valid = when (checkLicense(userInfo)) {
2026-05-15 21:00:24 +08:00
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
*/
2026-05-15 21:29:58 +08:00
@JvmStatic
suspend fun checkLicense(userInfo: LicenseUserInfo? = null): LicenseResult = withContext(Dispatchers.IO) {
2026-05-15 21:00:24 +08:00
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,
2026-05-15 21:29:58 +08:00
userInfo = userInfo,
2026-05-15 21:00:24 +08:00
)
)
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(),
2026-05-15 21:29:58 +08:00
userInfo = userInfo,
2026-05-15 21:00:24 +08:00
)
)
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).
*/
2026-05-15 21:29:58 +08:00
suspend fun reRegister(userInfo: LicenseUserInfo? = null): LicenseResult = withContext(Dispatchers.IO) {
2026-05-15 21:00:24 +08:00
requireInit()
store.token = null
store.status = null
store.statusTime = 0
2026-05-15 21:29:58 +08:00
checkLicense(userInfo)
2026-05-15 21:00:24 +08:00
}
/**
* 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/"
}
}