diff --git a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml
index fbdb5589..b18aa2d8 100644
--- a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml
+++ b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml
@@ -48,6 +48,9 @@
Не слушать этого участника в рации
Слушать этого участника в рации
Не удалось изменить настройку прослушивания рации
+ Рация: молчит
+ Рация: говорит
+ Рация: заглушён
"Рация: PTT / автоматическая передача по голосу"
"Плавающая кнопка остановлена"
"Текущий звонок"
diff --git a/tools/chatgui-android/app/src/main/res/values/strings.xml b/tools/chatgui-android/app/src/main/res/values/strings.xml
index 319394a6..bb3552d7 100644
--- a/tools/chatgui-android/app/src/main/res/values/strings.xml
+++ b/tools/chatgui-android/app/src/main/res/values/strings.xml
@@ -48,6 +48,9 @@
Mute this member in radio
Listen to this member in radio
Cannot change radio listening setting
+ Radio: silent
+ Radio: speaking
+ Radio: muted
"Radio: PTT / automatic voice transmission"
"Floating button stopped"
"Ongoing call"
diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/RadioTalkStateTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/RadioTalkStateTest.kt
new file mode 100644
index 00000000..b3ae8863
--- /dev/null
+++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/RadioTalkStateTest.kt
@@ -0,0 +1,52 @@
+package com.utun.chat.data
+
+import org.junit.Assert.*
+import org.junit.Test
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+
+class RadioTalkStateTest {
+ private fun event(group: Long = 7, node: Long = 42, on: Boolean = true): RadioTalkEvent {
+ val bytes = ByteBuffer.allocate(19).order(ByteOrder.LITTLE_ENDIAN)
+ .putLong(group).putLong(node).putShort(65535.toShort()).put(if (on) 1.toByte() else 0.toByte()).array()
+ return requireNotNull(parseRadioTalkEvent(bytes))
+ }
+
+ @Test fun stopRemovesOnlyItsSourceAndMuteDoesNotSuppressSpeech() {
+ var state = RadioTalkState(7).onTalk(event()).onTalk(event(node = 43))
+ assertEquals(setOf(42L, 43L), state.speakers)
+ val muted = ChatMember(42, "Alice", radioMuted = true)
+ assertTrue(muted.radioMuted && muted.nodeId in state.speakers)
+ assertSame(state, state.onTalk(event()))
+ state = state.onTalk(event(on = false))
+ assertEquals(setOf(43L), state.speakers)
+ assertSame(state, state.onTalk(event(on = false)))
+ assertTrue(state.onTalk(event(node = 43, on = false)).speakers.isEmpty())
+ }
+
+ @Test fun activeGroupOwnsSpeakersIndependentlyOfDisplayedMemberSnapshots() {
+ val group = 0xfedcba9876543210uL.toLong()
+ val state = RadioTalkState(group).onTalk(event(group))
+ assertEquals(setOf(42L), state.speakers)
+ assertSame(state, state.onTalk(event(group = 8)))
+ assertSame(state, state.onTalk(event(group = 8, on = false)))
+ val refreshed = ChatMember(42, "Updated", radioActive = false, radioMuted = true)
+ assertTrue(refreshed.nodeId in state.speakers)
+ val switched = RadioTalkState(8)
+ assertSame(switched, switched.onTalk(event(group)))
+ val stopped = RadioTalkState()
+ assertSame(stopped, stopped.onTalk(event(group)))
+ assertTrue(RadioTalkState(group).speakers.isEmpty())
+ }
+
+ @Test fun malformedEventsCannotChangeTheSpeakerState() {
+ assertNull(parseRadioTalkEvent(null)); assertNull(parseRadioTalkEvent(ByteArray(18)))
+ assertNull(parseRadioTalkEvent(ByteArray(20))); assertNull(parseRadioTalkEvent(ByteArray(19)))
+ val bytes = ByteBuffer.allocate(19).order(ByteOrder.LITTLE_ENDIAN).putLong(7).putLong(42).putShort(1).put(2).array()
+ assertNull(parseRadioTalkEvent(bytes))
+ bytes[18] = 1
+ assertEquals(1, requireNotNull(parseRadioTalkEvent(bytes)).stream)
+ assertEquals(65535, event().stream)
+ assertNull(parseRadioTalkEvent(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).putLong(0, 0).array()))
+ }
+}