feat: Implement deduplication for FCM token subscription and update related logic

This commit is contained in:
2026-02-25 23:03:28 +05:00
parent b7b99cdb40
commit 48861633ee
4 changed files with 41 additions and 85 deletions

View File

@@ -84,6 +84,13 @@ android {
resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" }
jniLibs { useLegacyPackaging = true } jniLibs { useLegacyPackaging = true }
} }
applicationVariants.all {
outputs.all {
val apkOut = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
apkOut.outputFileName = "Rosetta-${versionName}.apk"
}
}
} }
dependencies { dependencies {

View File

@@ -32,10 +32,8 @@ import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.RecentSearchesManager import com.rosetta.messenger.data.RecentSearchesManager
import com.rosetta.messenger.data.resolveAccountDisplayName import com.rosetta.messenger.data.resolveAccountDisplayName
import com.rosetta.messenger.database.RosettaDatabase import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.PacketPushNotification
import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.ProtocolState import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.network.PushNotificationAction
import com.rosetta.messenger.network.SearchUser import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.auth.AccountInfo import com.rosetta.messenger.ui.auth.AccountInfo
@@ -62,9 +60,7 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
private lateinit var preferencesManager: PreferencesManager private lateinit var preferencesManager: PreferencesManager
@@ -177,15 +173,6 @@ class MainActivity : FragmentActivity() {
return@setContent return@setContent
} }
// Ensure push token subscription is sent whenever protocol reaches AUTHENTICATED.
// This recovers token binding after reconnects and delayed handshakes.
LaunchedEffect(protocolState, currentAccount?.publicKey) {
currentAccount ?: return@LaunchedEffect
if (protocolState == ProtocolState.AUTHENTICATED) {
sendFcmTokenToServer()
}
}
RosettaAndroidTheme(darkTheme = isDarkTheme, animated = true) { RosettaAndroidTheme(darkTheme = isDarkTheme, animated = true) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -254,10 +241,6 @@ class MainActivity : FragmentActivity() {
accountManager.setLastLoggedPublicKey(it.publicKey) accountManager.setLastLoggedPublicKey(it.publicKey)
} }
// 📤 Отправляем FCM токен на сервер после успешной
// аутентификации
account?.let { sendFcmTokenToServer() }
// Reload accounts list // Reload accounts list
scope.launch { scope.launch {
val accounts = accountManager.getAllAccounts() val accounts = accountManager.getAllAccounts()
@@ -456,16 +439,14 @@ class MainActivity : FragmentActivity() {
saveFcmToken(token) saveFcmToken(token)
addFcmLog("💾 Токен сохранен локально") addFcmLog("💾 Токен сохранен локально")
if (ProtocolManager.state.value == ProtocolState.AUTHENTICATED) { // Token will be sent by ProtocolManager.onAuthenticated()
addFcmLog("🔁 Протокол уже AUTHENTICATED, отправляем токен сразу") // when protocol reaches AUTHENTICATED state
sendFcmTokenToServer()
}
} else { } else {
addFcmLog("⚠️ Токен пустой") addFcmLog("⚠️ Токен пустой")
} }
// Токен будет отправлен на сервер после успешной аутентификации // Токен будет отправлен через ProtocolManager.subscribePushTokenIfAvailable()
// (см. вызов sendFcmTokenToServer в onAccountLogin) // при достижении состояния AUTHENTICATED
} }
} catch (e: Exception) { } catch (e: Exception) {
addFcmLog("❌ Ошибка Firebase: ${e.message}") addFcmLog("❌ Ошибка Firebase: ${e.message}")
@@ -479,52 +460,6 @@ class MainActivity : FragmentActivity() {
prefs.edit().putString("fcm_token", token).apply() prefs.edit().putString("fcm_token", token).apply()
} }
/**
* Отправить FCM токен на сервер Вызывается после успешной аутентификации, когда аккаунт уже
* расшифрован
*/
private fun sendFcmTokenToServer() {
lifecycleScope.launch {
try {
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
val token = prefs.getString("fcm_token", null)
if (token == null) {
addFcmLog("⚠️ Нет сохраненного токена для отправки")
return@launch
}
val shortToken = "${token.take(12)}...${token.takeLast(8)}"
addFcmLog("📤 Подготовка к отправке токена на сервер")
addFcmLog("⏳ Ожидание аутентификации...")
// 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED
val authenticated = withTimeoutOrNull(5000) {
ProtocolManager.state.first { it == ProtocolState.AUTHENTICATED }
}
if (authenticated == null) {
addFcmLog("❌ Таймаут аутентификации (5000ms)")
return@launch
}
addFcmLog("✅ Аутентификация успешна")
addFcmLog("📨 Отправка токена: $shortToken")
val packet =
PacketPushNotification().apply {
this.notificationsToken = token
this.action = PushNotificationAction.SUBSCRIBE
}
ProtocolManager.send(packet)
addFcmLog("✅ Пакет отправлен на сервер (ID: 0x10)")
addFcmLog("🎉 FCM токен успешно зарегистрирован!")
} catch (e: Exception) {
addFcmLog("❌ Ошибка отправки: ${e.message}")
}
}
}
} }
private fun buildInitials(displayName: String): String = private fun buildInitials(displayName: String): String =

