feat: Enhance avatar management with detailed logging and error handling
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user