277 lines
11 KiB
Kotlin
277 lines
11 KiB
Kotlin
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
|
||
}
|
||
}
|
||
}
|