feat: Enhance WebSocket connection handling; prevent duplicate connections and improve reconnection logic
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user