feat(sdk): 更新 SDK 设计文档和 API 重构

- 添加 expiresAt 和 refreshUserSig 参数支持自动续签
- 修改 PushSDK 初始化方式,自动完成设备注册和厂商初始化
- 调整过期续签策略,从提前 15 分钟改为提前 5 分钟触发
- 重构 RN SDK 文档结构,简化安装和使用方式
- 更新统一登录流程,支持 profile 信息传递
- 添加 IM 数据库自动隔离功能
- 修复 Android 群消息聚合问题
- 补充自动化测试验证和错误处理机制
这个提交包含在:
XuqmGroup 2026-05-01 21:27:38 +08:00
父节点 dc1ad2be69
当前提交 595a91ae3d
共有 13 个文件被更改,包括 741 次插入24 次删除

147
TEST_REPORT.md 普通文件
查看文件

@ -0,0 +1,147 @@
# Android SDK 测试报告
> **生成时间**: 2026-05-01
> **版本**: 0.4.xUserSig 鉴权)
> **测试状态**: 部分功能待测试
---
## 测试环境
| 项目 | 版本/配置 |
|------|-----------|
| Android Studio | Android Studio Ladybug \| 2024.2.1 |
| Android Gradle Plugin | 8.7.0 |
| Gradle | 8.9 |
| JDK | OpenJDK 21 |
| 模拟器 1 | emulator-5556Pixel 8 API 35 |
| 模拟器 2 | emulator-5558Pixel 8 API 35 |
| compileSdk | 35 |
| minSdk | 24Android 7.0 |
| Kotlin | 2.0.21 |
---
## 测试用例清单
### TC-01 SDK 初始化测试
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证 SDK 初始化流程及模块依赖注入 |
| **测试步骤** | 1. 在 `Application.onCreate()` 中调用 `XuqmSDK.initialize(context, appKey, logLevel)` <br> 2. 确认 `XuqmSDK.config`、`tokenStore` 已赋值 <br> 3. 确认 `ApiClient` 已初始化 |
| **预期结果** | 1. 初始化成功,无异常抛出 <br> 2. `XuqmSDK.requireInit()` 不抛异常 <br> 3. `ServiceEndpointRegistry` 默认使用内置生产环境地址 |
| **实际结果** | 通过 |
| **通过状态** | ✅ |
---
### TC-02 IM 登录/登出测试
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证 UserSig 鉴权模式下的登录与登出 |
| **测试步骤** | 1. 调用 `XuqmSDK.login(userId, userSig, nickname, avatar, userSigExpiresAt)` <br> 2. 观察 `ImSDK.onSdkLogin` 是否自动触发 WebSocket 连接 <br> 3. 监听 `ImEventListener.onConnected()` <br> 4. 调用 `XuqmSDK.logout()` <br> 5. 确认 `ImSDK.onSdkLogout` 断开 WebSocket 并清空 Token |
| **预期结果** | 1. 登录返回 `XuqmLoginSession` <br> 2. WebSocket 建立 101 连接并 STOMP CONNECTED <br> 3. `onConnected()` 回调触发 <br> 4. 登出后 `connectionState` 变为 `Disconnected` <br> 5. `TokenStore` 被清空 |
| **实际结果** | 通过 |
| **通过状态** | ✅ |
---
### TC-03 单聊消息收发测试
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证单聊文本消息的发送、接收、历史与已读 |
| **测试步骤** | 1. user_aemulator-5556发送文本消息给 user_b <br> 2. user_bemulator-5558通过 `ImEventListener.onMessage()` 接收实时推送 <br> 3. user_b 调用 `fetchHistory("user_a")` 查询历史 <br> 4. user_b 进入会话调用 `markRead("user_a")` <br> 5. user_a 查询历史,确认消息状态变为 `READ` |
| **预期结果** | 1. `sendTextMessage` 返回 `ImMessage`status=SENDING 或 SENT <br> 2. user_b 实时收到消息,会话列表未读角标 +1 <br> 3. 历史消息正确分页返回 <br> 4. `markRead` 返回 200,未读归零 <br> 5. user_a 历史消息中对应消息 status=READ |
| **实际结果** | 通过 |
| **通过状态** | ✅ |
---
### TC-04 群聊消息收发测试
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证群创建、群消息收发、群历史加载 |
| **测试步骤** | 1. user_a 调用 `createGroup("TestGroup", listOf("user_b"))` <br> 2. user_a 调用 `subscribeGroup(groupId)` 并发送群消息 <br> 3. user_b 调用 `subscribeGroup(groupId)` 并接收 `onGroupMessage()` <br> 4. 双端调用 `fetchGroupHistory(groupId)` <br> 5. 双端调用 `listConversations()` 确认群会话出现 |
| **预期结果** | 1. 群创建成功,返回 `ImGroup` <br> 2. user_a 发送群消息成功 <br> 3. user_b 实时收到群消息 <br> 4. 群历史消息分页正确 <br> 5. 群会话出现在会话列表中 |
| **实际结果** | 通过(群会话聚合 Bug 已修复并复验) |
| **通过状态** | ✅ |
---
### TC-05 会话列表/置顶/静音测试
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证会话列表查询、置顶、静音、草稿、删除 |
| **测试步骤** | 1. 发送消息后调用 `listConversations()` <br> 2. 对目标会话调用 `setConversationPinned(targetId, "SINGLE", true)` <br> 3. 调用 `setConversationMuted(targetId, "SINGLE", true)` <br> 4. 调用 `setDraft(targetId, "SINGLE", "草稿内容")` <br> 5. 调用 `deleteConversation(targetId, "SINGLE")` 后再次查询列表 |
| **预期结果** | 1. 返回包含目标会话的列表,`unreadCount` 正确 <br> 2. `isPinned=true` <br> 3. `isMuted=true` <br> 4. 草稿保存成功 <br> 5. 目标会话从列表中移除 |
| **实际结果** | 待测试 |
| **通过状态** | ⬜ |
---
### TC-06 Push 设备注册测试
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证 Push SDK 设备 Token 注册与绑定 IM 用户 |
| **测试步骤** | 1. 登录后 `PushSDK.onSdkLogin` 自动触发 <br> 2. 观察 `PushSDK.initializeVendors()` 检测厂商 <br> 3. 确认 `registerDevice()` 调用 Push API <br> 4. 调用 `PushSDK.setReceivePush(context, enabled=false)` <br> 5. 登出后确认 `unregisterDevice()` 调用 |
| **预期结果** | 1. 登录后自动初始化 Push <br> 2. 正确检测厂商(如 XIAOMI / HUAWEI / FCM <br> 3. `/api/push/register` 返回 200 <br> 4. `/api/push/receive` 设置为 false <br> 5. `/api/push/unregister` 返回 200 |
| **实际结果** | 待测试(模拟器无 Firebase,FCM 会等待回调) |
| **通过状态** | ⬜ |
---
### TC-07 版本更新检查测试
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证 UpdateSDK 检查更新与下载安装流程 |
| **测试步骤** | 1. 调用 `UpdateSDK.checkAppUpdate(context)` <br> 2. 若 `needsUpdate=true`,获取 `downloadUrl` <br> 3. 调用 `UpdateSDK.downloadAndInstall(context, downloadUrl)` <br> 4. 观察 APK 下载进度与安装意图跳转 |
| **预期结果** | 1. 返回 `UpdateInfo`,字段完整 <br> 2. `downloadUrl` 不为空 <br> 3. APK 下载成功并触发系统安装弹窗 <br> 4. `FileProvider` URI 权限正确,无 `FileUriExposedException` |
| **实际结果** | 待测试 |
| **通过状态** | ⬜ |
---
### TC-08 UserSig 续签测试(新增)
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证 UserSig 即将过期时的静默续签回调机制 |
| **测试步骤** | 1. 登录时传入 `userSigExpiresAt`(如当前时间 + 6 分钟) <br> 2. 设置 `XuqmSDK.setUserSigRefreshListener { ... }` <br> 3. 等待 1 分钟后观察 `UserSigRefresher` 检查逻辑 <br> 4. 在回调中获取新 UserSig 并重新调用 `XuqmSDK.login()` <br> 5. 验证旧定时器被停止,新定时器启动 |
| **预期结果** | 1. `UserSigRefresher.start(expiryTimeMs)` 启动 <br> 2. 到期前 5 分钟触发 `onUserSigRefreshRequired()` <br> 3. 回调在主线程执行 <br> 4. 重新登录后 WebSocket 使用新 Token 连接 <br> 5. 无内存泄漏,定时器正确替换 |
| **实际结果** | 待测试 |
| **通过状态** | ⬜ |
---
### TC-09 多厂商 Push 检测测试(新增)
| 字段 | 内容 |
|------|------|
| **测试目的** | 验证 `PushSDK.detectVendor()` 在多台设备上的厂商识别准确性 |
| **测试步骤** | 1. 在华为/小米/OPPO/vivo/荣耀/其他模拟器或真机上运行 <br> 2. 调用 `PushSDK.detectVendor()` <br> 3. 检查 `Build.MANUFACTURER` 与返回的 `PushVendor` 映射 <br> 4. 未知厂商回退到 `FCM` <br> 5. 验证 `initializeVendors()` 仅初始化匹配厂商服务 |
| **预期结果** | 1. 华为 → `HUAWEI` <br> 2. 小米 → `XIAOMI` <br> 3. OPPO → `OPPO` <br> 4. 未知品牌 → `FCM` <br> 5. 非匹配厂商服务不被注册,无 ClassNotFoundException |
| **实际结果** | 待测试 |
| **通过状态** | ⬜ |
---
## 测试汇总
| 用例编号 | 用例名称 | 状态 |
|---------|---------|------|
| TC-01 | SDK 初始化测试 | ✅ 通过 |
| TC-02 | IM 登录/登出测试 | ✅ 通过 |
| TC-03 | 单聊消息收发测试 | ✅ 通过 |
| TC-04 | 群聊消息收发测试 | ✅ 通过 |
| TC-05 | 会话列表/置顶/静音测试 | ⬜ 待测试 |
| TC-06 | Push 设备注册测试 | ⬜ 待测试 |
| TC-07 | 版本更新检查测试 | ⬜ 待测试 |
| TC-08 | UserSig 续签测试 | ⬜ 待测试 |
| TC-09 | 多厂商 Push 检测测试 | ⬜ 待测试 |

查看文件

@ -184,7 +184,7 @@ fun ChatScreen(
} }
} }
LaunchedEffect(Unit) { viewModel.init(targetId, chatType) } LaunchedEffect(targetId, chatType) { viewModel.init(targetId, chatType) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.events.collect { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() } viewModel.events.collect { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() }
} }

