feat: Enhance logging in MessageRepository and ChatsListViewModel for better debugging and flow tracking

This commit is contained in:
k1ngsterr1
2026-01-13 23:28:48 +05:00
parent 14ef342e80
commit 2c173bda26
6 changed files with 232 additions and 94 deletions

View File

@@ -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)
}
}

View File

@@ -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<Int>
/**
* Получить общее количество непрочитанных сообщений
* Обновить диалог, пересчитав счетчики из таблицы 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<Int>
@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)
}

View File

@@ -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")
}
/**

View File

@@ -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}")
}
}
}

View File

@@ -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 шифруется для безопасного хранения
* <EFBFBD> Используем 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)
}

View File

@@ -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)
}
}