Исправлен race инициализации аккаунта после device verification

This commit is contained in:
2026-04-16 03:35:37 +05:00
parent 0d21769399
commit ab9145c77a
5 changed files with 144 additions and 16 deletions

View File

@@ -225,7 +225,27 @@ class MainActivity : FragmentActivity() {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val accounts = accountManager.getAllAccounts() val accounts = accountManager.getAllAccounts()
hasExistingAccount = accounts.isNotEmpty() hasExistingAccount = accounts.isNotEmpty()
accountInfoList = accounts.map { it.toAccountInfo() } val infos = accounts.map { it.toAccountInfo() }
accountInfoList = infos
// Reconcile process-cached account name with persisted profile data.
currentAccount?.let { cached ->
val persisted = infos.firstOrNull {
it.publicKey.equals(cached.publicKey, ignoreCase = true)
}
val persistedUsername = persisted?.username?.trim().orEmpty().ifBlank { null }
val normalizedCachedName =
resolveAccountDisplayName(
cached.publicKey,
persisted?.name ?: cached.name,
persistedUsername
)
if (normalizedCachedName != cached.name) {
val updated = cached.copy(name = normalizedCachedName)
currentAccount = updated
cacheSessionAccount(updated)
}
}
} }
// Wait for initial load // Wait for initial load
@@ -305,15 +325,29 @@ class MainActivity : FragmentActivity() {
onAuthComplete = { account -> onAuthComplete = { account ->
startCreateAccountFlow = false startCreateAccountFlow = false
val normalizedAccount = val normalizedAccount =
account?.let { account?.let { decrypted ->
val persisted =
accountInfoList.firstOrNull {
it.publicKey.equals(
decrypted.publicKey,
ignoreCase = true
)
}
val persistedUsername =
persisted?.username
?.trim()
.orEmpty()
.ifBlank { null }
val normalizedName = val normalizedName =
resolveAccountDisplayName( resolveAccountDisplayName(
it.publicKey, decrypted.publicKey,
it.name, persisted?.name
null ?: decrypted.name,
persistedUsername
) )
if (it.name == normalizedName) it if (decrypted.name == normalizedName)
else it.copy(name = normalizedName) decrypted
else decrypted.copy(name = normalizedName)
} }
currentAccount = normalizedAccount currentAccount = normalizedAccount
cacheSessionAccount(normalizedAccount) cacheSessionAccount(normalizedAccount)
@@ -321,6 +355,14 @@ class MainActivity : FragmentActivity() {
// Save as last logged account // Save as last logged account
normalizedAccount?.let { normalizedAccount?.let {
accountManager.setLastLoggedPublicKey(it.publicKey) accountManager.setLastLoggedPublicKey(it.publicKey)
// Initialize protocol/message account context
// immediately after auth completion to avoid
// packet processing race before MainScreen
// composition.
ProtocolManager.initializeAccount(
it.publicKey,
it.privateKey
)
} }
// Первый запуск после регистрации: // Первый запуск после регистрации:
@@ -354,6 +396,27 @@ class MainActivity : FragmentActivity() {
runCatching { runCatching {
accountManager.setCurrentAccount(it.publicKey) accountManager.setCurrentAccount(it.publicKey)
} }
// Force-refresh account title from persisted
// profile (name/username) to avoid temporary
// public-key alias in UI after login.
val persisted = accountManager.getAccount(it.publicKey)
val persistedUsername =
persisted?.username
?.trim()
.orEmpty()
.ifBlank { null }
val refreshedName =
resolveAccountDisplayName(
it.publicKey,
persisted?.name ?: it.name,
persistedUsername
)
if (refreshedName != it.name) {
val updated = it.copy(name = refreshedName)
currentAccount = updated
cacheSessionAccount(updated)
}
} }
val accounts = accountManager.getAllAccounts() val accounts = accountManager.getAllAccounts()
accountInfoList = accounts.map { it.toAccountInfo() } accountInfoList = accounts.map { it.toAccountInfo() }
@@ -367,9 +430,9 @@ class MainActivity : FragmentActivity() {
// lag // lag
currentAccount = null currentAccount = null
clearCachedSessionAccount() clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager com.rosetta.messenger.network.ProtocolManager
.disconnect() .disconnect()
scope.launch {
accountManager.logout() accountManager.logout()
} }
} }
@@ -416,9 +479,9 @@ class MainActivity : FragmentActivity() {
// lag // lag
currentAccount = null currentAccount = null
clearCachedSessionAccount() clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager com.rosetta.messenger.network.ProtocolManager
.disconnect() .disconnect()
scope.launch {
accountManager.logout() accountManager.logout()
} }
}, },
@@ -509,8 +572,8 @@ class MainActivity : FragmentActivity() {
// Switch to another account: logout current, then show unlock. // Switch to another account: logout current, then show unlock.
currentAccount = null currentAccount = null
clearCachedSessionAccount() clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager.disconnect() com.rosetta.messenger.network.ProtocolManager.disconnect()
scope.launch {
accountManager.logout() accountManager.logout()
} }
}, },
@@ -520,8 +583,8 @@ class MainActivity : FragmentActivity() {
preservedMainNavAccountKey = "" preservedMainNavAccountKey = ""
currentAccount = null currentAccount = null
clearCachedSessionAccount() clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager.disconnect() com.rosetta.messenger.network.ProtocolManager.disconnect()
scope.launch {
accountManager.logout() accountManager.logout()
} }
} }
@@ -535,8 +598,8 @@ class MainActivity : FragmentActivity() {
preservedMainNavAccountKey = "" preservedMainNavAccountKey = ""
currentAccount = null currentAccount = null
clearCachedSessionAccount() clearCachedSessionAccount()
scope.launch {
ProtocolManager.disconnect() ProtocolManager.disconnect()
scope.launch {
accountManager.logout() accountManager.logout()
} }
} }
@@ -941,6 +1004,15 @@ fun MainScreen(
CallManager.bindAccount(accountPublicKey) CallManager.bindAccount(accountPublicKey)
} }
// Global account binding for protocol/message repository.
// Keeps init independent from ChatsList composition timing.
LaunchedEffect(accountPublicKey, accountPrivateKey) {
val normalizedPublicKey = accountPublicKey.trim()
val normalizedPrivateKey = accountPrivateKey.trim()
if (normalizedPublicKey.isBlank() || normalizedPrivateKey.isBlank()) return@LaunchedEffect
ProtocolManager.initializeAccount(normalizedPublicKey, normalizedPrivateKey)
}
LaunchedEffect(callUiState.isVisible) { LaunchedEffect(callUiState.isVisible) {
if (callUiState.isVisible) { if (callUiState.isVisible) {
isCallOverlayExpanded = true isCallOverlayExpanded = true

View File

@@ -301,6 +301,32 @@ class Protocol(
startHeartbeat(packet.heartbeatInterval) startHeartbeat(packet.heartbeatInterval)
} }
} }
// Device verification resolution from primary device.
// Desktop typically continues after next handshake response; here we also
// add a safety re-handshake trigger on ACCEPT to avoid being stuck in
// DEVICE_VERIFICATION_REQUIRED if server doesn't immediately push 0x00.
waitPacket(0x18) { packet ->
val resolve = packet as? PacketDeviceResolve ?: return@waitPacket
when (resolve.solution) {
DeviceResolveSolution.ACCEPT -> {
log("✅ DEVICE VERIFICATION ACCEPTED (deviceId=${shortKey(resolve.deviceId, 12)})")
if (_state.value == ProtocolState.DEVICE_VERIFICATION_REQUIRED) {
setState(ProtocolState.CONNECTED, "Device verification accepted")
val publicKey = lastPublicKey
val privateHash = lastPrivateHash
if (!publicKey.isNullOrBlank() && !privateHash.isNullOrBlank()) {
startHandshake(publicKey, privateHash, lastDevice)
} else {
log("⚠️ ACCEPT received but credentials are missing, waiting for reconnect")
}
}
}
DeviceResolveSolution.DECLINE -> {
log("⛔ DEVICE VERIFICATION DECLINED (deviceId=${shortKey(resolve.deviceId, 12)})")
}
}
}
} }
/** /**
@@ -847,6 +873,11 @@ class Protocol(
"⚡ FAST RECONNECT CHECK: state=$currentState, hasCredentials=$hasCredentials, isConnecting=$isConnecting, reason=$reason" "⚡ FAST RECONNECT CHECK: state=$currentState, hasCredentials=$hasCredentials, isConnecting=$isConnecting, reason=$reason"
) )
if (isManuallyClosed) {
log("⚡ FAST RECONNECT SKIP: manually closed, reason=$reason")
return
}
if (!hasCredentials) return if (!hasCredentials) return
if (currentState == ProtocolState.CONNECTING && isConnecting) { if (currentState == ProtocolState.CONNECTING && isConnecting) {

View File

@@ -293,11 +293,28 @@ object ProtocolManager {
* Должен вызываться после авторизации пользователя * Должен вызываться после авторизации пользователя
*/ */
fun initializeAccount(publicKey: String, privateKey: String) { fun initializeAccount(publicKey: String, privateKey: String) {
val normalizedPublicKey = publicKey.trim()
val normalizedPrivateKey = privateKey.trim()
if (normalizedPublicKey.isBlank() || normalizedPrivateKey.isBlank()) {
addLog("⚠️ initializeAccount skipped: missing account credentials")
return
}
addLog(
"🔐 initializeAccount pk=${shortKeyForLog(normalizedPublicKey)} keyLen=${normalizedPrivateKey.length} state=${getProtocol().state.value}"
)
setSyncInProgress(false) setSyncInProgress(false)
clearTypingState() clearTypingState()
messageRepository?.initialize(publicKey, privateKey) messageRepository?.initialize(normalizedPublicKey, normalizedPrivateKey)
if (resyncRequiredAfterAccountInit || protocol?.isAuthenticated() == true) {
val shouldResync = resyncRequiredAfterAccountInit || protocol?.isAuthenticated() == true
if (shouldResync) {
// Late account init may happen while an old sync request flag is still set.
// Force a fresh synchronize request to recover dropped inbound packets.
resyncRequiredAfterAccountInit = false resyncRequiredAfterAccountInit = false
syncRequestInFlight = false
clearSyncRequestTimeout()
addLog("🔄 Account initialized (${shortKeyForLog(normalizedPublicKey)}) -> force sync")
requestSynchronize() requestSynchronize()
} }
// Send "Rosetta Updates" message on version change (like desktop useUpdateMessage) // Send "Rosetta Updates" message on version change (like desktop useUpdateMessage)

View File

@@ -29,6 +29,7 @@ import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.DecryptedAccount import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.EncryptedAccount import com.rosetta.messenger.data.EncryptedAccount
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -308,6 +309,9 @@ fun SetPasswordScreen(
) )
accountManager.saveAccount(account) accountManager.saveAccount(account)
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey) val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
// Initialize repository/account context before handshake completes to avoid
// "Sync postponed until account is initialized" race on first login.
ProtocolManager.initializeAccount(keyPair.publicKey, keyPair.privateKey)
startAuthHandshakeFast(keyPair.publicKey, privateKeyHash) startAuthHandshakeFast(keyPair.publicKey, privateKeyHash)
accountManager.setCurrentAccount(keyPair.publicKey) accountManager.setCurrentAccount(keyPair.publicKey)
val decryptedAccount = DecryptedAccount( val decryptedAccount = DecryptedAccount(

View File

@@ -45,6 +45,7 @@ import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.EncryptedAccount import com.rosetta.messenger.data.EncryptedAccount
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.ProtocolManager
import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.AvatarImage import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.chats.getAvatarColor import com.rosetta.messenger.ui.chats.getAvatarColor
@@ -116,6 +117,9 @@ val decryptedPrivateKey = CryptoManager.decryptWithPassword(
name = selectedAccount.name name = selectedAccount.name
) )
// Initialize repository/account context before handshake completes to avoid
// "Sync postponed until account is initialized" race.
ProtocolManager.initializeAccount(account.publicKey, decryptedPrivateKey)
startAuthHandshakeFast(account.publicKey, privateKeyHash) startAuthHandshakeFast(account.publicKey, privateKeyHash)
accountManager.setCurrentAccount(account.publicKey) accountManager.setCurrentAccount(account.publicKey)