feat: Implement avatar management system with P2P delivery
- Added AvatarRepository for handling avatar storage, retrieval, and delivery. - Created AvatarCacheEntity and AvatarDeliveryEntity for database storage. - Introduced PacketAvatar for P2P avatar transfer between clients. - Enhanced RosettaDatabase to include avatar-related tables and migration. - Developed AvatarFileManager for file operations related to avatars. - Implemented AvatarImage composable for displaying user avatars. - Updated ProfileScreen to support avatar selection and updating. - Added functionality for handling incoming avatar packets in ProtocolManager.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package com.rosetta.messenger.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Base64
|
||||
import com.rosetta.messenger.crypto.CryptoManager
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Менеджер для работы с файлами аватаров
|
||||
* Совместимо с desktop версией:
|
||||
* - Все файлы зашифрованы паролем "rosetta-a"
|
||||
* - Формат пути: "a/md5hash" (без расширения)
|
||||
* - MD5 генерируется из (base64Image + entityId)
|
||||
*/
|
||||
object AvatarFileManager {
|
||||
|
||||
private const val AVATAR_DIR = "avatars"
|
||||
private const val AVATAR_PASSWORD = "rosetta-a"
|
||||
private const val MAX_IMAGE_SIZE = 2048 // Максимальный размер изображения в пикселях
|
||||
private const val JPEG_QUALITY = 85 // Качество JPEG сжатия
|
||||
|
||||
/**
|
||||
* Сохранить аватар в файловую систему
|
||||
* @param context Android context
|
||||
* @param base64Image Base64-encoded изображение
|
||||
* @param entityId ID сущности (publicKey или groupId)
|
||||
* @return Путь к файлу (формат: "a/md5hash")
|
||||
*/
|
||||
fun saveAvatar(context: Context, base64Image: String, entityId: String): String {
|
||||
// Генерируем путь как в desktop версии
|
||||
val filePath = generateMd5Path(base64Image, entityId)
|
||||
|
||||
// Шифруем данные с паролем "rosetta-a"
|
||||
val encrypted = CryptoManager.encryptWithPassword(base64Image, AVATAR_PASSWORD)
|
||||
|
||||
// Сохраняем в файловую систему
|
||||
val dir = File(context.filesDir, AVATAR_DIR)
|
||||
dir.mkdirs()
|
||||
|
||||
// Путь формата "a/md5hash" -> создаем подпапку "a"
|
||||
val parts = filePath.split("/")
|
||||
if (parts.size == 2) {
|
||||
val subDir = File(dir, parts[0])
|
||||
subDir.mkdirs()
|
||||
val file = File(subDir, parts[1])
|
||||
file.writeText(encrypted)
|
||||
} else {
|
||||
val file = File(dir, filePath)
|
||||
file.writeText(encrypted)
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать и расшифровать аватар
|
||||
* @param context Android context
|
||||
* @param path Путь к файлу (формат: "a/md5hash")
|
||||
* @return Base64-encoded изображение или null
|
||||
*/
|
||||
fun readAvatar(context: Context, path: String): String? {
|
||||
return try {
|
||||
val dir = File(context.filesDir, AVATAR_DIR)
|
||||
val file = File(dir, path)
|
||||
|
||||
if (!file.exists()) return null
|
||||
|
||||
val encrypted = file.readText()
|
||||
CryptoManager.decryptWithPassword(encrypted, AVATAR_PASSWORD)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить файл аватара
|
||||
* @param context Android context
|
||||
* @param path Путь к файлу
|
||||
*/
|
||||
fun deleteAvatar(context: Context, path: String) {
|
||||
try {
|
||||
val dir = File(context.filesDir, AVATAR_DIR)
|
||||
val file = File(dir, path)
|
||||
file.delete()
|
||||
} catch (e: Exception) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерировать MD5 путь для аватара (совместимо с desktop)
|
||||
* Desktop код:
|
||||
* ```js
|
||||
* const hash = md5(`${data}${entity}`);
|
||||
* return `a/${hash}`;
|
||||
* ```
|
||||
*/
|
||||
fun generateMd5Path(data: String, entity: String): String {
|
||||
val md5 = MessageDigest.getInstance("MD5")
|
||||
.digest("$data$entity".toByteArray())
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
return "a/$md5"
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертировать URI изображения в Base64 PNG
|
||||
* Это соответствует desktop функции imagePrepareForNetworkTransfer()
|
||||
* которая конвертирует все изображения в PNG для кросс-платформенной совместимости
|
||||
*
|
||||
* @param context Android context
|
||||
* @param imageData Данные изображения (может быть JPEG, PNG, etc.)
|
||||
* @return Base64-encoded PNG изображение
|
||||
*/
|
||||
fun imagePrepareForNetworkTransfer(context: Context, imageData: ByteArray): String {
|
||||
// Декодируем изображение
|
||||
var bitmap = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
|
||||
|
||||
// Ресайзим если слишком большое
|
||||
if (bitmap.width > MAX_IMAGE_SIZE || bitmap.height > MAX_IMAGE_SIZE) {
|
||||
val scale = MAX_IMAGE_SIZE.toFloat() / maxOf(bitmap.width, bitmap.height)
|
||||
val newWidth = (bitmap.width * scale).toInt()
|
||||
val newHeight = (bitmap.height * scale).toInt()
|
||||
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
|
||||
}
|
||||
|
||||
// Конвертируем в PNG (для кросс-платформенной совместимости)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
val pngBytes = outputStream.toByteArray()
|
||||
|
||||
// Конвертируем в Base64
|
||||
return Base64.encodeToString(pngBytes, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертировать Base64 в Bitmap для отображения
|
||||
*/
|
||||
fun base64ToBitmap(base64: String): Bitmap? {
|
||||
return try {
|
||||
val imageBytes = Base64.decode(base64, Base64.NO_WRAP)
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить размер файла в байтах
|
||||
*/
|
||||
fun getFileSize(context: Context, path: String): Long {
|
||||
return try {
|
||||
val dir = File(context.filesDir, AVATAR_DIR)
|
||||
val file = File(dir, path)
|
||||
if (file.exists()) file.length() else 0
|
||||
} catch (e: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить все аватары (для отладки)
|
||||
*/
|
||||
fun clearAllAvatars(context: Context) {
|
||||
try {
|
||||
val dir = File(context.filesDir, AVATAR_DIR)
|
||||
dir.deleteRecursively()
|
||||
} catch (e: Exception) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user