diff --git a/TEST_REPORT.md b/TEST_REPORT.md new file mode 100644 index 0000000..cf55eb9 --- /dev/null +++ b/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)`
2. 确认 `XuqmSDK.config`、`tokenStore` 已赋值
3. 确认 `ApiClient` 已初始化 | +| **预期结果** | 1. 初始化成功,无异常抛出
2. `XuqmSDK.requireInit()` 不抛异常
3. `ServiceEndpointRegistry` 默认使用内置生产环境地址 | +| **实际结果** | 通过 | +| **通过状态** | ✅ | + +--- + +### TC-02 IM 登录/登出测试 + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证 UserSig 鉴权模式下的登录与登出 | +| **测试步骤** | 1. 调用 `XuqmSDK.login(userId, userSig, nickname, avatar, userSigExpiresAt)`
2. 观察 `ImSDK.onSdkLogin` 是否自动触发 WebSocket 连接
3. 监听 `ImEventListener.onConnected()`
4. 调用 `XuqmSDK.logout()`
5. 确认 `ImSDK.onSdkLogout` 断开 WebSocket 并清空 Token | +| **预期结果** | 1. 登录返回 `XuqmLoginSession`
2. WebSocket 建立 101 连接并 STOMP CONNECTED
3. `onConnected()` 回调触发
4. 登出后 `connectionState` 变为 `Disconnected`
5. `TokenStore` 被清空 | +| **实际结果** | 通过 | +| **通过状态** | ✅ | + +--- + +### TC-03 单聊消息收发测试 + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证单聊文本消息的发送、接收、历史与已读 | +| **测试步骤** | 1. user_a(emulator-5556)发送文本消息给 user_b
2. user_b(emulator-5558)通过 `ImEventListener.onMessage()` 接收实时推送
3. user_b 调用 `fetchHistory("user_a")` 查询历史
4. user_b 进入会话调用 `markRead("user_a")`
5. user_a 查询历史,确认消息状态变为 `READ` | +| **预期结果** | 1. `sendTextMessage` 返回 `ImMessage`(status=SENDING 或 SENT)
2. user_b 实时收到消息,会话列表未读角标 +1
3. 历史消息正确分页返回
4. `markRead` 返回 200,未读归零
5. user_a 历史消息中对应消息 status=READ | +| **实际结果** | 通过 | +| **通过状态** | ✅ | + +--- + +### TC-04 群聊消息收发测试 + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证群创建、群消息收发、群历史加载 | +| **测试步骤** | 1. user_a 调用 `createGroup("TestGroup", listOf("user_b"))`
2. user_a 调用 `subscribeGroup(groupId)` 并发送群消息
3. user_b 调用 `subscribeGroup(groupId)` 并接收 `onGroupMessage()`
4. 双端调用 `fetchGroupHistory(groupId)`
5. 双端调用 `listConversations()` 确认群会话出现 | +| **预期结果** | 1. 群创建成功,返回 `ImGroup`
2. user_a 发送群消息成功
3. user_b 实时收到群消息
4. 群历史消息分页正确
5. 群会话出现在会话列表中 | +| **实际结果** | 通过(群会话聚合 Bug 已修复并复验) | +| **通过状态** | ✅ | + +--- + +### TC-05 会话列表/置顶/静音测试 + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证会话列表查询、置顶、静音、草稿、删除 | +| **测试步骤** | 1. 发送消息后调用 `listConversations()`
2. 对目标会话调用 `setConversationPinned(targetId, "SINGLE", true)`
3. 调用 `setConversationMuted(targetId, "SINGLE", true)`
4. 调用 `setDraft(targetId, "SINGLE", "草稿内容")`
5. 调用 `deleteConversation(targetId, "SINGLE")` 后再次查询列表 | +| **预期结果** | 1. 返回包含目标会话的列表,`unreadCount` 正确
2. `isPinned=true`
3. `isMuted=true`
4. 草稿保存成功
5. 目标会话从列表中移除 | +| **实际结果** | 待测试 | +| **通过状态** | ⬜ | + +--- + +### TC-06 Push 设备注册测试 + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证 Push SDK 设备 Token 注册与绑定 IM 用户 | +| **测试步骤** | 1. 登录后 `PushSDK.onSdkLogin` 自动触发
2. 观察 `PushSDK.initializeVendors()` 检测厂商
3. 确认 `registerDevice()` 调用 Push API
4. 调用 `PushSDK.setReceivePush(context, enabled=false)`
5. 登出后确认 `unregisterDevice()` 调用 | +| **预期结果** | 1. 登录后自动初始化 Push
2. 正确检测厂商(如 XIAOMI / HUAWEI / FCM)
3. `/api/push/register` 返回 200
4. `/api/push/receive` 设置为 false
5. `/api/push/unregister` 返回 200 | +| **实际结果** | 待测试(模拟器无 Firebase,FCM 会等待回调) | +| **通过状态** | ⬜ | + +--- + +### TC-07 版本更新检查测试 + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证 UpdateSDK 检查更新与下载安装流程 | +| **测试步骤** | 1. 调用 `UpdateSDK.checkAppUpdate(context)`
2. 若 `needsUpdate=true`,获取 `downloadUrl`
3. 调用 `UpdateSDK.downloadAndInstall(context, downloadUrl)`
4. 观察 APK 下载进度与安装意图跳转 | +| **预期结果** | 1. 返回 `UpdateInfo`,字段完整
2. `downloadUrl` 不为空
3. APK 下载成功并触发系统安装弹窗
4. `FileProvider` URI 权限正确,无 `FileUriExposedException` | +| **实际结果** | 待测试 | +| **通过状态** | ⬜ | + +--- + +### TC-08 UserSig 续签测试(新增) + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证 UserSig 即将过期时的静默续签回调机制 | +| **测试步骤** | 1. 登录时传入 `userSigExpiresAt`(如当前时间 + 6 分钟)
2. 设置 `XuqmSDK.setUserSigRefreshListener { ... }`
3. 等待 1 分钟后观察 `UserSigRefresher` 检查逻辑
4. 在回调中获取新 UserSig 并重新调用 `XuqmSDK.login()`
5. 验证旧定时器被停止,新定时器启动 | +| **预期结果** | 1. `UserSigRefresher.start(expiryTimeMs)` 启动
2. 到期前 5 分钟触发 `onUserSigRefreshRequired()`
3. 回调在主线程执行
4. 重新登录后 WebSocket 使用新 Token 连接
5. 无内存泄漏,定时器正确替换 | +| **实际结果** | 待测试 | +| **通过状态** | ⬜ | + +--- + +### TC-09 多厂商 Push 检测测试(新增) + +| 字段 | 内容 | +|------|------| +| **测试目的** | 验证 `PushSDK.detectVendor()` 在多台设备上的厂商识别准确性 | +| **测试步骤** | 1. 在华为/小米/OPPO/vivo/荣耀/其他模拟器或真机上运行
2. 调用 `PushSDK.detectVendor()`
3. 检查 `Build.MANUFACTURER` 与返回的 `PushVendor` 映射
4. 未知厂商回退到 `FCM`
5. 验证 `initializeVendors()` 仅初始化匹配厂商服务 | +| **预期结果** | 1. 华为 → `HUAWEI`
2. 小米 → `XIAOMI`
3. OPPO → `OPPO`
4. 未知品牌 → `FCM`
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 检测测试 | ⬜ 待测试 | diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatScreen.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatScreen.kt index 44980aa..772b122 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatScreen.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatScreen.kt @@ -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() } } diff --git a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt index 7f8720b..92cc841 100644 --- a/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt +++ b/sample-app/src/main/java/com/xuqm/sdk/sample/ui/chat/ChatViewModel.kt @@ -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): List = - 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): List { + return messages + .distinctBy { it.id } + .sortedWith( + compareByDescending { it.createdAt } + .thenByDescending { it.id } + ) + } } diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmLoginSession.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmLoginSession.kt index 58dba2d..d52a064 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmLoginSession.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmLoginSession.kt @@ -6,4 +6,5 @@ data class XuqmLoginSession( val userSig: String, val nickname: String? = null, val avatar: String? = null, + val expiresAt: Long? = null, ) diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index ce32a16..2fd4050 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -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") } diff --git a/sdk-core/src/main/java/com/xuqm/sdk/auth/UserSigRefresher.kt b/sdk-core/src/main/java/com/xuqm/sdk/auth/UserSigRefresher.kt new file mode 100644 index 0000000..6b26171 --- /dev/null +++ b/sdk-core/src/main/java/com/xuqm/sdk/auth/UserSigRefresher.kt @@ -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 // 每分钟检查一次 + } +} 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 3274373..cee600a 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 @@ -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 = 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,27 +184,58 @@ 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 - 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) + + 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 + .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}") + } } - .addOnFailureListener { error -> - Log.w("XuqmPushSDK", "Unable to fetch FCM token: ${error.message}") + .onFailure { error -> + Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}") } } - .onFailure { error -> - Log.w("XuqmPushSDK", "Firebase Messaging not available: ${error.message}") - } + } } } diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt new file mode 100644 index 0000000..6e0ee84 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HonorPushService.kt @@ -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" + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt new file mode 100644 index 0000000..f42bee4 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/HuaweiPushService.kt @@ -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" + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt new file mode 100644 index 0000000..67cdaf4 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/OppoPushService.kt @@ -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" + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/PushVendorInterface.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/PushVendorInterface.kt new file mode 100644 index 0000000..6b32f8c --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/PushVendorInterface.kt @@ -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) +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt new file mode 100644 index 0000000..11348e6 --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/VivoPushService.kt @@ -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" + } +} diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt new file mode 100644 index 0000000..341e54d --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/XiaomiPushService.kt @@ -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" + } +}