feat(sdk): 更新 SDK 设计文档和 API 重构
- 添加 expiresAt 和 refreshUserSig 参数支持自动续签 - 修改 PushSDK 初始化方式,自动完成设备注册和厂商初始化 - 调整过期续签策略,从提前 15 分钟改为提前 5 分钟触发 - 重构 RN SDK 文档结构,简化安装和使用方式 - 更新统一登录流程,支持 profile 信息传递 - 添加 IM 数据库自动隔离功能 - 修复 Android 群消息聚合问题 - 补充自动化测试验证和错误处理机制
这个提交包含在:
父节点
dc1ad2be69
当前提交
595a91ae3d
147
TEST_REPORT.md
普通文件
147
TEST_REPORT.md
普通文件
@ -0,0 +1,147 @@
|
||||
# Android SDK 测试报告
|
||||
|
||||
> **生成时间**: 2026-05-01
|
||||
> **版本**: 0.4.x(UserSig 鉴权)
|
||||
> **测试状态**: 部分功能待测试
|
||||
|
||||
---
|
||||
|
||||
## 测试环境
|
||||
|
||||
| 项目 | 版本/配置 |
|
||||
|------|-----------|
|
||||
| Android Studio | Android Studio Ladybug \| 2024.2.1 |
|
||||
| Android Gradle Plugin | 8.7.0 |
|
||||
| Gradle | 8.9 |
|
||||
| JDK | OpenJDK 21 |
|
||||
| 模拟器 1 | emulator-5556(Pixel 8 API 35) |
|
||||
| 模拟器 2 | emulator-5558(Pixel 8 API 35) |
|
||||
| compileSdk | 35 |
|
||||
| minSdk | 24(Android 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_a(emulator-5556)发送文本消息给 user_b <br> 2. user_b(emulator-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) {
|
||||
viewModel.events.collect { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
||||
@ -101,11 +101,13 @@ class ChatViewModel : ViewModel() {
|
||||
if (initialized && this.chatType == "GROUP") {
|
||||
ImSDK.unsubscribeGroup(this.targetId)
|
||||
}
|
||||
pendingDeliveryTimeouts.values.forEach { it.cancel() }
|
||||
pendingDeliveryTimeouts.clear()
|
||||
this.targetId = targetId
|
||||
this.chatType = chatType
|
||||
nextHistoryPage = 0
|
||||
initialized = true
|
||||
_messages.value = cache.getHistoryPage(targetId, chatType, 0, HISTORY_PAGE_SIZE)
|
||||
_messages.value = emptyList()
|
||||
_hasMoreHistory.value = true
|
||||
_isLoadingMore.value = false
|
||||
_searchQuery.value = ""
|
||||
@ -400,7 +402,7 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
private fun mergeHistory(messages: List<ImMessage>): List<ImMessage> =
|
||||
messages.distinctBy { it.id }.sortedByDescending { it.createdAt }
|
||||
sortMessages(messages)
|
||||
|
||||
private fun requestScrollToBottom() {
|
||||
_scrollToBottomSignal.value = _scrollToBottomSignal.value + 1
|
||||
@ -646,7 +648,7 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
merged.add(message)
|
||||
}
|
||||
return merged.distinctBy { it.id }.sortedByDescending { it.createdAt }
|
||||
return sortMessages(merged)
|
||||
}
|
||||
|
||||
private fun mergeMessageRecord(existing: ImMessage, incoming: ImMessage): ImMessage {
|
||||
@ -661,14 +663,15 @@ class ChatViewModel : ViewModel() {
|
||||
mentionedUserIds = incoming.mentionedUserIds ?: existing.mentionedUserIds,
|
||||
groupReadCount = incoming.groupReadCount ?: existing.groupReadCount,
|
||||
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) {
|
||||
val updated = _messages.value.map { message ->
|
||||
val updated = sortMessages(_messages.value.map { message ->
|
||||
if (message.id == messageId) message.copy(status = status) else message
|
||||
}
|
||||
})
|
||||
_messages.value = updated
|
||||
cache.mergeHistory(targetId, chatType, updated)
|
||||
}
|
||||
@ -703,4 +706,13 @@ class ChatViewModel : ViewModel() {
|
||||
const val SEARCH_PAGE_SIZE = 50
|
||||
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 nickname: String? = null,
|
||||
val avatar: String? = null,
|
||||
val expiresAt: Long? = null,
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@ package com.xuqm.sdk
|
||||
|
||||
import android.content.Context
|
||||
import com.xuqm.sdk.auth.TokenStore
|
||||
import com.xuqm.sdk.auth.UserSigRefresher
|
||||
import com.xuqm.sdk.core.LogLevel
|
||||
import com.xuqm.sdk.core.ServiceEndpointRegistry
|
||||
import com.xuqm.sdk.core.ServiceEndpoints
|
||||
@ -25,6 +26,8 @@ object XuqmSDK {
|
||||
@Volatile
|
||||
private var loginSession: XuqmLoginSession? = null
|
||||
|
||||
private val userSigRefresher = UserSigRefresher()
|
||||
|
||||
fun initialize(
|
||||
context: Context,
|
||||
appKey: String,
|
||||
@ -60,11 +63,16 @@ object XuqmSDK {
|
||||
val currentLoginSession: XuqmLoginSession?
|
||||
get() = loginSession
|
||||
|
||||
fun setUserSigRefreshListener(listener: UserSigRefresher.UserSigRefreshListener?) {
|
||||
userSigRefresher.setRefreshListener(listener)
|
||||
}
|
||||
|
||||
suspend fun login(
|
||||
userId: String,
|
||||
userSig: String,
|
||||
nickname: String? = null,
|
||||
avatar: String? = null,
|
||||
userSigExpiresAt: Long? = null,
|
||||
): XuqmLoginSession = withContext(Dispatchers.IO) {
|
||||
requireInit()
|
||||
val session = XuqmLoginSession(
|
||||
@ -73,9 +81,13 @@ object XuqmSDK {
|
||||
userSig = userSig,
|
||||
nickname = nickname,
|
||||
avatar = avatar,
|
||||
expiresAt = userSigExpiresAt,
|
||||
)
|
||||
loginSession = session
|
||||
tokenStore.saveToken(userSig)
|
||||
userSigExpiresAt?.let {
|
||||
userSigRefresher.start(it)
|
||||
}
|
||||
notifyOptionalModules("onSdkLogin", session)
|
||||
session
|
||||
}
|
||||
@ -84,6 +96,7 @@ object XuqmSDK {
|
||||
val session = loginSession
|
||||
loginSession = null
|
||||
tokenStore.clear()
|
||||
userSigRefresher.stop()
|
||||
if (session != null) {
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
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.PushVendor
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
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) {
|
||||
val context = runCatching { XuqmSDK.appContext }.getOrNull() ?: return
|
||||
if (registeredUserId.get() == session.userId) return
|
||||
initializeVendors(context)
|
||||
bindImUser(context, session.userId)
|
||||
}
|
||||
|
||||
@ -132,8 +173,6 @@ object PushSDK {
|
||||
unregisterDevice(userId)
|
||||
}
|
||||
|
||||
private fun detectVendor(): PushVendor = PushVendor.FCM
|
||||
|
||||
private fun store(context: Context): PushRegistrationStore =
|
||||
PushRegistrationStore(context.applicationContext)
|
||||
|
||||
@ -145,9 +184,38 @@ object PushSDK {
|
||||
|
||||
private fun ensureNativePushToken(context: Context) {
|
||||
val vendor = detectVendor()
|
||||
if (vendor != PushVendor.FCM) return
|
||||
val registration = currentRegistration(context)
|
||||
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() }
|
||||
.onSuccess { messaging ->
|
||||
messaging.token
|
||||
@ -168,4 +236,6 @@ object PushSDK {
|
||||
Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt
vendored
普通文件
61
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt
vendored
普通文件
@ -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"
|
||||
}
|
||||
}
|
||||
95
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt
vendored
普通文件
95
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt
vendored
普通文件
@ -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"
|
||||
}
|
||||
}
|
||||
61
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt
vendored
普通文件
61
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt
vendored
普通文件
@ -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)
|
||||
}
|
||||
62
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt
vendored
普通文件
62
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt
vendored
普通文件
@ -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"
|
||||
}
|
||||
}
|
||||
71
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt
vendored
普通文件
71
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt
vendored
普通文件
@ -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"
|
||||
}
|
||||
}
|
||||
正在加载...
在新工单中引用
屏蔽一个用户