feat(push): 添加推送服务功能支持

- 实现了 ConversationViewModel 来管理对话列表的刷新和状态
- 集成了 FCM 推送服务支持并实现了自动令牌获取机制
- 构建了完整的 PushSDK 推送系统,支持华为、小米、OPPO、VIVO、荣耀等厂商推送
- 添加了推送配置管理和设备注册/注销功能
- 实现了跨平台推送令牌管理和服务绑定逻辑
- 扩展了服务器端功能服务管理器以支持推送服务激活请求流程
这个提交包含在:
XuqmGroup 2026-05-05 22:02:46 +08:00
父节点 e87b1b0af2
当前提交 9114672518
共有 5 个文件被更改,包括 78 次插入46 次删除

查看文件

@ -10,6 +10,7 @@ import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.ImGroup
import com.xuqm.sdk.sample.di.AppDependencies
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@ -21,6 +22,7 @@ class ConversationViewModel : ViewModel() {
}
private val cache = AppDependencies.localImCache
private var refreshJob: Job? = null
private val _conversations = MutableStateFlow<List<ConversationData>>(emptyList())
val conversations: StateFlow<List<ConversationData>> = _conversations
@ -88,7 +90,11 @@ class ConversationViewModel : ViewModel() {
fun refresh() {
Log.d(TAG, "refresh() called")
viewModelScope.launch {
if (refreshJob?.isActive == true) {
Log.d(TAG, "refresh skipped: already running")
return
}
refreshJob = viewModelScope.launch {
_isRefreshing.value = true
try {
val conversations = runCatching { ImSDK.listConversations() }.getOrDefault(emptyList())

查看文件

@ -29,8 +29,8 @@ android {
dependencies {
api(project(":sdk-core"))
api(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)
compileOnly(platform(libs.firebase.bom))
compileOnly(libs.firebase.messaging)
api("com.huawei.hms:push:6.12.0.300")
api("com.hihonor.mcs:push:7.0.41.301")
api("io.github.hebeiliang.mipush:Push:2.0.0")

查看文件

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@ -31,7 +33,12 @@
<application>
<meta-data
android:name="push_kit_auto_init_enabled"
android:value="true" />
android:value="false" />
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
tools:node="remove" />
<meta-data
android:name="com.vivo.push.app_id"
android:value="${XUQM_VIVO_APP_ID}" />

查看文件

@ -3,7 +3,6 @@ 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
@ -34,13 +33,26 @@ object PushSDK {
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)
return store(context).load(deviceId = deviceId, fallbackVendor = detectVendor())
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(
@ -49,6 +61,14 @@ object PushSDK {
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)
@ -109,11 +129,24 @@ object PushSDK {
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(
@ -121,18 +154,21 @@ object PushSDK {
userId = userId,
vendor = vendor.name,
token = pushToken,
deviceId = DeviceUtils.getDeviceId(context),
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)
}
}
}
@ -206,6 +242,8 @@ object PushSDK {
fun onSdkLogout() {
val userId = registeredUserId.getAndSet(null) ?: return
lastRegisteredDeviceKey.set(null)
registeringDeviceKey.set(null)
unregisterDevice(userId)
}
@ -230,25 +268,14 @@ object PushSDK {
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}")
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)) {
@ -258,27 +285,8 @@ object PushSDK {
} else {
Log.w(
"XuqmPushSDK",
"Vendor ${vendor.name} service not available, falling back to FCM",
"Vendor ${vendor.name} service not available, skipping native push registration",
)
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}")
}
}
}
}

查看文件

@ -26,6 +26,7 @@ class FcmPushService : PushVendorInterface {
override fun register(context: Context, config: PushVendorConfig) {
runCatching {
initializeFirebase(context)
val fcmClass = Class.forName("com.google.firebase.messaging.FirebaseMessaging")
val instance = fcmClass.getMethod("getInstance").invoke(null)
fcmClass.getMethod("getToken")
@ -68,4 +69,14 @@ class FcmPushService : PushVendorInterface {
companion object {
private const val TAG = "FcmPushService"
}
private fun initializeFirebase(context: Context) {
val firebaseAppClass = Class.forName("com.google.firebase.FirebaseApp")
val apps = firebaseAppClass.getMethod("getApps", Context::class.java)
.invoke(null, context) as? List<*>
if (apps.isNullOrEmpty()) {
firebaseAppClass.getMethod("initializeApp", Context::class.java)
.invoke(null, context)
}
}
}