Files
mobile-android/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt

277 lines
11 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}
}