Фикс: монотонный статус доставки — DELIVERED больше не откатывается на SENT. Логирование отправки/delivery в rosettadev1.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
Reference in New Issue
Block a user