feat(push): 添加推送SDK和消息服务实现
- 实现了 Android 推送 SDK,支持华为、小米、Oppo、Vivo、荣耀、FCM 等厂商推送 - 添加了推送配置管理和设备注册功能 - 实现了推送令牌管理和用户绑定功能 - 添加了消息发送、撤回、编辑等核心消息服务功能 - 实现了单聊和群聊消息历史记录管理 - 添加了消息读取回执和群组消息状态同步 - 实现了消息过滤、黑名单和权限控制 - 添加了离线消息推送和消息预览功能 - 实现了消息 Webhook 回调机制
这个提交包含在:
父节点
9114672518
当前提交
84221ff6b2
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户