feat: Implement protocol packet classes and binary stream for network communication
This commit is contained in:
187
app/src/main/java/com/rosetta/messenger/network/Packets.kt
Normal file
187
app/src/main/java/com/rosetta/messenger/network/Packets.kt
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package com.rosetta.messenger.network
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all protocol packets
|
||||||
|
*/
|
||||||
|
abstract class Packet {
|
||||||
|
abstract fun getPacketId(): Int
|
||||||
|
abstract fun receive(stream: Stream)
|
||||||
|
abstract fun send(): Stream
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handshake packet (ID: 0x00)
|
||||||
|
* First packet sent by client to authenticate with the server
|
||||||
|
*/
|
||||||
|
class PacketHandshake : Packet() {
|
||||||
|
var privateKey: String = ""
|
||||||
|
var publicKey: String = ""
|
||||||
|
var protocolVersion: Int = 1
|
||||||
|
var heartbeatInterval: Int = 15
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x00
|
||||||
|
|
||||||
|
override fun receive(stream: Stream) {
|
||||||
|
privateKey = stream.readString()
|
||||||
|
publicKey = stream.readString()
|
||||||
|
protocolVersion = stream.readInt8()
|
||||||
|
heartbeatInterval = stream.readInt8()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
val stream = Stream()
|
||||||
|
stream.writeInt16(getPacketId())
|
||||||
|
stream.writeString(privateKey)
|
||||||
|
stream.writeString(publicKey)
|
||||||
|
stream.writeInt8(protocolVersion)
|
||||||
|
stream.writeInt8(heartbeatInterval)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result packet (ID: 0x02)
|
||||||
|
* Server response for various operations
|
||||||
|
*/
|
||||||
|
class PacketResult : Packet() {
|
||||||
|
var resultCode: Int = 0
|
||||||
|
var message: String = ""
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x02
|
||||||
|
|
||||||
|
override fun receive(stream: Stream) {
|
||||||
|
resultCode = stream.readInt8()
|
||||||
|
message = stream.readString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
val stream = Stream()
|
||||||
|
stream.writeInt16(getPacketId())
|
||||||
|
stream.writeInt8(resultCode)
|
||||||
|
stream.writeString(message)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search packet (ID: 0x03)
|
||||||
|
* Search for users by username or public key
|
||||||
|
*/
|
||||||
|
class PacketSearch : Packet() {
|
||||||
|
var privateKey: String = ""
|
||||||
|
var search: String = ""
|
||||||
|
var users: List<SearchUser> = emptyList()
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x03
|
||||||
|
|
||||||
|
override fun receive(stream: Stream) {
|
||||||
|
privateKey = stream.readString()
|
||||||
|
search = stream.readString()
|
||||||
|
val userCount = stream.readInt32()
|
||||||
|
val usersList = mutableListOf<SearchUser>()
|
||||||
|
for (i in 0 until userCount) {
|
||||||
|
val user = SearchUser(
|
||||||
|
publicKey = stream.readString(),
|
||||||
|
title = stream.readString(),
|
||||||
|
username = stream.readString(),
|
||||||
|
verified = stream.readInt8(),
|
||||||
|
online = stream.readInt8()
|
||||||
|
)
|
||||||
|
usersList.add(user)
|
||||||
|
}
|
||||||
|
users = usersList
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
val stream = Stream()
|
||||||
|
stream.writeInt16(getPacketId())
|
||||||
|
stream.writeString(privateKey)
|
||||||
|
stream.writeString(search)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SearchUser(
|
||||||
|
val publicKey: String,
|
||||||
|
val title: String,
|
||||||
|
val username: String,
|
||||||
|
val verified: Int,
|
||||||
|
val online: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User Info packet (ID: 0x01)
|
||||||
|
* Get/Set user information
|
||||||
|
*/
|
||||||
|
class PacketUserInfo : Packet() {
|
||||||
|
var publicKey: String = ""
|
||||||
|
var title: String = ""
|
||||||
|
var username: String = ""
|
||||||
|
var verified: Int = 0
|
||||||
|
var online: Int = 0
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x01
|
||||||
|
|
||||||
|
override fun receive(stream: Stream) {
|
||||||
|
publicKey = stream.readString()
|
||||||
|
title = stream.readString()
|
||||||
|
username = stream.readString()
|
||||||
|
verified = stream.readInt8()
|
||||||
|
online = stream.readInt8()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
val stream = Stream()
|
||||||
|
stream.writeInt16(getPacketId())
|
||||||
|
stream.writeString(publicKey)
|
||||||
|
stream.writeString(title)
|
||||||
|
stream.writeString(username)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Online State packet (ID: 0x05)
|
||||||
|
* Notify about user online status
|
||||||
|
*/
|
||||||
|
class PacketOnlineState : Packet() {
|
||||||
|
var publicKey: String = ""
|
||||||
|
var online: Int = 0
|
||||||
|
var lastSeen: Long = 0
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x05
|
||||||
|
|
||||||
|
override fun receive(stream: Stream) {
|
||||||
|
publicKey = stream.readString()
|
||||||
|
online = stream.readInt8()
|
||||||
|
lastSeen = stream.readInt64()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
val stream = Stream()
|
||||||
|
stream.writeInt16(getPacketId())
|
||||||
|
stream.writeString(publicKey)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Online Subscribe packet (ID: 0x04)
|
||||||
|
* Subscribe to user online status updates
|
||||||
|
*/
|
||||||
|
class PacketOnlineSubscribe : Packet() {
|
||||||
|
var publicKey: String = ""
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x04
|
||||||
|
|
||||||
|
override fun receive(stream: Stream) {
|
||||||
|
publicKey = stream.readString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
val stream = Stream()
|
||||||
|
stream.writeInt16(getPacketId())
|
||||||
|
stream.writeString(publicKey)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
315
app/src/main/java/com/rosetta/messenger/network/Protocol.kt
Normal file
315
app/src/main/java/com/rosetta/messenger/network/Protocol.kt
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
package com.rosetta.messenger.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import okhttp3.*
|
||||||
|
import okio.ByteString
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol connection states
|
||||||
|
*/
|
||||||
|
enum class ProtocolState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
HANDSHAKING,
|
||||||
|
AUTHENTICATED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol client for Rosetta Messenger
|
||||||
|
* Handles WebSocket connection and packet exchange with server
|
||||||
|
*/
|
||||||
|
class Protocol(private val serverAddress: String) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "RosettaProtocol"
|
||||||
|
private const val RECONNECT_INTERVAL = 10000L // 10 seconds
|
||||||
|
private const val MAX_RECONNECT_ATTEMPTS = 5
|
||||||
|
private const val HANDSHAKE_TIMEOUT = 10000L // 10 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private var webSocket: WebSocket? = null
|
||||||
|
private var reconnectAttempts = 0
|
||||||
|
private var isManuallyClosed = false
|
||||||
|
private var handshakeComplete = false
|
||||||
|
private var handshakeJob: Job? = null
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(ProtocolState.DISCONNECTED)
|
||||||
|
val state: StateFlow<ProtocolState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _lastError = MutableStateFlow<String?>(null)
|
||||||
|
val lastError: StateFlow<String?> = _lastError.asStateFlow()
|
||||||
|
|
||||||
|
// Packet waiters - callbacks for specific packet types
|
||||||
|
private val packetWaiters = mutableMapOf<Int, MutableList<(Packet) -> Unit>>()
|
||||||
|
|
||||||
|
// Packet queue for packets sent before handshake complete
|
||||||
|
private val packetQueue = mutableListOf<Packet>()
|
||||||
|
|
||||||
|
// Last used credentials for reconnection
|
||||||
|
private var lastPublicKey: String? = null
|
||||||
|
private var lastPrivateHash: String? = null
|
||||||
|
|
||||||
|
// Supported packets
|
||||||
|
private val supportedPackets = mapOf(
|
||||||
|
0x00 to { PacketHandshake() },
|
||||||
|
0x01 to { PacketUserInfo() },
|
||||||
|
0x02 to { PacketResult() },
|
||||||
|
0x03 to { PacketSearch() },
|
||||||
|
0x04 to { PacketOnlineSubscribe() },
|
||||||
|
0x05 to { PacketOnlineState() }
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Register handshake response handler
|
||||||
|
waitPacket(0x00) { packet ->
|
||||||
|
if (packet is PacketHandshake) {
|
||||||
|
Log.d(TAG, "✅ Handshake response received, protocol version: ${packet.protocolVersion}")
|
||||||
|
handshakeJob?.cancel()
|
||||||
|
handshakeComplete = true
|
||||||
|
_state.value = ProtocolState.AUTHENTICATED
|
||||||
|
flushPacketQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize connection to server
|
||||||
|
*/
|
||||||
|
fun connect() {
|
||||||
|
if (_state.value == ProtocolState.CONNECTING || _state.value == ProtocolState.CONNECTED) {
|
||||||
|
Log.d(TAG, "Already connecting or connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isManuallyClosed = false
|
||||||
|
_state.value = ProtocolState.CONNECTING
|
||||||
|
_lastError.value = null
|
||||||
|
|
||||||
|
Log.d(TAG, "🔌 Connecting to: $serverAddress")
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(serverAddress)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
webSocket = client.newWebSocket(request, object : WebSocketListener() {
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
Log.d(TAG, "✅ WebSocket connected")
|
||||||
|
reconnectAttempts = 0
|
||||||
|
_state.value = ProtocolState.CONNECTED
|
||||||
|
|
||||||
|
// If we have saved credentials, start handshake automatically
|
||||||
|
lastPublicKey?.let { publicKey ->
|
||||||
|
lastPrivateHash?.let { privateHash ->
|
||||||
|
startHandshake(publicKey, privateHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
|
||||||
|
handleMessage(bytes.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
Log.d(TAG, "Received text message (unexpected): $text")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
Log.d(TAG, "WebSocket closing: $code - $reason")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
Log.d(TAG, "WebSocket closed: $code - $reason")
|
||||||
|
handleDisconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||||
|
Log.e(TAG, "❌ WebSocket error: ${t.message}")
|
||||||
|
_lastError.value = t.message
|
||||||
|
handleDisconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start handshake with server
|
||||||
|
*/
|
||||||
|
fun startHandshake(publicKey: String, privateHash: String) {
|
||||||
|
Log.d(TAG, "🤝 Starting handshake...")
|
||||||
|
Log.d(TAG, " Public key: ${publicKey.take(20)}...")
|
||||||
|
Log.d(TAG, " Private hash: ${privateHash.take(20)}...")
|
||||||
|
|
||||||
|
// Save credentials for reconnection
|
||||||
|
lastPublicKey = publicKey
|
||||||
|
lastPrivateHash = privateHash
|
||||||
|
|
||||||
|
if (_state.value != ProtocolState.CONNECTED && _state.value != ProtocolState.AUTHENTICATED) {
|
||||||
|
Log.d(TAG, "Not connected, will handshake after connection")
|
||||||
|
connect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.value = ProtocolState.HANDSHAKING
|
||||||
|
handshakeComplete = false
|
||||||
|
|
||||||
|
val handshake = PacketHandshake().apply {
|
||||||
|
this.publicKey = publicKey
|
||||||
|
this.privateKey = privateHash
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPacketDirect(handshake)
|
||||||
|
|
||||||
|
// Set handshake timeout
|
||||||
|
handshakeJob?.cancel()
|
||||||
|
handshakeJob = scope.launch {
|
||||||
|
delay(HANDSHAKE_TIMEOUT)
|
||||||
|
if (!handshakeComplete) {
|
||||||
|
Log.e(TAG, "❌ Handshake timeout")
|
||||||
|
_lastError.value = "Handshake timeout"
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send packet to server
|
||||||
|
* Packets are queued if handshake is not complete
|
||||||
|
*/
|
||||||
|
fun sendPacket(packet: Packet) {
|
||||||
|
if (!handshakeComplete && packet !is PacketHandshake) {
|
||||||
|
Log.d(TAG, "📦 Queueing packet: ${packet.getPacketId()}")
|
||||||
|
packetQueue.add(packet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendPacketDirect(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendPacketDirect(packet: Packet) {
|
||||||
|
val stream = packet.send()
|
||||||
|
val data = stream.getStream()
|
||||||
|
|
||||||
|
Log.d(TAG, "📤 Sending packet: ${packet.getPacketId()} (${data.size} bytes)")
|
||||||
|
|
||||||
|
webSocket?.send(ByteString.of(*data))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flushPacketQueue() {
|
||||||
|
Log.d(TAG, "📬 Flushing ${packetQueue.size} queued packets")
|
||||||
|
val packets = packetQueue.toList()
|
||||||
|
packetQueue.clear()
|
||||||
|
packets.forEach { sendPacketDirect(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMessage(data: ByteArray) {
|
||||||
|
try {
|
||||||
|
val stream = Stream(data)
|
||||||
|
val packetId = stream.readInt16()
|
||||||
|
|
||||||
|
Log.d(TAG, "📥 Received packet: $packetId")
|
||||||
|
|
||||||
|
val packetFactory = supportedPackets[packetId]
|
||||||
|
if (packetFactory == null) {
|
||||||
|
Log.w(TAG, "Unknown packet ID: $packetId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val packet = packetFactory()
|
||||||
|
packet.receive(stream)
|
||||||
|
|
||||||
|
// Notify waiters
|
||||||
|
packetWaiters[packetId]?.forEach { callback ->
|
||||||
|
try {
|
||||||
|
callback(packet)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error in packet handler: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error parsing packet: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDisconnect() {
|
||||||
|
_state.value = ProtocolState.DISCONNECTED
|
||||||
|
handshakeComplete = false
|
||||||
|
handshakeJob?.cancel()
|
||||||
|
|
||||||
|
if (!isManuallyClosed && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
reconnectAttempts++
|
||||||
|
Log.d(TAG, "🔄 Reconnecting in ${RECONNECT_INTERVAL}ms (attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS)")
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
delay(RECONNECT_INTERVAL)
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
} else if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
Log.e(TAG, "❌ Max reconnect attempts reached")
|
||||||
|
_lastError.value = "Unable to connect to server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register callback for specific packet type
|
||||||
|
*/
|
||||||
|
fun waitPacket(packetId: Int, callback: (Packet) -> Unit) {
|
||||||
|
packetWaiters.getOrPut(packetId) { mutableListOf() }.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister callback for specific packet type
|
||||||
|
*/
|
||||||
|
fun unwaitPacket(packetId: Int, callback: (Packet) -> Unit) {
|
||||||
|
packetWaiters[packetId]?.remove(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from server
|
||||||
|
*/
|
||||||
|
fun disconnect() {
|
||||||
|
Log.d(TAG, "Disconnecting...")
|
||||||
|
isManuallyClosed = true
|
||||||
|
handshakeJob?.cancel()
|
||||||
|
webSocket?.close(1000, "User disconnected")
|
||||||
|
webSocket = null
|
||||||
|
_state.value = ProtocolState.DISCONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if connected and authenticated
|
||||||
|
*/
|
||||||
|
fun isAuthenticated(): Boolean = _state.value == ProtocolState.AUTHENTICATED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if connected (may not be authenticated yet)
|
||||||
|
*/
|
||||||
|
fun isConnected(): Boolean = _state.value == ProtocolState.CONNECTED ||
|
||||||
|
_state.value == ProtocolState.HANDSHAKING ||
|
||||||
|
_state.value == ProtocolState.AUTHENTICATED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear saved credentials
|
||||||
|
*/
|
||||||
|
fun clearCredentials() {
|
||||||
|
lastPublicKey = null
|
||||||
|
lastPrivateHash = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release resources
|
||||||
|
*/
|
||||||
|
fun destroy() {
|
||||||
|
disconnect()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.rosetta.messenger.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton manager for Protocol instance
|
||||||
|
* Ensures single connection across the app
|
||||||
|
*/
|
||||||
|
object ProtocolManager {
|
||||||
|
private const val TAG = "ProtocolManager"
|
||||||
|
|
||||||
|
// Server address - same as React Native version
|
||||||
|
private const val SERVER_ADDRESS = "ws://46.28.71.12:3000"
|
||||||
|
|
||||||
|
private var protocol: Protocol? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create Protocol instance
|
||||||
|
*/
|
||||||
|
fun getProtocol(): Protocol {
|
||||||
|
if (protocol == null) {
|
||||||
|
Log.d(TAG, "Creating new Protocol instance")
|
||||||
|
protocol = Protocol(SERVER_ADDRESS)
|
||||||
|
}
|
||||||
|
return protocol!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection state flow
|
||||||
|
*/
|
||||||
|
val state: StateFlow<ProtocolState>
|
||||||
|
get() = getProtocol().state
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last error flow
|
||||||
|
*/
|
||||||
|
val lastError: StateFlow<String?>
|
||||||
|
get() = getProtocol().lastError
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to server
|
||||||
|
*/
|
||||||
|
fun connect() {
|
||||||
|
getProtocol().connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate with server
|
||||||
|
*/
|
||||||
|
fun authenticate(publicKey: String, privateHash: String) {
|
||||||
|
Log.d(TAG, "Authenticating...")
|
||||||
|
getProtocol().startHandshake(publicKey, privateHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send packet
|
||||||
|
*/
|
||||||
|
fun sendPacket(packet: Packet) {
|
||||||
|
getProtocol().sendPacket(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register packet handler
|
||||||
|
*/
|
||||||
|
fun waitPacket(packetId: Int, callback: (Packet) -> Unit) {
|
||||||
|
getProtocol().waitPacket(packetId, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister packet handler
|
||||||
|
*/
|
||||||
|
fun unwaitPacket(packetId: Int, callback: (Packet) -> Unit) {
|
||||||
|
getProtocol().unwaitPacket(packetId, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect and clear
|
||||||
|
*/
|
||||||
|
fun disconnect() {
|
||||||
|
protocol?.disconnect()
|
||||||
|
protocol?.clearCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy instance completely
|
||||||
|
*/
|
||||||
|
fun destroy() {
|
||||||
|
protocol?.destroy()
|
||||||
|
protocol = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if authenticated
|
||||||
|
*/
|
||||||
|
fun isAuthenticated(): Boolean = protocol?.isAuthenticated() ?: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if connected
|
||||||
|
*/
|
||||||
|
fun isConnected(): Boolean = protocol?.isConnected() ?: false
|
||||||
|
}
|
||||||
146
app/src/main/java/com/rosetta/messenger/network/Stream.kt
Normal file
146
app/src/main/java/com/rosetta/messenger/network/Stream.kt
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package com.rosetta.messenger.network
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binary stream for protocol packets
|
||||||
|
* Matches the React Native implementation exactly
|
||||||
|
*/
|
||||||
|
class Stream(stream: ByteArray = ByteArray(0)) {
|
||||||
|
private var _stream = mutableListOf<Int>()
|
||||||
|
private var _readPointer = 0
|
||||||
|
private var _writePointer = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
_stream = stream.map { it.toInt() and 0xFF }.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStream(): ByteArray {
|
||||||
|
return _stream.map { it.toByte() }.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStream(stream: ByteArray) {
|
||||||
|
_stream = stream.map { it.toInt() and 0xFF }.toMutableList()
|
||||||
|
_readPointer = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeInt8(value: Int) {
|
||||||
|
val negationBit = if (value < 0) 1 else 0
|
||||||
|
val int8Value = Math.abs(value) and 0xFF
|
||||||
|
|
||||||
|
ensureCapacity(_writePointer shr 3)
|
||||||
|
_stream[_writePointer shr 3] = _stream[_writePointer shr 3] or (negationBit shl (7 - (_writePointer and 7)))
|
||||||
|
_writePointer++
|
||||||
|
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
val bit = (int8Value shr (7 - i)) and 1
|
||||||
|
ensureCapacity(_writePointer shr 3)
|
||||||
|
_stream[_writePointer shr 3] = _stream[_writePointer shr 3] or (bit shl (7 - (_writePointer and 7)))
|
||||||
|
_writePointer++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt8(): Int {
|
||||||
|
var value = 0
|
||||||
|
val negationBit = (_stream[_readPointer shr 3] shr (7 - (_readPointer and 7))) and 1
|
||||||
|
_readPointer++
|
||||||
|
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
val bit = (_stream[_readPointer shr 3] shr (7 - (_readPointer and 7))) and 1
|
||||||
|
value = value or (bit shl (7 - i))
|
||||||
|
_readPointer++
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (negationBit == 1) -value else value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeBit(value: Int) {
|
||||||
|
val bit = value and 1
|
||||||
|
ensureCapacity(_writePointer shr 3)
|
||||||
|
_stream[_writePointer shr 3] = _stream[_writePointer shr 3] or (bit shl (7 - (_writePointer and 7)))
|
||||||
|
_writePointer++
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readBit(): Int {
|
||||||
|
val bit = (_stream[_readPointer shr 3] shr (7 - (_readPointer and 7))) and 1
|
||||||
|
_readPointer++
|
||||||
|
return bit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeBoolean(value: Boolean) {
|
||||||
|
writeBit(if (value) 1 else 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readBoolean(): Boolean {
|
||||||
|
return readBit() == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeInt16(value: Int) {
|
||||||
|
writeInt8(value shr 8)
|
||||||
|
writeInt8(value and 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt16(): Int {
|
||||||
|
val high = readInt8() shl 8
|
||||||
|
return high or readInt8()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeInt32(value: Int) {
|
||||||
|
writeInt16(value shr 16)
|
||||||
|
writeInt16(value and 0xFFFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt32(): Int {
|
||||||
|
val high = readInt16() shl 16
|
||||||
|
return high or readInt16()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeInt64(value: Long) {
|
||||||
|
val high = (value shr 32).toInt()
|
||||||
|
val low = (value and 0xFFFFFFFF).toInt()
|
||||||
|
writeInt32(high)
|
||||||
|
writeInt32(low)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt64(): Long {
|
||||||
|
val high = readInt32().toLong()
|
||||||
|
val low = (readInt32().toLong() and 0xFFFFFFFFL)
|
||||||
|
return (high shl 32) or low
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeString(value: String) {
|
||||||
|
writeInt32(value.length)
|
||||||
|
for (char in value) {
|
||||||
|
writeInt16(char.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readString(): String {
|
||||||
|
val length = readInt32()
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (i in 0 until length) {
|
||||||
|
sb.append(readInt16().toChar())
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeBytes(value: ByteArray) {
|
||||||
|
writeInt32(value.size)
|
||||||
|
for (byte in value) {
|
||||||
|
writeInt8(byte.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readBytes(): ByteArray {
|
||||||
|
val length = readInt32()
|
||||||
|
val bytes = ByteArray(length)
|
||||||
|
for (i in 0 until length) {
|
||||||
|
bytes[i] = readInt8().toByte()
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureCapacity(index: Int) {
|
||||||
|
while (_stream.size <= index) {
|
||||||
|
_stream.add(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.rosetta.messenger.ui.auth
|
package com.rosetta.messenger.ui.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
@@ -27,6 +28,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
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
|
||||||
|
|
||||||
@@ -426,6 +428,11 @@ fun SetPasswordScreen(
|
|||||||
accountManager.saveAccount(account)
|
accountManager.saveAccount(account)
|
||||||
accountManager.setCurrentAccount(keyPair.publicKey)
|
accountManager.setCurrentAccount(keyPair.publicKey)
|
||||||
|
|
||||||
|
// 🔌 Connect to server and authenticate
|
||||||
|
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
|
||||||
|
Log.d("SetPasswordScreen", "🔌 Connecting to server...")
|
||||||
|
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
|
||||||
|
|
||||||
onAccountCreated()
|
onAccountCreated()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = "Failed to create account: ${e.message}"
|
error = "Failed to create account: ${e.message}"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.rosetta.messenger.ui.auth
|
package com.rosetta.messenger.ui.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
@@ -29,6 +30,8 @@ import com.rosetta.messenger.R
|
|||||||
import com.rosetta.messenger.crypto.CryptoManager
|
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.network.ProtocolManager
|
||||||
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -249,6 +252,10 @@ fun UnlockScreen(
|
|||||||
name = account.name
|
name = account.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 🔌 Connect to server and authenticate
|
||||||
|
Log.d("UnlockScreen", "🔌 Connecting to server...")
|
||||||
|
ProtocolManager.authenticate(account.publicKey, privateKeyHash)
|
||||||
|
|
||||||
accountManager.setCurrentAccount(publicKey)
|
accountManager.setCurrentAccount(publicKey)
|
||||||
onUnlocked(decryptedAccount)
|
onUnlocked(decryptedAccount)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.airbnb.lottie.compose.*
|
import com.airbnb.lottie.compose.*
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -120,6 +122,9 @@ fun ChatsListScreen(
|
|||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Protocol connection state
|
||||||
|
val protocolState by ProtocolManager.state.collectAsState()
|
||||||
|
|
||||||
var visible by remember { mutableStateOf(false) }
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -283,11 +288,27 @@ fun ChatsListScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
// Title with connection status
|
||||||
|
Column {
|
||||||
Text(
|
Text(
|
||||||
"Rosetta",
|
"Rosetta",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 20.sp
|
fontSize = 20.sp
|
||||||
)
|
)
|
||||||
|
if (protocolState != ProtocolState.AUTHENTICATED) {
|
||||||
|
Text(
|
||||||
|
text = when (protocolState) {
|
||||||
|
ProtocolState.DISCONNECTED -> "Connecting..."
|
||||||
|
ProtocolState.CONNECTING -> "Connecting..."
|
||||||
|
ProtocolState.CONNECTED -> "Authenticating..."
|
||||||
|
ProtocolState.HANDSHAKING -> "Authenticating..."
|
||||||
|
ProtocolState.AUTHENTICATED -> ""
|
||||||
|
},
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = secondaryTextColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
|||||||
Reference in New Issue
Block a user