mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-19 12:18:43 -04:00
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:
@@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user