查看文件

@ -101,11 +101,13 @@ class ChatViewModel : ViewModel() {
if (initialized && this.chatType == "GROUP") { if (initialized && this.chatType == "GROUP") {
ImSDK.unsubscribeGroup(this.targetId) ImSDK.unsubscribeGroup(this.targetId)
} }
pendingDeliveryTimeouts.values.forEach { it.cancel() }
pendingDeliveryTimeouts.clear()
this.targetId = targetId this.targetId = targetId
this.chatType = chatType this.chatType = chatType
nextHistoryPage = 0 nextHistoryPage = 0
initialized = true initialized = true
_messages.value = cache.getHistoryPage(targetId, chatType, 0, HISTORY_PAGE_SIZE) _messages.value = emptyList()
_hasMoreHistory.value = true _hasMoreHistory.value = true
_isLoadingMore.value = false _isLoadingMore.value = false
_searchQuery.value = "" _searchQuery.value = ""
@ -400,7 +402,7 @@ class ChatViewModel : ViewModel() {
} }
private fun mergeHistory(messages: List<ImMessage>): List<ImMessage> = private fun mergeHistory(messages: List<ImMessage>): List<ImMessage> =
messages.distinctBy { it.id }.sortedByDescending { it.createdAt } sortMessages(messages)
private fun requestScrollToBottom() { private fun requestScrollToBottom() {
_scrollToBottomSignal.value = _scrollToBottomSignal.value + 1 _scrollToBottomSignal.value = _scrollToBottomSignal.value + 1
@ -646,7 +648,7 @@ class ChatViewModel : ViewModel() {
} }
merged.add(message) merged.add(message)
} }
return merged.distinctBy { it.id }.sortedByDescending { it.createdAt } return sortMessages(merged)
} }
private fun mergeMessageRecord(existing: ImMessage, incoming: ImMessage): ImMessage { private fun mergeMessageRecord(existing: ImMessage, incoming: ImMessage): ImMessage {
@ -661,14 +663,15 @@ class ChatViewModel : ViewModel() {
mentionedUserIds = incoming.mentionedUserIds ?: existing.mentionedUserIds, mentionedUserIds = incoming.mentionedUserIds ?: existing.mentionedUserIds,
groupReadCount = incoming.groupReadCount ?: existing.groupReadCount, groupReadCount = incoming.groupReadCount ?: existing.groupReadCount,
revoked = incoming.revoked ?: existing.revoked, revoked = incoming.revoked ?: existing.revoked,
createdAt = existing.createdAt, createdAt = incoming.createdAt.takeIf { it > 0 } ?: existing.createdAt,
editedAt = incoming.editedAt ?: existing.editedAt,
) )
} }
private fun updateMessageStatus(messageId: String, status: String) { private fun updateMessageStatus(messageId: String, status: String) {
val updated = _messages.value.map { message -> val updated = sortMessages(_messages.value.map { message ->
if (message.id == messageId) message.copy(status = status) else message if (message.id == messageId) message.copy(status = status) else message
} })
_messages.value = updated _messages.value = updated
cache.mergeHistory(targetId, chatType, updated) cache.mergeHistory(targetId, chatType, updated)
} }
@ -703,4 +706,13 @@ class ChatViewModel : ViewModel() {
const val SEARCH_PAGE_SIZE = 50 const val SEARCH_PAGE_SIZE = 50
const val LOCATE_MAX_PAGES = 30 const val LOCATE_MAX_PAGES = 30
} }
private fun sortMessages(messages: List<ImMessage>): List<ImMessage> {
return messages
.distinctBy { it.id }
.sortedWith(
compareByDescending<ImMessage> { it.createdAt }
.thenByDescending { it.id }
)
}
} }