View File

@@ -33,6 +33,10 @@ object ProtocolManager {
private var messageRepository: MessageRepository? = null private var messageRepository: MessageRepository? = null
private var appContext: Context? = null private var appContext: Context? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Guard: prevent duplicate FCM token subscribe within a single session
@Volatile
private var lastSubscribedToken: String? = null
// Debug logs for dev console - 🚀 ОТКЛЮЧЕНО для производительности // Debug logs for dev console - 🚀 ОТКЛЮЧЕНО для производительности
// Логи только в Logcat, не в StateFlow (это вызывало ANR!) // Логи только в Logcat, не в StateFlow (это вызывало ANR!)
@@ -393,20 +397,37 @@ object ProtocolManager {
subscribePushTokenIfAvailable() subscribePushTokenIfAvailable()
} }
private fun subscribePushTokenIfAvailable() { /**
* Send FCM push token to server (SUBSCRIBE).
* Deduplicates: won't re-send the same token within one connection session.
* Called internally on AUTHENTICATED and can be called from
* [com.rosetta.messenger.push.RosettaFirebaseMessagingService.onNewToken]
* when Firebase rotates the token mid-session.
*
* @param forceToken if non-null, use this token instead of reading SharedPreferences
* (used by onNewToken which already has the fresh token).
*/
fun subscribePushTokenIfAvailable(forceToken: String? = null) {
val context = appContext ?: return val context = appContext ?: return
val token = val token = (forceToken
context.getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE) ?: context.getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
.getString("fcm_token", null) .getString("fcm_token", null))
?.trim() ?.trim()
.orEmpty() .orEmpty()
if (token.isEmpty()) return if (token.isEmpty()) return
// Dedup: don't send the same token twice in one connection session
if (token == lastSubscribedToken) {
addLog("🔔 Push token already subscribed this session — skipped")
return
}
val packet = PacketPushNotification().apply { val packet = PacketPushNotification().apply {
notificationsToken = token notificationsToken = token
action = PushNotificationAction.SUBSCRIBE action = PushNotificationAction.SUBSCRIBE
} }
send(packet) send(packet)
lastSubscribedToken = token
addLog("🔔 Push token subscribe requested on AUTHENTICATED") addLog("🔔 Push token subscribe requested on AUTHENTICATED")
} }
@@ -803,6 +824,7 @@ object ProtocolManager {
_devices.value = emptyList() _devices.value = emptyList()
_pendingDeviceVerification.value = null _pendingDeviceVerification.value = null
setSyncInProgress(false) setSyncInProgress(false)
lastSubscribedToken = null // reset so token is re-sent on next connect
} }
/** /**

View File

@@ -13,9 +13,7 @@ import com.rosetta.messenger.MainActivity
import com.rosetta.messenger.R import com.rosetta.messenger.R
import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.PreferencesManager import com.rosetta.messenger.data.PreferencesManager
import com.rosetta.messenger.network.PacketPushNotification
import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.PushNotificationAction
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -70,15 +68,9 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
saveFcmToken(token) saveFcmToken(token)
// Best-effort: если соединение уже авторизовано — сразу обновляем подписку на push. // Best-effort: если соединение уже авторизовано — сразу обновляем подписку на push.
// Используем единую точку отправки в ProtocolManager (с дедупликацией).
if (ProtocolManager.isAuthenticated()) { if (ProtocolManager.isAuthenticated()) {
runCatching { runCatching { ProtocolManager.subscribePushTokenIfAvailable(forceToken = token) }
ProtocolManager.send(
PacketPushNotification().apply {
notificationsToken = token
action = PushNotificationAction.SUBSCRIBE
}
)
}
} }
} }