feat: Save last logged account public key after authentication for improved user experience
This commit is contained in:
@@ -146,6 +146,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
onAuthComplete = { account ->
|
onAuthComplete = { account ->
|
||||||
currentAccount = account
|
currentAccount = account
|
||||||
hasExistingAccount = true
|
hasExistingAccount = true
|
||||||
|
// Save as last logged account
|
||||||
|
accountManager.setLastLoggedPublicKey(account.publicKey)
|
||||||
// Reload accounts list
|
// Reload accounts list
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
|
|||||||
68
app/src/main/java/com/rosetta/messenger/network/Chunker.kt
Normal file
68
app/src/main/java/com/rosetta/messenger/network/Chunker.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package com.rosetta.messenger.network
|
||||||
|
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okio.ByteString
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunker for splitting large packets into chunks
|
||||||
|
* Matches Desktop implementation exactly
|
||||||
|
*/
|
||||||
|
class Chunker(
|
||||||
|
private val webSocket: WebSocket,
|
||||||
|
private val logger: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val MAX_PACKET_SIZE_BEFORE_SLICE = 1024 // bytes
|
||||||
|
private const val MAX_CHUNK_SIZE = 1024 // bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun log(message: String) {
|
||||||
|
logger(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send stream, chunking if necessary
|
||||||
|
*/
|
||||||
|
fun send(stream: Stream) {
|
||||||
|
val data = stream.getStream()
|
||||||
|
|
||||||
|
// If packet is small enough, send directly
|
||||||
|
if (data.size <= MAX_PACKET_SIZE_BEFORE_SLICE) {
|
||||||
|
webSocket.send(ByteString.of(*data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split into chunks
|
||||||
|
val totalChunks = (data.size + MAX_CHUNK_SIZE - 1) / MAX_CHUNK_SIZE
|
||||||
|
val chunkId = generateChunkId()
|
||||||
|
|
||||||
|
log("📦 Chunking packet: ${data.size} bytes -> $totalChunks chunks (id: $chunkId)")
|
||||||
|
|
||||||
|
for (i in 0 until totalChunks) {
|
||||||
|
val start = i * MAX_CHUNK_SIZE
|
||||||
|
val end = minOf((i + 1) * MAX_CHUNK_SIZE, data.size)
|
||||||
|
val chunkData = data.sliceArray(start until end)
|
||||||
|
|
||||||
|
val chunkPacket = PacketChunk().apply {
|
||||||
|
this.chunkId = chunkId
|
||||||
|
this.chunkIndex = i
|
||||||
|
this.totalChunks = totalChunks
|
||||||
|
this.data = chunkData
|
||||||
|
}
|
||||||
|
|
||||||
|
val chunkStream = chunkPacket.send()
|
||||||
|
webSocket.send(ByteString.of(*chunkStream.getStream()))
|
||||||
|
|
||||||
|
log("📦 Sent chunk $i/$totalChunks (${chunkData.size} bytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random chunk ID (like Desktop version)
|
||||||
|
*/
|
||||||
|
private fun generateChunkId(): String {
|
||||||
|
val chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
return (1..26).map { chars[Random.nextInt(chars.length)] }.joinToString("")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -418,3 +418,33 @@ class PacketTyping : Packet() {
|
|||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk packet (ID: 0x09)
|
||||||
|
* Для разбиения больших пакетов на части (как в Desktop)
|
||||||
|
*/
|
||||||
|
class PacketChunk : Packet() {
|
||||||
|
var chunkId: String = ""
|
||||||
|
var chunkIndex: Int = 0
|
||||||
|
var totalChunks: Int = 0
|
||||||
|
var data: ByteArray = ByteArray(0)
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x09
|
||||||
|
|
||||||
|
override fun receive(stream: Stream) {
|
||||||
|
chunkId = stream.readString()
|
||||||
|
chunkIndex = stream.readInt32()
|
||||||
|
totalChunks = stream.readInt32()
|
||||||
|
data = stream.readBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
val stream = Stream()
|
||||||
|
stream.writeInt16(getPacketId())
|
||||||
|
stream.writeString(chunkId)
|
||||||
|
stream.writeInt32(chunkIndex)
|
||||||
|
stream.writeInt32(totalChunks)
|
||||||
|
stream.writeBytes(data)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class Protocol(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RosettaProtocol"
|
private const val TAG = "RosettaProtocol"
|
||||||
private const val RECONNECT_INTERVAL = 5000L // 5 seconds (как в Архиве)
|
private const val RECONNECT_INTERVAL = 5000L // 5 seconds (как в Архиве)
|
||||||
private const val MAX_RECONNECT_ATTEMPTS = 10 // Увеличил лимит
|
|
||||||
private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds
|
private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +46,6 @@ class Protocol(
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
private var webSocket: WebSocket? = null
|
private var webSocket: WebSocket? = null
|
||||||
private var reconnectAttempts = 0
|
|
||||||
private var isManuallyClosed = false
|
private var isManuallyClosed = false
|
||||||
private var handshakeComplete = false
|
private var handshakeComplete = false
|
||||||
private var handshakeJob: Job? = null
|
private var handshakeJob: Job? = null
|
||||||
@@ -84,6 +82,7 @@ class Protocol(
|
|||||||
0x06 to { PacketMessage() },
|
0x06 to { PacketMessage() },
|
||||||
0x07 to { PacketRead() },
|
0x07 to { PacketRead() },
|
||||||
0x08 to { PacketDelivery() },
|
0x08 to { PacketDelivery() },
|
||||||
|
0x09 to { PacketChunk() },
|
||||||
0x0B to { PacketTyping() }
|
0x0B to { PacketTyping() }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -180,7 +179,6 @@ 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 connected")
|
log("✅ WebSocket connected")
|
||||||
reconnectAttempts = 0
|
|
||||||
_state.value = ProtocolState.CONNECTED
|
_state.value = ProtocolState.CONNECTED
|
||||||
|
|
||||||
// If we have saved credentials, start handshake automatically
|
// If we have saved credentials, start handshake automatically
|
||||||
@@ -294,12 +292,11 @@ class Protocol(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val sent = socket.send(ByteString.of(*data))
|
// 📦 Используем Chunker для отправки (как в Desktop)
|
||||||
if (sent) {
|
// Если пакет большой, он будет разбит на части автоматически
|
||||||
log("✅ Packet ${packet.getPacketId()} sent successfully")
|
val chunker = Chunker(socket) { msg -> log(msg) }
|
||||||
} else {
|
chunker.send(stream)
|
||||||
log("❌ Failed to send packet ${packet.getPacketId()} - send() returned false")
|
log("✅ Packet ${packet.getPacketId()} sent successfully")
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}")
|
log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -358,25 +355,22 @@ class Protocol(
|
|||||||
handshakeJob?.cancel()
|
handshakeJob?.cancel()
|
||||||
heartbeatJob?.cancel()
|
heartbeatJob?.cancel()
|
||||||
|
|
||||||
// Автоматический reconnect (упрощённая логика как в Архиве)
|
// Автоматический reconnect (простая логика как в Архиве - без счётчиков)
|
||||||
if (!isManuallyClosed) {
|
if (!isManuallyClosed) {
|
||||||
log("🔄 Connection lost from $previousState, reconnecting...")
|
log("🔄 Connection lost from $previousState, attempting to reconnect...")
|
||||||
reconnectAttempts++
|
log("🔄 Reconnecting in ${RECONNECT_INTERVAL}ms")
|
||||||
|
|
||||||
val delay = if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
||||||
RECONNECT_INTERVAL
|
|
||||||
} else {
|
|
||||||
// После максимума попыток - ждём дольше
|
|
||||||
reconnectAttempts = 0
|
|
||||||
30000L
|
|
||||||
}
|
|
||||||
|
|
||||||
log("🔄 Reconnecting in ${delay}ms (attempt $reconnectAttempts)")
|
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(delay)
|
delay(RECONNECT_INTERVAL)
|
||||||
if (!isManuallyClosed) {
|
if (!isManuallyClosed) {
|
||||||
connect()
|
connect()
|
||||||
|
// В Desktop проверяется socket?.readyState == WebSocket.OPEN
|
||||||
|
// В Android аналог - проверка state
|
||||||
|
if (_state.value == ProtocolState.CONNECTED ||
|
||||||
|
_state.value == ProtocolState.AUTHENTICATED) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
// В Desktop emit('reconnect') происходит здесь
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,8 +494,6 @@ fun SetPasswordScreen(
|
|||||||
|
|
||||||
accountManager.saveAccount(account)
|
accountManager.saveAccount(account)
|
||||||
accountManager.setCurrentAccount(keyPair.publicKey)
|
accountManager.setCurrentAccount(keyPair.publicKey)
|
||||||
// Save as last logged account for next time
|
|
||||||
accountManager.setLastLoggedPublicKey(keyPair.publicKey)
|
|
||||||
|
|
||||||
// 🔌 Connect to server and authenticate
|
// 🔌 Connect to server and authenticate
|
||||||
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
|
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
|
||||||
|
|||||||
@@ -587,8 +587,6 @@ fun UnlockScreen(
|
|||||||
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
|
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
|
||||||
|
|
||||||
accountManager.setCurrentAccount(account.publicKey)
|
accountManager.setCurrentAccount(account.publicKey)
|
||||||
// Save as last logged account for next time
|
|
||||||
accountManager.setLastLoggedPublicKey(account.publicKey)
|
|
||||||
onUnlocked(decryptedAccount)
|
onUnlocked(decryptedAccount)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
Reference in New Issue
Block a user