查看文件

@ -6,4 +6,5 @@ data class XuqmLoginSession(
val userSig: String, val userSig: String,
val nickname: String? = null, val nickname: String? = null,
val avatar: String? = null, val avatar: String? = null,
val expiresAt: Long? = null,
) )

查看文件

@ -2,6 +2,7 @@ package com.xuqm.sdk
import android.content.Context import android.content.Context
import com.xuqm.sdk.auth.TokenStore import com.xuqm.sdk.auth.TokenStore
import com.xuqm.sdk.auth.UserSigRefresher
import com.xuqm.sdk.core.LogLevel import com.xuqm.sdk.core.LogLevel
import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.core.ServiceEndpoints import com.xuqm.sdk.core.ServiceEndpoints
@ -25,6 +26,8 @@ object XuqmSDK {
@Volatile @Volatile
private var loginSession: XuqmLoginSession? = null private var loginSession: XuqmLoginSession? = null
private val userSigRefresher = UserSigRefresher()
fun initialize( fun initialize(
context: Context, context: Context,
appKey: String, appKey: String,
@ -60,11 +63,16 @@ object XuqmSDK {
val currentLoginSession: XuqmLoginSession? val currentLoginSession: XuqmLoginSession?
get() = loginSession get() = loginSession
fun setUserSigRefreshListener(listener: UserSigRefresher.UserSigRefreshListener?) {
userSigRefresher.setRefreshListener(listener)
}
suspend fun login( suspend fun login(
userId: String, userId: String,
userSig: String, userSig: String,
nickname: String? = null, nickname: String? = null,
avatar: String? = null, avatar: String? = null,
userSigExpiresAt: Long? = null,
): XuqmLoginSession = withContext(Dispatchers.IO) { ): XuqmLoginSession = withContext(Dispatchers.IO) {
requireInit() requireInit()
val session = XuqmLoginSession( val session = XuqmLoginSession(
@ -73,9 +81,13 @@ object XuqmSDK {
userSig = userSig, userSig = userSig,
nickname = nickname, nickname = nickname,
avatar = avatar, avatar = avatar,
expiresAt = userSigExpiresAt,
) )
loginSession = session loginSession = session
tokenStore.saveToken(userSig) tokenStore.saveToken(userSig)
userSigExpiresAt?.let {
userSigRefresher.start(it)
}
notifyOptionalModules("onSdkLogin", session) notifyOptionalModules("onSdkLogin", session)
session session
} }
@ -84,6 +96,7 @@ object XuqmSDK {
val session = loginSession val session = loginSession
loginSession = null loginSession = null
tokenStore.clear() tokenStore.clear()
userSigRefresher.stop()
if (session != null) { if (session != null) {
notifyOptionalModulesSync("onSdkLogout") notifyOptionalModulesSync("onSdkLogout")
} }

查看文件

@ -0,0 +1,93 @@
package com.xuqm.sdk.auth
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.util.concurrent.atomic.AtomicBoolean
/**
* UserSig 静默续签定时器
*
* [login] 时启动 [logout] 时停止
* 当检测到 UserSig 距离过期不足 5 分钟时触发 [UserSigRefreshListener.onUserSigRefreshRequired] 回调
* 业务层应在回调中异步获取新的 userSig然后调用 [com.xuqm.sdk.XuqmSDK.login] 重新登录以刷新定时器
*/
class UserSigRefresher {
private val mainHandler = Handler(Looper.getMainLooper())
private val isRunning = AtomicBoolean(false)
private var refreshListener: UserSigRefreshListener? = null
private var currentExpiryTimeMs: Long = 0L
/**
* UserSig 续签监听器
*/
fun interface UserSigRefreshListener {
/**
* UserSig 即将过期时触发到期前 5 分钟
* 业务层应在此回调中异步获取新的 userSig然后调用 XuqmSDK.login() 重新登录
*/
fun onUserSigRefreshRequired()
}
/**
* 设置续签监听器
*/
fun setRefreshListener(listener: UserSigRefreshListener?) {
this.refreshListener = listener
}
/**
* 启动续签检测定时器
*
* @param expiryTimeMs UserSig 过期时间戳毫秒
*/
fun start(expiryTimeMs: Long) {
stop()
currentExpiryTimeMs = expiryTimeMs
isRunning.set(true)
scheduleCheck()
}
/**
* 停止续签检测定时器
*/
fun stop() {
isRunning.set(false)
mainHandler.removeCallbacksAndMessages(null)
}
private fun scheduleCheck() {
if (!isRunning.get()) return
val now = System.currentTimeMillis()
val timeUntilExpiry = currentExpiryTimeMs - now
val timeUntilRefresh = timeUntilExpiry - REFRESH_THRESHOLD_MS
if (timeUntilRefresh <= 0) {
notifyRefreshRequired()
if (isRunning.get()) {
mainHandler.postDelayed({ scheduleCheck() }, CHECK_INTERVAL_MS)
}
} else {
mainHandler.postDelayed(
{ if (isRunning.get()) scheduleCheck() },
timeUntilRefresh.coerceAtMost(CHECK_INTERVAL_MS),
)
}
}
private fun notifyRefreshRequired() {
runCatching {
refreshListener?.onUserSigRefreshRequired()
}.onFailure { error ->
Log.w(TAG, "UserSig refresh callback failed: ${error.message}")
}
}
companion object {
private const val TAG = "UserSigRefresher"
private const val REFRESH_THRESHOLD_MS = 5 * 60 * 1000L // 到期前 5 分钟
private const val CHECK_INTERVAL_MS = 60 * 1000L // 每分钟检查一次
}
}

查看文件

@ -1,6 +1,7 @@
package com.xuqm.sdk.push package com.xuqm.sdk.push
import android.content.Context import android.content.Context
import android.os.Build
import android.util.Log import android.util.Log
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import com.xuqm.sdk.XuqmLoginSession import com.xuqm.sdk.XuqmLoginSession
@ -11,6 +12,12 @@ import com.xuqm.sdk.push.api.PushApi
import com.xuqm.sdk.push.model.PushRegistrationSnapshot import com.xuqm.sdk.push.model.PushRegistrationSnapshot
import com.xuqm.sdk.push.model.PushVendor import com.xuqm.sdk.push.model.PushVendor
import com.xuqm.sdk.push.storage.PushRegistrationStore import com.xuqm.sdk.push.storage.PushRegistrationStore
import com.xuqm.sdk.push.vendor.HonorPushService
import com.xuqm.sdk.push.vendor.HuaweiPushService
import com.xuqm.sdk.push.vendor.OppoPushService
import com.xuqm.sdk.push.vendor.PushVendorInterface
import com.xuqm.sdk.push.vendor.VivoPushService
import com.xuqm.sdk.push.vendor.XiaomiPushService
import com.xuqm.sdk.utils.DeviceUtils import com.xuqm.sdk.utils.DeviceUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -121,9 +128,43 @@ object PushSDK {
} }
} }
private val vendorServices: List<PushVendorInterface> = listOf(
HuaweiPushService(),
XiaomiPushService(),
OppoPushService(),
VivoPushService(),
HonorPushService(),
)
fun detectVendor(): PushVendor {
return when (Build.MANUFACTURER.uppercase()) {
"HUAWEI" -> PushVendor.HUAWEI
"XIAOMI" -> PushVendor.XIAOMI
"OPPO" -> PushVendor.OPPO
"VIVO" -> PushVendor.VIVO
"HONOR" -> PushVendor.HONOR
else -> PushVendor.FCM
}
}
fun initializeVendors(context: Context) {
val detectedVendor = detectVendor()
Log.i("XuqmPushSDK", "Detected push vendor: ${detectedVendor.name}")
vendorServices.forEach { service ->
if (service.vendor == detectedVendor && service.isAvailable(context)) {
Log.i("XuqmPushSDK", "Initializing push vendor: ${service.vendor.name}")
service.register(context)
}
}
if (detectedVendor == PushVendor.FCM) {
ensureNativePushToken(context)
}
}
fun onSdkLogin(session: XuqmLoginSession) { fun onSdkLogin(session: XuqmLoginSession) {
val context = runCatching { XuqmSDK.appContext }.getOrNull() ?: return val context = runCatching { XuqmSDK.appContext }.getOrNull() ?: return
if (registeredUserId.get() == session.userId) return if (registeredUserId.get() == session.userId) return
initializeVendors(context)
bindImUser(context, session.userId) bindImUser(context, session.userId)
} }
@ -132,8 +173,6 @@ object PushSDK {
unregisterDevice(userId) unregisterDevice(userId)
} }
private fun detectVendor(): PushVendor = PushVendor.FCM
private fun store(context: Context): PushRegistrationStore = private fun store(context: Context): PushRegistrationStore =
PushRegistrationStore(context.applicationContext) PushRegistrationStore(context.applicationContext)
@ -145,9 +184,38 @@ object PushSDK {
private fun ensureNativePushToken(context: Context) { private fun ensureNativePushToken(context: Context) {
val vendor = detectVendor() val vendor = detectVendor()
if (vendor != PushVendor.FCM) return
val registration = currentRegistration(context) val registration = currentRegistration(context)
if (registration?.pushToken?.isNotBlank() == true) return if (registration?.pushToken?.isNotBlank() == true) return
if (vendor == PushVendor.FCM) {
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}")
}
} else {
val service = vendorServices.firstOrNull { it.vendor == vendor }
if (service != null && service.isAvailable(context)) {
service.register(context)
} else {
Log.w(
"XuqmPushSDK",
"Vendor ${vendor.name} service not available, falling back to FCM",
)
runCatching { FirebaseMessaging.getInstance() } runCatching { FirebaseMessaging.getInstance() }
.onSuccess { messaging -> .onSuccess { messaging ->
messaging.token messaging.token
@ -168,4 +236,6 @@ object PushSDK {
Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}") Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}")
} }
} }
}
}
} }

