Фикс: монотонный статус доставки — DELIVERED больше не откатывается на SENT. Логирование отправки/delivery в rosettadev1.

This commit is contained in:
2026-04-08 16:24:11 +05:00
parent 0427e2ba17
commit 8bfbba3159
3 changed files with 93 additions and 26 deletions

View File

@@ -1009,18 +1009,28 @@ class MessageRepository private constructor(private val context: Context) {
} }
/** Обработка подтверждения доставки */ /** Обработка подтверждения доставки */
private fun devLog(msg: String) {
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
val line = "$ts [MsgRepo] $msg"
android.util.Log.d("MsgRepo", msg)
try {
val dir = java.io.File(context.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
java.io.File(dir, "rosettadev1.txt").appendText("$line\n")
} catch (_: Exception) {}
}
suspend fun handleDelivery(packet: PacketDelivery) { suspend fun handleDelivery(packet: PacketDelivery) {
val account = currentAccount ?: return val account = currentAccount ?: return
// 📝 LOG: Получено подтверждение доставки devLog("DELIVERY RECEIVED: msgId=${packet.messageId.take(8)}..., to=${packet.toPublicKey.take(12)}...")
MessageLogger.logDeliveryStatus( MessageLogger.logDeliveryStatus(
messageId = packet.messageId, messageId = packet.messageId,
toPublicKey = packet.toPublicKey, toPublicKey = packet.toPublicKey,
status = "DELIVERED" status = "DELIVERED"
) )
// Desktop parity: update both delivery status AND timestamp on delivery confirmation.
// Desktop sets timestamp = Date.now() when PacketDelivery arrives (useSynchronize.ts).
val deliveryTimestamp = System.currentTimeMillis() val deliveryTimestamp = System.currentTimeMillis()
messageDao.updateDeliveryStatusAndTimestamp( messageDao.updateDeliveryStatusAndTimestamp(
account, packet.messageId, DeliveryStatus.DELIVERED.value, deliveryTimestamp account, packet.messageId, DeliveryStatus.DELIVERED.value, deliveryTimestamp

View File

@@ -57,6 +57,18 @@ object ProtocolManager {
private var groupRepository: GroupRepository? = null private var groupRepository: GroupRepository? = null
private var appContext: Context? = null private var appContext: Context? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private fun protocolDevLog(msg: String) {
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
val line = "$ts [Protocol] $msg"
android.util.Log.d("Protocol", msg)
try {
val ctx = appContext ?: return
val dir = java.io.File(ctx.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
java.io.File(dir, "rosettadev1.txt").appendText("$line\n")
} catch (_: Exception) {}
}
@Volatile private var packetHandlersRegistered = false @Volatile private var packetHandlersRegistered = false
@Volatile private var stateMonitoringStarted = false @Volatile private var stateMonitoringStarted = false
@Volatile private var syncRequestInFlight = false @Volatile private var syncRequestInFlight = false
@@ -335,22 +347,24 @@ object ProtocolManager {
} }
// Обработчик доставки (0x08) // Обработчик доставки (0x08)
// Desktop parity: useDialogFiber.ts updates sync time on delivery (await updateSyncTime(Date.now()))
waitPacket(0x08) { packet -> waitPacket(0x08) { packet ->
val deliveryPacket = packet as PacketDelivery val deliveryPacket = packet as PacketDelivery
protocolDevLog("PACKET 0x08 DELIVERY: msgId=${deliveryPacket.messageId.take(8)}..., to=${deliveryPacket.toPublicKey.take(12)}...")
launchInboundPacketTask { launchInboundPacketTask {
val repository = messageRepository val repository = messageRepository
if (repository == null || !repository.isInitialized()) { if (repository == null || !repository.isInitialized()) {
protocolDevLog(" DELIVERY SKIPPED: repo not init")
requireResyncAfterAccountInit("⏳ Delivery status before account init, scheduling re-sync") requireResyncAfterAccountInit("⏳ Delivery status before account init, scheduling re-sync")
markInboundProcessingFailure("Delivery packet skipped before account init") markInboundProcessingFailure("Delivery packet skipped before account init")
return@launchInboundPacketTask return@launchInboundPacketTask
} }
try { try {
repository.handleDelivery(deliveryPacket) repository.handleDelivery(deliveryPacket)
// iOS parity: cancel retry timer on delivery ACK
resolveOutgoingRetry(deliveryPacket.messageId) resolveOutgoingRetry(deliveryPacket.messageId)
protocolDevLog(" DELIVERY HANDLED OK: ${deliveryPacket.messageId.take(8)}...")
} catch (e: Exception) { } catch (e: Exception) {
protocolDevLog(" DELIVERY ERROR: ${e.javaClass.simpleName}: ${e.message}")
markInboundProcessingFailure("Delivery processing failed", e) markInboundProcessingFailure("Delivery processing failed", e)
return@launchInboundPacketTask return@launchInboundPacketTask
} }

View File

@@ -37,6 +37,18 @@ import org.json.JSONObject
*/ */
class ChatViewModel(application: Application) : AndroidViewModel(application) { class ChatViewModel(application: Application) : AndroidViewModel(application) {
private fun devLog(msg: String) {
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
val line = "$ts [SendMsg] $msg"
android.util.Log.d("SendMsg", msg)
try {
val ctx = getApplication<Application>()
val dir = java.io.File(ctx.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
java.io.File(dir, "rosettadev1.txt").appendText("$line\n")
} catch (_: Exception) {}
}
companion object { companion object {
private const val TAG = "ChatViewModel" private const val TAG = "ChatViewModel"
private const val PAGE_SIZE = 30 private const val PAGE_SIZE = 30
@@ -529,23 +541,26 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val opponent = opponentKey ?: return@collect val opponent = opponentKey ?: return@collect
val currentDialogKey = getDialogKey(account, opponent) val currentDialogKey = getDialogKey(account, opponent)
devLog("DELIVERY EVENT: msgId=${update.messageId.take(8)}..., status=${update.status}, dialogKey=${update.dialogKey.take(20)}..., currentKey=${currentDialogKey.take(20)}..., match=${update.dialogKey == currentDialogKey}")
if (update.dialogKey == currentDialogKey) { if (update.dialogKey == currentDialogKey) {
if (!isDialogActive) return@collect
when (update.status) { when (update.status) {
DeliveryStatus.DELIVERED -> { DeliveryStatus.DELIVERED -> {
// Обновляем конкретное сообщение devLog(" → updateMessageStatus DELIVERED: ${update.messageId.take(8)}...")
updateMessageStatus(update.messageId, MessageStatus.DELIVERED) updateMessageStatus(update.messageId, MessageStatus.DELIVERED)
} }
DeliveryStatus.ERROR -> { DeliveryStatus.ERROR -> {
// Синхронизируем ошибку отправки с открытым диалогом devLog(" → updateMessageStatus ERROR: ${update.messageId.take(8)}...")
updateMessageStatus(update.messageId, MessageStatus.ERROR) updateMessageStatus(update.messageId, MessageStatus.ERROR)
} }
DeliveryStatus.READ -> { DeliveryStatus.READ -> {
// Помечаем все исходящие как прочитанные devLog(" → markAllOutgoingAsRead")
markAllOutgoingAsRead() markAllOutgoingAsRead()
} }
else -> {} else -> {}
} }
} else {
devLog(" → SKIPPED (dialog mismatch)")
} }
} }
} }
@@ -602,22 +617,45 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
private fun updateMessageStatus(messageId: String, status: MessageStatus) { private fun updateMessageStatus(messageId: String, status: MessageStatus) {
val currentMessages = _messages.value
val currentStatus = currentMessages.find { it.id == messageId }?.status
devLog("updateMessageStatus: msgId=${messageId.take(8)}..., newStatus=$status, currentStatus=$currentStatus, totalMsgs=${currentMessages.size}")
_messages.value = _messages.value =
_messages.value.map { msg -> currentMessages.map { msg ->
if (msg.id != messageId) return@map msg if (msg.id != messageId) return@map msg
// Keep read status monotonic: late DELIVERED must not downgrade READ. // Monotonic status: never downgrade
val mergedStatus = // SENDING < SENT < DELIVERED < READ
when (status) { val statusOrder = mapOf(
MessageStatus.DELIVERED -> MessageStatus.ERROR to 0,
if (msg.status == MessageStatus.READ) MessageStatus.READ MessageStatus.SENDING to 1,
else MessageStatus.DELIVERED MessageStatus.SENT to 2,
else -> status MessageStatus.DELIVERED to 3,
} MessageStatus.READ to 4
if (mergedStatus != msg.status) msg.copy(status = mergedStatus) else msg )
val currentOrd = statusOrder[msg.status] ?: 0
val newOrd = statusOrder[status] ?: 0
// ERROR can always override, otherwise only upgrade
val mergedStatus = if (status == MessageStatus.ERROR) {
status
} else if (newOrd > currentOrd) {
status
} else {
msg.status
}
if (mergedStatus != msg.status) {
devLog(" → STATUS CHANGED: ${msg.status}$mergedStatus")
msg.copy(status = mergedStatus)
} else {
devLog(" → STATUS NOT UPGRADED: ${msg.status}$status, skip")
msg
}
} }
// 🔥 Также обновляем кэш!
updateCacheFromCurrentMessages() updateCacheFromCurrentMessages()
} }
@@ -2834,6 +2872,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val messageId = UUID.randomUUID().toString().replace("-", "").take(32) val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
devLog("=== SEND START: msgId=${messageId.take(8)}..., to=${recipient.take(12)}..., text='${text.take(30)}' ===")
devLog(" isForward=$isForward, replyMsgs=${replyMsgsToSend.size}, isConnected=${ProtocolManager.isConnected()}, isAuth=${ProtocolManager.isAuthenticated()}")
// 🔥 Формируем ReplyData для отображения в UI (только первое сообщение) // 🔥 Формируем ReplyData для отображения в UI (только первое сообщение)
// Используется для обычного reply (не forward). // Используется для обычного reply (не forward).
val replyData: ReplyData? = val replyData: ReplyData? =
@@ -3093,15 +3134,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
packet.attachments.forEachIndexed { idx, att -> } packet.attachments.forEachIndexed { idx, att -> }
// 📁 Для Saved Messages - НЕ отправляем пакет на сервер // 📁 Для Saved Messages - НЕ отправляем пакет на сервер
// Только сохраняем локально
val isSavedMessages = (sender == recipient) val isSavedMessages = (sender == recipient)
if (!isSavedMessages) { if (!isSavedMessages) {
// Отправляем пакет только для обычных диалогов devLog(" SENDING packet: msgId=${messageId.take(8)}..., isConnected=${ProtocolManager.isConnected()}, isAuth=${ProtocolManager.isAuthenticated()}")
ProtocolManager.send(packet) ProtocolManager.send(packet)
devLog(" SENT packet OK: msgId=${messageId.take(8)}...")
} else {
devLog(" Saved Messages — skip send")
} }
// 3. 🎯 UI обновление в Main потоке
// Для обычных диалогов статус остаётся SENDING до PacketDelivery(messageId).
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (isSavedMessages) { if (isSavedMessages) {
updateMessageStatus(messageId, MessageStatus.SENT) updateMessageStatus(messageId, MessageStatus.SENT)
@@ -3168,10 +3209,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
saveDialog(text, timestamp) saveDialog(text, timestamp)
} catch (e: Exception) { } catch (e: Exception) {
devLog(" SEND ERROR: msgId=${messageId.take(8)}..., ${e.javaClass.simpleName}: ${e.message}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.ERROR) updateMessageStatus(messageId, MessageStatus.ERROR)
} }
// Update error status in DB + dialog
updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value) updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value)
saveDialog(text, timestamp) saveDialog(text, timestamp)
} finally { } finally {
@@ -3389,7 +3430,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
attachments = finalMessageAttachments attachments = finalMessageAttachments
} }
if (!isSavedMessages) { if (!isSavedMessages) {
devLog(" FWD SENDING: msgId=${messageId.take(8)}..., to=${recipientPublicKey.take(12)}..., isConn=${ProtocolManager.isConnected()}, isAuth=${ProtocolManager.isAuthenticated()}")
ProtocolManager.send(packet) ProtocolManager.send(packet)
devLog(" FWD SENT OK: msgId=${messageId.take(8)}...")
} }
val finalAttachmentsJson = val finalAttachmentsJson =