feat: Enhance logging and state management in Protocol and MessageRepository; improve dialog read status handling in ChatViewModel

This commit is contained in:
k1ngsterr1
2026-01-16 17:11:50 +05:00
parent 6386164ae7
commit 6d506e681b
4 changed files with 132 additions and 41 deletions

View File

@@ -310,13 +310,17 @@ class MessageRepository private constructor(private val context: Context) {
// ✅ Проверяем существование перед вставкой (защита от дубликатов) // ✅ Проверяем существование перед вставкой (защита от дубликатов)
val stillExists = messageDao.messageExists(account, messageId) val stillExists = messageDao.messageExists(account, messageId)
android.util.Log.d("MessageRepo", "📥 INCOMING: messageId=${messageId.take(16)}..., stillExists=$stillExists")
if (!stillExists) { if (!stillExists) {
// Сохраняем в БД только если сообщения нет // Сохраняем в БД только если сообщения нет
messageDao.insertMessage(entity) messageDao.insertMessage(entity)
android.util.Log.d("MessageRepo", "📥 INSERTED message with read=0, fromMe=0")
} }
// Обновляем диалог // Обновляем диалог ПОСЛЕ вставки сообщения
updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true) updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true)
android.util.Log.d("MessageRepo", "📥 Dialog updated")
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа // 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
requestUserInfo(packet.fromPublicKey) requestUserInfo(packet.fromPublicKey)
@@ -469,21 +473,25 @@ class MessageRepository private constructor(private val context: Context) {
val account = currentAccount ?: return val account = currentAccount ?: return
val privateKey = currentPrivateKey ?: return val privateKey = currentPrivateKey ?: return
android.util.Log.d("MessageRepo", "📊 updateDialog: opponent=${opponentKey.take(16)}..., incrementUnread=$incrementUnread")
try { try {
// 🔥 КРИТИЧНО: Сначала считаем реальное количество непрочитанных из messages // 🔥 КРИТИЧНО: Сначала считаем реальное количество непрочитанных из messages
val unreadCount = messageDao.getUnreadCountForDialog(account, opponentKey) val unreadCount = messageDao.getUnreadCountForDialog(account, opponentKey)
android.util.Log.d("MessageRepo", "📊 unreadCount from DB: $unreadCount")
// 🔒 Шифруем lastMessage // 🔒 Шифруем lastMessage
val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey) val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey)
// Проверяем существует ли диалог // Проверяем существует ли диалог
val existing = dialogDao.getDialog(account, opponentKey) val existing = dialogDao.getDialog(account, opponentKey)
android.util.Log.d("MessageRepo", "📊 existing dialog: ${existing != null}, currentUnread=${existing?.unreadCount}")
if (existing != null) { if (existing != null) {
// Обновляем существующий диалог // Обновляем существующий диалог
dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp) dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp)
dialogDao.updateUnreadCount(account, opponentKey, unreadCount) dialogDao.updateUnreadCount(account, opponentKey, unreadCount)
android.util.Log.d("MessageRepo", "📊 UPDATED dialog unread to: $unreadCount")
} else { } else {
// Создаем новый диалог // Создаем новый диалог
dialogDao.insertDialog(DialogEntity( dialogDao.insertDialog(DialogEntity(
@@ -493,9 +501,12 @@ class MessageRepository private constructor(private val context: Context) {
lastMessageTimestamp = timestamp, lastMessageTimestamp = timestamp,
unreadCount = unreadCount unreadCount = unreadCount
)) ))
android.util.Log.d("MessageRepo", "📊 CREATED new dialog with unread: $unreadCount")
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("MessageRepo", "📊 ERROR in updateDialog: ${e.message}")
e.printStackTrace()
} }
} }

View File