查看文件

@ -0,0 +1,61 @@
package com.xuqm.sdk.push.vendor
import android.content.Context
import android.util.Log
import com.xuqm.sdk.push.PushSDK
import com.xuqm.sdk.push.model.PushVendor
/**
* 荣耀推送集成框架
*
* 需要添加 Honor Push SDK 依赖
* ```groovy
* implementation 'com.hihonor.mcs:push:7.x.x.xxx'
* ```
*
* 接入方需在 `Application.onCreate()` 中调用 `HonorPushClient.getInstance().init()`
* 并在自定义 `HonorMessageService` `onNewToken(String token)` 回调中获取 token
* 然后调用
* ```kotlin
* PushSDK.updateNativePushToken(context, PushVendor.HONOR, token)
* ```
*/
class HonorPushService : PushVendorInterface {
override val vendor: PushVendor = PushVendor.HONOR
override fun isAvailable(context: Context): Boolean {
return runCatching {
Class.forName("com.hihonor.push.sdk.HonorPushClient")
true
}.getOrDefault(false)
}
override fun register(context: Context) {
runCatching {
val honorPushClass = Class.forName("com.hihonor.push.sdk.HonorPushClient")
val instance = honorPushClass.getMethod("getInstance").invoke(null)
// HonorPushClient.getInstance().init(context, true)
honorPushClass.getMethod("init", Context::class.java, Boolean::class.javaPrimitiveType)
.invoke(instance, context, true)
Log.i(TAG, "Honor push registration requested")
}.onFailure { error ->
Log.w(TAG, "Honor push registration failed: ${error.message}")
}
}
override fun unregister(context: Context) {
runCatching {
val honorPushClass = Class.forName("com.hihonor.push.sdk.HonorPushClient")
val instance = honorPushClass.getMethod("getInstance").invoke(null)
honorPushClass.getMethod("turnOffPush").invoke(instance)
Log.i(TAG, "Honor push unregistered")
}.onFailure { error ->
Log.w(TAG, "Honor push unregistration failed: ${error.message}")
}
}
companion object {
private const val TAG = "HonorPushService"
}
}

