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

386 lines
12 KiB
Markdown
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.
# 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)
```kotlin
// Шифрование для 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)
```kotlin
// Сохранение
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)
```kotlin
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)
```kotlin
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)
```kotlin
@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
```kotlin
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:
```kotlin
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
#### Отображение аватара
```kotlin
@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
)
// ... остальной контент
}
}
```
#### Смена аватара
```kotlin
@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 для загрузки
```kotlin
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
```kotlin
LaunchedEffect(opponentPublicKey) {
// Проверяем нужно ли отправить аватар
val isDelivered = avatarRepository.isAvatarDelivered(opponentPublicKey)
val hasAvatar = avatarRepository.getLatestAvatar(currentPublicKey) != null
if (!isDelivered && hasAvatar) {
// Показываем prompt или отправляем автоматически
avatarRepository.sendAvatarTo(opponentPublicKey)
}
}
```
## Тестирование
### 1. Локальное тестирование
```kotlin
// В тестовом классе
@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
```bash
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 версия
Не совместимо (другой протокол)