比较提交

..

没有共同的提交。46777173432347e6734b1f96a9b809e143a8620b 和 dcb263edc6a4f984484fa91226c535e01b3b1e88 的历史完全不同。

共有 17 个文件被更改,包括 41 次插入590 次删除

查看文件

@ -34,17 +34,6 @@ class LocalImCache(context: Context) {
saveConversations(updated) saveConversations(updated)
} }
fun markConversationRead(targetId: String, chatType: String) {
val updated = loadConversations().map { conversation ->
if (conversation.targetId == targetId && conversation.chatType == chatType) {
conversation.copy(unreadCount = 0)
} else {
conversation
}
}
saveConversations(updated)
}
fun loadHistory(targetId: String, chatType: String): List<ImMessage> = fun loadHistory(targetId: String, chatType: String): List<ImMessage> =
readMessageList(historyKey(targetId, chatType)) readMessageList(historyKey(targetId, chatType))
@ -144,10 +133,7 @@ class LocalImCache(context: Context) {
content = obj.optString("content"), content = obj.optString("content"),
status = obj.optString("status"), status = obj.optString("status"),
mentionedUserIds = obj.optString("mentionedUserIds").takeIf { it.isNotBlank() }, mentionedUserIds = obj.optString("mentionedUserIds").takeIf { it.isNotBlank() },
groupReadCount = obj.optInt("groupReadCount").takeIf { obj.has("groupReadCount") },
revoked = obj.optBoolean("revoked").takeIf { obj.has("revoked") },
createdAt = obj.optLong("createdAt"), createdAt = obj.optLong("createdAt"),
editedAt = obj.optLong("editedAt").takeIf { obj.has("editedAt") },
) )
) )
} }
@ -188,10 +174,7 @@ class LocalImCache(context: Context) {
put("content", message.content) put("content", message.content)
put("status", message.status) put("status", message.status)
put("mentionedUserIds", message.mentionedUserIds ?: "") put("mentionedUserIds", message.mentionedUserIds ?: "")
message.groupReadCount?.let { put("groupReadCount", it) }
put("revoked", message.revoked ?: false)
put("createdAt", message.createdAt) put("createdAt", message.createdAt)
message.editedAt?.let { put("editedAt", it) }
} }
) )
} }

查看文件

