From 2c173bda26c316a4c75e88f4f00516b799f979e7 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 13 Jan 2026 23:28:48 +0500 Subject: [PATCH] feat: Enhance logging in MessageRepository and ChatsListViewModel for better debugging and flow tracking --- .../messenger/data/MessageRepository.kt | 117 +++++++++++++++--- .../messenger/database/MessageEntities.kt | 85 ++++++++++++- .../com/rosetta/messenger/network/Protocol.kt | 2 + .../messenger/network/ProtocolManager.kt | 14 ++- .../messenger/ui/chats/ChatViewModel.kt | 66 +++------- .../messenger/ui/chats/ChatsListViewModel.kt | 42 +++---- 6 files changed, 232 insertions(+), 94 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 dc5b8f4..c059327 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -75,23 +75,44 @@ class MessageRepository private constructor(private val context: Context) { INSTANCE ?: MessageRepository(context.applicationContext).also { INSTANCE = it } } } + + /** + * Генерация детерминированного messageId на основе данных сообщения + * Аналог generateRandomKeyFormSeed из Архива + */ + fun generateMessageId(fromPublicKey: String, toPublicKey: String, timestamp: Long): String { + val seed = fromPublicKey + toPublicKey + timestamp.toString() + val hash = java.security.MessageDigest.getInstance("SHA-256") + .digest(seed.toByteArray()) + // Берём первые 16 символов hex-представления + return hash.take(8).joinToString("") { String.format("%02x", it) } + } } /** * Инициализация с текущим аккаунтом */ fun initialize(publicKey: String, privateKey: String) { + android.util.Log.d("MessageRepository", "🔐 initialize() called with publicKey: ${publicKey.take(16)}...") currentAccount = publicKey currentPrivateKey = privateKey // Загрузка диалогов scope.launch { dialogDao.getDialogsFlow(publicKey).collect { entities -> + android.util.Log.d("MessageRepository", "📋 MessageRepository dialogsFlow emitted: ${entities.size} dialogs") _dialogs.value = entities.map { it.toDialog() } } } } + /** + * Проверка инициализации + */ + fun isInitialized(): Boolean { + return currentAccount != null && currentPrivateKey != null + } + /** * Получить поток сообщений для диалога */ @@ -210,13 +231,39 @@ class MessageRepository private constructor(private val context: Context) { * Обработка входящего сообщения */ suspend fun handleIncomingMessage(packet: PacketMessage) { - val account = currentAccount ?: return - val privateKey = currentPrivateKey ?: return + android.util.Log.d("MessageRepository", "═══════════════════════════════════════") + android.util.Log.d("MessageRepository", "📩 handleIncomingMessage START") + android.util.Log.d("MessageRepository", " from: ${packet.fromPublicKey.take(20)}...") - // Проверяем, не дубликат ли - if (messageDao.messageExists(account, packet.messageId)) return + // 🔥 Генерируем messageId если он пустой (как в Архиве - generateRandomKeyFormSeed) + val messageId = if (packet.messageId.isBlank()) { + generateMessageId(packet.fromPublicKey, packet.toPublicKey, packet.timestamp) + } else { + packet.messageId + } + android.util.Log.d("MessageRepository", " messageId: $messageId (original: ${packet.messageId})") + android.util.Log.d("MessageRepository", " currentAccount: ${currentAccount?.take(20) ?: "NULL"}...") + android.util.Log.d("MessageRepository", " currentPrivateKey: ${if (currentPrivateKey != null) "SET" else "NULL"}") + + val account = currentAccount ?: run { + android.util.Log.e("MessageRepository", "❌ ABORT: currentAccount is NULL!") + return + } + val privateKey = currentPrivateKey ?: run { + android.util.Log.e("MessageRepository", "❌ ABORT: currentPrivateKey is NULL!") + return + } + + // Проверяем, не дубликат ли (используем сгенерированный messageId) + val isDuplicate = messageDao.messageExists(account, messageId) + android.util.Log.d("MessageRepository", " isDuplicate: $isDuplicate") + if (isDuplicate) { + android.util.Log.d("MessageRepository", "⚠️ Skipping duplicate message") + return + } val dialogKey = getDialogKey(packet.fromPublicKey) + android.util.Log.d("MessageRepository", " dialogKey: $dialogKey") try { // Расшифровываем @@ -247,21 +294,25 @@ class MessageRepository private constructor(private val context: Context) { read = 0, fromMe = 0, delivered = DeliveryStatus.DELIVERED.value, - messageId = packet.messageId, + messageId = messageId, // 🔥 Используем сгенерированный messageId! plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст attachments = attachmentsJson, dialogKey = dialogKey ) messageDao.insertMessage(entity) + android.util.Log.d("MessageRepository", "✅ Message saved to DB: ${packet.messageId.take(16)}...") // Обновляем диалог + android.util.Log.d("MessageRepository", "🔄 Calling updateDialog for ${packet.fromPublicKey.take(16)}...") updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true) + android.util.Log.d("MessageRepository", "✅ updateDialog completed!") // Обновляем кэш val message = entity.toMessage() updateMessageCache(dialogKey, message) } catch (e: Exception) { + android.util.Log.e("MessageRepository", "❌ Error handling incoming message", e) e.printStackTrace() } } @@ -301,13 +352,20 @@ class MessageRepository private constructor(private val context: Context) { /** * Отметить диалог как прочитанный + * 🔥 После обновления messages обновляем диалог через updateDialogFromMessages */ suspend fun markDialogAsRead(opponentKey: String) { val account = currentAccount ?: return val dialogKey = getDialogKey(opponentKey) + // Отмечаем сообщения как прочитанные messageDao.markDialogAsRead(account, dialogKey) - dialogDao.clearUnreadCount(account, opponentKey) + + // 🔥 КРИТИЧНО: Пересчитываем счетчики из таблицы messages + // чтобы unread_count обновился моментально + dialogDao.updateDialogFromMessages(account, opponentKey) + + android.util.Log.d("MessageRepository", "✅ Dialog marked as read and updated from messages") } /** @@ -392,21 +450,42 @@ class MessageRepository private constructor(private val context: Context) { incrementUnread: Boolean = false ) { val account = currentAccount ?: return + val privateKey = currentPrivateKey ?: return - val existing = dialogDao.getDialog(account, opponentKey) - if (existing != null) { - dialogDao.updateLastMessage(account, opponentKey, lastMessage, timestamp) - if (incrementUnread) { - dialogDao.incrementUnreadCount(account, opponentKey) + android.util.Log.d("MessageRepository", "📝 Updating dialog for ${opponentKey.take(16)}...") + android.util.Log.d("MessageRepository", " lastMessage: ${lastMessage.take(50)}") + + try { + // 🔥 КРИТИЧНО: Сначала считаем реальное количество непрочитанных из messages + val unreadCount = messageDao.getUnreadCountForDialog(account, opponentKey) + android.util.Log.d("MessageRepository", " unreadCount from messages: $unreadCount") + + // 🔒 Шифруем lastMessage + val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey) + + // Проверяем существует ли диалог + val existing = dialogDao.getDialog(account, opponentKey) + + if (existing != null) { + // Обновляем существующий диалог + android.util.Log.d("MessageRepository", " ✏️ Updating existing dialog...") + dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp) + dialogDao.updateUnreadCount(account, opponentKey, unreadCount) + } else { + // Создаем новый диалог + android.util.Log.d("MessageRepository", " ➕ Creating new dialog...") + dialogDao.insertDialog(DialogEntity( + account = account, + opponentKey = opponentKey, + lastMessage = encryptedLastMessage, + lastMessageTimestamp = timestamp, + unreadCount = unreadCount + )) } - } else { - dialogDao.insertDialog(DialogEntity( - account = account, - opponentKey = opponentKey, - lastMessage = lastMessage, - lastMessageTimestamp = timestamp, - unreadCount = if (incrementUnread) 1 else 0 - )) + + android.util.Log.d("MessageRepository", " ✅ Dialog updated successfully!") + } catch (e: Exception) { + android.util.Log.e("MessageRepository", " ❌ Error updating dialog", e) } } 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 6cf6f73..46463c3 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -198,6 +198,19 @@ interface MessageDao { @Query("DELETE FROM messages WHERE account = :account AND message_id = :messageId") suspend fun deleteMessage(account: String, messageId: String) + /** + * Получить количество непрочитанных сообщений для диалога + * Считает только входящие сообщения (from_me = 0) которые не прочитаны (read = 0) + */ + @Query(""" + SELECT COUNT(*) FROM messages + WHERE account = :account + AND from_public_key = :opponentKey + AND from_me = 0 + AND read = 0 + """) + suspend fun getUnreadCountForDialog(account: String, opponentKey: String): Int + /** * Удалить все сообщения диалога */ @@ -345,8 +358,74 @@ interface DialogDao { fun getTotalUnreadCountExcludingFlow(account: String, excludeOpponentKey: String): Flow /** - * Получить общее количество непрочитанных сообщений + * Обновить диалог, пересчитав счетчики из таблицы messages + * Этот метод аналогичен updateDialog из Архива - обновляет все поля диалога одним запросом + * + * Логика: + * 1. Берем последнее сообщение (по timestamp DESC) + * 2. Считаем количество непрочитанных сообщений (from_me = 0 AND read = 0) + * 3. Обновляем диалог или создаем новый */ - @Query("SELECT COALESCE(SUM(unread_count), 0) FROM dialogs WHERE account = :account") - fun getTotalUnreadCountFlow(account: String): Flow + @Query(""" + INSERT OR REPLACE INTO dialogs ( + account, + opponent_key, + opponent_title, + opponent_username, + last_message, + last_message_timestamp, + unread_count, + is_online, + last_seen, + verified + ) + SELECT + :account AS account, + :opponentKey AS opponent_key, + COALESCE( + (SELECT opponent_title FROM dialogs WHERE account = :account AND opponent_key = :opponentKey), + '' + ) AS opponent_title, + COALESCE( + (SELECT opponent_username FROM dialogs WHERE account = :account AND opponent_key = :opponentKey), + '' + ) AS opponent_username, + COALESCE( + (SELECT plain_message FROM messages + WHERE account = :account + AND ((from_public_key = :opponentKey AND to_public_key = :account) + OR (from_public_key = :account AND to_public_key = :opponentKey)) + ORDER BY timestamp DESC LIMIT 1), + '' + ) AS last_message, + COALESCE( + (SELECT MAX(timestamp) FROM messages + WHERE account = :account + AND ((from_public_key = :opponentKey AND to_public_key = :account) + OR (from_public_key = :account AND to_public_key = :opponentKey))), + 0 + ) AS last_message_timestamp, + COALESCE( + (SELECT COUNT(*) FROM messages + WHERE account = :account + AND from_public_key = :opponentKey + AND to_public_key = :account + AND from_me = 0 + AND read = 0), + 0 + ) AS unread_count, + COALESCE( + (SELECT is_online FROM dialogs WHERE account = :account AND opponent_key = :opponentKey), + 0 + ) AS is_online, + COALESCE( + (SELECT last_seen FROM dialogs WHERE account = :account AND opponent_key = :opponentKey), + 0 + ) AS last_seen, + COALESCE( + (SELECT verified FROM dialogs WHERE account = :account AND opponent_key = :opponentKey), + 0 + ) AS verified + """) + suspend fun updateDialogFromMessages(account: String, opponentKey: String) } diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index b9a49ce..dc39678 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -392,6 +392,8 @@ class Protocol( */ fun waitPacket(packetId: Int, callback: (Packet) -> Unit) { packetWaiters.getOrPut(packetId) { mutableListOf() }.add(callback) + val count = packetWaiters[packetId]?.size ?: 0 + log("📝 waitPacket(0x${Integer.toHexString(packetId)}) registered. Total handlers for 0x${Integer.toHexString(packetId)}: $count") } /** diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index d478043..3d47344 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -62,8 +62,10 @@ object ProtocolManager { * Инициализация с контекстом для доступа к MessageRepository */ fun initialize(context: Context) { + addLog("🚀 ProtocolManager.initialize() called") messageRepository = MessageRepository.getInstance(context) setupPacketHandlers() + addLog("🚀 ProtocolManager.initialize() completed") } /** @@ -79,10 +81,15 @@ object ProtocolManager { * Настройка обработчиков пакетов */ private fun setupPacketHandlers() { + addLog("📦 setupPacketHandlers() - Registering packet handlers...") + // Обработчик входящих сообщений (0x06) waitPacket(0x06) { packet -> + addLog("📦 ⚡⚡⚡ PACKET 0x06 RECEIVED IN PROTOCOL_MANAGER!!! ⚡⚡⚡") val messagePacket = packet as PacketMessage addLog("📩 Incoming message from ${messagePacket.fromPublicKey.take(16)}...") + addLog(" messageRepository = ${if (messageRepository != null) "OK" else "NULL"}") + addLog(" messageRepository.isInitialized = ${messageRepository?.isInitialized() ?: false}") // ⚡ ВАЖНО: Отправляем подтверждение доставки обратно серверу // Без этого сервер не будет отправлять следующие сообщения! @@ -94,7 +101,12 @@ object ProtocolManager { addLog("✅ Sent delivery confirmation for message ${messagePacket.messageId.take(16)}...") scope.launch { - messageRepository?.handleIncomingMessage(messagePacket) + try { + messageRepository?.handleIncomingMessage(messagePacket) + addLog("✅ handleIncomingMessage completed!") + } catch (e: Exception) { + addLog("❌ handleIncomingMessage ERROR: ${e.message}") + } } } 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 5a320cc..da36b9a 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 @@ -464,7 +464,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { launch(Dispatchers.IO) { // Отмечаем как прочитанные в БД messageDao.markDialogAsRead(account, dialogKey) - dialogDao.clearUnreadCount(account, opponent) + // 🔥 Пересчитываем счетчики из messages + dialogDao.updateDialogFromMessages(account, opponent) // Отправляем read receipt собеседнику if (messages.isNotEmpty()) { @@ -519,7 +520,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // Фоновые операции messageDao.markDialogAsRead(account, dialogKey) - dialogDao.clearUnreadCount(account, opponent) + // 🔥 Пересчитываем счетчики из messages + dialogDao.updateDialogFromMessages(account, opponent) } catch (e: Exception) { } @@ -1013,33 +1015,18 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { /** * Сохранить диалог в базу данных - * 🔒 lastMessage шифруется для безопасного хранения + * � Используем updateDialogFromMessages для пересчета счетчиков из messages */ private suspend fun saveDialog(lastMessage: String, timestamp: Long) { val account = myPublicKey ?: return val opponent = opponentKey ?: return - val privateKey = myPrivateKey ?: return try { - // 🔒 Шифруем lastMessage перед сохранением - val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey) + // 🔥 КРИТИЧНО: Используем updateDialogFromMessages который пересчитывает счетчики + // напрямую из таблицы messages, как в Архиве! + dialogDao.updateDialogFromMessages(account, opponent) - val existingDialog = dialogDao.getDialog(account, opponent) - - if (existingDialog != null) { - // Обновляем последнее сообщение - dialogDao.updateLastMessage(account, opponent, encryptedLastMessage, timestamp) - } else { - // Создаём новый диалог - dialogDao.insertDialog(DialogEntity( - account = account, - opponentKey = opponent, - opponentTitle = opponentTitle, - opponentUsername = opponentUsername, - lastMessage = encryptedLastMessage, - lastMessageTimestamp = timestamp - )) - } + Log.d(TAG, "✅ Dialog saved/updated from messages table") } catch (e: Exception) { Log.e(TAG, "Dialog save error", e) } @@ -1050,34 +1037,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { */ private suspend fun updateDialog(opponentKey: String, lastMessage: String, timestamp: Long, incrementUnread: Boolean) { val account = myPublicKey ?: return - val privateKey = myPrivateKey ?: return try { - // 🔒 Шифруем lastMessage для диалога - val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey) + // 🔥 КРИТИЧНО: Используем updateDialogFromMessages который пересчитывает счетчики + // напрямую из таблицы messages, как в Архиве! + // Это гарантирует что unread_count всегда соответствует реальному количеству непрочитанных + dialogDao.updateDialogFromMessages(account, opponentKey) - val existingDialog = dialogDao.getDialog(account, opponentKey) - - if (existingDialog != null) { - // Обновляем последнее сообщение - dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp) - - // Инкрементируем непрочитанные если нужно - if (incrementUnread) { - dialogDao.incrementUnreadCount(account, opponentKey) - } - } else { - // Создаём новый диалог - dialogDao.insertDialog(DialogEntity( - account = account, - opponentKey = opponentKey, - opponentTitle = opponentTitle, - opponentUsername = opponentUsername, - lastMessage = encryptedLastMessage, // 🔒 Зашифрованный - lastMessageTimestamp = timestamp, - unreadCount = if (incrementUnread) 1 else 0 - )) - } + Log.d(TAG, "✅ Dialog updated from messages table for $opponentKey") } catch (e: Exception) { Log.e(TAG, "updateDialog error", e) } @@ -1252,12 +1219,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { if (lastIncoming.timestamp.time <= lastReadMessageTimestamp) return - // Отмечаем в БД и очищаем счетчик непрочитанных + // Отмечаем в БД и пересчитываем счетчики viewModelScope.launch(Dispatchers.IO) { try { val dialogKey = getDialogKey(account, opponent) messageDao.markDialogAsRead(account, dialogKey) - dialogDao.clearUnreadCount(account, opponent) + // 🔥 Пересчитываем счетчики из messages + dialogDao.updateDialogFromMessages(account, opponent) } catch (e: Exception) { Log.e(TAG, "Mark as read error", e) } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt index fe4cda5..f302820 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt @@ -54,14 +54,20 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio * Установить текущий аккаунт и загрузить диалоги */ fun setAccount(publicKey: String, privateKey: String) { - if (currentAccount == publicKey) return + if (currentAccount == publicKey) { + android.util.Log.d("ChatsListViewModel", "⚠️ setAccount called again for same account, skipping") + return + } currentAccount = publicKey currentPrivateKey = privateKey + android.util.Log.d("ChatsListViewModel", "✅ Setting up dialogs Flow for account: ${publicKey.take(16)}...") + viewModelScope.launch { dialogDao.getDialogsFlow(publicKey) .flowOn(Dispatchers.IO) // 🚀 Flow работает на IO .map { dialogsList -> + android.util.Log.d("ChatsListViewModel", "📋 Dialogs Flow emitted: ${dialogsList.size} dialogs") // 🔓 Расшифровываем lastMessage на IO потоке (PBKDF2 - тяжелая операция!) dialogsList.map { dialog -> val decryptedLastMessage = try { @@ -91,7 +97,9 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio } } .flowOn(Dispatchers.Default) // 🚀 map выполняется на Default (CPU) + .flowOn(Dispatchers.Main) // 🎯 КРИТИЧНО: Обновляем UI на главном потоке! .collect { decryptedDialogs -> + android.util.Log.d("ChatsListViewModel", "✅ Updated UI with ${decryptedDialogs.size} decrypted dialogs") _dialogs.value = decryptedDialogs // 🟢 Подписываемся на онлайн-статусы всех собеседников @@ -125,6 +133,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio /** * Создать или обновить диалог после отправки/получения сообщения + * 🔥 Используем updateDialogFromMessages для пересчета счетчиков из messages */ suspend fun upsertDialog( opponentKey: String, @@ -136,31 +145,20 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio isOnline: Int = 0 ) { if (currentAccount.isEmpty()) return - val privateKey = currentPrivateKey ?: return - // 🔒 Шифруем lastMessage перед сохранением - val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey) - - val existingDialog = dialogDao.getDialog(currentAccount, opponentKey) - - if (existingDialog != null) { - // Обновляем - dialogDao.updateLastMessage(currentAccount, opponentKey, encryptedLastMessage, timestamp) + try { + // 🔥 КРИТИЧНО: Используем updateDialogFromMessages который пересчитывает счетчики + // напрямую из таблицы messages, как в Архиве! + dialogDao.updateDialogFromMessages(currentAccount, opponentKey) + + // Обновляем информацию о собеседнике если есть if (opponentTitle.isNotEmpty()) { dialogDao.updateOpponentInfo(currentAccount, opponentKey, opponentTitle, opponentUsername, verified) } - } else { - // Создаём новый - dialogDao.insertDialog(DialogEntity( - account = currentAccount, - opponentKey = opponentKey, - opponentTitle = opponentTitle, - opponentUsername = opponentUsername, - lastMessage = encryptedLastMessage, // 🔒 Зашифрованный - lastMessageTimestamp = timestamp, - verified = verified, - isOnline = isOnline - )) + + android.util.Log.d("ChatsListViewModel", "✅ Dialog upserted from messages table") + } catch (e: Exception) { + android.util.Log.e("ChatsListViewModel", "Error upserting dialog", e) } }