feat: Enhance avatar management with detailed logging and error handling

This commit is contained in:
k1ngsterr1
2026-01-24 00:26:23 +05:00
parent b08bea2c14
commit 1367864008
11 changed files with 107 additions and 324 deletions

View File

@@ -1,37 +1,29 @@
package com.rosetta.messenger.repository
import android.content.Context
import android.util.Base64
import android.util.Log
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.database.AvatarCacheEntity
import com.rosetta.messenger.database.AvatarDao
import com.rosetta.messenger.database.AvatarDeliveryEntity
import com.rosetta.messenger.network.PacketAvatar
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.utils.AvatarFileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import java.security.MessageDigest
/**
* Репозиторий для работы с аватарами
* Совместимо с desktop версией (AvatarProvider)
*
* Возможности:
* - Загрузка/сохранение аватаров
* - P2P доставка аватаров (PacketAvatar 0x0C)
* - Multi-layer кэширование (Memory + SQLite + Encrypted Files)
* - Трекинг доставки
* - Поддержка истории аватаров
* Desktop логика:
* - Аватары передаются как attachment в сообщениях (AttachmentType.AVATAR)
* - Локальное хранение: SQLite (avatar_cache) + зашифрованные файлы
* - Memory cache для декодированных изображений
*
* НЕТ отдельного P2P пакета для аватаров!
*/
class AvatarRepository(
private val context: Context,
private val avatarDao: AvatarDao,
private val currentPublicKey: String,
private val currentPrivateKey: String,
private val protocolManager: ProtocolManager
private val currentPublicKey: String
) {
companion object {
private const val TAG = "AvatarRepository"
@@ -88,6 +80,8 @@ class AvatarRepository(
/**
* Сохранить полученный аватар от другого пользователя
* Вызывается при получении attachment с типом AVATAR в сообщении
*
* @param fromPublicKey Публичный ключ отправителя
* @param base64Image Base64-encoded изображение
*/
@@ -116,14 +110,21 @@ class AvatarRepository(
}
/**
* Изменить свой аватар
* Изменить свой аватар (сохранить локально)
* Отправка происходит через сообщение с attachment типа AVATAR
*
* @param base64Image Base64-encoded изображение
*/
suspend fun changeMyAvatar(base64Image: String) {
withContext(Dispatchers.IO) {
try {
Log.d(TAG, "🔄 changeMyAvatar called")
Log.d(TAG, "👤 Current public key: ${currentPublicKey.take(16)}...")
Log.d(TAG, "📊 Base64 image length: ${base64Image.length} chars")
// Сохраняем файл
val filePath = AvatarFileManager.saveAvatar(context, base64Image, currentPublicKey)
Log.d(TAG, "✅ Avatar file saved: $filePath")
// Сохраняем в БД
val entity = AvatarCacheEntity(
@@ -132,146 +133,15 @@ class AvatarRepository(
timestamp = System.currentTimeMillis()
)
avatarDao.insertAvatar(entity)
// Очищаем трекинг доставки (новый аватар нужно доставить всем заново)
avatarDao.clearDeliveryForAccount(currentPublicKey)
Log.d(TAG, "✅ Avatar inserted to DB")
// Очищаем старые аватары
avatarDao.deleteOldAvatars(currentPublicKey, MAX_AVATAR_HISTORY)
Log.d(TAG, "Changed my avatar")
Log.d(TAG, "🎉 Avatar changed successfully!")
} catch (e: Exception) {
Log.e(TAG, "Failed to change avatar", e)
}
}
}
/**
* Отправить свой аватар контакту через PacketAvatar
* @param toPublicKey Публичный ключ получателя
*/
suspend fun sendAvatarTo(toPublicKey: String) {
withContext(Dispatchers.IO) {
try {
// Проверяем, не доставлен ли уже
if (avatarDao.isAvatarDelivered(toPublicKey, currentPublicKey)) {
Log.d(TAG, "Avatar already delivered to $toPublicKey")
return@withContext
}
// Получаем свой аватар
val myAvatar = getLatestAvatar(currentPublicKey)
if (myAvatar == null) {
Log.d(TAG, "No avatar to send")
return@withContext
}
// Шифруем ChaCha20
val chachaResult = CryptoManager.chacha20Encrypt(myAvatar.base64Data)
// Объединяем key + nonce (32 + 24 = 56 байт)
val keyNonceHex = chachaResult.key + chachaResult.nonce
// Шифруем RSA (публичным ключом получателя)
val keyNonceBytes = keyNonceHex.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
val keyNonceBase64 = Base64.encodeToString(keyNonceBytes, Base64.NO_WRAP)
val encryptedKeyNonce = CryptoManager.encrypt(keyNonceBase64, toPublicKey)
// Создаем пакет
val packet = PacketAvatar().apply {
privateKey = CryptoManager.generatePrivateKeyHash(currentPrivateKey)
fromPublicKey = currentPublicKey
this.toPublicKey = toPublicKey
chachaKey = encryptedKeyNonce
blob = chachaResult.ciphertext
}
// Отправляем через протокол
protocolManager.sendPacket(packet)
// Отмечаем как доставленный
markAvatarDelivered(toPublicKey)
Log.d(TAG, "Sent avatar to $toPublicKey")
} catch (e: Exception) {
Log.e(TAG, "Failed to send avatar", e)
}
}
}
/**
* Отметить аватар как доставленный (без фактической отправки)
*/
suspend fun markAvatarDelivered(toPublicKey: String) {
withContext(Dispatchers.IO) {
try {
val entity = AvatarDeliveryEntity(
publicKey = toPublicKey,
account = currentPublicKey
)
avatarDao.markAvatarDelivered(entity)
} catch (e: Exception) {
Log.e(TAG, "Failed to mark avatar delivered", e)
}
}
}
/**
* Проверить доставлен ли аватар контакту
*/
suspend fun isAvatarDelivered(toPublicKey: String): Boolean {
return withContext(Dispatchers.IO) {
avatarDao.isAvatarDelivered(toPublicKey, currentPublicKey)
}
}
/**
* Обработать входящий PacketAvatar
*/
suspend fun handleIncomingAvatar(packet: PacketAvatar) {
withContext(Dispatchers.IO) {
try {
// Проверяем, что пакет адресован нам
if (packet.toPublicKey != currentPublicKey) {
Log.w(TAG, "Avatar packet not for us")
return@withContext
}
// Расшифровываем ChaCha key+nonce (RSA)
val keyNonceBase64 = CryptoManager.decrypt(packet.chachaKey, currentPrivateKey)
if (keyNonceBase64 == null) {
Log.e(TAG, "Failed to decrypt ChaCha key")
return@withContext
}
// Декодируем из Base64
val keyNonceBytes = Base64.decode(keyNonceBase64, Base64.NO_WRAP)
val keyNonceHex = keyNonceBytes.joinToString("") { "%02x".format(it) }
// Разделяем на key (32 байта = 64 hex) и nonce (24 байта = 48 hex)
if (keyNonceHex.length != 112) { // 64 + 48
Log.e(TAG, "Invalid key+nonce length: ${keyNonceHex.length}")
return@withContext
}
val keyHex = keyNonceHex.substring(0, 64)
val nonceHex = keyNonceHex.substring(64, 112)
// Расшифровываем blob (ChaCha20)
val base64Image = CryptoManager.chacha20Decrypt(packet.blob, nonceHex, keyHex)
if (base64Image == null) {
Log.e(TAG, "Failed to decrypt avatar blob")
return@withContext
}
// Сохраняем аватар
saveAvatar(packet.fromPublicKey, base64Image)
Log.d(TAG, "Received avatar from ${packet.fromPublicKey}")
} catch (e: Exception) {
Log.e(TAG, "Failed to handle incoming avatar", e)
Log.e(TAG, "Failed to change avatar: ${e.message}", e)
throw e
}
}
}