- 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.
12 KiB
12 KiB
Avatar Implementation Guide
Обзор
Реализована полная система аватаров для Rosetta Messenger Android, совместимая с desktop версией:
- ✅ P2P доставка аватаров (PacketAvatar 0x0C)
- ✅ Multi-layer кэширование (Memory + SQLite + Encrypted Files)
- ✅ ChaCha20-Poly1305 шифрование для передачи
- ✅ Password-based шифрование для хранения
- ✅ Трекинг доставки аватаров
- ✅ UI компоненты (AvatarImage, AvatarPlaceholder)
Архитектура
1. Crypto Layer (CryptoManager.kt)
// Шифрование для P2P передачи
val result = CryptoManager.chacha20Encrypt(base64Image)
// result.key, result.nonce, result.ciphertext
// Шифрование для локального хранения (пароль "rosetta-a")
val encrypted = CryptoManager.encryptWithPassword(data, "rosetta-a")
2. Database Layer
- avatar_cache: Хранит пути к зашифрованным файлам
- avatar_delivery: Трекинг доставки (кому отправлен аватар)
3. File Storage (AvatarFileManager.kt)
// Сохранение
val path = AvatarFileManager.saveAvatar(context, base64Image, publicKey)
// Путь: "a/md5hash"
// Чтение
val base64Image = AvatarFileManager.readAvatar(context, path)
// Конвертация изображения в PNG Base64
val base64Png = AvatarFileManager.imagePrepareForNetworkTransfer(context, imageBytes)
4. Repository Layer (AvatarRepository.kt)
val avatarRepository = AvatarRepository(
context = context,
avatarDao = database.avatarDao(),
currentPublicKey = myPublicKey,
currentPrivateKey = myPrivateKey,
protocolManager = ProtocolManager
)
// Получить аватары пользователя
val avatars: StateFlow<List<AvatarInfo>> = avatarRepository.getAvatars(publicKey)
// Изменить свой аватар
avatarRepository.changeMyAvatar(base64Image)
// Отправить аватар контакту
avatarRepository.sendAvatarTo(contactPublicKey)
// Обработать входящий аватар
avatarRepository.handleIncomingAvatar(packetAvatar)
5. Network Layer (PacketAvatar)
class PacketAvatar : Packet() {
var privateKey: String = "" // Hash приватного ключа
var fromPublicKey: String = "" // Отправитель
var toPublicKey: String = "" // Получатель
var chachaKey: String = "" // RSA-encrypted ChaCha20 key+nonce
var blob: String = "" // ChaCha20-encrypted avatar data
override fun getPacketId(): Int = 0x0C
}
6. UI Layer (AvatarImage.kt)
@Composable
fun AvatarImage(
publicKey: String,
avatarRepository: AvatarRepository?,
size: Dp = 40.dp,
isDarkTheme: Boolean,
onClick: (() -> Unit)? = null,
showOnlineIndicator: Boolean = false,
isOnline: Boolean = false
)
Интеграция
Шаг 1: Инициализация в Application/Activity
class MainActivity : ComponentActivity() {
private lateinit var avatarRepository: AvatarRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val database = RosettaDatabase.getDatabase(applicationContext)
// После авторизации пользователя
avatarRepository = AvatarRepository(
context = applicationContext,
avatarDao = database.avatarDao(),
currentPublicKey = currentAccount.publicKey,
currentPrivateKey = currentAccount.privateKey,
protocolManager = ProtocolManager
)
// Передаем в ProtocolManager для обработки входящих пакетов
ProtocolManager.setAvatarRepository(avatarRepository)
}
}
Шаг 2: Обновление ProtocolManager
Добавьте в ProtocolManager:
object ProtocolManager {
private var avatarRepository: AvatarRepository? = null
fun setAvatarRepository(repository: AvatarRepository) {
avatarRepository = repository
}
// В setupPacketHandlers() обработчик уже добавлен:
waitPacket(0x0C) { packet ->
scope.launch(Dispatchers.IO) {
avatarRepository?.handleIncomingAvatar(packet as PacketAvatar)
}
}
}
Шаг 3: Использование в UI
Отображение аватара
@Composable
fun ChatListItem(
publicKey: String,
avatarRepository: AvatarRepository?,
isDarkTheme: Boolean
) {
Row {
AvatarImage(
publicKey = publicKey,
avatarRepository = avatarRepository,
size = 48.dp,
isDarkTheme = isDarkTheme,
showOnlineIndicator = true,
isOnline = user.isOnline
)
// ... остальной контент
}
}
Смена аватара
@Composable
fun ProfileScreen(
avatarRepository: AvatarRepository,
viewModel: ProfileViewModel
) {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
viewModel.uploadAvatar(it, avatarRepository)
}
}
IconButton(onClick = { launcher.launch("image/*") }) {
Icon(Icons.Default.Edit, "Change Avatar")
}
}
ViewModel для загрузки
class ProfileViewModel : ViewModel() {
fun uploadAvatar(uri: Uri, avatarRepository: AvatarRepository) {
viewModelScope.launch {
try {
// Читаем файл
val inputStream = context.contentResolver.openInputStream(uri)
val imageBytes = inputStream?.readBytes()
inputStream?.close()
// Конвертируем в PNG Base64
val base64Png = AvatarFileManager.imagePrepareForNetworkTransfer(
context,
imageBytes!!
)
// Сохраняем
avatarRepository.changeMyAvatar(base64Png)
// Показываем успех
_uiState.value = _uiState.value.copy(
showSuccess = true,
successMessage = "Avatar updated"
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
showError = true,
errorMessage = e.message
)
}
}
}
}
Шаг 4: Автоматическая доставка аватаров
В ChatDetailScreen
LaunchedEffect(opponentPublicKey) {
// Проверяем нужно ли отправить аватар
val isDelivered = avatarRepository.isAvatarDelivered(opponentPublicKey)
val hasAvatar = avatarRepository.getLatestAvatar(currentPublicKey) != null
if (!isDelivered && hasAvatar) {
// Показываем prompt или отправляем автоматически
avatarRepository.sendAvatarTo(opponentPublicKey)
}
}
Тестирование
1. Локальное тестирование
// В тестовом классе
@Test
fun testAvatarEncryptionRoundTrip() {
val originalData = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
// Encrypt
val result = CryptoManager.chacha20Encrypt(originalData)
// Decrypt
val decrypted = CryptoManager.chacha20Decrypt(
result.ciphertext,
result.nonce,
result.key
)
assertEquals(originalData, decrypted)
}
@Test
fun testPasswordEncryption() {
val data = "test image data"
val password = "rosetta-a"
val encrypted = CryptoManager.encryptWithPassword(data, password)
val decrypted = CryptoManager.decryptWithPassword(encrypted, password)
assertEquals(data, decrypted)
}
2. P2P тестирование
- Установите приложение на 2 устройства/эмулятора
- Авторизуйтесь с разными аккаунтами
- Установите аватар на первом устройстве
- Откройте чат с первым пользователем на втором устройстве
- Проверьте что аватар отобразился
3. Проверка в Logcat
adb logcat -s Protocol:D AvatarRepository:D
Ожидаемые логи:
Protocol: 🖼️ Received avatar from 02a1b2c3...
AvatarRepository: Received avatar from 02a1b2c3...
AvatarRepository: Saved avatar for 02a1b2c3...
Производительность
Memory Management
- Memory cache ограничен (хранит только активные диалоги)
- Используйте
avatarRepository.clearMemoryCache()при выходе из чата - Файлы хранятся зашифрованными (экономия памяти)
Network
- Аватары отправляются только 1 раз каждому контакту
- Используется трекинг доставки (avatar_delivery таблица)
- Chunking не реализован (лимит ~5MB на аватар)
Database
- WAL mode включен для производительности
- Индексы на (public_key, timestamp) для быстрого поиска
- Автоматическое удаление старых аватаров (хранится MAX_AVATAR_HISTORY = 5)
Troubleshooting
Аватары не отображаются
- Проверьте что AvatarRepository инициализирован
- Проверьте логи декрипции файлов
- Проверьте наличие файлов в
context.filesDir/avatars/
Аватары не отправляются
- Проверьте что PacketAvatar (0x0C) зарегистрирован в Protocol.kt
- Проверьте что ProtocolManager имеет ссылку на AvatarRepository
- Проверьте статус соединения (AUTHENTICATED)
Ошибки шифрования
- Проверьте версию Google Tink (должна быть 1.10.0+)
- Проверьте что пароль "rosetta-a" используется везде
- Проверьте совместимость с desktop (тестируйте кросс-платформенно)
Будущие улучшения
Приоритет Высокий
- Интеграция AvatarRepository в MainActivity
- Image Picker в AvatarPicker composable
- Avatar upload UI в ProfileScreen
- Automatic delivery prompt (как в desktop)
Приоритет Средний
- Avatar compression перед upload
- Multiple avatar sizes (thumbnail, full)
- Avatar history viewer в профиле
- Group avatars support
Приоритет Низкий
- Chunking для больших файлов (>5MB)
- Coil disk cache integration
- Avatar delete functionality
- System avatars preloading
Совместимость
Desktop версия
✅ ChaCha20-Poly1305 шифрование
✅ PBKDF2+AES для хранения
✅ MD5 path generation
✅ PacketAvatar структура
✅ Delivery tracking
React Native версия
⚠️ Требует тестирования (скорее всего совместимо)
Telegram версия
❌ Не совместимо (другой протокол)