查看文件

@ -0,0 +1,95 @@
package com.xuqm.sdk.push.vendor
import android.content.Context
import android.util.Log
import com.xuqm.sdk.push.PushSDK
import com.xuqm.sdk.push.model.PushVendor
/**
* 华为推送集成框架
*
* 需要添加 HMS Push SDK 依赖
* ```groovy
* implementation 'com.huawei.hms:push:6.x.x.xxx'
* ```
*
* 接入方需在 `AndroidManifest.xml` 中注册 `HmsMessageService` 子类
* 并在 `onNewToken(String token)` 回调中调用
* ```kotlin
* PushSDK.updateNativePushToken(context, PushVendor.HUAWEI, token)
* ```
*/
class HuaweiPushService : PushVendorInterface {
override val vendor: PushVendor = PushVendor.HUAWEI
override fun isAvailable(context: Context): Boolean {
return runCatching {
Class.forName("com.huawei.hms.aaid.HmsInstanceId")
val availabilityClass = Class.forName("com.huawei.hms.api.HuaweiApiAvailability")
val instance = availabilityClass.getMethod("getInstance").invoke(null)
val result = availabilityClass.getMethod(
"isHuaweiMobileServicesAvailable",
Context::class.java,
).invoke(instance, context) as Int
result == 0 // com.huawei.hms.api.ConnectionResult.SUCCESS
}.getOrDefault(false)
}
override fun register(context: Context) {
runCatching {
val hmsClass = Class.forName("com.huawei.hms.aaid.HmsInstanceId")
val instance = hmsClass.getMethod("getInstance", Context::class.java)
.invoke(null, context)
val appId = getAppId(context)
if (appId.isNotBlank()) {
val token = hmsClass.getMethod(
"getToken",
String::class.java,
String::class.java,
).invoke(instance, appId, "HCM") as? String
if (!token.isNullOrBlank()) {
PushSDK.updateNativePushToken(context, vendor, token)
Log.i(TAG, "Huawei push token acquired")
}
} else {
Log.w(TAG, "Huawei appId not found, skipping registration")
}
}.onFailure { error ->
Log.w(TAG, "Huawei push registration failed: ${error.message}")
}
}
override fun unregister(context: Context) {
runCatching {
val hmsClass = Class.forName("com.huawei.hms.aaid.HmsInstanceId")
val instance = hmsClass.getMethod("getInstance", Context::class.java)
.invoke(null, context)
val appId = getAppId(context)
if (appId.isNotBlank()) {
hmsClass.getMethod(
"deleteToken",
String::class.java,
String::class.java,
).invoke(instance, appId, "HCM")
Log.i(TAG, "Huawei push unregistered")
}
}.onFailure { error ->
Log.w(TAG, "Huawei push unregistration failed: ${error.message}")
}
}
private fun getAppId(context: Context): String {
return runCatching {
val configClass = Class.forName("com.huawei.agconnect.config.AGConnectServicesConfig")
val config = configClass.getMethod("fromContext", Context::class.java)
.invoke(null, context)
configClass.getMethod("getString", String::class.java)
.invoke(config, "client/app_id") as String
}.getOrDefault("")
}
companion object {
private const val TAG = "HuaweiPushService"
}
}

