From 84221ff6b2a3c814a57185a565b81de66b75e0c5 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 5 May 2026 22:16:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(push):=20=E6=B7=BB=E5=8A=A0=E6=8E=A8?= =?UTF-8?q?=E9=80=81SDK=E5=92=8C=E6=B6=88=E6=81=AF=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 Android 推送 SDK,支持华为、小米、Oppo、Vivo、荣耀、FCM 等厂商推送 - 添加了推送配置管理和设备注册功能 - 实现了推送令牌管理和用户绑定功能 - 添加了消息发送、撤回、编辑等核心消息服务功能 - 实现了单聊和群聊消息历史记录管理 - 添加了消息读取回执和群组消息状态同步 - 实现了消息过滤、黑名单和权限控制 - 添加了离线消息推送和消息预览功能 - 实现了消息 Webhook 回调机制 --- .../push/PushNotificationChannelManager.kt | 80 +++++++++++++++++++ .../main/java/com/xuqm/sdk/push/PushSDK.kt | 17 +++- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 sdk-push/src/main/java/com/xuqm/sdk/push/PushNotificationChannelManager.kt diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/PushNotificationChannelManager.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/PushNotificationChannelManager.kt new file mode 100644 index 0000000..8eeffd4 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/PushNotificationChannelManager.kt @@ -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() + 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 + } + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt index a5c7cf2..8a68fb5 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/PushSDK.kt @@ -37,6 +37,8 @@ object PushSDK { private val lastRegisteredDeviceKey = AtomicReference(null) @Volatile private var cachedVendorConfig: PushVendorConfig? = null + @Volatile + private var cachedConfigAt: Long = 0L fun currentRegistration(context: Context): PushRegistrationSnapshot? { XuqmSDK.requireInit() @@ -55,6 +57,9 @@ object PushSDK { return registration } + fun notificationChannelIdFor(context: Context, routeType: String): String? = + PushNotificationChannelManager.channelIdFor(context.applicationContext, routeType) + fun updateNativePushToken( context: Context, vendor: PushVendor, @@ -221,6 +226,7 @@ object PushSDK { Log.i("XuqmPushSDK", "Detected push vendor: ${detectedVendor.name}") scope.launch { val config = loadVendorConfig() + PushNotificationChannelManager.apply(context.applicationContext, cachedPushConfig) vendorServices.forEach { service -> if (service.vendor == detectedVendor && service.isAvailable(context)) { Log.i("XuqmPushSDK", "Initializing push vendor: ${service.vendor.name}") @@ -302,10 +308,16 @@ object PushSDK { } }.getOrNull() + @Volatile + private var cachedPushConfig: com.google.gson.JsonObject? = null + 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 pushConfig = configApi.sdkConfig(XuqmSDK.appKey).data?.pushConfig + cachedPushConfig = pushConfig + PushNotificationChannelManager.apply(XuqmSDK.appContext, pushConfig) PushVendorConfig( huaweiAppId = pushConfig?.getAsJsonObject("huawei")?.get("appId")?.asString.orEmpty(), xiaomiAppId = pushConfig?.getAsJsonObject("xiaomi")?.get("appId")?.asString.orEmpty(), @@ -321,6 +333,9 @@ object PushSDK { PushVendorConfig() } cachedVendorConfig = loaded + cachedConfigAt = now return loaded } + + private const val CONFIG_CACHE_TTL_MS = 5 * 60 * 1000L }