feat: implement device verification flow with new UI components and protocol handling

This commit is contained in:
2026-02-18 04:40:22 +05:00
parent edff3b32c3
commit cacd6dc029
24 changed files with 1645 additions and 195 deletions

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
/**

View File

@@ -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()
}