diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt index 991a861..25d32bc 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/ImClient.kt @@ -131,6 +131,17 @@ class ImClient( ) } + private fun sendSync() { + sendFrame( + "SEND", + mapOf( + "destination" to "/app/chat.sync", + "content-type" to "application/json", + ), + gson.toJson(mapOf("appId" to appId)), + ) + } + fun addListener(listener: ImEventListener) = listeners.add(listener) fun removeListener(listener: ImEventListener) = listeners.remove(listener) @@ -187,6 +198,7 @@ class ImClient( sendSubscribe(destination, id) } } + sendSync() } "MESSAGE" -> { runCatching { diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt index ff6cf2f..e755120 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt @@ -8,7 +8,11 @@ import com.xuqm.sdk.im.api.AttributeKeysRequest import com.xuqm.sdk.im.api.CreateGroupRequest import com.xuqm.sdk.im.api.GroupReadReceiptRequest import com.xuqm.sdk.im.api.ImApi +import com.xuqm.sdk.im.model.BatchFriendRequest +import com.xuqm.sdk.im.model.BatchRequestIds +import com.xuqm.sdk.im.model.BatchUserIds import com.xuqm.sdk.im.model.EditMessageRequest +import com.xuqm.sdk.im.model.ModifyMemberInfoRequest import com.xuqm.sdk.im.api.MuteGroupMemberRequest import com.xuqm.sdk.im.api.SetGroupRoleRequest import com.xuqm.sdk.im.api.TransferOwnerRequest @@ -627,6 +631,16 @@ object ImSDK { runCatching { listConversations().sumOf { it.unreadCount } }.getOrDefault(0) } + suspend fun offlineMessageCount(): Int = + withContext(Dispatchers.IO) { + api.offlineMessageCount(XuqmSDK.appKey).data?.get("count") ?: 0 + } + + suspend fun syncOfflineMessages(): List = + withContext(Dispatchers.IO) { + api.syncOfflineMessages(XuqmSDK.appKey).data ?: emptyList() + } + suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) = withContext(Dispatchers.IO) { api.setConversationPinned(targetId, XuqmSDK.appKey, chatType, pinned) @@ -667,6 +681,33 @@ object ImSDK { api.adminGroupReadReceipts(groupId, XuqmSDK.appKey, GroupReadReceiptRequest(messageIds)).data ?: emptyList() } + suspend fun batchAddFriends(friendIds: List) = + withContext(Dispatchers.IO) { api.batchAddFriends(XuqmSDK.appKey, BatchFriendRequest(friendIds)) } + + suspend fun batchRemoveFriends(friendIds: List) = + withContext(Dispatchers.IO) { api.batchRemoveFriends(XuqmSDK.appKey, BatchFriendRequest(friendIds)) } + + suspend fun batchAcceptFriendRequests(requestIds: List) = + withContext(Dispatchers.IO) { api.batchAcceptFriendRequests(XuqmSDK.appKey, BatchRequestIds(requestIds)) } + + suspend fun batchRejectFriendRequests(requestIds: List) = + withContext(Dispatchers.IO) { api.batchRejectFriendRequests(XuqmSDK.appKey, BatchRequestIds(requestIds)) } + + suspend fun batchAddGroupMembers(groupId: String, userIds: List) = + withContext(Dispatchers.IO) { api.batchAddGroupMembers(groupId, XuqmSDK.appKey, BatchUserIds(userIds)) } + + suspend fun batchRemoveGroupMembers(groupId: String, userIds: List) = + withContext(Dispatchers.IO) { api.batchRemoveGroupMembers(groupId, XuqmSDK.appKey, BatchUserIds(userIds)) } + + suspend fun batchAcceptGroupJoinRequests(groupId: String, requestIds: List) = + withContext(Dispatchers.IO) { api.batchAcceptGroupJoinRequests(groupId, XuqmSDK.appKey, BatchRequestIds(requestIds)) } + + suspend fun batchRejectGroupJoinRequests(groupId: String, requestIds: List) = + withContext(Dispatchers.IO) { api.batchRejectGroupJoinRequests(groupId, XuqmSDK.appKey, BatchRequestIds(requestIds)) } + + suspend fun modifyGroupMemberInfo(groupId: String, userId: String, nickname: String? = null, role: String? = null) = + withContext(Dispatchers.IO) { api.modifyGroupMemberInfo(groupId, userId, XuqmSDK.appKey, ModifyMemberInfoRequest(nickname, role)) } + fun addListener(listener: ImEventListener) { Log.d(TAG, "addListener listener=${listener.javaClass.name}") listeners.add(listener) diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt index c06d2ff..492fb38 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/api/ImApi.kt @@ -1,6 +1,9 @@ package com.xuqm.sdk.im.api import com.xuqm.sdk.im.model.ConversationData +import com.xuqm.sdk.im.model.BatchFriendRequest +import com.xuqm.sdk.im.model.BatchRequestIds +import com.xuqm.sdk.im.model.BatchUserIds import com.xuqm.sdk.im.model.BlacklistCheckResult import com.xuqm.sdk.im.model.BlacklistEntry import com.xuqm.sdk.im.model.ConversationGroupItem @@ -10,6 +13,7 @@ import com.xuqm.sdk.im.model.GroupReadReceiptSummary import com.xuqm.sdk.im.model.ImGroup import com.xuqm.sdk.im.model.GroupJoinRequest import com.xuqm.sdk.im.model.ImMessage +import com.xuqm.sdk.im.model.ModifyMemberInfoRequest import com.xuqm.sdk.im.model.PageResult import com.xuqm.sdk.im.model.UserProfile import retrofit2.http.Body @@ -400,4 +404,37 @@ interface ImApi { @Query("appId") appId: String, @Body request: GroupReadReceiptRequest, ): ApiResponse> + + @POST("api/im/friends/batch") + suspend fun batchAddFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse + + @POST("api/im/friends/batch/remove") + suspend fun batchRemoveFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse + + @POST("api/im/friend-requests/batch/accept") + suspend fun batchAcceptFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + + @POST("api/im/friend-requests/batch/reject") + suspend fun batchRejectFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + + @POST("api/im/groups/{groupId}/members/batch") + suspend fun batchAddGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse + + @POST("api/im/groups/{groupId}/members/batch/remove") + suspend fun batchRemoveGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse + + @POST("api/im/groups/{groupId}/join-requests/batch/accept") + suspend fun batchAcceptGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + + @POST("api/im/groups/{groupId}/join-requests/batch/reject") + suspend fun batchRejectGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse + + @PUT("api/im/groups/{groupId}/members/{userId}/info") + suspend fun modifyGroupMemberInfo(@Path("groupId") groupId: String, @Path("userId") userId: String, @Query("appId") appId: String, @Body request: ModifyMemberInfoRequest): ApiResponse + + @GET("api/im/messages/offline/count") + suspend fun offlineMessageCount(@Query("appId") appId: String): ApiResponse> + + @POST("api/im/messages/offline") + suspend fun syncOfflineMessages(@Query("appId") appId: String): ApiResponse> } diff --git a/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt b/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt index 52775a7..161a0f8 100644 --- a/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt +++ b/sdk-im/src/main/java/com/xuqm/sdk/im/model/ImMessage.kt @@ -117,6 +117,14 @@ data class GroupReadReceiptSummary( val unreadCount: Int, ) +data class BatchFriendRequest(val friendIds: List) + +data class BatchRequestIds(val requestIds: List) + +data class BatchUserIds(val userIds: List) + +data class ModifyMemberInfoRequest(val nickname: String? = null, val role: String? = null) + enum class ChatType { SINGLE, GROUP } enum class MsgType { diff --git a/sdk-push/build.gradle.kts b/sdk-push/build.gradle.kts index 1a49f6a..48a7a55 100644 --- a/sdk-push/build.gradle.kts +++ b/sdk-push/build.gradle.kts @@ -27,4 +27,12 @@ dependencies { api(project(":sdk-core")) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) + + // Optional vendor push SDKs — add the ones you need in your app module. + // These are NOT declared here because they require proprietary Maven repos: + // Huawei: com.huawei.hms:push (via Huawei Maven repo) + // Xiaomi: com.xiaomi.mipush:mipush (via Xiaomi Maven repo) + // OPPO: com.heytap.mcs:push (via OPPO Maven repo) + // vivo: com.vivo.pushsdk:pushsdk (via vivo Maven repo) + // Honor: com.hihonor.mcs:push (via Honor Maven repo) } 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 cee600a..aff63b2 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 @@ -12,6 +12,7 @@ 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.FcmPushService import com.xuqm.sdk.push.vendor.HonorPushService import com.xuqm.sdk.push.vendor.HuaweiPushService import com.xuqm.sdk.push.vendor.OppoPushService @@ -37,7 +38,7 @@ object PushSDK { return store(context).load(deviceId = deviceId, fallbackVendor = detectVendor()) } - internal fun updateNativePushToken( + fun updateNativePushToken( context: Context, vendor: PushVendor, pushToken: String, @@ -121,7 +122,11 @@ object PushSDK { XuqmSDK.requireInit() scope.launch { runCatching { - api.unregisterDevice(XuqmSDK.appKey, userId) + val reg = store(XuqmSDK.appContext).load( + deviceId = DeviceUtils.getDeviceId(XuqmSDK.appContext), + fallbackVendor = detectVendor(), + ) + api.unregisterDevice(XuqmSDK.appKey, userId, reg?.vendor?.name ?: detectVendor().name) registeredUserId.compareAndSet(userId, null) store(XuqmSDK.appContext).updateLastUserId(null) } @@ -134,6 +139,7 @@ object PushSDK { OppoPushService(), VivoPushService(), HonorPushService(), + FcmPushService(), ) fun detectVendor(): PushVendor { diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt index 07dd1b9..621be87 100644 --- a/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/api/PushApi.kt @@ -14,10 +14,11 @@ interface PushApi { @Query("token") token: String, ) - @DELETE("api/push/device/unregister") + @DELETE("api/push/unregister") suspend fun unregisterDevice( @Query("appId") appId: String, @Query("userId") userId: String, + @Query("vendor") vendor: String, ) @POST("api/push/receive-push") diff --git a/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt new file mode 100644 index 0000000..d7853af --- /dev/null +++ b/sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt @@ -0,0 +1,70 @@ +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 + +/** + * FCM (Firebase Cloud Messaging) push integration. + * + * FCM token is obtained automatically via [com.xuqm.sdk.push.fcm.XuqmFirebaseMessagingService]. + * This service only ensures FCM is available and requests a token refresh + * when [register] is called explicitly. + */ +class FcmPushService : PushVendorInterface { + + override val vendor: PushVendor = PushVendor.FCM + + override fun isAvailable(context: Context): Boolean { + return runCatching { + Class.forName("com.google.firebase.messaging.FirebaseMessaging") + true + }.getOrDefault(false) + } + + override fun register(context: Context) { + runCatching { + val fcmClass = Class.forName("com.google.firebase.messaging.FirebaseMessaging") + val instance = fcmClass.getMethod("getInstance").invoke(null) + fcmClass.getMethod("getToken") + .invoke(instance) + ?.let { task -> + val addOnSuccess = task::class.java.getMethod( + "addOnSuccessListener", + Class.forName("com.google.android.gms.tasks.OnSuccessListener"), + ) + val proxy = java.lang.reflect.Proxy.newProxyInstance( + context.classLoader, + arrayOf(Class.forName("com.google.android.gms.tasks.OnSuccessListener")) + ) { _, _, args -> + val token = args?.getOrNull(0) as? String + if (!token.isNullOrBlank()) { + PushSDK.updateNativePushToken(context, vendor, token) + Log.i(TAG, "FCM token acquired") + } + null + } + addOnSuccess.invoke(task, proxy) + } + Log.i(TAG, "FCM token refresh requested") + }.onFailure { error -> + Log.w(TAG, "FCM token refresh failed: ${error.message}") + } + } + + override fun unregister(context: Context) { + runCatching { + val fcmClass = Class.forName("com.google.firebase.messaging.FirebaseMessaging") + val instance = fcmClass.getMethod("getInstance").invoke(null) + fcmClass.getMethod("deleteToken").invoke(instance) + Log.i(TAG, "FCM token deleted") + }.onFailure { error -> + Log.w(TAG, "FCM unregistration failed: ${error.message}") + } + } + + companion object { + private const val TAG = "FcmPushService" + } +} 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 index 67cdaf4..7a00274 100644 --- 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 @@ -1,9 +1,12 @@ package com.xuqm.sdk.push.vendor import android.content.Context +import android.content.pm.PackageManager +import android.os.Bundle import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor +import java.lang.reflect.Proxy /** * OPPO 推送集成框架 @@ -13,11 +16,10 @@ import com.xuqm.sdk.push.model.PushVendor * implementation 'com.heytap.mcs:push:3.x.x' * ``` * - * 接入方需在 `Application.onCreate()` 中调用 `PushManager.getInstance().register()`, - * 并在自定义 `PushCallback` 的 `onRegister()` 回调中获取 token, - * 然后调用: - * ```kotlin - * PushSDK.updateNativePushToken(context, PushVendor.OPPO, token) + * 在 `AndroidManifest.xml` 的 `` 下配置: + * ```xml + * + * * ``` */ class OppoPushService : PushVendorInterface { @@ -35,9 +37,41 @@ class OppoPushService : PushVendorInterface { 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") + + val meta = context.packageManager.getApplicationInfo( + context.packageName, PackageManager.GET_META_DATA + ).metaData ?: Bundle.EMPTY + val appKey = meta.getString("XUQM_OPPO_APP_KEY", "") + val appSecret = meta.getString("XUQM_OPPO_APP_SECRET", "") + + if (appKey.isNotBlank() && appSecret.isNotBlank()) { + val callbackClass = Class.forName("com.heytap.mcssdk.callback.PushCallback") + val proxy = Proxy.newProxyInstance( + callbackClass.classLoader, + arrayOf(callbackClass) + ) { _, method, args -> + when (method.name) { + "onRegister" -> { + val regId = args?.getOrNull(1) as? String + if (!regId.isNullOrBlank()) { + PushSDK.updateNativePushToken(context, vendor, regId) + Log.i(TAG, "OPPO push token acquired") + } + } + } + null + } + pushManagerClass.getMethod( + "register", + Context::class.java, + String::class.java, + String::class.java, + callbackClass, + ).invoke(instance, context, appKey, appSecret, proxy) + Log.i(TAG, "OPPO push registration requested") + } else { + Log.w(TAG, "OPPO appKey/appSecret not configured in meta-data, skipping registration") + } }.onFailure { error -> Log.w(TAG, "OPPO push registration failed: ${error.message}") } 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 index 341e54d..389199a 100644 --- 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 @@ -1,6 +1,8 @@ package com.xuqm.sdk.push.vendor import android.content.Context +import android.content.pm.PackageManager +import android.os.Bundle import android.util.Log import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.model.PushVendor @@ -13,7 +15,12 @@ import com.xuqm.sdk.push.model.PushVendor * implementation 'com.xiaomi.mipush:mipush:5.x.x' * ``` * - * 接入方需在 `Application.onCreate()` 中调用 `MiPushClient.registerPush()`, + * 在 `AndroidManifest.xml` 的 `` 下配置: + * ```xml + * + * + * ``` + * * 并在自定义 `PushMessageReceiver` 的 `onReceiveRegisterResult()` 回调中获取 token, * 然后调用: * ```kotlin @@ -34,10 +41,11 @@ class XiaomiPushService : PushVendorInterface { 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 = "" + val meta = context.packageManager.getApplicationInfo( + context.packageName, PackageManager.GET_META_DATA + ).metaData ?: Bundle.EMPTY + val appId = meta.getString("XUQM_XIAOMI_APP_ID", "") + val appKey = meta.getString("XUQM_XIAOMI_APP_KEY", "") if (appId.isNotBlank() && appKey.isNotBlank()) { miPushClass.getMethod( "registerPush", @@ -47,7 +55,7 @@ class XiaomiPushService : PushVendorInterface { ).invoke(null, context, appId, appKey) Log.i(TAG, "Xiaomi push registration requested") } else { - Log.w(TAG, "Xiaomi appId/appKey not configured, skipping registration") + Log.w(TAG, "Xiaomi appId/appKey not configured in meta-data, skipping registration") } }.onFailure { error -> Log.w(TAG, "Xiaomi push registration failed: ${error.message}")