比较提交
没有共同的提交。46777173432347e6734b1f96a9b809e143a8620b 和 dcb263edc6a4f984484fa91226c535e01b3b1e88 的历史完全不同。
4677717343
...
dcb263edc6
@ -34,17 +34,6 @@ class LocalImCache(context: Context) {
|
||||
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> =
|
||||
readMessageList(historyKey(targetId, chatType))
|
||||
|
||||
@ -144,10 +133,7 @@ class LocalImCache(context: Context) {
|
||||
content = obj.optString("content"),
|
||||
status = obj.optString("status"),
|
||||
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"),
|
||||
editedAt = obj.optLong("editedAt").takeIf { obj.has("editedAt") },
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -188,10 +174,7 @@ class LocalImCache(context: Context) {
|
||||
put("content", message.content)
|
||||
put("status", message.status)
|
||||
put("mentionedUserIds", message.mentionedUserIds ?: "")
|
||||
message.groupReadCount?.let { put("groupReadCount", it) }
|
||||
put("revoked", message.revoked ?: false)
|
||||
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.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@ -69,10 +68,10 @@ import com.xuqm.sdk.ui.SearchBarField
|
||||
import coil3.compose.AsyncImage
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.FileProvider
|
||||
import com.xuqm.sdk.file.FileSDK
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.TextRange
|
||||
|
||||
@ -92,7 +91,6 @@ fun ChatScreen(
|
||||
val draftText by viewModel.draftText.collectAsStateWithLifecycle()
|
||||
val mentionableUsers by viewModel.mentionableUsers.collectAsStateWithLifecycle()
|
||||
val replyTargetMessage by viewModel.replyTargetMessage.collectAsStateWithLifecycle()
|
||||
val editingMessage by viewModel.editingMessage.collectAsStateWithLifecycle()
|
||||
val scrollSignal by viewModel.scrollToBottomSignal.collectAsStateWithLifecycle()
|
||||
val isLoadingMore by viewModel.isLoadingMore.collectAsStateWithLifecycle()
|
||||
val hasMoreHistory by viewModel.hasMoreHistory.collectAsStateWithLifecycle()
|
||||
@ -105,7 +103,6 @@ fun ChatScreen(
|
||||
var draftValue by remember { mutableStateOf(TextFieldValue(draftText)) }
|
||||
var showSearchBar by remember { mutableStateOf(false) }
|
||||
val replyTarget = replyTargetMessage
|
||||
val editingTarget = editingMessage
|
||||
var pendingCameraAction by remember { mutableStateOf<CameraAction?>(null) }
|
||||
var pendingCaptureUri by remember { mutableStateOf<Uri?>(null) }
|
||||
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(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -446,7 +426,6 @@ fun ChatScreen(
|
||||
isOwn = msg.fromId == viewModel.currentUserId,
|
||||
currentUserId = viewModel.currentUserId,
|
||||
onReply = viewModel::startReply,
|
||||
onEdit = viewModel::startEdit,
|
||||
)
|
||||
}
|
||||
if (searchResults.isEmpty()) {
|
||||
@ -476,7 +455,6 @@ fun ChatScreen(
|
||||
isOwn = msg.fromId == viewModel.currentUserId,
|
||||
currentUserId = viewModel.currentUserId,
|
||||
onReply = viewModel::startReply,
|
||||
onEdit = viewModel::startEdit,
|
||||
)
|
||||
}
|
||||
if (isLoadingMore) {
|
||||
@ -534,11 +512,8 @@ private fun MessageBubble(
|
||||
isOwn: Boolean,
|
||||
currentUserId: String,
|
||||
onReply: (ImMessage) -> Unit,
|
||||
onEdit: (ImMessage) -> Unit,
|
||||
) {
|
||||
val media = message.mediaContent()
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val arrangement = if (isOwn) Arrangement.End else Arrangement.Start
|
||||
val bubbleColor = if (isOwn) MaterialTheme.colorScheme.primaryContainer
|
||||
else MaterialTheme.colorScheme.surfaceVariant
|
||||
@ -567,13 +542,7 @@ private fun MessageBubble(
|
||||
.padding(horizontal = 4.dp)
|
||||
.combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
if (isOwn && message.msgType.uppercase() == "TEXT") {
|
||||
onEdit(message)
|
||||
} else {
|
||||
onReply(message)
|
||||
}
|
||||
},
|
||||
onLongClick = { onReply(message) },
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||
@ -588,27 +557,6 @@ private fun MessageBubble(
|
||||
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()
|
||||
if (mentionedUserIds.isNotBlank() &&
|
||||
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"
|
||||
}
|
||||
|
||||
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? {
|
||||
if (!value.selection.collapsed) return null
|
||||
val cursor = value.selection.end.coerceIn(0, value.text.length)
|
||||
|
||||
@ -46,9 +46,6 @@ class ChatViewModel : ViewModel() {
|
||||
private val _replyTargetMessage = MutableStateFlow<ImMessage?>(null)
|
||||
val replyTargetMessage: StateFlow<ImMessage?> = _replyTargetMessage
|
||||
|
||||
private val _editingMessage = MutableStateFlow<ImMessage?>(null)
|
||||
val editingMessage: StateFlow<ImMessage?> = _editingMessage
|
||||
|
||||
private val _mentionableUsers = MutableStateFlow<List<UserData>>(emptyList())
|
||||
val mentionableUsers: StateFlow<List<UserData>> = _mentionableUsers
|
||||
|
||||
@ -74,14 +71,6 @@ class ChatViewModel : ViewModel() {
|
||||
override fun onGroupMessage(message: ImMessage) {
|
||||
handleIncomingMessage(message)
|
||||
}
|
||||
|
||||
override fun onRead(message: ImMessage) {
|
||||
handleIncomingMessage(message)
|
||||
}
|
||||
|
||||
override fun onRevoke(message: ImMessage) {
|
||||
handleIncomingMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
fun init(targetId: String, chatType: String) {
|
||||
@ -101,7 +90,6 @@ class ChatViewModel : ViewModel() {
|
||||
_draftText.value = cache.loadDraft(targetId, chatType)
|
||||
_mentionableUsers.value = emptyList()
|
||||
_replyTargetMessage.value = null
|
||||
_editingMessage.value = null
|
||||
ImSDK.addListener(listener)
|
||||
if (chatType == "GROUP") {
|
||||
ImSDK.subscribeGroup(targetId)
|
||||
@ -112,10 +100,6 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
viewModelScope.launch {
|
||||
runCatching { ImSDK.markRead(targetId, chatType) }
|
||||
.onSuccess {
|
||||
cache.markConversationRead(targetId, chatType)
|
||||
AppDependencies.notifyConversationChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,25 +170,6 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
fun sendText(content: String) {
|
||||
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 sent = if (replyTarget != null) {
|
||||
ImSDK.sendQuoteMessage(
|
||||
@ -225,7 +190,6 @@ class ChatViewModel : ViewModel() {
|
||||
_draftText.value = ""
|
||||
cache.clearDraft(targetId, chatType)
|
||||
_replyTargetMessage.value = null
|
||||
_editingMessage.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,28 +217,6 @@ class ChatViewModel : ViewModel() {
|
||||
_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 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) {
|
||||
viewModelScope.launch {
|
||||
runCatching { ImSDK.markRead(targetId, chatType) }
|
||||
.onSuccess {
|
||||
cache.markConversationRead(targetId, chatType)
|
||||
AppDependencies.notifyConversationChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -506,10 +444,6 @@ class ChatViewModel : ViewModel() {
|
||||
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()) {
|
||||
cache.upsertConversation(
|
||||
ConversationData(
|
||||
@ -550,7 +484,6 @@ class ChatViewModel : ViewModel() {
|
||||
status = incoming.status.ifBlank { existing.status },
|
||||
mentionedUserIds = incoming.mentionedUserIds ?: existing.mentionedUserIds,
|
||||
groupReadCount = incoming.groupReadCount ?: existing.groupReadCount,
|
||||
revoked = incoming.revoked ?: existing.revoked,
|
||||
createdAt = existing.createdAt,
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,8 +83,6 @@ class ContactViewModel(
|
||||
private val listener = object : ImEventListener {
|
||||
override fun onMessage(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 {
|
||||
|
||||
@ -49,22 +49,6 @@ class ConversationViewModel : ViewModel() {
|
||||
)
|
||||
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 {
|
||||
|
||||
@ -313,7 +313,6 @@ fun GroupSettingsScreen(
|
||||
) {
|
||||
val group by viewModel.currentGroup.collectAsStateWithLifecycle()
|
||||
val members by viewModel.members.collectAsStateWithLifecycle()
|
||||
val groupMembers by viewModel.groupMembers.collectAsStateWithLifecycle()
|
||||
val joinRequests by viewModel.joinRequests.collectAsStateWithLifecycle()
|
||||
val connectionState by ImSDK.connectionState.collectAsStateWithLifecycle()
|
||||
var showEditDialog by remember { mutableStateOf(false) }
|
||||
@ -321,15 +320,14 @@ fun GroupSettingsScreen(
|
||||
var showRemoveMemberDialog by remember { mutableStateOf(false) }
|
||||
var editName by remember { mutableStateOf("") }
|
||||
var editAnnouncement by remember { mutableStateOf("") }
|
||||
val memberProfiles = remember(group, members, groupMembers) {
|
||||
val sourceMembers = if (groupMembers.isNotEmpty()) groupMembers else members
|
||||
val memberProfiles = remember(group, members) {
|
||||
val ids = group?.memberIds
|
||||
?.split(",")
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotBlank() }
|
||||
.orEmpty()
|
||||
ids.mapNotNull { id ->
|
||||
sourceMembers.firstOrNull { it.userId == id } ?: UserData(id, "")
|
||||
members.firstOrNull { it.userId == id } ?: UserData(id, "")
|
||||
}
|
||||
}
|
||||
val memberNameById = remember(members) {
|
||||
|
||||
@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.xuqm.sdk.im.ImSDK
|
||||
import com.xuqm.sdk.im.model.GroupJoinRequest
|
||||
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.repo.AuthRepository
|
||||
import com.xuqm.sdk.sample.di.AppDependencies
|
||||
@ -23,9 +22,6 @@ class GroupViewModel : ViewModel() {
|
||||
private val _members = MutableStateFlow<List<UserData>>(emptyList())
|
||||
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())
|
||||
val publicGroups: StateFlow<List<ImGroup>> = _publicGroups
|
||||
|
||||
@ -62,10 +58,7 @@ class GroupViewModel : ViewModel() {
|
||||
fun loadGroupInfo(groupId: String) {
|
||||
viewModelScope.launch {
|
||||
runCatching { ImSDK.getGroupInfo(groupId) }
|
||||
.onSuccess {
|
||||
_currentGroup.value = it
|
||||
loadGroupMembersInternal(groupId)
|
||||
}
|
||||
.onSuccess { _currentGroup.value = it }
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +75,6 @@ class GroupViewModel : ViewModel() {
|
||||
try {
|
||||
loadGroupsInternal()
|
||||
loadMembersInternal()
|
||||
_currentGroup.value?.let { loadGroupMembersInternal(it.id) }
|
||||
loadPublicGroupsInternal(_publicGroupQuery.value)
|
||||
} finally {
|
||||
_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() {
|
||||
runCatching { ImSDK.listGroups() }
|
||||
.onSuccess { _groups.value = it }
|
||||
@ -201,8 +186,4 @@ class GroupViewModel : ViewModel() {
|
||||
runCatching { ImSDK.listPublicGroups(keyword.ifBlank { null }) }
|
||||
.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 com.xuqm.sdk.core.ServiceEndpointRegistry
|
||||
import com.xuqm.sdk.network.ApiClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Part
|
||||
import java.io.File
|
||||
|
||||
data class FileUploadResult(
|
||||
val url: String,
|
||||
@ -93,31 +90,4 @@ object FileSDK {
|
||||
"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)
|
||||
},
|
||||
)
|
||||
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") {
|
||||
listeners.forEach { it.onGroupMessage(msg) }
|
||||
} else {
|
||||
|
||||
@ -6,7 +6,6 @@ import com.xuqm.sdk.core.ServiceEndpointRegistry
|
||||
import com.xuqm.sdk.im.api.AddMemberRequest
|
||||
import com.xuqm.sdk.im.api.CreateGroupRequest
|
||||
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.SetMutedRequest
|
||||
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.ImGroup
|
||||
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.FriendRequest
|
||||
import com.xuqm.sdk.file.FileUploadResult
|
||||
@ -143,12 +140,6 @@ object ImSDK {
|
||||
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(
|
||||
toId: String,
|
||||
chatType: String,
|
||||
@ -442,48 +433,6 @@ object ImSDK {
|
||||
).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> =
|
||||
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() }
|
||||
|
||||
@ -495,43 +444,9 @@ object ImSDK {
|
||||
suspend fun getGroupInfo(groupId: String): ImGroup? =
|
||||
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> =
|
||||
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) =
|
||||
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.BlacklistEntry
|
||||
import com.xuqm.sdk.im.model.EditMessageRequest
|
||||
import com.xuqm.sdk.im.model.FriendRequest
|
||||
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.PageResult
|
||||
import com.xuqm.sdk.im.model.UserProfile
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
@ -85,32 +83,6 @@ interface ImApi {
|
||||
@Query("keyword") keyword: String? = null,
|
||||
): 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")
|
||||
suspend fun createGroup(
|
||||
@Query("appId") appId: String,
|
||||
@ -120,20 +92,6 @@ interface ImApi {
|
||||
@GET("api/im/groups/{groupId}")
|
||||
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}")
|
||||
suspend fun updateGroupInfo(
|
||||
@Path("groupId") groupId: String,
|
||||
@ -291,19 +249,6 @@ interface ImApi {
|
||||
@Query("chatType") chatType: String,
|
||||
): 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")
|
||||
suspend fun setDraft(
|
||||
@Path("targetId") targetId: String,
|
||||
|
||||
@ -7,7 +7,5 @@ interface ImEventListener {
|
||||
fun onDisconnected(reason: String?) {}
|
||||
fun onMessage(message: ImMessage) {}
|
||||
fun onGroupMessage(message: ImMessage) {}
|
||||
fun onRead(message: ImMessage) {}
|
||||
fun onRevoke(message: ImMessage) {}
|
||||
fun onError(error: String) {}
|
||||
}
|
||||
|
||||
@ -2,22 +2,6 @@ package com.xuqm.sdk.im.model
|
||||
|
||||
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(
|
||||
val id: String,
|
||||
val appId: String,
|
||||
@ -30,9 +14,7 @@ data class ImMessage(
|
||||
val status: String,
|
||||
val mentionedUserIds: String? = null,
|
||||
val groupReadCount: Int? = null,
|
||||
val revoked: Boolean? = null,
|
||||
val createdAt: Long,
|
||||
val editedAt: Long? = null,
|
||||
)
|
||||
|
||||
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 androidx.core.content.FileProvider
|
||||
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.network.ApiClient
|
||||
import com.xuqm.sdk.update.api.UpdateApi
|
||||
import com.xuqm.sdk.update.model.RnUpdateInfo
|
||||
import com.xuqm.sdk.update.model.UpdateInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -21,6 +22,10 @@ object UpdateSDK {
|
||||
private fun normalizeDownloadUrl(rawUrl: String?): String? {
|
||||
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 {
|
||||
val uri = Uri.parse(rawUrl)
|
||||
if (uri.path?.startsWith("/files/apk/") == true) {
|
||||
@ -48,7 +53,7 @@ object UpdateSDK {
|
||||
onProgress: (Int) -> Unit = {},
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val apkFile = File(context.getExternalFilesDir(null), "update.apk")
|
||||
FileSDK.downloadToFile(downloadUrl, apkFile, onProgress)
|
||||
FileTransfer.downloadToFile(downloadUrl, apkFile, onProgress)
|
||||
withContext(Dispatchers.Main) { installApk(context, apkFile) }
|
||||
}
|
||||
|
||||
@ -60,4 +65,14 @@ object UpdateSDK {
|
||||
}
|
||||
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
|
||||
|
||||
import com.xuqm.sdk.update.model.UpdateInfo
|
||||
import com.xuqm.sdk.update.model.RnUpdateInfo
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
@ -13,4 +14,12 @@ interface UpdateApi {
|
||||
@Query("platform") platform: String,
|
||||
@Query("currentVersionCode") currentVersionCode: Int,
|
||||
): 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 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 = "",
|
||||
)
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户