block creation or sending of duplicate channels. (#3913)

This commit is contained in:
Dane Evans 2025-12-06 23:47:33 +11:00 committed by GitHub
parent 499ed58311
commit 7db7f61386
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 108 additions and 9 deletions

View file

@ -96,6 +96,7 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.model.util.hasDuplicateKeys
import org.meshtastic.core.model.util.qrCode
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.navigation.Route
@ -107,6 +108,7 @@ import org.meshtastic.core.strings.are_you_sure_change_default
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.cant_change_no_radio
import org.meshtastic.core.strings.channel_invalid
import org.meshtastic.core.strings.channel_key_already_in_use
import org.meshtastic.core.strings.copy
import org.meshtastic.core.strings.edit
import org.meshtastic.core.strings.modem_preset
@ -231,6 +233,12 @@ fun ChannelScreen(
// Send new channel settings to the device
fun installSettings(newChannelSet: ChannelSet) {
// Check for duplicate keys before installing
if (newChannelSet.hasDuplicateKeys()) {
scope.launch { context.showToast(Res.string.channel_key_already_in_use) }
return
}
// Try to change the radio, if it fails, tell the user why and throw away their edits
try {
viewModel.setChannels(newChannelSet)

View file

@ -89,3 +89,21 @@ fun ChannelSet.qrCode(shouldAdd: Boolean): Bitmap? = try {
Timber.e("URL was too complex to render as barcode")
null
}
/**
* Check if the ChannelSet contains any duplicate PSKs.
*
* @return true if there are duplicate PSKs, false otherwise
*/
fun ChannelSet.hasDuplicateKeys(): Boolean {
val pskList = mutableListOf<ByteArray>()
for (setting in settingsList) {
val channel = Channel(setting, loraConfig)
val pskBytes = channel.psk.toByteArray()
if (pskList.any { it contentEquals pskBytes }) {
return true
}
pskList.add(pskBytes)
}
return false
}

View file

@ -218,6 +218,7 @@
<string name="meshtastic_service_notifications">Service notifications</string>
<string name="about">About</string>
<string name="channel_invalid">This Channel URL is invalid and can not be used</string>
<string name="channel_key_already_in_use">A channel with this key is already in use</string>
<string name="contact_invalid">This contact is invalid and can not be added</string>
<string name="debug_panel">Debug Panel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>

View file

@ -40,9 +40,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
@ -50,15 +52,19 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.util.hasDuplicateKeys
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.accept
import org.meshtastic.core.strings.add
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.channel_key_already_in_use
import org.meshtastic.core.strings.new_channel_rcvd
import org.meshtastic.core.strings.replace
import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset
import org.meshtastic.proto.channelSet
@ -290,10 +296,16 @@ fun ScannedQrCodeDialog(
)
}
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
TextButton(
onClick = {
onDismiss()
onConfirm(selectedChannelSet)
if (selectedChannelSet.hasDuplicateKeys()) {
coroutineScope.launch { context.showToast(Res.string.channel_key_already_in_use) }
} else {
onDismiss()
onConfirm(selectedChannelSet)
}
},
enabled = selectedChannelSet.settingsCount in 1..8,
) {

View file

@ -37,27 +37,33 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.model.util.hasDuplicateKeys
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.add
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.channel_key_already_in_use
import org.meshtastic.core.strings.channel_name
import org.meshtastic.core.strings.channels
import org.meshtastic.core.strings.press_and_drag
@ -67,6 +73,7 @@ import org.meshtastic.core.ui.component.PreferenceFooter
import org.meshtastic.core.ui.component.dragContainer
import org.meshtastic.core.ui.component.dragDropItemsIndexed
import org.meshtastic.core.ui.component.rememberDragDropState
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.radio.channel.component.ChannelCard
import org.meshtastic.feature.settings.radio.channel.component.ChannelConfigHeader
@ -75,6 +82,7 @@ import org.meshtastic.feature.settings.radio.channel.component.ChannelLegendDial
import org.meshtastic.feature.settings.radio.channel.component.EditChannelDialog
import org.meshtastic.feature.settings.radio.channel.component.SECONDARY_CHANNEL_EPOCH
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
import org.meshtastic.proto.AppOnlyProtos
import org.meshtastic.proto.ChannelProtos.ChannelSettings
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
import org.meshtastic.proto.channelSettings
@ -135,21 +143,69 @@ private fun ChannelConfigScreen(
settingsList.size != settingsListInput.size ||
settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 }
// Check if the current channel list has duplicate PSKs - recompute when list changes
var hasDuplicateKeys by remember { mutableStateOf(false) }
LaunchedEffect(settingsListInput.size, settingsListInput.toList(), loraConfig) {
val channelSet =
AppOnlyProtos.ChannelSet.newBuilder()
.apply {
addAllSettings(settingsListInput.toList())
setLoraConfig(loraConfig)
}
.build()
hasDuplicateKeys = channelSet.hasDuplicateKeys()
}
var showEditChannelDialog: Int? by rememberSaveable { mutableStateOf(null) }
var showChannelLegendDialog by rememberSaveable { mutableStateOf(false) }
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
if (showEditChannelDialog != null) {
val index = showEditChannelDialog ?: return
EditChannelDialog(
channelSettings = with(settingsListInput) { if (size > index) get(index) else channelSettings {} },
modemPresetName = modemPresetName,
onAddClick = {
if (settingsListInput.size > index) {
settingsListInput[index] = it
onAddClick = { newChannelSettings ->
val isEditing = index < settingsListInput.size
// Build a temporary list to check for duplicates
val tempList = settingsListInput.toMutableList()
if (isEditing) {
tempList[index] = newChannelSettings
} else {
settingsListInput.add(it)
tempList.add(newChannelSettings)
}
// Check for duplicates in the temporary list
val tempChannelSet =
AppOnlyProtos.ChannelSet.newBuilder()
.apply {
addAllSettings(tempList)
setLoraConfig(loraConfig)
}
.build()
val hasDuplicate = tempChannelSet.hasDuplicateKeys()
if (hasDuplicate) {
coroutineScope.launch { context.showToast(Res.string.channel_key_already_in_use) }
// If this was a new channel added by FAB (at the end), remove it
// FAB adds channel then opens dialog, so index will be lastIndex
if (index == settingsListInput.size - 1 && index >= 0) {
settingsListInput.removeAt(index)
showEditChannelDialog = null
}
// If editing existing channel, don't update - keep original and dialog open
} else {
if (isEditing) {
settingsListInput[index] = newChannelSettings
} else {
settingsListInput.add(newChannelSettings)
}
showEditChannelDialog = null
}
showEditChannelDialog = null
},
onDismissRequest = { showEditChannelDialog = null },
)
@ -238,7 +294,7 @@ private fun ChannelConfigScreen(
item { Spacer(modifier = Modifier.weight(1f)) }
item {
PreferenceFooter(
enabled = enabled && isEditing,
enabled = enabled && isEditing && !hasDuplicateKeys,
negativeText = stringResource(Res.string.cancel),
onNegativeClicked = {
focusManager.clearFocus()
@ -248,7 +304,11 @@ private fun ChannelConfigScreen(
positiveText = stringResource(Res.string.send),
onPositiveClicked = {
focusManager.clearFocus()
onPositiveClicked(settingsListInput)
if (hasDuplicateKeys) {
coroutineScope.launch { context.showToast(Res.string.channel_key_already_in_use) }
} else {
onPositiveClicked(settingsListInput)
}
},
)
}