Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -58,6 +59,7 @@ import com.k1af.ft8af.rigs.InstructionSet
import com.k1af.ft8af.ui.ToastMessage
import com.k1af.ft8af.x6100.X6100Radio
import com.k1af.ft8af.x6100.XieguRadioFactory
import kotlinx.coroutines.delay
import radio.ks3ckc.ft8af.theme.*

// ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -251,25 +253,25 @@ fun FlexRadioPickerDialog(
var ipText by remember { mutableStateOf("") }
val discoveredRadios = remember { mutableStateListOf<FlexRadio>() }

// Subscribe to FlexRadioFactory discovery events
DisposableEffect(Unit) {
// Poll discovery while the dialog is open so radios found *after* opening
// (discovery typically takes a few seconds) appear live — no need to close
// and reopen the picker. The previous one-shot event listener only fired on
// the first add, so a radio that surfaced moments later went unseen until
// the dialog was reopened. We rebuild the list only when the discovered set
// actually changes (see FlexPickerLogic) to avoid recomposing / resetting
// the scroll position every tick.
LaunchedEffect(Unit) {
val factory = FlexRadioFactory.getInstance()
// Seed with any already-discovered radios
discoveredRadios.clear()
discoveredRadios.addAll(factory.flexRadios)

val listener = object : FlexRadioFactory.OnFlexRadioEvents {
override fun OnFlexRadioAdded(flexRadio: FlexRadio) {
discoveredRadios.clear()
discoveredRadios.addAll(factory.flexRadios)
}
override fun OnFlexRadioInvalid(flexRadio: FlexRadio) {
while (true) {
val latest = factory.flexRadios.toList()
val displayedKeys = discoveredRadios.map { FlexPickerLogic.identityKey(it.serial, it.ip) }
val latestKeys = latest.map { FlexPickerLogic.identityKey(it.serial, it.ip) }
if (FlexPickerLogic.listChanged(displayedKeys, latestKeys)) {
discoveredRadios.clear()
discoveredRadios.addAll(factory.flexRadios)
discoveredRadios.addAll(latest)
}
delay(1000)
}
factory.setOnFlexRadioEvents(listener)
onDispose { factory.setOnFlexRadioEvents(null) }
}

Dialog(onDismissRequest = onDismiss) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package radio.ks3ckc.ft8af.ui.settings

/**
* Pure helpers for the FlexRadio picker's live-discovery list.
*
* The picker polls [com.k1af.ft8af.flex.FlexRadioFactory] once a second while
* open. These functions keep that polling cheap and stable: derive a stable
* identity per radio, and only rebuild the displayed list when the discovered
* set actually changed (so the list doesn't recompose / reset its scroll every
* tick). Kept free of Android/Compose types so it's unit-testable.
*/
internal object FlexPickerLogic {

/**
* Stable identity for a discovered radio. Prefers the serial number; falls
* back to the IP when the serial is missing/blank. Trimmed so incidental
* whitespace doesn't read as a change.
*/
fun identityKey(serial: String?, ip: String?): String {
val s = serial?.trim().orEmpty()
if (s.isNotEmpty()) return s
return ip?.trim().orEmpty()
}

/**
* Whether the latest discovery snapshot differs from what's displayed,
* compared by identity key in order. When false, the caller skips the
* (otherwise per-second) list rebuild.
*/
fun listChanged(displayedKeys: List<String>, latestKeys: List<String>): Boolean {
return displayedKeys != latestKeys
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package radio.ks3ckc.ft8af.ui.settings

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
* Unit tests for [FlexPickerLogic], the live-discovery list helpers behind the
* FlexRadio picker polling fix.
*/
class FlexPickerLogicTest {

@Test
fun identityKey_prefersSerial() {
assertThat(FlexPickerLogic.identityKey("0123-4567-89", "192.168.86.159"))
.isEqualTo("0123-4567-89")
}

@Test
fun identityKey_fallsBackToIpWhenSerialBlank() {
assertThat(FlexPickerLogic.identityKey(" ", "192.168.86.159"))
.isEqualTo("192.168.86.159")
assertThat(FlexPickerLogic.identityKey(null, "192.168.86.159"))
.isEqualTo("192.168.86.159")
}

@Test
fun identityKey_trimsWhitespace() {
assertThat(FlexPickerLogic.identityKey(" FLEX-1 ", null)).isEqualTo("FLEX-1")
}

@Test
fun identityKey_emptyWhenNothingKnown() {
assertThat(FlexPickerLogic.identityKey(null, null)).isEmpty()
}

@Test
fun listChanged_falseWhenIdentical() {
// The common per-tick case: nothing new discovered → no rebuild.
assertThat(FlexPickerLogic.listChanged(listOf("a", "b"), listOf("a", "b"))).isFalse()
}

@Test
fun listChanged_trueWhenRadioAppears() {
// A radio surfaces after the dialog is already open — must update live.
assertThat(FlexPickerLogic.listChanged(emptyList(), listOf("a"))).isTrue()
}

@Test
fun listChanged_trueWhenRadioDropsOff() {
assertThat(FlexPickerLogic.listChanged(listOf("a", "b"), listOf("a"))).isTrue()
}
}
Loading