feat: Enhance WebSocket connection handling; prevent duplicate connections and improve reconnection logic

This commit is contained in:
k1ngsterr1
2026-01-17 02:14:16 +05:00
parent 7f681d627a
commit 1ea2d917dc
2 changed files with 104 additions and 28 deletions

View File

@@ -1,10 +1,12 @@
package com.rosetta.messenger package com.rosetta.messenger
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.lifecycleScope
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutLinearInEasing
@@ -72,17 +74,7 @@ class MainActivity : ComponentActivity() {
// 🔔 Инициализируем Firebase для push-уведомлений // 🔔 Инициализируем Firebase для push-уведомлений
initializeFirebase() initializeFirebase()
// <20> Логируем текущий FCM токен при запуске // <20> Предзагружаем эмодзи в фоне для мгновенного открытия пикера
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")
}
// <20>🚀 Предзагружаем эмодзи в фоне для мгновенного открытия пикера
// Используем новый оптимизированный кэш // Используем новый оптимизированный кэш
OptimizedEmojiCache.preload(this) OptimizedEmojiCache.preload(this)
@@ -186,6 +178,10 @@ class MainActivity : ComponentActivity() {
hasExistingAccount = true hasExistingAccount = true
// Save as last logged account // Save as last logged account
account?.let { accountManager.setLastLoggedPublicKey(it.publicKey) } account?.let { accountManager.setLastLoggedPublicKey(it.publicKey) }
// 📤 Отправляем FCM токен на сервер после успешной аутентификации
account?.let { sendFcmTokenToServer(it) }
// Reload accounts list // Reload accounts list
scope.launch { scope.launch {
val accounts = accountManager.getAllAccounts() val accounts = accountManager.getAllAccounts()
@@ -268,8 +264,8 @@ class MainActivity : ComponentActivity() {
// Сохраняем токен локально // Сохраняем токен локально
token?.let { saveFcmToken(it) } token?.let { saveFcmToken(it) }
// TODO: Отправляем токен на сервер если аккаунт залогинен // Токен будет отправлен на сервер после успешной аутентификации
// token?.let { sendFcmTokenToServer(it) } // (см. вызов sendFcmTokenToServer в onAccountLogin)
} }
Log.d(TAG, "✅ Firebase initialized successfully") Log.d(TAG, "✅ Firebase initialized successfully")
@@ -286,6 +282,40 @@ class MainActivity : ComponentActivity() {
prefs.edit().putString("fcm_token", token).apply() prefs.edit().putString("fcm_token", token).apply()
Log.d(TAG, "💾 FCM token saved locally") 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 @Composable

View File

@@ -53,6 +53,8 @@ class Protocol(
private var reconnectAttempts = 0 private var reconnectAttempts = 0
private var lastStateChangeTime = System.currentTimeMillis() private var lastStateChangeTime = System.currentTimeMillis()
private var lastSuccessfulConnection = 0L private var lastSuccessfulConnection = 0L
private var reconnectJob: Job? = null // Для отмены запланированных переподключений
private var isConnecting = false // Флаг для защиты от одновременных подключений
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -190,13 +192,22 @@ class Protocol(
*/ */
fun connect() { fun connect() {
val currentState = _state.value 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)") log("⚠️ Already connecting, skipping... (preventing duplicate connect)")
return return
} }
// Отменяем любые запланированные переподключения
reconnectJob?.cancel()
reconnectJob = null
log("🔄 Cancelled any pending reconnect jobs")
// Устанавливаем флаг ПЕРЕД любыми операциями
isConnecting = true
reconnectAttempts++ reconnectAttempts++
log("📊 RECONNECT ATTEMPT #$reconnectAttempts") log("📊 RECONNECT ATTEMPT #$reconnectAttempts")
@@ -224,15 +235,24 @@ class Protocol(
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
log("✅ WebSocket OPEN: response=${response.code}, hasCredentials=${lastPublicKey != null}") log("✅ WebSocket OPEN: response=${response.code}, hasCredentials=${lastPublicKey != null}")
// Сбрасываем флаг подключения
isConnecting = false
setState(ProtocolState.CONNECTED, "WebSocket onOpen callback") setState(ProtocolState.CONNECTED, "WebSocket onOpen callback")
// If we have saved credentials, start handshake automatically // КРИТИЧНО: проверяем что не идет уже handshake
lastPublicKey?.let { publicKey -> if (_state.value != ProtocolState.HANDSHAKING && _state.value != ProtocolState.AUTHENTICATED) {
lastPrivateHash?.let { privateHash -> // If we have saved credentials, start handshake automatically
log("🤝 Auto-starting handshake with saved credentials") lastPublicKey?.let { publicKey ->
startHandshake(publicKey, privateHash) lastPrivateHash?.let { privateHash ->
} log("🤝 Auto-starting handshake with saved credentials")
} ?: log("⚠️ No saved credentials, waiting for manual handshake") 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) { override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
@@ -250,6 +270,7 @@ class Protocol(
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
log("❌ WebSocket CLOSED: code=$code reason='$reason' state=${_state.value} manuallyClosed=$isManuallyClosed") log("❌ WebSocket CLOSED: code=$code reason='$reason' state=${_state.value} manuallyClosed=$isManuallyClosed")
isConnecting = false // Сбрасываем флаг
handleDisconnect() handleDisconnect()
} }
@@ -260,6 +281,7 @@ class Protocol(
log(" Manually closed: $isManuallyClosed") log(" Manually closed: $isManuallyClosed")
log(" Reconnect attempts: $reconnectAttempts") log(" Reconnect attempts: $reconnectAttempts")
t.printStackTrace() t.printStackTrace()
isConnecting = false // Сбрасываем флаг
_lastError.value = t.message _lastError.value = t.message
handleDisconnect() handleDisconnect()
} }
@@ -273,12 +295,24 @@ class Protocol(
log("🤝 Starting handshake...") log("🤝 Starting handshake...")
log(" Public key: ${publicKey.take(20)}...") log(" Public key: ${publicKey.take(20)}...")
log(" Private hash: ${privateHash.take(20)}...") log(" Private hash: ${privateHash.take(20)}...")
log(" Current state: ${_state.value}")
// Save credentials for reconnection // Save credentials for reconnection
lastPublicKey = publicKey lastPublicKey = publicKey
lastPrivateHash = privateHash 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") log("⚠️ HANDSHAKE DEFERRED: Not connected (state=${_state.value}), will handshake after connection")
connect() connect()
return return
@@ -409,7 +443,13 @@ class Protocol(
private fun handleDisconnect() { private fun handleDisconnect() {
val previousState = _state.value 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") setState(ProtocolState.DISCONNECTED, "Disconnect handler called from $previousState")
handshakeComplete = false handshakeComplete = false
@@ -418,6 +458,9 @@ class Protocol(
// Автоматический reconnect с защитой от бесконечных попыток // Автоматический reconnect с защитой от бесконечных попыток
if (!isManuallyClosed) { if (!isManuallyClosed) {
// КРИТИЧНО: отменяем предыдущий reconnect job если есть
reconnectJob?.cancel()
// Экспоненциальная задержка: 1s, 2s, 4s, 8s, максимум 30s // Экспоненциальная задержка: 1s, 2s, 4s, 8s, максимум 30s
val delayMs = minOf(1000L * (1 shl minOf(reconnectAttempts - 1, 4)), 30000L) val delayMs = minOf(1000L * (1 shl minOf(reconnectAttempts - 1, 4)), 30000L)
log("🔄 SCHEDULING RECONNECT: attempt #$reconnectAttempts, delay=${delayMs}ms") 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") log("⚠️ WARNING: Too many reconnect attempts ($reconnectAttempts), may be stuck in loop")
} }
scope.launch { reconnectJob = scope.launch {
delay(delayMs) delay(delayMs)
if (!isManuallyClosed) { if (!isManuallyClosed && !isConnecting) {
log("🔄 EXECUTING RECONNECT after ${delayMs}ms delay") log("🔄 EXECUTING RECONNECT after ${delayMs}ms delay")
connect() connect()
} else { } else {
log("🔄 RECONNECT CANCELLED: was manually closed during delay") log("🔄 RECONNECT CANCELLED: manuallyClosed=$isManuallyClosed, isConnecting=$isConnecting")
} }
} }
} else { } else {
@@ -460,8 +503,11 @@ class Protocol(
* Disconnect from server * Disconnect from server
*/ */
fun disconnect() { fun disconnect() {
log("Disconnecting...") log("🔌 Manual disconnect requested")
isManuallyClosed = true isManuallyClosed = true
isConnecting = false // Сбрасываем флаг
reconnectJob?.cancel() // Отменяем запланированные переподключения
reconnectJob = null
handshakeJob?.cancel() handshakeJob?.cancel()
heartbeatJob?.cancel() heartbeatJob?.cancel()
webSocket?.close(1000, "User disconnected") webSocket?.close(1000, "User disconnected")