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 сжатия /** * Получить пароль для шифрования аватаров (для совместимости с desktop) */ fun getAvatarPassword(): String = AVATAR_PASSWORD /** * Сохранить аватар в файловую систему * @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 { android.util.Log.d("AvatarFileManager", "💾 saveAvatar called") android.util.Log.d("AvatarFileManager", "📊 Base64 length: ${base64Image.length}") android.util.Log.d("AvatarFileManager", "👤 Entity ID: ${entityId.take(16)}...") // Генерируем путь как в desktop версии val filePath = generateMd5Path(base64Image, entityId) android.util.Log.d("AvatarFileManager", "🔗 Generated file path: $filePath") // Шифруем данные с паролем "rosetta-a" android.util.Log.d("AvatarFileManager", "🔐 Encrypting with password...") val encrypted = CryptoManager.encryptWithPassword(base64Image, AVATAR_PASSWORD) android.util.Log.d("AvatarFileManager", "✅ Encrypted length: ${encrypted.length}") // Сохраняем в файловую систему val dir = File(context.filesDir, AVATAR_DIR) dir.mkdirs() android.util.Log.d("AvatarFileManager", "📁 Base dir: ${dir.absolutePath}") // Путь формата "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) android.util.Log.d("AvatarFileManager", "💾 Saved to: ${file.absolutePath}") } else { val file = File(dir, filePath) file.writeText(encrypted) android.util.Log.d("AvatarFileManager", "💾 Saved to: ${file.absolutePath}") } android.util.Log.d("AvatarFileManager", "✅ Avatar saved successfully") 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 } } /** * Проверить существует ли аватар по attachment ID (как в Desktop) * Desktop: readFile(`a/${md5(attachment.id + publicKey)}`) * * @param context Android context * @param attachmentId ID attachment * @param publicKey Публичный ключ пользователя * @return true если файл существует */ fun hasAvatarByAttachmentId(context: Context, attachmentId: String, publicKey: String): Boolean { val path = generatePathByAttachmentId(attachmentId, publicKey) val dir = File(context.filesDir, AVATAR_DIR) val file = File(dir, path) return file.exists() } /** * Прочитать аватар по attachment ID (как в Desktop) * Desktop: readFile(`a/${md5(attachment.id + publicKey)}`) * * @param context Android context * @param attachmentId ID attachment * @param publicKey Публичный ключ пользователя * @return Base64-encoded изображение или null */ fun readAvatarByAttachmentId(context: Context, attachmentId: String, publicKey: String): String? { val path = generatePathByAttachmentId(attachmentId, publicKey) return readAvatar(context, path) } /** * Сохранить аватар по attachment ID (как в Desktop) * Desktop: writeFile(`a/${md5(attachment.id + publicKey)}`, encrypted) * * @param context Android context * @param base64Image Base64-encoded изображение * @param attachmentId ID attachment * @param publicKey Публичный ключ пользователя * @return Путь к файлу */ fun saveAvatarByAttachmentId(context: Context, base64Image: String, attachmentId: String, publicKey: String): String { val path = generatePathByAttachmentId(attachmentId, publicKey) // Шифруем данные с паролем "rosetta-a" val encrypted = CryptoManager.encryptWithPassword(base64Image, AVATAR_PASSWORD) // Сохраняем в файловую систему val dir = File(context.filesDir, AVATAR_DIR) dir.mkdirs() val parts = path.split("/") if (parts.size == 2) { val subDir = File(dir, parts[0]) subDir.mkdirs() val file = File(subDir, parts[1]) file.writeText(encrypted) android.util.Log.d("AvatarFileManager", "💾 Avatar saved to: ${file.absolutePath}") } return path } /** * Генерировать путь по attachment ID (как в Desktop) * Desktop: `a/${md5(attachment.id + publicKey)}` */ private fun generatePathByAttachmentId(attachmentId: String, publicKey: String): String { val md5 = MessageDigest.getInstance("MD5") .digest("$attachmentId$publicKey".toByteArray()) .joinToString("") { "%02x".format(it) } return "a/$md5" } /** * Генерировать 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 { // Check for data URI prefix val actualBase64 = if (base64.contains(",")) { android.util.Log.d("AvatarFileManager", "🔍 Removing data URI prefix, orig len=${base64.length}") base64.substringAfter(",") } else { base64 } android.util.Log.d("AvatarFileManager", "🔍 Decoding base64, len=${actualBase64.length}, prefix=${actualBase64.take(50)}") val imageBytes = Base64.decode(actualBase64, Base64.NO_WRAP) android.util.Log.d("AvatarFileManager", "🔍 Decoded bytes=${imageBytes.size}") val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) android.util.Log.d("AvatarFileManager", "🔍 Bitmap result=${bitmap != null}, size=${bitmap?.width}x${bitmap?.height}") bitmap } catch (e: Exception) { android.util.Log.e("AvatarFileManager", "❌ base64ToBitmap error: ${e.message}") 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 } } }