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 addListener(listener: ImEventListener) = listeners.add(listener)
fun removeListener(listener: ImEventListener) = listeners.remove(listener) fun removeListener(listener: ImEventListener) = listeners.remove(listener)
@ -187,6 +198,7 @@ class ImClient(
sendSubscribe(destination, id) sendSubscribe(destination, id)
} }
} }
sendSync()
} }
"MESSAGE" -> { "MESSAGE" -> {
runCatching { 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.CreateGroupRequest
import com.xuqm.sdk.im.api.GroupReadReceiptRequest import com.xuqm.sdk.im.api.GroupReadReceiptRequest
import com.xuqm.sdk.im.api.ImApi 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.EditMessageRequest
import com.xuqm.sdk.im.model.ModifyMemberInfoRequest
import com.xuqm.sdk.im.api.MuteGroupMemberRequest import com.xuqm.sdk.im.api.MuteGroupMemberRequest
import com.xuqm.sdk.im.api.SetGroupRoleRequest import com.xuqm.sdk.im.api.SetGroupRoleRequest
import com.xuqm.sdk.im.api.TransferOwnerRequest import com.xuqm.sdk.im.api.TransferOwnerRequest
@ -627,6 +631,16 @@ object ImSDK {
runCatching { listConversations().sumOf { it.unreadCount } }.getOrDefault(0) 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) = suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
api.setConversationPinned(targetId, XuqmSDK.appKey, chatType, pinned) api.setConversationPinned(targetId, XuqmSDK.appKey, chatType, pinned)
@ -667,6 +681,33 @@ object ImSDK {
api.adminGroupReadReceipts(groupId, XuqmSDK.appKey, GroupReadReceiptRequest(messageIds)).data ?: emptyList() 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) { fun addListener(listener: ImEventListener) {
Log.d(TAG, "addListener listener=${listener.javaClass.name}") Log.d(TAG, "addListener listener=${listener.javaClass.name}")
listeners.add(listener) listeners.add(listener)

查看文件

@ -1,6 +1,9 @@
package com.xuqm.sdk.im.api package com.xuqm.sdk.im.api
import com.xuqm.sdk.im.model.ConversationData 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.BlacklistCheckResult
import com.xuqm.sdk.im.model.BlacklistEntry import com.xuqm.sdk.im.model.BlacklistEntry
import com.xuqm.sdk.im.model.ConversationGroupItem 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.ImGroup
import com.xuqm.sdk.im.model.GroupJoinRequest import com.xuqm.sdk.im.model.GroupJoinRequest
import com.xuqm.sdk.im.model.ImMessage 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.PageResult
import com.xuqm.sdk.im.model.UserProfile import com.xuqm.sdk.im.model.UserProfile
import retrofit2.http.Body import retrofit2.http.Body
@ -400,4 +404,37 @@ interface ImApi {
@Query("appId") appId: String, @Query("appId") appId: String,
@Body request: GroupReadReceiptRequest, @Body request: GroupReadReceiptRequest,
): ApiResponse<List<GroupReadReceiptSummary>> ): 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, 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 ChatType { SINGLE, GROUP }
enum class MsgType { enum class MsgType {

查看文件

@ -27,4 +27,12 @@ dependencies {
api(project(":sdk-core")) api(project(":sdk-core"))
implementation(platform(libs.firebase.bom)) implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging) 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.PushRegistrationSnapshot
import com.xuqm.sdk.push.model.PushVendor import com.xuqm.sdk.push.model.PushVendor
import com.xuqm.sdk.push.storage.PushRegistrationStore import com.xuqm.sdk.push.storage.PushRegistrationStore
import com.xuqm.sdk.push.vendor.FcmPushService
import com.xuqm.sdk.push.vendor.HonorPushService import com.xuqm.sdk.push.vendor.HonorPushService
import com.xuqm.sdk.push.vendor.HuaweiPushService import com.xuqm.sdk.push.vendor.HuaweiPushService
import com.xuqm.sdk.push.vendor.OppoPushService import com.xuqm.sdk.push.vendor.OppoPushService
@ -37,7 +38,7 @@ object PushSDK {
return store(context).load(deviceId = deviceId, fallbackVendor = detectVendor()) return store(context).load(deviceId = deviceId, fallbackVendor = detectVendor())
} }
internal fun updateNativePushToken( fun updateNativePushToken(
context: Context, context: Context,
vendor: PushVendor, vendor: PushVendor,
pushToken: String, pushToken: String,
@ -121,7 +122,11 @@ object PushSDK {
XuqmSDK.requireInit() XuqmSDK.requireInit()
scope.launch { scope.launch {
runCatching { 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) registeredUserId.compareAndSet(userId, null)
store(XuqmSDK.appContext).updateLastUserId(null) store(XuqmSDK.appContext).updateLastUserId(null)
} }
@ -134,6 +139,7 @@ object PushSDK {
OppoPushService(), OppoPushService(),
VivoPushService(), VivoPushService(),
HonorPushService(), HonorPushService(),
FcmPushService(),
) )
fun detectVendor(): PushVendor { fun detectVendor(): PushVendor {

查看文件

@ -14,10 +14,11 @@ interface PushApi {
@Query("token") token: String, @Query("token") token: String,
) )
@DELETE("api/push/device/unregister") @DELETE("api/push/unregister")
suspend fun unregisterDevice( suspend fun unregisterDevice(
@Query("appId") appId: String, @Query("appId") appId: String,
@Query("userId") userId: String, @Query("userId") userId: String,
@Query("vendor") vendor: String,
) )
@POST("api/push/receive-push") @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 package com.xuqm.sdk.push.vendor
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log import android.util.Log
import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.PushSDK
import com.xuqm.sdk.push.model.PushVendor import com.xuqm.sdk.push.model.PushVendor
import java.lang.reflect.Proxy
/** /**
* OPPO 推送集成框架 * OPPO 推送集成框架
@ -13,11 +16,10 @@ import com.xuqm.sdk.push.model.PushVendor
* implementation 'com.heytap.mcs:push:3.x.x' * implementation 'com.heytap.mcs:push:3.x.x'
* ``` * ```
* *
* 接入方需在 `Application.onCreate()` 中调用 `PushManager.getInstance().register()` * `AndroidManifest.xml` `<application>` 下配置
* 并在自定义 `PushCallback` `onRegister()` 回调中获取 token * ```xml
* 然后调用 * <meta-data android:name="XUQM_OPPO_APP_KEY" android:value="xxxxxxxx" />
* ```kotlin * <meta-data android:name="XUQM_OPPO_APP_SECRET" android:value="xxxxxxxx" />
* PushSDK.updateNativePushToken(context, PushVendor.OPPO, token)
* ``` * ```
*/ */
class OppoPushService : PushVendorInterface { class OppoPushService : PushVendorInterface {
@ -35,9 +37,41 @@ class OppoPushService : PushVendorInterface {
runCatching { runCatching {
val pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager") val pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager")
val instance = pushManagerClass.getMethod("getInstance").invoke(null) val instance = pushManagerClass.getMethod("getInstance").invoke(null)
// PushManager.getInstance().register(context, appKey, appSecret, pushCallback)
// 需要业务层配置 AppKey 和 AppSecret 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") Log.i(TAG, "OPPO push registration requested")
} else {
Log.w(TAG, "OPPO appKey/appSecret not configured in meta-data, skipping registration")
}
}.onFailure { error -> }.onFailure { error ->
Log.w(TAG, "OPPO push registration failed: ${error.message}") Log.w(TAG, "OPPO push registration failed: ${error.message}")
} }

查看文件

@ -1,6 +1,8 @@
package com.xuqm.sdk.push.vendor package com.xuqm.sdk.push.vendor
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log import android.util.Log
import com.xuqm.sdk.push.PushSDK import com.xuqm.sdk.push.PushSDK
import com.xuqm.sdk.push.model.PushVendor 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' * 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 * 并在自定义 `PushMessageReceiver` `onReceiveRegisterResult()` 回调中获取 token
* 然后调用 * 然后调用
* ```kotlin * ```kotlin
@ -34,10 +41,11 @@ class XiaomiPushService : PushVendorInterface {
override fun register(context: Context) { override fun register(context: Context) {
runCatching { runCatching {
val miPushClass = Class.forName("com.xiaomi.mipush.sdk.MiPushClient") val miPushClass = Class.forName("com.xiaomi.mipush.sdk.MiPushClient")
// MiPushClient.registerPush(context, appId, appKey) val meta = context.packageManager.getApplicationInfo(
// 需要业务层配置 AppID 和 AppKey,此处仅做框架调用示例 context.packageName, PackageManager.GET_META_DATA
val appId = "" ).metaData ?: Bundle.EMPTY
val appKey = "" val appId = meta.getString("XUQM_XIAOMI_APP_ID", "")
val appKey = meta.getString("XUQM_XIAOMI_APP_KEY", "")
if (appId.isNotBlank() && appKey.isNotBlank()) { if (appId.isNotBlank() && appKey.isNotBlank()) {
miPushClass.getMethod( miPushClass.getMethod(
"registerPush", "registerPush",
@ -47,7 +55,7 @@ class XiaomiPushService : PushVendorInterface {
).invoke(null, context, appId, appKey) ).invoke(null, context, appId, appKey)
Log.i(TAG, "Xiaomi push registration requested") Log.i(TAG, "Xiaomi push registration requested")
} else { } 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 -> }.onFailure { error ->
Log.w(TAG, "Xiaomi push registration failed: ${error.message}") Log.w(TAG, "Xiaomi push registration failed: ${error.message}")