@ -44,7 +44,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -69,10 +68,10 @@ import com.xuqm.sdk.ui.SearchBarField
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.xuqm.sdk.file.FileSDK
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
@ -92,7 +91,6 @@ fun ChatScreen(
val draftText by viewModel.draftText.collectAsStateWithLifecycle() val draftText by viewModel.draftText.collectAsStateWithLifecycle()
val mentionableUsers by viewModel.mentionableUsers.collectAsStateWithLifecycle() val mentionableUsers by viewModel.mentionableUsers.collectAsStateWithLifecycle()
val replyTargetMessage by viewModel.replyTargetMessage.collectAsStateWithLifecycle() val replyTargetMessage by viewModel.replyTargetMessage.collectAsStateWithLifecycle()
val editingMessage by viewModel.editingMessage.collectAsStateWithLifecycle()
val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle() val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle()
val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle() val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle()
val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle() val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle()
@ -105,7 +103,6 @@ fun ChatScreen(
var draftValue by remember { mutableStateOf(TextFieldValue(draftText)) } var draftValue by remember { mutableStateOf(TextFieldValue(draftText)) }
var showSearchBar by remember { mutableStateOf(false) } var showSearchBar by remember { mutableStateOf(false) }
val replyTarget = replyTargetMessage val replyTarget = replyTargetMessage
val editingTarget = editingMessage
var pendingCameraAction by remember { mutableStateOf<CameraAction?>(null) } var pendingCameraAction by remember { mutableStateOf<CameraAction?>(null) }
var pendingCaptureUri by remember { mutableStateOf<Uri?>(null) } var pendingCaptureUri by remember { mutableStateOf<Uri?>(null) }
var isRecordingVoice by remember { mutableStateOf(false) } var isRecordingVoice by remember { mutableStateOf(false) }
@ -302,23 +299,6 @@ fun ChatScreen(
} }
} }
} }
if (editingTarget != null) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "编辑: ${editingTarget.previewText()}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.weight(1f),
)
TextButton(onClick = viewModel::clearEdit) {
Text("取消")
}
}
}
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -446,7 +426,6 @@ fun ChatScreen(
isOwn = msg.fromId == viewModel.currentUserId, isOwn = msg.fromId == viewModel.currentUserId,
currentUserId = viewModel.currentUserId, currentUserId = viewModel.currentUserId,
onReply = viewModel::startReply, onReply = viewModel::startReply,
onEdit = viewModel::startEdit,
) )
} }
if (searchResults.isEmpty()) { if (searchResults.isEmpty()) {
@ -476,7 +455,6 @@ fun ChatScreen(
isOwn = msg.fromId == viewModel.currentUserId, isOwn = msg.fromId == viewModel.currentUserId,
currentUserId = viewModel.currentUserId, currentUserId = viewModel.currentUserId,
onReply = viewModel::startReply, onReply = viewModel::startReply,
onEdit = viewModel::startEdit,
) )
} }
if (isLoadingMore) { if (isLoadingMore) {
@ -534,11 +512,8 @@ private fun MessageBubble(
isOwn: Boolean, isOwn: Boolean,
currentUserId: String, currentUserId: String,
onReply: (ImMessage) -> Unit, onReply: (ImMessage) -> Unit,
onEdit: (ImMessage) -> Unit,
) { ) {
val media = message.mediaContent() val media = message.mediaContent()
val context = LocalContext.current
val scope = rememberCoroutineScope()
val arrangement = if (isOwn) Arrangement.End else Arrangement.Start val arrangement = if (isOwn) Arrangement.End else Arrangement.Start
val bubbleColor = if (isOwn) MaterialTheme.colorScheme.primaryContainer val bubbleColor = if (isOwn) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.surfaceVariant
@ -567,13 +542,7 @@ private fun MessageBubble(
.padding(horizontal = 4.dp) .padding(horizontal = 4.dp)
.combinedClickable( .combinedClickable(
onClick = {}, onClick = {},
onLongClick = { onLongClick = { onReply(message) },
if (isOwn && message.msgType.uppercase() == "TEXT") {
onEdit(message)
} else {
onReply(message)
}
},
), ),
) { ) {
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) { Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
@ -588,27 +557,6 @@ private fun MessageBubble(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
} }
if (media?.url.isNullOrBlank().not() && message.msgType.uppercase() in setOf("IMAGE", "VIDEO", "AUDIO", "FILE")) {
TextButton(
onClick = {
scope.launch {
runCatching {
val saved = FileSDK.downloadToAppFiles(
context = context,
downloadUrl = media.url,
fileName = media.name?.takeIf { it.isNotBlank() } ?: defaultDownloadFileName(message),
directoryName = "demo-downloads",
)
Toast.makeText(context, "已保存到 ${saved.absolutePath}", Toast.LENGTH_SHORT).show()
}.onFailure {
Toast.makeText(context, "下载失败:${it.message ?: "未知错误"}", Toast.LENGTH_SHORT).show()
}
}
},
) {
Text("下载")
}
}
val mentionedUserIds = message.mentionedUserIds.orEmpty() val mentionedUserIds = message.mentionedUserIds.orEmpty()
if (mentionedUserIds.isNotBlank() && if (mentionedUserIds.isNotBlank() &&
mentionedUserIds.split(",").map { it.trim() }.contains(currentUserId) mentionedUserIds.split(",").map { it.trim() }.contains(currentUserId)
@ -817,17 +765,6 @@ private fun formatDuration(ms: Long): String {
return if (minutes > 0) String.format("%d:%02d", minutes, seconds) else "${seconds}s" return if (minutes > 0) String.format("%d:%02d", minutes, seconds) else "${seconds}s"
} }
private fun defaultDownloadFileName(message: ImMessage): String {
val suffix = when (message.msgType.uppercase()) {
"IMAGE" -> "jpg"
"VIDEO" -> "mp4"
"AUDIO" -> "m4a"
"FILE" -> "bin"
else -> "dat"
}
return "${message.msgType.lowercase()}_${message.id}.$suffix"
}
private fun mentionQueryAtCursor(value: TextFieldValue): String? { private fun mentionQueryAtCursor(value: TextFieldValue): String? {
if (!value.selection.collapsed) return null if (!value.selection.collapsed) return null
val cursor = value.selection.end.coerceIn(0, value.text.length) val cursor = value.selection.end.coerceIn(0, value.text.length)

查看文件

@ -46,9 +46,6 @@ class ChatViewModel : ViewModel() {
private val _replyTargetMessage = MutableStateFlow<ImMessage?>(null) private val _replyTargetMessage = MutableStateFlow<ImMessage?>(null)
val replyTargetMessage: StateFlow<ImMessage?> = _replyTargetMessage val replyTargetMessage: StateFlow<ImMessage?> = _replyTargetMessage
private val _editingMessage = MutableStateFlow<ImMessage?>(null)
val editingMessage: StateFlow<ImMessage?> = _editingMessage
private val _mentionableUsers = MutableStateFlow<List<UserData>>(emptyList()) private val _mentionableUsers = MutableStateFlow<List<UserData>>(emptyList())
val mentionableUsers: StateFlow<List<UserData>> = _mentionableUsers val mentionableUsers: StateFlow<List<UserData>> = _mentionableUsers
@ -74,14 +71,6 @@ class ChatViewModel : ViewModel() {
override fun onGroupMessage(message: ImMessage) { override fun onGroupMessage(message: ImMessage) {
handleIncomingMessage(message) handleIncomingMessage(message)
} }
override fun onRead(message: ImMessage) {
handleIncomingMessage(message)
}
override fun onRevoke(message: ImMessage) {
handleIncomingMessage(message)
}
} }
fun init(targetId: String, chatType: String) { fun init(targetId: String, chatType: String) {
@ -101,7 +90,6 @@ class ChatViewModel : ViewModel() {
_draftText.value = cache.loadDraft(targetId, chatType) _draftText.value = cache.loadDraft(targetId, chatType)
_mentionableUsers.value = emptyList() _mentionableUsers.value = emptyList()
_replyTargetMessage.value = null _replyTargetMessage.value = null
_editingMessage.value = null
ImSDK.addListener(listener) ImSDK.addListener(listener)
if (chatType == "GROUP") { if (chatType == "GROUP") {
ImSDK.subscribeGroup(targetId) ImSDK.subscribeGroup(targetId)
@ -112,10 +100,6 @@ class ChatViewModel : ViewModel() {
} }
viewModelScope.launch { viewModelScope.launch {
runCatching { ImSDK.markRead(targetId, chatType) } runCatching { ImSDK.markRead(targetId, chatType) }
.onSuccess {
cache.markConversationRead(targetId, chatType)
AppDependencies.notifyConversationChanged()
}
} }
} }
@ -186,25 +170,6 @@ class ChatViewModel : ViewModel() {
fun sendText(content: String) { fun sendText(content: String) {
if (content.isBlank()) return if (content.isBlank()) return
val editingTarget = _editingMessage.value
if (editingTarget != null) {
viewModelScope.launch {
runCatching { ImSDK.editMessage(editingTarget.id, content) }
.onSuccess { updated ->
handleIncomingMessage(updated)
_editingMessage.value = null
_replyTargetMessage.value = null
_draftText.value = ""
cache.clearDraft(targetId, chatType)
}
.onFailure {
_events.tryEmit("消息编辑失败,请检查网络后重试")
Log.w(TAG, "editMessage failed messageId=${editingTarget.id}", it)
}
}
return
}
val replyTarget = _replyTargetMessage.value val replyTarget = _replyTargetMessage.value
val sent = if (replyTarget != null) { val sent = if (replyTarget != null) {
ImSDK.sendQuoteMessage( ImSDK.sendQuoteMessage(
@ -225,7 +190,6 @@ class ChatViewModel : ViewModel() {
_draftText.value = "" _draftText.value = ""
cache.clearDraft(targetId, chatType) cache.clearDraft(targetId, chatType)
_replyTargetMessage.value = null _replyTargetMessage.value = null
_editingMessage.value = null
} }
} }
@ -253,28 +217,6 @@ class ChatViewModel : ViewModel() {
_replyTargetMessage.value = null _replyTargetMessage.value = null
} }
fun startEdit(message: ImMessage) {
_editingMessage.value = message
_replyTargetMessage.value = null
updateDraft(message.textContent())
}
fun clearEdit() {
_editingMessage.value = null
}
fun revokeMessage(messageId: String) {
if (!initialized) return
viewModelScope.launch {
runCatching { ImSDK.revokeMessage(messageId) }
.onSuccess { revoked -> handleIncomingMessage(revoked) }
.onFailure {
_events.tryEmit("消息撤回失败,请检查网络后重试")
Log.w(TAG, "revokeMessage failed messageId=$messageId", it)
}
}
}
fun sendImage(uri: Uri) = sendAttachment(uri, AttachmentKind.IMAGE) fun sendImage(uri: Uri) = sendAttachment(uri, AttachmentKind.IMAGE)
fun sendImageBytes(fileName: String, bytes: ByteArray, width: Int? = null, height: Int? = null) { fun sendImageBytes(fileName: String, bytes: ByteArray, width: Int? = null, height: Int? = null) {
@ -491,10 +433,6 @@ class ChatViewModel : ViewModel() {
if (message.status.uppercase() != "READ" && message.fromId != currentUserId) { if (message.status.uppercase() != "READ" && message.fromId != currentUserId) {
viewModelScope.launch { viewModelScope.launch {
runCatching { ImSDK.markRead(targetId, chatType) } runCatching { ImSDK.markRead(targetId, chatType) }
.onSuccess {
cache.markConversationRead(targetId, chatType)
AppDependencies.notifyConversationChanged()
}
} }
} }
} }
@ -506,10 +444,6 @@ class ChatViewModel : ViewModel() {
requestScrollToBottom() requestScrollToBottom()
} }
private fun ImMessage.textContent(): String {
return if (msgType.uppercase() == "TEXT") content else previewText()
}
private fun updateConversationPreview(lastMsgContent: String, lastMsgType: String, lastMsgTime: Long = System.currentTimeMillis()) { private fun updateConversationPreview(lastMsgContent: String, lastMsgType: String, lastMsgTime: Long = System.currentTimeMillis()) {
cache.upsertConversation( cache.upsertConversation(
ConversationData( ConversationData(
@ -550,7 +484,6 @@ class ChatViewModel : ViewModel() {
status = incoming.status.ifBlank { existing.status }, status = incoming.status.ifBlank { existing.status },
mentionedUserIds = incoming.mentionedUserIds ?: existing.mentionedUserIds, mentionedUserIds = incoming.mentionedUserIds ?: existing.mentionedUserIds,
groupReadCount = incoming.groupReadCount ?: existing.groupReadCount, groupReadCount = incoming.groupReadCount ?: existing.groupReadCount,
revoked = incoming.revoked ?: existing.revoked,
createdAt = existing.createdAt, createdAt = existing.createdAt,
) )
} }

查看文件

@ -83,8 +83,6 @@ class ContactViewModel(
private val listener = object : ImEventListener { private val listener = object : ImEventListener {
override fun onMessage(message: ImMessage) = handleNotification(message) override fun onMessage(message: ImMessage) = handleNotification(message)
override fun onGroupMessage(message: ImMessage) = handleNotification(message) override fun onGroupMessage(message: ImMessage) = handleNotification(message)
override fun onRead(message: ImMessage) = handleNotification(message)
override fun onRevoke(message: ImMessage) = handleNotification(message)
} }
init { init {

查看文件

@ -49,22 +49,6 @@ class ConversationViewModel : ViewModel() {
) )
refresh() refresh()
} }
override fun onRead(message: ImMessage) {
Log.d(
TAG,
"incoming read refresh request id=${message.id} from=${message.fromId} to=${message.toId} msgType=${message.msgType}",
)
refresh()
}
override fun onRevoke(message: ImMessage) {
Log.d(
TAG,
"incoming revoke refresh request id=${message.id} from=${message.fromId} to=${message.toId} msgType=${message.msgType}",
)
refresh()
}
} }
init { init {

查看文件

@ -313,7 +313,6 @@ fun GroupSettingsScreen(
) { ) {
val group by viewModel.currentGroup.collectAsStateWithLifecycle() val group by viewModel.currentGroup.collectAsStateWithLifecycle()
val members by viewModel.members.collectAsStateWithLifecycle() val members by viewModel.members.collectAsStateWithLifecycle()
val groupMembers by viewModel.groupMembers.collectAsStateWithLifecycle()
val joinRequests by viewModel.joinRequests.collectAsStateWithLifecycle() val joinRequests by viewModel.joinRequests.collectAsStateWithLifecycle()
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle() val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
var showEditDialog by remember { mutableStateOf(false) } var showEditDialog by remember { mutableStateOf(false) }
@ -321,15 +320,14 @@ fun GroupSettingsScreen(
var showRemoveMemberDialog by remember { mutableStateOf(false) } var showRemoveMemberDialog by remember { mutableStateOf(false) }
var editName by remember { mutableStateOf("") } var editName by remember { mutableStateOf("") }
var editAnnouncement by remember { mutableStateOf("") } var editAnnouncement by remember { mutableStateOf("") }
val memberProfiles = remember(group, members, groupMembers) { val memberProfiles = remember(group, members) {
val sourceMembers = if (groupMembers.isNotEmpty()) groupMembers else members
val ids = group?.memberIds val ids = group?.memberIds
?.split(",") ?.split(",")
?.map { it.trim() } ?.map { it.trim() }
?.filter { it.isNotBlank() } ?.filter { it.isNotBlank() }
.orEmpty() .orEmpty()
ids.mapNotNull { id -> ids.mapNotNull { id ->
sourceMembers.firstOrNull { it.userId == id } ?: UserData(id, "") members.firstOrNull { it.userId == id } ?: UserData(id, "")
} }
} }
val memberNameById = remember(members) { val memberNameById = remember(members) {

查看文件

@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope
import com.xuqm.sdk.im.ImSDK import com.xuqm.sdk.im.ImSDK
import com.xuqm.sdk.im.model.GroupJoinRequest import com.xuqm.sdk.im.model.GroupJoinRequest
import com.xuqm.sdk.im.model.ImGroup import com.xuqm.sdk.im.model.ImGroup
import com.xuqm.sdk.im.model.UserProfile
import com.xuqm.sdk.sample.data.api.UserData import com.xuqm.sdk.sample.data.api.UserData
import com.xuqm.sdk.sample.data.repo.AuthRepository import com.xuqm.sdk.sample.data.repo.AuthRepository
import com.xuqm.sdk.sample.di.AppDependencies import com.xuqm.sdk.sample.di.AppDependencies
@ -23,9 +22,6 @@ class GroupViewModel : ViewModel() {
private val _members = MutableStateFlow<List<UserData>>(emptyList()) private val _members = MutableStateFlow<List<UserData>>(emptyList())
val members: StateFlow<List<UserData>> = _members val members: StateFlow<List<UserData>> = _members
private val _groupMembers = MutableStateFlow<List<UserData>>(emptyList())
val groupMembers: StateFlow<List<UserData>> = _groupMembers
private val _publicGroups = MutableStateFlow<List<ImGroup>>(emptyList()) private val _publicGroups = MutableStateFlow<List<ImGroup>>(emptyList())
val publicGroups: StateFlow<List<ImGroup>> = _publicGroups val publicGroups: StateFlow<List<ImGroup>> = _publicGroups
@ -62,10 +58,7 @@ class GroupViewModel : ViewModel() {
fun loadGroupInfo(groupId: String) { fun loadGroupInfo(groupId: String) {
viewModelScope.launch { viewModelScope.launch {
runCatching { ImSDK.getGroupInfo(groupId) } runCatching { ImSDK.getGroupInfo(groupId) }
.onSuccess { .onSuccess { _currentGroup.value = it }
_currentGroup.value = it
loadGroupMembersInternal(groupId)
}
} }
} }
@ -82,7 +75,6 @@ class GroupViewModel : ViewModel() {
try { try {
loadGroupsInternal() loadGroupsInternal()
loadMembersInternal() loadMembersInternal()
_currentGroup.value?.let { loadGroupMembersInternal(it.id) }
loadPublicGroupsInternal(_publicGroupQuery.value) loadPublicGroupsInternal(_publicGroupQuery.value)
} finally { } finally {
_isRefreshing.value = false _isRefreshing.value = false
@ -185,13 +177,6 @@ class GroupViewModel : ViewModel() {
} }
} }
private suspend fun loadGroupMembersInternal(groupId: String) {
runCatching { ImSDK.listGroupMembers(groupId) }
.onSuccess { profiles ->
_groupMembers.value = profiles.map { it.toUserData() }
}
}
private suspend fun loadGroupsInternal() { private suspend fun loadGroupsInternal() {
runCatching { ImSDK.listGroups() } runCatching { ImSDK.listGroups() }
.onSuccess { _groups.value = it } .onSuccess { _groups.value = it }
@ -201,8 +186,4 @@ class GroupViewModel : ViewModel() {
runCatching { ImSDK.listPublicGroups(keyword.ifBlank { null }) } runCatching { ImSDK.listPublicGroups(keyword.ifBlank { null }) }
.onSuccess { _publicGroups.value = it } .onSuccess { _publicGroups.value = it }
} }
private fun UserProfile.toUserData(): UserData {
return UserData(userId = userId, nickname = nickname, avatar = avatar)
}
} }

查看文件

@ -4,15 +4,12 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.network.ApiClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.http.Multipart import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Part import retrofit2.http.Part
import java.io.File
data class FileUploadResult( data class FileUploadResult(
val url: String, val url: String,
@ -93,31 +90,4 @@ object FileSDK {
"File upload failed" "File upload failed"
} }
} }
suspend fun downloadToFile(
downloadUrl: String,
targetFile: File,
onProgress: (Int) -> Unit = {},
): File = withContext(Dispatchers.IO) {
FileTransfer.downloadToFile(downloadUrl, targetFile, onProgress)
targetFile
}
suspend fun downloadToAppFiles(
context: Context,
downloadUrl: String,
fileName: String? = null,
directoryName: String? = null,
onProgress: (Int) -> Unit = {},
): File = withContext(Dispatchers.IO) {
val dir = if (directoryName.isNullOrBlank()) {
context.getExternalFilesDir(null) ?: context.filesDir
} else {
File(context.getExternalFilesDir(null) ?: context.filesDir, directoryName).apply { mkdirs() }
}
val resolvedName = fileName?.takeIf { it.isNotBlank() } ?: downloadUrl.substringAfterLast('/').takeIf { it.isNotBlank() } ?: "download.bin"
val target = File(dir, resolvedName)
FileTransfer.downloadToFile(downloadUrl, target, onProgress)
target
}
} }

查看文件

@ -203,13 +203,6 @@ class ImClient(
append(" status=").append(msg.status) append(" status=").append(msg.status)
}, },
) )
if (msg.status.uppercase() == "READ") {
listeners.forEach { it.onRead(msg) }
}
if (msg.status.uppercase() == "REVOKED" || msg.msgType.uppercase() == "REVOKED") {
listeners.forEach { it.onRevoke(msg) }
return
}
if (msg.chatType.uppercase() == "GROUP") { if (msg.chatType.uppercase() == "GROUP") {
listeners.forEach { it.onGroupMessage(msg) } listeners.forEach { it.onGroupMessage(msg) }
} else { } else {

查看文件

@ -6,7 +6,6 @@ import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.im.api.AddMemberRequest import com.xuqm.sdk.im.api.AddMemberRequest
import com.xuqm.sdk.im.api.CreateGroupRequest import com.xuqm.sdk.im.api.CreateGroupRequest
import com.xuqm.sdk.im.api.ImApi import com.xuqm.sdk.im.api.ImApi
import com.xuqm.sdk.im.model.EditMessageRequest
import com.xuqm.sdk.im.api.MuteGroupMemberRequest import com.xuqm.sdk.im.api.MuteGroupMemberRequest
import com.xuqm.sdk.im.api.SetMutedRequest import com.xuqm.sdk.im.api.SetMutedRequest
import com.xuqm.sdk.im.api.SetPinnedRequest import com.xuqm.sdk.im.api.SetPinnedRequest
@ -18,8 +17,6 @@ import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.GroupJoinRequest import com.xuqm.sdk.im.model.GroupJoinRequest
import com.xuqm.sdk.im.model.ImGroup import com.xuqm.sdk.im.model.ImGroup
import com.xuqm.sdk.im.model.ImMessage import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.PageResult
import com.xuqm.sdk.im.model.UserProfile
import com.xuqm.sdk.im.model.BlacklistEntry import com.xuqm.sdk.im.model.BlacklistEntry
import com.xuqm.sdk.im.model.FriendRequest import com.xuqm.sdk.im.model.FriendRequest
import com.xuqm.sdk.file.FileUploadResult import com.xuqm.sdk.file.FileUploadResult
@ -143,12 +140,6 @@ object ImSDK {
return sendMessage(toId, chatType, "TEXT", content, mentionedUserIds) return sendMessage(toId, chatType, "TEXT", content, mentionedUserIds)
} }
suspend fun editMessage(messageId: String, content: String): ImMessage =
withContext(Dispatchers.IO) { api.editMessage(messageId, XuqmSDK.appId, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") }
suspend fun revokeMessage(messageId: String): ImMessage =
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appId).data ?: throw IllegalStateException("revoke message failed") }
fun sendImageMessage( fun sendImageMessage(
toId: String, toId: String,
chatType: String, chatType: String,
@ -442,48 +433,6 @@ object ImSDK {
).data ?: emptyList() ).data ?: emptyList()
} }
suspend fun locateHistoryPage(
toId: String,
messageId: String,
pageSize: Int = 20,
maxPages: Int = 20,
): List<ImMessage>? = locatePage(
maxPages = maxPages,
loadPage = { page -> fetchHistory(toId, page, pageSize) },
messageId = messageId,
pageSize = pageSize,
)
suspend fun locateGroupHistoryPage(
groupId: String,
messageId: String,
pageSize: Int = 20,
maxPages: Int = 20,
): List<ImMessage>? = locatePage(
maxPages = maxPages,
loadPage = { page -> fetchGroupHistory(groupId, page, pageSize) },
messageId = messageId,
pageSize = pageSize,
)
private suspend fun locatePage(
maxPages: Int,
loadPage: suspend (Int) -> List<ImMessage>,
messageId: String,
pageSize: Int,
): List<ImMessage>? {
repeat(maxPages.coerceAtLeast(1)) { page ->
val messages = loadPage(page)
if (messages.any { it.id == messageId }) {
return messages
}
if (messages.size < pageSize) {
return null
}
}
return null
}
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.appId).data ?: emptyList() }
@ -495,43 +444,9 @@ object ImSDK {
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> =
withContext(Dispatchers.IO) { api.listGroupMembers(groupId, XuqmSDK.appId).data ?: emptyList() }
suspend fun searchGroupMembers(groupId: String, keyword: String, size: Int = 20): List<UserProfile> =
withContext(Dispatchers.IO) { api.searchGroupMembers(groupId, XuqmSDK.appId, 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.appId, keyword).data ?: emptyList() }
suspend fun searchUsers(keyword: String, size: Int = 20): List<UserProfile> =
withContext(Dispatchers.IO) { api.searchUsers(XuqmSDK.appId, keyword, size).data ?: emptyList() }
suspend fun searchGroups(keyword: String, size: Int = 20): List<ImGroup> =
withContext(Dispatchers.IO) { api.searchGroups(XuqmSDK.appId, keyword, size).data ?: emptyList() }
suspend fun searchMessages(
keyword: String? = null,
chatType: String? = null,
msgType: String? = null,
startTime: LocalDateTime? = null,
endTime: LocalDateTime? = null,
page: Int = 0,
size: Int = 20,
): PageResult<ImMessage> =
withContext(Dispatchers.IO) {
api.searchMessages(
XuqmSDK.appId,
keyword,
chatType,
msgType,
startTime?.toString(),
endTime?.toString(),
page,
size,
).data ?: PageResult()
}
suspend fun updateGroupInfo(groupId: String, name: String? = null, announcement: String? = null) = suspend fun updateGroupInfo(groupId: String, name: String? = null, announcement: String? = null) =
withContext(Dispatchers.IO) { api.updateGroupInfo(groupId, UpdateGroupRequest(name, announcement)) } withContext(Dispatchers.IO) { api.updateGroupInfo(groupId, UpdateGroupRequest(name, announcement)) }

查看文件

@ -2,12 +2,10 @@ 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.BlacklistEntry import com.xuqm.sdk.im.model.BlacklistEntry
import com.xuqm.sdk.im.model.EditMessageRequest
import com.xuqm.sdk.im.model.FriendRequest import com.xuqm.sdk.im.model.FriendRequest
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.PageResult
import com.xuqm.sdk.im.model.UserProfile import com.xuqm.sdk.im.model.UserProfile
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
@ -85,32 +83,6 @@ interface ImApi {
@Query("keyword") keyword: String? = null, @Query("keyword") keyword: String? = null,
): ApiResponse<List<ImGroup>> ): ApiResponse<List<ImGroup>>
@GET("api/im/admin/users/search")
suspend fun searchUsers(
@Query("appId") appId: String,
@Query("keyword") keyword: String,
@Query("size") size: Int = 20,
): ApiResponse<List<UserProfile>>
@GET("api/im/admin/groups/search")
suspend fun searchGroups(
@Query("appId") appId: String,
@Query("keyword") keyword: String,
@Query("size") size: Int = 20,
): ApiResponse<List<ImGroup>>
@GET("api/im/admin/messages/search")
suspend fun searchMessages(
@Query("appId") appId: String,
@Query("keyword") keyword: String? = null,
@Query("chatType") chatType: String? = null,
@Query("msgType") msgType: String? = null,
@Query("startTime") startTime: String? = null,
@Query("endTime") endTime: String? = null,
@Query("page") page: Int = 0,
@Query("size") size: Int = 20,
): ApiResponse<PageResult<ImMessage>>
@POST("api/im/groups") @POST("api/im/groups")
suspend fun createGroup( suspend fun createGroup(
@Query("appId") appId: String, @Query("appId") appId: String,
@ -120,20 +92,6 @@ interface ImApi {
@GET("api/im/groups/{groupId}") @GET("api/im/groups/{groupId}")
suspend fun getGroupInfo(@Path("groupId") groupId: String): ApiResponse<ImGroup> suspend fun getGroupInfo(@Path("groupId") groupId: String): ApiResponse<ImGroup>
@GET("api/im/groups/{groupId}/members")
suspend fun listGroupMembers(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
): ApiResponse<List<UserProfile>>
@GET("api/im/groups/{groupId}/members/search")
suspend fun searchGroupMembers(
@Path("groupId") groupId: String,
@Query("appId") appId: String,
@Query("keyword") keyword: String,
@Query("size") size: Int = 20,
): ApiResponse<List<UserProfile>>
@PUT("api/im/groups/{groupId}") @PUT("api/im/groups/{groupId}")
suspend fun updateGroupInfo( suspend fun updateGroupInfo(
@Path("groupId") groupId: String, @Path("groupId") groupId: String,
@ -291,19 +249,6 @@ interface ImApi {
@Query("chatType") chatType: String, @Query("chatType") chatType: String,
): ApiResponse<Unit> ): ApiResponse<Unit>
@PUT("api/im/messages/{messageId}")
suspend fun editMessage(
@Path("messageId") messageId: String,
@Query("appId") appId: String,
@Body request: EditMessageRequest,
): ApiResponse<ImMessage>
@POST("api/im/messages/{messageId}/revoke")
suspend fun revokeMessage(
@Path("messageId") messageId: String,
@Query("appId") appId: String,
): ApiResponse<ImMessage>
@PUT("api/im/conversations/{targetId}/draft") @PUT("api/im/conversations/{targetId}/draft")
suspend fun setDraft( suspend fun setDraft(
@Path("targetId") targetId: String, @Path("targetId") targetId: String,

查看文件

@ -7,7 +7,5 @@ interface ImEventListener {
fun onDisconnected(reason: String?) {} fun onDisconnected(reason: String?) {}
fun onMessage(message: ImMessage) {} fun onMessage(message: ImMessage) {}
fun onGroupMessage(message: ImMessage) {} fun onGroupMessage(message: ImMessage) {}
fun onRead(message: ImMessage) {}
fun onRevoke(message: ImMessage) {}
fun onError(error: String) {} fun onError(error: String) {}
} }

查看文件

@ -2,22 +2,6 @@ package com.xuqm.sdk.im.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class PageResult<T>(
val content: List<T> = emptyList(),
val totalElements: Long = 0,
val totalPages: Int = 0,
val size: Int = 0,
val number: Int = 0,
val numberOfElements: Int = 0,
val first: Boolean = false,
val last: Boolean = false,
val empty: Boolean = true,
)
data class EditMessageRequest(
val content: String,
)
data class ImMessage( data class ImMessage(
val id: String, val id: String,
val appId: String, val appId: String,
@ -30,9 +14,7 @@ data class ImMessage(
val status: String, val status: String,
val mentionedUserIds: String? = null, val mentionedUserIds: String? = null,
val groupReadCount: Int? = null, val groupReadCount: Int? = null,
val revoked: Boolean? = null,
val createdAt: Long, val createdAt: Long,
val editedAt: Long? = null,
) )
data class ConversationData( data class ConversationData(

查看文件

@ -1,199 +0,0 @@
/**
* XuqmGroup Update Service — Android Gradle Release Task
*
* Copy this file to your app module directory and apply it:
* apply(from = "xuqm_release.gradle.kts")
*
* Then run:
* ./gradlew xuqmRelease
*
* Config: xuqm.properties in the project root (or module root)
* ---
* xuqm.serverUrl=https://update.dev.xuqinmin.com
* xuqm.appId=your-app-id
* xuqm.apiToken=your-api-token
* ---
*
* The task:
* 1. Reads versionName / versionCode from the android extension
* 2. Checks the latest version on the update server
* 3. Aborts if the local versionCode is not greater than the server's
* 4. Assembles the release APK (assembleRelease)
* 5. Uploads the APK + metadata to the update service as a DRAFT
* 6. Optionally triggers server-side store submission
*/
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Files
import java.util.Properties
import java.util.UUID
// ── Config loading ────────────────────────────────────────────────────────
fun loadXuqmConfig(projectDir: File): Properties {
val props = Properties()
listOf(
File(projectDir, "xuqm.properties"),
File(projectDir.parentFile, "xuqm.properties"),
).firstOrNull { it.exists() }?.inputStream()?.use(props::load)
?: throw GradleException("xuqm.properties not found in module or project root")
return props
}
// ── HTTP helper ───────────────────────────────────────────────────────────
fun httpGet(url: String, token: String): String {
val client = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Bearer $token")
.GET().build()
return client.send(req, HttpResponse.BodyHandlers.ofString()).body()
}
/**
* Multipart POST — minimal implementation without external dependencies.
* Returns response body string.
*/
fun httpMultipartPost(url: String, token: String, parts: Map<String, Any>): String {
val boundary = UUID.randomUUID().toString()
val baos = java.io.ByteArrayOutputStream()
fun writeln(s: String) = baos.write(("$s\r\n").toByteArray())
for ((name, value) in parts) {
writeln("--$boundary")
when (value) {
is File -> {
writeln("Content-Disposition: form-data; name=\"$name\"; filename=\"${value.name}\"")
writeln("Content-Type: application/octet-stream")
writeln("")
baos.write(value.readBytes())
writeln("")
}
else -> {
writeln("Content-Disposition: form-data; name=\"$name\"")
writeln("")
writeln(value.toString())
}
}
}
writeln("--$boundary--")
val body = baos.toByteArray()
val client = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Bearer $token")
.header("Content-Type", "multipart/form-data; boundary=$boundary")
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build()
return client.send(req, HttpResponse.BodyHandlers.ofString()).body()
}
fun parseJson(json: String, key: String): String? =
Regex("\"$key\"\\s*:\\s*\"([^\"]+)\"").find(json)?.groupValues?.get(1)
fun parseJsonInt(json: String, key: String): Int? =
Regex("\"$key\"\\s*:\\s*(\\d+)").find(json)?.groupValues?.get(1)?.toIntOrNull()
// ── Task registration ─────────────────────────────────────────────────────
tasks.register("xuqmRelease") {
group = "xuqm"
description = "Build release APK and upload to XuqmGroup Update Service"
// Ensure assembleRelease runs first
dependsOn("assembleRelease")
doLast {
val cfg = loadXuqmConfig(projectDir)
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
val appId = cfg.getProperty("xuqm.appId") ?: throw GradleException("xuqm.appId missing")
val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing")
val storeTargets = cfg.getProperty("xuqm.storeTargets", "") // e.g. "HUAWEI,MI,OPPO"
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean()
val scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "")
val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "")
// ── 1. Read local version ──────────────────────────────────────────
val android = project.extensions.findByName("android")
?: throw GradleException("This task must run in an Android module")
val defaultConfig = android::class.java.getMethod("getDefaultConfig").invoke(android)
val versionName = defaultConfig::class.java.getMethod("getVersionName").invoke(defaultConfig) as? String
?: throw GradleException("versionName not set")
val versionCode = (defaultConfig::class.java.getMethod("getVersionCode").invoke(defaultConfig) as? Int)
?: throw GradleException("versionCode not set")
val applicationId = defaultConfig::class.java.getMethod("getApplicationId").invoke(defaultConfig) as? String ?: ""
println("[xuqm] Local version: $versionName ($versionCode), packageName: $applicationId")
// ── 2. Check server latest ─────────────────────────────────────────
val listResp = httpGet("$serverUrl/api/v1/updates/app/list?appId=$appId&platform=ANDROID", apiToken)
// Find highest published versionCode
val serverVersionCode = Regex("\"versionCode\"\\s*:\\s*(\\d+)").findAll(listResp)
.mapNotNull { it.groupValues[1].toIntOrNull() }
.maxOrNull() ?: 0
println("[xuqm] Server latest versionCode: $serverVersionCode")
if (versionCode <= serverVersionCode) {
throw GradleException(
"[xuqm] Local versionCode ($versionCode) must be greater than server ($serverVersionCode). " +
"Please bump versionCode in build.gradle.kts before releasing."
)
}
// ── 3. Locate APK ─────────────────────────────────────────────────
val apkDir = File(buildDir, "outputs/apk/release")
val apkFile = apkDir.listFiles { f -> f.extension == "apk" }?.firstOrNull()
?: throw GradleException("No APK found in ${apkDir.absolutePath}. Did assembleRelease succeed?")
println("[xuqm] APK: ${apkFile.absolutePath}")
// ── 4. Upload to update service ───────────────────────────────────
val parts = mutableMapOf<String, Any>(
"appId" to appId,
"platform" to "ANDROID",
"versionName" to versionName,
"versionCode" to versionCode,
"forceUpdate" to "false",
"autoPublishAfterReview" to autoPublish.toString(),
"apkFile" to apkFile,
)
if (applicationId.isNotBlank()) parts["packageName"] = applicationId
if (storeTargets.isNotBlank()) parts["storeSubmitTargets"] = "[\"${storeTargets.split(",").joinToString("\",\"")}\"]"
if (scheduledAt.isNotBlank()) parts["scheduledPublishAt"] = scheduledAt
if (webhookUrl.isNotBlank()) parts["webhookUrl"] = webhookUrl
println("[xuqm] Uploading APK...")
val uploadResp = httpMultipartPost("$serverUrl/api/v1/updates/app/upload", apiToken, parts)
val versionId = parseJson(uploadResp, "id")
?: throw GradleException("[xuqm] Upload failed:\n$uploadResp")
println("[xuqm] Uploaded, version ID: $versionId")
// ── 5. Trigger server-side store submission ────────────────────────
if (storeTargets.isNotBlank()) {
println("[xuqm] Triggering store submission: $storeTargets ...")
val storeBody = """{"storeTypes":[${storeTargets.split(",").joinToString(",") { "\"$it\"" }}]}"""
val client = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder(URI.create("$serverUrl/api/v1/updates/store/app/$versionId/execute-submit"))
.header("Authorization", "Bearer $apiToken")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(storeBody))
.build()
val storeResp = client.send(req, HttpResponse.BodyHandlers.ofString())
println("[xuqm] Store submission triggered (HTTP ${storeResp.statusCode()})")
}
println("[xuqm] Done. Version is in DRAFT state.")
if (scheduledAt.isNotBlank()) {
println("[xuqm] Will auto-publish at: $scheduledAt")
} else if (autoPublish) {
println("[xuqm] Will auto-publish after all store reviews pass.")
} else {
println("[xuqm] Publish manually: POST $serverUrl/api/v1/updates/app/$versionId/publish")
}
}
}

查看文件

@ -5,10 +5,11 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.file.FileSDK import com.xuqm.sdk.file.FileTransfer
import com.xuqm.sdk.core.ServiceEndpointRegistry import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.network.ApiClient import com.xuqm.sdk.network.ApiClient
import com.xuqm.sdk.update.api.UpdateApi import com.xuqm.sdk.update.api.UpdateApi
import com.xuqm.sdk.update.model.RnUpdateInfo
import com.xuqm.sdk.update.model.UpdateInfo import com.xuqm.sdk.update.model.UpdateInfo
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -21,6 +22,10 @@ object UpdateSDK {
private fun normalizeDownloadUrl(rawUrl: String?): String? { private fun normalizeDownloadUrl(rawUrl: String?): String? {
if (rawUrl.isNullOrBlank()) return rawUrl if (rawUrl.isNullOrBlank()) return rawUrl
if (rawUrl.contains("/api/v1/updates/api/v1/rn/files/")) {
return rawUrl.replace("/api/v1/updates/api/v1/rn/files/", "/api/v1/rn/files/")
}
return runCatching { return runCatching {
val uri = Uri.parse(rawUrl) val uri = Uri.parse(rawUrl)
if (uri.path?.startsWith("/files/apk/") == true) { if (uri.path?.startsWith("/files/apk/") == true) {
@ -48,7 +53,7 @@ object UpdateSDK {
onProgress: (Int) -> Unit = {}, onProgress: (Int) -> Unit = {},
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
val apkFile = File(context.getExternalFilesDir(null), "update.apk") val apkFile = File(context.getExternalFilesDir(null), "update.apk")
FileSDK.downloadToFile(downloadUrl, apkFile, onProgress) FileTransfer.downloadToFile(downloadUrl, apkFile, onProgress)
withContext(Dispatchers.Main) { installApk(context, apkFile) } withContext(Dispatchers.Main) { installApk(context, apkFile) }
} }
@ -60,4 +65,14 @@ object UpdateSDK {
} }
context.startActivity(intent) context.startActivity(intent)
} }
suspend fun checkRnUpdate(moduleId: String, currentVersion: String): RnUpdateInfo? =
withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
runCatching {
api.checkRnUpdate(XuqmSDK.appId, moduleId, "ANDROID", currentVersion).data?.let {
it.copy(downloadUrl = normalizeDownloadUrl(it.downloadUrl) ?: it.downloadUrl)
}
}.getOrNull()
}
} }

查看文件

@ -1,6 +1,7 @@
package com.xuqm.sdk.update.api package com.xuqm.sdk.update.api
import com.xuqm.sdk.update.model.UpdateInfo import com.xuqm.sdk.update.model.UpdateInfo
import com.xuqm.sdk.update.model.RnUpdateInfo
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
@ -13,4 +14,12 @@ interface UpdateApi {
@Query("platform") platform: String, @Query("platform") platform: String,
@Query("currentVersionCode") currentVersionCode: Int, @Query("currentVersionCode") currentVersionCode: Int,
): ApiResponse<UpdateInfo> ): ApiResponse<UpdateInfo>
@GET("api/v1/rn/update/check")
suspend fun checkRnUpdate(
@Query("appId") appId: String,
@Query("moduleId") moduleId: String,
@Query("platform") platform: String,
@Query("currentVersion") currentVersion: String,
): ApiResponse<RnUpdateInfo>
} }

查看文件

@ -10,3 +10,12 @@ data class UpdateInfo(
val appStoreUrl: String = "", val appStoreUrl: String = "",
val marketUrl: String = "", val marketUrl: String = "",
) )
data class RnUpdateInfo(
val needsUpdate: Boolean,
val latestVersion: String = "",
val downloadUrl: String = "",
val md5: String = "",
val minCommonVersion: String = "0.0.0",
val note: String = "",
)