fix(settings): add input validation for BLE PIN, LoRa modem, and ambient lighting (#5477)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-18 07:52:17 -05:00
committed by GitHub
parent f4b6b02ace
commit 1dd47bc090
3 changed files with 159 additions and 58 deletions

View File

@@ -21,6 +21,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.platform.LocalFocusManager
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
@@ -38,6 +39,9 @@ import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.ModuleConfig
private const val MAX_LED_CURRENT = 31
private const val MAX_RGB_VALUE = 255
@Composable
fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
@@ -67,36 +71,72 @@ fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U
containerColor = CardDefaults.cardColors().containerColor,
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.current),
value = formState.value.current,
LedColorFields(
config = formState.value,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy(current = it) },
)
EditTextPreference(
title = stringResource(Res.string.red),
value = formState.value.red,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy(red = it) },
)
EditTextPreference(
title = stringResource(Res.string.green),
value = formState.value.green,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy(green = it) },
)
EditTextPreference(
title = stringResource(Res.string.blue),
value = formState.value.blue,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy(blue = it) },
focusManager = focusManager,
onConfigChange = { formState.value = it },
)
}
}
}
}
@Composable
private fun LedColorFields(
config: ModuleConfig.AmbientLightingConfig,
enabled: Boolean,
focusManager: FocusManager,
onConfigChange: (ModuleConfig.AmbientLightingConfig) -> Unit,
) {
androidx.compose.foundation.layout.Column {
EditTextPreference(
title = stringResource(Res.string.current),
value = config.current,
enabled = enabled,
isError = config.current !in 0..MAX_LED_CURRENT,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it in 0..MAX_LED_CURRENT) {
onConfigChange(config.copy(current = it))
}
},
)
EditTextPreference(
title = stringResource(Res.string.red),
value = config.red,
enabled = enabled,
isError = config.red !in 0..MAX_RGB_VALUE,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it in 0..MAX_RGB_VALUE) {
onConfigChange(config.copy(red = it))
}
},
)
EditTextPreference(
title = stringResource(Res.string.green),
value = config.green,
enabled = enabled,
isError = config.green !in 0..MAX_RGB_VALUE,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it in 0..MAX_RGB_VALUE) {
onConfigChange(config.copy(green = it))
}
},
)
EditTextPreference(
title = stringResource(Res.string.blue),
value = config.blue,
enabled = enabled,
isError = config.blue !in 0..MAX_RGB_VALUE,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it in 0..MAX_RGB_VALUE) {
onConfigChange(config.copy(blue = it))
}
},
)
}
}

View File

@@ -17,11 +17,17 @@
package org.meshtastic.feature.settings.radio.component
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.resources.Res
@@ -37,6 +43,8 @@ import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.proto.Config
private const val PIN_LENGTH = 6
@Composable
fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
@@ -79,18 +87,41 @@ fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
onItemSelected = { formState.value = formState.value.copy(mode = it) },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.fixed_pin),
value = formState.value.fixed_pin,
FixedPinPreference(
pinValue = formState.value.fixed_pin,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it.toString().length == 6) { // ensure 6 digits
formState.value = formState.value.copy(fixed_pin = it)
}
},
focusManager = focusManager,
onPinChange = { formState.value = formState.value.copy(fixed_pin = it) },
)
}
}
}
}
@Composable
private fun FixedPinPreference(
pinValue: Int,
enabled: Boolean,
focusManager: androidx.compose.ui.focus.FocusManager,
onPinChange: (Int) -> Unit,
) {
var pinState by remember(pinValue) { mutableStateOf(pinValue.toString().padStart(PIN_LENGTH, '0')) }
val pinIsError = pinState.length != PIN_LENGTH || !pinState.all { it.isDigit() }
EditTextPreference(
title = stringResource(Res.string.fixed_pin),
value = pinState,
enabled = enabled,
isError = pinIsError,
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { input ->
if (input.length <= PIN_LENGTH && input.all { it.isDigit() }) {
pinState = input
if (input.length == PIN_LENGTH) {
onPinChange(input.toInt())
}
}
},
)
}

View File

@@ -64,6 +64,9 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.util.hopLimits
import org.meshtastic.proto.Config
private val SPREAD_FACTOR_RANGE = 7..12
private val CODING_RATE_RANGE = 5..8
@Composable
fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
@@ -115,28 +118,11 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
onItemSelected = { formState.value = formState.value.copy(modem_preset = it) },
)
} else {
EditTextPreference(
title = stringResource(Res.string.bandwidth),
value = formState.value.bandwidth,
enabled = state.connected && !formState.value.use_preset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy(bandwidth = it) },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.spread_factor),
value = formState.value.spread_factor,
enabled = state.connected && !formState.value.use_preset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy(spread_factor = it) },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.coding_rate),
value = formState.value.coding_rate,
enabled = state.connected && !formState.value.use_preset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy(coding_rate = it) },
ManualModemSettings(
config = formState.value,
enabled = state.connected,
focusManager = focusManager,
onConfigChange = { formState.value = it },
)
}
}
@@ -250,3 +236,47 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
}
}
}
@Composable
private fun ManualModemSettings(
config: Config.LoRaConfig,
enabled: Boolean,
focusManager: androidx.compose.ui.focus.FocusManager,
onConfigChange: (Config.LoRaConfig) -> Unit,
) {
androidx.compose.foundation.layout.Column {
EditTextPreference(
title = stringResource(Res.string.bandwidth),
value = config.bandwidth,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { onConfigChange(config.copy(bandwidth = it)) },
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.spread_factor),
value = config.spread_factor,
enabled = enabled,
isError = config.spread_factor !in SPREAD_FACTOR_RANGE,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it in SPREAD_FACTOR_RANGE) {
onConfigChange(config.copy(spread_factor = it))
}
},
)
HorizontalDivider()
EditTextPreference(
title = stringResource(Res.string.coding_rate),
value = config.coding_rate,
enabled = enabled,
isError = config.coding_rate !in CODING_RATE_RANGE,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
if (it in CODING_RATE_RANGE) {
onConfigChange(config.copy(coding_rate = it))
}
},
)
}
}