XuqmGroup-AndroidSDK/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt
XuqmGroup 9114672518 feat(push): 添加推送服务功能支持
- 实现了 ConversationViewModel 来管理对话列表的刷新和状态
- 集成了 FCM 推送服务支持并实现了自动令牌获取机制
- 构建了完整的 PushSDK 推送系统,支持华为、小米、OPPO、VIVO、荣耀等厂商推送
- 添加了推送配置管理和设备注册/注销功能
- 实现了跨平台推送令牌管理和服务绑定逻辑
- 扩展了服务器端功能服务管理器以支持推送服务激活请求流程
2026-05-05 22:02:46 +08:00

327 行
12 KiB
Kotlin

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<String?>(null)
private val registeringDeviceKey = AtomicReference<String?>(null)
private val lastRegisteredDeviceKey = AtomicReference<String?>(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<PushVendorInterface> = 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
}
}