@@ -49,11 +49,44 @@ class Protocol(
private var handshakeComplete = false private var handshakeComplete = false
private var handshakeJob: Job? = null private var handshakeJob: Job? = null
// 🔍 Диагностика соединения
private var reconnectAttempts = 0
private var lastStateChangeTime = System.currentTimeMillis()
private var lastSuccessfulConnection = 0L
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val _state = MutableStateFlow(ProtocolState.DISCONNECTED) private val _state = MutableStateFlow(ProtocolState.DISCONNECTED)
val state: StateFlow<ProtocolState> = _state.asStateFlow() val state: StateFlow<ProtocolState> = _state.asStateFlow()
/**
* 🔍 Безопасное изменение состояния с подробным логированием
*/
private fun setState(newState: ProtocolState, reason: String = "") {
val oldState = _state.value
val now = System.currentTimeMillis()
val timeSinceLastChange = now - lastStateChangeTime
if (oldState != newState) {
log("🔄 STATE CHANGE: $oldState -> $newState (${timeSinceLastChange}ms since last change)")
if (reason.isNotEmpty()) {
log(" Reason: $reason")
}
log(" Reconnect attempts: $reconnectAttempts")
log(" Last successful: ${if (lastSuccessfulConnection > 0) "${(now - lastSuccessfulConnection)/1000}s ago" else "never"}")
_state.value = newState
lastStateChangeTime = now
// Сброс счетчика при успешном подключении
if (newState == ProtocolState.AUTHENTICATED) {
lastSuccessfulConnection = now
reconnectAttempts = 0
log("✅ CONNECTION FULLY ESTABLISHED")
}
}
}
private val _lastError = MutableStateFlow<String?>(null) private val _lastError = MutableStateFlow<String?>(null)
val lastError: StateFlow<String?> = _lastError.asStateFlow() val lastError: StateFlow<String?> = _lastError.asStateFlow()
@@ -89,10 +122,10 @@ class Protocol(
// Register handshake response handler // Register handshake response handler
waitPacket(0x00) { packet -> waitPacket(0x00) { packet ->
if (packet is PacketHandshake) { if (packet is PacketHandshake) {
log("✅ Handshake response received, protocol version: ${packet.protocolVersion}") log("✅ HANDSHAKE SUCCESS: protocol=${packet.protocolVersion}, heartbeat=${packet.heartbeatInterval}s")
handshakeJob?.cancel() handshakeJob?.cancel()
handshakeComplete = true handshakeComplete = true
_state.value = ProtocolState.AUTHENTICATED setState(ProtocolState.AUTHENTICATED, "Handshake response received")
flushPacketQueue() flushPacketQueue()
// Start heartbeat with interval from server // Start heartbeat with interval from server
@@ -110,7 +143,7 @@ class Protocol(
// Отправляем чаще - каждые 1/3 интервала (чтобы не терять соединение) // Отправляем чаще - каждые 1/3 интервала (чтобы не терять соединение)
val intervalMs = (intervalSeconds * 1000L) / 3 val intervalMs = (intervalSeconds * 1000L) / 3
log("💓 Starting heartbeat with server interval: ${intervalSeconds}s (sending every ${intervalMs}ms = ${intervalMs/1000}s)") log("💓 HEARTBEAT START: server=${intervalSeconds}s, sending=${intervalMs/1000}s, state=${_state.value}")
heartbeatJob = scope.launch { heartbeatJob = scope.launch {
// ⚡ СРАЗУ отправляем первый heartbeat (как в Архиве) // ⚡ СРАЗУ отправляем первый heartbeat (как в Архиве)
@@ -128,21 +161,27 @@ class Protocol(
*/ */
private fun sendHeartbeat() { private fun sendHeartbeat() {
try { try {
if (_state.value == ProtocolState.AUTHENTICATED) { val currentState = _state.value
val socketAlive = webSocket != null
if (currentState == ProtocolState.AUTHENTICATED) {
val sent = webSocket?.send("heartbeat") ?: false val sent = webSocket?.send("heartbeat") ?: false
if (sent) { if (sent) {
log("💓 Heartbeat sent") log("💓 Heartbeat OK (socket=$socketAlive, state=$currentState)")
} else { } else {
log("💔 Heartbeat failed - socket closed or null") log("💔 HEARTBEAT FAILED: socket=$socketAlive, state=$currentState, manuallyClosed=$isManuallyClosed")
// Триггерим reconnect если heartbeat не прошёл // Триггерим reconnect если heartbeat не прошёл
if (!isManuallyClosed) { if (!isManuallyClosed) {
log("🔄 Triggering reconnect due to failed heartbeat") log("🔄 TRIGGERING RECONNECT due to failed heartbeat")
handleDisconnect() handleDisconnect()
} }
} }
} else {
log("💔 HEARTBEAT SKIPPED: state=$currentState (not AUTHENTICATED), socket=$socketAlive")
} }
} catch (e: Exception) { } catch (e: Exception) {
log("💔 Heartbeat error: ${e.message}") log("💔 HEARTBEAT EXCEPTION: ${e.message}")
e.printStackTrace()
} }
} }
@@ -150,11 +189,17 @@ class Protocol(
* Initialize connection to server * Initialize connection to server
*/ */
fun connect() { fun connect() {
if (_state.value == ProtocolState.CONNECTING) { val currentState = _state.value
log("Already connecting, skipping...") log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts")
if (currentState == ProtocolState.CONNECTING) {
log("⚠️ Already connecting, skipping... (preventing duplicate connect)")
return return
} }
reconnectAttempts++
log("📊 RECONNECT ATTEMPT #$reconnectAttempts")
// Закрываем старый сокет если есть (как в Архиве) // Закрываем старый сокет если есть (как в Архиве)
webSocket?.let { oldSocket -> webSocket?.let { oldSocket ->
try { try {
@@ -167,10 +212,10 @@ class Protocol(
webSocket = null webSocket = null
isManuallyClosed = false isManuallyClosed = false
_state.value = ProtocolState.CONNECTING setState(ProtocolState.CONNECTING, "Starting new connection attempt #$reconnectAttempts")
_lastError.value = null _lastError.value = null
log("🔌 Connecting to: $serverAddress") log("🔌 Connecting to: $serverAddress (attempt #$reconnectAttempts)")
val request = Request.Builder() val request = Request.Builder()
.url(serverAddress) .url(serverAddress)
@@ -178,15 +223,16 @@ class Protocol(
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
log("✅ WebSocket connected") log("✅ WebSocket OPEN: response=${response.code}, hasCredentials=${lastPublicKey != null}")
_state.value = ProtocolState.CONNECTED setState(ProtocolState.CONNECTED, "WebSocket onOpen callback")
// If we have saved credentials, start handshake automatically // If we have saved credentials, start handshake automatically
lastPublicKey?.let { publicKey -> lastPublicKey?.let { publicKey ->
lastPrivateHash?.let { privateHash -> lastPrivateHash?.let { privateHash ->
log("🤝 Auto-starting handshake with saved credentials")
startHandshake(publicKey, privateHash) startHandshake(publicKey, privateHash)
} }
} } ?: log("⚠️ No saved credentials, waiting for manual handshake")
} }
override fun onMessage(webSocket: WebSocket, bytes: ByteString) { override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
@@ -199,23 +245,20 @@ class Protocol(
} }
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
// Code 3887 - кастомный код сервера, просто делаем reconnect тихо log("⚠️ WebSocket CLOSING: code=$code reason='$reason' state=${_state.value}")
if (code != 3887) {
log("⚠️ WebSocket closing: code=$code reason='$reason'")
}
} }
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
// Для кода 3887 не логируем - это частое закрытие сервером log("❌ WebSocket CLOSED: code=$code reason='$reason' state=${_state.value} manuallyClosed=$isManuallyClosed")
if (code != 3887) {
log("❌ WebSocket closed: code=$code reason='$reason'")
}
handleDisconnect() handleDisconnect()
} }
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
log("❌ WebSocket failure: ${t.message}") log("❌ WebSocket FAILURE: ${t.message}")
log(" Response: ${response?.code} ${response?.message}") log(" Response: ${response?.code} ${response?.message}")
log(" State: ${_state.value}")
log(" Manually closed: $isManuallyClosed")
log(" Reconnect attempts: $reconnectAttempts")
t.printStackTrace() t.printStackTrace()
_lastError.value = t.message _lastError.value = t.message
handleDisconnect() handleDisconnect()
@@ -236,12 +279,12 @@ class Protocol(
lastPrivateHash = privateHash lastPrivateHash = privateHash
if (_state.value != ProtocolState.CONNECTED && _state.value != ProtocolState.AUTHENTICATED) { if (_state.value != ProtocolState.CONNECTED && _state.value != ProtocolState.AUTHENTICATED) {
log("Not connected, will handshake after connection") log("⚠️ HANDSHAKE DEFERRED: Not connected (state=${_state.value}), will handshake after connection")
connect() connect()
return return
} }
_state.value = ProtocolState.HANDSHAKING setState(ProtocolState.HANDSHAKING, "Starting handshake")
handshakeComplete = false handshakeComplete = false
val handshake = PacketHandshake().apply { val handshake = PacketHandshake().apply {
@@ -366,22 +409,34 @@ class Protocol(
private fun handleDisconnect() { private fun handleDisconnect() {
val previousState = _state.value val previousState = _state.value
_state.value = ProtocolState.DISCONNECTED log("🔌 DISCONNECT HANDLER: previousState=$previousState, manuallyClosed=$isManuallyClosed, reconnectAttempts=$reconnectAttempts")
setState(ProtocolState.DISCONNECTED, "Disconnect handler called from $previousState")
handshakeComplete = false handshakeComplete = false
handshakeJob?.cancel() handshakeJob?.cancel()
heartbeatJob?.cancel() heartbeatJob?.cancel()
// Автоматический reconnect (простая логика как в Архиве - без счётчиков) // Автоматический reconnect с защитой от бесконечных попыток
if (!isManuallyClosed) { if (!isManuallyClosed) {
log("🔄 Reconnecting silently...") // Экспоненциальная задержка: 1s, 2s, 4s, 8s, максимум 30s
val delayMs = minOf(1000L * (1 shl minOf(reconnectAttempts - 1, 4)), 30000L)
log("🔄 SCHEDULING RECONNECT: attempt #$reconnectAttempts, delay=${delayMs}ms")
if (reconnectAttempts > 20) {
log("⚠️ WARNING: Too many reconnect attempts ($reconnectAttempts), may be stuck in loop")
}
scope.launch { scope.launch {
// Быстрый reconnect - 1 секунда delay(delayMs)
delay(1000L)
if (!isManuallyClosed) { if (!isManuallyClosed) {
log("🔄 EXECUTING RECONNECT after ${delayMs}ms delay")
connect() connect()
} else {
log("🔄 RECONNECT CANCELLED: was manually closed during delay")
} }
} }
} else {
log("🔌 No reconnect: connection was manually closed")
} }
} }

View File

@@ -62,9 +62,21 @@ object ProtocolManager {
addLog("🚀 ProtocolManager.initialize() called") addLog("🚀 ProtocolManager.initialize() called")
messageRepository = MessageRepository.getInstance(context) messageRepository = MessageRepository.getInstance(context)
setupPacketHandlers() setupPacketHandlers()
setupStateMonitoring()
addLog("🚀 ProtocolManager.initialize() completed") addLog("🚀 ProtocolManager.initialize() completed")
} }
/**
* 🔍 Мониторинг состояния соединения
*/
private fun setupStateMonitoring() {
scope.launch {
getProtocol().state.collect { newState ->
addLog("📊 STATE MONITOR: Connection state changed to $newState")
}
}
}
/** /**
* 🔥 Инициализация аккаунта - КРИТИЧНО для получения сообщений! * 🔥 Инициализация аккаунта - КРИТИЧНО для получения сообщений!
* Должен вызываться после авторизации пользователя * Должен вызываться после авторизации пользователя
@@ -210,7 +222,7 @@ object ProtocolManager {
* Connect to server * Connect to server
*/ */
fun connect() { fun connect() {
addLog("Connect requested") addLog("🔌 CONNECT REQUESTED from ProtocolManager")
getProtocol().connect() getProtocol().connect()
} }
@@ -218,9 +230,9 @@ object ProtocolManager {
* Authenticate with server * Authenticate with server
*/ */
fun authenticate(publicKey: String, privateHash: String) { fun authenticate(publicKey: String, privateHash: String) {
addLog("Authenticate called") addLog("🔐 AUTHENTICATE called from ProtocolManager")
addLog("PublicKey: ${publicKey.take(30)}...") addLog(" PublicKey: ${publicKey.take(30)}...")
addLog("PrivateHash: ${privateHash.take(20)}...") addLog(" PrivateHash: ${privateHash.take(20)}...")
getProtocol().startHandshake(publicKey, privateHash) getProtocol().startHandshake(publicKey, privateHash)
} }

View File

@@ -271,6 +271,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
lastReadMessageTimestamp = 0L lastReadMessageTimestamp = 0L
readReceiptSentForCurrentDialog = false readReceiptSentForCurrentDialog = false
isDialogActive = true // 🔥 Диалог активен! isDialogActive = true // 🔥 Диалог активен!
android.util.Log.d("ChatViewModel", "✅ Dialog active flag set to TRUE in openDialog")
// 📨 Применяем Forward сообщения СРАЗУ после сброса // 📨 Применяем Forward сообщения СРАЗУ после сброса
if (hasForward) { if (hasForward) {
@@ -306,7 +307,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
* Как setCurrentDialogPublicKeyView("") в архиве * Как setCurrentDialogPublicKeyView("") в архиве
*/ */
fun closeDialog() { fun closeDialog() {
android.util.Log.d("ChatViewModel", "🔒 CLOSE DIALOG")
isDialogActive = false isDialogActive = false
android.util.Log.d("ChatViewModel", "❌ Dialog active flag set to FALSE")
} }
/** /**
@@ -398,8 +401,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔥 Фоновые операции БЕЗ блокировки UI // 🔥 Фоновые операции БЕЗ блокировки UI
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
// Отмечаем как прочитанные в БД // 👁️ Отмечаем как прочитанные ТОЛЬКО если диалог активен
messageDao.markDialogAsRead(account, dialogKey) if (isDialogActive) {
android.util.Log.d("ChatViewModel", "📖 Marking dialog as read (dialog is active)")
messageDao.markDialogAsRead(account, dialogKey)
} else {
android.util.Log.d("ChatViewModel", "⏸️ NOT marking as read (dialog not active)")
}
// 🔥 Пересчитываем счетчики из messages // 🔥 Пересчитываем счетчики из messages
dialogDao.updateDialogFromMessages(account, opponent) dialogDao.updateDialogFromMessages(account, opponent)
@@ -464,8 +472,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
hasMoreMessages = entities.size >= PAGE_SIZE hasMoreMessages = entities.size >= PAGE_SIZE
currentOffset = entities.size currentOffset = entities.size
// Фоновые операции // 👁️ Фоновые операции - НЕ помечаем как прочитанные если диалог неактивен!
messageDao.markDialogAsRead(account, dialogKey) if (isDialogActive) {
android.util.Log.d("ChatViewModel", "📖 Marking dialog as read in refresh (dialog is active)")
messageDao.markDialogAsRead(account, dialogKey)
} else {
android.util.Log.d("ChatViewModel", "⏸️ NOT marking as read in refresh (dialog not active)")
}
// 🔥 Пересчитываем счетчики из messages // 🔥 Пересчитываем счетчики из messages
dialogDao.updateDialogFromMessages(account, opponent) dialogDao.updateDialogFromMessages(account, opponent)