feat(radioconfig): use current location for position config (#2644)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich 2025-08-07 14:31:10 -05:00 committed by GitHub
parent da1fbc7963
commit 33c5391a67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 222 additions and 162 deletions

View file

@ -17,11 +17,16 @@
package com.geeksville.mesh.ui.radioconfig
import android.Manifest
import android.app.Application
import android.content.pm.PackageManager
import android.location.Location
import android.net.Uri
import android.os.RemoteException
import android.util.Base64
import androidx.annotation.RequiresPermission
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
@ -52,6 +57,7 @@ import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
import com.geeksville.mesh.navigation.RadioConfigRoutes
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.service.MeshService.ConnectionState
import com.geeksville.mesh.util.UiText
import com.google.protobuf.MessageLite
@ -72,9 +78,7 @@ import org.json.JSONObject
import java.io.FileOutputStream
import javax.inject.Inject
/**
* Data class that represents the current RadioConfig state.
*/
/** Data class that represents the current RadioConfig state. */
data class RadioConfigState(
val isLocal: Boolean = false,
val connected: Boolean = false,
@ -90,23 +94,40 @@ data class RadioConfigState(
)
@HiltViewModel
class RadioConfigViewModel @Inject constructor(
class RadioConfigViewModel
@Inject
constructor(
savedStateHandle: SavedStateHandle,
private val app: Application,
private val radioConfigRepository: RadioConfigRepository,
) : ViewModel(), Logging {
private val meshService: IMeshService? get() = radioConfigRepository.meshService
private val locationRepository: LocationRepository,
) : ViewModel(),
Logging {
private val meshService: IMeshService?
get() = radioConfigRepository.meshService
private val destNum = savedStateHandle.toRoute<RadioConfigRoutes.RadioConfig>().destNum
private val _destNode = MutableStateFlow<Node?>(null)
val destNode: StateFlow<Node?> get() = _destNode
val destNode: StateFlow<Node?>
get() = _destNode
private val requestIds = MutableStateFlow(hashSetOf<Int>())
private val _radioConfigState = MutableStateFlow(RadioConfigState())
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
private val _currentDeviceProfile = MutableStateFlow(deviceProfile {})
val currentDeviceProfile get() = _currentDeviceProfile.value
val currentDeviceProfile
get() = _currentDeviceProfile.value
@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION)
suspend fun getCurrentLocation(): Location? = if (
ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
) {
locationRepository.getLocations().firstOrNull()
} else {
null
}
init {
radioConfigRepository.nodeDBbyNum
@ -118,72 +139,76 @@ class RadioConfigViewModel @Inject constructor(
}
.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach {
_currentDeviceProfile.value = it
}.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach { _currentDeviceProfile.value = it }.launchIn(viewModelScope)
radioConfigRepository.meshPacketFlow.onEach(::processPacketResponse)
.launchIn(viewModelScope)
radioConfigRepository.meshPacketFlow.onEach(::processPacketResponse).launchIn(viewModelScope)
combine(radioConfigRepository.connectionState, radioConfigState) { connState, configState ->
_radioConfigState.update { it.copy(connected = connState == ConnectionState.CONNECTED) }
if (connState.isDisconnected() && configState.responseState.isWaiting()) {
sendError(R.string.disconnected)
}
}.launchIn(viewModelScope)
}
.launchIn(viewModelScope)
radioConfigRepository.myNodeInfo.onEach { ni ->
_radioConfigState.update { it.copy(isLocal = destNum == null || destNum == ni?.myNodeNum) }
}.launchIn(viewModelScope)
radioConfigRepository.myNodeInfo
.onEach { ni ->
_radioConfigState.update { it.copy(isLocal = destNum == null || destNum == ni?.myNodeNum) }
}
.launchIn(viewModelScope)
debug("RadioConfigViewModel created")
}
private val myNodeInfo: StateFlow<MyNodeEntity?> get() = radioConfigRepository.myNodeInfo
val myNodeNum get() = myNodeInfo.value?.myNodeNum
val maxChannels get() = myNodeInfo.value?.maxChannels ?: 8
private val myNodeInfo: StateFlow<MyNodeEntity?>
get() = radioConfigRepository.myNodeInfo
val myNodeNum
get() = myNodeInfo.value?.myNodeNum
val maxChannels
get() = myNodeInfo.value?.maxChannels ?: 8
val hasPaFan: Boolean
get() = destNode.value?.user?.hwModel in setOf(
null,
MeshProtos.HardwareModel.UNRECOGNIZED,
MeshProtos.HardwareModel.UNSET,
MeshProtos.HardwareModel.BETAFPV_2400_TX,
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT_NANO,
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT,
)
get() =
destNode.value?.user?.hwModel in
setOf(
null,
MeshProtos.HardwareModel.UNRECOGNIZED,
MeshProtos.HardwareModel.UNSET,
MeshProtos.HardwareModel.BETAFPV_2400_TX,
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT_NANO,
MeshProtos.HardwareModel.RADIOMASTER_900_BANDIT,
)
override fun onCleared() {
super.onCleared()
debug("RadioConfigViewModel cleared")
}
private fun request(
destNum: Int,
requestAction: suspend (IMeshService, Int, Int) -> Unit,
errorMessage: String,
) = viewModelScope.launch {
meshService?.let { service ->
val packetId = service.packetId
try {
requestAction(service, packetId, destNum)
requestIds.update { it.apply { add(packetId) } }
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
val total = maxOf(requestIds.value.size, state.responseState.total)
state.copy(responseState = state.responseState.copy(total = total))
} else {
state.copy(
route = "", // setter (response is PortNum.ROUTING_APP)
responseState = ResponseState.Loading(),
)
private fun request(destNum: Int, requestAction: suspend (IMeshService, Int, Int) -> Unit, errorMessage: String) =
viewModelScope.launch {
meshService?.let { service ->
val packetId = service.packetId
try {
requestAction(service, packetId, destNum)
requestIds.update { it.apply { add(packetId) } }
_radioConfigState.update { state ->
if (state.responseState is ResponseState.Loading) {
val total = maxOf(requestIds.value.size, state.responseState.total)
state.copy(responseState = state.responseState.copy(total = total))
} else {
state.copy(
route = "", // setter (response is PortNum.ROUTING_APP)
responseState = ResponseState.Loading(),
)
}
}
} catch (ex: RemoteException) {
errormsg("$errorMessage: ${ex.message}")
}
} catch (ex: RemoteException) {
errormsg("$errorMessage: ${ex.message}")
}
}
}
fun setOwner(user: MeshProtos.User) {
setRemoteOwner(destNode.value?.num ?: return, user)
@ -201,19 +226,14 @@ class RadioConfigViewModel @Inject constructor(
private fun getOwner(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteOwner(packetId, dest) },
"Request getOwner error"
"Request getOwner error",
)
fun updateChannels(
new: List<ChannelProtos.ChannelSettings>,
old: List<ChannelProtos.ChannelSettings>,
) {
fun updateChannels(new: List<ChannelProtos.ChannelSettings>, old: List<ChannelProtos.ChannelSettings>) {
val destNum = destNode.value?.num ?: return
getChannelList(new, old).forEach { setRemoteChannel(destNum, it) }
if (destNum == myNodeNum) viewModelScope.launch {
radioConfigRepository.replaceAllSettings(new)
}
if (destNum == myNodeNum) viewModelScope.launch { radioConfigRepository.replaceAllSettings(new) }
_radioConfigState.update { it.copy(channelList = new) }
}
@ -225,16 +245,14 @@ class RadioConfigViewModel @Inject constructor(
private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) = request(
destNum,
{ service, packetId, dest ->
service.setRemoteChannel(packetId, dest, channel.toByteArray())
},
"Request setRemoteChannel error"
{ service, packetId, dest -> service.setRemoteChannel(packetId, dest, channel.toByteArray()) },
"Request setRemoteChannel error",
)
private fun getChannel(destNum: Int, index: Int) = request(
destNum,
{ service, packetId, dest -> service.getRemoteChannel(packetId, dest, index) },
"Request getChannel error"
"Request getChannel error",
)
fun setConfig(config: ConfigProtos.Config) {
@ -284,7 +302,7 @@ class RadioConfigViewModel @Inject constructor(
private fun getRingtone(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.getRingtone(packetId, dest) },
"Request getRingtone error"
"Request getRingtone error",
)
fun setCannedMessages(messages: String) {
@ -296,26 +314,23 @@ class RadioConfigViewModel @Inject constructor(
private fun getCannedMessages(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.getCannedMessages(packetId, dest) },
"Request getCannedMessages error"
"Request getCannedMessages error",
)
private fun requestShutdown(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.requestShutdown(packetId, dest) },
"Request shutdown error"
"Request shutdown error",
)
private fun requestReboot(destNum: Int) = request(
destNum,
{ service, packetId, dest -> service.requestReboot(packetId, dest) },
"Request reboot error"
)
private fun requestReboot(destNum: Int) =
request(destNum, { service, packetId, dest -> service.requestReboot(packetId, dest) }, "Request reboot error")
private fun requestFactoryReset(destNum: Int) {
request(
destNum,
{ service, packetId, dest -> service.requestFactoryReset(packetId, dest) },
"Request factory reset error"
"Request factory reset error",
)
if (destNum == myNodeNum) {
viewModelScope.launch { radioConfigRepository.clearNodeDB() }
@ -326,7 +341,7 @@ class RadioConfigViewModel @Inject constructor(
request(
destNum,
{ service, packetId, dest -> service.requestNodedbReset(packetId, dest) },
"Request NodeDB reset error"
"Request NodeDB reset error",
)
if (destNum == myNodeNum) {
viewModelScope.launch { radioConfigRepository.clearNodeDB() }
@ -339,13 +354,14 @@ class RadioConfigViewModel @Inject constructor(
when (route) {
AdminRoute.REBOOT.name -> requestReboot(destNum)
AdminRoute.SHUTDOWN.name -> with(radioConfigState.value) {
if (metadata != null && !metadata.canShutdown) {
sendError(R.string.cant_shutdown)
} else {
requestShutdown(destNum)
AdminRoute.SHUTDOWN.name ->
with(radioConfigState.value) {
if (metadata != null && !metadata.canShutdown) {
sendError(R.string.cant_shutdown)
} else {
requestShutdown(destNum)
}
}
}
AdminRoute.FACTORY_RESET.name -> requestFactoryReset(destNum)
AdminRoute.NODEDB_RESET.name -> requestNodedbReset(destNum)
@ -363,10 +379,7 @@ class RadioConfigViewModel @Inject constructor(
fun removeFixedPosition() = setFixedPosition(Position(0.0, 0.0, 0))
fun importProfile(
uri: Uri,
onResult: (DeviceProfile) -> Unit,
) = viewModelScope.launch(Dispatchers.IO) {
fun importProfile(uri: Uri, onResult: (DeviceProfile) -> Unit) = viewModelScope.launch(Dispatchers.IO) {
try {
app.contentResolver.openInputStream(uri).use { inputStream ->
val bytes = inputStream?.readBytes()
@ -379,9 +392,8 @@ class RadioConfigViewModel @Inject constructor(
}
}
fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch {
writeToUri(uri, profile)
}
fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch { writeToUri(uri, profile) }
private suspend fun writeToUri(uri: Uri, message: MessageLite) = withContext(Dispatchers.IO) {
try {
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
@ -396,33 +408,31 @@ class RadioConfigViewModel @Inject constructor(
}
}
fun exportSecurityConfig(uri: Uri, securityConfig: SecurityConfig) = viewModelScope.launch {
writeSecurityKeysJsonToUri(uri, securityConfig)
}
fun exportSecurityConfig(uri: Uri, securityConfig: SecurityConfig) =
viewModelScope.launch { writeSecurityKeysJsonToUri(uri, securityConfig) }
private val indentSpaces = 4
private suspend fun writeSecurityKeysJsonToUri(uri: Uri, securityConfig: SecurityConfig) =
withContext(Dispatchers.IO) {
try {
val publicKeyBytes =
securityConfig.publicKey.toByteArray()
val privateKeyBytes =
securityConfig.privateKey.toByteArray()
val publicKeyBytes = securityConfig.publicKey.toByteArray()
val privateKeyBytes = securityConfig.privateKey.toByteArray()
// Convert byte arrays to Base64 strings for human readability in JSON
val publicKeyBase64 = Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
val privateKeyBase64 = Base64.encodeToString(privateKeyBytes, Base64.NO_WRAP)
// Create a JSON object
val jsonObject = JSONObject().apply {
put("timestamp", System.currentTimeMillis())
put("public_key", publicKeyBase64)
put("private_key", privateKeyBase64)
}
val jsonObject =
JSONObject().apply {
put("timestamp", System.currentTimeMillis())
put("public_key", publicKeyBase64)
put("private_key", privateKeyBase64)
}
// Convert JSON object to a string
val jsonString =
jsonObject.toString(indentSpaces)
val jsonString = jsonObject.toString(indentSpaces)
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
FileOutputStream(parcelFileDescriptor.fileDescriptor).use { outputStream ->
@ -436,16 +446,18 @@ class RadioConfigViewModel @Inject constructor(
sendError(ex.customMessage)
}
}
fun installProfile(protobuf: DeviceProfile) = with(protobuf) {
meshService?.beginEditSettings()
if (hasLongName() || hasShortName()) {
destNode.value?.user?.let {
val user = MeshProtos.User.newBuilder()
.setId(it.id)
.setLongName(if (hasLongName()) longName else it.longName)
.setShortName(if (hasShortName()) shortName else it.shortName)
.setIsLicensed(it.isLicensed)
.build()
val user =
MeshProtos.User.newBuilder()
.setId(it.id)
.setLongName(if (hasLongName()) longName else it.longName)
.setShortName(if (hasShortName()) shortName else it.shortName)
.setIsLicensed(it.isLicensed)
.build()
setOwner(user)
}
}
@ -460,9 +472,8 @@ class RadioConfigViewModel @Inject constructor(
if (hasConfig()) {
val descriptor = ConfigProtos.Config.getDescriptor()
config.allFields.forEach { (field, value) ->
val newConfig = ConfigProtos.Config.newBuilder()
.setField(descriptor.findFieldByName(field.name), value)
.build()
val newConfig =
ConfigProtos.Config.newBuilder().setField(descriptor.findFieldByName(field.name), value).build()
setConfig(newConfig)
}
}
@ -472,9 +483,10 @@ class RadioConfigViewModel @Inject constructor(
if (hasModuleConfig()) {
val descriptor = ModuleConfigProtos.ModuleConfig.getDescriptor()
moduleConfig.allFields.forEach { (field, value) ->
val newConfig = ModuleConfigProtos.ModuleConfig.newBuilder()
.setField(descriptor.findFieldByName(field.name), value)
.build()
val newConfig =
ModuleConfigProtos.ModuleConfig.newBuilder()
.setField(descriptor.findFieldByName(field.name), value)
.build()
setModuleConfig(newConfig)
}
}
@ -553,9 +565,13 @@ class RadioConfigViewModel @Inject constructor(
}
}
private val Exception.customMessage: String get() = "${javaClass.simpleName}: $message"
private val Exception.customMessage: String
get() = "${javaClass.simpleName}: $message"
private fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error))
private fun sendError(@StringRes id: Int) = setResponseStateError(UiText.StringResource(id))
private fun setResponseStateError(error: UiText) {
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
}
@ -612,9 +628,8 @@ class RadioConfigViewModel @Inject constructor(
if (response.role != ChannelProtos.Channel.Role.DISABLED) {
_radioConfigState.update { state ->
state.copy(
channelList = state.channelList.toMutableList().apply {
add(response.index, response.settings)
}
channelList =
state.channelList.toMutableList().apply { add(response.index, response.settings) },
)
}
incrementCompleted()

View file

@ -17,19 +17,29 @@
package com.geeksville.mesh.ui.radioconfig.components
import android.Manifest
import android.annotation.SuppressLint
import android.location.Location
import android.os.Build
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.location.LocationCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ConfigProtos
@ -45,29 +55,31 @@ import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.launch
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PositionConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
val coroutineScope = rememberCoroutineScope()
var phoneLocation: Location? by remember { mutableStateOf(null) }
val node by viewModel.destNode.collectAsStateWithLifecycle()
val currentPosition = Position(
latitude = node?.latitude ?: 0.0,
longitude = node?.longitude ?: 0.0,
altitude = node?.position?.altitude ?: 0,
time = 1, // ignore time for fixed_position
)
val currentPosition =
Position(
latitude = node?.latitude ?: 0.0,
longitude = node?.longitude ?: 0.0,
altitude = node?.position?.altitude ?: 0,
time = 1, // ignore time for fixed_position
)
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
state = state.responseState,
onDismiss = viewModel::clearPacketResponse,
)
PacketResponseStateDialog(state = state.responseState, onDismiss = viewModel::clearPacketResponse)
}
PositionConfigItemList(
phoneLocation = phoneLocation,
location = currentPosition,
positionConfig = state.radioConfig.position,
enabled = state.connected,
@ -84,25 +96,54 @@ fun PositionConfigScreen(
}
val config = config { position = positionInput }
viewModel.setConfig(config)
}
},
onUseCurrentLocation = {
@SuppressLint("MissingPermission")
coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() }
},
)
}
@OptIn(ExperimentalPermissionsApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun PositionConfigItemList(
phoneLocation: Location? = null,
location: Position,
positionConfig: PositionConfig,
enabled: Boolean,
onSaveClicked: (position: Position, config: PositionConfig) -> Unit,
onUseCurrentLocation: suspend () -> Unit,
) {
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
val locationPermissionState =
rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) { granted ->
if (granted) {
coroutineScope.launch { onUseCurrentLocation() }
}
}
var locationInput by rememberSaveable { mutableStateOf(location) }
var positionInput by rememberSaveable { mutableStateOf(positionConfig) }
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LaunchedEffect(phoneLocation) {
if (phoneLocation != null) {
locationInput =
Position(
latitude = phoneLocation.latitude,
longitude = phoneLocation.longitude,
altitude =
LocationCompat.hasMslAltitude(phoneLocation).let {
if (it && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
phoneLocation.mslAltitudeMeters.toInt()
} else {
phoneLocation.altitude.toInt()
}
},
)
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { PreferenceCategory(text = stringResource(R.string.position_config)) }
item {
@ -111,9 +152,7 @@ fun PositionConfigItemList(
value = positionInput.positionBroadcastSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
positionInput = positionInput.copy { positionBroadcastSecs = it }
}
onValueChanged = { positionInput = positionInput.copy { positionBroadcastSecs = it } },
)
}
@ -122,9 +161,7 @@ fun PositionConfigItemList(
title = stringResource(R.string.smart_position_enabled),
checked = positionInput.positionBroadcastSmartEnabled,
enabled = enabled,
onCheckedChange = {
positionInput = positionInput.copy { positionBroadcastSmartEnabled = it }
}
onCheckedChange = { positionInput = positionInput.copy { positionBroadcastSmartEnabled = it } },
)
}
item { HorizontalDivider() }
@ -136,9 +173,7 @@ fun PositionConfigItemList(
value = positionInput.broadcastSmartMinimumDistance,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
positionInput = positionInput.copy { broadcastSmartMinimumDistance = it }
}
onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumDistance = it } },
)
}
@ -148,9 +183,7 @@ fun PositionConfigItemList(
value = positionInput.broadcastSmartMinimumIntervalSecs,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = {
positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it }
}
onValueChanged = { positionInput = positionInput.copy { broadcastSmartMinimumIntervalSecs = it } },
)
}
}
@ -160,7 +193,7 @@ fun PositionConfigItemList(
title = stringResource(R.string.use_fixed_position),
checked = positionInput.fixedPosition,
enabled = enabled,
onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } }
onCheckedChange = { positionInput = positionInput.copy { fixedPosition = it } },
)
}
item { HorizontalDivider() }
@ -176,7 +209,7 @@ fun PositionConfigItemList(
if (value >= -90 && value <= 90.0) {
locationInput = locationInput.copy(latitude = value)
}
}
},
)
}
item {
@ -189,7 +222,7 @@ fun PositionConfigItemList(
if (value >= -180 && value <= 180.0) {
locationInput = locationInput.copy(longitude = value)
}
}
},
)
}
item {
@ -198,22 +231,29 @@ fun PositionConfigItemList(
value = locationInput.altitude,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { value ->
locationInput = locationInput.copy(altitude = value)
}
onValueChanged = { value -> locationInput = locationInput.copy(altitude = value) },
)
}
item {
TextButton(
enabled = enabled,
onClick = { coroutineScope.launch { locationPermissionState.launchPermissionRequest() } },
) {
Text(text = stringResource(R.string.position_config_set_fixed_from_phone))
}
}
}
item {
DropDownPreference(
title = stringResource(R.string.gps_mode),
enabled = enabled,
items = ConfigProtos.Config.PositionConfig.GpsMode.entries
items =
ConfigProtos.Config.PositionConfig.GpsMode.entries
.filter { it != ConfigProtos.Config.PositionConfig.GpsMode.UNRECOGNIZED }
.map { it to it.name },
selectedItem = positionInput.gpsMode,
onItemSelected = { positionInput = positionInput.copy { gpsMode = it } }
onItemSelected = { positionInput = positionInput.copy { gpsMode = it } },
)
}
item { HorizontalDivider() }
@ -224,7 +264,7 @@ fun PositionConfigItemList(
value = positionInput.gpsUpdateInterval,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } }
onValueChanged = { positionInput = positionInput.copy { gpsUpdateInterval = it } },
)
}
@ -233,10 +273,13 @@ fun PositionConfigItemList(
title = stringResource(R.string.position_flags),
value = positionInput.positionFlags,
enabled = enabled,
items = ConfigProtos.Config.PositionConfig.PositionFlags.entries
.filter { it != PositionConfig.PositionFlags.UNSET && it != PositionConfig.PositionFlags.UNRECOGNIZED }
items =
ConfigProtos.Config.PositionConfig.PositionFlags.entries
.filter {
it != PositionConfig.PositionFlags.UNSET && it != PositionConfig.PositionFlags.UNRECOGNIZED
}
.map { it.number to it.name },
onItemSelected = { positionInput = positionInput.copy { positionFlags = it } }
onItemSelected = { positionInput = positionInput.copy { positionFlags = it } },
)
}
item { HorizontalDivider() }
@ -247,7 +290,7 @@ fun PositionConfigItemList(
value = positionInput.rxGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { rxGpio = it } }
onValueChanged = { positionInput = positionInput.copy { rxGpio = it } },
)
}
@ -257,7 +300,7 @@ fun PositionConfigItemList(
value = positionInput.txGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { txGpio = it } }
onValueChanged = { positionInput = positionInput.copy { txGpio = it } },
)
}
@ -267,7 +310,7 @@ fun PositionConfigItemList(
value = positionInput.gpsEnGpio,
enabled = enabled,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { positionInput = positionInput.copy { gpsEnGpio = it } }
onValueChanged = { positionInput = positionInput.copy { gpsEnGpio = it } },
)
}
@ -282,7 +325,7 @@ fun PositionConfigItemList(
onSaveClicked = {
focusManager.clearFocus()
onSaveClicked(locationInput, positionInput)
}
},
)
}
}
@ -296,5 +339,6 @@ private fun PositionConfigPreview() {
positionConfig = PositionConfig.getDefaultInstance(),
enabled = true,
onSaveClicked = { _, _ -> },
onUseCurrentLocation = {},
)
}