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:
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user