From 8aa17383cf943b646e98ecd3f85b0326057c56fb Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 12 Jan 2026 17:16:44 +0500 Subject: [PATCH] feat: Save last logged account public key after authentication for improved user experience --- .../com/rosetta/messenger/MainActivity.kt | 2 + .../com/rosetta/messenger/network/Chunker.kt | 68 +++++++++++++++++++ .../com/rosetta/messenger/network/Packets.kt | 30 ++++++++ .../com/rosetta/messenger/network/Protocol.kt | 40 +++++------ .../messenger/ui/auth/SetPasswordScreen.kt | 2 - .../rosetta/messenger/ui/auth/UnlockScreen.kt | 2 - 6 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/com/rosetta/messenger/network/Chunker.kt diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index e4bb6cb..7db543f 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -146,6 +146,8 @@ class MainActivity : ComponentActivity() { onAuthComplete = { account -> currentAccount = account hasExistingAccount = true + // Save as last logged account + accountManager.setLastLoggedPublicKey(account.publicKey) // Reload accounts list scope.launch { val accounts = accountManager.getAllAccounts() diff --git a/app/src/main/java/com/rosetta/messenger/network/Chunker.kt b/app/src/main/java/com/rosetta/messenger/network/Chunker.kt new file mode 100644 index 0000000..dd89d5e --- /dev/null +++ b/app/src/main/java/com/rosetta/messenger/network/Chunker.kt @@ -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("") + } +} diff --git a/app/src/main/java/com/rosetta/messenger/network/Packets.kt b/app/src/main/java/com/rosetta/messenger/network/Packets.kt index e9ab8a1..05d15e4 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Packets.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Packets.kt @@ -418,3 +418,33 @@ class PacketTyping : Packet() { 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 + } +} diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index b9be4b8..8e693f5 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -31,7 +31,6 @@ class Protocol( companion object { private const val TAG = "RosettaProtocol" private const val RECONNECT_INTERVAL = 5000L // 5 seconds (ΠΊΠ°ΠΊ Π² АрхивС) - private const val MAX_RECONNECT_ATTEMPTS = 10 // Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ» Π»ΠΈΠΌΠΈΡ‚ private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds } @@ -47,7 +46,6 @@ class Protocol( .build() private var webSocket: WebSocket? = null - private var reconnectAttempts = 0 private var isManuallyClosed = false private var handshakeComplete = false private var handshakeJob: Job? = null @@ -84,6 +82,7 @@ class Protocol( 0x06 to { PacketMessage() }, 0x07 to { PacketRead() }, 0x08 to { PacketDelivery() }, + 0x09 to { PacketChunk() }, 0x0B to { PacketTyping() } ) @@ -180,7 +179,6 @@ class Protocol( webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { log("βœ… WebSocket connected") - reconnectAttempts = 0 _state.value = ProtocolState.CONNECTED // If we have saved credentials, start handshake automatically @@ -294,12 +292,11 @@ class Protocol( } try { - val sent = socket.send(ByteString.of(*data)) - if (sent) { - log("βœ… Packet ${packet.getPacketId()} sent successfully") - } else { - log("❌ Failed to send packet ${packet.getPacketId()} - send() returned false") - } + // πŸ“¦ Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Chunker для ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ (ΠΊΠ°ΠΊ Π² Desktop) + // Если ΠΏΠ°ΠΊΠ΅Ρ‚ большой, ΠΎΠ½ Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π°Π·Π±ΠΈΡ‚ Π½Π° части автоматичСски + val chunker = Chunker(socket) { msg -> log(msg) } + chunker.send(stream) + log("βœ… Packet ${packet.getPacketId()} sent successfully") } catch (e: Exception) { log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}") e.printStackTrace() @@ -358,25 +355,22 @@ class Protocol( handshakeJob?.cancel() heartbeatJob?.cancel() - // АвтоматичСский reconnect (упрощённая Π»ΠΎΠ³ΠΈΠΊΠ° ΠΊΠ°ΠΊ Π² АрхивС) + // АвтоматичСский reconnect (простая Π»ΠΎΠ³ΠΈΠΊΠ° ΠΊΠ°ΠΊ Π² АрхивС - Π±Π΅Π· счётчиков) if (!isManuallyClosed) { - log("πŸ”„ Connection lost from $previousState, reconnecting...") - reconnectAttempts++ - - val delay = if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { - RECONNECT_INTERVAL - } else { - // ПослС максимума ΠΏΠΎΠΏΡ‹Ρ‚ΠΎΠΊ - ΠΆΠ΄Ρ‘ΠΌ дольшС - reconnectAttempts = 0 - 30000L - } - - log("πŸ”„ Reconnecting in ${delay}ms (attempt $reconnectAttempts)") + log("πŸ”„ Connection lost from $previousState, attempting to reconnect...") + log("πŸ”„ Reconnecting in ${RECONNECT_INTERVAL}ms") scope.launch { - delay(delay) + delay(RECONNECT_INTERVAL) if (!isManuallyClosed) { connect() + // Π’ Desktop провСряСтся socket?.readyState == WebSocket.OPEN + // Π’ Android Π°Π½Π°Π»ΠΎΠ³ - ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° state + if (_state.value == ProtocolState.CONNECTED || + _state.value == ProtocolState.AUTHENTICATED) { + return@launch + } + // Π’ Desktop emit('reconnect') происходит здСсь } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt index ee002ab..352d285 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt @@ -494,8 +494,6 @@ fun SetPasswordScreen( accountManager.saveAccount(account) accountManager.setCurrentAccount(keyPair.publicKey) - // Save as last logged account for next time - accountManager.setLastLoggedPublicKey(keyPair.publicKey) // πŸ”Œ Connect to server and authenticate val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey) diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index 95cb06b..50c87fd 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -587,8 +587,6 @@ fun UnlockScreen( ProtocolManager.authenticate(account.publicKey, privateKeyHash) accountManager.setCurrentAccount(account.publicKey) - // Save as last logged account for next time - accountManager.setLastLoggedPublicKey(account.publicKey) onUnlocked(decryptedAccount) } catch (e: Exception) {