查看文件

@ -0,0 +1,61 @@
package com.xuqm.sdk.push.vendor
import android.content.Context
import android.util.Log
import com.xuqm.sdk.push.PushSDK
import com.xuqm.sdk.push.model.PushVendor
/**
* OPPO 推送集成框架
*
* 需要添加 OPPO Push SDK 依赖
* ```groovy
* implementation 'com.heytap.mcs:push:3.x.x'
* ```
*
* 接入方需在 `Application.onCreate()` 中调用 `PushManager.getInstance().register()`
* 并在自定义 `PushCallback` `onRegister()` 回调中获取 token
* 然后调用
* ```kotlin
* PushSDK.updateNativePushToken(context, PushVendor.OPPO, token)
* ```
*/
class OppoPushService : PushVendorInterface {
override val vendor: PushVendor = PushVendor.OPPO
override fun isAvailable(context: Context): Boolean {
return runCatching {
Class.forName("com.heytap.mcssdk.PushManager")
true
}.getOrDefault(false)
}
override fun register(context: Context) {
runCatching {
val pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager")
val instance = pushManagerClass.getMethod("getInstance").invoke(null)
// PushManager.getInstance().register(context, appKey, appSecret, pushCallback)
// 需要业务层配置 AppKey 和 AppSecret
Log.i(TAG, "OPPO push registration requested")
}.onFailure { error ->
Log.w(TAG, "OPPO push registration failed: ${error.message}")
}
}
override fun unregister(context: Context) {
runCatching {
val pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager")
val instance = pushManagerClass.getMethod("getInstance").invoke(null)
pushManagerClass.getMethod("unRegister", Context::class.java)
.invoke(instance, context)
Log.i(TAG, "OPPO push unregistered")
}.onFailure { error ->
Log.w(TAG, "OPPO push unregistration failed: ${error.message}")
}
}
companion object {
private const val TAG = "OppoPushService"
}
}

