- 实现了 ConversationViewModel 来管理对话列表的刷新和状态 - 集成了 FCM 推送服务支持并实现了自动令牌获取机制 - 构建了完整的 PushSDK 推送系统,支持华为、小米、OPPO、VIVO、荣耀等厂商推送 - 添加了推送配置管理和设备注册/注销功能 - 实现了跨平台推送令牌管理和服务绑定逻辑 - 扩展了服务器端功能服务管理器以支持推送服务激活请求流程
327 行
12 KiB
Kotlin
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
|
|
}
|
|
}
|