feat(android-sdk): 添加完整的IM客户端SDK实现
- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能 - 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力 - 实现了群组管理功能,包括创建、成员管理、权限设置等操作 - 添加了好友关系链管理,支持添加、删除、分组等操作 - 实现了会话管理功能,包括置顶、免打扰、已读状态等 - 添加了黑名单、资料管理、搜索等辅助功能 - 补齐了批量操作接口,提升客户端操作效率 - 实现了WebSocket连接管理和事件监听机制 - 添加了离线消息同步和状态管理功能
这个提交包含在:
父节点
cfd0382ba2
当前提交
d9c9e4f858
@ -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")
|
||||
|
||||
70
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt
vendored
普通文件
70
sdk-push/src/main/java/com/xuqm/sdk/push/vendor/FcmPushService.kt
vendored
普通文件
@ -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
|
||||
|
||||
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}")
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户