From ddad40a9699b576031a385bf48ad37658e7c7622 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sun, 18 Aug 2024 08:32:20 -0300 Subject: [PATCH] refactor: add more granular Position precision options closes #1186 --- .../components/PositionPrecisionPreference.kt | 104 ++++++++++++++---- .../mesh/ui/components/SwitchPreference.kt | 6 +- .../ui/components/config/EditChannelDialog.kt | 63 ++++++----- .../mesh/util/DistanceExtensions.kt | 80 +++++++++----- 4 files changed, 169 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/PositionPrecisionPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/components/PositionPrecisionPreference.kt index 1fa5f66e2..96dfac25f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/PositionPrecisionPreference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/PositionPrecisionPreference.kt @@ -1,17 +1,33 @@ package com.geeksville.mesh.ui.components -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Slider +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.geeksville.mesh.util.DistanceUnit +import com.geeksville.mesh.util.toDistanceString +import java.util.Locale +import kotlin.math.pow -private enum class PositionPrecision(val value: Int) { - HIGH_PRECISION(32), - MED_PRECISION(16), - LOW_PRECISION(11), - DISABLED(0), - ; -} +private const val PositionEnabled = 32 +private const val PositionDisabled = 0 + +const val PositionPrecisionMin = 10 +const val PositionPrecisionMax = 19 +const val PositionPrecisionDefault = 13 + +@Suppress("MagicNumber") +fun precisionBitsToMeters(bits: Int): Double = 23905787.925008 * 0.5.pow(bits.toDouble()) @Composable fun PositionPrecisionPreference( @@ -21,24 +37,64 @@ fun PositionPrecisionPreference( onValueChanged: (Int) -> Unit, modifier: Modifier = Modifier, ) { - val focusManager = LocalFocusManager.current - val useDropDown = PositionPrecision.entries.any { it.value == value } + val unit = remember { DistanceUnit.getFromLocale(Locale.getDefault()) } - if (useDropDown) { - DropDownPreference( + Column(modifier = modifier) { + SwitchPreference( title = title, + checked = value != PositionDisabled, enabled = enabled, - items = PositionPrecision.entries.map { it.value to it.name }, - selectedItem = value, - onItemSelected = onValueChanged, - ) - } else { - EditTextPreference( - title = title, - value = value, - enabled = enabled, - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = onValueChanged, + onCheckedChange = { enabled -> + val newValue = if (enabled) PositionEnabled else PositionDisabled + onValueChanged(newValue) + }, + padding = PaddingValues(0.dp) ) + AnimatedVisibility(visible = value != PositionDisabled) { + SwitchPreference( + title = "Precise location", + checked = value == PositionEnabled, + enabled = enabled, + onCheckedChange = { enabled -> + val newValue = if (enabled) PositionEnabled else PositionPrecisionDefault + onValueChanged(newValue) + }, + padding = PaddingValues(0.dp) + ) + } + AnimatedVisibility(visible = value in (PositionDisabled + 1).. Unit, modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(horizontal = 16.dp), ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .size(48.dp) - .padding(start = 16.dp, end = 16.dp), + .padding(padding), verticalAlignment = Alignment.CenterVertically ) { Text( diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/EditChannelDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/EditChannelDialog.kt index e9d6cd006..670f6cb38 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/EditChannelDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/EditChannelDialog.kt @@ -3,7 +3,9 @@ package com.geeksville.mesh.ui.components.config import android.util.Base64 import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions @@ -16,8 +18,8 @@ import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Switch import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.Close import androidx.compose.material.icons.twotone.Refresh @@ -26,7 +28,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -41,11 +42,14 @@ import com.geeksville.mesh.copy import com.geeksville.mesh.model.Channel import com.geeksville.mesh.ui.components.EditTextPreference import com.geeksville.mesh.ui.components.PositionPrecisionPreference +import com.geeksville.mesh.ui.components.SwitchPreference import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import com.google.protobuf.ByteString import com.google.protobuf.kotlin.toByteString import java.security.SecureRandom +@Suppress("LongMethod") +@OptIn(ExperimentalLayoutApi::class) @Composable fun EditChannelDialog( channelSettings: ChannelProtos.ChannelSettings, @@ -133,32 +137,28 @@ fun EditChannelDialog( }, ) - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Uplink enabled", // TODO move to resource strings - modifier = modifier.weight(1f) - ) - Switch( - checked = channelInput.uplinkEnabled, - onCheckedChange = { - channelInput = channelInput.copy { uplinkEnabled = it } - }, - ) - } + SwitchPreference( + title = "Uplink enabled", + checked = channelInput.uplinkEnabled, + enabled = true, + onCheckedChange = { + channelInput = channelInput.copy { uplinkEnabled = it } + }, + padding = PaddingValues(0.dp) + ) - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Downlink enabled", // TODO move to resource strings - modifier = modifier.weight(1f) - ) - Switch( - checked = channelInput.downlinkEnabled, - onCheckedChange = { - channelInput = channelInput.copy { downlinkEnabled = it } - }, - ) - } + SwitchPreference( + title = "Downlink enabled", + checked = channelInput.downlinkEnabled, + enabled = true, + onCheckedChange = { + channelInput = channelInput.copy { downlinkEnabled = it } + }, + padding = PaddingValues(0.dp) + ) PositionPrecisionPreference( - title = "Position", + title = "Position enabled", enabled = true, value = channelInput.moduleSettings.positionPrecision, onValueChanged = { @@ -170,22 +170,21 @@ fun EditChannelDialog( } }, buttons = { - Row( - modifier = modifier.fillMaxWidth(), + FlowRow( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, ) { - Button( + TextButton( modifier = modifier .fillMaxWidth() - .padding(start = 24.dp) .weight(1f), onClick = onDismissRequest ) { Text(stringResource(R.string.cancel)) } Button( modifier = modifier .fillMaxWidth() - .padding(end = 24.dp) .weight(1f), onClick = { onAddClick(channelInput.copy { name = channelInput.name.trim() }) diff --git a/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt b/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt index fd6924701..4455cada4 100644 --- a/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt +++ b/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt @@ -1,46 +1,74 @@ package com.geeksville.mesh.util -import com.geeksville.mesh.ConfigProtos +import android.icu.util.LocaleData +import android.icu.util.ULocale +import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig +import java.util.Locale enum class DistanceUnit( val symbol: String, val multiplier: Float, val system: Int ) { - METERS("m", 1F, ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE), - KILOMETERS("km", 0.001F, ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE), - FEET("ft", 3.28084F, ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE), - MILES("mi", 0.000621371F, ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE), + METER("m", multiplier = 1F, DisplayConfig.DisplayUnits.METRIC_VALUE), + KILOMETER("km", multiplier = 0.001F, DisplayConfig.DisplayUnits.METRIC_VALUE), + FOOT("ft", multiplier = 3.28084F, DisplayConfig.DisplayUnits.IMPERIAL_VALUE), + MILE("mi", multiplier = 0.000621371F, DisplayConfig.DisplayUnits.IMPERIAL_VALUE), + ; + + companion object { + fun getFromLocale(locale: Locale): DisplayConfig.DisplayUnits { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) { + LocaleData.MeasurementSystem.SI -> DisplayConfig.DisplayUnits.METRIC + else -> DisplayConfig.DisplayUnits.IMPERIAL + } + } else { + when (locale.country.uppercase(locale)) { + "US", "LR", "MM", "GB" -> DisplayConfig.DisplayUnits.IMPERIAL + else -> DisplayConfig.DisplayUnits.METRIC + } + } + } + } } fun Int.metersIn(unit: DistanceUnit): Float { return this * unit.multiplier } -fun Int.metersIn(system: ConfigProtos.Config.DisplayConfig.DisplayUnits): Float { - return this * when (system.number) { - ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE -> DistanceUnit.METERS.multiplier - ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FEET.multiplier - else -> throw IllegalArgumentException("Unknown distance system $system") +fun Int.metersIn(system: DisplayConfig.DisplayUnits): Float { + val unit = when (system.number) { + DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT + else -> DistanceUnit.METER } + return this.metersIn(unit) } fun Float.toString(unit: DistanceUnit): String { - return "%.1f %s".format(this, unit.symbol) + return if (unit in setOf(DistanceUnit.METER, DistanceUnit.FOOT)) { + "%.0f %s" + } else { + "%.1f %s" + }.format(this, unit.symbol) } -fun Float.toString( - system: ConfigProtos.Config.DisplayConfig.DisplayUnits -): String { - return "%.1f %s".format(this, - when (system.number) { - ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE -> { - DistanceUnit.METERS.symbol - } - ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> { - DistanceUnit.FEET.symbol - } - else -> throw IllegalArgumentException("Unknown distance system $system") - }, - ) -} \ No newline at end of file +fun Float.toString(system: DisplayConfig.DisplayUnits): String { + val unit = when (system.number) { + DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT + else -> DistanceUnit.METER + } + return this.toString(unit) +} + +private const val KILOMETER_THRESHOLD = 1000 +private const val MILE_THRESHOLD = 1609 +fun Int.toDistanceString(system: DisplayConfig.DisplayUnits): String { + val unit = if (system.number == DisplayConfig.DisplayUnits.METRIC_VALUE) { + if (this < KILOMETER_THRESHOLD) DistanceUnit.METER else DistanceUnit.KILOMETER + } else { + if (this < MILE_THRESHOLD) DistanceUnit.FOOT else DistanceUnit.MILE + } + val valueInUnit = this * unit.multiplier + return valueInUnit.toString(unit) +}