Фикс: монотонный статус доставки — 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) {
val account = currentAccount ?: return
// 📝 LOG: Получено подтверждение доставки
devLog("DELIVERY RECEIVED: msgId=${packet.messageId.take(8)}..., to=${packet.toPublicKey.take(12)}...")
MessageLogger.logDeliveryStatus(
messageId = packet.messageId,
toPublicKey = packet.toPublicKey,
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()
messageDao.updateDeliveryStatusAndTimestamp(
account, packet.messageId, DeliveryStatus.DELIVERED.value, deliveryTimestamp

View File

@@ -57,6 +57,18 @@ object ProtocolManager {
private var groupRepository: GroupRepository? = null
private var appContext: Context? = null
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 stateMonitoringStarted = false
@Volatile private var syncRequestInFlight = false
@@ -335,22 +347,24 @@ object ProtocolManager {
}
// Обработчик доставки (0x08)
// Desktop parity: useDialogFiber.ts updates sync time on delivery (await updateSyncTime(Date.now()))
waitPacket(0x08) { packet ->
val deliveryPacket = packet as PacketDelivery
protocolDevLog("PACKET 0x08 DELIVERY: msgId=${deliveryPacket.messageId.take(8)}..., to=${deliveryPacket.toPublicKey.take(12)}...")
launchInboundPacketTask {
val repository = messageRepository
if (repository == null || !repository.isInitialized()) {
protocolDevLog(" DELIVERY SKIPPED: repo not init")
requireResyncAfterAccountInit("⏳ Delivery status before account init, scheduling re-sync")
markInboundProcessingFailure("Delivery packet skipped before account init")
return@launchInboundPacketTask
}
try {
repository.handleDelivery(deliveryPacket)
// iOS parity: cancel retry timer on delivery ACK
resolveOutgoingRetry(deliveryPacket.messageId)
protocolDevLog(" DELIVERY HANDLED OK: ${deliveryPacket.messageId.take(8)}...")
} catch (e: Exception) {
protocolDevLog(" DELIVERY ERROR: ${e.javaClass.simpleName}: ${e.message}")
markInboundProcessingFailure("Delivery processing failed", e)
return@launchInboundPacketTask
}

View File

@@ -37,6 +37,18 @@ import org.json.JSONObject
*/
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 {
private const val TAG = "ChatViewModel"
private const val PAGE_SIZE = 30
@@ -529,23 +541,26 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val opponent = opponentKey ?: return@collect
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 (!isDialogActive) return@collect
when (update.status) {
DeliveryStatus.DELIVERED -> {
// Обновляем конкретное сообщение
devLog(" → updateMessageStatus DELIVERED: ${update.messageId.take(8)}...")
updateMessageStatus(update.messageId, MessageStatus.DELIVERED)
}
DeliveryStatus.ERROR -> {
// Синхронизируем ошибку отправки с открытым диалогом
devLog(" → updateMessageStatus ERROR: ${update.messageId.take(8)}...")
updateMessageStatus(update.messageId, MessageStatus.ERROR)
}
DeliveryStatus.READ -> {
// Помечаем все исходящие как прочитанные
devLog(" → markAllOutgoingAsRead")
markAllOutgoingAsRead()
}
else -> {}
}
} else {
devLog(" → SKIPPED (dialog mismatch)")
}
}
}
@@ -602,22 +617,45 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
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.map { msg ->
currentMessages.map { msg ->
if (msg.id != messageId) return@map msg
// Keep read status monotonic: late DELIVERED must not downgrade READ.
val mergedStatus =
when (status) {
MessageStatus.DELIVERED ->
if (msg.status == MessageStatus.READ) MessageStatus.READ
else MessageStatus.DELIVERED
else -> status
}
if (mergedStatus != msg.status) msg.copy(status = mergedStatus) else msg
// Monotonic status: never downgrade
// SENDING < SENT < DELIVERED < READ
val statusOrder = mapOf(
MessageStatus.ERROR to 0,
MessageStatus.SENDING to 1,
MessageStatus.SENT to 2,
MessageStatus.DELIVERED to 3,
MessageStatus.READ to 4
)
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()
}
@@ -2834,6 +2872,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
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 (только первое сообщение)
// Используется для обычного reply (не forward).
val replyData: ReplyData? =
@@ -3093,15 +3134,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
packet.attachments.forEachIndexed { idx, att -> }
// 📁 Для Saved Messages - НЕ отправляем пакет на сервер
// Только сохраняем локально
val isSavedMessages = (sender == recipient)
if (!isSavedMessages) {
// Отправляем пакет только для обычных диалогов
devLog(" SENDING packet: msgId=${messageId.take(8)}..., isConnected=${ProtocolManager.isConnected()}, isAuth=${ProtocolManager.isAuthenticated()}")
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) {
if (isSavedMessages) {
updateMessageStatus(messageId, MessageStatus.SENT)
@@ -3168,10 +3209,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
saveDialog(text, timestamp)
} catch (e: Exception) {
devLog(" SEND ERROR: msgId=${messageId.take(8)}..., ${e.javaClass.simpleName}: ${e.message}")
withContext(Dispatchers.Main) {
updateMessageStatus(messageId, MessageStatus.ERROR)
}
// Update error status in DB + dialog
updateMessageStatusInDb(messageId, DeliveryStatus.ERROR.value)
saveDialog(text, timestamp)
} finally {
@@ -3389,7 +3430,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
attachments = finalMessageAttachments
}
if (!isSavedMessages) {
devLog(" FWD SENDING: msgId=${messageId.take(8)}..., to=${recipientPublicKey.take(12)}..., isConn=${ProtocolManager.isConnected()}, isAuth=${ProtocolManager.isAuthenticated()}")
ProtocolManager.send(packet)
devLog(" FWD SENT OK: msgId=${messageId.take(8)}...")
}
val finalAttachmentsJson =