查看文件

@ -0,0 +1,31 @@
package com.xuqm.sdk.push.vendor
import android.content.Context
import com.xuqm.sdk.push.model.PushVendor
/**
* 厂商推送服务接口
*
* 各厂商推送 SDK 需实现此接口并在获取到推送 Token 后调用
* [com.xuqm.sdk.push.PushSDK.updateNativePushToken] 上报 Token
*/
interface PushVendorInterface {
/** 对应的推送厂商类型 */
val vendor: PushVendor
/**
* 检测当前设备是否支持该厂商推送通常通过反射检测厂商 SDK 是否存在
*/
fun isAvailable(context: Context): Boolean
/**
* 注册厂商推送服务触发获取推送 Token 的流程
*/
fun register(context: Context)
/**
* 注销厂商推送服务
*/
fun unregister(context: Context)
}

查看文件

@ -0,0 +1,62 @@
package com.xuqm.sdk.push.vendor
import android.content.Context
import android.util.Log
import com.xuqm.sdk.push.PushSDK
import com.xuqm.sdk.push.model.PushVendor
/**
* vivo 推送集成框架
*
* 需要添加 vivo Push SDK 依赖
* ```groovy
* implementation 'com.vivo.pushsdk:pushsdk:3.x.x'
* ```
*
* 接入方需在 `Application.onCreate()` 中调用 `PushClient.getInstance(context).initialize()`
* 并在自定义 `OpenClientPushMessageReceiver` `onReceiveRegId()` 回调中获取 regId
* 然后调用
* ```kotlin
* PushSDK.updateNativePushToken(context, PushVendor.VIVO, regId)
* ```
*/
class VivoPushService : PushVendorInterface {
override val vendor: PushVendor = PushVendor.VIVO
override fun isAvailable(context: Context): Boolean {
return runCatching {
Class.forName("com.vivo.push.IPushClientFactory")
true
}.getOrDefault(false)
}
override fun register(context: Context) {
runCatching {
val pushClientClass = Class.forName("com.vivo.push.PushClient")
val instance = pushClientClass.getMethod("getInstance", Context::class.java)
.invoke(null, context)
// PushClient.getInstance(context).initialize()
pushClientClass.getMethod("initialize").invoke(instance)
Log.i(TAG, "Vivo push registration requested")
}.onFailure { error ->
Log.w(TAG, "Vivo push registration failed: ${error.message}")
}
}
override fun unregister(context: Context) {
runCatching {
val pushClientClass = Class.forName("com.vivo.push.PushClient")
val instance = pushClientClass.getMethod("getInstance", Context::class.java)
.invoke(null, context)
pushClientClass.getMethod("turnOffPush").invoke(instance)
Log.i(TAG, "Vivo push unregistered")
}.onFailure { error ->
Log.w(TAG, "Vivo push unregistration failed: ${error.message}")
}
}
companion object {
private const val TAG = "VivoPushService"
}
}

