Remove unnecessary logging statements across various components to clean up code and improve readability. This includes removing debug, error, and warning logs from attachment handling, image processing, media loading, and profile management functionalities. Additionally, a script has been added to automate the removal of log statements from Kotlin files.
This commit is contained in:
@@ -15,7 +15,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,7 +93,6 @@ fun AnimatedKeyboardTransition(
|
|||||||
|
|
||||||
if (shouldLog && lastLoggedAlpha.value != animatedAlpha) {
|
if (shouldLog && lastLoggedAlpha.value != animatedAlpha) {
|
||||||
lastLoggedAlpha.value = animatedAlpha
|
lastLoggedAlpha.value = animatedAlpha
|
||||||
Log.d(tag, "🎬 Alpha: ${alphaPercent}%, show=$showEmojiPicker, shouldShow=$shouldShowBox, transitioning=$isTransitioningToKeyboard, kb=${coordinator.keyboardHeight.value.toInt()}dp")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldShowBox) {
|
if (shouldShowBox) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.rosette.android.ui.keyboard
|
|||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
@@ -95,10 +94,8 @@ class KeyboardTransitionCoordinator {
|
|||||||
isEmojiVisible = true // 🔥 ВАЖНО: Устанавливаем флаг видимости emoji!
|
isEmojiVisible = true // 🔥 ВАЖНО: Устанавливаем флаг видимости emoji!
|
||||||
|
|
||||||
// Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji)
|
// Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji)
|
||||||
Log.d(TAG, " ⌨️ Hiding keyboard...")
|
|
||||||
try {
|
try {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
Log.d(TAG, " ✅ hideKeyboard() completed")
|
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
isKeyboardVisible = false
|
isKeyboardVisible = false
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.Manifest
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -77,7 +76,6 @@ class MainActivity : FragmentActivity() {
|
|||||||
if (_fcmLogs.size > 20) {
|
if (_fcmLogs.size > 20) {
|
||||||
_fcmLogs.removeAt(_fcmLogs.size - 1)
|
_fcmLogs.removeAt(_fcmLogs.size - 1)
|
||||||
}
|
}
|
||||||
Log.d(TAG, "FCM: $message")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFcmLogs() {
|
fun clearFcmLogs() {
|
||||||
@@ -491,7 +489,6 @@ fun MainScreen(
|
|||||||
val encryptedAccount = accountManager.getAccount(accountPublicKey)
|
val encryptedAccount = accountManager.getAccount(accountPublicKey)
|
||||||
accountUsername = encryptedAccount?.username ?: ""
|
accountUsername = encryptedAccount?.username ?: ""
|
||||||
accountName = encryptedAccount?.name ?: accountName
|
accountName = encryptedAccount?.name ?: accountName
|
||||||
Log.d("MainActivity", "Loaded from DB: name=$accountName, username=$accountUsername")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,7 +627,6 @@ fun MainScreen(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MainActivity", "Error verifying password", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -658,7 +654,6 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
onDeleteAccount = {
|
onDeleteAccount = {
|
||||||
// TODO: Implement account deletion
|
// TODO: Implement account deletion
|
||||||
Log.d("MainActivity", "Delete account requested")
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -769,7 +764,6 @@ fun MainScreen(
|
|||||||
mainScreenScope.launch {
|
mainScreenScope.launch {
|
||||||
onAccountInfoUpdated()
|
onAccountInfoUpdated()
|
||||||
}
|
}
|
||||||
Log.d("MainActivity", "Profile saved: name=$name, username=$username, UI updated")
|
|
||||||
},
|
},
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
onNavigateToTheme = {
|
onNavigateToTheme = {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger
|
package com.rosetta.messenger
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
|
||||||
import com.rosetta.messenger.utils.CrashReportManager
|
import com.rosetta.messenger.utils.CrashReportManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,12 +15,10 @@ class RosettaApplication : Application() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
Log.d(TAG, "Application starting...")
|
|
||||||
|
|
||||||
// Инициализируем crash reporter
|
// Инициализируем crash reporter
|
||||||
initCrashReporting()
|
initCrashReporting()
|
||||||
|
|
||||||
Log.d(TAG, "Application initialized successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,9 +27,7 @@ class RosettaApplication : Application() {
|
|||||||
private fun initCrashReporting() {
|
private fun initCrashReporting() {
|
||||||
try {
|
try {
|
||||||
CrashReportManager.init(this)
|
CrashReportManager.init(this)
|
||||||
Log.d(TAG, "Crash reporting initialized")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initialize crash reporting", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,16 +437,13 @@ object MessageCrypto {
|
|||||||
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
|
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
|
||||||
|
|
||||||
val decryptedUtf8Bytes = cipher.doFinal(encryptedKey)
|
val decryptedUtf8Bytes = cipher.doFinal(encryptedKey)
|
||||||
android.util.Log.d("MessageCrypto", "🔓 AES decrypted raw bytes: ${decryptedUtf8Bytes.size}, hex=${decryptedUtf8Bytes.toHex()}")
|
|
||||||
|
|
||||||
// ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1!
|
// ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1!
|
||||||
// Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary')
|
// Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary')
|
||||||
// Это декодирует UTF-8 в строку, потом берёт charCode каждого символа
|
// Это декодирует UTF-8 в строку, потом берёт charCode каждого символа
|
||||||
val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8)
|
val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8)
|
||||||
android.util.Log.d("MessageCrypto", "🔓 UTF-8 string length: ${utf8String.length}, chars: ${utf8String.take(20).map { it.code }}")
|
|
||||||
|
|
||||||
val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1)
|
val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1)
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Latin1 bytes: ${originalBytes.size}, hex=${originalBytes.toHex().take(80)}...")
|
|
||||||
|
|
||||||
return originalBytes
|
return originalBytes
|
||||||
}
|
}
|
||||||
@@ -527,8 +524,6 @@ object MessageCrypto {
|
|||||||
chachaKeyPlain: ByteArray
|
chachaKeyPlain: ByteArray
|
||||||
): String? {
|
): String? {
|
||||||
return try {
|
return try {
|
||||||
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPlainKey(bytes): data length=${encryptedData.length}, key=${chachaKeyPlain.size} bytes")
|
|
||||||
android.util.Log.d("MessageCrypto", "🔑 Raw key bytes hex: ${chachaKeyPlain.toHex()}")
|
|
||||||
|
|
||||||
// Desktop использует key.toString('binary') → encrypt → decrypt → toString('utf-8') → PBKDF2
|
// Desktop использует key.toString('binary') → encrypt → decrypt → toString('utf-8') → PBKDF2
|
||||||
// Это эквивалентно: raw bytes → Latin1 string → UTF-8 encode → шифрование →
|
// Это эквивалентно: raw bytes → Latin1 string → UTF-8 encode → шифрование →
|
||||||
@@ -545,91 +540,60 @@ object MessageCrypto {
|
|||||||
// Вариант 1: UTF-8 decode (как Node.js Buffer.toString('utf-8'))
|
// Вариант 1: UTF-8 decode (как Node.js Buffer.toString('utf-8'))
|
||||||
val password1 = bytesToJsUtf8String(chachaKeyPlain)
|
val password1 = bytesToJsUtf8String(chachaKeyPlain)
|
||||||
val passwordBytes1 = password1.toByteArray(Charsets.UTF_8)
|
val passwordBytes1 = password1.toByteArray(Charsets.UTF_8)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V1 (UTF-8 decode → string → UTF-8 encode): ${passwordBytes1.size} bytes")
|
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V1 hex: ${passwordBytes1.toHex().take(60)}...")
|
|
||||||
|
|
||||||
// Вариант 2: Latin1 decode (каждый byte = char 0-255)
|
// Вариант 2: Latin1 decode (каждый byte = char 0-255)
|
||||||
val password2 = String(chachaKeyPlain, Charsets.ISO_8859_1)
|
val password2 = String(chachaKeyPlain, Charsets.ISO_8859_1)
|
||||||
val passwordBytes2 = password2.toByteArray(Charsets.UTF_8)
|
val passwordBytes2 = password2.toByteArray(Charsets.UTF_8)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V2 (Latin1 → string → UTF-8 encode): ${passwordBytes2.size} bytes")
|
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V2 hex: ${passwordBytes2.toHex().take(60)}...")
|
|
||||||
|
|
||||||
// Вариант 3: Raw bytes напрямую (без string conversion)
|
// Вариант 3: Raw bytes напрямую (без string conversion)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V3 (raw bytes): ${chachaKeyPlain.size} bytes")
|
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V3 hex: ${chachaKeyPlain.toHex().take(60)}...")
|
|
||||||
|
|
||||||
// Пробуем расшифровать с КАЖДЫМ вариантом
|
// Пробуем расшифровать с КАЖДЫМ вариантом
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Trying V1 (UTF-8 roundtrip)...")
|
|
||||||
val pbkdf2Key1 = generatePBKDF2Key(password1)
|
val pbkdf2Key1 = generatePBKDF2Key(password1)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V1 PBKDF2 key: ${pbkdf2Key1.toHex()}")
|
|
||||||
val result1 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key1)
|
val result1 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key1)
|
||||||
if (result1 != null) {
|
if (result1 != null) {
|
||||||
android.util.Log.d("MessageCrypto", "✅ V1 SUCCESS!")
|
|
||||||
return result1
|
return result1
|
||||||
}
|
}
|
||||||
android.util.Log.d("MessageCrypto", "❌ V1 failed")
|
|
||||||
|
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Trying V2 (Latin1 → UTF-8)...")
|
|
||||||
val pbkdf2Key2 = generatePBKDF2Key(password2)
|
val pbkdf2Key2 = generatePBKDF2Key(password2)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V2 PBKDF2 key: ${pbkdf2Key2.toHex()}")
|
|
||||||
val result2 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key2)
|
val result2 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key2)
|
||||||
if (result2 != null) {
|
if (result2 != null) {
|
||||||
android.util.Log.d("MessageCrypto", "✅ V2 SUCCESS!")
|
|
||||||
return result2
|
return result2
|
||||||
}
|
}
|
||||||
android.util.Log.d("MessageCrypto", "❌ V2 failed")
|
|
||||||
|
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Trying V3 (raw bytes for PBKDF2)...")
|
|
||||||
val pbkdf2Key3 = generatePBKDF2KeyFromBytes(chachaKeyPlain)
|
val pbkdf2Key3 = generatePBKDF2KeyFromBytes(chachaKeyPlain)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V3 PBKDF2 key: ${pbkdf2Key3.toHex()}")
|
|
||||||
val result3 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key3)
|
val result3 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key3)
|
||||||
if (result3 != null) {
|
if (result3 != null) {
|
||||||
android.util.Log.d("MessageCrypto", "✅ V3 SUCCESS!")
|
|
||||||
return result3
|
return result3
|
||||||
}
|
}
|
||||||
android.util.Log.d("MessageCrypto", "❌ V3 failed")
|
|
||||||
|
|
||||||
// V4: Стандартный Java PBKDF2 (PBEKeySpec с char[]) - для совместимости с Android encryptReplyBlob
|
// V4: Стандартный Java PBKDF2 (PBEKeySpec с char[]) - для совместимости с Android encryptReplyBlob
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Trying V4 (Java SecretKeyFactory with Latin1 password)...")
|
|
||||||
val pbkdf2Key4 = generatePBKDF2KeyJava(password2)
|
val pbkdf2Key4 = generatePBKDF2KeyJava(password2)
|
||||||
val result4 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key4)
|
val result4 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key4)
|
||||||
if (result4 != null) {
|
if (result4 != null) {
|
||||||
android.util.Log.d("MessageCrypto", "✅ V4 SUCCESS!")
|
|
||||||
return result4
|
return result4
|
||||||
}
|
}
|
||||||
android.util.Log.d("MessageCrypto", "❌ V4 failed")
|
|
||||||
|
|
||||||
// V5: Стандартный Java PBKDF2 с UTF-8 password
|
// V5: Стандартный Java PBKDF2 с UTF-8 password
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Trying V5 (Java SecretKeyFactory with UTF-8 password)...")
|
|
||||||
val pbkdf2Key5 = generatePBKDF2KeyJava(password1)
|
val pbkdf2Key5 = generatePBKDF2KeyJava(password1)
|
||||||
val result5 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key5)
|
val result5 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key5)
|
||||||
if (result5 != null) {
|
if (result5 != null) {
|
||||||
android.util.Log.d("MessageCrypto", "✅ V5 SUCCESS!")
|
|
||||||
return result5
|
return result5
|
||||||
}
|
}
|
||||||
android.util.Log.d("MessageCrypto", "❌ V5 failed")
|
|
||||||
|
|
||||||
// V6: Java CharsetDecoder with REPLACE для UTF-8 (может отличаться от bytesToJsUtf8String)
|
// V6: Java CharsetDecoder with REPLACE для UTF-8 (может отличаться от bytesToJsUtf8String)
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Trying V6 (Java CharsetDecoder REPLACE)...")
|
|
||||||
val decoder = java.nio.charset.StandardCharsets.UTF_8.newDecoder()
|
val decoder = java.nio.charset.StandardCharsets.UTF_8.newDecoder()
|
||||||
decoder.onMalformedInput(java.nio.charset.CodingErrorAction.REPLACE)
|
decoder.onMalformedInput(java.nio.charset.CodingErrorAction.REPLACE)
|
||||||
decoder.onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPLACE)
|
decoder.onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPLACE)
|
||||||
val password6 = decoder.decode(java.nio.ByteBuffer.wrap(chachaKeyPlain)).toString()
|
val password6 = decoder.decode(java.nio.ByteBuffer.wrap(chachaKeyPlain)).toString()
|
||||||
val passwordBytes6 = password6.toByteArray(Charsets.UTF_8)
|
val passwordBytes6 = password6.toByteArray(Charsets.UTF_8)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V6 password bytes: ${passwordBytes6.size}, hex: ${passwordBytes6.toHex().take(60)}...")
|
|
||||||
val pbkdf2Key6 = generatePBKDF2Key(password6)
|
val pbkdf2Key6 = generatePBKDF2Key(password6)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 V6 PBKDF2 key: ${pbkdf2Key6.toHex()}")
|
|
||||||
val result6 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key6)
|
val result6 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key6)
|
||||||
if (result6 != null) {
|
if (result6 != null) {
|
||||||
android.util.Log.d("MessageCrypto", "✅ V6 SUCCESS!")
|
|
||||||
return result6
|
return result6
|
||||||
}
|
}
|
||||||
android.util.Log.d("MessageCrypto", "❌ V6 failed")
|
|
||||||
|
|
||||||
android.util.Log.d("MessageCrypto", "❌ All variants failed!")
|
|
||||||
null
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPlainKey failed: ${e.message}", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -650,25 +614,20 @@ object MessageCrypto {
|
|||||||
passwordLatin1: String
|
passwordLatin1: String
|
||||||
): String? {
|
): String? {
|
||||||
return try {
|
return try {
|
||||||
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPassword: data length=${encryptedData.length}, password=${passwordLatin1.length} chars")
|
|
||||||
|
|
||||||
// Конвертируем Latin1 string → bytes → UTF-8 string (эмулируем Desktop)
|
// Конвертируем Latin1 string → bytes → UTF-8 string (эмулируем Desktop)
|
||||||
// Desktop: Buffer.from(str, 'binary').toString('utf-8')
|
// Desktop: Buffer.from(str, 'binary').toString('utf-8')
|
||||||
val passwordBytes = passwordLatin1.toByteArray(Charsets.ISO_8859_1)
|
val passwordBytes = passwordLatin1.toByteArray(Charsets.ISO_8859_1)
|
||||||
val passwordUtf8 = bytesToJsUtf8String(passwordBytes)
|
val passwordUtf8 = bytesToJsUtf8String(passwordBytes)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 Password UTF-8: ${passwordUtf8.length} chars")
|
|
||||||
|
|
||||||
// Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations, sha1)
|
// Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations, sha1)
|
||||||
// Crypto-js конвертирует passwordUtf8 string в UTF-8 bytes для PBKDF2
|
// Crypto-js конвертирует passwordUtf8 string в UTF-8 bytes для PBKDF2
|
||||||
val pbkdf2Key = generatePBKDF2Key(passwordUtf8)
|
val pbkdf2Key = generatePBKDF2Key(passwordUtf8)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 PBKDF2 key: ${pbkdf2Key.toHex()}")
|
|
||||||
|
|
||||||
// Расшифровываем AES-256-CBC + zlib decompress
|
// Расшифровываем AES-256-CBC + zlib decompress
|
||||||
val result = decryptWithPBKDF2Key(encryptedData, pbkdf2Key)
|
val result = decryptWithPBKDF2Key(encryptedData, pbkdf2Key)
|
||||||
android.util.Log.d("MessageCrypto", "✅ Decryption: ${if (result != null) "success (${result.length} chars)" else "FAILED"}")
|
|
||||||
result
|
result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPassword failed: ${e.message}", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,16 +643,13 @@ object MessageCrypto {
|
|||||||
myPrivateKey: String
|
myPrivateKey: String
|
||||||
): String? {
|
): String? {
|
||||||
return try {
|
return try {
|
||||||
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlob: data length=${encryptedData.length}, key length=${encryptedKey.length}")
|
|
||||||
|
|
||||||
// 1. Расшифровываем ChaCha ключ+nonce (56 bytes) через ECDH
|
// 1. Расшифровываем ChaCha ключ+nonce (56 bytes) через ECDH
|
||||||
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 Decrypted keyAndNonce: ${keyAndNonce.size} bytes, hex=${keyAndNonce.toHex().take(40)}...")
|
|
||||||
|
|
||||||
// 2. Используем ВСЕ 56 байт как password для PBKDF2
|
// 2. Используем ВСЕ 56 байт как password для PBKDF2
|
||||||
decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)
|
decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlob failed: ${e.message}", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -791,20 +747,14 @@ object MessageCrypto {
|
|||||||
*/
|
*/
|
||||||
private fun decryptWithPBKDF2Key(encryptedData: String, pbkdf2Key: ByteArray): String? {
|
private fun decryptWithPBKDF2Key(encryptedData: String, pbkdf2Key: ByteArray): String? {
|
||||||
return try {
|
return try {
|
||||||
android.util.Log.d("MessageCrypto", "🔓 decryptWithPBKDF2Key: data length=${encryptedData.length}")
|
|
||||||
android.util.Log.d("MessageCrypto", "🔓 First 100 chars: ${encryptedData.take(100)}")
|
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Contains colon: ${encryptedData.contains(":")}")
|
|
||||||
|
|
||||||
val parts = encryptedData.split(":")
|
val parts = encryptedData.split(":")
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Split parts: ${parts.size}")
|
|
||||||
if (parts.size != 2) {
|
if (parts.size != 2) {
|
||||||
android.util.Log.e("MessageCrypto", "❌ Invalid format: expected 2 parts, got ${parts.size}")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val iv = android.util.Base64.decode(parts[0], android.util.Base64.DEFAULT)
|
val iv = android.util.Base64.decode(parts[0], android.util.Base64.DEFAULT)
|
||||||
val ciphertext = android.util.Base64.decode(parts[1], android.util.Base64.DEFAULT)
|
val ciphertext = android.util.Base64.decode(parts[1], android.util.Base64.DEFAULT)
|
||||||
android.util.Log.d("MessageCrypto", "🔓 IV: ${iv.size} bytes, Ciphertext: ${ciphertext.size} bytes")
|
|
||||||
|
|
||||||
// AES-256-CBC расшифровка
|
// AES-256-CBC расшифровка
|
||||||
val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
|
val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
@@ -812,7 +762,6 @@ object MessageCrypto {
|
|||||||
val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
|
val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
|
||||||
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
||||||
val decrypted = cipher.doFinal(ciphertext)
|
val decrypted = cipher.doFinal(ciphertext)
|
||||||
android.util.Log.d("MessageCrypto", "🔓 AES decrypted: ${decrypted.size} bytes")
|
|
||||||
|
|
||||||
// Zlib декомпрессия
|
// Zlib декомпрессия
|
||||||
val inflater = java.util.zip.Inflater()
|
val inflater = java.util.zip.Inflater()
|
||||||
@@ -826,10 +775,8 @@ object MessageCrypto {
|
|||||||
inflater.end()
|
inflater.end()
|
||||||
|
|
||||||
val result = String(outputStream.toByteArray(), Charsets.UTF_8)
|
val result = String(outputStream.toByteArray(), Charsets.UTF_8)
|
||||||
android.util.Log.d("MessageCrypto", "🔓 Decompressed: ${result.length} chars")
|
|
||||||
result
|
result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("MessageCrypto", "❌ decryptWithPBKDF2Key failed: ${e.message}", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger.data
|
package com.rosetta.messenger.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import com.rosetta.messenger.crypto.MessageCrypto
|
import com.rosetta.messenger.crypto.MessageCrypto
|
||||||
import com.rosetta.messenger.database.*
|
import com.rosetta.messenger.database.*
|
||||||
@@ -204,7 +203,6 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
|
|
||||||
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
|
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
|
||||||
// Desktop хранит зашифрованный ключ, расшифровывает только при использовании
|
// Desktop хранит зашифрованный ключ, расшифровывает только при использовании
|
||||||
android.util.Log.d("MessageRepository", "🔑 Outgoing chacha_key (encrypted): ${encryptedKey.length} chars")
|
|
||||||
|
|
||||||
// Сериализуем attachments в JSON
|
// Сериализуем attachments в JSON
|
||||||
val attachmentsJson = serializeAttachments(attachments)
|
val attachmentsJson = serializeAttachments(attachments)
|
||||||
@@ -235,16 +233,13 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
)
|
)
|
||||||
messageDao.insertMessage(entity)
|
messageDao.insertMessage(entity)
|
||||||
|
|
||||||
Log.d("MessageRepository", "<EFBFBD> Inserted OUTGOING message: fromMe=1, delivered=${entity.delivered}, read=0")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 КРИТИЧНО: Обновляем диалог через updateDialogFromMessages
|
// 🔥 КРИТИЧНО: Обновляем диалог через updateDialogFromMessages
|
||||||
Log.d("MessageRepository", "🔄 Calling updateDialogFromMessages after sending...")
|
|
||||||
dialogDao.updateDialogFromMessages(account, toPublicKey)
|
dialogDao.updateDialogFromMessages(account, toPublicKey)
|
||||||
|
|
||||||
// 🔥 Логируем что записалось в диалог
|
// 🔥 Логируем что записалось в диалог
|
||||||
val dialog = dialogDao.getDialog(account, toPublicKey)
|
val dialog = dialogDao.getDialog(account, toPublicKey)
|
||||||
Log.d("MessageRepository", "🎯 DIALOG AFTER SEND: lastMsgFromMe=${dialog?.lastMessageFromMe}, delivered=${dialog?.lastMessageDelivered}, read=${dialog?.lastMessageRead}")
|
|
||||||
|
|
||||||
// 🔥 Отмечаем что я отправлял сообщения в этот диалог (перемещает из requests в chats)
|
// 🔥 Отмечаем что я отправлял сообщения в этот диалог (перемещает из requests в chats)
|
||||||
val updatedRows = dialogDao.markIHaveSent(account, toPublicKey)
|
val updatedRows = dialogDao.markIHaveSent(account, toPublicKey)
|
||||||
@@ -316,7 +311,6 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
|
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
|
||||||
// Desktop: хранит зашифрованный ключ, расшифровывает только при использовании
|
// Desktop: хранит зашифрованный ключ, расшифровывает только при использовании
|
||||||
// Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8')
|
// Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8')
|
||||||
android.util.Log.d("MessageRepository", "🔑 Incoming chacha_key (encrypted): ${packet.chachaKey.length} chars")
|
|
||||||
|
|
||||||
// Расшифровываем
|
// Расшифровываем
|
||||||
val plainText = MessageCrypto.decryptIncoming(
|
val plainText = MessageCrypto.decryptIncoming(
|
||||||
@@ -365,16 +359,13 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
// Сохраняем в БД только если сообщения нет
|
// Сохраняем в БД только если сообщения нет
|
||||||
messageDao.insertMessage(entity)
|
messageDao.insertMessage(entity)
|
||||||
|
|
||||||
Log.d("MessageRepository", "<EFBFBD> Inserted INCOMING message: fromMe=0, delivered=${entity.delivered}, read=0")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 КРИТИЧНО: Обновляем диалог через updateDialogFromMessages
|
// 🔥 КРИТИЧНО: Обновляем диалог через updateDialogFromMessages
|
||||||
Log.d("MessageRepository", "🔄 Calling updateDialogFromMessages after incoming message...")
|
|
||||||
dialogDao.updateDialogFromMessages(account, packet.fromPublicKey)
|
dialogDao.updateDialogFromMessages(account, packet.fromPublicKey)
|
||||||
|
|
||||||
// 🔥 Логируем что записалось в диалог
|
// 🔥 Логируем что записалось в диалог
|
||||||
val dialog = dialogDao.getDialog(account, packet.fromPublicKey)
|
val dialog = dialogDao.getDialog(account, packet.fromPublicKey)
|
||||||
Log.d("MessageRepository", "🎯 DIALOG AFTER INCOMING: lastMsgFromMe=${dialog?.lastMessageFromMe}, delivered=${dialog?.lastMessageDelivered}, read=${dialog?.lastMessageRead}")
|
|
||||||
|
|
||||||
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
|
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
|
||||||
requestUserInfo(packet.fromPublicKey)
|
requestUserInfo(packet.fromPublicKey)
|
||||||
@@ -416,19 +407,15 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
suspend fun handleRead(packet: PacketRead) {
|
suspend fun handleRead(packet: PacketRead) {
|
||||||
val account = currentAccount ?: return
|
val account = currentAccount ?: return
|
||||||
|
|
||||||
Log.d("MessageRepository", "🔥🔥🔥 handleRead START: fromPublicKey=${packet.fromPublicKey.take(16)}...")
|
|
||||||
|
|
||||||
// Проверяем последнее сообщение ДО обновления
|
// Проверяем последнее сообщение ДО обновления
|
||||||
val lastMsgBefore = messageDao.getLastMessageDebug(account, packet.fromPublicKey)
|
val lastMsgBefore = messageDao.getLastMessageDebug(account, packet.fromPublicKey)
|
||||||
Log.d("MessageRepository", "📊 BEFORE markAllAsRead: fromMe=${lastMsgBefore?.fromMe}, delivered=${lastMsgBefore?.delivered}, read=${lastMsgBefore?.read}, timestamp=${lastMsgBefore?.timestamp}")
|
|
||||||
|
|
||||||
// Отмечаем все наши исходящие сообщения к этому собеседнику как прочитанные
|
// Отмечаем все наши исходящие сообщения к этому собеседнику как прочитанные
|
||||||
messageDao.markAllAsRead(account, packet.fromPublicKey)
|
messageDao.markAllAsRead(account, packet.fromPublicKey)
|
||||||
Log.d("MessageRepository", "✅ markAllAsRead completed")
|
|
||||||
|
|
||||||
// 🔥 DEBUG: Проверяем последнее сообщение ПОСЛЕ обновления
|
// 🔥 DEBUG: Проверяем последнее сообщение ПОСЛЕ обновления
|
||||||
val lastMsgAfter = messageDao.getLastMessageDebug(account, packet.fromPublicKey)
|
val lastMsgAfter = messageDao.getLastMessageDebug(account, packet.fromPublicKey)
|
||||||
Log.d("MessageRepository", "📊 AFTER markAllAsRead: fromMe=${lastMsgAfter?.fromMe}, delivered=${lastMsgAfter?.delivered}, read=${lastMsgAfter?.read}, timestamp=${lastMsgAfter?.timestamp}")
|
|
||||||
|
|
||||||
// Обновляем кэш - все исходящие сообщения помечаем как прочитанные
|
// Обновляем кэш - все исходящие сообщения помечаем как прочитанные
|
||||||
val dialogKey = getDialogKey(packet.fromPublicKey)
|
val dialogKey = getDialogKey(packet.fromPublicKey)
|
||||||
@@ -440,13 +427,10 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 КРИТИЧНО: Обновляем диалог чтобы lastMessageRead обновился
|
// 🔥 КРИТИЧНО: Обновляем диалог чтобы lastMessageRead обновился
|
||||||
Log.d("MessageRepository", "🔄 Calling updateDialogFromMessages...")
|
|
||||||
dialogDao.updateDialogFromMessages(account, packet.fromPublicKey)
|
dialogDao.updateDialogFromMessages(account, packet.fromPublicKey)
|
||||||
|
|
||||||
// Логируем что записалось в диалог
|
// Логируем что записалось в диалог
|
||||||
val dialog = dialogDao.getDialog(account, packet.fromPublicKey)
|
val dialog = dialogDao.getDialog(account, packet.fromPublicKey)
|
||||||
Log.d("MessageRepository", "🎯 DIALOG AFTER UPDATE: lastMsgFromMe=${dialog?.lastMessageFromMe}, delivered=${dialog?.lastMessageDelivered}, read=${dialog?.lastMessageRead}")
|
|
||||||
Log.d("MessageRepository", "🔥🔥🔥 handleRead END")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -640,19 +624,15 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
suspend fun updateDialogUserInfo(publicKey: String, title: String, username: String, verified: Int) {
|
suspend fun updateDialogUserInfo(publicKey: String, title: String, username: String, verified: Int) {
|
||||||
val account = currentAccount ?: return
|
val account = currentAccount ?: return
|
||||||
|
|
||||||
android.util.Log.d("MessageRepo", "🔄 updateDialogUserInfo: ${publicKey.take(16)}... title='$title' username='$username'")
|
|
||||||
|
|
||||||
// Проверяем существует ли диалог с этим пользователем
|
// Проверяем существует ли диалог с этим пользователем
|
||||||
val existing = dialogDao.getDialog(account, publicKey)
|
val existing = dialogDao.getDialog(account, publicKey)
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
android.util.Log.d("MessageRepo", "✅ Updating dialog opponent info in DB for ${publicKey.take(16)}...")
|
|
||||||
dialogDao.updateOpponentInfo(account, publicKey, title, username, verified)
|
dialogDao.updateOpponentInfo(account, publicKey, title, username, verified)
|
||||||
|
|
||||||
// 🔥 Проверим что данные сохранились
|
// 🔥 Проверим что данные сохранились
|
||||||
val updated = dialogDao.getDialog(account, publicKey)
|
val updated = dialogDao.getDialog(account, publicKey)
|
||||||
android.util.Log.d("MessageRepo", "📝 After update: title='${updated?.opponentTitle}' username='${updated?.opponentUsername}'")
|
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.w("MessageRepo", "⚠️ Dialog not found for ${publicKey.take(16)}...")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,14 +749,11 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
encryptedKey: String,
|
encryptedKey: String,
|
||||||
privateKey: String
|
privateKey: String
|
||||||
) {
|
) {
|
||||||
Log.d("MessageRepository", "📸 processAvatarAttachments: ${attachments.size} attachments from ${fromPublicKey.take(16)}")
|
|
||||||
|
|
||||||
for (attachment in attachments) {
|
for (attachment in attachments) {
|
||||||
Log.d("MessageRepository", "📸 Attachment type=${attachment.type} (AVATAR=${AttachmentType.AVATAR}), blob size=${attachment.blob.length}")
|
|
||||||
|
|
||||||
if (attachment.type == AttachmentType.AVATAR && attachment.blob.isNotEmpty()) {
|
if (attachment.type == AttachmentType.AVATAR && attachment.blob.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
Log.d("MessageRepository", "📸 Found AVATAR attachment! Decrypting...")
|
|
||||||
|
|
||||||
// 1. Расшифровываем blob с ChaCha ключом сообщения
|
// 1. Расшифровываем blob с ChaCha ключом сообщения
|
||||||
val decryptedBlob = MessageCrypto.decryptAttachmentBlob(
|
val decryptedBlob = MessageCrypto.decryptAttachmentBlob(
|
||||||
@@ -785,12 +762,10 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
privateKey
|
privateKey
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d("MessageRepository", "📸 Decrypted blob: ${decryptedBlob?.take(50) ?: "NULL"}")
|
|
||||||
|
|
||||||
if (decryptedBlob != null) {
|
if (decryptedBlob != null) {
|
||||||
// 2. Сохраняем аватар в кэш
|
// 2. Сохраняем аватар в кэш
|
||||||
val filePath = AvatarFileManager.saveAvatar(context, decryptedBlob, fromPublicKey)
|
val filePath = AvatarFileManager.saveAvatar(context, decryptedBlob, fromPublicKey)
|
||||||
Log.d("MessageRepository", "📸 Avatar saved to: $filePath")
|
|
||||||
|
|
||||||
val entity = AvatarCacheEntity(
|
val entity = AvatarCacheEntity(
|
||||||
publicKey = fromPublicKey,
|
publicKey = fromPublicKey,
|
||||||
@@ -798,17 +773,13 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
avatarDao.insertAvatar(entity)
|
avatarDao.insertAvatar(entity)
|
||||||
Log.d("MessageRepository", "📸 Avatar inserted to DB for ${fromPublicKey.take(16)}")
|
|
||||||
|
|
||||||
// 3. Очищаем старые аватары (оставляем последние 5)
|
// 3. Очищаем старые аватары (оставляем последние 5)
|
||||||
avatarDao.deleteOldAvatars(fromPublicKey, 5)
|
avatarDao.deleteOldAvatars(fromPublicKey, 5)
|
||||||
|
|
||||||
Log.d("MessageRepository", "📸 ✅ Successfully saved avatar for $fromPublicKey")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w("MessageRepository", "📸 ⚠️ Decryption returned null!")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MessageRepository", "📸 ❌ Failed to process avatar attachment", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -830,7 +801,6 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
// Сохраняем только IMAGE, не FILE (файлы загружаются с CDN при необходимости)
|
// Сохраняем только IMAGE, не FILE (файлы загружаются с CDN при необходимости)
|
||||||
if (attachment.type == AttachmentType.IMAGE && attachment.blob.isNotEmpty()) {
|
if (attachment.type == AttachmentType.IMAGE && attachment.blob.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
Log.d("MessageRepository", "🖼️ Processing IMAGE attachment: ${attachment.id}")
|
|
||||||
|
|
||||||
// 1. Расшифровываем blob с ChaCha ключом сообщения
|
// 1. Расшифровываем blob с ChaCha ключом сообщения
|
||||||
val decryptedBlob = MessageCrypto.decryptAttachmentBlob(
|
val decryptedBlob = MessageCrypto.decryptAttachmentBlob(
|
||||||
@@ -850,15 +820,11 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (saved) {
|
if (saved) {
|
||||||
Log.d("MessageRepository", "🖼️ ✅ Image saved to file: ${attachment.id}")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w("MessageRepository", "🖼️ ⚠️ Failed to save image to file")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w("MessageRepository", "🖼️ ⚠️ Decryption returned null for image")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("MessageRepository", "🖼️ ❌ Failed to process image attachment", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class Protocol(
|
|||||||
|
|
||||||
private fun log(message: String) {
|
private fun log(message: String) {
|
||||||
// TEMPORARY: Enable logging for debugging PacketUserInfo
|
// TEMPORARY: Enable logging for debugging PacketUserInfo
|
||||||
android.util.Log.d(TAG, message)
|
|
||||||
logger(message)
|
logger(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,13 +157,11 @@ object ProtocolManager {
|
|||||||
// Обновляет информацию о пользователе в диалогах когда приходит ответ от сервера
|
// Обновляет информацию о пользователе в диалогах когда приходит ответ от сервера
|
||||||
waitPacket(0x03) { packet ->
|
waitPacket(0x03) { packet ->
|
||||||
val searchPacket = packet as PacketSearch
|
val searchPacket = packet as PacketSearch
|
||||||
android.util.Log.d("Protocol", "🔍 Search/UserInfo response: ${searchPacket.users.size} users")
|
|
||||||
|
|
||||||
// Обновляем информацию о пользователях в диалогах
|
// Обновляем информацию о пользователях в диалогах
|
||||||
if (searchPacket.users.isNotEmpty()) {
|
if (searchPacket.users.isNotEmpty()) {
|
||||||
scope.launch(Dispatchers.IO) { // 🔥 Запускаем на IO потоке для работы с БД
|
scope.launch(Dispatchers.IO) { // 🔥 Запускаем на IO потоке для работы с БД
|
||||||
searchPacket.users.forEach { user ->
|
searchPacket.users.forEach { user ->
|
||||||
android.util.Log.d("Protocol", "📝 Updating user info: ${user.publicKey.take(16)}... title='${user.title}' username='${user.username}'")
|
|
||||||
messageRepository?.updateDialogUserInfo(
|
messageRepository?.updateDialogUserInfo(
|
||||||
user.publicKey,
|
user.publicKey,
|
||||||
user.title,
|
user.title,
|
||||||
@@ -178,7 +176,6 @@ object ProtocolManager {
|
|||||||
// 🚀 Обработчик транспортного сервера (0x0F)
|
// 🚀 Обработчик транспортного сервера (0x0F)
|
||||||
waitPacket(0x0F) { packet ->
|
waitPacket(0x0F) { packet ->
|
||||||
val transportPacket = packet as PacketRequestTransport
|
val transportPacket = packet as PacketRequestTransport
|
||||||
android.util.Log.d("Protocol", "🚀 Transport server: ${transportPacket.transportServer}")
|
|
||||||
TransportManager.setTransportServer(transportPacket.transportServer)
|
TransportManager.setTransportServer(transportPacket.transportServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.rosetta.messenger.network
|
package com.rosetta.messenger.network
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -53,7 +52,6 @@ object TransportManager {
|
|||||||
*/
|
*/
|
||||||
fun setTransportServer(server: String) {
|
fun setTransportServer(server: String) {
|
||||||
transportServer = server
|
transportServer = server
|
||||||
Log.d(TAG, "🚀 Transport server set: $server")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +64,6 @@ object TransportManager {
|
|||||||
*/
|
*/
|
||||||
private fun getActiveServer(): String {
|
private fun getActiveServer(): String {
|
||||||
val server = transportServer ?: FALLBACK_TRANSPORT_SERVER
|
val server = transportServer ?: FALLBACK_TRANSPORT_SERVER
|
||||||
Log.d(TAG, "📡 Using transport server: $server (configured: ${transportServer != null})")
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +84,6 @@ object TransportManager {
|
|||||||
suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
|
suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
|
||||||
val server = getActiveServer()
|
val server = getActiveServer()
|
||||||
|
|
||||||
Log.d(TAG, "📤 Uploading file: $id to $server")
|
|
||||||
Log.d(TAG, "📤 Content length: ${content.length}, first 50 chars: ${content.take(50)}")
|
|
||||||
|
|
||||||
// Добавляем в список загрузок
|
// Добавляем в список загрузок
|
||||||
_uploading.value = _uploading.value + TransportState(id, 0)
|
_uploading.value = _uploading.value + TransportState(id, 0)
|
||||||
@@ -96,7 +91,6 @@ object TransportManager {
|
|||||||
try {
|
try {
|
||||||
// 🔥 КРИТИЧНО: Преобразуем строку в байты (как desktop делает new Blob([content]))
|
// 🔥 КРИТИЧНО: Преобразуем строку в байты (как desktop делает new Blob([content]))
|
||||||
val contentBytes = content.toByteArray(Charsets.UTF_8)
|
val contentBytes = content.toByteArray(Charsets.UTF_8)
|
||||||
Log.d(TAG, "📤 Content bytes length: ${contentBytes.size}")
|
|
||||||
|
|
||||||
val requestBody = MultipartBody.Builder()
|
val requestBody = MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
@@ -115,8 +109,6 @@ object TransportManager {
|
|||||||
val response = suspendCoroutine<Response> { cont ->
|
val response = suspendCoroutine<Response> { cont ->
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
Log.e(TAG, "❌ Upload failed for $id: ${e.message}")
|
|
||||||
Log.e(TAG, "❌ Stack trace: ${e.stackTraceToString()}")
|
|
||||||
cont.resumeWithException(e)
|
cont.resumeWithException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,19 +120,16 @@ object TransportManager {
|
|||||||
|
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorBody = response.body?.string() ?: "No error body"
|
val errorBody = response.body?.string() ?: "No error body"
|
||||||
Log.e(TAG, "❌ Upload failed: ${response.code}, body: $errorBody")
|
|
||||||
throw IOException("Upload failed: ${response.code}")
|
throw IOException("Upload failed: ${response.code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val responseBody = response.body?.string()
|
val responseBody = response.body?.string()
|
||||||
?: throw IOException("Empty response")
|
?: throw IOException("Empty response")
|
||||||
|
|
||||||
Log.d(TAG, "📤 Response body: $responseBody")
|
|
||||||
|
|
||||||
// Parse JSON response to get tag
|
// Parse JSON response to get tag
|
||||||
val tag = org.json.JSONObject(responseBody).getString("t")
|
val tag = org.json.JSONObject(responseBody).getString("t")
|
||||||
|
|
||||||
Log.d(TAG, "✅ Upload complete: $id -> $tag")
|
|
||||||
|
|
||||||
// Обновляем прогресс до 100%
|
// Обновляем прогресс до 100%
|
||||||
_uploading.value = _uploading.value.map {
|
_uploading.value = _uploading.value.map {
|
||||||
@@ -163,7 +152,6 @@ object TransportManager {
|
|||||||
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
|
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
|
||||||
val server = getActiveServer()
|
val server = getActiveServer()
|
||||||
|
|
||||||
Log.d(TAG, "📥 Downloading file: $id (tag: $tag) from $server")
|
|
||||||
|
|
||||||
// Добавляем в список скачиваний
|
// Добавляем в список скачиваний
|
||||||
_downloading.value = _downloading.value + TransportState(id, 0)
|
_downloading.value = _downloading.value + TransportState(id, 0)
|
||||||
@@ -193,7 +181,6 @@ object TransportManager {
|
|||||||
val content = response.body?.string()
|
val content = response.body?.string()
|
||||||
?: throw IOException("Empty response")
|
?: throw IOException("Empty response")
|
||||||
|
|
||||||
Log.d(TAG, "✅ Download complete: $id (${content.length} bytes)")
|
|
||||||
|
|
||||||
// Обновляем прогресс до 100%
|
// Обновляем прогресс до 100%
|
||||||
_downloading.value = _downloading.value.map {
|
_downloading.value = _downloading.value.map {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger.repository
|
package com.rosetta.messenger.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import com.rosetta.messenger.database.AvatarCacheEntity
|
import com.rosetta.messenger.database.AvatarCacheEntity
|
||||||
import com.rosetta.messenger.database.AvatarDao
|
import com.rosetta.messenger.database.AvatarDao
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
@@ -57,7 +56,6 @@ class AvatarRepository(
|
|||||||
// Подписываемся на изменения в БД с использованием repository scope
|
// Подписываемся на изменения в БД с использованием repository scope
|
||||||
avatarDao.getAvatars(publicKey)
|
avatarDao.getAvatars(publicKey)
|
||||||
.onEach { entities ->
|
.onEach { entities ->
|
||||||
Log.d(TAG, "📥 DB update for $publicKey: ${entities.size} entities")
|
|
||||||
val avatars = if (allDecode) {
|
val avatars = if (allDecode) {
|
||||||
// Загружаем всю историю
|
// Загружаем всю историю
|
||||||
entities.mapNotNull { entity ->
|
entities.mapNotNull { entity ->
|
||||||
@@ -70,7 +68,6 @@ class AvatarRepository(
|
|||||||
}?.let { listOf(it) } ?: emptyList()
|
}?.let { listOf(it) } ?: emptyList()
|
||||||
}
|
}
|
||||||
flow.value = avatars
|
flow.value = avatars
|
||||||
Log.d(TAG, "✅ Flow updated for $publicKey: ${avatars.size} avatars")
|
|
||||||
}
|
}
|
||||||
.launchIn(repositoryScope)
|
.launchIn(repositoryScope)
|
||||||
|
|
||||||
@@ -97,7 +94,6 @@ class AvatarRepository(
|
|||||||
try {
|
try {
|
||||||
// Сохраняем файл
|
// Сохраняем файл
|
||||||
val filePath = AvatarFileManager.saveAvatar(context, base64Image, fromPublicKey)
|
val filePath = AvatarFileManager.saveAvatar(context, base64Image, fromPublicKey)
|
||||||
Log.d(TAG, "💾 Avatar file saved for $fromPublicKey: $filePath")
|
|
||||||
|
|
||||||
// Сохраняем в БД
|
// Сохраняем в БД
|
||||||
val entity = AvatarCacheEntity(
|
val entity = AvatarCacheEntity(
|
||||||
@@ -106,7 +102,6 @@ class AvatarRepository(
|
|||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
avatarDao.insertAvatar(entity)
|
avatarDao.insertAvatar(entity)
|
||||||
Log.d(TAG, "💾 Avatar entity inserted to DB for $fromPublicKey")
|
|
||||||
|
|
||||||
// Очищаем старые аватары (оставляем только последние N)
|
// Очищаем старые аватары (оставляем только последние N)
|
||||||
avatarDao.deleteOldAvatars(fromPublicKey, MAX_AVATAR_HISTORY)
|
avatarDao.deleteOldAvatars(fromPublicKey, MAX_AVATAR_HISTORY)
|
||||||
@@ -117,15 +112,11 @@ class AvatarRepository(
|
|||||||
val avatarInfo = loadAndDecryptAvatar(entity)
|
val avatarInfo = loadAndDecryptAvatar(entity)
|
||||||
if (avatarInfo != null) {
|
if (avatarInfo != null) {
|
||||||
cachedFlow.value = listOf(avatarInfo)
|
cachedFlow.value = listOf(avatarInfo)
|
||||||
Log.d(TAG, "✅ Memory cache updated for $fromPublicKey")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "ℹ️ No memory cache for $fromPublicKey, will be loaded on next getAvatars()")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "✅ Saved avatar for $fromPublicKey")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to save avatar for $fromPublicKey", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,13 +130,9 @@ class AvatarRepository(
|
|||||||
suspend fun changeMyAvatar(base64Image: String) {
|
suspend fun changeMyAvatar(base64Image: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
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)
|
val filePath = AvatarFileManager.saveAvatar(context, base64Image, currentPublicKey)
|
||||||
Log.d(TAG, "✅ Avatar file saved: $filePath")
|
|
||||||
|
|
||||||
// Сохраняем в БД
|
// Сохраняем в БД
|
||||||
val entity = AvatarCacheEntity(
|
val entity = AvatarCacheEntity(
|
||||||
@@ -154,14 +141,11 @@ class AvatarRepository(
|
|||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
avatarDao.insertAvatar(entity)
|
avatarDao.insertAvatar(entity)
|
||||||
Log.d(TAG, "✅ Avatar inserted to DB")
|
|
||||||
|
|
||||||
// Очищаем старые аватары
|
// Очищаем старые аватары
|
||||||
avatarDao.deleteOldAvatars(currentPublicKey, MAX_AVATAR_HISTORY)
|
avatarDao.deleteOldAvatars(currentPublicKey, MAX_AVATAR_HISTORY)
|
||||||
|
|
||||||
Log.d(TAG, "🎉 Avatar changed successfully!")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to change avatar: ${e.message}", e)
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,8 +157,6 @@ class AvatarRepository(
|
|||||||
suspend fun deleteMyAvatar() {
|
suspend fun deleteMyAvatar() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "🗑️ deleteMyAvatar called")
|
|
||||||
Log.d(TAG, "👤 Current public key: ${currentPublicKey.take(16)}...")
|
|
||||||
|
|
||||||
// Получаем все аватары пользователя
|
// Получаем все аватары пользователя
|
||||||
val avatars = avatarDao.getAvatarsByPublicKey(currentPublicKey)
|
val avatars = avatarDao.getAvatarsByPublicKey(currentPublicKey)
|
||||||
@@ -183,22 +165,17 @@ class AvatarRepository(
|
|||||||
for (avatar in avatars) {
|
for (avatar in avatars) {
|
||||||
try {
|
try {
|
||||||
AvatarFileManager.deleteAvatar(context, avatar.avatar)
|
AvatarFileManager.deleteAvatar(context, avatar.avatar)
|
||||||
Log.d(TAG, "✅ Deleted avatar file: ${avatar.avatar}")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "⚠️ Failed to delete avatar file: ${avatar.avatar}", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем из БД
|
// Удаляем из БД
|
||||||
avatarDao.deleteAllAvatars(currentPublicKey)
|
avatarDao.deleteAllAvatars(currentPublicKey)
|
||||||
Log.d(TAG, "✅ Avatars deleted from DB")
|
|
||||||
|
|
||||||
// Очищаем memory cache
|
// Очищаем memory cache
|
||||||
memoryCache.remove(currentPublicKey)
|
memoryCache.remove(currentPublicKey)
|
||||||
|
|
||||||
Log.d(TAG, "🎉 Avatar deleted successfully!")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to delete avatar: ${e.message}", e)
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,11 +194,9 @@ class AvatarRepository(
|
|||||||
timestamp = entity.timestamp
|
timestamp = entity.timestamp
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to read avatar file: ${entity.avatar}")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load avatar", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ private suspend fun performUnlock(
|
|||||||
name = account.name
|
name = account.name
|
||||||
)
|
)
|
||||||
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Starting connection and authentication...")
|
|
||||||
|
|
||||||
// Connect to server and authenticate
|
// Connect to server and authenticate
|
||||||
ProtocolManager.connect()
|
ProtocolManager.connect()
|
||||||
@@ -130,12 +129,10 @@ private suspend fun performUnlock(
|
|||||||
|
|
||||||
kotlinx.coroutines.delay(300)
|
kotlinx.coroutines.delay(300)
|
||||||
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Starting authentication...")
|
|
||||||
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
|
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
|
||||||
|
|
||||||
accountManager.setCurrentAccount(account.publicKey)
|
accountManager.setCurrentAccount(account.publicKey)
|
||||||
|
|
||||||
android.util.Log.d("UnlockScreen", "✅ Unlock complete")
|
|
||||||
onSuccess(decryptedAccount)
|
onSuccess(decryptedAccount)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onError("Failed to unlock: ${e.message}")
|
onError("Failed to unlock: ${e.message}")
|
||||||
@@ -181,7 +178,6 @@ fun UnlockScreen(
|
|||||||
val biometricPrefs = remember { BiometricPreferences(context) }
|
val biometricPrefs = remember { BiometricPreferences(context) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
android.util.Log.d("UnlockScreen", "🎬 SCREEN INITIALIZED - Activity: $activity")
|
|
||||||
|
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
@@ -203,7 +199,6 @@ fun UnlockScreen(
|
|||||||
|
|
||||||
// Load accounts and pre-select last used account
|
// Load accounts and pre-select last used account
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
android.util.Log.d("UnlockScreen", "📦 LOADING DATA...")
|
|
||||||
// ⚡ СИНХРОННОЕ чтение последнего аккаунта ДО загрузки списка
|
// ⚡ СИНХРОННОЕ чтение последнего аккаунта ДО загрузки списка
|
||||||
val lastLoggedKey = accountManager.getLastLoggedPublicKey()
|
val lastLoggedKey = accountManager.getLastLoggedPublicKey()
|
||||||
|
|
||||||
@@ -243,38 +238,27 @@ fun UnlockScreen(
|
|||||||
}
|
}
|
||||||
savedPasswordsMap = passwords
|
savedPasswordsMap = passwords
|
||||||
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Biometric available: $biometricAvailable")
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Biometric enabled: $isBiometricEnabled")
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Activity: $activity")
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Selected account: ${selectedAccount?.publicKey}")
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Saved passwords count: ${passwords.size}")
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Has password for selected: ${selectedAccount?.let { passwords.containsKey(it.publicKey) }}")
|
|
||||||
|
|
||||||
dataLoaded = true
|
dataLoaded = true
|
||||||
|
|
||||||
android.util.Log.d("UnlockScreen", "✅ DATA LOADED")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для запуска биометрической разблокировки
|
// Функция для запуска биометрической разблокировки
|
||||||
fun tryBiometricUnlock() {
|
fun tryBiometricUnlock() {
|
||||||
if (selectedAccount == null || activity == null) {
|
if (selectedAccount == null || activity == null) {
|
||||||
android.util.Log.d("UnlockScreen", "❌ Cannot start biometric: no account or activity")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val encryptedPassword = savedPasswordsMap[selectedAccount!!.publicKey]
|
val encryptedPassword = savedPasswordsMap[selectedAccount!!.publicKey]
|
||||||
if (encryptedPassword == null) {
|
if (encryptedPassword == null) {
|
||||||
android.util.Log.d("UnlockScreen", "❌ No saved password for account")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
android.util.Log.d("UnlockScreen", "🔐 Starting biometric unlock...")
|
|
||||||
|
|
||||||
biometricManager.decryptPassword(
|
biometricManager.decryptPassword(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
encryptedData = encryptedPassword,
|
encryptedData = encryptedPassword,
|
||||||
onSuccess = { decryptedPassword ->
|
onSuccess = { decryptedPassword ->
|
||||||
android.util.Log.d("UnlockScreen", "✅ Biometric success, unlocking...")
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
performUnlock(
|
performUnlock(
|
||||||
selectedAccount = selectedAccount,
|
selectedAccount = selectedAccount,
|
||||||
@@ -289,11 +273,9 @@ fun UnlockScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError = { errorMessage ->
|
onError = { errorMessage ->
|
||||||
android.util.Log.e("UnlockScreen", "❌ Biometric error: $errorMessage")
|
|
||||||
error = errorMessage
|
error = errorMessage
|
||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
android.util.Log.d("UnlockScreen", "🚫 Biometric cancelled")
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ fun ChatDetailScreen(
|
|||||||
contract = ActivityResultContracts.TakePicture()
|
contract = ActivityResultContracts.TakePicture()
|
||||||
) { success ->
|
) { success ->
|
||||||
if (success && cameraImageUri != null) {
|
if (success && cameraImageUri != null) {
|
||||||
android.util.Log.d("ChatDetailScreen", "📷 Photo captured: $cameraImageUri")
|
|
||||||
// Открываем редактор вместо прямой отправки
|
// Открываем редактор вместо прямой отправки
|
||||||
pendingCameraPhotoUri = cameraImageUri
|
pendingCameraPhotoUri = cameraImageUri
|
||||||
}
|
}
|
||||||
@@ -210,13 +209,11 @@ fun ChatDetailScreen(
|
|||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
android.util.Log.d("ChatDetailScreen", "📄 File selected: $uri")
|
|
||||||
val fileName = MediaUtils.getFileName(context, uri)
|
val fileName = MediaUtils.getFileName(context, uri)
|
||||||
val fileSize = MediaUtils.getFileSize(context, uri)
|
val fileSize = MediaUtils.getFileSize(context, uri)
|
||||||
|
|
||||||
// Проверяем размер файла
|
// Проверяем размер файла
|
||||||
if (fileSize > MediaUtils.MAX_FILE_SIZE_MB * 1024 * 1024) {
|
if (fileSize > MediaUtils.MAX_FILE_SIZE_MB * 1024 * 1024) {
|
||||||
android.util.Log.w("ChatDetailScreen", "📄 File too large: ${fileSize / 1024 / 1024}MB")
|
|
||||||
// TODO: Показать ошибку
|
// TODO: Показать ошибку
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -1994,7 +1991,6 @@ fun ChatDetailScreen(
|
|||||||
currentUserPublicKey = currentUserPublicKey,
|
currentUserPublicKey = currentUserPublicKey,
|
||||||
onMediaSelected = { selectedMedia ->
|
onMediaSelected = { selectedMedia ->
|
||||||
// 📸 Открываем edit screen для выбранных изображений
|
// 📸 Открываем edit screen для выбранных изображений
|
||||||
android.util.Log.d("ChatDetailScreen", "📸 Opening editor for ${selectedMedia.size} media items")
|
|
||||||
|
|
||||||
// Собираем URI изображений (пока без видео)
|
// Собираем URI изображений (пока без видео)
|
||||||
val imageUris = selectedMedia
|
val imageUris = selectedMedia
|
||||||
@@ -2020,7 +2016,6 @@ fun ChatDetailScreen(
|
|||||||
)
|
)
|
||||||
cameraLauncher.launch(cameraImageUri!!)
|
cameraLauncher.launch(cameraImageUri!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("ChatDetailScreen", "📷 Failed to create camera file", e)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOpenFilePicker = {
|
onOpenFilePicker = {
|
||||||
@@ -2029,7 +2024,6 @@ fun ChatDetailScreen(
|
|||||||
},
|
},
|
||||||
onAvatarClick = {
|
onAvatarClick = {
|
||||||
// 👤 Отправляем свой аватар (как в desktop)
|
// 👤 Отправляем свой аватар (как в desktop)
|
||||||
android.util.Log.d("ChatDetailScreen", "👤 Sending avatar...")
|
|
||||||
viewModel.sendAvatarMessage()
|
viewModel.sendAvatarMessage()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.app.Application
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
@@ -334,8 +333,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* Открыть диалог
|
* Открыть диалог
|
||||||
*/
|
*/
|
||||||
fun openDialog(publicKey: String, title: String = "", username: String = "") {
|
fun openDialog(publicKey: String, title: String = "", username: String = "") {
|
||||||
Log.d(TAG, "🚪 openDialog START: publicKey=$publicKey, title=$title")
|
|
||||||
Log.d(TAG, "🔍 Current state: opponentKey=$opponentKey, myPublicKey=$myPublicKey")
|
|
||||||
|
|
||||||
// 🔥 ВСЕГДА перезагружаем данные - не кешируем, т.к. диалог мог быть удалён
|
// 🔥 ВСЕГДА перезагружаем данные - не кешируем, т.к. диалог мог быть удалён
|
||||||
// if (opponentKey == publicKey) {
|
// if (opponentKey == publicKey) {
|
||||||
@@ -354,11 +351,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val forwardMessages = ForwardManager.getForwardMessagesForChat(publicKey)
|
val forwardMessages = ForwardManager.getForwardMessagesForChat(publicKey)
|
||||||
val hasForward = forwardMessages.isNotEmpty()
|
val hasForward = forwardMessages.isNotEmpty()
|
||||||
if (hasForward) {
|
if (hasForward) {
|
||||||
Log.d(TAG, "📨 Has forward messages: ${forwardMessages.size}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем состояние
|
// Сбрасываем состояние
|
||||||
Log.d(TAG, "🔄 Resetting state, setting messages to empty")
|
|
||||||
_messages.value = emptyList()
|
_messages.value = emptyList()
|
||||||
_opponentOnline.value = false
|
_opponentOnline.value = false
|
||||||
_opponentTyping.value = false
|
_opponentTyping.value = false
|
||||||
@@ -370,7 +365,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
subscribedToOnlineStatus = false // 🔥 Сбрасываем флаг подписки при смене диалога
|
subscribedToOnlineStatus = false // 🔥 Сбрасываем флаг подписки при смене диалога
|
||||||
isDialogActive = true // 🔥 Диалог активен!
|
isDialogActive = true // 🔥 Диалог активен!
|
||||||
|
|
||||||
Log.d(TAG, "📊 State after reset: messagesCount=${_messages.value.size}, isDialogActive=$isDialogActive")
|
|
||||||
|
|
||||||
// 📨 Применяем Forward сообщения СРАЗУ после сброса
|
// 📨 Применяем Forward сообщения СРАЗУ после сброса
|
||||||
if (hasForward) {
|
if (hasForward) {
|
||||||
@@ -427,13 +421,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val opponent = opponentKey ?: return
|
val opponent = opponentKey ?: return
|
||||||
val dialogKey = getDialogKey(account, opponent)
|
val dialogKey = getDialogKey(account, opponent)
|
||||||
|
|
||||||
Log.d(TAG, "📥 loadMessagesFromDatabase START: account=$account, opponent=$opponent, dialogKey=$dialogKey")
|
|
||||||
|
|
||||||
// 📁 Проверяем является ли это Saved Messages
|
// 📁 Проверяем является ли это Saved Messages
|
||||||
val isSavedMessages = (opponent == account)
|
val isSavedMessages = (opponent == account)
|
||||||
|
|
||||||
if (isLoadingMessages) {
|
if (isLoadingMessages) {
|
||||||
Log.d(TAG, "⚠️ loadMessagesFromDatabase SKIP - already loading")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isLoadingMessages = true
|
isLoadingMessages = true
|
||||||
@@ -442,10 +434,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
try {
|
try {
|
||||||
// 🔥 МГНОВЕННАЯ загрузка из кэша если есть!
|
// 🔥 МГНОВЕННАЯ загрузка из кэша если есть!
|
||||||
val cachedMessages = dialogMessagesCache[dialogKey]
|
val cachedMessages = dialogMessagesCache[dialogKey]
|
||||||
Log.d(TAG, "📦 Cache check: dialogKey=$dialogKey, cachedCount=${cachedMessages?.size ?: 0}")
|
|
||||||
|
|
||||||
if (cachedMessages != null && cachedMessages.isNotEmpty()) {
|
if (cachedMessages != null && cachedMessages.isNotEmpty()) {
|
||||||
Log.d(TAG, "✅ Using cached messages: ${cachedMessages.size}")
|
|
||||||
withContext(Dispatchers.Main.immediate) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
_messages.value = cachedMessages
|
_messages.value = cachedMessages
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
@@ -466,11 +456,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
messageDao.getMessageCount(account, dialogKey)
|
messageDao.getMessageCount(account, dialogKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📊 DB message count: totalCount=$totalCount, isSavedMessages=$isSavedMessages")
|
|
||||||
|
|
||||||
if (totalCount == 0) {
|
if (totalCount == 0) {
|
||||||
// Пустой диалог - сразу показываем пустое состояние без скелетона
|
// Пустой диалог - сразу показываем пустое состояние без скелетона
|
||||||
Log.d(TAG, "📭 Empty dialog - showing empty state")
|
|
||||||
withContext(Dispatchers.Main.immediate) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
_messages.value = emptyList()
|
_messages.value = emptyList()
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
@@ -481,24 +469,20 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
// 🔥 Есть сообщения - показываем скелетон и загружаем с задержкой для анимации
|
// 🔥 Есть сообщения - показываем скелетон и загружаем с задержкой для анимации
|
||||||
if (delayMs > 0) {
|
if (delayMs > 0) {
|
||||||
Log.d(TAG, "⏳ Delaying load for ${delayMs}ms")
|
|
||||||
withContext(Dispatchers.Main.immediate) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
_isLoading.value = true // Показываем скелетон
|
_isLoading.value = true // Показываем скелетон
|
||||||
}
|
}
|
||||||
delay(delayMs)
|
delay(delayMs)
|
||||||
Log.d(TAG, "✅ Delay finished, starting DB load")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 🔥 Получаем первую страницу - используем специальный метод для saved messages
|
// 🔥 Получаем первую страницу - используем специальный метод для saved messages
|
||||||
Log.d(TAG, "🔍 Querying DB for messages...")
|
|
||||||
val entities = if (isSavedMessages) {
|
val entities = if (isSavedMessages) {
|
||||||
messageDao.getMessagesForSavedDialog(account, limit = PAGE_SIZE, offset = 0)
|
messageDao.getMessagesForSavedDialog(account, limit = PAGE_SIZE, offset = 0)
|
||||||
} else {
|
} else {
|
||||||
messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0)
|
messageDao.getMessages(account, dialogKey, limit = PAGE_SIZE, offset = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📋 Loaded entities from DB: count=${entities.size}")
|
|
||||||
|
|
||||||
hasMoreMessages = entities.size >= PAGE_SIZE
|
hasMoreMessages = entities.size >= PAGE_SIZE
|
||||||
currentOffset = entities.size
|
currentOffset = entities.size
|
||||||
@@ -517,11 +501,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "✅ Decrypted messages: count=${messages.size}")
|
|
||||||
|
|
||||||
// 🔥 Сохраняем в кэш для мгновенной повторной загрузки!
|
// 🔥 Сохраняем в кэш для мгновенной повторной загрузки!
|
||||||
dialogMessagesCache[dialogKey] = messages.toList()
|
dialogMessagesCache[dialogKey] = messages.toList()
|
||||||
Log.d(TAG, "💾 Saved to cache: dialogKey=$dialogKey, count=${messages.size}")
|
|
||||||
|
|
||||||
// 🔥 СРАЗУ обновляем UI - пользователь видит сообщения мгновенно
|
// 🔥 СРАЗУ обновляем UI - пользователь видит сообщения мгновенно
|
||||||
// НО сохраняем оптимистичные сообщения (SENDING), которые ещё не в БД
|
// НО сохраняем оптимистичные сообщения (SENDING), которые ещё не в БД
|
||||||
@@ -558,9 +540,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
isLoadingMessages = false
|
isLoadingMessages = false
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Error loading messages from DB", e)
|
|
||||||
Log.e(TAG, "❌ Exception details: ${e.message}")
|
|
||||||
Log.e(TAG, "❌ Stack trace: ${e.stackTraceToString()}")
|
|
||||||
withContext(Dispatchers.Main.immediate) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -828,7 +807,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
result
|
result
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("ChatViewModel", "Failed to parse attachments", e)
|
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -985,7 +963,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📸 Failed to parse reply attachments from JSON: ${e.message}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1256,13 +1233,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val isFirstMessage = messageDao.getMessageCount(sender, sender, recipient) == 0
|
val isFirstMessage = messageDao.getMessageCount(sender, sender, recipient) == 0
|
||||||
if (isFirstMessage) {
|
if (isFirstMessage) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "📸 First message to user - checking for avatar...")
|
|
||||||
// Получаем свой аватар из AvatarRepository
|
// Получаем свой аватар из AvatarRepository
|
||||||
val avatarDao = database.avatarDao()
|
val avatarDao = database.avatarDao()
|
||||||
val myAvatar = avatarDao.getLatestAvatar(sender)
|
val myAvatar = avatarDao.getLatestAvatar(sender)
|
||||||
|
|
||||||
if (myAvatar != null) {
|
if (myAvatar != null) {
|
||||||
Log.d(TAG, "📸 Found my avatar, path: ${myAvatar.avatar}")
|
|
||||||
// Читаем и расшифровываем аватар
|
// Читаем и расшифровываем аватар
|
||||||
val avatarBlob = com.rosetta.messenger.utils.AvatarFileManager.readAvatar(
|
val avatarBlob = com.rosetta.messenger.utils.AvatarFileManager.readAvatar(
|
||||||
getApplication(),
|
getApplication(),
|
||||||
@@ -1270,10 +1245,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (avatarBlob != null && avatarBlob.isNotEmpty()) {
|
if (avatarBlob != null && avatarBlob.isNotEmpty()) {
|
||||||
Log.d(TAG, "📸 Avatar blob read successfully, length: ${avatarBlob.length}")
|
|
||||||
// Шифруем аватар с ChaCha ключом для отправки
|
// Шифруем аватар с ChaCha ключом для отправки
|
||||||
val encryptedAvatarBlob = MessageCrypto.encryptReplyBlob(avatarBlob, plainKeyAndNonce)
|
val encryptedAvatarBlob = MessageCrypto.encryptReplyBlob(avatarBlob, plainKeyAndNonce)
|
||||||
Log.d(TAG, "📸 Avatar encrypted, length: ${encryptedAvatarBlob.length}")
|
|
||||||
|
|
||||||
val avatarAttachmentId = "avatar_${timestamp}"
|
val avatarAttachmentId = "avatar_${timestamp}"
|
||||||
messageAttachments.add(MessageAttachment(
|
messageAttachments.add(MessageAttachment(
|
||||||
@@ -1282,15 +1255,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
type = AttachmentType.AVATAR,
|
type = AttachmentType.AVATAR,
|
||||||
preview = ""
|
preview = ""
|
||||||
))
|
))
|
||||||
Log.d(TAG, "📸 ✅ Avatar attached to first message!")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "📸 Avatar blob is null or empty!")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "📸 No avatar found for current user")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📸 Failed to attach avatar to first message", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1427,11 +1396,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val privateKey = myPrivateKey
|
val privateKey = myPrivateKey
|
||||||
|
|
||||||
if (recipient == null || sender == null || privateKey == null) {
|
if (recipient == null || sender == null || privateKey == null) {
|
||||||
Log.e(TAG, "📸 Cannot send image: missing keys")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSending) {
|
if (isSending) {
|
||||||
Log.w(TAG, "📸 Already sending message")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1462,7 +1429,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
_messages.value = _messages.value + optimisticMessage
|
_messages.value = _messages.value + optimisticMessage
|
||||||
_inputText.value = ""
|
_inputText.value = ""
|
||||||
|
|
||||||
Log.d(TAG, "📸 Sending image message: id=$messageId")
|
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -1485,9 +1451,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
var uploadTag = ""
|
var uploadTag = ""
|
||||||
|
|
||||||
if (!isSavedMessages) {
|
if (!isSavedMessages) {
|
||||||
Log.d(TAG, "📤 Uploading image to Transport Server...")
|
|
||||||
uploadTag = TransportManager.uploadFile(attachmentId, encryptedImageBlob)
|
uploadTag = TransportManager.uploadFile(attachmentId, encryptedImageBlob)
|
||||||
Log.d(TAG, "📤 Upload complete, tag: $uploadTag")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview содержит tag::blurhash (как в desktop)
|
// Preview содержит tag::blurhash (как в desktop)
|
||||||
@@ -1556,10 +1520,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
saveDialog(if (text.isNotEmpty()) text else "photo", timestamp)
|
saveDialog(if (text.isNotEmpty()) text else "photo", timestamp)
|
||||||
Log.d(TAG, "📸 ✅ Image message sent successfully")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📸 ❌ Failed to send image message", e)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||||
}
|
}
|
||||||
@@ -1590,11 +1552,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val privateKey = myPrivateKey
|
val privateKey = myPrivateKey
|
||||||
|
|
||||||
if (recipient == null || sender == null || privateKey == null) {
|
if (recipient == null || sender == null || privateKey == null) {
|
||||||
Log.e(TAG, "🖼️ Cannot send image group: missing keys")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSending) {
|
if (isSending) {
|
||||||
Log.w(TAG, "🖼️ Already sending message")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,7 +1588,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
_messages.value = _messages.value + optimisticMessage
|
_messages.value = _messages.value + optimisticMessage
|
||||||
_inputText.value = ""
|
_inputText.value = ""
|
||||||
|
|
||||||
Log.d(TAG, "🖼️ Sending image group: ${images.size} images, id=$messageId")
|
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -1687,7 +1646,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
put("height", imageData.height)
|
put("height", imageData.height)
|
||||||
})
|
})
|
||||||
|
|
||||||
Log.d(TAG, "🖼️ Image $index uploaded: tag=${uploadTag?.take(20) ?: "null"}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаём пакет
|
// Создаём пакет
|
||||||
@@ -1725,10 +1683,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
saveDialog(if (text.isNotEmpty()) text else "📷 ${images.size} photos", timestamp)
|
saveDialog(if (text.isNotEmpty()) text else "📷 ${images.size} photos", timestamp)
|
||||||
Log.d(TAG, "🖼️ ✅ Image group sent successfully")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "🖼️ ❌ Failed to send image group", e)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||||
}
|
}
|
||||||
@@ -1751,11 +1707,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val privateKey = myPrivateKey
|
val privateKey = myPrivateKey
|
||||||
|
|
||||||
if (recipient == null || sender == null || privateKey == null) {
|
if (recipient == null || sender == null || privateKey == null) {
|
||||||
Log.e(TAG, "📄 Cannot send file: missing keys")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSending) {
|
if (isSending) {
|
||||||
Log.w(TAG, "📄 Already sending message")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1785,7 +1739,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
_messages.value = _messages.value + optimisticMessage
|
_messages.value = _messages.value + optimisticMessage
|
||||||
_inputText.value = ""
|
_inputText.value = ""
|
||||||
|
|
||||||
Log.d(TAG, "📄 Sending file message: $fileName ($fileSize bytes)")
|
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -1807,9 +1760,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
var uploadTag = ""
|
var uploadTag = ""
|
||||||
|
|
||||||
if (!isSavedMessages) {
|
if (!isSavedMessages) {
|
||||||
Log.d(TAG, "📤 Uploading file to Transport Server...")
|
|
||||||
uploadTag = TransportManager.uploadFile(attachmentId, encryptedFileBlob)
|
uploadTag = TransportManager.uploadFile(attachmentId, encryptedFileBlob)
|
||||||
Log.d(TAG, "📤 Upload complete, tag: $uploadTag")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview содержит tag::size::name (как в desktop)
|
// Preview содержит tag::size::name (как в desktop)
|
||||||
@@ -1865,10 +1816,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
saveDialog(if (text.isNotEmpty()) text else "file", timestamp)
|
saveDialog(if (text.isNotEmpty()) text else "file", timestamp)
|
||||||
Log.d(TAG, "📄 ✅ File message sent successfully")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📄 ❌ Failed to send file message", e)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||||
}
|
}
|
||||||
@@ -1888,11 +1837,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val userPrivateKey = myPrivateKey
|
val userPrivateKey = myPrivateKey
|
||||||
|
|
||||||
if (recipient == null || sender == null || userPrivateKey == null) {
|
if (recipient == null || sender == null || userPrivateKey == null) {
|
||||||
Log.e(TAG, "👤 Cannot send avatar: missing keys")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSending) {
|
if (isSending) {
|
||||||
Log.w(TAG, "👤 Already sending message")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1903,13 +1850,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "👤 Fetching current user avatar...")
|
|
||||||
// Получаем свой аватар из AvatarRepository
|
// Получаем свой аватар из AvatarRepository
|
||||||
val avatarDao = database.avatarDao()
|
val avatarDao = database.avatarDao()
|
||||||
val myAvatar = avatarDao.getLatestAvatar(sender)
|
val myAvatar = avatarDao.getLatestAvatar(sender)
|
||||||
|
|
||||||
if (myAvatar == null) {
|
if (myAvatar == null) {
|
||||||
Log.w(TAG, "👤 No avatar found for current user")
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
getApplication(),
|
getApplication(),
|
||||||
@@ -1921,7 +1866,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "👤 Found avatar, path: ${myAvatar.avatar}")
|
|
||||||
// Читаем и расшифровываем аватар
|
// Читаем и расшифровываем аватар
|
||||||
val avatarBlob = com.rosetta.messenger.utils.AvatarFileManager.readAvatar(
|
val avatarBlob = com.rosetta.messenger.utils.AvatarFileManager.readAvatar(
|
||||||
getApplication(),
|
getApplication(),
|
||||||
@@ -1929,7 +1873,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (avatarBlob == null || avatarBlob.isEmpty()) {
|
if (avatarBlob == null || avatarBlob.isEmpty()) {
|
||||||
Log.w(TAG, "👤 Avatar blob is null or empty!")
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
getApplication(),
|
getApplication(),
|
||||||
@@ -1941,8 +1884,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "👤 Avatar blob read successfully, length: ${avatarBlob.length}")
|
|
||||||
Log.d(TAG, "👤 Avatar blob first 100 chars: ${avatarBlob.take(100)}")
|
|
||||||
|
|
||||||
// 🔥 КРИТИЧНО: Desktop ожидает полный data URL, а не просто Base64!
|
// 🔥 КРИТИЧНО: Desktop ожидает полный data URL, а не просто Base64!
|
||||||
// Добавляем префикс если его нет
|
// Добавляем префикс если его нет
|
||||||
@@ -1951,7 +1892,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
} else {
|
} else {
|
||||||
"data:image/png;base64,$avatarBlob"
|
"data:image/png;base64,$avatarBlob"
|
||||||
}
|
}
|
||||||
Log.d(TAG, "👤 Avatar data URL length: ${avatarDataUrl.length}")
|
|
||||||
|
|
||||||
// Генерируем blurhash для preview (как на desktop)
|
// Генерируем blurhash для preview (как на desktop)
|
||||||
val avatarBlurhash = withContext(Dispatchers.IO) {
|
val avatarBlurhash = withContext(Dispatchers.IO) {
|
||||||
@@ -1963,11 +1903,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to generate blurhash for avatar: ${e.message}")
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "👤 Avatar blurhash generated: ${avatarBlurhash.take(20)}...")
|
|
||||||
|
|
||||||
// 1. 🚀 Optimistic UI
|
// 1. 🚀 Optimistic UI
|
||||||
val optimisticMessage = ChatMessage(
|
val optimisticMessage = ChatMessage(
|
||||||
@@ -2001,7 +1939,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// НЕ с AVATAR_PASSWORD! AVATAR_PASSWORD используется только для локального хранения
|
// НЕ с AVATAR_PASSWORD! AVATAR_PASSWORD используется только для локального хранения
|
||||||
// Используем avatarDataUrl (с префиксом data:image/...) а не avatarBlob!
|
// Используем avatarDataUrl (с префиксом data:image/...) а не avatarBlob!
|
||||||
val encryptedAvatarBlob = MessageCrypto.encryptReplyBlob(avatarDataUrl, plainKeyAndNonce)
|
val encryptedAvatarBlob = MessageCrypto.encryptReplyBlob(avatarDataUrl, plainKeyAndNonce)
|
||||||
Log.d(TAG, "👤 Avatar encrypted with ChaCha key, length: ${encryptedAvatarBlob.length}")
|
|
||||||
|
|
||||||
val avatarAttachmentId = "avatar_$timestamp"
|
val avatarAttachmentId = "avatar_$timestamp"
|
||||||
|
|
||||||
@@ -2011,18 +1948,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
if (!isSavedMessages) {
|
if (!isSavedMessages) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "👤 📤 Uploading avatar to Transport Server...")
|
|
||||||
uploadTag = TransportManager.uploadFile(avatarAttachmentId, encryptedAvatarBlob)
|
uploadTag = TransportManager.uploadFile(avatarAttachmentId, encryptedAvatarBlob)
|
||||||
Log.d(TAG, "👤 📤 Upload complete, tag: $uploadTag")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "👤 ❌ Failed to upload avatar to Transport Server", e)
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview содержит tag::blurhash (как в desktop)
|
// Preview содержит tag::blurhash (как в desktop)
|
||||||
val previewWithTag = if (uploadTag.isNotEmpty()) "$uploadTag::$avatarBlurhash" else avatarBlurhash
|
val previewWithTag = if (uploadTag.isNotEmpty()) "$uploadTag::$avatarBlurhash" else avatarBlurhash
|
||||||
Log.d(TAG, "👤 Preview with tag: ${previewWithTag.take(50)}...")
|
|
||||||
|
|
||||||
val avatarAttachment = MessageAttachment(
|
val avatarAttachment = MessageAttachment(
|
||||||
id = avatarAttachmentId,
|
id = avatarAttachmentId,
|
||||||
@@ -2083,12 +2016,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
saveDialog("\$a=Avatar", timestamp)
|
saveDialog("\$a=Avatar", timestamp)
|
||||||
|
|
||||||
Log.d(TAG, "👤 💾 Avatar message saved to database: messageId=$messageId")
|
|
||||||
|
|
||||||
Log.d(TAG, "👤 ✅ Avatar message sent successfully!")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "👤 ❌ Failed to send avatar message", e)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
@@ -2419,7 +2349,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e(TAG, "Failed to clear chat history", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2437,7 +2366,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val bytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
val bytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
||||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to decode base64 to bitmap: ${e.message}")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
// 📁 НЕ загружаем для Saved Messages
|
// 📁 НЕ загружаем для Saved Messages
|
||||||
val isSavedMessages = (dialog.account == dialog.opponentKey)
|
val isSavedMessages = (dialog.account == dialog.opponentKey)
|
||||||
if (!isSavedMessages && (dialog.opponentTitle.isEmpty() || dialog.opponentTitle == dialog.opponentKey || dialog.opponentTitle == dialog.opponentKey.take(7))) {
|
if (!isSavedMessages && (dialog.opponentTitle.isEmpty() || dialog.opponentTitle == dialog.opponentKey || dialog.opponentTitle == dialog.opponentKey.take(7))) {
|
||||||
android.util.Log.d("ChatsListVM", "🔍 Dialog needs user info: ${dialog.opponentKey.take(16)}... title='${dialog.opponentTitle}'")
|
|
||||||
loadUserInfoForDialog(dialog.opponentKey)
|
loadUserInfoForDialog(dialog.opponentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,12 +165,10 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("ChatsListVM", "Failed to parse attachments", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Лог для отладки - показываем и старые и новые значения
|
// 🔥 Лог для отладки - показываем и старые и новые значения
|
||||||
android.util.Log.d("ChatsListVM", "📊 Dialog ${dialog.opponentKey.take(16)}... | OLD: fromMe=${dialog.lastMessageFromMe}, del=${dialog.lastMessageDelivered}, read=${dialog.lastMessageRead} | NEW: fromMe=$actualFromMe, del=$actualDelivered, read=$actualRead | attachment=$attachmentType")
|
|
||||||
|
|
||||||
DialogUiModel(
|
DialogUiModel(
|
||||||
id = dialog.id,
|
id = dialog.id,
|
||||||
@@ -212,13 +209,11 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
dialogDao.getRequestsFlow(publicKey)
|
dialogDao.getRequestsFlow(publicKey)
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.map { requestsList ->
|
.map { requestsList ->
|
||||||
android.util.Log.d("ChatsListVM", "📬 getRequestsFlow emitted: ${requestsList.size} requests")
|
|
||||||
requestsList.map { dialog ->
|
requestsList.map { dialog ->
|
||||||
// 🔥 Загружаем информацию о пользователе если её нет
|
// 🔥 Загружаем информацию о пользователе если её нет
|
||||||
// 📁 НЕ загружаем для Saved Messages
|
// 📁 НЕ загружаем для Saved Messages
|
||||||
val isSavedMessages = (dialog.account == dialog.opponentKey)
|
val isSavedMessages = (dialog.account == dialog.opponentKey)
|
||||||
if (!isSavedMessages && (dialog.opponentTitle.isEmpty() || dialog.opponentTitle == dialog.opponentKey)) {
|
if (!isSavedMessages && (dialog.opponentTitle.isEmpty() || dialog.opponentTitle == dialog.opponentKey)) {
|
||||||
android.util.Log.d("ChatsListVM", "📬 Request needs user info: ${dialog.opponentKey.take(16)}... title='${dialog.opponentTitle}'")
|
|
||||||
loadUserInfoForRequest(dialog.opponentKey)
|
loadUserInfoForRequest(dialog.opponentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +245,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("ChatsListVM", "Failed to parse attachments in request", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,18 +470,15 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
private fun loadUserInfoForDialog(publicKey: String) {
|
private fun loadUserInfoForDialog(publicKey: String) {
|
||||||
// 📁 Не запрашиваем информацию о самом себе (Saved Messages)
|
// 📁 Не запрашиваем информацию о самом себе (Saved Messages)
|
||||||
if (publicKey == currentAccount) {
|
if (publicKey == currentAccount) {
|
||||||
android.util.Log.d("ChatsListVM", "📁 Skipping loadUserInfoForDialog for Saved Messages")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Не запрашиваем если уже запрашивали
|
// 🔥 Не запрашиваем если уже запрашивали
|
||||||
if (requestedUserInfoKeys.contains(publicKey)) {
|
if (requestedUserInfoKeys.contains(publicKey)) {
|
||||||
android.util.Log.d("ChatsListVM", "⏭️ Skipping loadUserInfoForDialog - already requested for ${publicKey.take(16)}...")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requestedUserInfoKeys.add(publicKey)
|
requestedUserInfoKeys.add(publicKey)
|
||||||
|
|
||||||
android.util.Log.d("ChatsListVM", "🔍 loadUserInfoForDialog: ${publicKey.take(16)}...")
|
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -499,7 +490,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
// 🔥 ВАЖНО: Используем хеш ключа, как в MessageRepository.requestUserInfo
|
// 🔥 ВАЖНО: Используем хеш ключа, как в MessageRepository.requestUserInfo
|
||||||
val privateKeyHash = CryptoManager.generatePrivateKeyHash(currentUserPrivateKey)
|
val privateKeyHash = CryptoManager.generatePrivateKeyHash(currentUserPrivateKey)
|
||||||
|
|
||||||
android.util.Log.d("ChatsListVM", "📤 Sending PacketSearch for user info: ${publicKey.take(16)}...")
|
|
||||||
|
|
||||||
// Запрашиваем информацию о пользователе с сервера
|
// Запрашиваем информацию о пользователе с сервера
|
||||||
val packet = PacketSearch().apply {
|
val packet = PacketSearch().apply {
|
||||||
@@ -508,7 +498,6 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
|||||||
}
|
}
|
||||||
ProtocolManager.send(packet)
|
ProtocolManager.send(packet)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("ChatsListVM", "❌ loadUserInfoForRequest error: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.rosetta.messenger.ui.chats
|
package com.rosetta.messenger.ui.chats
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rosetta.messenger.network.PacketSearch
|
import com.rosetta.messenger.network.PacketSearch
|
||||||
@@ -48,43 +47,35 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
val currentQuery = lastSearchedText
|
val currentQuery = lastSearchedText
|
||||||
val responseSearch = packet.search
|
val responseSearch = packet.search
|
||||||
|
|
||||||
Log.d(TAG, "📥 PacketSearch received: ${packet.users.size} users, search='$responseSearch', ourQuery='$currentQuery'")
|
|
||||||
|
|
||||||
// Принимаем ответ только если:
|
// Принимаем ответ только если:
|
||||||
// 1. search в ответе совпадает с нашим запросом, ИЛИ
|
// 1. search в ответе совпадает с нашим запросом, ИЛИ
|
||||||
// 2. search пустой но мы ждём ответ (lastSearchedText не пустой)
|
// 2. search пустой но мы ждём ответ (lastSearchedText не пустой)
|
||||||
// НО: если search пустой и мы НЕ ждём ответ - игнорируем
|
// НО: если search пустой и мы НЕ ждём ответ - игнорируем
|
||||||
if (responseSearch.isEmpty() && currentQuery.isEmpty()) {
|
if (responseSearch.isEmpty() && currentQuery.isEmpty()) {
|
||||||
Log.d(TAG, "📥 Ignoring empty search response - no active search")
|
|
||||||
return@handler
|
return@handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если search не пустой и не совпадает с нашим запросом - игнорируем
|
// Если search не пустой и не совпадает с нашим запросом - игнорируем
|
||||||
if (responseSearch.isNotEmpty() && responseSearch != currentQuery) {
|
if (responseSearch.isNotEmpty() && responseSearch != currentQuery) {
|
||||||
Log.d(TAG, "📥 Ignoring search response - search mismatch: '$responseSearch' != '$currentQuery'")
|
|
||||||
return@handler
|
return@handler
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📥 ACCEPTED PacketSearch response: ${packet.users.size} users")
|
|
||||||
packet.users.forEachIndexed { index, user ->
|
packet.users.forEachIndexed { index, user ->
|
||||||
Log.d(TAG, " [$index] publicKey=${user.publicKey.take(16)}... title=${user.title} username=${user.username}")
|
|
||||||
}
|
}
|
||||||
_searchResults.value = packet.users
|
_searchResults.value = packet.users
|
||||||
_isSearching.value = false
|
_isSearching.value = false
|
||||||
Log.d(TAG, "📥 Updated searchResults, isSearching=false")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Регистрируем обработчик пакетов поиска
|
// Регистрируем обработчик пакетов поиска
|
||||||
Log.d(TAG, "🟢 INIT: Registering searchPacketHandler for 0x03")
|
|
||||||
ProtocolManager.waitPacket(0x03, searchPacketHandler)
|
ProtocolManager.waitPacket(0x03, searchPacketHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
// Отписываемся от пакетов при уничтожении ViewModel
|
// Отписываемся от пакетов при уничтожении ViewModel
|
||||||
Log.d(TAG, "🔴 onCleared: Unregistering searchPacketHandler")
|
|
||||||
ProtocolManager.unwaitPacket(0x03, searchPacketHandler)
|
ProtocolManager.unwaitPacket(0x03, searchPacketHandler)
|
||||||
searchJob?.cancel()
|
searchJob?.cancel()
|
||||||
}
|
}
|
||||||
@@ -101,7 +92,6 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
* Аналогично handleSearch в React Native
|
* Аналогично handleSearch в React Native
|
||||||
*/
|
*/
|
||||||
fun onSearchQueryChange(query: String) {
|
fun onSearchQueryChange(query: String) {
|
||||||
Log.d(TAG, "🔍 onSearchQueryChange: query='$query' lastSearchedText='$lastSearchedText'")
|
|
||||||
_searchQuery.value = query
|
_searchQuery.value = query
|
||||||
|
|
||||||
// Отменяем предыдущий поиск
|
// Отменяем предыдущий поиск
|
||||||
@@ -109,7 +99,6 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
|
|
||||||
// Если пустой запрос - очищаем результаты
|
// Если пустой запрос - очищаем результаты
|
||||||
if (query.trim().isEmpty()) {
|
if (query.trim().isEmpty()) {
|
||||||
Log.d(TAG, "🔍 Empty query, clearing results")
|
|
||||||
_searchResults.value = emptyList()
|
_searchResults.value = emptyList()
|
||||||
_isSearching.value = false
|
_isSearching.value = false
|
||||||
lastSearchedText = ""
|
lastSearchedText = ""
|
||||||
@@ -118,36 +107,30 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
|
|
||||||
// Если текст уже был найден - не повторяем поиск
|
// Если текст уже был найден - не повторяем поиск
|
||||||
if (query == lastSearchedText) {
|
if (query == lastSearchedText) {
|
||||||
Log.d(TAG, "🔍 Query same as lastSearchedText, skipping")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем индикатор загрузки
|
// Показываем индикатор загрузки
|
||||||
_isSearching.value = true
|
_isSearching.value = true
|
||||||
Log.d(TAG, "🔍 Starting search job with 1s debounce")
|
|
||||||
|
|
||||||
// Запускаем поиск с задержкой 1 секунда (как в React Native)
|
// Запускаем поиск с задержкой 1 секунда (как в React Native)
|
||||||
searchJob = viewModelScope.launch {
|
searchJob = viewModelScope.launch {
|
||||||
delay(1000) // debounce
|
delay(1000) // debounce
|
||||||
|
|
||||||
Log.d(TAG, "🔍 After debounce: protocolState=${ProtocolManager.state.value}")
|
|
||||||
|
|
||||||
// Проверяем состояние протокола
|
// Проверяем состояние протокола
|
||||||
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
|
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
|
||||||
Log.d(TAG, "🔍 Protocol not authenticated, aborting")
|
|
||||||
_isSearching.value = false
|
_isSearching.value = false
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, не изменился ли запрос
|
// Проверяем, не изменился ли запрос
|
||||||
if (query != _searchQuery.value) {
|
if (query != _searchQuery.value) {
|
||||||
Log.d(TAG, "🔍 Query changed during debounce, aborting")
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSearchedText = query
|
lastSearchedText = query
|
||||||
|
|
||||||
Log.d(TAG, "📤 SENDING PacketSearch: query='$query' privateKeyHash=${privateKeyHash.take(16)}...")
|
|
||||||
|
|
||||||
// Создаем и отправляем пакет поиска
|
// Создаем и отправляем пакет поиска
|
||||||
val packetSearch = PacketSearch().apply {
|
val packetSearch = PacketSearch().apply {
|
||||||
@@ -156,7 +139,6 @@ class SearchUsersViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.sendPacket(packetSearch)
|
ProtocolManager.sendPacket(packetSearch)
|
||||||
Log.d(TAG, "📤 PacketSearch sent!")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.rosetta.messenger.ui.chats.components
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -461,10 +460,6 @@ fun ImageAttachment(
|
|||||||
fillMaxSize: Boolean = false,
|
fillMaxSize: Boolean = false,
|
||||||
onImageClick: (attachmentId: String) -> Unit = {}
|
onImageClick: (attachmentId: String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"🖼️ ImageAttachment: id=${attachment.id}, isOutgoing=$isOutgoing, messageStatus=$messageStatus, showTimeOverlay=$showTimeOverlay"
|
|
||||||
)
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -492,12 +487,10 @@ fun ImageAttachment(
|
|||||||
when {
|
when {
|
||||||
// 1. Если blob уже есть в памяти → DOWNLOADED
|
// 1. Если blob уже есть в памяти → DOWNLOADED
|
||||||
attachment.blob.isNotEmpty() -> {
|
attachment.blob.isNotEmpty() -> {
|
||||||
Log.d(TAG, "📦 Blob already in memory for ${attachment.id}")
|
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
}
|
}
|
||||||
// 2. Если preview НЕ содержит UUID → это наш локальный файл → DOWNLOADED
|
// 2. Если preview НЕ содержит UUID → это наш локальный файл → DOWNLOADED
|
||||||
!isDownloadTag(attachment.preview) -> {
|
!isDownloadTag(attachment.preview) -> {
|
||||||
Log.d(TAG, "📦 No download tag, local file for ${attachment.id}")
|
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
}
|
}
|
||||||
// 3. Есть UUID (download tag) → проверяем файловую систему
|
// 3. Есть UUID (download tag) → проверяем файловую систему
|
||||||
@@ -511,10 +504,8 @@ fun ImageAttachment(
|
|||||||
senderPublicKey
|
senderPublicKey
|
||||||
)
|
)
|
||||||
if (hasLocal) {
|
if (hasLocal) {
|
||||||
Log.d(TAG, "📦 Found local file for ${attachment.id}")
|
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "📥 Need to download ${attachment.id}")
|
|
||||||
DownloadStatus.NOT_DOWNLOADED
|
DownloadStatus.NOT_DOWNLOADED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,25 +516,14 @@ fun ImageAttachment(
|
|||||||
if (preview.isNotEmpty() && !isDownloadTag(preview)) {
|
if (preview.isNotEmpty() && !isDownloadTag(preview)) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"🎨 Decoding blurhash: ${preview.take(30)}... (length: ${preview.length})"
|
|
||||||
)
|
|
||||||
blurhashBitmap = BlurHash.decode(preview, 200, 200)
|
blurhashBitmap = BlurHash.decode(preview, 200, 200)
|
||||||
if (blurhashBitmap != null) {
|
if (blurhashBitmap != null) {
|
||||||
Log.d(TAG, "✅ Blurhash decoded successfully")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ Blurhash decode returned null")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to decode blurhash: ${e.message}", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"⚠️ No valid blurhash preview (preview='${preview.take(20)}...', isDownloadTag=${isDownloadTag(preview)})"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загружаем изображение если статус DOWNLOADED
|
// Загружаем изображение если статус DOWNLOADED
|
||||||
@@ -551,11 +531,9 @@ fun ImageAttachment(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// 1. Сначала пробуем blob из памяти
|
// 1. Сначала пробуем blob из памяти
|
||||||
if (attachment.blob.isNotEmpty()) {
|
if (attachment.blob.isNotEmpty()) {
|
||||||
Log.d(TAG, "🖼️ Loading image from blob")
|
|
||||||
imageBitmap = base64ToBitmap(attachment.blob)
|
imageBitmap = base64ToBitmap(attachment.blob)
|
||||||
} else {
|
} else {
|
||||||
// 2. Читаем из файловой системы (как в Desktop getBlob)
|
// 2. Читаем из файловой системы (как в Desktop getBlob)
|
||||||
Log.d(TAG, "🖼️ Loading image from local file")
|
|
||||||
val localBlob =
|
val localBlob =
|
||||||
AttachmentFileManager.readAttachment(
|
AttachmentFileManager.readAttachment(
|
||||||
context,
|
context,
|
||||||
@@ -565,9 +543,7 @@ fun ImageAttachment(
|
|||||||
)
|
)
|
||||||
if (localBlob != null) {
|
if (localBlob != null) {
|
||||||
imageBitmap = base64ToBitmap(localBlob)
|
imageBitmap = base64ToBitmap(localBlob)
|
||||||
Log.d(TAG, "✅ Image loaded from local file")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ Failed to read local file, need to download")
|
|
||||||
downloadStatus = DownloadStatus.NOT_DOWNLOADED
|
downloadStatus = DownloadStatus.NOT_DOWNLOADED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,34 +556,22 @@ fun ImageAttachment(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
downloadStatus = DownloadStatus.DOWNLOADING
|
downloadStatus = DownloadStatus.DOWNLOADING
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
Log.d(TAG, "📥 Starting IMAGE download")
|
|
||||||
Log.d(TAG, "📝 Attachment ID: ${attachment.id}")
|
|
||||||
Log.d(TAG, "🏷️ Download tag: $downloadTag")
|
|
||||||
Log.d(TAG, "👤 Sender public key: ${senderPublicKey.take(16)}...")
|
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
|
|
||||||
// Скачиваем зашифрованный контент
|
// Скачиваем зашифрованный контент
|
||||||
Log.d(TAG, "⬇️ Downloading encrypted content from CDN...")
|
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
||||||
val downloadTime = System.currentTimeMillis() - startTime
|
val downloadTime = System.currentTimeMillis() - startTime
|
||||||
Log.d(TAG, "✅ Downloaded ${encryptedContent.length} chars in ${downloadTime}ms")
|
|
||||||
downloadProgress = 0.5f
|
downloadProgress = 0.5f
|
||||||
|
|
||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
Log.d(TAG, "🔓 Starting decryption...")
|
|
||||||
|
|
||||||
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
||||||
// Сначала расшифровываем его, получаем raw bytes
|
// Сначала расшифровываем его, получаем raw bytes
|
||||||
Log.d(TAG, "🔑 Decrypting ChaCha key from sender...")
|
|
||||||
val decryptedKeyAndNonce =
|
val decryptedKeyAndNonce =
|
||||||
MessageCrypto.decryptKeyFromSender(chachaKey, privateKey)
|
MessageCrypto.decryptKeyFromSender(chachaKey, privateKey)
|
||||||
Log.d(TAG, "🔑 ChaCha key decrypted: ${decryptedKeyAndNonce.size} bytes")
|
|
||||||
|
|
||||||
// Используем decryptAttachmentBlobWithPlainKey который правильно конвертирует
|
// Используем decryptAttachmentBlobWithPlainKey который правильно конвертирует
|
||||||
// bytes в password
|
// bytes в password
|
||||||
Log.d(TAG, "🔓 Decrypting image blob with PBKDF2...")
|
|
||||||
val decryptStartTime = System.currentTimeMillis()
|
val decryptStartTime = System.currentTimeMillis()
|
||||||
val decrypted =
|
val decrypted =
|
||||||
MessageCrypto.decryptAttachmentBlobWithPlainKey(
|
MessageCrypto.decryptAttachmentBlobWithPlainKey(
|
||||||
@@ -615,21 +579,13 @@ fun ImageAttachment(
|
|||||||
decryptedKeyAndNonce
|
decryptedKeyAndNonce
|
||||||
)
|
)
|
||||||
val decryptTime = System.currentTimeMillis() - decryptStartTime
|
val decryptTime = System.currentTimeMillis() - decryptStartTime
|
||||||
Log.d(TAG, "🔓 Decryption completed in ${decryptTime}ms")
|
|
||||||
downloadProgress = 0.8f
|
downloadProgress = 0.8f
|
||||||
|
|
||||||
if (decrypted != null) {
|
if (decrypted != null) {
|
||||||
Log.d(TAG, "✅ Decrypted blob: ${decrypted.length} chars")
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Log.d(TAG, "🖼️ Converting to bitmap...")
|
|
||||||
imageBitmap = base64ToBitmap(decrypted)
|
imageBitmap = base64ToBitmap(decrypted)
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"✅ Bitmap created: ${imageBitmap?.width}x${imageBitmap?.height}"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 💾 Сохраняем в файловую систему (как в Desktop)
|
// 💾 Сохраняем в файловую систему (как в Desktop)
|
||||||
Log.d(TAG, "💾 Saving to local storage...")
|
|
||||||
val saved =
|
val saved =
|
||||||
AttachmentFileManager.saveAttachment(
|
AttachmentFileManager.saveAttachment(
|
||||||
context = context,
|
context = context,
|
||||||
@@ -638,32 +594,18 @@ fun ImageAttachment(
|
|||||||
publicKey = senderPublicKey,
|
publicKey = senderPublicKey,
|
||||||
privateKey = privateKey
|
privateKey = privateKey
|
||||||
)
|
)
|
||||||
Log.d(TAG, "💾 Image saved to local storage: $saved")
|
|
||||||
}
|
}
|
||||||
downloadProgress = 1f
|
downloadProgress = 1f
|
||||||
downloadStatus = DownloadStatus.DOWNLOADED
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
Log.d(TAG, "✅ IMAGE DOWNLOAD COMPLETE")
|
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
Log.e(TAG, "❌ DECRYPTION RETURNED NULL")
|
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
Log.e(TAG, "❌ IMAGE DOWNLOAD FAILED")
|
|
||||||
Log.e(TAG, "📝 Attachment ID: ${attachment.id}")
|
|
||||||
Log.e(TAG, "🏷️ Download tag: $downloadTag")
|
|
||||||
Log.e(TAG, "❌ Error: ${e.message}")
|
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ Cannot download image: empty download tag for ${attachment.id}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,7 +924,6 @@ fun FileAttachment(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
downloadStatus = DownloadStatus.DOWNLOADING
|
downloadStatus = DownloadStatus.DOWNLOADING
|
||||||
Log.d(TAG, "📥 Downloading file: $fileName")
|
|
||||||
|
|
||||||
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
||||||
downloadProgress = 0.6f
|
downloadProgress = 0.6f
|
||||||
@@ -1005,12 +946,10 @@ fun FileAttachment(
|
|||||||
// TODO: Save to Downloads folder
|
// TODO: Save to Downloads folder
|
||||||
downloadProgress = 1f
|
downloadProgress = 1f
|
||||||
downloadStatus = DownloadStatus.DOWNLOADED
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
Log.d(TAG, "✅ File downloaded: $fileName")
|
|
||||||
} else {
|
} else {
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ File download failed", e)
|
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1221,12 +1160,10 @@ fun AvatarAttachment(
|
|||||||
when {
|
when {
|
||||||
// 1. Если blob уже есть в памяти → DOWNLOADED
|
// 1. Если blob уже есть в памяти → DOWNLOADED
|
||||||
attachment.blob.isNotEmpty() -> {
|
attachment.blob.isNotEmpty() -> {
|
||||||
Log.d(TAG, "📦 Avatar blob in memory for ${attachment.id}")
|
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
}
|
}
|
||||||
// 2. Если preview НЕ содержит UUID → локальный файл → DOWNLOADED
|
// 2. Если preview НЕ содержит UUID → локальный файл → DOWNLOADED
|
||||||
!isDownloadTag(attachment.preview) -> {
|
!isDownloadTag(attachment.preview) -> {
|
||||||
Log.d(TAG, "📦 No download tag for avatar ${attachment.id}")
|
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
}
|
}
|
||||||
// 3. Есть UUID (download tag) → проверяем файловую систему
|
// 3. Есть UUID (download tag) → проверяем файловую систему
|
||||||
@@ -1239,10 +1176,8 @@ fun AvatarAttachment(
|
|||||||
senderPublicKey
|
senderPublicKey
|
||||||
)
|
)
|
||||||
if (hasLocal) {
|
if (hasLocal) {
|
||||||
Log.d(TAG, "📦 Found local avatar file for ${attachment.id}")
|
|
||||||
DownloadStatus.DOWNLOADED
|
DownloadStatus.DOWNLOADED
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "📥 Need to download avatar ${attachment.id}")
|
|
||||||
DownloadStatus.NOT_DOWNLOADED
|
DownloadStatus.NOT_DOWNLOADED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1255,7 +1190,6 @@ fun AvatarAttachment(
|
|||||||
try {
|
try {
|
||||||
blurhashBitmap = BlurHash.decode(preview, 100, 100)
|
blurhashBitmap = BlurHash.decode(preview, 100, 100)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to decode avatar blurhash: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1265,11 +1199,9 @@ fun AvatarAttachment(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// 1. Сначала пробуем blob из памяти
|
// 1. Сначала пробуем blob из памяти
|
||||||
if (attachment.blob.isNotEmpty()) {
|
if (attachment.blob.isNotEmpty()) {
|
||||||
Log.d(TAG, "🖼️ Loading avatar from blob")
|
|
||||||
avatarBitmap = base64ToBitmap(attachment.blob)
|
avatarBitmap = base64ToBitmap(attachment.blob)
|
||||||
} else {
|
} else {
|
||||||
// 2. Читаем из файловой системы (как в Desktop getBlob)
|
// 2. Читаем из файловой системы (как в Desktop getBlob)
|
||||||
Log.d(TAG, "🖼️ Loading avatar from local file")
|
|
||||||
val localBlob =
|
val localBlob =
|
||||||
AvatarFileManager.readAvatarByAttachmentId(
|
AvatarFileManager.readAvatarByAttachmentId(
|
||||||
context,
|
context,
|
||||||
@@ -1278,9 +1210,7 @@ fun AvatarAttachment(
|
|||||||
)
|
)
|
||||||
if (localBlob != null) {
|
if (localBlob != null) {
|
||||||
avatarBitmap = base64ToBitmap(localBlob)
|
avatarBitmap = base64ToBitmap(localBlob)
|
||||||
Log.d(TAG, "✅ Avatar loaded from local file")
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ Failed to read local avatar file")
|
|
||||||
downloadStatus = DownloadStatus.NOT_DOWNLOADED
|
downloadStatus = DownloadStatus.NOT_DOWNLOADED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1293,32 +1223,20 @@ fun AvatarAttachment(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
downloadStatus = DownloadStatus.DOWNLOADING
|
downloadStatus = DownloadStatus.DOWNLOADING
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
Log.d(TAG, "👤 Starting AVATAR download")
|
|
||||||
Log.d(TAG, "📝 Attachment ID: ${attachment.id}")
|
|
||||||
Log.d(TAG, "🏷️ Download tag: $downloadTag")
|
|
||||||
Log.d(TAG, "👤 Sender public key: ${senderPublicKey.take(16)}...")
|
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
|
|
||||||
Log.d(TAG, "⬇️ Downloading encrypted avatar from CDN...")
|
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
||||||
val downloadTime = System.currentTimeMillis() - startTime
|
val downloadTime = System.currentTimeMillis() - startTime
|
||||||
Log.d(TAG, "✅ Downloaded ${encryptedContent.length} chars in ${downloadTime}ms")
|
|
||||||
|
|
||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
Log.d(TAG, "🔓 Starting decryption...")
|
|
||||||
|
|
||||||
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
||||||
// Сначала расшифровываем его, получаем raw bytes
|
// Сначала расшифровываем его, получаем raw bytes
|
||||||
Log.d(TAG, "🔑 Decrypting ChaCha key from sender...")
|
|
||||||
val decryptedKeyAndNonce =
|
val decryptedKeyAndNonce =
|
||||||
MessageCrypto.decryptKeyFromSender(chachaKey, privateKey)
|
MessageCrypto.decryptKeyFromSender(chachaKey, privateKey)
|
||||||
Log.d(TAG, "🔑 ChaCha key decrypted: ${decryptedKeyAndNonce.size} bytes")
|
|
||||||
|
|
||||||
// Используем decryptAttachmentBlobWithPlainKey который правильно конвертирует
|
// Используем decryptAttachmentBlobWithPlainKey который правильно конвертирует
|
||||||
// bytes в password
|
// bytes в password
|
||||||
Log.d(TAG, "🔓 Decrypting avatar blob with PBKDF2...")
|
|
||||||
val decryptStartTime = System.currentTimeMillis()
|
val decryptStartTime = System.currentTimeMillis()
|
||||||
val decrypted =
|
val decrypted =
|
||||||
MessageCrypto.decryptAttachmentBlobWithPlainKey(
|
MessageCrypto.decryptAttachmentBlobWithPlainKey(
|
||||||
@@ -1326,21 +1244,13 @@ fun AvatarAttachment(
|
|||||||
decryptedKeyAndNonce
|
decryptedKeyAndNonce
|
||||||
)
|
)
|
||||||
val decryptTime = System.currentTimeMillis() - decryptStartTime
|
val decryptTime = System.currentTimeMillis() - decryptStartTime
|
||||||
Log.d(TAG, "🔓 Decryption completed in ${decryptTime}ms")
|
|
||||||
|
|
||||||
if (decrypted != null) {
|
if (decrypted != null) {
|
||||||
Log.d(TAG, "✅ Decrypted blob: ${decrypted.length} chars")
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Log.d(TAG, "🖼️ Converting to bitmap...")
|
|
||||||
avatarBitmap = base64ToBitmap(decrypted)
|
avatarBitmap = base64ToBitmap(decrypted)
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"✅ Bitmap created: ${avatarBitmap?.width}x${avatarBitmap?.height}"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 💾 Сохраняем в файловую систему по attachment.id (как в Desktop)
|
// 💾 Сохраняем в файловую систему по attachment.id (как в Desktop)
|
||||||
// Desktop: writeFile(`a/${md5(attachment.id + publicKey)}`, encrypted)
|
// Desktop: writeFile(`a/${md5(attachment.id + publicKey)}`, encrypted)
|
||||||
Log.d(TAG, "💾 Saving avatar to file system by attachment ID...")
|
|
||||||
val path =
|
val path =
|
||||||
AvatarFileManager.saveAvatarByAttachmentId(
|
AvatarFileManager.saveAvatarByAttachmentId(
|
||||||
context = context,
|
context = context,
|
||||||
@@ -1348,69 +1258,36 @@ fun AvatarAttachment(
|
|||||||
attachmentId = attachment.id,
|
attachmentId = attachment.id,
|
||||||
publicKey = senderPublicKey
|
publicKey = senderPublicKey
|
||||||
)
|
)
|
||||||
Log.d(TAG, "💾 Avatar saved to: $path")
|
|
||||||
}
|
}
|
||||||
// Сохраняем аватар в репозиторий (для UI обновления)
|
// Сохраняем аватар в репозиторий (для UI обновления)
|
||||||
// Если это исходящее сообщение с аватаром, сохраняем для текущего
|
// Если это исходящее сообщение с аватаром, сохраняем для текущего
|
||||||
// пользователя
|
// пользователя
|
||||||
val targetPublicKey =
|
val targetPublicKey =
|
||||||
if (isOutgoing && currentUserPublicKey.isNotEmpty()) {
|
if (isOutgoing && currentUserPublicKey.isNotEmpty()) {
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"💾 Saving avatar to repository for CURRENT user ${currentUserPublicKey.take(16)}..."
|
|
||||||
)
|
|
||||||
currentUserPublicKey
|
currentUserPublicKey
|
||||||
} else {
|
} else {
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"💾 Saving avatar to repository for SENDER ${senderPublicKey.take(16)}..."
|
|
||||||
)
|
|
||||||
senderPublicKey
|
senderPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// ВАЖНО: ждем завершения сохранения в репозиторий
|
// ВАЖНО: ждем завершения сохранения в репозиторий
|
||||||
if (avatarRepository != null) {
|
if (avatarRepository != null) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "📤 Calling avatarRepository.saveAvatar()...")
|
|
||||||
avatarRepository.saveAvatar(targetPublicKey, decrypted)
|
avatarRepository.saveAvatar(targetPublicKey, decrypted)
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"✅ Avatar saved to repository for ${targetPublicKey.take(16)}"
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to save avatar to repository: ${e.message}", e)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e(
|
|
||||||
TAG,
|
|
||||||
"❌ avatarRepository is NULL! Cannot save avatar for ${targetPublicKey.take(16)}"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadStatus = DownloadStatus.DOWNLOADED
|
downloadStatus = DownloadStatus.DOWNLOADED
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
Log.d(TAG, "✅ AVATAR DOWNLOAD COMPLETE")
|
|
||||||
Log.d(TAG, "=====================================")
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
Log.e(TAG, "❌ AVATAR DECRYPTION RETURNED NULL")
|
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
Log.e(TAG, "❌ AVATAR DOWNLOAD FAILED")
|
|
||||||
Log.e(TAG, "📝 Attachment ID: ${attachment.id}")
|
|
||||||
Log.e(TAG, "🏷️ Download tag: $downloadTag")
|
|
||||||
Log.e(TAG, "👤 Sender: ${senderPublicKey.take(16)}...")
|
|
||||||
Log.e(TAG, "❌ Error: ${e.message}")
|
|
||||||
Log.e(TAG, "=====================================")
|
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
downloadStatus = DownloadStatus.ERROR
|
downloadStatus = DownloadStatus.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ Cannot download avatar: empty download tag for ${attachment.id}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1690,7 +1567,6 @@ private fun base64ToBitmap(base64: String): Bitmap? {
|
|||||||
val bytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
val bytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
||||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to decode base64 to bitmap: ${e.message}")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.rosetta.messenger.ui.chats.components
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -817,7 +816,6 @@ fun ReplyBubble(
|
|||||||
imageBitmap = decoded
|
imageBitmap = decoded
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ReplyBubble", "Failed to load image: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1173,7 +1171,6 @@ fun ReplyImagePreview(
|
|||||||
fullImageBitmap = decoded
|
fullImageBitmap = decoded
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ReplyImagePreview", "Failed to load full image: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Intent
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -692,18 +691,15 @@ private suspend fun saveEditedImage(
|
|||||||
saveSettings,
|
saveSettings,
|
||||||
object : PhotoEditor.OnSaveListener {
|
object : PhotoEditor.OnSaveListener {
|
||||||
override fun onSuccess(imagePath: String) {
|
override fun onSuccess(imagePath: String) {
|
||||||
Log.d(TAG, "Image saved to: $imagePath")
|
|
||||||
onResult(Uri.fromFile(File(imagePath)))
|
onResult(Uri.fromFile(File(imagePath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(exception: Exception) {
|
override fun onFailure(exception: Exception) {
|
||||||
Log.e(TAG, "Failed to save image", exception)
|
|
||||||
onResult(null)
|
onResult(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error saving image", e)
|
|
||||||
withContext(Dispatchers.Main) { onResult(null) }
|
withContext(Dispatchers.Main) { onResult(null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,19 +729,16 @@ private suspend fun saveEditedImageSync(context: Context, photoEditor: PhotoEdit
|
|||||||
saveSettings,
|
saveSettings,
|
||||||
object : PhotoEditor.OnSaveListener {
|
object : PhotoEditor.OnSaveListener {
|
||||||
override fun onSuccess(imagePath: String) {
|
override fun onSuccess(imagePath: String) {
|
||||||
Log.d(TAG, "Image saved sync to: $imagePath")
|
|
||||||
continuation.resume(Uri.fromFile(File(imagePath)))
|
continuation.resume(Uri.fromFile(File(imagePath)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(exception: Exception) {
|
override fun onFailure(exception: Exception) {
|
||||||
Log.e(TAG, "Failed to save image sync", exception)
|
|
||||||
continuation.resume(null)
|
continuation.resume(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error saving image sync", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -829,7 +822,6 @@ private fun launchCrop(
|
|||||||
|
|
||||||
launcher.launch(intent)
|
launcher.launch(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error launching crop", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -952,7 +944,6 @@ fun MultiImageEditorScreen(
|
|||||||
photoEditors[page] = editor
|
photoEditors[page] = editor
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error loading image for page $page", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -973,7 +964,6 @@ fun MultiImageEditorScreen(
|
|||||||
view.source.setImageBitmap(bitmap)
|
view.source.setImageBitmap(bitmap)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error reloading image", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1297,7 +1287,6 @@ private fun AsyncImageLoader(uri: Uri, modifier: Modifier = Modifier) {
|
|||||||
bitmap = BitmapFactory.decodeStream(inputStream)
|
bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
inputStream?.close()
|
inputStream?.close()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error loading image", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.rosetta.messenger.ui.chats.components
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
@@ -341,7 +340,6 @@ private fun ZoomableImage(
|
|||||||
previewBitmap = base64ToBitmapSafe(blurPart)
|
previewBitmap = base64ToBitmapSafe(blurPart)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load preview: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +375,6 @@ private fun ZoomableImage(
|
|||||||
// 3. Скачиваем с CDN
|
// 3. Скачиваем с CDN
|
||||||
val downloadTag = getDownloadTag(image.preview)
|
val downloadTag = getDownloadTag(image.preview)
|
||||||
if (downloadTag.isNotEmpty()) {
|
if (downloadTag.isNotEmpty()) {
|
||||||
Log.d(TAG, "📥 Downloading image from CDN: ${image.attachmentId}")
|
|
||||||
|
|
||||||
val encryptedContent = TransportManager.downloadFile(image.attachmentId, downloadTag)
|
val encryptedContent = TransportManager.downloadFile(image.attachmentId, downloadTag)
|
||||||
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(image.chachaKey, privateKey)
|
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(image.chachaKey, privateKey)
|
||||||
@@ -405,7 +402,6 @@ private fun ZoomableImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load image: ${e.message}", e)
|
|
||||||
loadError = e.message
|
loadError = e.message
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +594,6 @@ private fun base64ToBitmapSafe(base64String: String): Bitmap? {
|
|||||||
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
||||||
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to decode base64 to bitmap: ${e.message}")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
@@ -918,9 +917,7 @@ private suspend fun loadMediaItems(context: Context): List<MediaItem> = withCont
|
|||||||
// Sort all items by date
|
// Sort all items by date
|
||||||
items.sortByDescending { it.dateModified }
|
items.sortByDescending { it.dateModified }
|
||||||
|
|
||||||
Log.d(TAG, "📸 Loaded ${items.size} media items")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to load media items", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items
|
items
|
||||||
|
|||||||
@@ -68,20 +68,16 @@ fun AvatarImage(
|
|||||||
|
|
||||||
// Логируем для отладки
|
// Логируем для отладки
|
||||||
LaunchedEffect(publicKey, avatars) {
|
LaunchedEffect(publicKey, avatars) {
|
||||||
android.util.Log.d("AvatarImage", "📸 publicKey=${publicKey.take(16)}... avatars=${avatars.size} repository=${avatarRepository != null}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Декодируем первый аватар
|
// Декодируем первый аватар
|
||||||
LaunchedEffect(avatars) {
|
LaunchedEffect(avatars) {
|
||||||
bitmap = if (avatars.isNotEmpty()) {
|
bitmap = if (avatars.isNotEmpty()) {
|
||||||
android.util.Log.d("AvatarImage", "🔄 Decoding avatar for ${publicKey.take(16)}... base64 length=${avatars.first().base64Data?.length ?: 0}")
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val result = AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
val result = AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
||||||
android.util.Log.d("AvatarImage", "✅ Decoded bitmap for ${publicKey.take(16)}... result=${result != null} size=${result?.width}x${result?.height}")
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.d("AvatarImage", "⚠️ No avatars for ${publicKey.take(16)}...")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +97,6 @@ fun AvatarImage(
|
|||||||
) {
|
) {
|
||||||
// Log what we're showing
|
// Log what we're showing
|
||||||
LaunchedEffect(bitmap) {
|
LaunchedEffect(bitmap) {
|
||||||
android.util.Log.d("AvatarImage", "🖼️ Showing for ${publicKey.take(16)}... bitmap=${bitmap != null}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
|
|||||||
@@ -232,7 +232,6 @@ fun OtherProfileScreen(
|
|||||||
val account = viewModel.myPublicKey ?: return@launch
|
val account = viewModel.myPublicKey ?: return@launch
|
||||||
database.dialogDao().deleteDialog(account, user.publicKey)
|
database.dialogDao().deleteDialog(account, user.publicKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("OtherProfileScreen", "Failed to delete dialog", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -191,13 +190,11 @@ fun ProfileScreen(
|
|||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
Log.d(TAG, "✂️ Crop result: resultCode=${result.resultCode}")
|
|
||||||
|
|
||||||
val croppedUri = ImageCropHelper.getCroppedImageUri(result)
|
val croppedUri = ImageCropHelper.getCroppedImageUri(result)
|
||||||
val error = ImageCropHelper.getCropError(result)
|
val error = ImageCropHelper.getCropError(result)
|
||||||
|
|
||||||
if (croppedUri != null) {
|
if (croppedUri != null) {
|
||||||
Log.d(TAG, "✅ Cropped image URI: $croppedUri")
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
// Читаем обрезанное изображение
|
// Читаем обрезанное изображение
|
||||||
@@ -205,10 +202,8 @@ fun ProfileScreen(
|
|||||||
val imageBytes = inputStream?.readBytes()
|
val imageBytes = inputStream?.readBytes()
|
||||||
inputStream?.close()
|
inputStream?.close()
|
||||||
|
|
||||||
Log.d(TAG, "📊 Cropped image bytes: ${imageBytes?.size ?: 0} bytes")
|
|
||||||
|
|
||||||
if (imageBytes != null) {
|
if (imageBytes != null) {
|
||||||
Log.d(TAG, "🔄 Converting cropped image to PNG Base64...")
|
|
||||||
val base64Png =
|
val base64Png =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
AvatarFileManager.imagePrepareForNetworkTransfer(
|
AvatarFileManager.imagePrepareForNetworkTransfer(
|
||||||
@@ -217,12 +212,10 @@ fun ProfileScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "✅ Converted to Base64: ${base64Png.length} chars")
|
|
||||||
|
|
||||||
// Сохраняем аватар через репозиторий
|
// Сохраняем аватар через репозиторий
|
||||||
avatarRepository?.changeMyAvatar(base64Png)
|
avatarRepository?.changeMyAvatar(base64Png)
|
||||||
|
|
||||||
Log.d(TAG, "🎉 Avatar update completed")
|
|
||||||
|
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
context,
|
context,
|
||||||
@@ -232,7 +225,6 @@ fun ProfileScreen(
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to process cropped avatar", e)
|
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
context,
|
context,
|
||||||
"Failed to update avatar: ${e.message}",
|
"Failed to update avatar: ${e.message}",
|
||||||
@@ -242,7 +234,6 @@ fun ProfileScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (error != null) {
|
} else if (error != null) {
|
||||||
Log.e(TAG, "❌ Crop error", error)
|
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
context,
|
context,
|
||||||
"Failed to crop image: ${error.message}",
|
"Failed to crop image: ${error.message}",
|
||||||
@@ -250,7 +241,6 @@ fun ProfileScreen(
|
|||||||
)
|
)
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "⚠️ Crop cancelled")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,13 +248,11 @@ fun ProfileScreen(
|
|||||||
val imagePickerLauncher =
|
val imagePickerLauncher =
|
||||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
||||||
uri: Uri? ->
|
uri: Uri? ->
|
||||||
Log.d(TAG, "🖼️ Image picker result: uri=$uri")
|
|
||||||
uri?.let {
|
uri?.let {
|
||||||
// Запускаем uCrop для обрезки
|
// Запускаем uCrop для обрезки
|
||||||
val cropIntent = ImageCropHelper.createCropIntent(context, it, isDarkTheme)
|
val cropIntent = ImageCropHelper.createCropIntent(context, it, isDarkTheme)
|
||||||
cropLauncher.launch(cropIntent)
|
cropLauncher.launch(cropIntent)
|
||||||
}
|
}
|
||||||
?: Log.w(TAG, "⚠️ URI is null, image picker cancelled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Цвета в зависимости от темы
|
// Цвета в зависимости от темы
|
||||||
@@ -749,20 +737,16 @@ private fun CollapsingProfileHeader(
|
|||||||
var avatarBitmap by remember(avatars) { mutableStateOf<android.graphics.Bitmap?>(null) }
|
var avatarBitmap by remember(avatars) { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||||
var dominantColor by remember { mutableStateOf<Color?>(null) }
|
var dominantColor by remember { mutableStateOf<Color?>(null) }
|
||||||
|
|
||||||
Log.d(TAG, "🎨 CollapsingProfileHeader: hasAvatar=$hasAvatar, avatars.size=${avatars.size}, dominantColor=$dominantColor")
|
|
||||||
|
|
||||||
LaunchedEffect(avatars, publicKey) {
|
LaunchedEffect(avatars, publicKey) {
|
||||||
Log.d(TAG, "🎨 LaunchedEffect avatars: size=${avatars.size}, publicKey=${publicKey.take(8)}")
|
|
||||||
if (avatars.isNotEmpty()) {
|
if (avatars.isNotEmpty()) {
|
||||||
val loadedBitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
val loadedBitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||||
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
||||||
}
|
}
|
||||||
avatarBitmap = loadedBitmap
|
avatarBitmap = loadedBitmap
|
||||||
Log.d(TAG, "🎨 Bitmap loaded: ${loadedBitmap?.width}x${loadedBitmap?.height}")
|
|
||||||
// Извлекаем доминантный цвет из нижней части аватарки (где будет текст)
|
// Извлекаем доминантный цвет из нижней части аватарки (где будет текст)
|
||||||
loadedBitmap?.let { bitmap ->
|
loadedBitmap?.let { bitmap ->
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "🎨 Extracting color from bitmap ${bitmap.width}x${bitmap.height}")
|
|
||||||
// Берем нижнюю треть изображения для более точного определения
|
// Берем нижнюю треть изображения для более точного определения
|
||||||
val bottomThird =
|
val bottomThird =
|
||||||
android.graphics.Bitmap.createBitmap(
|
android.graphics.Bitmap.createBitmap(
|
||||||
@@ -772,20 +756,15 @@ private fun CollapsingProfileHeader(
|
|||||||
bitmap.width,
|
bitmap.width,
|
||||||
(bitmap.height / 3).coerceAtLeast(1)
|
(bitmap.height / 3).coerceAtLeast(1)
|
||||||
)
|
)
|
||||||
Log.d(TAG, "🎨 Bottom third: ${bottomThird.width}x${bottomThird.height}")
|
|
||||||
val palette = AndroidPalette.from(bottomThird).generate()
|
val palette = AndroidPalette.from(bottomThird).generate()
|
||||||
Log.d(TAG, "🎨 Palette generated, dominantSwatch=${palette.dominantSwatch}, mutedSwatch=${palette.mutedSwatch}")
|
|
||||||
// Используем доминантный цвет или muted swatch
|
// Используем доминантный цвет или muted swatch
|
||||||
val swatch = palette.dominantSwatch ?: palette.mutedSwatch
|
val swatch = palette.dominantSwatch ?: palette.mutedSwatch
|
||||||
if (swatch != null) {
|
if (swatch != null) {
|
||||||
val extractedColor = Color(swatch.rgb)
|
val extractedColor = Color(swatch.rgb)
|
||||||
Log.d(TAG, "🎨 Extracted dominant color: R=${extractedColor.red}, G=${extractedColor.green}, B=${extractedColor.blue}, isLight=${isColorLight(extractedColor)}")
|
|
||||||
dominantColor = extractedColor
|
dominantColor = extractedColor
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "🎨 No swatch found in palette!")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to extract dominant color: ${e.message}", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -799,7 +778,6 @@ private fun CollapsingProfileHeader(
|
|||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (hasAvatar && dominantColor != null) {
|
if (hasAvatar && dominantColor != null) {
|
||||||
val isLight = isColorLight(dominantColor!!)
|
val isLight = isColorLight(dominantColor!!)
|
||||||
Log.d(TAG, "🎨 Text color: hasAvatar=$hasAvatar, dominantColor=$dominantColor, isLight=$isLight")
|
|
||||||
if (isLight) Color.Black else Color.White
|
if (isLight) Color.Black else Color.White
|
||||||
} else {
|
} else {
|
||||||
if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White
|
if (isColorLight(avatarColors.backgroundColor)) Color.Black else Color.White
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger.ui.settings
|
package com.rosetta.messenger.ui.settings
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
@@ -52,7 +51,6 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
val timestamp = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
|
val timestamp = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
|
||||||
val logMessage = "[$timestamp] $message"
|
val logMessage = "[$timestamp] $message"
|
||||||
_state.value = _state.value.copy(logs = _state.value.logs + logMessage)
|
_state.value = _state.value.copy(logs = _state.value.logs + logMessage)
|
||||||
Log.d("ProfileViewModel", logMessage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,15 +97,10 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
addLog(" - privateKey: ${privateKeyHash.take(16)}... (length: ${privateKeyHash.length})")
|
addLog(" - privateKey: ${privateKeyHash.take(16)}... (length: ${privateKeyHash.length})")
|
||||||
|
|
||||||
// CRITICAL: Log full packet data for debugging
|
// CRITICAL: Log full packet data for debugging
|
||||||
Log.d("ProfileViewModel", "📤 SENDING PacketUserInfo:")
|
|
||||||
Log.d("ProfileViewModel", " username='$username' (${username.length} chars)")
|
|
||||||
Log.d("ProfileViewModel", " title='$actualTitle' (${actualTitle.length} chars)")
|
|
||||||
Log.d("ProfileViewModel", " privateKey='${privateKeyHash.take(32)}...' (${privateKeyHash.length} chars)")
|
|
||||||
|
|
||||||
addLog("Sending packet to server...")
|
addLog("Sending packet to server...")
|
||||||
ProtocolManager.send(packet)
|
ProtocolManager.send(packet)
|
||||||
addLog("Packet sent successfully")
|
addLog("Packet sent successfully")
|
||||||
Log.d("ProfileViewModel", "✅ Packet sent to ProtocolManager")
|
|
||||||
addLog("Waiting for PacketResult (0x02) from server...")
|
addLog("Waiting for PacketResult (0x02) from server...")
|
||||||
|
|
||||||
// Set timeout for server response
|
// Set timeout for server response
|
||||||
@@ -135,7 +128,6 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
// Wait for response (handled in handlePacketResult)
|
// Wait for response (handled in handlePacketResult)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ProfileViewModel", "Error saving profile", e)
|
|
||||||
addLog("❌ ERROR: ${e.message}")
|
addLog("❌ ERROR: ${e.message}")
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
@@ -191,9 +183,7 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
try {
|
try {
|
||||||
accountManager.updateAccountName(publicKey, name)
|
accountManager.updateAccountName(publicKey, name)
|
||||||
accountManager.updateAccountUsername(publicKey, username)
|
accountManager.updateAccountUsername(publicKey, username)
|
||||||
Log.d("ProfileViewModel", "Local profile updated: name=$name, username=$username")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ProfileViewModel", "Error updating local profile", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger.utils
|
package com.rosetta.messenger.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -53,12 +52,10 @@ object AttachmentFileManager {
|
|||||||
): Boolean {
|
): Boolean {
|
||||||
return try {
|
return try {
|
||||||
if (blob.isEmpty()) {
|
if (blob.isEmpty()) {
|
||||||
Log.w(TAG, "💾 Empty blob, skipping save")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val filePath = generatePath(attachmentId, publicKey)
|
val filePath = generatePath(attachmentId, publicKey)
|
||||||
Log.d(TAG, "💾 Saving attachment: $attachmentId -> $filePath")
|
|
||||||
|
|
||||||
// Шифруем данные приватным ключом (как в desktop)
|
// Шифруем данные приватным ключом (как в desktop)
|
||||||
val encrypted = CryptoManager.encryptWithPassword(blob, privateKey)
|
val encrypted = CryptoManager.encryptWithPassword(blob, privateKey)
|
||||||
@@ -73,7 +70,6 @@ object AttachmentFileManager {
|
|||||||
subDir.mkdirs()
|
subDir.mkdirs()
|
||||||
val file = File(subDir, parts[1])
|
val file = File(subDir, parts[1])
|
||||||
file.writeText(encrypted)
|
file.writeText(encrypted)
|
||||||
Log.d(TAG, "💾 Saved to: ${file.absolutePath} (${encrypted.length} bytes)")
|
|
||||||
} else {
|
} else {
|
||||||
val file = File(dir, filePath)
|
val file = File(dir, filePath)
|
||||||
file.writeText(encrypted)
|
file.writeText(encrypted)
|
||||||
@@ -81,7 +77,6 @@ object AttachmentFileManager {
|
|||||||
|
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Error saving attachment", e)
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,17 +101,14 @@ object AttachmentFileManager {
|
|||||||
val file = File(dir, filePath)
|
val file = File(dir, filePath)
|
||||||
|
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Log.d(TAG, "📖 File not found: $filePath")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val encrypted = file.readText()
|
val encrypted = file.readText()
|
||||||
val decrypted = CryptoManager.decryptWithPassword(encrypted, privateKey)
|
val decrypted = CryptoManager.decryptWithPassword(encrypted, privateKey)
|
||||||
|
|
||||||
Log.d(TAG, "📖 Read attachment: $attachmentId (${decrypted?.length ?: 0} bytes)")
|
|
||||||
decrypted
|
decrypted
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Error reading attachment", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +145,6 @@ object AttachmentFileManager {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Error deleting attachment", e)
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,10 +156,8 @@ object AttachmentFileManager {
|
|||||||
return try {
|
return try {
|
||||||
val dir = File(context.filesDir, ATTACHMENTS_DIR)
|
val dir = File(context.filesDir, ATTACHMENTS_DIR)
|
||||||
dir.deleteRecursively()
|
dir.deleteRecursively()
|
||||||
Log.d(TAG, "🗑️ Cache cleared")
|
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Error clearing cache", e)
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,23 +36,16 @@ object AvatarFileManager {
|
|||||||
* @return Путь к файлу (формат: "a/md5hash")
|
* @return Путь к файлу (формат: "a/md5hash")
|
||||||
*/
|
*/
|
||||||
fun saveAvatar(context: Context, base64Image: String, entityId: String): String {
|
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 версии
|
// Генерируем путь как в desktop версии
|
||||||
val filePath = generateMd5Path(base64Image, entityId)
|
val filePath = generateMd5Path(base64Image, entityId)
|
||||||
android.util.Log.d("AvatarFileManager", "🔗 Generated file path: $filePath")
|
|
||||||
|
|
||||||
// Шифруем данные с паролем "rosetta-a"
|
// Шифруем данные с паролем "rosetta-a"
|
||||||
android.util.Log.d("AvatarFileManager", "🔐 Encrypting with password...")
|
|
||||||
val encrypted = CryptoManager.encryptWithPassword(base64Image, AVATAR_PASSWORD)
|
val encrypted = CryptoManager.encryptWithPassword(base64Image, AVATAR_PASSWORD)
|
||||||
android.util.Log.d("AvatarFileManager", "✅ Encrypted length: ${encrypted.length}")
|
|
||||||
|
|
||||||
// Сохраняем в файловую систему
|
// Сохраняем в файловую систему
|
||||||
val dir = File(context.filesDir, AVATAR_DIR)
|
val dir = File(context.filesDir, AVATAR_DIR)
|
||||||
dir.mkdirs()
|
dir.mkdirs()
|
||||||
android.util.Log.d("AvatarFileManager", "📁 Base dir: ${dir.absolutePath}")
|
|
||||||
|
|
||||||
// Путь формата "a/md5hash" -> создаем подпапку "a"
|
// Путь формата "a/md5hash" -> создаем подпапку "a"
|
||||||
val parts = filePath.split("/")
|
val parts = filePath.split("/")
|
||||||
@@ -61,14 +54,11 @@ object AvatarFileManager {
|
|||||||
subDir.mkdirs()
|
subDir.mkdirs()
|
||||||
val file = File(subDir, parts[1])
|
val file = File(subDir, parts[1])
|
||||||
file.writeText(encrypted)
|
file.writeText(encrypted)
|
||||||
android.util.Log.d("AvatarFileManager", "💾 Saved to: ${file.absolutePath}")
|
|
||||||
} else {
|
} else {
|
||||||
val file = File(dir, filePath)
|
val file = File(dir, filePath)
|
||||||
file.writeText(encrypted)
|
file.writeText(encrypted)
|
||||||
android.util.Log.d("AvatarFileManager", "💾 Saved to: ${file.absolutePath}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android.util.Log.d("AvatarFileManager", "✅ Avatar saved successfully")
|
|
||||||
return filePath
|
return filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +153,6 @@ object AvatarFileManager {
|
|||||||
subDir.mkdirs()
|
subDir.mkdirs()
|
||||||
val file = File(subDir, parts[1])
|
val file = File(subDir, parts[1])
|
||||||
file.writeText(encrypted)
|
file.writeText(encrypted)
|
||||||
android.util.Log.d("AvatarFileManager", "💾 Avatar saved to: ${file.absolutePath}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
return path
|
||||||
@@ -232,19 +221,14 @@ object AvatarFileManager {
|
|||||||
return try {
|
return try {
|
||||||
// Check for data URI prefix
|
// Check for data URI prefix
|
||||||
val actualBase64 = if (base64.contains(",")) {
|
val actualBase64 = if (base64.contains(",")) {
|
||||||
android.util.Log.d("AvatarFileManager", "🔍 Removing data URI prefix, orig len=${base64.length}")
|
|
||||||
base64.substringAfter(",")
|
base64.substringAfter(",")
|
||||||
} else {
|
} else {
|
||||||
base64
|
base64
|
||||||
}
|
}
|
||||||
android.util.Log.d("AvatarFileManager", "🔍 Decoding base64, len=${actualBase64.length}, prefix=${actualBase64.take(50)}")
|
|
||||||
val imageBytes = Base64.decode(actualBase64, Base64.NO_WRAP)
|
val imageBytes = Base64.decode(actualBase64, Base64.NO_WRAP)
|
||||||
android.util.Log.d("AvatarFileManager", "🔍 Decoded bytes=${imageBytes.size}")
|
|
||||||
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||||
android.util.Log.d("AvatarFileManager", "🔍 Bitmap result=${bitmap != null}, size=${bitmap?.width}x${bitmap?.height}")
|
|
||||||
bitmap
|
bitmap
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("AvatarFileManager", "❌ base64ToBitmap error: ${e.message}")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.rosetta.messenger.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
@@ -34,7 +33,6 @@ class CrashReportManager private constructor(private val context: Context) : Thr
|
|||||||
if (INSTANCE == null) {
|
if (INSTANCE == null) {
|
||||||
INSTANCE = CrashReportManager(context.applicationContext)
|
INSTANCE = CrashReportManager(context.applicationContext)
|
||||||
Thread.setDefaultUncaughtExceptionHandler(INSTANCE)
|
Thread.setDefaultUncaughtExceptionHandler(INSTANCE)
|
||||||
Log.d(TAG, "Crash reporter initialized")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +85,6 @@ class CrashReportManager private constructor(private val context: Context) : Thr
|
|||||||
try {
|
try {
|
||||||
saveCrashReport(thread, throwable)
|
saveCrashReport(thread, throwable)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error saving crash report", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вызываем дефолтный handler (чтобы система тоже обработала краш)
|
// Вызываем дефолтный handler (чтобы система тоже обработала краш)
|
||||||
@@ -111,7 +108,6 @@ class CrashReportManager private constructor(private val context: Context) : Thr
|
|||||||
val report = buildCrashReport(thread, throwable)
|
val report = buildCrashReport(thread, throwable)
|
||||||
crashFile.writeText(report)
|
crashFile.writeText(report)
|
||||||
|
|
||||||
Log.d(TAG, "Crash report saved: $fileName")
|
|
||||||
|
|
||||||
// Удаляем старые файлы если их слишком много
|
// Удаляем старые файлы если их слишком много
|
||||||
cleanupOldCrashFiles(crashDir)
|
cleanupOldCrashFiles(crashDir)
|
||||||
@@ -199,7 +195,6 @@ class CrashReportManager private constructor(private val context: Context) : Thr
|
|||||||
if (files.size > MAX_CRASH_FILES) {
|
if (files.size > MAX_CRASH_FILES) {
|
||||||
files.drop(MAX_CRASH_FILES).forEach { file ->
|
files.drop(MAX_CRASH_FILES).forEach { file ->
|
||||||
file.delete()
|
file.delete()
|
||||||
Log.d(TAG, "Deleted old crash file: ${file.name}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import com.vanniktech.blurhash.BlurHash
|
import com.vanniktech.blurhash.BlurHash
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -33,7 +32,6 @@ object MediaUtils {
|
|||||||
*/
|
*/
|
||||||
suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
|
suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "📸 Converting image to Base64: $uri")
|
|
||||||
|
|
||||||
// Открываем InputStream
|
// Открываем InputStream
|
||||||
val inputStream: InputStream = context.contentResolver.openInputStream(uri)
|
val inputStream: InputStream = context.contentResolver.openInputStream(uri)
|
||||||
@@ -44,11 +42,9 @@ object MediaUtils {
|
|||||||
inputStream.close()
|
inputStream.close()
|
||||||
|
|
||||||
if (originalBitmap == null) {
|
if (originalBitmap == null) {
|
||||||
Log.e(TAG, "📸 Failed to decode image")
|
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📸 Original size: ${originalBitmap.width}x${originalBitmap.height}")
|
|
||||||
|
|
||||||
// Масштабируем если слишком большое
|
// Масштабируем если слишком большое
|
||||||
val scaledBitmap = scaleDownBitmap(originalBitmap, MAX_IMAGE_SIZE)
|
val scaledBitmap = scaleDownBitmap(originalBitmap, MAX_IMAGE_SIZE)
|
||||||
@@ -56,7 +52,6 @@ object MediaUtils {
|
|||||||
originalBitmap.recycle()
|
originalBitmap.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📸 Scaled size: ${scaledBitmap.width}x${scaledBitmap.height}")
|
|
||||||
|
|
||||||
// Конвертируем в PNG Base64
|
// Конвертируем в PNG Base64
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
@@ -67,10 +62,8 @@ object MediaUtils {
|
|||||||
|
|
||||||
scaledBitmap.recycle()
|
scaledBitmap.recycle()
|
||||||
|
|
||||||
Log.d(TAG, "📸 ✅ Image converted to Base64, length: ${base64.length}")
|
|
||||||
base64
|
base64
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📸 ❌ Failed to convert image to Base64", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +73,6 @@ object MediaUtils {
|
|||||||
*/
|
*/
|
||||||
suspend fun generateBlurhash(context: Context, uri: Uri): String = withContext(Dispatchers.IO) {
|
suspend fun generateBlurhash(context: Context, uri: Uri): String = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "🎨 Generating blurhash for: $uri")
|
|
||||||
|
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
?: return@withContext ""
|
?: return@withContext ""
|
||||||
@@ -93,7 +85,6 @@ object MediaUtils {
|
|||||||
inputStream.close()
|
inputStream.close()
|
||||||
|
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
Log.e(TAG, "🎨 Failed to decode image for blurhash")
|
|
||||||
return@withContext ""
|
return@withContext ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +92,8 @@ object MediaUtils {
|
|||||||
val blurhash = BlurHash.encode(bitmap, 4, 3)
|
val blurhash = BlurHash.encode(bitmap, 4, 3)
|
||||||
bitmap.recycle()
|
bitmap.recycle()
|
||||||
|
|
||||||
Log.d(TAG, "🎨 ✅ Blurhash generated: $blurhash")
|
|
||||||
blurhash ?: ""
|
blurhash ?: ""
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "🎨 ❌ Failed to generate blurhash", e)
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +103,6 @@ object MediaUtils {
|
|||||||
*/
|
*/
|
||||||
suspend fun uriToBase64File(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
|
suspend fun uriToBase64File(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "📄 Converting file to Base64: $uri")
|
|
||||||
|
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
?: return@withContext null
|
?: return@withContext null
|
||||||
@@ -124,10 +112,8 @@ object MediaUtils {
|
|||||||
|
|
||||||
val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||||||
|
|
||||||
Log.d(TAG, "📄 ✅ File converted to Base64, length: ${base64.length}")
|
|
||||||
base64
|
base64
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📄 ❌ Failed to convert file to Base64", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +144,6 @@ object MediaUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to get file name", e)
|
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
@@ -212,7 +197,6 @@ object MediaUtils {
|
|||||||
}
|
}
|
||||||
hash ?: ""
|
hash ?: ""
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to generate blurhash from bitmap", e)
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,10 +228,8 @@ object MediaUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "📐 Image dimensions: ${width}x${height}")
|
|
||||||
Pair(width, height)
|
Pair(width, height)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📐 Failed to get image dimensions", e)
|
|
||||||
Pair(0, 0)
|
Pair(0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,10 +252,8 @@ object MediaUtils {
|
|||||||
}
|
}
|
||||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
|
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
|
||||||
|
|
||||||
Log.d(TAG, "📐 Image dimensions from base64: ${options.outWidth}x${options.outHeight}")
|
|
||||||
Pair(options.outWidth, options.outHeight)
|
Pair(options.outWidth, options.outHeight)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "📐 Failed to get image dimensions from base64", e)
|
|
||||||
Pair(0, 0)
|
Pair(0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user