From 58455cf32aa467abd3ae07963466f9c23732cda5 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 20 Mar 2026 19:20:06 +0500 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BB=D0=BE=D0=B6=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=B3=D0=B0=D0=BB=D0=BE=D1=87=D0=BA=D0=B8=20=D0=B8=20=D1=81?= =?UTF-8?q?=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messenger/data/MessageRepository.kt | 65 +++++++++-- .../messenger/database/MessageEntities.kt | 7 +- .../messenger/ui/chats/ChatDetailScreen.kt | 103 ++++++++++++++++- .../messenger/ui/chats/ChatViewModel.kt | 104 +++++++++++------- .../messenger/ui/chats/ChatsListScreen.kt | 11 +- .../rosetta/messenger/update/UpdateManager.kt | 42 ++++++- 6 files changed, 270 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index f90c150..8645c66 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -806,6 +806,15 @@ class MessageRepository private constructor(private val context: Context) { packet.chachaKey } + val isSelfDialog = packet.toPublicKey.trim() == account + // Для исходящих сообщений статус доставки меняется ТОЛЬКО по PacketDelivery. + val initialDeliveredStatus = + if (isOwnMessage && !isSelfDialog) { + DeliveryStatus.WAITING.value + } else { + DeliveryStatus.DELIVERED.value + } + // Создаем entity для кэша и возможной вставки val entity = MessageEntity( @@ -817,7 +826,7 @@ class MessageRepository private constructor(private val context: Context) { chachaKey = storedChachaKey, read = 0, fromMe = if (isOwnMessage) 1 else 0, - delivered = DeliveryStatus.DELIVERED.value, + delivered = initialDeliveredStatus, messageId = messageId, // 🔥 Используем сгенерированный messageId! plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст attachments = attachmentsJson, @@ -946,18 +955,34 @@ class MessageRepository private constructor(private val context: Context) { // Desktop parity (group): from=groupMember, to=groupId -> mark own group messages as read. if (!isOwnReadSync && isGroupDialogKey(toPublicKey)) { val dialogKey = getDialogKey(toPublicKey) - messageDao.markAllAsRead(account, toPublicKey) + val updatedRows = messageDao.markAllAsRead(account, toPublicKey) - val readCount = messageCache[dialogKey]?.value?.count { it.isFromMe && !it.isRead } ?: 0 + val readCount = + messageCache[dialogKey]?.value?.count { + it.isFromMe && + !it.isRead && + (it.deliveryStatus == DeliveryStatus.DELIVERED || + it.deliveryStatus == DeliveryStatus.READ) + } ?: 0 messageCache[dialogKey]?.let { flow -> flow.value = flow.value.map { msg -> - if (msg.isFromMe && !msg.isRead) msg.copy(isRead = true) else msg + if (msg.isFromMe && + !msg.isRead && + (msg.deliveryStatus == DeliveryStatus.DELIVERED || + msg.deliveryStatus == DeliveryStatus.READ) + ) { + msg.copy(isRead = true, deliveryStatus = DeliveryStatus.READ) + } else { + msg + } } } - _deliveryStatusEvents.tryEmit(DeliveryStatusUpdate(dialogKey, "", DeliveryStatus.READ)) - MessageLogger.logReadStatus(fromPublicKey = toPublicKey, messagesCount = readCount) + if (updatedRows > 0) { + _deliveryStatusEvents.tryEmit(DeliveryStatusUpdate(dialogKey, "", DeliveryStatus.READ)) + } + MessageLogger.logReadStatus(fromPublicKey = toPublicKey, messagesCount = minOf(readCount, updatedRows)) dialogDao.updateDialogFromMessages(account, toPublicKey) return } @@ -981,20 +1006,36 @@ class MessageRepository private constructor(private val context: Context) { } // Opponent read our outgoing messages. - messageDao.markAllAsRead(account, opponentKey) + val updatedRows = messageDao.markAllAsRead(account, opponentKey) - val readCount = messageCache[dialogKey]?.value?.count { it.isFromMe && !it.isRead } ?: 0 + val readCount = + messageCache[dialogKey]?.value?.count { + it.isFromMe && + !it.isRead && + (it.deliveryStatus == DeliveryStatus.DELIVERED || + it.deliveryStatus == DeliveryStatus.READ) + } ?: 0 messageCache[dialogKey]?.let { flow -> flow.value = flow.value.map { msg -> - if (msg.isFromMe && !msg.isRead) msg.copy(isRead = true) else msg + if (msg.isFromMe && + !msg.isRead && + (msg.deliveryStatus == DeliveryStatus.DELIVERED || + msg.deliveryStatus == DeliveryStatus.READ) + ) { + msg.copy(isRead = true, deliveryStatus = DeliveryStatus.READ) + } else { + msg + } } } - // Notify current dialog UI: all outgoing messages are now read. - _deliveryStatusEvents.tryEmit(DeliveryStatusUpdate(dialogKey, "", DeliveryStatus.READ)) + // Notify current dialog UI only when there are real DB read updates. + if (updatedRows > 0) { + _deliveryStatusEvents.tryEmit(DeliveryStatusUpdate(dialogKey, "", DeliveryStatus.READ)) + } - MessageLogger.logReadStatus(fromPublicKey = opponentKey, messagesCount = readCount) + MessageLogger.logReadStatus(fromPublicKey = opponentKey, messagesCount = minOf(readCount, updatedRows)) dialogDao.updateDialogFromMessages(account, opponentKey) } diff --git a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt index e592a88..fa3a803 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -400,10 +400,13 @@ interface MessageDao { @Query( """ UPDATE messages SET read = 1 - WHERE account = :account AND to_public_key = :opponent AND from_me = 1 + WHERE account = :account + AND to_public_key = :opponent + AND from_me = 1 + AND delivered IN (1, 3) """ ) - suspend fun markAllAsRead(account: String, opponent: String) + suspend fun markAllAsRead(account: String, opponent: String): Int /** 🔥 DEBUG: Получить последнее сообщение в диалоге для отладки */ @Query( diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 967a1be..2a0fad3 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -159,21 +159,68 @@ private data class IncomingRunAvatarUiState( val overlays: List ) +private val copiedTimeRegex = Regex("^\\d{1,2}:\\d{2}(?:\\s?[AaPp][Mm])?$") + +private fun sanitizeCopyText(rawText: String, senderName: String): String { + if (rawText.isBlank()) return "" + + val normalizedSender = senderName.trim() + val lines = + rawText + .replace("\r\n", "\n") + .split('\n') + .map { it.trim() } + .filter { it.isNotEmpty() } + .toMutableList() + + while (lines.isNotEmpty()) { + val first = lines.first() + val isSenderLine = + normalizedSender.isNotBlank() && + first.equals(normalizedSender, ignoreCase = true) + val isForwardHeader = first.startsWith("Forwarded from ", ignoreCase = true) + if (!isSenderLine && !isForwardHeader) break + lines.removeAt(0) + } + + while (lines.isNotEmpty() && copiedTimeRegex.matches(lines.last())) { + lines.removeAt(lines.lastIndex) + } + + return lines.joinToString("\n").trim() +} + private fun extractCopyableMessageText(message: ChatMessage): String { - val directText = message.text.trim() + val directText = sanitizeCopyText(message.text, message.senderName) if (directText.isNotEmpty()) { return directText } val forwardedText = message.forwardedMessages - .mapNotNull { forwarded -> forwarded.text.trim().takeIf { it.isNotEmpty() } } + .mapNotNull { forwarded -> + sanitizeCopyText( + rawText = forwarded.text, + senderName = + forwarded.forwardedFromName + .ifBlank { forwarded.senderName } + ) + .takeIf { it.isNotEmpty() } + } .joinToString("\n\n") if (forwardedText.isNotEmpty()) { return forwardedText } - val replyText = message.replyData?.text?.trim().orEmpty() + val replyText = + message.replyData + ?.let { reply -> + sanitizeCopyText( + rawText = reply.text, + senderName = reply.forwardedFromName.ifBlank { reply.senderName } + ) + } + .orEmpty() if (replyText.isNotEmpty()) { return replyText } @@ -181,6 +228,48 @@ private fun extractCopyableMessageText(message: ChatMessage): String { return "" } +@Composable +private fun GroupRejoinRequiredState( + isDarkTheme: Boolean, + hasChatWallpaper: Boolean, + modifier: Modifier = Modifier +) { + val titleColor = if (isDarkTheme || hasChatWallpaper) Color.White else Color(0xFF1C1C1E) + val subtitleColor = titleColor.copy(alpha = 0.72f) + + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.speech)) + val progress by + animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever + ) + + Column( + modifier = modifier.fillMaxSize().padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier.size(120.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Join Group Again", + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + color = titleColor + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Group sync key is missing. Rejoin this group to load messages.", + fontSize = 14.sp, + color = subtitleColor + ) + } +} + @OptIn( ExperimentalMaterial3Api::class, androidx.compose.foundation.ExperimentalFoundationApi::class, @@ -636,6 +725,7 @@ fun ChatDetailScreen( // If typing, the user is obviously online — never show "offline" while typing val isOnline = rawIsOnline || isTyping val isLoading by viewModel.isLoading.collectAsState() // 🔥 Для скелетона + val groupRequiresRejoin by viewModel.groupRequiresRejoin.collectAsState() val showMessageSkeleton by produceState(initialValue = false, key1 = isLoading) { if (!isLoading) { @@ -2402,6 +2492,13 @@ fun ChatDetailScreen( label = "listBottomPadding" ) when { + groupRequiresRejoin -> { + GroupRejoinRequiredState( + isDarkTheme = isDarkTheme, + hasChatWallpaper = hasChatWallpaper, + modifier = Modifier.fillMaxSize() + ) + } // 🔥 СКЕЛЕТОН - показываем пока загружаются // сообщения showMessageSkeleton -> { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 3ca1e9d..f708a28 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -166,6 +166,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { private val _isLoadingMore = MutableStateFlow(false) val isLoadingMore: StateFlow = _isLoadingMore.asStateFlow() + private val _groupRequiresRejoin = MutableStateFlow(false) + val groupRequiresRejoin: StateFlow = _groupRequiresRejoin.asStateFlow() + private val _opponentTyping = MutableStateFlow(false) val opponentTyping: StateFlow = _opponentTyping.asStateFlow() private var typingTimeoutJob: kotlinx.coroutines.Job? = null @@ -448,10 +451,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val nextStatus = when (msg.status) { - // Read event can promote only already-sent/delivered messages. - MessageStatus.SENT, + // Read event can promote only messages that were already delivery-confirmed. MessageStatus.DELIVERED, MessageStatus.READ -> MessageStatus.READ + MessageStatus.SENT, MessageStatus.SENDING, MessageStatus.ERROR -> msg.status } @@ -609,6 +612,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { if (!isGroupDialogKey(publicKey)) { groupKeyCache.remove(publicKey) } + _groupRequiresRejoin.value = false groupSenderNameCache.clear() groupSenderResolveRequested.clear() @@ -735,6 +739,25 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { loadingJob = viewModelScope.launch(Dispatchers.IO) { try { + if (isGroupDialogKey(opponent)) { + val hasGroupKey = resolveGroupKeyForDialog(opponent) != null + withContext(Dispatchers.Main.immediate) { + _groupRequiresRejoin.value = !hasGroupKey + if (!hasGroupKey) { + _messages.value = emptyList() + _isLoading.value = false + } + } + if (!hasGroupKey) { + hasMoreMessages = false + currentOffset = 0 + isLoadingMessages = false + return@launch + } + } else { + _groupRequiresRejoin.value = false + } + // 🔥 МГНОВЕННАЯ загрузка из кэша если есть! val cachedMessages = dialogMessagesCache[cacheKey(account, dialogKey)] @@ -2574,9 +2597,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } // 3. 🎯 UI обновление в Main потоке + // Для обычных диалогов статус остаётся SENDING до PacketDelivery(messageId). withContext(Dispatchers.Main) { - // 📁 Для Saved Messages - сразу SENT, для обычных - ждём delivery - updateMessageStatus(messageId, MessageStatus.SENT) + if (isSavedMessages) { + updateMessageStatus(messageId, MessageStatus.SENT) + } } // 4. 💾 Сохранение в БД с attachments @@ -2923,13 +2948,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { updateMessageStatusAndAttachmentsInDb( messageId = messageId, - delivered = 1, + delivered = if (isSavedMessages) 1 else 0, attachmentsJson = finalAttachmentsJson ) if (isCurrentDialogTarget) { withContext(Dispatchers.Main) { - updateMessageStatus(messageId, MessageStatus.SENT) + if (isSavedMessages) { + updateMessageStatus(messageId, MessageStatus.SENT) + } } } refreshTargetDialog() @@ -3275,19 +3302,22 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // (сообщение уже существует в БД от optimistic UI, поэтому просто обновляем) val finalAttachmentsJson = attachmentsJson // Уже без localUri - if (!isSavedMessages) { - updateMessageStatusAndAttachmentsInDb(messageId, 1, finalAttachmentsJson) - } else { - updateMessageStatusAndAttachmentsInDb(messageId, 1, finalAttachmentsJson) - } + val deliveryStatus = if (isSavedMessages) 1 else 0 + updateMessageStatusAndAttachmentsInDb(messageId, deliveryStatus, finalAttachmentsJson) logPhotoPipeline(messageId, "db status+attachments updated") withContext(Dispatchers.Main) { - updateMessageStatus(messageId, MessageStatus.SENT) + if (isSavedMessages) { + updateMessageStatus(messageId, MessageStatus.SENT) + } // Также очищаем localUri в UI updateMessageAttachments(messageId, null) } - logPhotoPipeline(messageId, "ui status switched to SENT") + logPhotoPipeline( + messageId, + if (isSavedMessages) "ui status switched to SENT" + else "ui status kept at SENDING until delivery ACK" + ) saveDialog( lastMessage = if (caption.isNotEmpty()) caption else "photo", @@ -3468,13 +3498,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { opponentPublicKey = recipient ) - // 🔥 После успешной отправки обновляем статус на DELIVERED (1) в БД и UI - if (!isSavedMessages) { - updateMessageStatusInDb(messageId, 1) // DELIVERED + // Для обычных диалогов остаёмся в SENDING до PacketDelivery(messageId). + withContext(Dispatchers.Main) { + if (isSavedMessages) { + updateMessageStatus(messageId, MessageStatus.SENT) + } } - withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.SENT) } - saveDialog( lastMessage = if (text.isNotEmpty()) text else "photo", timestamp = timestamp, @@ -3739,7 +3769,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { updateMessageStatusAndAttachmentsInDb( messageId = messageId, - delivered = 1, + delivered = if (isSavedMessages) 1 else 0, attachmentsJson = finalDbAttachments.toString() ) @@ -3748,7 +3778,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { _messages.value.map { msg -> if (msg.id != messageId) return@map msg msg.copy( - status = MessageStatus.SENT, + status = + if (isSavedMessages) MessageStatus.SENT + else MessageStatus.SENDING, attachments = msg.attachments.map { current -> val final = finalAttachmentsById[current.id] @@ -3974,14 +4006,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { opponentPublicKey = recipient ) - // 🔥 Обновляем статус в БД после отправки - if (!isSavedMessages) { - updateMessageStatusInDb(messageId, 1) // DELIVERED + // Обновляем UI: для обычных чатов оставляем SENDING до PacketDelivery(messageId). + withContext(Dispatchers.Main) { + if (isSavedMessages) { + updateMessageStatus(messageId, MessageStatus.SENT) + } } - // Обновляем UI - withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.SENT) } - saveDialog( lastMessage = if (text.isNotEmpty()) text else "📷 ${images.size} photos", timestamp = timestamp, @@ -4164,13 +4195,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { attachmentsJson = attachmentsJson ) - // 🔥 После успешной отправки обновляем статус на DELIVERED (1) в БД и UI - if (!isSavedMessages) { - updateMessageStatusInDb(messageId, 1) // DELIVERED + // Для обычных чатов статус подтверждаем только по PacketDelivery(messageId). + withContext(Dispatchers.Main) { + if (isSavedMessages) { + updateMessageStatus(messageId, MessageStatus.SENT) + } } - withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.SENT) } - saveDialog(if (text.isNotEmpty()) text else "file", timestamp) } catch (e: Exception) { withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.ERROR) } @@ -4394,14 +4425,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { attachmentsJson = attachmentsJson ) - // 🔥 Обновляем статус в БД после отправки - if (!isSavedMessages) { - updateMessageStatusInDb(messageId, 1) // DELIVERED + // Обновляем UI: для обычных чатов остаёмся в SENDING до PacketDelivery(messageId). + withContext(Dispatchers.Main) { + if (isSavedMessages) { + updateMessageStatus(messageId, MessageStatus.SENT) + } } - // Обновляем UI - withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.SENT) } - saveDialog("\$a=Avatar", timestamp) } catch (e: Exception) { withContext(Dispatchers.Main) { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index f3e773d..db3523f 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -2465,6 +2465,8 @@ fun ChatsListScreen( dialog.opponentKey, isSelected = isSelectedDialog, + syncInProgress = + syncInProgress, onSwipeStarted = { swipedItemKey = dialog.opponentKey @@ -3501,6 +3503,7 @@ fun SwipeableDialogItem( isDrawerOpen: Boolean = false, isSwipedOpen: Boolean = false, isSelected: Boolean = false, + syncInProgress: Boolean = false, onSwipeStarted: () -> Unit = {}, onSwipeClosed: () -> Unit = {}, onClick: () -> Unit, @@ -3901,6 +3904,7 @@ fun SwipeableDialogItem( isPinned = isPinned, isBlocked = isBlocked, isMuted = isMuted, + syncInProgress = syncInProgress, avatarRepository = avatarRepository, onClick = null // Tap handled by parent pointerInput ) @@ -3917,6 +3921,7 @@ fun DialogItemContent( isPinned: Boolean = false, isBlocked: Boolean = false, isMuted: Boolean = false, + syncInProgress: Boolean = false, avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, onClick: (() -> Unit)? = null ) { @@ -4148,8 +4153,8 @@ fun DialogItemContent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { - // � Скрываем статус доставки когда есть черновик (как в Telegram) - if (dialog.draftText.isNullOrEmpty()) { + // Скрываем статус доставки при черновике и во время синхронизации. + if (dialog.draftText.isNullOrEmpty() && !syncInProgress) { // �📁 Для Saved Messages ВСЕГДА показываем синие двойные // галочки (прочитано) if (dialog.isSavedMessages) { @@ -4284,7 +4289,7 @@ fun DialogItemContent( } } } - } // 📝 end if (draftText.isNullOrEmpty) — скрываем статус при наличии черновика + } // 📝 end if (draftText.isNullOrEmpty && !syncInProgress) val formattedTime = remember(dialog.lastMessageTimestamp) { diff --git a/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt b/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt index e1eb9f5..59e8002 100644 --- a/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt +++ b/app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt @@ -338,8 +338,12 @@ object UpdateManager { persistState(ctx) } - val version = latestUpdateInfo?.version - _updateState.value = if (version != null) UpdateState.UpdateAvailable(version) else UpdateState.Idle + val availableVersion = resolveAvailableUpdateVersion() + _updateState.value = if (availableVersion != null) { + UpdateState.UpdateAvailable(availableVersion) + } else { + UpdateState.Idle + } _downloadProgress.value = 0 } @@ -366,7 +370,7 @@ object UpdateManager { } private fun setUpdateAvailableOrIdleState() { - val version = latestUpdateInfo?.version ?: activeApkVersion + val version = resolveAvailableUpdateVersion() _updateState.value = if (!version.isNullOrBlank()) { UpdateState.UpdateAvailable(version) } else { @@ -374,6 +378,29 @@ object UpdateManager { } } + private fun resolveAvailableUpdateVersion(): String? { + val latestVersion = + latestUpdateInfo + ?.takeIf { + !it.servicePackUrl.isNullOrBlank() && + it.version.isNotBlank() && + compareVersions(it.version, appVersion) > 0 + } + ?.version + if (!latestVersion.isNullOrBlank()) { + return latestVersion + } + + val persistedVersion = activeApkVersion + return if (!persistedVersion.isNullOrBlank() && + compareVersions(persistedVersion, appVersion) > 0 + ) { + persistedVersion + } else { + null + } + } + private fun restorePersistedState(context: Context) { val preferences = prefs(context) val restoredId = preferences.getLong(KEY_DOWNLOAD_ID, -1L).takeIf { it >= 0L } @@ -529,12 +556,17 @@ object UpdateManager { if (items == null || items.length() == 0) { sduLog("No items in response, trying legacy format...") // Fallback на старый формат /updates/get + val version = obj.optString("version", "") + val rawServicePackUrl = + obj.optString("service_pack_url", "").takeIf { it.isNotEmpty() && it != "null" } + val hasNewerVersion = + version.isNotBlank() && compareVersions(version, appVersion) > 0 return UpdateInfo( - version = obj.optString("version", ""), + version = version, platform = obj.optString("platform", ""), arch = obj.optString("arch", ""), kernelUpdateRequired = obj.optBoolean("kernel_update_required", false), - servicePackUrl = obj.optString("service_pack_url", "").takeIf { it.isNotEmpty() && it != "null" }, + servicePackUrl = rawServicePackUrl?.takeIf { hasNewerVersion }, kernelUrl = obj.optString("kernel_url", "").takeIf { it.isNotEmpty() && it != "null" } ) }