From 70ed7fbc6ea02841e06f81f572ba6b16045a4ecf Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 11 Jan 2026 16:19:44 +0500 Subject: [PATCH 1/3] feat: Fix WebSocket connection issues after account registration by ensuring connection before authentication --- FIX_WEBSOCKET_CONNECT.md | 69 +++++++++++++++++++ .../rosetta/messenger/providers/AuthState.kt | 16 ++++- .../messenger/ui/auth/SetPasswordScreen.kt | 3 + .../rosetta/messenger/ui/auth/UnlockScreen.kt | 3 + 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 FIX_WEBSOCKET_CONNECT.md diff --git a/FIX_WEBSOCKET_CONNECT.md b/FIX_WEBSOCKET_CONNECT.md new file mode 100644 index 0000000..30f5202 --- /dev/null +++ b/FIX_WEBSOCKET_CONNECT.md @@ -0,0 +1,69 @@ +# Fix: WebSocket Connection After Registration + +## Проблема + +После первой регистрации аккаунта в Android приложении не работали запросы на сервер до перезапуска приложения. + +## Причина + +При регистрации или разблокировке аккаунта вызывался `ProtocolManager.authenticate()` до того, как WebSocket соединение было установлено. + +Последовательность была следующая: + +1. Создание/разблокировка аккаунта +2. Вызов `ProtocolManager.authenticate(publicKey, privateKeyHash)` +3. `authenticate()` вызывает `getProtocol().startHandshake()` +4. `startHandshake()` проверяет что не подключено и вызывает `connect()`, но затем сразу возвращается +5. Соединение устанавливается асинхронно, но handshake не происходит вовремя +6. Приложение переходит к экрану чатов, запросы не проходят + +После перезапуска приложения `ProtocolManager.connect()` вызывался в `ChatsListScreen`, поэтому все работало нормально. + +## Решение + +Добавлен явный вызов `ProtocolManager.connect()` перед `authenticate()` в следующих местах: + +### 1. AuthState.kt + +- Метод `createAccount()` - после создания аккаунта +- Метод `unlock()` - после разблокировки + +### 2. SetPasswordScreen.kt + +- При создании нового аккаунта после установки пароля + +### 3. UnlockScreen.kt + +- При разблокировке существующего аккаунта + +Добавлена задержка 500ms после `connect()` для того, чтобы WebSocket успел установить соединение до попытки аутентификации. + +## Измененные файлы + +- `app/src/main/java/com/rosetta/messenger/providers/AuthState.kt` +- `app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt` +- `app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt` + +## Код изменений + +```kotlin +// Было: +ProtocolManager.authenticate(publicKey, privateKeyHash) + +// Стало: +ProtocolManager.connect() +kotlinx.coroutines.delay(500) // Даём время на установку соединения +ProtocolManager.authenticate(publicKey, privateKeyHash) +``` + +## Тестирование + +После этого исправления: + +1. Зарегистрируйте новый аккаунт +2. Сразу после регистрации попробуйте отправить сообщение или выполнить другой запрос +3. Запросы должны проходить без перезапуска приложения + +## Дата + +11 января 2026 diff --git a/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt b/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt index 9b7bc52..5893ba8 100644 --- a/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt +++ b/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt @@ -175,7 +175,13 @@ class AuthStateManager( loadAccounts() - // Step 8: Authenticate with protocol + // Step 8: Connect and authenticate with protocol + Log.d(TAG, "🌐 Connecting to protocol server...") + ProtocolManager.connect() + + // Give WebSocket time to connect before authenticating + kotlinx.coroutines.delay(500) + Log.d(TAG, "🌐 Authenticating with protocol server...") ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash) @@ -236,7 +242,13 @@ class AuthStateManager( status = AuthStatus.Authenticated(decryptedAccount) )} - // Authenticate with protocol + // Connect and authenticate with protocol + Log.d(TAG, "🌐 Connecting to protocol server...") + ProtocolManager.connect() + + // Give WebSocket time to connect before authenticating + kotlinx.coroutines.delay(500) + Log.d(TAG, "🌐 Authenticating with protocol server...") ProtocolManager.authenticate(decryptedAccount.publicKey, decryptedAccount.privateKeyHash) diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt index 91e1f24..60bbebf 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt @@ -497,6 +497,9 @@ fun SetPasswordScreen( // 🔌 Connect to server and authenticate val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey) Log.d("SetPasswordScreen", "🔌 Connecting to server...") + ProtocolManager.connect() + // Give WebSocket time to connect before authenticating + kotlinx.coroutines.delay(500) ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash) onAccountCreated() diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index 8e3551c..50c87fd 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -581,6 +581,9 @@ fun UnlockScreen( // Connect to server and authenticate Log.d("UnlockScreen", "Connecting to server...") + ProtocolManager.connect() + // Give WebSocket time to connect before authenticating + kotlinx.coroutines.delay(500) ProtocolManager.authenticate(account.publicKey, privateKeyHash) accountManager.setCurrentAccount(account.publicKey) From 161a4fe61b180ba81c45db7fb8a6715fb2e3fc37 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 11 Jan 2026 16:44:58 +0500 Subject: [PATCH 2/3] feat: Update account creation flow to return DecryptedAccount and configure kapt for Room --- app/build.gradle.kts | 3 ++- .../java/com/rosetta/messenger/ui/auth/AuthFlow.kt | 2 +- .../rosetta/messenger/ui/auth/SetPasswordScreen.kt | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7a90bd3..f5237c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("kotlin-kapt") } android { @@ -79,7 +80,7 @@ dependencies { // Room for database implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") - annotationProcessor("androidx.room:room-compiler:2.6.1") + kapt("androidx.room:room-compiler:2.6.1") // Biometric authentication implementation("androidx.biometric:biometric:1.1.0") diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt index 8c11943..a1964c8 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/AuthFlow.kt @@ -130,7 +130,7 @@ fun AuthFlow( currentScreen = AuthScreen.CONFIRM_SEED } }, - onAccountCreated = { onAuthComplete(null) } + onAccountCreated = { account -> onAuthComplete(account) } ) } diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt index 60bbebf..352d285 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.rosetta.messenger.crypto.CryptoManager import com.rosetta.messenger.data.AccountManager +import com.rosetta.messenger.data.DecryptedAccount import com.rosetta.messenger.data.EncryptedAccount import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.ui.onboarding.PrimaryBlue @@ -38,7 +39,7 @@ fun SetPasswordScreen( seedPhrase: List, isDarkTheme: Boolean, onBack: () -> Unit, - onAccountCreated: () -> Unit + onAccountCreated: (DecryptedAccount) -> Unit ) { val themeAnimSpec = tween(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f)) val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec) @@ -502,7 +503,16 @@ fun SetPasswordScreen( kotlinx.coroutines.delay(500) ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash) - onAccountCreated() + // Create DecryptedAccount to pass to callback + val decryptedAccount = DecryptedAccount( + publicKey = keyPair.publicKey, + privateKey = keyPair.privateKey, + seedPhrase = seedPhrase, + privateKeyHash = privateKeyHash, + name = truncatedKey + ) + + onAccountCreated(decryptedAccount) } catch (e: Exception) { error = "Failed to create account: ${e.message}" isCreating = false From 304b57af30d85a02db4104dcc92bde07b9b32183 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 11 Jan 2026 16:54:03 +0500 Subject: [PATCH 3/3] feat: Implement heartbeat mechanism to maintain WebSocket connection --- .../com/rosetta/messenger/network/Protocol.kt | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index 7db47d4..045fba7 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -69,6 +69,9 @@ class Protocol( private var lastPublicKey: String? = null private var lastPrivateHash: String? = null + // Heartbeat + private var heartbeatJob: Job? = null + // Supported packets private val supportedPackets = mapOf( 0x00 to { PacketHandshake() }, @@ -92,6 +95,30 @@ class Protocol( handshakeComplete = true _state.value = ProtocolState.AUTHENTICATED flushPacketQueue() + + // Start heartbeat with interval from server + startHeartbeat(packet.heartbeatInterval) + } + } + } + + /** + * Start heartbeat to keep connection alive + */ + private fun startHeartbeat(intervalSeconds: Int) { + heartbeatJob?.cancel() + + val intervalMs = (intervalSeconds * 1000L) / 2 // Send at half the interval + log("💓 Starting heartbeat with interval: ${intervalSeconds}s (sending every ${intervalMs}ms)") + + heartbeatJob = scope.launch { + while (isActive) { + delay(intervalMs) + if (webSocket?.send("heartbeat") == true) { + log("💓 Heartbeat sent") + } else { + log("💔 Heartbeat failed to send") + } } } } @@ -100,13 +127,19 @@ class Protocol( * Initialize connection to server */ fun connect() { - if (_state.value == ProtocolState.CONNECTING || _state.value == ProtocolState.CONNECTED) { - log("Already connecting or connected") + if (_state.value == ProtocolState.CONNECTING) { + log("Already connecting, skipping...") + return + } + + // Allow reconnection even if connected (for manual reconnect) + if (_state.value == ProtocolState.CONNECTED || _state.value == ProtocolState.AUTHENTICATED) { + log("Already connected/authenticated, skipping...") return } isManuallyClosed = false - reconnectAttempts = 0 // Reset reconnect attempts on new connection + // Don't reset reconnectAttempts here - it's reset on successful connection in onOpen _state.value = ProtocolState.CONNECTING _lastError.value = null @@ -266,6 +299,7 @@ class Protocol( _state.value = ProtocolState.DISCONNECTED handshakeComplete = false handshakeJob?.cancel() + heartbeatJob?.cancel() if (!isManuallyClosed && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++ @@ -302,6 +336,7 @@ class Protocol( log("Disconnecting...") isManuallyClosed = true handshakeJob?.cancel() + heartbeatJob?.cancel() webSocket?.close(1000, "User disconnected") webSocket = null _state.value = ProtocolState.DISCONNECTED @@ -332,6 +367,7 @@ class Protocol( */ fun destroy() { disconnect() + heartbeatJob?.cancel() scope.cancel() } }