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.
This commit is contained in:
2026-01-23 03:04:27 +05:00
parent 6fdad7a4c1
commit b08bea2c14
12 changed files with 1670 additions and 18 deletions

View File

@@ -0,0 +1,129 @@
package com.rosetta.messenger.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
/**
* Entity для кэша аватаров - хранит пути к зашифрованным файлам
* Совместимо с desktop версией (AvatarProvider)
*/
@Entity(
tableName = "avatar_cache",
indices = [
Index(value = ["public_key", "timestamp"])
]
)
data class AvatarCacheEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "public_key")
val publicKey: String,
@ColumnInfo(name = "avatar")
val avatar: String, // Путь к файлу (формат: "a/md5hash")
@ColumnInfo(name = "timestamp")
val timestamp: Long // Unix timestamp
)
/**
* Entity для трекинга доставки аватаров
* Отслеживает кому уже был отправлен текущий аватар
*/
@Entity(
tableName = "avatar_delivery",
indices = [
Index(value = ["public_key", "account"], unique = true)
]
)
data class AvatarDeliveryEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "public_key")
val publicKey: String, // Публичный ключ получателя
@ColumnInfo(name = "account")
val account: String // Публичный ключ отправителя (мой аккаунт)
)
/**
* DAO для работы с аватарами
*/
@Dao
interface AvatarDao {
// ============ Avatar Cache ============
/**
* Получить все аватары пользователя (отсортированные по времени)
*/
@Query("SELECT * FROM avatar_cache WHERE public_key = :publicKey ORDER BY timestamp DESC")
fun getAvatars(publicKey: String): Flow<List<AvatarCacheEntity>>
/**
* Получить последний аватар пользователя
*/
@Query("SELECT * FROM avatar_cache WHERE public_key = :publicKey ORDER BY timestamp DESC LIMIT 1")
suspend fun getLatestAvatar(publicKey: String): AvatarCacheEntity?
/**
* Получить последний аватар пользователя как Flow
*/
@Query("SELECT * FROM avatar_cache WHERE public_key = :publicKey ORDER BY timestamp DESC LIMIT 1")
fun getLatestAvatarFlow(publicKey: String): Flow<AvatarCacheEntity?>
/**
* Сохранить новый аватар
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAvatar(avatar: AvatarCacheEntity)
/**
* Удалить все аватары пользователя (при смене аватара)
*/
@Query("DELETE FROM avatar_cache WHERE public_key = :publicKey")
suspend fun deleteAvatars(publicKey: String)
/**
* Удалить старые аватары (оставить только N последних)
*/
@Query("""
DELETE FROM avatar_cache
WHERE public_key = :publicKey
AND id NOT IN (
SELECT id FROM avatar_cache
WHERE public_key = :publicKey
ORDER BY timestamp DESC
LIMIT :keepCount
)
""")
suspend fun deleteOldAvatars(publicKey: String, keepCount: Int = 5)
// ============ Avatar Delivery ============
/**
* Проверить доставлен ли аватар контакту
*/
@Query("SELECT COUNT(*) > 0 FROM avatar_delivery WHERE public_key = :publicKey AND account = :account")
suspend fun isAvatarDelivered(publicKey: String, account: String): Boolean
/**
* Отметить аватар как доставленный
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun markAvatarDelivered(delivery: AvatarDeliveryEntity)
/**
* Удалить все записи о доставке для аккаунта (при смене аватара)
*/
@Query("DELETE FROM avatar_delivery WHERE account = :account")
suspend fun clearDeliveryForAccount(account: String)
/**
* Получить список контактов, которым доставлен аватар
*/
@Query("SELECT public_key FROM avatar_delivery WHERE account = :account")
suspend fun getDeliveredContacts(account: String): List<String>
}

View File

@@ -12,9 +12,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
EncryptedAccountEntity::class,
MessageEntity::class,
DialogEntity::class,
BlacklistEntity::class
BlacklistEntity::class,
AvatarCacheEntity::class,
AvatarDeliveryEntity::class
],
version = 6,
version = 7,
exportSchema = false
)
abstract class RosettaDatabase : RoomDatabase() {
@@ -22,6 +24,7 @@ abstract class RosettaDatabase : RoomDatabase() {
abstract fun messageDao(): MessageDao
abstract fun dialogDao(): DialogDao
abstract fun blacklistDao(): BlacklistDao
abstract fun avatarDao(): AvatarDao
companion object {
@Volatile
@@ -42,6 +45,31 @@ abstract class RosettaDatabase : RoomDatabase() {
database.execSQL("ALTER TABLE encrypted_accounts ADD COLUMN username TEXT")
}
}
private val MIGRATION_6_7 = object : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
// Создаем таблицу для кэша аватаров
database.execSQL("""
CREATE TABLE IF NOT EXISTS avatar_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
public_key TEXT NOT NULL,
avatar TEXT NOT NULL,
timestamp INTEGER NOT NULL
)
""")
database.execSQL("CREATE INDEX IF NOT EXISTS index_avatar_cache_public_key_timestamp ON avatar_cache (public_key, timestamp)")
// Создаем таблицу для трекинга доставки аватаров
database.execSQL("""
CREATE TABLE IF NOT EXISTS avatar_delivery (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
public_key TEXT NOT NULL,
account TEXT NOT NULL
)
""")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_avatar_delivery_public_key_account ON avatar_delivery (public_key, account)")
}
}
fun getDatabase(context: Context): RosettaDatabase {
return INSTANCE ?: synchronized(this) {
@@ -51,7 +79,7 @@ abstract class RosettaDatabase : RoomDatabase() {
"rosetta_secure.db"
)
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) // WAL mode for performance
.addMigrations(MIGRATION_4_5, MIGRATION_5_6)
.addMigrations(MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7)
.fallbackToDestructiveMigration() // Для разработки - только если миграция не найдена
.build()
INSTANCE = instance