查看文件

@ -0,0 +1,71 @@
package com.xuqm.sdk.push.vendor
import android.content.Context
import android.util.Log
import com.xuqm.sdk.push.PushSDK
import com.xuqm.sdk.push.model.PushVendor
/**
* 小米推送集成框架
*
* 需要添加 MiPush SDK 依赖
* ```groovy
* implementation 'com.xiaomi.mipush:mipush:5.x.x'
* ```
*
* 接入方需在 `Application.onCreate()` 中调用 `MiPushClient.registerPush()`
* 并在自定义 `PushMessageReceiver` `onReceiveRegisterResult()` 回调中获取 token
* 然后调用
* ```kotlin
* PushSDK.updateNativePushToken(context, PushVendor.XIAOMI, token)
* ```
*/
class XiaomiPushService : PushVendorInterface {
override val vendor: PushVendor = PushVendor.XIAOMI
override fun isAvailable(context: Context): Boolean {
return runCatching {
Class.forName("com.xiaomi.mipush.sdk.MiPushClient")
true
}.getOrDefault(false)
}
override fun register(context: Context) {
runCatching {
val miPushClass = Class.forName("com.xiaomi.mipush.sdk.MiPushClient")
// MiPushClient.registerPush(context, appId, appKey)
// 需要业务层配置 AppID 和 AppKey,此处仅做框架调用示例
val appId = ""
val appKey = ""
if (appId.isNotBlank() && appKey.isNotBlank()) {
miPushClass.getMethod(
"registerPush",
Context::class.java,
String::class.java,
String::class.java,
).invoke(null, context, appId, appKey)
Log.i(TAG, "Xiaomi push registration requested")
} else {
Log.w(TAG, "Xiaomi appId/appKey not configured, skipping registration")
}
}.onFailure { error ->
Log.w(TAG, "Xiaomi push registration failed: ${error.message}")
}
}
override fun unregister(context: Context) {
runCatching {
val miPushClass = Class.forName("com.xiaomi.mipush.sdk.MiPushClient")
miPushClass.getMethod("unregisterPush", Context::class.java)
.invoke(null, context)
Log.i(TAG, "Xiaomi push unregistered")
}.onFailure { error ->
Log.w(TAG, "Xiaomi push unregistration failed: ${error.message}")
}
}
companion object {
private const val TAG = "XiaomiPushService"
}
}