From 1ea2d917dcdb53290ef2518485c444944c712217 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 17 Jan 2026 02:14:16 +0500 Subject: [PATCH] feat: Enhance WebSocket connection handling; prevent duplicate connections and improve reconnection logic --- .../com/rosetta/messenger/MainActivity.kt | 56 ++++++++++---- .../com/rosetta/messenger/network/Protocol.kt | 76 +++++++++++++++---- 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 87dfe0b..9de4df6 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -1,10 +1,12 @@ package com.rosetta.messenger +import android.content.Context import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.lifecycle.lifecycleScope import androidx.compose.animation.* import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutLinearInEasing @@ -72,17 +74,7 @@ class MainActivity : ComponentActivity() { // 🔔 Инициализируем Firebase для push-уведомлений initializeFirebase() - // � Логируем текущий FCM токен при запуске - val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE) - val savedToken = prefs.getString("fcm_token", null) - if (savedToken != null) { - Log.d(TAG, "📱 Current FCM token on app start (short): ${savedToken.take(20)}...") - Log.d(TAG, "📱 Current FCM token on app start (FULL): $savedToken") - } else { - Log.d(TAG, "📱 No FCM token saved yet") - } - - // �🚀 Предзагружаем эмодзи в фоне для мгновенного открытия пикера + // � Предзагружаем эмодзи в фоне для мгновенного открытия пикера // Используем новый оптимизированный кэш OptimizedEmojiCache.preload(this) @@ -186,6 +178,10 @@ class MainActivity : ComponentActivity() { hasExistingAccount = true // Save as last logged account account?.let { accountManager.setLastLoggedPublicKey(it.publicKey) } + + // 📤 Отправляем FCM токен на сервер после успешной аутентификации + account?.let { sendFcmTokenToServer(it) } + // Reload accounts list scope.launch { val accounts = accountManager.getAllAccounts() @@ -268,8 +264,8 @@ class MainActivity : ComponentActivity() { // Сохраняем токен локально token?.let { saveFcmToken(it) } - // TODO: Отправляем токен на сервер если аккаунт залогинен - // token?.let { sendFcmTokenToServer(it) } + // Токен будет отправлен на сервер после успешной аутентификации + // (см. вызов sendFcmTokenToServer в onAccountLogin) } Log.d(TAG, "✅ Firebase initialized successfully") @@ -286,6 +282,40 @@ class MainActivity : ComponentActivity() { prefs.edit().putString("fcm_token", token).apply() Log.d(TAG, "💾 FCM token saved locally") } + + /** + * Отправить FCM токен на сервер + * Вызывается после успешной аутентификации, когда аккаунт уже расшифрован + */ + private fun sendFcmTokenToServer(account: DecryptedAccount) { + lifecycleScope.launch { + try { + val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE) + val token = prefs.getString("fcm_token", null) + + if (token == null) { + Log.d(TAG, "⚠️ Cannot send FCM token: Token not found") + return@launch + } + + Log.d(TAG, "📤 Sending FCM token to server...") + Log.d(TAG, " Token (short): ${token.take(20)}...") + Log.d(TAG, " PublicKey: ${account.publicKey.take(20)}...") + + val packet = PacketPushToken().apply { + this.privateKey = account.privateKey + this.publicKey = account.publicKey + this.pushToken = token + this.platform = "android" + } + + ProtocolManager.send(packet) + Log.d(TAG, "✅ FCM token sent to server") + } catch (e: Exception) { + Log.e(TAG, "❌ Error sending FCM token to server", e) + } + } + } } @Composable 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 a849982..4b06a46 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -53,6 +53,8 @@ class Protocol( private var reconnectAttempts = 0 private var lastStateChangeTime = System.currentTimeMillis() private var lastSuccessfulConnection = 0L + private var reconnectJob: Job? = null // Для отмены запланированных переподключений + private var isConnecting = false // Флаг для защиты от одновременных подключений private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -190,13 +192,22 @@ class Protocol( */ fun connect() { val currentState = _state.value - log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts") + log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting") - if (currentState == ProtocolState.CONNECTING) { + // КРИТИЧНО: проверяем флаг isConnecting, а не только состояние + if (isConnecting || currentState == ProtocolState.CONNECTING) { log("⚠️ Already connecting, skipping... (preventing duplicate connect)") return } + // Отменяем любые запланированные переподключения + reconnectJob?.cancel() + reconnectJob = null + log("🔄 Cancelled any pending reconnect jobs") + + // Устанавливаем флаг ПЕРЕД любыми операциями + isConnecting = true + reconnectAttempts++ log("📊 RECONNECT ATTEMPT #$reconnectAttempts") @@ -224,15 +235,24 @@ class Protocol( webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { log("✅ WebSocket OPEN: response=${response.code}, hasCredentials=${lastPublicKey != null}") + + // Сбрасываем флаг подключения + isConnecting = false + setState(ProtocolState.CONNECTED, "WebSocket onOpen callback") - // If we have saved credentials, start handshake automatically - lastPublicKey?.let { publicKey -> - lastPrivateHash?.let { privateHash -> - log("🤝 Auto-starting handshake with saved credentials") - startHandshake(publicKey, privateHash) - } - } ?: log("⚠️ No saved credentials, waiting for manual handshake") + // КРИТИЧНО: проверяем что не идет уже handshake + if (_state.value != ProtocolState.HANDSHAKING && _state.value != ProtocolState.AUTHENTICATED) { + // If we have saved credentials, start handshake automatically + lastPublicKey?.let { publicKey -> + lastPrivateHash?.let { privateHash -> + log("🤝 Auto-starting handshake with saved credentials") + startHandshake(publicKey, privateHash) + } + } ?: log("⚠️ No saved credentials, waiting for manual handshake") + } else { + log("⚠️ Skipping auto-handshake: already in state ${_state.value}") + } } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { @@ -250,6 +270,7 @@ class Protocol( override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { log("❌ WebSocket CLOSED: code=$code reason='$reason' state=${_state.value} manuallyClosed=$isManuallyClosed") + isConnecting = false // Сбрасываем флаг handleDisconnect() } @@ -260,6 +281,7 @@ class Protocol( log(" Manually closed: $isManuallyClosed") log(" Reconnect attempts: $reconnectAttempts") t.printStackTrace() + isConnecting = false // Сбрасываем флаг _lastError.value = t.message handleDisconnect() } @@ -273,12 +295,24 @@ class Protocol( log("🤝 Starting handshake...") log(" Public key: ${publicKey.take(20)}...") log(" Private hash: ${privateHash.take(20)}...") + log(" Current state: ${_state.value}") // Save credentials for reconnection lastPublicKey = publicKey lastPrivateHash = privateHash - if (_state.value != ProtocolState.CONNECTED && _state.value != ProtocolState.AUTHENTICATED) { + // КРИТИЧНО: если уже в handshake или authenticated, не начинаем заново + if (_state.value == ProtocolState.HANDSHAKING) { + log("⚠️ HANDSHAKE IGNORED: Already handshaking") + return + } + + if (_state.value == ProtocolState.AUTHENTICATED) { + log("⚠️ HANDSHAKE IGNORED: Already authenticated") + return + } + + if (_state.value != ProtocolState.CONNECTED) { log("⚠️ HANDSHAKE DEFERRED: Not connected (state=${_state.value}), will handshake after connection") connect() return @@ -409,7 +443,13 @@ class Protocol( private fun handleDisconnect() { val previousState = _state.value - log("🔌 DISCONNECT HANDLER: previousState=$previousState, manuallyClosed=$isManuallyClosed, reconnectAttempts=$reconnectAttempts") + log("🔌 DISCONNECT HANDLER: previousState=$previousState, manuallyClosed=$isManuallyClosed, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting") + + // КРИТИЧНО: если уже идет подключение, не делаем ничего + if (isConnecting) { + log("⚠️ DISCONNECT IGNORED: connection already in progress") + return + } setState(ProtocolState.DISCONNECTED, "Disconnect handler called from $previousState") handshakeComplete = false @@ -418,6 +458,9 @@ class Protocol( // Автоматический reconnect с защитой от бесконечных попыток if (!isManuallyClosed) { + // КРИТИЧНО: отменяем предыдущий reconnect job если есть + reconnectJob?.cancel() + // Экспоненциальная задержка: 1s, 2s, 4s, 8s, максимум 30s val delayMs = minOf(1000L * (1 shl minOf(reconnectAttempts - 1, 4)), 30000L) log("🔄 SCHEDULING RECONNECT: attempt #$reconnectAttempts, delay=${delayMs}ms") @@ -426,13 +469,13 @@ class Protocol( log("⚠️ WARNING: Too many reconnect attempts ($reconnectAttempts), may be stuck in loop") } - scope.launch { + reconnectJob = scope.launch { delay(delayMs) - if (!isManuallyClosed) { + if (!isManuallyClosed && !isConnecting) { log("🔄 EXECUTING RECONNECT after ${delayMs}ms delay") connect() } else { - log("🔄 RECONNECT CANCELLED: was manually closed during delay") + log("🔄 RECONNECT CANCELLED: manuallyClosed=$isManuallyClosed, isConnecting=$isConnecting") } } } else { @@ -460,8 +503,11 @@ class Protocol( * Disconnect from server */ fun disconnect() { - log("Disconnecting...") + log("🔌 Manual disconnect requested") isManuallyClosed = true + isConnecting = false // Сбрасываем флаг + reconnectJob?.cancel() // Отменяем запланированные переподключения + reconnectJob = null handshakeJob?.cancel() heartbeatJob?.cancel() webSocket?.close(1000, "User disconnected")