feat(push): 添加推送SDK和消息服务实现

- 实现了 Android 推送 SDK,支持华为、小米、Oppo、Vivo、荣耀、FCM 等厂商推送
- 添加了推送配置管理和设备注册功能
- 实现了推送令牌管理和用户绑定功能
- 添加了消息发送、撤回、编辑等核心消息服务功能
- 实现了单聊和群聊消息历史记录管理
- 添加了消息读取回执和群组消息状态同步
- 实现了消息过滤、黑名单和权限控制
- 添加了离线消息推送和消息预览功能
- 实现了消息 Webhook 回调机制
这个提交包含在:
XuqmGroup 2026-05-05 22:16:11 +08:00
父节点 9114672518
当前提交 84221ff6b2
共有 2 个文件被更改,包括 96 次插入1 次删除

查看文件

@ -0,0 +1,80 @@
package com.xuqm.sdk.push
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.util.Log
import com.google.gson.JsonObject
internal object PushNotificationChannelManager {
private const val TAG = "XuqmPushChannel"
private const val PREFS_NAME = "xuqm_push_channels"
private const val KEY_PREFIX_ROUTE = "route_"
fun apply(context: Context, pushConfig: JsonObject?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || pushConfig == null) return
val channels = pushConfig.getAsJsonArray("channels") ?: return
val channelByKey = mutableMapOf<String, String>()
val manager = context.getSystemService(NotificationManager::class.java)
channels.mapNotNull { it.takeIf { item -> item.isJsonObject }?.asJsonObject }
.forEach { channel ->
val key = channel.text("key").ifBlank { return@forEach }
val baseChannelId = channel.text("channelId").ifBlank { key }
val version = channel.int("version", 1).coerceAtLeast(1)
val effectiveChannelId = "${baseChannelId}_v$version"
val notificationChannel = NotificationChannel(
effectiveChannelId,
channel.text("name").ifBlank { key },
channel.importance(),
).apply {
description = channel.text("description")
enableVibration(channel.bool("vibration", true))
setShowBadge(channel.bool("badge", true))
if (!channel.bool("sound", true)) {
setSound(null, null)
}
}
manager.createNotificationChannel(notificationChannel)
channelByKey[key] = effectiveChannelId
Log.d(TAG, "Notification channel ready key=$key id=$effectiveChannelId")
}
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().apply {
channelByKey.forEach { (key, channelId) -> putString("channel_$key", channelId) }
val routing = pushConfig.getAsJsonObject("routing")
routing?.entrySet()?.forEach { (type, routeElement) ->
val route = routeElement.takeIf { it.isJsonObject }?.asJsonObject ?: return@forEach
val channelKey = route.text("channel")
val channelId = channelByKey[channelKey] ?: return@forEach
putString(KEY_PREFIX_ROUTE + type, channelId)
}
}.apply()
}
fun channelIdFor(context: Context, routeType: String): String? =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString(KEY_PREFIX_ROUTE + routeType, null)
private fun JsonObject.text(key: String): String =
runCatching { get(key)?.takeIf { !it.isJsonNull }?.asString?.trim().orEmpty() }.getOrDefault("")
private fun JsonObject.bool(key: String, fallback: Boolean): Boolean =
runCatching { get(key)?.takeIf { !it.isJsonNull }?.asBoolean ?: fallback }.getOrDefault(fallback)
private fun JsonObject.int(key: String, fallback: Int): Int =
runCatching { get(key)?.takeIf { !it.isJsonNull }?.asInt ?: fallback }.getOrDefault(fallback)
private fun JsonObject.importance(): Int {
return when (text("importance").uppercase()) {
"MIN" -> NotificationManager.IMPORTANCE_MIN
"LOW" -> NotificationManager.IMPORTANCE_LOW
"HIGH" -> NotificationManager.IMPORTANCE_HIGH
"MAX" -> NotificationManager.IMPORTANCE_MAX
else -> NotificationManager.IMPORTANCE_DEFAULT
}
}
}

查看文件

@ -37,6 +37,8 @@ object PushSDK {
private val lastRegisteredDeviceKey = AtomicReference<String?>(null) private val lastRegisteredDeviceKey = AtomicReference<String?>(null)
@Volatile @Volatile
private var cachedVendorConfig: PushVendorConfig? = null private var cachedVendorConfig: PushVendorConfig? = null
@Volatile
private var cachedConfigAt: Long = 0L
fun currentRegistration(context: Context): PushRegistrationSnapshot? { fun currentRegistration(context: Context): PushRegistrationSnapshot? {
XuqmSDK.requireInit() XuqmSDK.requireInit()
@ -55,6 +57,9 @@ object PushSDK {
return registration return registration
} }
fun notificationChannelIdFor(context: Context, routeType: String): String? =
PushNotificationChannelManager.channelIdFor(context.applicationContext, routeType)
fun updateNativePushToken( fun updateNativePushToken(
context: Context, context: Context,
vendor: PushVendor, vendor: PushVendor,
@ -221,6 +226,7 @@ object PushSDK {
Log.i("XuqmPushSDK", "Detected push vendor: ${detectedVendor.name}") Log.i("XuqmPushSDK", "Detected push vendor: ${detectedVendor.name}")
scope.launch { scope.launch {
val config = loadVendorConfig() val config = loadVendorConfig()
PushNotificationChannelManager.apply(context.applicationContext, cachedPushConfig)
vendorServices.forEach { service -> vendorServices.forEach { service ->
if (service.vendor == detectedVendor && service.isAvailable(context)) { if (service.vendor == detectedVendor && service.isAvailable(context)) {
Log.i("XuqmPushSDK", "Initializing push vendor: ${service.vendor.name}") Log.i("XuqmPushSDK", "Initializing push vendor: ${service.vendor.name}")
@ -302,10 +308,16 @@ object PushSDK {
} }
}.getOrNull() }.getOrNull()
@Volatile
private var cachedPushConfig: com.google.gson.JsonObject? = null
private suspend fun loadVendorConfig(): PushVendorConfig { private suspend fun loadVendorConfig(): PushVendorConfig {
cachedVendorConfig?.let { return it } val now = System.currentTimeMillis()
cachedVendorConfig?.takeIf { now - cachedConfigAt < CONFIG_CACHE_TTL_MS }?.let { return it }
val loaded = runCatching { val loaded = runCatching {
val pushConfig = configApi.sdkConfig(XuqmSDK.appKey).data?.pushConfig val pushConfig = configApi.sdkConfig(XuqmSDK.appKey).data?.pushConfig
cachedPushConfig = pushConfig
PushNotificationChannelManager.apply(XuqmSDK.appContext, pushConfig)
PushVendorConfig( PushVendorConfig(
huaweiAppId = pushConfig?.getAsJsonObject("huawei")?.get("appId")?.asString.orEmpty(), huaweiAppId = pushConfig?.getAsJsonObject("huawei")?.get("appId")?.asString.orEmpty(),
xiaomiAppId = pushConfig?.getAsJsonObject("xiaomi")?.get("appId")?.asString.orEmpty(), xiaomiAppId = pushConfig?.getAsJsonObject("xiaomi")?.get("appId")?.asString.orEmpty(),
@ -321,6 +333,9 @@ object PushSDK {
PushVendorConfig() PushVendorConfig()
} }
cachedVendorConfig = loaded cachedVendorConfig = loaded
cachedConfigAt = now
return loaded return loaded
} }
private const val CONFIG_CACHE_TTL_MS = 5 * 60 * 1000L
} }