Files
mobile-android/AVATAR_IMPLEMENTATION.md
k1ngsterr1 b08bea2c14 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.
2026-01-23 03:04:27 +05:00

12 KiB
Raw Blame History

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 тестирование

  1. Установите приложение на 2 устройства/эмулятора
  2. Авторизуйтесь с разными аккаунтами
  3. Установите аватар на первом устройстве
  4. Откройте чат с первым пользователем на втором устройстве
  5. Проверьте что аватар отобразился

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

Аватары не отображаются

  1. Проверьте что AvatarRepository инициализирован
  2. Проверьте логи декрипции файлов
  3. Проверьте наличие файлов в context.filesDir/avatars/

Аватары не отправляются

  1. Проверьте что PacketAvatar (0x0C) зарегистрирован в Protocol.kt
  2. Проверьте что ProtocolManager имеет ссылку на AvatarRepository
  3. Проверьте статус соединения (AUTHENTICATED)

Ошибки шифрования

  1. Проверьте версию Google Tink (должна быть 1.10.0+)
  2. Проверьте что пароль "rosetta-a" используется везде
  3. Проверьте совместимость с 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 версия

Не совместимо (другой протокол)