diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 2b85a7b..e9d1631 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -516,15 +516,13 @@ fun MainScreen( val profileState by profileViewModel.state.collectAsState() // AvatarRepository для работы с аватарами - val avatarRepository = remember(accountPublicKey, accountPrivateKey) { + val avatarRepository = remember(accountPublicKey) { if (accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") { val database = RosettaDatabase.getDatabase(context) AvatarRepository( context = context, avatarDao = database.avatarDao(), - currentPublicKey = accountPublicKey, - currentPrivateKey = accountPrivateKey, - protocolManager = ProtocolManager + currentPublicKey = accountPublicKey ) } else { null @@ -782,7 +780,8 @@ fun MainScreen( showLogsScreen = true }, viewModel = profileViewModel, - avatarRepository = avatarRepository + avatarRepository = avatarRepository, + dialogDao = RosettaDatabase.getDatabase(context).dialogDao() ) } } diff --git a/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt b/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt index b47f0fa..2907331 100644 --- a/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt +++ b/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt @@ -411,29 +411,30 @@ object CryptoManager { */ fun encrypt(data: String, publicKeyHex: String): String { val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1") - val keyPairGenerator = KeyPairGenerator.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME) - keyPairGenerator.initialize(ecSpec, SecureRandom()) - // Generate ephemeral key pair - val ephemeralKeyPair = keyPairGenerator.generateKeyPair() - val ephemeralPrivateKey = ephemeralKeyPair.private as org.bouncycastle.jce.interfaces.ECPrivateKey - val ephemeralPublicKey = ephemeralKeyPair.public as org.bouncycastle.jce.interfaces.ECPublicKey + // Generate ephemeral private key (random 32 bytes) + val ephemeralPrivateKeyBytes = ByteArray(32) + SecureRandom().nextBytes(ephemeralPrivateKeyBytes) + val ephemeralPrivateKeyBigInt = BigInteger(1, ephemeralPrivateKeyBytes) + + // Generate ephemeral public key from private key + val ephemeralPublicKeyPoint = ecSpec.g.multiply(ephemeralPrivateKeyBigInt) // Parse recipient's public key val recipientPublicKeyBytes = publicKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() val recipientPublicKeyPoint = ecSpec.curve.decodePoint(recipientPublicKeyBytes) - val recipientPublicKeySpec = ECPublicKeySpec(recipientPublicKeyPoint, ecSpec) - val keyFactory = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME) - val recipientPublicKey = keyFactory.generatePublic(recipientPublicKeySpec) - // Compute shared secret using ECDH - val keyAgreement = javax.crypto.KeyAgreement.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME) - keyAgreement.init(ephemeralPrivateKey) - keyAgreement.doPhase(recipientPublicKey, true) - val sharedSecret = keyAgreement.generateSecret() + // Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey) + val sharedPoint = recipientPublicKeyPoint.multiply(ephemeralPrivateKeyBigInt).normalize() - // Use first 32 bytes (x-coordinate) as AES key - val sharedKey = sharedSecret.copyOfRange(1, 33) + // Use x-coordinate of shared point as AES key (32 bytes) + val sharedKeyBytes = sharedPoint.affineXCoord.encoded + val sharedKey = if (sharedKeyBytes.size >= 32) { + sharedKeyBytes.copyOfRange(sharedKeyBytes.size - 32, sharedKeyBytes.size) + } else { + // Pad with leading zeros if needed + ByteArray(32 - sharedKeyBytes.size) + sharedKeyBytes + } val key = SecretKeySpec(sharedKey, "AES") // Generate random IV @@ -446,8 +447,7 @@ object CryptoManager { cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec) val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) - // Get ephemeral private key bytes - val ephemeralPrivateKeyBytes = ephemeralPrivateKey.d.toByteArray() + // Normalize ephemeral private key to 32 bytes val normalizedPrivateKey = if (ephemeralPrivateKeyBytes.size > 32) { ephemeralPrivateKeyBytes.copyOfRange(ephemeralPrivateKeyBytes.size - 32, ephemeralPrivateKeyBytes.size) } else { diff --git a/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt b/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt index 560e01a..cb998d6 100644 --- a/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/AvatarEntities.kt @@ -6,6 +6,10 @@ import kotlinx.coroutines.flow.Flow /** * Entity для кэша аватаров - хранит пути к зашифрованным файлам * Совместимо с desktop версией (AvatarProvider) + * + * Desktop логика: + * - Аватары передаются как attachment в сообщениях (AttachmentType.AVATAR) + * - Локальное хранение: SQLite + зашифрованные файлы */ @Entity( tableName = "avatar_cache", @@ -27,35 +31,12 @@ data class AvatarCacheEntity( 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 ============ - /** * Получить все аватары пользователя (отсортированные по времени) */ @@ -81,7 +62,7 @@ interface AvatarDao { suspend fun insertAvatar(avatar: AvatarCacheEntity) /** - * Удалить все аватары пользователя (при смене аватара) + * Удалить все аватары пользователя */ @Query("DELETE FROM avatar_cache WHERE public_key = :publicKey") suspend fun deleteAvatars(publicKey: String) @@ -100,30 +81,4 @@ interface AvatarDao { ) """) 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 } diff --git a/app/src/main/java/com/rosetta/messenger/database/RosettaDatabase.kt b/app/src/main/java/com/rosetta/messenger/database/RosettaDatabase.kt index 8c967d3..dcdc10f 100644 --- a/app/src/main/java/com/rosetta/messenger/database/RosettaDatabase.kt +++ b/app/src/main/java/com/rosetta/messenger/database/RosettaDatabase.kt @@ -13,10 +13,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase MessageEntity::class, DialogEntity::class, BlacklistEntity::class, - AvatarCacheEntity::class, - AvatarDeliveryEntity::class + AvatarCacheEntity::class ], - version = 7, + version = 8, exportSchema = false ) abstract class RosettaDatabase : RoomDatabase() { @@ -58,16 +57,13 @@ abstract class RosettaDatabase : RoomDatabase() { ) """) 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)") + } + } + + private val MIGRATION_7_8 = object : Migration(7, 8) { + override fun migrate(database: SupportSQLiteDatabase) { + // Удаляем таблицу avatar_delivery (больше не нужна) + database.execSQL("DROP TABLE IF EXISTS avatar_delivery") } } @@ -79,7 +75,7 @@ abstract class RosettaDatabase : RoomDatabase() { "rosetta_secure.db" ) .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) // WAL mode for performance - .addMigrations(MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7) + .addMigrations(MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8) .fallbackToDestructiveMigration() // Для разработки - только если миграция не найдена .build() INSTANCE = instance diff --git a/app/src/main/java/com/rosetta/messenger/network/Packets.kt b/app/src/main/java/com/rosetta/messenger/network/Packets.kt index 94741d7..50e6bfd 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Packets.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Packets.kt @@ -479,59 +479,6 @@ class PacketPushToken : Packet() { } } -/** - * Avatar packet (ID: 0x0C) - * P2P доставка аватаров между клиентами - * Совместимо с desktop версией (AvatarProvider) - * - * Структура: - * - privateKey: Hash приватного ключа отправителя (для аутентификации) - * - fromPublicKey: Публичный ключ отправителя - * - toPublicKey: Публичный ключ получателя - * - chachaKey: RSA-encrypted ChaCha20 key+nonce (hex) - * - blob: ChaCha20-encrypted avatar data (hex, base64 изображение внутри) - * - * Процесс шифрования (отправка): - * 1. Генерируется случайный ChaCha20 key (32 байта) + nonce (24 байта) - * 2. Base64-изображение шифруется ChaCha20-Poly1305 - * 3. ChaCha key+nonce конкатенируются и шифруются RSA публичным ключом получателя - * 4. Отправляется пакет с зашифрованными данными - * - * Процесс расшифровки (получение): - * 1. RSA-расшифровка chachaKey приватным ключом получателя - * 2. Извлечение key (32 байта) и nonce (24 байта) - * 3. ChaCha20-расшифровка blob - * 4. Получение base64-изображения - */ -class PacketAvatar : Packet() { - var privateKey: String = "" - var fromPublicKey: String = "" - var toPublicKey: String = "" - var chachaKey: String = "" // RSA-encrypted (hex) - var blob: String = "" // ChaCha20-encrypted (hex) - - override fun getPacketId(): Int = 0x0C - - override fun receive(stream: Stream) { - privateKey = stream.readString() - fromPublicKey = stream.readString() - toPublicKey = stream.readString() - chachaKey = stream.readString() - blob = stream.readString() - } - - override fun send(): Stream { - val stream = Stream() - stream.writeInt16(getPacketId()) - stream.writeString(privateKey) - stream.writeString(fromPublicKey) - stream.writeString(toPublicKey) - stream.writeString(chachaKey) - stream.writeString(blob) - return stream - } -} - /** * Push Notification Action */ diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index c868bfb..f86f0a1 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -119,8 +119,7 @@ class Protocol( 0x07 to { PacketRead() }, 0x08 to { PacketDelivery() }, 0x09 to { PacketChunk() }, - 0x0B to { PacketTyping() }, - 0x0C to { PacketAvatar() } + 0x0B to { PacketTyping() } ) init { diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index 87e32c0..6b08027 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -174,19 +174,6 @@ object ProtocolManager { } } } - - // 🖼️ Обработчик аватаров (0x0C) - // P2P доставка аватаров между клиентами - waitPacket(0x0C) { packet -> - val avatarPacket = packet as PacketAvatar - android.util.Log.d("Protocol", "🖼️ Received avatar from ${avatarPacket.fromPublicKey.take(16)}...") - - scope.launch(Dispatchers.IO) { - // TODO: Передавать avatarRepository через initialize() - // Пока что логируем для отладки - // avatarRepository?.handleIncomingAvatar(avatarPacket) - } - } } /** diff --git a/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt b/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt index dd45fe2..7259f44 100644 --- a/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/repository/AvatarRepository.kt @@ -1,37 +1,29 @@ package com.rosetta.messenger.repository import android.content.Context -import android.util.Base64 import android.util.Log -import com.rosetta.messenger.crypto.CryptoManager import com.rosetta.messenger.database.AvatarCacheEntity import com.rosetta.messenger.database.AvatarDao -import com.rosetta.messenger.database.AvatarDeliveryEntity -import com.rosetta.messenger.network.PacketAvatar -import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.utils.AvatarFileManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext -import java.security.MessageDigest /** * Репозиторий для работы с аватарами * Совместимо с desktop версией (AvatarProvider) * - * Возможности: - * - Загрузка/сохранение аватаров - * - P2P доставка аватаров (PacketAvatar 0x0C) - * - Multi-layer кэширование (Memory + SQLite + Encrypted Files) - * - Трекинг доставки - * - Поддержка истории аватаров + * Desktop логика: + * - Аватары передаются как attachment в сообщениях (AttachmentType.AVATAR) + * - Локальное хранение: SQLite (avatar_cache) + зашифрованные файлы + * - Memory cache для декодированных изображений + * + * НЕТ отдельного P2P пакета для аватаров! */ class AvatarRepository( private val context: Context, private val avatarDao: AvatarDao, - private val currentPublicKey: String, - private val currentPrivateKey: String, - private val protocolManager: ProtocolManager + private val currentPublicKey: String ) { companion object { private const val TAG = "AvatarRepository" @@ -88,6 +80,8 @@ class AvatarRepository( /** * Сохранить полученный аватар от другого пользователя + * Вызывается при получении attachment с типом AVATAR в сообщении + * * @param fromPublicKey Публичный ключ отправителя * @param base64Image Base64-encoded изображение */ @@ -116,14 +110,21 @@ class AvatarRepository( } /** - * Изменить свой аватар + * Изменить свой аватар (сохранить локально) + * Отправка происходит через сообщение с attachment типа AVATAR + * * @param base64Image Base64-encoded изображение */ suspend fun changeMyAvatar(base64Image: String) { withContext(Dispatchers.IO) { try { + Log.d(TAG, "🔄 changeMyAvatar called") + Log.d(TAG, "👤 Current public key: ${currentPublicKey.take(16)}...") + Log.d(TAG, "📊 Base64 image length: ${base64Image.length} chars") + // Сохраняем файл val filePath = AvatarFileManager.saveAvatar(context, base64Image, currentPublicKey) + Log.d(TAG, "✅ Avatar file saved: $filePath") // Сохраняем в БД val entity = AvatarCacheEntity( @@ -132,146 +133,15 @@ class AvatarRepository( timestamp = System.currentTimeMillis() ) avatarDao.insertAvatar(entity) - - // Очищаем трекинг доставки (новый аватар нужно доставить всем заново) - avatarDao.clearDeliveryForAccount(currentPublicKey) + Log.d(TAG, "✅ Avatar inserted to DB") // Очищаем старые аватары avatarDao.deleteOldAvatars(currentPublicKey, MAX_AVATAR_HISTORY) - Log.d(TAG, "Changed my avatar") + Log.d(TAG, "🎉 Avatar changed successfully!") } catch (e: Exception) { - Log.e(TAG, "Failed to change avatar", e) - } - } - } - - /** - * Отправить свой аватар контакту через PacketAvatar - * @param toPublicKey Публичный ключ получателя - */ - suspend fun sendAvatarTo(toPublicKey: String) { - withContext(Dispatchers.IO) { - try { - // Проверяем, не доставлен ли уже - if (avatarDao.isAvatarDelivered(toPublicKey, currentPublicKey)) { - Log.d(TAG, "Avatar already delivered to $toPublicKey") - return@withContext - } - - // Получаем свой аватар - val myAvatar = getLatestAvatar(currentPublicKey) - if (myAvatar == null) { - Log.d(TAG, "No avatar to send") - return@withContext - } - - // Шифруем ChaCha20 - val chachaResult = CryptoManager.chacha20Encrypt(myAvatar.base64Data) - - // Объединяем key + nonce (32 + 24 = 56 байт) - val keyNonceHex = chachaResult.key + chachaResult.nonce - - // Шифруем RSA (публичным ключом получателя) - val keyNonceBytes = keyNonceHex.chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() - val keyNonceBase64 = Base64.encodeToString(keyNonceBytes, Base64.NO_WRAP) - val encryptedKeyNonce = CryptoManager.encrypt(keyNonceBase64, toPublicKey) - - // Создаем пакет - val packet = PacketAvatar().apply { - privateKey = CryptoManager.generatePrivateKeyHash(currentPrivateKey) - fromPublicKey = currentPublicKey - this.toPublicKey = toPublicKey - chachaKey = encryptedKeyNonce - blob = chachaResult.ciphertext - } - - // Отправляем через протокол - protocolManager.sendPacket(packet) - - // Отмечаем как доставленный - markAvatarDelivered(toPublicKey) - - Log.d(TAG, "Sent avatar to $toPublicKey") - } catch (e: Exception) { - Log.e(TAG, "Failed to send avatar", e) - } - } - } - - /** - * Отметить аватар как доставленный (без фактической отправки) - */ - suspend fun markAvatarDelivered(toPublicKey: String) { - withContext(Dispatchers.IO) { - try { - val entity = AvatarDeliveryEntity( - publicKey = toPublicKey, - account = currentPublicKey - ) - avatarDao.markAvatarDelivered(entity) - } catch (e: Exception) { - Log.e(TAG, "Failed to mark avatar delivered", e) - } - } - } - - /** - * Проверить доставлен ли аватар контакту - */ - suspend fun isAvatarDelivered(toPublicKey: String): Boolean { - return withContext(Dispatchers.IO) { - avatarDao.isAvatarDelivered(toPublicKey, currentPublicKey) - } - } - - /** - * Обработать входящий PacketAvatar - */ - suspend fun handleIncomingAvatar(packet: PacketAvatar) { - withContext(Dispatchers.IO) { - try { - // Проверяем, что пакет адресован нам - if (packet.toPublicKey != currentPublicKey) { - Log.w(TAG, "Avatar packet not for us") - return@withContext - } - - // Расшифровываем ChaCha key+nonce (RSA) - val keyNonceBase64 = CryptoManager.decrypt(packet.chachaKey, currentPrivateKey) - if (keyNonceBase64 == null) { - Log.e(TAG, "Failed to decrypt ChaCha key") - return@withContext - } - - // Декодируем из Base64 - val keyNonceBytes = Base64.decode(keyNonceBase64, Base64.NO_WRAP) - val keyNonceHex = keyNonceBytes.joinToString("") { "%02x".format(it) } - - // Разделяем на key (32 байта = 64 hex) и nonce (24 байта = 48 hex) - if (keyNonceHex.length != 112) { // 64 + 48 - Log.e(TAG, "Invalid key+nonce length: ${keyNonceHex.length}") - return@withContext - } - - val keyHex = keyNonceHex.substring(0, 64) - val nonceHex = keyNonceHex.substring(64, 112) - - // Расшифровываем blob (ChaCha20) - val base64Image = CryptoManager.chacha20Decrypt(packet.blob, nonceHex, keyHex) - if (base64Image == null) { - Log.e(TAG, "Failed to decrypt avatar blob") - return@withContext - } - - // Сохраняем аватар - saveAvatar(packet.fromPublicKey, base64Image) - - Log.d(TAG, "Received avatar from ${packet.fromPublicKey}") - } catch (e: Exception) { - Log.e(TAG, "Failed to handle incoming avatar", e) + Log.e(TAG, "❌ Failed to change avatar: ${e.message}", e) + throw e } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index aaa0ad9..e660fe3 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -735,24 +735,28 @@ fun UnlockScreen( onUnlocking = { isUnlocking = it }, onError = { error = it }, onSuccess = { decryptedAccount -> - // If biometric is enabled, save password + // If biometric is enabled and password not saved yet, save password if (biometricAvailable is BiometricAvailability.Available && isBiometricEnabled && activity != null) { scope.launch { - biometricManager.encryptPassword( - activity = activity, - password = password, - onSuccess = { encryptedPassword -> - scope.launch { - biometricPrefs.saveEncryptedPassword( - decryptedAccount.publicKey, - encryptedPassword - ) - } - }, - onError = { /* Ignore save errors */ }, - onCancel = { /* User cancelled */ } - ) + // Check if password is already saved + val hasPassword = biometricPrefs.hasEncryptedPassword(decryptedAccount.publicKey) + if (!hasPassword) { + biometricManager.encryptPassword( + activity = activity, + password = password, + onSuccess = { encryptedPassword -> + scope.launch { + biometricPrefs.saveEncryptedPassword( + decryptedAccount.publicKey, + encryptedPassword + ) + } + }, + onError = { /* Ignore save errors */ }, + onCancel = { /* User cancelled */ } + ) + } } } onUnlocked(decryptedAccount) diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt index e4c2af6..8eb4fc9 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt @@ -144,7 +144,8 @@ fun ProfileScreen( onNavigateToSafety: () -> Unit = {}, onNavigateToLogs: () -> Unit = {}, viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), - avatarRepository: AvatarRepository? = null + avatarRepository: AvatarRepository? = null, + dialogDao: com.rosetta.messenger.database.DialogDao? = null ) { val context = LocalContext.current val activity = context as? FragmentActivity @@ -169,32 +170,46 @@ fun ProfileScreen( val imagePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> + Log.d(TAG, "🖼️ Image picker result: uri=$uri") uri?.let { scope.launch { try { + Log.d(TAG, "📁 Reading image from URI: $uri") // Читаем файл изображения val inputStream = context.contentResolver.openInputStream(uri) val imageBytes = inputStream?.readBytes() inputStream?.close() + Log.d(TAG, "📊 Image bytes read: ${imageBytes?.size ?: 0} bytes") + if (imageBytes != null) { + Log.d(TAG, "🔄 Converting to PNG Base64...") // Конвертируем в PNG Base64 (кросс-платформенная совместимость) val base64Png = withContext(Dispatchers.IO) { AvatarFileManager.imagePrepareForNetworkTransfer(context, imageBytes) } + Log.d(TAG, "✅ Converted to Base64: ${base64Png.length} chars") + Log.d(TAG, "🔐 Avatar repository available: ${avatarRepository != null}") + Log.d(TAG, "👤 Current public key: ${accountPublicKey.take(16)}...") + // Сохраняем аватар через репозиторий + Log.d(TAG, "💾 Calling avatarRepository.changeMyAvatar()...") avatarRepository?.changeMyAvatar(base64Png) + Log.d(TAG, "🎉 Avatar update completed") + // Показываем успешное сообщение android.widget.Toast.makeText( context, "Avatar updated successfully", android.widget.Toast.LENGTH_SHORT ).show() + } else { + Log.e(TAG, "❌ Image bytes are null") } } catch (e: Exception) { - Log.e(TAG, "Failed to upload avatar", e) + Log.e(TAG, "❌ Failed to upload avatar", e) android.widget.Toast.makeText( context, "Failed to update avatar: ${e.message}", @@ -202,7 +217,7 @@ fun ProfileScreen( ).show() } } - } + } ?: Log.w(TAG, "⚠️ URI is null, image picker cancelled") } // Цвета в зависимости от темы diff --git a/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt b/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt index 77c22fc..1dbe458 100644 --- a/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt +++ b/app/src/main/java/com/rosetta/messenger/utils/AvatarFileManager.kt @@ -31,15 +31,23 @@ object AvatarFileManager { * @return Путь к файлу (формат: "a/md5hash") */ fun saveAvatar(context: Context, base64Image: String, entityId: String): String { + android.util.Log.d("AvatarFileManager", "💾 saveAvatar called") + android.util.Log.d("AvatarFileManager", "📊 Base64 length: ${base64Image.length}") + android.util.Log.d("AvatarFileManager", "👤 Entity ID: ${entityId.take(16)}...") + // Генерируем путь как в desktop версии val filePath = generateMd5Path(base64Image, entityId) + android.util.Log.d("AvatarFileManager", "🔗 Generated file path: $filePath") // Шифруем данные с паролем "rosetta-a" + android.util.Log.d("AvatarFileManager", "🔐 Encrypting with password...") val encrypted = CryptoManager.encryptWithPassword(base64Image, AVATAR_PASSWORD) + android.util.Log.d("AvatarFileManager", "✅ Encrypted length: ${encrypted.length}") // Сохраняем в файловую систему val dir = File(context.filesDir, AVATAR_DIR) dir.mkdirs() + android.util.Log.d("AvatarFileManager", "📁 Base dir: ${dir.absolutePath}") // Путь формата "a/md5hash" -> создаем подпапку "a" val parts = filePath.split("/") @@ -48,11 +56,14 @@ object AvatarFileManager { subDir.mkdirs() val file = File(subDir, parts[1]) file.writeText(encrypted) + android.util.Log.d("AvatarFileManager", "💾 Saved to: ${file.absolutePath}") } else { val file = File(dir, filePath) file.writeText(encrypted) + android.util.Log.d("AvatarFileManager", "💾 Saved to: ${file.absolutePath}") } + android.util.Log.d("AvatarFileManager", "✅ Avatar saved successfully") return filePath }