docs(sdk): 添加 Android SDK 文档和 API 设计规范
- 新增 Android SDK 使用文档,包含模块结构、集成方式和快速开始指南 - 添加 SDK API 重设计规范,统一初始化和登录接口设计 - 补充安全设计规范,完善 UserSig 鉴权和敏感数据处理方案 - 创建平台 REST API 规范,定义服务端到服务端的调用接口 - 添加离线推送架构设计,集成各大厂商推送服务与 IM 联动方案
这个提交包含在:
父节点
48ddea9f68
当前提交
a1614840e5
@ -55,7 +55,7 @@ dependencies {
|
|||||||
```kotlin
|
```kotlin
|
||||||
XuqmSDK.initialize(
|
XuqmSDK.initialize(
|
||||||
context = this,
|
context = this,
|
||||||
appId = "ak_your_app_id",
|
appKey = "ak_your_app_key",
|
||||||
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN
|
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -102,7 +102,7 @@ sample-app 里也提供了一个“环境设置”页面,可以直接在外网
|
|||||||
|
|
||||||
| 参数 | 类型 | 说明 |
|
| 参数 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `appId` | String | 应用标识(租户平台获取) |
|
| `appKey` | String | 应用标识(租户平台获取) |
|
||||||
| `logLevel` | LogLevel | 日志等级 |
|
| `logLevel` | LogLevel | 日志等级 |
|
||||||
|
|
||||||
### TokenStore
|
### TokenStore
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
|
import org.gradle.api.publish.PublishingExtension
|
||||||
|
|
||||||
apply(plugin = "maven-publish")
|
apply(plugin = "maven-publish")
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
(extensions.findByType(com.android.build.gradle.LibraryExtension::class.java))?.let {
|
if (extensions.findByName("android") != null) {
|
||||||
|
val releaseComponent = components.findByName("release") ?: return@afterEvaluate
|
||||||
extensions.configure<PublishingExtension> {
|
extensions.configure<PublishingExtension> {
|
||||||
publications {
|
publications {
|
||||||
register<MavenPublication>("release") {
|
register<MavenPublication>("release") {
|
||||||
from(components["release"])
|
from(releaseComponent)
|
||||||
groupId = rootProject.group.toString()
|
groupId = rootProject.group.toString()
|
||||||
artifactId = project.name
|
artifactId = project.name
|
||||||
version = rootProject.version.toString()
|
version = rootProject.version.toString()
|
||||||
@ -13,10 +16,10 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
url = uri(rootProject.ext["nexusUrl"] as String)
|
url = uri(rootProject.extra["nexusUrl"] as String)
|
||||||
credentials {
|
credentials {
|
||||||
username = rootProject.ext["nexusUser"] as String
|
username = rootProject.extra["nexusUser"] as String
|
||||||
password = rootProject.ext["nexusPassword"] as String
|
password = rootProject.extra["nexusPassword"] as String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class XuqmSampleApp : Application() {
|
|||||||
AppDependencies.init(this)
|
AppDependencies.init(this)
|
||||||
XuqmSDK.initialize(
|
XuqmSDK.initialize(
|
||||||
context = this,
|
context = this,
|
||||||
appId = "ak_demo_chat",
|
appKey = "ak_demo_chat",
|
||||||
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
|
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.WARN,
|
||||||
)
|
)
|
||||||
appScope.launch {
|
appScope.launch {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package com.xuqm.sdk
|
package com.xuqm.sdk
|
||||||
|
|
||||||
data class XuqmLoginSession(
|
data class XuqmLoginSession(
|
||||||
val appId: String,
|
val appKey: String,
|
||||||
val userId: String,
|
val userId: String,
|
||||||
val userSig: String,
|
val userSig: String,
|
||||||
val nickname: String? = null,
|
val nickname: String? = null,
|
||||||
|
|||||||
@ -27,10 +27,10 @@ object XuqmSDK {
|
|||||||
|
|
||||||
fun initialize(
|
fun initialize(
|
||||||
context: Context,
|
context: Context,
|
||||||
appId: String,
|
appKey: String,
|
||||||
logLevel: LogLevel = LogLevel.WARN,
|
logLevel: LogLevel = LogLevel.WARN,
|
||||||
) {
|
) {
|
||||||
config = SDKConfig(appId, logLevel)
|
config = SDKConfig(appKey, logLevel)
|
||||||
appContext = context.applicationContext
|
appContext = context.applicationContext
|
||||||
tokenStore = TokenStore(context.applicationContext)
|
tokenStore = TokenStore(context.applicationContext)
|
||||||
ApiClient.init(config, tokenStore)
|
ApiClient.init(config, tokenStore)
|
||||||
@ -55,7 +55,7 @@ object XuqmSDK {
|
|||||||
check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." }
|
check(initialized) { "XuqmSDK not initialized. Call XuqmSDK.initialize() first." }
|
||||||
}
|
}
|
||||||
|
|
||||||
val appId: String get() = config.appId
|
val appKey: String get() = config.appKey
|
||||||
|
|
||||||
val currentLoginSession: XuqmLoginSession?
|
val currentLoginSession: XuqmLoginSession?
|
||||||
get() = loginSession
|
get() = loginSession
|
||||||
@ -68,7 +68,7 @@ object XuqmSDK {
|
|||||||
): XuqmLoginSession = withContext(Dispatchers.IO) {
|
): XuqmLoginSession = withContext(Dispatchers.IO) {
|
||||||
requireInit()
|
requireInit()
|
||||||
val session = XuqmLoginSession(
|
val session = XuqmLoginSession(
|
||||||
appId = appId,
|
appKey = appKey,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
userSig = userSig,
|
userSig = userSig,
|
||||||
nickname = nickname,
|
nickname = nickname,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package com.xuqm.sdk.core
|
package com.xuqm.sdk.core
|
||||||
|
|
||||||
data class SDKConfig(
|
data class SDKConfig(
|
||||||
val appId: String,
|
val appKey: String,
|
||||||
val logLevel: LogLevel = LogLevel.WARN,
|
val logLevel: LogLevel = LogLevel.WARN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -144,10 +144,10 @@ object ImSDK {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun editMessage(messageId: String, content: String): ImMessage =
|
suspend fun editMessage(messageId: String, content: String): ImMessage =
|
||||||
withContext(Dispatchers.IO) { api.editMessage(messageId, XuqmSDK.appId, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") }
|
withContext(Dispatchers.IO) { api.editMessage(messageId, XuqmSDK.appKey, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") }
|
||||||
|
|
||||||
suspend fun revokeMessage(messageId: String): ImMessage =
|
suspend fun revokeMessage(messageId: String): ImMessage =
|
||||||
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appId).data ?: throw IllegalStateException("revoke message failed") }
|
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appKey).data ?: throw IllegalStateException("revoke message failed") }
|
||||||
|
|
||||||
fun sendImageMessage(
|
fun sendImageMessage(
|
||||||
toId: String,
|
toId: String,
|
||||||
@ -407,7 +407,7 @@ object ImSDK {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
api.fetchHistory(
|
api.fetchHistory(
|
||||||
toId,
|
toId,
|
||||||
XuqmSDK.appId,
|
XuqmSDK.appKey,
|
||||||
msgType,
|
msgType,
|
||||||
keyword,
|
keyword,
|
||||||
startTime?.toString(),
|
startTime?.toString(),
|
||||||
@ -432,7 +432,7 @@ object ImSDK {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
api.fetchGroupHistory(
|
api.fetchGroupHistory(
|
||||||
groupId,
|
groupId,
|
||||||
XuqmSDK.appId,
|
XuqmSDK.appKey,
|
||||||
msgType,
|
msgType,
|
||||||
keyword,
|
keyword,
|
||||||
startTime?.toString(),
|
startTime?.toString(),
|
||||||
@ -485,30 +485,30 @@ object ImSDK {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun listGroups(): List<ImGroup> =
|
suspend fun listGroups(): List<ImGroup> =
|
||||||
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appKey).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun createGroup(name: String, memberIds: List<String>, groupType: String = "WORK"): ImGroup? =
|
suspend fun createGroup(name: String, memberIds: List<String>, groupType: String = "WORK"): ImGroup? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
api.createGroup(XuqmSDK.appId, CreateGroupRequest(name, memberIds, groupType)).data
|
api.createGroup(XuqmSDK.appKey, CreateGroupRequest(name, memberIds, groupType)).data
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGroupInfo(groupId: String): ImGroup? =
|
suspend fun getGroupInfo(groupId: String): ImGroup? =
|
||||||
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
|
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
|
||||||
|
|
||||||
suspend fun listGroupMembers(groupId: String): List<UserProfile> =
|
suspend fun listGroupMembers(groupId: String): List<UserProfile> =
|
||||||
withContext(Dispatchers.IO) { api.listGroupMembers(groupId, XuqmSDK.appId).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listGroupMembers(groupId, XuqmSDK.appKey).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun searchGroupMembers(groupId: String, keyword: String, size: Int = 20): List<UserProfile> =
|
suspend fun searchGroupMembers(groupId: String, keyword: String, size: Int = 20): List<UserProfile> =
|
||||||
withContext(Dispatchers.IO) { api.searchGroupMembers(groupId, XuqmSDK.appId, keyword, size).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.searchGroupMembers(groupId, XuqmSDK.appKey, keyword, size).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun listPublicGroups(keyword: String? = null): List<ImGroup> =
|
suspend fun listPublicGroups(keyword: String? = null): List<ImGroup> =
|
||||||
withContext(Dispatchers.IO) { api.listPublicGroups(XuqmSDK.appId, keyword).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listPublicGroups(XuqmSDK.appKey, keyword).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun searchUsers(keyword: String, size: Int = 20): List<UserProfile> =
|
suspend fun searchUsers(keyword: String, size: Int = 20): List<UserProfile> =
|
||||||
withContext(Dispatchers.IO) { api.searchUsers(XuqmSDK.appId, keyword, size).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.searchUsers(XuqmSDK.appKey, keyword, size).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun searchGroups(keyword: String, size: Int = 20): List<ImGroup> =
|
suspend fun searchGroups(keyword: String, size: Int = 20): List<ImGroup> =
|
||||||
withContext(Dispatchers.IO) { api.searchGroups(XuqmSDK.appId, keyword, size).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.searchGroups(XuqmSDK.appKey, keyword, size).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun searchMessages(
|
suspend fun searchMessages(
|
||||||
keyword: String? = null,
|
keyword: String? = null,
|
||||||
@ -521,7 +521,7 @@ object ImSDK {
|
|||||||
): PageResult<ImMessage> =
|
): PageResult<ImMessage> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
api.searchMessages(
|
api.searchMessages(
|
||||||
XuqmSDK.appId,
|
XuqmSDK.appKey,
|
||||||
keyword,
|
keyword,
|
||||||
chatType,
|
chatType,
|
||||||
msgType,
|
msgType,
|
||||||
@ -554,49 +554,49 @@ object ImSDK {
|
|||||||
withContext(Dispatchers.IO) { api.dismissGroup(groupId) }
|
withContext(Dispatchers.IO) { api.dismissGroup(groupId) }
|
||||||
|
|
||||||
suspend fun sendGroupJoinRequest(groupId: String, remark: String? = null): GroupJoinRequest? =
|
suspend fun sendGroupJoinRequest(groupId: String, remark: String? = null): GroupJoinRequest? =
|
||||||
withContext(Dispatchers.IO) { api.sendGroupJoinRequest(groupId, XuqmSDK.appId, remark).data }
|
withContext(Dispatchers.IO) { api.sendGroupJoinRequest(groupId, XuqmSDK.appKey, remark).data }
|
||||||
|
|
||||||
suspend fun listGroupJoinRequests(groupId: String): List<GroupJoinRequest> =
|
suspend fun listGroupJoinRequests(groupId: String): List<GroupJoinRequest> =
|
||||||
withContext(Dispatchers.IO) { api.listGroupJoinRequests(groupId, XuqmSDK.appId).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listGroupJoinRequests(groupId, XuqmSDK.appKey).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun acceptGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
|
suspend fun acceptGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
|
||||||
withContext(Dispatchers.IO) { api.acceptGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data }
|
withContext(Dispatchers.IO) { api.acceptGroupJoinRequest(groupId, requestId, XuqmSDK.appKey).data }
|
||||||
|
|
||||||
suspend fun rejectGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
|
suspend fun rejectGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
|
||||||
withContext(Dispatchers.IO) { api.rejectGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data }
|
withContext(Dispatchers.IO) { api.rejectGroupJoinRequest(groupId, requestId, XuqmSDK.appKey).data }
|
||||||
|
|
||||||
suspend fun listFriends(): List<String> =
|
suspend fun listFriends(): List<String> =
|
||||||
withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appId).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appKey).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun addFriend(friendId: String) =
|
suspend fun addFriend(friendId: String) =
|
||||||
withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appId, friendId) }
|
withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appKey, friendId) }
|
||||||
|
|
||||||
suspend fun removeFriend(friendId: String) =
|
suspend fun removeFriend(friendId: String) =
|
||||||
withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appId) }
|
withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appKey) }
|
||||||
|
|
||||||
suspend fun listFriendRequests(direction: String = "incoming"): List<FriendRequest> =
|
suspend fun listFriendRequests(direction: String = "incoming"): List<FriendRequest> =
|
||||||
withContext(Dispatchers.IO) { api.listFriendRequests(XuqmSDK.appId, direction).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listFriendRequests(XuqmSDK.appKey, direction).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun sendFriendRequest(friendId: String, remark: String? = null): FriendRequest? =
|
suspend fun sendFriendRequest(friendId: String, remark: String? = null): FriendRequest? =
|
||||||
withContext(Dispatchers.IO) { api.sendFriendRequest(XuqmSDK.appId, friendId, remark).data }
|
withContext(Dispatchers.IO) { api.sendFriendRequest(XuqmSDK.appKey, friendId, remark).data }
|
||||||
|
|
||||||
suspend fun acceptFriendRequest(requestId: String): FriendRequest? =
|
suspend fun acceptFriendRequest(requestId: String): FriendRequest? =
|
||||||
withContext(Dispatchers.IO) { api.acceptFriendRequest(requestId, XuqmSDK.appId).data }
|
withContext(Dispatchers.IO) { api.acceptFriendRequest(requestId, XuqmSDK.appKey).data }
|
||||||
|
|
||||||
suspend fun rejectFriendRequest(requestId: String): FriendRequest? =
|
suspend fun rejectFriendRequest(requestId: String): FriendRequest? =
|
||||||
withContext(Dispatchers.IO) { api.rejectFriendRequest(requestId, XuqmSDK.appId).data }
|
withContext(Dispatchers.IO) { api.rejectFriendRequest(requestId, XuqmSDK.appKey).data }
|
||||||
|
|
||||||
suspend fun listBlacklist(): List<BlacklistEntry> =
|
suspend fun listBlacklist(): List<BlacklistEntry> =
|
||||||
withContext(Dispatchers.IO) { api.listBlacklist(XuqmSDK.appId).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listBlacklist(XuqmSDK.appKey).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun addToBlacklist(blockedUserId: String): BlacklistEntry? =
|
suspend fun addToBlacklist(blockedUserId: String): BlacklistEntry? =
|
||||||
withContext(Dispatchers.IO) { api.addToBlacklist(XuqmSDK.appId, blockedUserId).data }
|
withContext(Dispatchers.IO) { api.addToBlacklist(XuqmSDK.appKey, blockedUserId).data }
|
||||||
|
|
||||||
suspend fun removeFromBlacklist(blockedUserId: String) =
|
suspend fun removeFromBlacklist(blockedUserId: String) =
|
||||||
withContext(Dispatchers.IO) { api.removeFromBlacklist(XuqmSDK.appId, blockedUserId) }
|
withContext(Dispatchers.IO) { api.removeFromBlacklist(XuqmSDK.appKey, blockedUserId) }
|
||||||
|
|
||||||
suspend fun getProfile(userId: String) =
|
suspend fun getProfile(userId: String) =
|
||||||
withContext(Dispatchers.IO) { api.getProfile(userId, XuqmSDK.appId).data }
|
withContext(Dispatchers.IO) { api.getProfile(userId, XuqmSDK.appKey).data }
|
||||||
|
|
||||||
suspend fun updateProfile(
|
suspend fun updateProfile(
|
||||||
userId: String,
|
userId: String,
|
||||||
@ -604,11 +604,11 @@ object ImSDK {
|
|||||||
avatar: String? = null,
|
avatar: String? = null,
|
||||||
gender: String? = null,
|
gender: String? = null,
|
||||||
) = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
api.updateProfile(userId, XuqmSDK.appId, nickname, avatar, gender).data
|
api.updateProfile(userId, XuqmSDK.appKey, nickname, avatar, gender).data
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun listConversations(): List<ConversationData> =
|
suspend fun listConversations(): List<ConversationData> =
|
||||||
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appId).data ?: emptyList() }
|
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appKey).data ?: emptyList() }
|
||||||
|
|
||||||
suspend fun getTotalUnreadCount(): Int =
|
suspend fun getTotalUnreadCount(): Int =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@ -626,13 +626,13 @@ object ImSDK {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
|
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
|
||||||
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) }
|
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appKey, chatType) }
|
||||||
|
|
||||||
suspend fun setDraft(targetId: String, chatType: String, draft: String) =
|
suspend fun setDraft(targetId: String, chatType: String, draft: String) =
|
||||||
withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appId, chatType, draft) }
|
withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appKey, chatType, draft) }
|
||||||
|
|
||||||
suspend fun deleteConversation(targetId: String, chatType: String) =
|
suspend fun deleteConversation(targetId: String, chatType: String) =
|
||||||
withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appId, chatType) }
|
withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appKey, chatType) }
|
||||||
|
|
||||||
fun addListener(listener: ImEventListener) {
|
fun addListener(listener: ImEventListener) {
|
||||||
Log.d(TAG, "addListener listener=${listener.javaClass.name}")
|
Log.d(TAG, "addListener listener=${listener.javaClass.name}")
|
||||||
@ -669,7 +669,7 @@ object ImSDK {
|
|||||||
_connectionState.value = ImConnectionState.Connecting
|
_connectionState.value = ImConnectionState.Connecting
|
||||||
currentToken = token
|
currentToken = token
|
||||||
Log.d(TAG, "connectWithToken userId=$currentUserId activeGroups=${activeGroupSubscriptions.size}")
|
Log.d(TAG, "connectWithToken userId=$currentUserId activeGroups=${activeGroupSubscriptions.size}")
|
||||||
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId)
|
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appKey)
|
||||||
client?.addListener(connectionListener)
|
client?.addListener(connectionListener)
|
||||||
listeners.forEach { client?.addListener(it) }
|
listeners.forEach { client?.addListener(it) }
|
||||||
reconnectEnabled = true
|
reconnectEnabled = true
|
||||||
@ -742,7 +742,7 @@ object ImSDK {
|
|||||||
): ImMessage {
|
): ImMessage {
|
||||||
return ImMessage(
|
return ImMessage(
|
||||||
id = messageId,
|
id = messageId,
|
||||||
appId = XuqmSDK.appId,
|
appId = XuqmSDK.appKey,
|
||||||
fromId = currentUserId,
|
fromId = currentUserId,
|
||||||
toId = toId,
|
toId = toId,
|
||||||
chatType = chatType,
|
chatType = chatType,
|
||||||
@ -769,7 +769,7 @@ object ImSDK {
|
|||||||
reconnectAttempts += 1
|
reconnectAttempts += 1
|
||||||
_connectionState.value = ImConnectionState.Connecting
|
_connectionState.value = ImConnectionState.Connecting
|
||||||
Log.d(TAG, "reconnect attempt=$reconnectAttempts")
|
Log.d(TAG, "reconnect attempt=$reconnectAttempts")
|
||||||
client = ImClient(ServiceEndpointRegistry.imWsUrl, currentToken, XuqmSDK.appId)
|
client = ImClient(ServiceEndpointRegistry.imWsUrl, currentToken, XuqmSDK.appKey)
|
||||||
client?.addListener(connectionListener)
|
client?.addListener(connectionListener)
|
||||||
listeners.forEach { client?.addListener(it) }
|
listeners.forEach { client?.addListener(it) }
|
||||||
client?.connect()
|
client?.connect()
|
||||||
|
|||||||
@ -67,7 +67,7 @@ object PushSDK {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
api.setReceivePush(
|
api.setReceivePush(
|
||||||
appId = XuqmSDK.appId,
|
appId = XuqmSDK.appKey,
|
||||||
userId = resolvedUserId,
|
userId = resolvedUserId,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
)
|
)
|
||||||
@ -95,7 +95,7 @@ object PushSDK {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
api.registerDevice(
|
api.registerDevice(
|
||||||
appId = XuqmSDK.appId,
|
appId = XuqmSDK.appKey,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
vendor = vendor.name,
|
vendor = vendor.name,
|
||||||
token = pushToken,
|
token = pushToken,
|
||||||
@ -114,7 +114,7 @@ object PushSDK {
|
|||||||
XuqmSDK.requireInit()
|
XuqmSDK.requireInit()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
api.unregisterDevice(XuqmSDK.appId, userId)
|
api.unregisterDevice(XuqmSDK.appKey, userId)
|
||||||
registeredUserId.compareAndSet(userId, null)
|
registeredUserId.compareAndSet(userId, null)
|
||||||
store(XuqmSDK.appContext).updateLastUserId(null)
|
store(XuqmSDK.appContext).updateLastUserId(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,13 @@
|
|||||||
* Config: xuqm.properties in the project root (or module root)
|
* Config: xuqm.properties in the project root (or module root)
|
||||||
* ---
|
* ---
|
||||||
* xuqm.serverUrl=https://update.dev.xuqinmin.com
|
* xuqm.serverUrl=https://update.dev.xuqinmin.com
|
||||||
* xuqm.appId=your-app-id
|
* xuqm.appKey=your-app-key
|
||||||
* xuqm.apiToken=your-api-token
|
* xuqm.apiToken=your-api-token
|
||||||
|
* xuqm.storeTargets=HUAWEI,MI,OPPO # optional
|
||||||
|
* xuqm.autoPublishAfterReview=false
|
||||||
|
* xuqm.publishImmediately=false # optional: publish update-service record immediately
|
||||||
|
* xuqm.scheduledPublishAt= # optional ISO datetime
|
||||||
|
* xuqm.webhookUrl= # optional webhook for review status changes
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
* The task:
|
* The task:
|
||||||
@ -31,7 +36,6 @@ import java.net.URI
|
|||||||
import java.net.http.HttpClient
|
import java.net.http.HttpClient
|
||||||
import java.net.http.HttpRequest
|
import java.net.http.HttpRequest
|
||||||
import java.net.http.HttpResponse
|
import java.net.http.HttpResponse
|
||||||
import java.nio.file.Files
|
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@ -101,6 +105,35 @@ fun parseJson(json: String, key: String): String? =
|
|||||||
fun parseJsonInt(json: String, key: String): Int? =
|
fun parseJsonInt(json: String, key: String): Int? =
|
||||||
Regex("\"$key\"\\s*:\\s*(\\d+)").find(json)?.groupValues?.get(1)?.toIntOrNull()
|
Regex("\"$key\"\\s*:\\s*(\\d+)").find(json)?.groupValues?.get(1)?.toIntOrNull()
|
||||||
|
|
||||||
|
fun promptLine(message: String): String? {
|
||||||
|
val console = System.console() ?: return null
|
||||||
|
print(message)
|
||||||
|
return console.readLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun promptYesNo(message: String, default: Boolean = false): Boolean {
|
||||||
|
val suffix = if (default) " [Y/n]: " else " [y/N]: "
|
||||||
|
val answer = promptLine(message + suffix)?.trim().orEmpty()
|
||||||
|
return when {
|
||||||
|
answer.isBlank() -> default
|
||||||
|
answer.equals("y", ignoreCase = true) || answer.equals("yes", ignoreCase = true) -> true
|
||||||
|
answer.equals("n", ignoreCase = true) || answer.equals("no", ignoreCase = true) -> false
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun promptReleaseMode(): String {
|
||||||
|
val answer = promptLine(
|
||||||
|
"Publish mode? [1=manual, 2=publish now, 3=scheduled, 4=auto after review]: "
|
||||||
|
)?.trim().orEmpty()
|
||||||
|
return when (answer) {
|
||||||
|
"2" -> "NOW"
|
||||||
|
"3" -> "SCHEDULED"
|
||||||
|
"4" -> "AUTO_REVIEW"
|
||||||
|
else -> "MANUAL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Task registration ─────────────────────────────────────────────────────
|
// ── Task registration ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
tasks.register("xuqmRelease") {
|
tasks.register("xuqmRelease") {
|
||||||
@ -113,12 +146,20 @@ tasks.register("xuqmRelease") {
|
|||||||
doLast {
|
doLast {
|
||||||
val cfg = loadXuqmConfig(projectDir)
|
val cfg = loadXuqmConfig(projectDir)
|
||||||
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
|
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
|
||||||
val appId = cfg.getProperty("xuqm.appId") ?: throw GradleException("xuqm.appId missing")
|
val appKey = cfg.getProperty("xuqm.appKey") ?: throw GradleException("xuqm.appKey missing")
|
||||||
val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing")
|
val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing")
|
||||||
val storeTargets = cfg.getProperty("xuqm.storeTargets", "") // e.g. "HUAWEI,MI,OPPO"
|
val storeTargets = cfg.getProperty("xuqm.storeTargets", "") // e.g. "HUAWEI,MI,OPPO"
|
||||||
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean()
|
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean()
|
||||||
val scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "")
|
val publishImmediately = cfg.getProperty("xuqm.publishImmediately", "false").toBoolean()
|
||||||
|
var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "")
|
||||||
val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "")
|
val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "")
|
||||||
|
var publishMode = cfg.getProperty("xuqm.publishMode", "").trim().uppercase()
|
||||||
|
if (publishMode.isBlank() && !publishImmediately && scheduledAt.isBlank() && !autoPublish && System.console() != null) {
|
||||||
|
publishMode = promptReleaseMode()
|
||||||
|
if (publishMode == "SCHEDULED" && scheduledAt.isBlank()) {
|
||||||
|
scheduledAt = promptLine("Scheduled publish time (ISO datetime): ")?.trim().orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 1. Read local version ──────────────────────────────────────────
|
// ── 1. Read local version ──────────────────────────────────────────
|
||||||
val android = project.extensions.findByName("android")
|
val android = project.extensions.findByName("android")
|
||||||
@ -129,10 +170,10 @@ tasks.register("xuqmRelease") {
|
|||||||
val versionCode = (defaultConfig::class.java.getMethod("getVersionCode").invoke(defaultConfig) as? Int)
|
val versionCode = (defaultConfig::class.java.getMethod("getVersionCode").invoke(defaultConfig) as? Int)
|
||||||
?: throw GradleException("versionCode not set")
|
?: throw GradleException("versionCode not set")
|
||||||
val applicationId = defaultConfig::class.java.getMethod("getApplicationId").invoke(defaultConfig) as? String ?: ""
|
val applicationId = defaultConfig::class.java.getMethod("getApplicationId").invoke(defaultConfig) as? String ?: ""
|
||||||
println("[xuqm] Local version: $versionName ($versionCode), packageName: $applicationId")
|
println("[xuqm] Local version: $versionName ($versionCode), packageName: $applicationId, appKey: $appKey")
|
||||||
|
|
||||||
// ── 2. Check server latest ─────────────────────────────────────────
|
// ── 2. Check server latest ─────────────────────────────────────────
|
||||||
val listResp = httpGet("$serverUrl/api/v1/updates/app/list?appId=$appId&platform=ANDROID", apiToken)
|
val listResp = httpGet("$serverUrl/api/v1/updates/app/list?appId=$appKey&platform=ANDROID", apiToken)
|
||||||
// Find highest published versionCode
|
// Find highest published versionCode
|
||||||
val serverVersionCode = Regex("\"versionCode\"\\s*:\\s*(\\d+)").findAll(listResp)
|
val serverVersionCode = Regex("\"versionCode\"\\s*:\\s*(\\d+)").findAll(listResp)
|
||||||
.mapNotNull { it.groupValues[1].toIntOrNull() }
|
.mapNotNull { it.groupValues[1].toIntOrNull() }
|
||||||
@ -154,7 +195,7 @@ tasks.register("xuqmRelease") {
|
|||||||
|
|
||||||
// ── 4. Upload to update service ───────────────────────────────────
|
// ── 4. Upload to update service ───────────────────────────────────
|
||||||
val parts = mutableMapOf<String, Any>(
|
val parts = mutableMapOf<String, Any>(
|
||||||
"appId" to appId,
|
"appId" to appKey,
|
||||||
"platform" to "ANDROID",
|
"platform" to "ANDROID",
|
||||||
"versionName" to versionName,
|
"versionName" to versionName,
|
||||||
"versionCode" to versionCode,
|
"versionCode" to versionCode,
|
||||||
@ -166,6 +207,7 @@ tasks.register("xuqmRelease") {
|
|||||||
if (storeTargets.isNotBlank()) parts["storeSubmitTargets"] = "[\"${storeTargets.split(",").joinToString("\",\"")}\"]"
|
if (storeTargets.isNotBlank()) parts["storeSubmitTargets"] = "[\"${storeTargets.split(",").joinToString("\",\"")}\"]"
|
||||||
if (scheduledAt.isNotBlank()) parts["scheduledPublishAt"] = scheduledAt
|
if (scheduledAt.isNotBlank()) parts["scheduledPublishAt"] = scheduledAt
|
||||||
if (webhookUrl.isNotBlank()) parts["webhookUrl"] = webhookUrl
|
if (webhookUrl.isNotBlank()) parts["webhookUrl"] = webhookUrl
|
||||||
|
if (publishMode == "NOW") parts["publishImmediately"] = "true"
|
||||||
|
|
||||||
println("[xuqm] Uploading APK...")
|
println("[xuqm] Uploading APK...")
|
||||||
val uploadResp = httpMultipartPost("$serverUrl/api/v1/updates/app/upload", apiToken, parts)
|
val uploadResp = httpMultipartPost("$serverUrl/api/v1/updates/app/upload", apiToken, parts)
|
||||||
@ -187,12 +229,24 @@ tasks.register("xuqmRelease") {
|
|||||||
println("[xuqm] Store submission triggered (HTTP ${storeResp.statusCode()})")
|
println("[xuqm] Store submission triggered (HTTP ${storeResp.statusCode()})")
|
||||||
}
|
}
|
||||||
|
|
||||||
println("[xuqm] Done. Version is in DRAFT state.")
|
if (publishImmediately || publishMode == "NOW") {
|
||||||
if (scheduledAt.isNotBlank()) {
|
println("[xuqm] Published immediately in update service.")
|
||||||
println("[xuqm] Will auto-publish at: $scheduledAt")
|
} else if (publishMode == "SCHEDULED" || scheduledAt.isNotBlank()) {
|
||||||
|
println("[xuqm] Will auto-publish at: ${scheduledAt.ifBlank { "(use update service scheduled publish config)" }}")
|
||||||
} else if (autoPublish) {
|
} else if (autoPublish) {
|
||||||
println("[xuqm] Will auto-publish after all store reviews pass.")
|
println("[xuqm] Will auto-publish after all store reviews pass.")
|
||||||
} else {
|
} else if (System.console() != null && promptYesNo("[xuqm] Publish now?", false)) {
|
||||||
|
val client = HttpClient.newHttpClient()
|
||||||
|
val req = HttpRequest.newBuilder(URI.create("$serverUrl/api/v1/updates/app/$versionId/publish"))
|
||||||
|
.header("Authorization", "Bearer $apiToken")
|
||||||
|
.POST(HttpRequest.BodyPublishers.noBody())
|
||||||
|
.build()
|
||||||
|
val publishResp = client.send(req, HttpResponse.BodyHandlers.ofString())
|
||||||
|
println("[xuqm] Publish HTTP ${publishResp.statusCode()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
println("[xuqm] Done. Version is in DRAFT state.")
|
||||||
|
if (!publishImmediately && publishMode != "NOW" && scheduledAt.isBlank() && !autoPublish) {
|
||||||
println("[xuqm] Publish manually: POST $serverUrl/api/v1/updates/app/$versionId/publish")
|
println("[xuqm] Publish manually: POST $serverUrl/api/v1/updates/app/$versionId/publish")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ object UpdateSDK {
|
|||||||
val versionCode = context.packageManager
|
val versionCode = context.packageManager
|
||||||
.getPackageInfo(context.packageName, 0).longVersionCode.toInt()
|
.getPackageInfo(context.packageName, 0).longVersionCode.toInt()
|
||||||
runCatching {
|
runCatching {
|
||||||
api.checkUpdate(XuqmSDK.appId, "ANDROID", versionCode).data?.let {
|
api.checkUpdate(XuqmSDK.appKey, "ANDROID", versionCode).data?.let {
|
||||||
it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl)
|
it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl)
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户