Open the Android DM device panel and validate snapshot decoding

This commit is contained in:
evgeny committed 2026-11-03 14:16:45 +01:00
1 parent 317ea6565f
commit 9e7366d5fc
3 files changed
+57

No files matched your search

@@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
@@ -42,6 +43,8 @@ fun DmChatScreen(conv: DmConversation, messages: List<Message>, viewModel: ChatV
onRecordVideo: () -> Unit, onPlayVideo: (Message) -> Unit,
onCapturePhoto: () -> Unit, onViewPhoto: (Message) -> Unit) {
val target = ConversationTarget.Pm(conv.convId, conv.generation)
var showDevices by rememberSaveable(conv.convId, conv.generation) { mutableStateOf(false) }
if (showDevices && conv.mode == 1) DmMembersSheet(conv) { showDevices = false }
val playback by viewModel.playbackState.collectAsState()
val callAvailable by viewModel.dmCallAvailable.collectAsState()
val callState by viewModel.callState.collectAsState()
@@ -118,6 +121,9 @@ fun DmChatScreen(conv: DmConversation, messages: List<Message>, viewModel: ChatV
}, navigationIcon = {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back)) }
}, actions = {
if (conv.mode == 1) IconButton(onClick = { showDevices = true }) {
Icon(Icons.Filled.Group, stringResource(R.string.dm_members_title))
}
IconButton(onClick = { viewModel.showPmInvite(conv.peerNodeId, conv.displayName, target.nativeId) }, enabled = !conv.closed) {
Icon(Icons.Filled.Add, stringResource(R.string.invite_to_group))
}
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="dm_members_title">Устройства беседы</string>
<string name="dm_my_devices">Мои устройства</string>
<string name="dm_peer_devices">Устройства собеседника</string>
<string name="dm_members_unavailable">Не удалось обновить список устройств</string>
<string name="dm_mode_title">Устройства для этой беседы</string>
<string name="dm_mode_groups">С подгруппами устройств</string>
<string name="dm_mode_device">Только эти устройства</string>
@@ -0,0 +1,47 @@
package com.utun.chat.data
import org.junit.Assert.*
import org.junit.Test
import java.nio.ByteBuffer
import java.nio.ByteOrder
class DmMembersTest {
private val target = ConversationTarget.Pm("42", 7)
private fun frame(vararg entries: Triple<Int, Long, Boolean>): ByteArray {
val data = ByteArray(13 + entries.size * (MEMBER_WIRE_SIZE + 1))
data[0] = 2; data[1] = '4'.code.toByte(); data[2] = '2'.code.toByte()
ByteBuffer.wrap(data, 3, 10).order(ByteOrder.LITTLE_ENDIAN).putLong(7).putShort(entries.size.toShort())
entries.forEachIndexed { i, (side, node, self) ->
val offset = 13 + i * (MEMBER_WIRE_SIZE + 1); data[offset] = side.toByte()
ByteBuffer.wrap(data, offset + 1, 8).order(ByteOrder.LITTLE_ENDIAN).putLong(node)
data[offset + 9] = if (self) 1 else 0; data[offset + 10] = if (self) 1 else 0
"node$node".toByteArray().copyInto(data, offset + 16)
}
return data
}
@Test fun separatesOwnAndPeerDevicesIncludingOffline() {
val members = parseDmMembers(frame(Triple(1, 9L, false), Triple(0, 7L, false), Triple(0, 21L, true)), target)
assertEquals(listOf(21L, 7L, 9L), members.map { it.member.nodeId })
assertEquals(listOf(0, 0, 1), members.map { it.side })
assertEquals(listOf(true, false, false), members.map { it.member.online })
assertTrue(members.first().member.isSelf)
}
@Test fun emptyOrFallbackSideDoesNotRequireAGroup() {
assertTrue(parseDmMembers(frame(), target).isEmpty())
val members = parseDmMembers(frame(Triple(1, 9L, false)), target)
assertEquals(1, members.size); assertEquals("", members.single().member.devgroup)
}
@Test fun rejectsOtherConversationGenerationAndMalformedRecords() {
val data = frame(Triple(0, 7L, true), Triple(1, 9L, false))
fun rejected(bytes: ByteArray, selected: ConversationTarget.Pm = target) {
assertThrows(IllegalArgumentException::class.java) { parseDmMembers(bytes, selected) }
}
rejected(data, target.copy(id = "43")); rejected(data, target.copy(generation = 8))
rejected(data.copyOf(data.size - 1)); rejected(data + 0.toByte())
rejected(frame(Triple(0, 7L, true), Triple(1, 7L, false)))
rejected(frame(Triple(2, 9L, false))); rejected(frame(Triple(1, 0L, false)))
}
}