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

查看文件

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

查看文件

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@ -31,7 +33,12 @@
<application> <application>
<meta-data <meta-data
android:name="push_kit_auto_init_enabled" 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 <meta-data
android:name="com.vivo.push.app_id" android:name="com.vivo.push.app_id"
android:value="${XUQM_VIVO_APP_ID}" /> android:value="${XUQM_VIVO_APP_ID}" />

查看文件

@ -3,7 +3,6 @@ package com.xuqm.sdk.push
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import com.google.firebase.messaging.FirebaseMessaging
import com.xuqm.sdk.XuqmLoginSession import com.xuqm.sdk.XuqmLoginSession
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.ServiceEndpointRegistry 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 configApi: PushConfigApi get() = ApiClient.create(PushConfigApi::class.java, ServiceEndpointRegistry.controlBaseUrl)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val registeredUserId = AtomicReference<String?>(null) private val registeredUserId = AtomicReference<String?>(null)
private val registeringDeviceKey = AtomicReference<String?>(null)
private val lastRegisteredDeviceKey = AtomicReference<String?>(null)
@Volatile @Volatile
private var cachedVendorConfig: PushVendorConfig? = null private var cachedVendorConfig: PushVendorConfig? = null
fun currentRegistration(context: Context): PushRegistrationSnapshot? { fun currentRegistration(context: Context): PushRegistrationSnapshot? {
XuqmSDK.requireInit() XuqmSDK.requireInit()
val deviceId = DeviceUtils.getDeviceId(context) 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( fun updateNativePushToken(
@ -49,6 +61,14 @@ object PushSDK {
pushToken: String, pushToken: String,
) { ) {
XuqmSDK.requireInit() 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() val normalizedToken = pushToken.trim()
require(normalizedToken.isNotBlank()) { "pushToken must not be blank" } require(normalizedToken.isNotBlank()) { "pushToken must not be blank" }
store(context).save(vendor, normalizedToken) store(context).save(vendor, normalizedToken)
@ -109,11 +129,24 @@ object PushSDK {
val registration = currentRegistration(context) val registration = currentRegistration(context)
val vendor = registration?.vendor ?: detectVendor() val vendor = registration?.vendor ?: detectVendor()
val pushToken = registration?.pushToken?.takeIf { it.isNotBlank() } val pushToken = registration?.pushToken?.takeIf { it.isNotBlank() }
val deviceId = DeviceUtils.getDeviceId(context)
if (pushToken.isNullOrBlank()) { if (pushToken.isNullOrBlank()) {
Log.w("XuqmPushSDK", "Native push token not ready yet, waiting for onNewToken()") Log.w("XuqmPushSDK", "Native push token not ready yet, waiting for onNewToken()")
ensureNativePushToken(context) ensureNativePushToken(context)
return 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 { scope.launch {
runCatching { runCatching {
api.registerDevice( api.registerDevice(
@ -121,18 +154,21 @@ object PushSDK {
userId = userId, userId = userId,
vendor = vendor.name, vendor = vendor.name,
token = pushToken, token = pushToken,
deviceId = DeviceUtils.getDeviceId(context), deviceId = deviceId,
brand = Build.MANUFACTURER.orEmpty(), brand = Build.MANUFACTURER.orEmpty(),
model = Build.MODEL.orEmpty(), model = Build.MODEL.orEmpty(),
osVersion = DeviceUtils.getOsVersion(), osVersion = DeviceUtils.getOsVersion(),
appVersion = appVersion(context), appVersion = appVersion(context),
) )
registeredUserId.set(userId) registeredUserId.set(userId)
lastRegisteredDeviceKey.set(registrationKey)
store(context).updateLastUserId(userId) store(context).updateLastUserId(userId)
Log.i( Log.i(
"XuqmPushSDK", "XuqmPushSDK",
"Registered push device for userId=$userId vendor=${vendor.name}", "Registered push device for userId=$userId vendor=${vendor.name}",
) )
}.also {
registeringDeviceKey.compareAndSet(registrationKey, null)
} }
} }
} }
@ -206,6 +242,8 @@ object PushSDK {
fun onSdkLogout() { fun onSdkLogout() {
val userId = registeredUserId.getAndSet(null) ?: return val userId = registeredUserId.getAndSet(null) ?: return
lastRegisteredDeviceKey.set(null)
registeringDeviceKey.set(null)
unregisterDevice(userId) unregisterDevice(userId)
} }
@ -230,25 +268,14 @@ object PushSDK {
if (registration?.pushToken?.isNotBlank() == true) return if (registration?.pushToken?.isNotBlank() == true) return
if (vendor == PushVendor.FCM) { if (vendor == PushVendor.FCM) {
runCatching { FirebaseMessaging.getInstance() } val service = vendorServices.firstOrNull { it.vendor == PushVendor.FCM }
.onSuccess { messaging -> if (service != null && service.isAvailable(context)) {
messaging.token scope.launch {
.addOnSuccessListener { token -> service.register(context, loadVendorConfig())
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 {
Log.w("XuqmPushSDK", "FCM service not available, skipping native push registration")
}
} else { } else {
val service = vendorServices.firstOrNull { it.vendor == vendor } val service = vendorServices.firstOrNull { it.vendor == vendor }
if (service != null && service.isAvailable(context)) { if (service != null && service.isAvailable(context)) {
@ -258,27 +285,8 @@ object PushSDK {
} else { } else {
Log.w( Log.w(
"XuqmPushSDK", "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) { override fun register(context: Context, config: PushVendorConfig) {
runCatching { runCatching {
initializeFirebase(context)
val fcmClass = Class.forName("com.google.firebase.messaging.FirebaseMessaging") val fcmClass = Class.forName("com.google.firebase.messaging.FirebaseMessaging")
val instance = fcmClass.getMethod("getInstance").invoke(null) val instance = fcmClass.getMethod("getInstance").invoke(null)
fcmClass.getMethod("getToken") fcmClass.getMethod("getToken")
@ -68,4 +69,14 @@ class FcmPushService : PushVendorInterface {
companion object { companion object {
private const val TAG = "FcmPushService" 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)
}
}
} }