feat(push): 添加推送服务功能支持
- 实现了 ConversationViewModel 来管理对话列表的刷新和状态 - 集成了 FCM 推送服务支持并实现了自动令牌获取机制 - 构建了完整的 PushSDK 推送系统,支持华为、小米、OPPO、VIVO、荣耀等厂商推送 - 添加了推送配置管理和设备注册/注销功能 - 实现了跨平台推送令牌管理和服务绑定逻辑 - 扩展了服务器端功能服务管理器以支持推送服务激活请求流程
这个提交包含在:
父节点
e87b1b0af2
当前提交
9114672518
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户