feat: Implement deduplication for FCM token subscription and update related logic
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user