feat: implement device verification flow with new UI components and protocol handling
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package com.rosetta.messenger.network
|
||||
|
||||
enum class DeviceState(val value: Int) {
|
||||
ONLINE(0),
|
||||
OFFLINE(1);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): DeviceState {
|
||||
return entries.firstOrNull { it.value == value } ?: OFFLINE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeviceVerifyState(val value: Int) {
|
||||
VERIFIED(0),
|
||||
NOT_VERIFIED(1);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): DeviceVerifyState {
|
||||
return entries.firstOrNull { it.value == value } ?: VERIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceEntry(
|
||||
val deviceId: String,
|
||||
val deviceName: String,
|
||||
val deviceOs: String,
|
||||
val deviceStatus: DeviceState,
|
||||
val deviceVerify: DeviceVerifyState
|
||||
)
|
||||
|
||||
class PacketDeviceList : Packet() {
|
||||
var devices: List<DeviceEntry> = emptyList()
|
||||
|
||||
override fun getPacketId(): Int = 0x17
|
||||
|
||||
override fun receive(stream: Stream) {
|
||||
val deviceCount = stream.readInt16()
|
||||
val parsedDevices = mutableListOf<DeviceEntry>()
|
||||
repeat(deviceCount) {
|
||||
parsedDevices += DeviceEntry(
|
||||
deviceId = stream.readString(),
|
||||
deviceName = stream.readString(),
|
||||
deviceOs = stream.readString(),
|
||||
deviceStatus = DeviceState.fromValue(stream.readInt8()),
|
||||
deviceVerify = DeviceVerifyState.fromValue(stream.readInt8())
|
||||
)
|
||||
}
|
||||
devices = parsedDevices
|
||||
}
|
||||
|
||||
override fun send(): Stream {
|
||||
val stream = Stream()
|
||||
stream.writeInt16(getPacketId())
|
||||
stream.writeInt16(devices.size)
|
||||
devices.forEach { device ->
|
||||
stream.writeString(device.deviceId)
|
||||
stream.writeString(device.deviceName)
|
||||
stream.writeString(device.deviceOs)
|
||||
stream.writeInt8(device.deviceStatus.value)
|
||||
stream.writeInt8(device.deviceVerify.value)
|
||||
}
|
||||
return stream
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.rosetta.messenger.network
|
||||
|
||||
data class DeviceInfo(
|
||||
var deviceId: String = "",
|
||||
var deviceName: String = "",
|
||||
var deviceOs: String = ""
|
||||
)
|
||||
|
||||
class PacketDeviceNew : Packet() {
|
||||
var ipAddress: String = ""
|
||||
var device: DeviceInfo = DeviceInfo()
|
||||
|
||||
override fun getPacketId(): Int = 0x09
|
||||
|
||||
override fun receive(stream: Stream) {
|
||||
ipAddress = stream.readString()
|
||||
device = DeviceInfo(
|
||||
deviceId = stream.readString(),
|
||||
deviceName = stream.readString(),
|
||||
deviceOs = stream.readString()
|
||||
)
|
||||
}
|
||||
|
||||
override fun send(): Stream {
|
||||
val stream = Stream()
|
||||
stream.writeInt16(getPacketId())
|
||||
stream.writeString(ipAddress)
|
||||
stream.writeString(device.deviceId)
|
||||
stream.writeString(device.deviceName)
|
||||
stream.writeString(device.deviceOs)
|
||||
return stream
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.rosetta.messenger.network
|
||||
|
||||
enum class DeviceResolveSolution(val value: Int) {
|
||||
ACCEPT(0),
|
||||
DECLINE(1);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): DeviceResolveSolution {
|
||||
return entries.firstOrNull { it.value == value } ?: DECLINE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PacketDeviceResolve : Packet() {
|
||||
var deviceId: String = ""
|
||||
var solution: DeviceResolveSolution = DeviceResolveSolution.DECLINE
|
||||
|
||||
override fun getPacketId(): Int = 0x18
|
||||
|
||||
override fun receive(stream: Stream) {
|
||||
deviceId = stream.readString()
|
||||
solution = DeviceResolveSolution.fromValue(stream.readInt8())
|
||||
}
|
||||
|
||||
override fun send(): Stream {
|
||||
val stream = Stream()
|
||||
stream.writeInt16(getPacketId())
|
||||
stream.writeString(deviceId)
|
||||
stream.writeInt8(solution.value)
|
||||
return stream
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
package com.rosetta.messenger.network
|
||||
|
||||
enum class HandshakeState(val value: Int) {
|
||||
COMPLETED(0),
|
||||
NEED_DEVICE_VERIFICATION(1);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): HandshakeState {
|
||||
return entries.firstOrNull { it.value == value } ?: COMPLETED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class HandshakeDevice(
|
||||
var deviceId: String = "",
|
||||
var deviceName: String = "",
|
||||
var deviceOs: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Handshake packet (ID: 0x00)
|
||||
* First packet sent by client to authenticate with the server
|
||||
@@ -9,6 +26,8 @@ class PacketHandshake : Packet() {
|
||||
var publicKey: String = ""
|
||||
var protocolVersion: Int = 1
|
||||
var heartbeatInterval: Int = 15
|
||||
var device: HandshakeDevice = HandshakeDevice()
|
||||
var handshakeState: HandshakeState = HandshakeState.NEED_DEVICE_VERIFICATION
|
||||
|
||||
override fun getPacketId(): Int = 0x00
|
||||
|
||||
@@ -17,6 +36,12 @@ class PacketHandshake : Packet() {
|
||||
publicKey = stream.readString()
|
||||
protocolVersion = stream.readInt8()
|
||||
heartbeatInterval = stream.readInt8()
|
||||
device = HandshakeDevice(
|
||||
deviceId = stream.readString(),
|
||||
deviceName = stream.readString(),
|
||||
deviceOs = stream.readString()
|
||||
)
|
||||
handshakeState = HandshakeState.fromValue(stream.readInt8())
|
||||
}
|
||||
|
||||
override fun send(): Stream {
|
||||
@@ -26,6 +51,10 @@ class PacketHandshake : Packet() {
|
||||
stream.writeString(publicKey)
|
||||
stream.writeInt8(protocolVersion)
|
||||
stream.writeInt8(heartbeatInterval)
|
||||
stream.writeString(device.deviceId)
|
||||
stream.writeString(device.deviceName)
|
||||
stream.writeString(device.deviceOs)
|
||||
stream.writeInt8(handshakeState.value)
|
||||
return stream
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class PacketMessage : Packet() {
|
||||
var timestamp: Long = 0
|
||||
var privateKey: String = "" // Hash приватного ключа (для авторизации)
|
||||
var messageId: String = ""
|
||||
var aesChachaKey: String = "" // ChaCha key+nonce зашифрованный приватным ключом отправителя
|
||||
var attachments: List<MessageAttachment> = emptyList()
|
||||
|
||||
override fun getPacketId(): Int = 0x06
|
||||
@@ -36,6 +37,7 @@ class PacketMessage : Packet() {
|
||||
))
|
||||
}
|
||||
attachments = attachmentsList
|
||||
aesChachaKey = stream.readString()
|
||||
}
|
||||
|
||||
override fun send(): Stream {
|
||||
@@ -56,6 +58,7 @@ class PacketMessage : Packet() {
|
||||
stream.writeString(attachment.blob)
|
||||
stream.writeInt8(attachment.type.value)
|
||||
}
|
||||
stream.writeString(aesChachaKey)
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.rosetta.messenger.network
|
||||
|
||||
enum class SyncStatus(val value: Int) {
|
||||
NOT_NEEDED(0),
|
||||
BATCH_START(1),
|
||||
BATCH_END(2);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): SyncStatus {
|
||||
return entries.firstOrNull { it.value == value } ?: NOT_NEEDED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync packet (ID: 0x19)
|
||||
* Используется для батчевой синхронизации сообщений и read-статусов
|
||||
*/
|
||||
class PacketSync : Packet() {
|
||||
var status: SyncStatus = SyncStatus.NOT_NEEDED
|
||||
var timestamp: Long = 0
|
||||
|
||||
override fun getPacketId(): Int = 0x19
|
||||
|
||||
override fun receive(stream: Stream) {
|
||||
status = SyncStatus.fromValue(stream.readInt8())
|
||||
timestamp = stream.readInt64()
|
||||
}
|
||||
|
||||
override fun send(): Stream {
|
||||
val stream = Stream()
|
||||
stream.writeInt16(getPacketId())
|
||||
stream.writeInt8(status.value)
|
||||
stream.writeInt64(timestamp)
|
||||
return stream
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ enum class ProtocolState {
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
HANDSHAKING,
|
||||
DEVICE_VERIFICATION_REQUIRED,
|
||||
AUTHENTICATED
|
||||
}
|
||||
|
||||
@@ -102,6 +103,7 @@ class Protocol(
|
||||
// Last used credentials for reconnection
|
||||
private var lastPublicKey: String? = null
|
||||
private var lastPrivateHash: String? = null
|
||||
private var lastDevice: HandshakeDevice = HandshakeDevice()
|
||||
|
||||
// Getters for ProtocolManager to fetch own profile
|
||||
fun getPublicKey(): String? = lastPublicKey
|
||||
@@ -121,21 +123,40 @@ class Protocol(
|
||||
0x06 to { PacketMessage() },
|
||||
0x07 to { PacketRead() },
|
||||
0x08 to { PacketDelivery() },
|
||||
0x09 to { PacketChunk() },
|
||||
0x0B to { PacketTyping() }
|
||||
0x09 to { PacketDeviceNew() },
|
||||
0x0B to { PacketTyping() },
|
||||
0x0F to { PacketRequestTransport() },
|
||||
0x17 to { PacketDeviceList() },
|
||||
0x18 to { PacketDeviceResolve() },
|
||||
0x19 to { PacketSync() }
|
||||
)
|
||||
|
||||
init {
|
||||
// Register handshake response handler
|
||||
waitPacket(0x00) { packet ->
|
||||
if (packet is PacketHandshake) {
|
||||
log("✅ HANDSHAKE SUCCESS: protocol=${packet.protocolVersion}, heartbeat=${packet.heartbeatInterval}s")
|
||||
handshakeJob?.cancel()
|
||||
handshakeComplete = true
|
||||
setState(ProtocolState.AUTHENTICATED, "Handshake response received")
|
||||
flushPacketQueue()
|
||||
|
||||
// Start heartbeat with interval from server
|
||||
|
||||
when (packet.handshakeState) {
|
||||
HandshakeState.COMPLETED -> {
|
||||
log("✅ HANDSHAKE COMPLETE: protocol=${packet.protocolVersion}, heartbeat=${packet.heartbeatInterval}s")
|
||||
handshakeComplete = true
|
||||
setState(ProtocolState.AUTHENTICATED, "Handshake completed")
|
||||
flushPacketQueue()
|
||||
}
|
||||
|
||||
HandshakeState.NEED_DEVICE_VERIFICATION -> {
|
||||
log("🔐 HANDSHAKE NEEDS DEVICE VERIFICATION")
|
||||
handshakeComplete = false
|
||||
setState(
|
||||
ProtocolState.DEVICE_VERIFICATION_REQUIRED,
|
||||
"Handshake requires device verification"
|
||||
)
|
||||
packetQueue.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Keep heartbeat in both handshake states to maintain server session.
|
||||
startHeartbeat(packet.heartbeatInterval)
|
||||
}
|
||||
}
|
||||
@@ -171,7 +192,10 @@ class Protocol(
|
||||
val currentState = _state.value
|
||||
val socketAlive = webSocket != null
|
||||
|
||||
if (currentState == ProtocolState.AUTHENTICATED) {
|
||||
if (
|
||||
currentState == ProtocolState.AUTHENTICATED ||
|
||||
currentState == ProtocolState.DEVICE_VERIFICATION_REQUIRED
|
||||
) {
|
||||
val sent = webSocket?.send("heartbeat") ?: false
|
||||
if (sent) {
|
||||
log("💓 Heartbeat OK (socket=$socketAlive, state=$currentState)")
|
||||
@@ -184,7 +208,9 @@ class Protocol(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log("💔 HEARTBEAT SKIPPED: state=$currentState (not AUTHENTICATED), socket=$socketAlive")
|
||||
log(
|
||||
"💔 HEARTBEAT SKIPPED: state=$currentState (not ready), socket=$socketAlive"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log("💔 HEARTBEAT EXCEPTION: ${e.message}")
|
||||
@@ -200,7 +226,11 @@ class Protocol(
|
||||
log("🔌 CONNECT CALLED: currentState=$currentState, reconnectAttempts=$reconnectAttempts, isConnecting=$isConnecting")
|
||||
|
||||
// КРИТИЧНО: Если уже подключены и аутентифицированы - не переподключаемся!
|
||||
if (currentState == ProtocolState.AUTHENTICATED || currentState == ProtocolState.HANDSHAKING) {
|
||||
if (
|
||||
currentState == ProtocolState.AUTHENTICATED ||
|
||||
currentState == ProtocolState.HANDSHAKING ||
|
||||
currentState == ProtocolState.DEVICE_VERIFICATION_REQUIRED
|
||||
) {
|
||||
log("✅ Already authenticated or handshaking, skipping connect()")
|
||||
return
|
||||
}
|
||||
@@ -264,7 +294,7 @@ class Protocol(
|
||||
lastPublicKey?.let { publicKey ->
|
||||
lastPrivateHash?.let { privateHash ->
|
||||
log("🤝 Auto-starting handshake with saved credentials")
|
||||
startHandshake(publicKey, privateHash)
|
||||
startHandshake(publicKey, privateHash, lastDevice)
|
||||
}
|
||||
} ?: log("⚠️ No saved credentials, waiting for manual handshake")
|
||||
} else {
|
||||
@@ -308,19 +338,24 @@ class Protocol(
|
||||
/**
|
||||
* Start handshake with server
|
||||
*/
|
||||
fun startHandshake(publicKey: String, privateHash: String) {
|
||||
fun startHandshake(publicKey: String, privateHash: String, device: HandshakeDevice) {
|
||||
log("🤝 Starting handshake...")
|
||||
log(" Public key: ${publicKey.take(20)}...")
|
||||
log(" Private hash: ${privateHash.take(20)}...")
|
||||
log(" Current state: ${_state.value}")
|
||||
|
||||
// Detect account switch: already authenticated but with different credentials
|
||||
val switchingAccount = (_state.value == ProtocolState.AUTHENTICATED || _state.value == ProtocolState.HANDSHAKING) &&
|
||||
val switchingAccount = (
|
||||
_state.value == ProtocolState.AUTHENTICATED ||
|
||||
_state.value == ProtocolState.HANDSHAKING ||
|
||||
_state.value == ProtocolState.DEVICE_VERIFICATION_REQUIRED
|
||||
) &&
|
||||
lastPublicKey != null && lastPublicKey != publicKey
|
||||
|
||||
// Save credentials for reconnection
|
||||
lastPublicKey = publicKey
|
||||
lastPrivateHash = privateHash
|
||||
lastDevice = device
|
||||
|
||||
// If switching accounts, force disconnect and reconnect with new credentials
|
||||
if (switchingAccount) {
|
||||
@@ -340,6 +375,11 @@ class Protocol(
|
||||
log("⚠️ HANDSHAKE IGNORED: Already authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
if (_state.value == ProtocolState.DEVICE_VERIFICATION_REQUIRED) {
|
||||
log("⚠️ HANDSHAKE IGNORED: Waiting for device verification")
|
||||
return
|
||||
}
|
||||
|
||||
if (_state.value != ProtocolState.CONNECTED) {
|
||||
log("⚠️ HANDSHAKE DEFERRED: Not connected (state=${_state.value}), will handshake after connection")
|
||||
@@ -353,6 +393,7 @@ class Protocol(
|
||||
val handshake = PacketHandshake().apply {
|
||||
this.publicKey = publicKey
|
||||
this.privateKey = privateHash
|
||||
this.device = device
|
||||
}
|
||||
|
||||
sendPacketDirect(handshake)
|
||||
@@ -411,10 +452,12 @@ class Protocol(
|
||||
}
|
||||
|
||||
try {
|
||||
// 📦 Используем Chunker для отправки (как в Desktop)
|
||||
// Если пакет большой, он будет разбит на части автоматически
|
||||
val chunker = Chunker(socket) { msg -> log(msg) }
|
||||
chunker.send(stream)
|
||||
val sent = socket.send(ByteString.of(*data))
|
||||
if (!sent) {
|
||||
log("❌ WebSocket rejected packet ${packet.getPacketId()}, re-queueing")
|
||||
packetQueue.add(packet)
|
||||
return
|
||||
}
|
||||
log("✅ Packet ${packet.getPacketId()} sent successfully")
|
||||
} catch (e: Exception) {
|
||||
log("❌ Exception sending packet ${packet.getPacketId()}: ${e.message}")
|
||||
@@ -554,6 +597,7 @@ class Protocol(
|
||||
*/
|
||||
fun isConnected(): Boolean = _state.value == ProtocolState.CONNECTED ||
|
||||
_state.value == ProtocolState.HANDSHAKING ||
|
||||
_state.value == ProtocolState.DEVICE_VERIFICATION_REQUIRED ||
|
||||
_state.value == ProtocolState.AUTHENTICATED
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.rosetta.messenger.network
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.rosetta.messenger.data.AccountManager
|
||||
import com.rosetta.messenger.data.MessageRepository
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.security.SecureRandom
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
@@ -21,6 +24,9 @@ object ProtocolManager {
|
||||
|
||||
// Server address - same as React Native version
|
||||
private const val SERVER_ADDRESS = "ws://46.28.71.12:3000"
|
||||
private const val DEVICE_PREFS = "rosetta_protocol"
|
||||
private const val DEVICE_ID_KEY = "device_id"
|
||||
private const val DEVICE_ID_LENGTH = 128
|
||||
|
||||
private var protocol: Protocol? = null
|
||||
private var messageRepository: MessageRepository? = null
|
||||
@@ -36,6 +42,13 @@ object ProtocolManager {
|
||||
private val _typingUsers = MutableStateFlow<Set<String>>(emptySet())
|
||||
val typingUsers: StateFlow<Set<String>> = _typingUsers.asStateFlow()
|
||||
|
||||
// Connected devices and pending verification requests
|
||||
private val _devices = MutableStateFlow<List<DeviceEntry>>(emptyList())
|
||||
val devices: StateFlow<List<DeviceEntry>> = _devices.asStateFlow()
|
||||
|
||||
private val _pendingDeviceVerification = MutableStateFlow<DeviceEntry?>(null)
|
||||
val pendingDeviceVerification: StateFlow<DeviceEntry?> = _pendingDeviceVerification.asStateFlow()
|
||||
|
||||
// Сигнал обновления own profile (username/name загружены с сервера)
|
||||
private val _ownProfileUpdated = MutableStateFlow(0L)
|
||||
val ownProfileUpdated: StateFlow<Long> = _ownProfileUpdated.asStateFlow()
|
||||
@@ -50,6 +63,10 @@ object ProtocolManager {
|
||||
|
||||
// 🚀 Флаг для включения UI логов (по умолчанию ВЫКЛЮЧЕНО - это вызывало ANR!)
|
||||
private var uiLogsEnabled = false
|
||||
private var lastProtocolState: ProtocolState? = null
|
||||
@Volatile private var syncBatchInProgress = false
|
||||
@Volatile private var resyncRequiredAfterAccountInit = false
|
||||
private val inboundPacketTasks = AtomicInteger(0)
|
||||
|
||||
fun addLog(message: String) {
|
||||
val timestamp = dateFormat.format(Date())
|
||||
@@ -85,7 +102,11 @@ object ProtocolManager {
|
||||
private fun setupStateMonitoring() {
|
||||
scope.launch {
|
||||
getProtocol().state.collect { newState ->
|
||||
// State monitoring without logging
|
||||
val previous = lastProtocolState
|
||||
if (newState == ProtocolState.AUTHENTICATED && previous != ProtocolState.AUTHENTICATED) {
|
||||
onAuthenticated()
|
||||
}
|
||||
lastProtocolState = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,9 +116,13 @@ object ProtocolManager {
|
||||
* Должен вызываться после авторизации пользователя
|
||||
*/
|
||||
fun initializeAccount(publicKey: String, privateKey: String) {
|
||||
val start = System.currentTimeMillis()
|
||||
syncBatchInProgress = false
|
||||
messageRepository?.initialize(publicKey, privateKey)
|
||||
}
|
||||
if (resyncRequiredAfterAccountInit || protocol?.isAuthenticated() == true) {
|
||||
resyncRequiredAfterAccountInit = false
|
||||
requestSynchronize()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка обработчиков пакетов
|
||||
@@ -115,9 +140,17 @@ object ProtocolManager {
|
||||
}
|
||||
send(deliveryPacket)
|
||||
|
||||
scope.launch {
|
||||
launchInboundPacketTask {
|
||||
val repository = messageRepository
|
||||
if (repository == null || !repository.isInitialized()) {
|
||||
requireResyncAfterAccountInit("⏳ Incoming message before account init, scheduling re-sync")
|
||||
return@launchInboundPacketTask
|
||||
}
|
||||
try {
|
||||
messageRepository?.handleIncomingMessage(messagePacket)
|
||||
repository.handleIncomingMessage(messagePacket)
|
||||
if (!syncBatchInProgress) {
|
||||
repository.updateLastSyncTimestamp(messagePacket.timestamp)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Silent error handling
|
||||
}
|
||||
@@ -128,8 +161,13 @@ object ProtocolManager {
|
||||
waitPacket(0x08) { packet ->
|
||||
val deliveryPacket = packet as PacketDelivery
|
||||
|
||||
scope.launch {
|
||||
messageRepository?.handleDelivery(deliveryPacket)
|
||||
launchInboundPacketTask {
|
||||
val repository = messageRepository
|
||||
if (repository == null || !repository.isInitialized()) {
|
||||
requireResyncAfterAccountInit("⏳ Delivery status before account init, scheduling re-sync")
|
||||
return@launchInboundPacketTask
|
||||
}
|
||||
repository.handleDelivery(deliveryPacket)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,10 +176,41 @@ object ProtocolManager {
|
||||
waitPacket(0x07) { packet ->
|
||||
val readPacket = packet as PacketRead
|
||||
|
||||
scope.launch {
|
||||
messageRepository?.handleRead(readPacket)
|
||||
launchInboundPacketTask {
|
||||
val repository = messageRepository
|
||||
if (repository == null || !repository.isInitialized()) {
|
||||
requireResyncAfterAccountInit("⏳ Read status before account init, scheduling re-sync")
|
||||
return@launchInboundPacketTask
|
||||
}
|
||||
repository.handleRead(readPacket)
|
||||
if (!syncBatchInProgress) {
|
||||
repository.updateLastSyncTimestamp(System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔐 New device login attempt (0x09) — desktop parity (system Safe message)
|
||||
waitPacket(0x09) { packet ->
|
||||
val devicePacket = packet as PacketDeviceNew
|
||||
|
||||
addLog(
|
||||
"🔐 DEVICE LOGIN ATTEMPT: ip=${devicePacket.ipAddress}, device=${devicePacket.device.deviceName}, os=${devicePacket.device.deviceOs}"
|
||||
)
|
||||
|
||||
launchInboundPacketTask {
|
||||
messageRepository?.addDeviceLoginSystemMessage(
|
||||
ipAddress = devicePacket.ipAddress,
|
||||
deviceId = devicePacket.device.deviceId,
|
||||
deviceName = devicePacket.device.deviceName,
|
||||
deviceOs = devicePacket.device.deviceOs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 🔄 Обработчик батчевой синхронизации (0x19)
|
||||
waitPacket(0x19) { packet ->
|
||||
handleSyncPacket(packet as PacketSync)
|
||||
}
|
||||
|
||||
// 🟢 Обработчик онлайн-статуса (0x05)
|
||||
waitPacket(0x05) { packet ->
|
||||
@@ -168,6 +237,16 @@ object ProtocolManager {
|
||||
_typingUsers.value = _typingUsers.value - typingPacket.fromPublicKey
|
||||
}
|
||||
}
|
||||
|
||||
// 📱 Обработчик списка устройств (0x17)
|
||||
waitPacket(0x17) { packet ->
|
||||
val devicesPacket = packet as PacketDeviceList
|
||||
val parsedDevices = devicesPacket.devices
|
||||
_devices.value = parsedDevices
|
||||
_pendingDeviceVerification.value = parsedDevices.firstOrNull { device ->
|
||||
device.deviceVerify == DeviceVerifyState.NOT_VERIFIED
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 Обработчик поиска/user info (0x03)
|
||||
// Обновляет информацию о пользователе в диалогах когда приходит ответ от сервера
|
||||
@@ -224,6 +303,78 @@ object ProtocolManager {
|
||||
TransportManager.setTransportServer(transportPacket.transportServer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchInboundPacketTask(block: suspend () -> Unit) {
|
||||
inboundPacketTasks.incrementAndGet()
|
||||
scope.launch {
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
inboundPacketTasks.decrementAndGet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireResyncAfterAccountInit(reason: String) {
|
||||
if (!resyncRequiredAfterAccountInit) {
|
||||
addLog(reason)
|
||||
}
|
||||
resyncRequiredAfterAccountInit = true
|
||||
}
|
||||
|
||||
private suspend fun waitInboundPacketTasks(timeoutMs: Long = 15_000L) {
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
while (inboundPacketTasks.get() > 0 && System.currentTimeMillis() < deadline) {
|
||||
delay(25)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAuthenticated() {
|
||||
TransportManager.requestTransportServer()
|
||||
fetchOwnProfile()
|
||||
requestSynchronize()
|
||||
}
|
||||
|
||||
private fun requestSynchronize() {
|
||||
scope.launch {
|
||||
val repository = messageRepository
|
||||
if (repository == null || !repository.isInitialized()) {
|
||||
requireResyncAfterAccountInit("⏳ Sync postponed until account is initialized")
|
||||
return@launch
|
||||
}
|
||||
val lastSync = repository.getLastSyncTimestamp()
|
||||
sendSynchronize(lastSync)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSynchronize(timestamp: Long) {
|
||||
val packet = PacketSync().apply {
|
||||
status = SyncStatus.NOT_NEEDED
|
||||
this.timestamp = timestamp
|
||||
}
|
||||
send(packet)
|
||||
}
|
||||
|
||||
private fun handleSyncPacket(packet: PacketSync) {
|
||||
scope.launch {
|
||||
when (packet.status) {
|
||||
SyncStatus.BATCH_START -> {
|
||||
syncBatchInProgress = true
|
||||
}
|
||||
SyncStatus.BATCH_END -> {
|
||||
syncBatchInProgress = true
|
||||
waitInboundPacketTasks()
|
||||
messageRepository?.updateLastSyncTimestamp(packet.timestamp)
|
||||
syncBatchInProgress = false
|
||||
sendSynchronize(packet.timestamp)
|
||||
}
|
||||
SyncStatus.NOT_NEEDED -> {
|
||||
syncBatchInProgress = false
|
||||
messageRepository?.updateLastSyncTimestamp(packet.timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create Protocol instance
|
||||
@@ -258,14 +409,8 @@ object ProtocolManager {
|
||||
* Authenticate with server
|
||||
*/
|
||||
fun authenticate(publicKey: String, privateHash: String) {
|
||||
getProtocol().startHandshake(publicKey, privateHash)
|
||||
|
||||
// 🚀 Запрашиваем транспортный сервер и own profile после авторизации
|
||||
scope.launch {
|
||||
delay(500) // Даём время на завершение handshake
|
||||
TransportManager.requestTransportServer()
|
||||
fetchOwnProfile()
|
||||
}
|
||||
val device = buildHandshakeDevice()
|
||||
getProtocol().startHandshake(publicKey, privateHash, device)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,6 +525,28 @@ object ProtocolManager {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a pending device login request.
|
||||
*/
|
||||
fun acceptDevice(deviceId: String) {
|
||||
val packet = PacketDeviceResolve().apply {
|
||||
this.deviceId = deviceId
|
||||
this.solution = DeviceResolveSolution.ACCEPT
|
||||
}
|
||||
send(packet)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decline a pending device login request.
|
||||
*/
|
||||
fun declineDevice(deviceId: String) {
|
||||
val packet = PacketDeviceResolve().apply {
|
||||
this.deviceId = deviceId
|
||||
this.solution = DeviceResolveSolution.DECLINE
|
||||
}
|
||||
send(packet)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send packet (simplified)
|
||||
@@ -408,6 +575,50 @@ object ProtocolManager {
|
||||
fun unwaitPacket(packetId: Int, callback: (Packet) -> Unit) {
|
||||
getProtocol().unwaitPacket(packetId, callback)
|
||||
}
|
||||
|
||||
private fun buildHandshakeDevice(): HandshakeDevice {
|
||||
val context = appContext
|
||||
val deviceId = if (context != null) {
|
||||
getOrCreateDeviceId(context)
|
||||
} else {
|
||||
generateDeviceId()
|
||||
}
|
||||
val manufacturer = Build.MANUFACTURER.orEmpty().trim()
|
||||
val model = Build.MODEL.orEmpty().trim()
|
||||
val name = listOf(manufacturer, model)
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
.joinToString(" ")
|
||||
.ifBlank { "Android Device" }
|
||||
val os = "Android ${Build.VERSION.RELEASE ?: "Unknown"}"
|
||||
|
||||
return HandshakeDevice(
|
||||
deviceId = deviceId,
|
||||
deviceName = name,
|
||||
deviceOs = os
|
||||
)
|
||||
}
|
||||
|
||||
private fun getOrCreateDeviceId(context: Context): String {
|
||||
val prefs = context.getSharedPreferences(DEVICE_PREFS, Context.MODE_PRIVATE)
|
||||
val cached = prefs.getString(DEVICE_ID_KEY, null)
|
||||
if (!cached.isNullOrBlank()) {
|
||||
return cached
|
||||
}
|
||||
val newId = generateDeviceId()
|
||||
prefs.edit().putString(DEVICE_ID_KEY, newId).apply()
|
||||
return newId
|
||||
}
|
||||
|
||||
private fun generateDeviceId(): String {
|
||||
val chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
val random = SecureRandom()
|
||||
return buildString(DEVICE_ID_LENGTH) {
|
||||
repeat(DEVICE_ID_LENGTH) {
|
||||
append(chars[random.nextInt(chars.length)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect and clear
|
||||
@@ -415,6 +626,10 @@ object ProtocolManager {
|
||||
fun disconnect() {
|
||||
protocol?.disconnect()
|
||||
protocol?.clearCredentials()
|
||||
_devices.value = emptyList()
|
||||
_pendingDeviceVerification.value = null
|
||||
syncBatchInProgress = false
|
||||
inboundPacketTasks.set(0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -423,6 +638,10 @@ object ProtocolManager {
|
||||
fun destroy() {
|
||||
protocol?.destroy()
|
||||
protocol = null
|
||||
_devices.value = emptyList()
|
||||
_pendingDeviceVerification.value = null
|
||||
syncBatchInProgress = false
|
||||
inboundPacketTasks.set(0)
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user