package com.xuqm.sdk.push import android.content.Context import android.os.Build import android.util.Log import com.google.firebase.messaging.FirebaseMessaging 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) @Volatile private var cachedVendorConfig: PushVendorConfig? = null fun currentRegistration(context: Context): PushRegistrationSnapshot? { XuqmSDK.requireInit() val deviceId = DeviceUtils.getDeviceId(context) return store(context).load(deviceId = deviceId, fallbackVendor = detectVendor()) } fun updateNativePushToken( context: Context, vendor: PushVendor, pushToken: String, ) { XuqmSDK.requireInit() 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() } if (pushToken.isNullOrBlank()) { Log.w("XuqmPushSDK", "Native push token not ready yet, waiting for onNewToken()") ensureNativePushToken(context) return } scope.launch { runCatching { api.registerDevice( appId = XuqmSDK.appKey, userId = userId, vendor = vendor.name, token = pushToken, deviceId = DeviceUtils.getDeviceId(context), brand = Build.MANUFACTURER.orEmpty(), model = Build.MODEL.orEmpty(), osVersion = DeviceUtils.getOsVersion(), appVersion = appVersion(context), ) registeredUserId.set(userId) store(context).updateLastUserId(userId) Log.i( "XuqmPushSDK", "Registered push device for userId=$userId vendor=${vendor.name}", ) } } } 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 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) { runCatching { FirebaseMessaging.getInstance() } .onSuccess { messaging -> messaging.token .addOnSuccessListener { token -> if (token.isNotBlank()) { store(context).save(PushVendor.FCM, token) val sessionUserId = XuqmSDK.currentLoginSession?.userId if (sessionUserId != null) { registerDevice(context, sessionUserId) } } } .addOnFailureListener { error -> Log.w("XuqmPushSDK", "Unable to fetch FCM token: ${error.message}") } } .onFailure { error -> Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}") } } 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, falling back to FCM", ) runCatching { FirebaseMessaging.getInstance() } .onSuccess { messaging -> messaging.token .addOnSuccessListener { token -> if (token.isNotBlank()) { store(context).save(PushVendor.FCM, token) val sessionUserId = XuqmSDK.currentLoginSession?.userId if (sessionUserId != null) { registerDevice(context, sessionUserId) } } } .addOnFailureListener { error -> Log.w("XuqmPushSDK", "Unable to fetch FCM token: ${error.message}") } } .onFailure { error -> Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}") } } } } 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 } }