package com.xuqm.sdk.push import android.content.Context import android.os.Build import android.util.Log import com.xuqm.sdk.XuqmLoginSession import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.push.api.PushApi import com.xuqm.sdk.push.api.PushConfigApi import com.xuqm.sdk.push.model.PushRegistrationSnapshot import com.xuqm.sdk.push.model.PushVendor import com.xuqm.sdk.push.model.PushVendorConfig import com.xuqm.sdk.push.storage.PushRegistrationStore import com.xuqm.sdk.push.vendor.FcmPushService import com.xuqm.sdk.push.vendor.HonorPushService import com.xuqm.sdk.push.vendor.HuaweiPushService import com.xuqm.sdk.push.vendor.OppoPushService import com.xuqm.sdk.push.vendor.PushVendorInterface import com.xuqm.sdk.push.vendor.VivoPushService import com.xuqm.sdk.push.vendor.XiaomiPushService import com.xuqm.sdk.utils.DeviceUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicReference object PushSDK { private val api: PushApi get() = ApiClient.create(PushApi::class.java, ServiceEndpointRegistry.pushBaseUrl) private val configApi: PushConfigApi get() = ApiClient.create(PushConfigApi::class.java, ServiceEndpointRegistry.controlBaseUrl) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val registeredUserId = AtomicReference(null) private val registeringDeviceKey = AtomicReference(null) private val lastRegisteredDeviceKey = AtomicReference(null) @Volatile private var cachedVendorConfig: PushVendorConfig? = null fun currentRegistration(context: Context): PushRegistrationSnapshot? { XuqmSDK.requireInit() val deviceId = DeviceUtils.getDeviceId(context) val detectedVendor = detectVendor() val registration = store(context).load(deviceId = deviceId, fallbackVendor = detectedVendor) ?: return null if (registration.vendor != detectedVendor) { Log.i( "XuqmPushSDK", "Clearing cached ${registration.vendor.name} push token on ${detectedVendor.name} device", ) store(context).clearToken() return null } return registration } fun updateNativePushToken( context: Context, vendor: PushVendor, pushToken: String, ) { XuqmSDK.requireInit() val detectedVendor = detectVendor() if (vendor != detectedVendor) { Log.i( "XuqmPushSDK", "Ignoring ${vendor.name} push token on ${detectedVendor.name} device", ) return } val normalizedToken = pushToken.trim() require(normalizedToken.isNotBlank()) { "pushToken must not be blank" } store(context).save(vendor, normalizedToken) val sessionUserId = XuqmSDK.currentLoginSession?.userId if (sessionUserId != null) { bindImUser(context, sessionUserId) } } fun bindImUser(context: Context, userId: String) { if (!isReceivePushEnabled(context)) return ensureNativePushToken(context) registerDevice(context, userId) } fun refreshNativePushToken( context: Context, vendor: PushVendor = detectVendor(), ) { XuqmSDK.requireInit() ensureNativePushToken(context.applicationContext, vendor) } fun unbindImUser(userId: String) { unregisterDevice(userId) } fun setReceivePush( context: Context, userId: String? = XuqmSDK.currentLoginSession?.userId, enabled: Boolean, ) { XuqmSDK.requireInit() store(context).setReceivePush(enabled) val resolvedUserId = userId ?: registeredUserId.get() if (resolvedUserId != null) { scope.launch { runCatching { api.setReceivePush( appId = XuqmSDK.appKey, userId = resolvedUserId, deviceId = DeviceUtils.getDeviceId(context), enabled = enabled, ) if (enabled) { bindImUser(context, resolvedUserId) } } } } } fun registerDevice( context: Context, userId: String, ) { XuqmSDK.requireInit() val registration = currentRegistration(context) val vendor = registration?.vendor ?: detectVendor() val pushToken = registration?.pushToken?.takeIf { it.isNotBlank() } val deviceId = DeviceUtils.getDeviceId(context) if (pushToken.isNullOrBlank()) { Log.w("XuqmPushSDK", "Native push token not ready yet, waiting for onNewToken()") ensureNativePushToken(context) return } Log.e(">>>>>>>>>>>>>>>>", pushToken) val registrationKey = listOf(userId, vendor.name, pushToken, deviceId).joinToString("|") if (lastRegisteredDeviceKey.get() == registrationKey) { Log.d("XuqmPushSDK", "Skipping duplicate push device registration for userId=$userId vendor=${vendor.name}") return } if (!registeringDeviceKey.compareAndSet(null, registrationKey)) { if (registeringDeviceKey.get() == registrationKey) { Log.d("XuqmPushSDK", "Push device registration already in progress for userId=$userId vendor=${vendor.name}") return } } scope.launch { runCatching { api.registerDevice( appId = XuqmSDK.appKey, userId = userId, vendor = vendor.name, token = pushToken, deviceId = deviceId, brand = Build.MANUFACTURER.orEmpty(), model = Build.MODEL.orEmpty(), osVersion = DeviceUtils.getOsVersion(), appVersion = appVersion(context), ) registeredUserId.set(userId) lastRegisteredDeviceKey.set(registrationKey) store(context).updateLastUserId(userId) Log.i( "XuqmPushSDK", "Registered push device for userId=$userId vendor=${vendor.name}", ) }.also { registeringDeviceKey.compareAndSet(registrationKey, null) } } } fun unregisterDevice(userId: String) { XuqmSDK.requireInit() scope.launch { runCatching { val reg = store(XuqmSDK.appContext).load( deviceId = DeviceUtils.getDeviceId(XuqmSDK.appContext), fallbackVendor = detectVendor(), ) api.unregisterDevice( XuqmSDK.appKey, userId, reg?.vendor?.name ?: detectVendor().name, DeviceUtils.getDeviceId(XuqmSDK.appContext), ) registeredUserId.compareAndSet(userId, null) store(XuqmSDK.appContext).updateLastUserId(null) } } } private val vendorServices: List = listOf( HuaweiPushService(), XiaomiPushService(), OppoPushService(), VivoPushService(), HonorPushService(), FcmPushService(), ) fun detectVendor(): PushVendor { return when (Build.MANUFACTURER.uppercase()) { "HUAWEI" -> PushVendor.HUAWEI "XIAOMI" -> PushVendor.XIAOMI "REDMI", "POCO" -> PushVendor.XIAOMI "OPPO" -> PushVendor.OPPO "REALME", "ONEPLUS" -> PushVendor.OPPO "VIVO" -> PushVendor.VIVO "IQOO" -> PushVendor.VIVO "HONOR" -> PushVendor.HONOR else -> PushVendor.FCM } } fun initializeVendors(context: Context) { val detectedVendor = detectVendor() Log.i("XuqmPushSDK", "Detected push vendor: ${detectedVendor.name}") scope.launch { val config = loadVendorConfig() vendorServices.forEach { service -> if (service.vendor == detectedVendor && service.isAvailable(context)) { Log.i("XuqmPushSDK", "Initializing push vendor: ${service.vendor.name}") service.register(context, config) } } if (detectedVendor == PushVendor.FCM) { ensureNativePushToken(context) } } } fun onSdkLogin(session: XuqmLoginSession) { val context = runCatching { XuqmSDK.appContext }.getOrNull() ?: return if (registeredUserId.get() == session.userId) return initializeVendors(context) bindImUser(context, session.userId) } fun onSdkLogout() { val userId = registeredUserId.getAndSet(null) ?: return lastRegisteredDeviceKey.set(null) registeringDeviceKey.set(null) unregisterDevice(userId) } private fun store(context: Context): PushRegistrationStore = PushRegistrationStore(context.applicationContext) private fun isReceivePushEnabled(context: Context): Boolean = store(context).load( deviceId = DeviceUtils.getDeviceId(context), fallbackVendor = detectVendor(), )?.receivePush ?: true private fun ensureNativePushToken(context: Context) { ensureNativePushToken(context, detectVendor()) } private fun ensureNativePushToken( context: Context, vendor: PushVendor, ) { val registration = currentRegistration(context) if (registration?.pushToken?.isNotBlank() == true) return if (vendor == PushVendor.FCM) { val service = vendorServices.firstOrNull { it.vendor == PushVendor.FCM } if (service != null && service.isAvailable(context)) { scope.launch { service.register(context, loadVendorConfig()) } } else { Log.w("XuqmPushSDK", "FCM service not available, skipping native push registration") } } else { val service = vendorServices.firstOrNull { it.vendor == vendor } if (service != null && service.isAvailable(context)) { scope.launch { service.register(context, loadVendorConfig()) } } else { Log.w( "XuqmPushSDK", "Vendor ${vendor.name} service not available, skipping native push registration", ) } } } private fun appVersion(context: Context): String? = runCatching { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { "${packageInfo.versionName ?: ""}(${packageInfo.longVersionCode})" } else { @Suppress("DEPRECATION") "${packageInfo.versionName ?: ""}(${packageInfo.versionCode})" } }.getOrNull() private suspend fun loadVendorConfig(): PushVendorConfig { cachedVendorConfig?.let { return it } val loaded = runCatching { val pushConfig = configApi.sdkConfig(XuqmSDK.appKey).data?.pushConfig PushVendorConfig( huaweiAppId = pushConfig?.getAsJsonObject("huawei")?.get("appId")?.asString.orEmpty(), xiaomiAppId = pushConfig?.getAsJsonObject("xiaomi")?.get("appId")?.asString.orEmpty(), xiaomiAppKey = pushConfig?.getAsJsonObject("xiaomi")?.get("appKey")?.asString.orEmpty(), oppoAppKey = pushConfig?.getAsJsonObject("oppo")?.get("appKey")?.asString.orEmpty(), oppoAppSecret = pushConfig?.getAsJsonObject("oppo")?.get("masterSecret")?.asString.orEmpty(), vivoAppId = pushConfig?.getAsJsonObject("vivo")?.get("appId")?.asString.orEmpty(), vivoAppKey = pushConfig?.getAsJsonObject("vivo")?.get("appKey")?.asString.orEmpty(), honorAppId = pushConfig?.getAsJsonObject("honor")?.get("appId")?.asString.orEmpty(), ) }.getOrElse { error -> Log.w("XuqmPushSDK", "Unable to load tenant push config: ${error.message}") PushVendorConfig() } cachedVendorConfig = loaded return loaded } }