feat(android-sdk): 添加完整的IM客户端SDK实现

- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能
- 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力
- 实现了群组管理功能,包括创建、成员管理、权限设置等操作
- 添加了好友关系链管理,支持添加、删除、分组等操作
- 实现了会话管理功能,包括置顶、免打扰、已读状态等
- 添加了黑名单、资料管理、搜索等辅助功能
- 补齐了批量操作接口,提升客户端操作效率
- 实现了WebSocket连接管理和事件监听机制
- 添加了离线消息同步和状态管理功能
这个提交包含在:
XuqmGroup 2026-05-02 22:57:55 +08:00
父节点 cfd0382ba2
当前提交 d9c9e4f858
共有 10 个文件被更改,包括 242 次插入17 次删除

查看文件

@ -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 {

查看文件

@ -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<ImMessage> =
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<String>) =
withContext(Dispatchers.IO) { api.batchAddFriends(XuqmSDK.appKey, BatchFriendRequest(friendIds)) }
suspend fun batchRemoveFriends(friendIds: List<String>) =
withContext(Dispatchers.IO) { api.batchRemoveFriends(XuqmSDK.appKey, BatchFriendRequest(friendIds)) }
suspend fun batchAcceptFriendRequests(requestIds: List<String>) =
withContext(Dispatchers.IO) { api.batchAcceptFriendRequests(XuqmSDK.appKey, BatchRequestIds(requestIds)) }
suspend fun batchRejectFriendRequests(requestIds: List<String>) =
withContext(Dispatchers.IO) { api.batchRejectFriendRequests(XuqmSDK.appKey, BatchRequestIds(requestIds)) }
suspend fun batchAddGroupMembers(groupId: String, userIds: List<String>) =
withContext(Dispatchers.IO) { api.batchAddGroupMembers(groupId, XuqmSDK.appKey, BatchUserIds(userIds)) }
suspend fun batchRemoveGroupMembers(groupId: String, userIds: List<String>) =
withContext(Dispatchers.IO) { api.batchRemoveGroupMembers(groupId, XuqmSDK.appKey, BatchUserIds(userIds)) }
suspend fun batchAcceptGroupJoinRequests(groupId: String, requestIds: List<String>) =
withContext(Dispatchers.IO) { api.batchAcceptGroupJoinRequests(groupId, XuqmSDK.appKey, BatchRequestIds(requestIds)) }
suspend fun batchRejectGroupJoinRequests(groupId: String, requestIds: List<String>) =
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)

查看文件

@ -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<List<GroupReadReceiptSummary>>
@POST("api/im/friends/batch")
suspend fun batchAddFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse<Unit>
@POST("api/im/friends/batch/remove")
suspend fun batchRemoveFriends(@Query("appId") appId: String, @Body request: BatchFriendRequest): ApiResponse<Unit>
@POST("api/im/friend-requests/batch/accept")
suspend fun batchAcceptFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@POST("api/im/friend-requests/batch/reject")
suspend fun batchRejectFriendRequests(@Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/members/batch")
suspend fun batchAddGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/members/batch/remove")
suspend fun batchRemoveGroupMembers(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchUserIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/join-requests/batch/accept")
suspend fun batchAcceptGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@POST("api/im/groups/{groupId}/join-requests/batch/reject")
suspend fun batchRejectGroupJoinRequests(@Path("groupId") groupId: String, @Query("appId") appId: String, @Body request: BatchRequestIds): ApiResponse<Unit>
@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<Unit>
@GET("api/im/messages/offline/count")
suspend fun offlineMessageCount(@Query("appId") appId: String): ApiResponse<Map<String, Int>>
@POST("api/im/messages/offline")
suspend fun syncOfflineMessages(@Query("appId") appId: String): ApiResponse<List<ImMessage>>
}

查看文件

@ -117,6 +117,14 @@ data class GroupReadReceiptSummary(
val unreadCount: Int,
)
data class BatchFriendRequest(val friendIds: List<String>)
data class BatchRequestIds(val requestIds: List<String>)
data class BatchUserIds(val userIds: List<String>)
data class ModifyMemberInfoRequest(val nickname: String? = null, val role: String? = null)
enum class ChatType { SINGLE, GROUP }
enum class MsgType {

查看文件

@ -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)
}

查看文件

@ -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 {

查看文件

@ -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")

查看文件

@ -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"
}
}

查看文件

@ -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` `<application>` 下配置
* ```xml
* <meta-data android:name="XUQM_OPPO_APP_KEY" android:value="xxxxxxxx" />
* <meta-data android:name="XUQM_OPPO_APP_SECRET" android:value="xxxxxxxx" />
* ```
*/
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}")
}

查看文件

@ -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` `<application>` 下配置
* ```xml
* <meta-data android:name="XUQM_XIAOMI_APP_ID" android:value="288230376xxxxxxxx" />
* <meta-data android:name="XUQM_XIAOMI_APP_KEY" android:value="xxxxxxxxxxxx" />
* ```
*
* 并在自定义 `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}")