mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-07 06:12:56 -05:00
feat: add MQTT Map Reporting consent (#2006)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.ui.radioconfig.components.MapReportingPreference
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MapReportingPreferenceTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun getString(id: Int): String =
|
||||
InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
|
||||
|
||||
var mapReportingEnabled = false
|
||||
var shouldReportLocation = false
|
||||
var positionPrecision = 5
|
||||
var positionReportingInterval = 60
|
||||
|
||||
var mapReportingEnabledChanged = { enabled: Boolean ->
|
||||
mapReportingEnabled = enabled
|
||||
}
|
||||
var shouldReportLocationChanged = { enabled: Boolean ->
|
||||
shouldReportLocation = enabled
|
||||
}
|
||||
var positionPrecisionChanged = { precision: Int ->
|
||||
positionPrecision = precision
|
||||
}
|
||||
var positionReportingIntervalChanged = { interval: Int ->
|
||||
positionReportingInterval = interval
|
||||
}
|
||||
|
||||
|
||||
private fun testMapReportingPreference() = composeTestRule.setContent {
|
||||
Column {
|
||||
MapReportingPreference(
|
||||
mapReportingEnabled = mapReportingEnabled,
|
||||
shouldReportLocation = shouldReportLocation,
|
||||
positionPrecision = positionPrecision,
|
||||
onMapReportingEnabledChanged = mapReportingEnabledChanged,
|
||||
onShouldReportLocationChanged = shouldReportLocationChanged,
|
||||
onPositionPrecisionChanged = positionPrecisionChanged,
|
||||
publishIntervalSecs = positionReportingInterval,
|
||||
onPublishIntervalSecsChanged = positionReportingIntervalChanged,
|
||||
enabled = true,
|
||||
focusManager = LocalFocusManager.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapReportingPreference_showsText() {
|
||||
composeTestRule.apply {
|
||||
testMapReportingPreference()
|
||||
// Verify that the dialog title is displayed
|
||||
onNodeWithText(getString(R.string.map_reporting)).assertIsDisplayed()
|
||||
onNodeWithText(getString(R.string.map_reporting_summary)).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMapReportingPreference_toggleMapReporting() {
|
||||
composeTestRule.apply {
|
||||
testMapReportingPreference()
|
||||
onNodeWithText(getString(R.string.i_agree)).assertIsNotDisplayed()
|
||||
onNodeWithText(getString(R.string.map_reporting)).performClick()
|
||||
Assert.assertFalse(mapReportingEnabled)
|
||||
Assert.assertFalse(shouldReportLocation)
|
||||
onNodeWithText(getString(R.string.i_agree)).assertIsDisplayed()
|
||||
onNodeWithText(getString(R.string.i_agree)).performClick()
|
||||
Assert.assertTrue(shouldReportLocation)
|
||||
Assert.assertTrue(mapReportingEnabled)
|
||||
onNodeWithText(getString(R.string.map_reporting)).performClick()
|
||||
onNodeWithText(getString(R.string.i_agree)).assertIsNotDisplayed()
|
||||
Assert.assertTrue(shouldReportLocation)
|
||||
Assert.assertFalse(mapReportingEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,9 @@ import kotlin.math.roundToInt
|
||||
private const val PositionEnabled = 32
|
||||
private const val PositionDisabled = 0
|
||||
|
||||
const val PositionPrecisionMin = 10
|
||||
const val PositionPrecisionMax = 19
|
||||
const val PositionPrecisionDefault = 13
|
||||
const val PositionPrecisionMin = 12
|
||||
const val PositionPrecisionMax = 15
|
||||
const val PositionPrecisionDefault = 14
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun precisionBitsToMeters(bits: Int): Double = 23905787.925008 * 0.5.pow(bits.toDouble())
|
||||
|
||||
@@ -17,18 +17,14 @@
|
||||
|
||||
package com.geeksville.mesh.ui.common.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -36,78 +32,57 @@ import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
summary: String,
|
||||
summary: String = "",
|
||||
checked: Boolean,
|
||||
enabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues? = null,
|
||||
containerColor: Color? = null,
|
||||
) {
|
||||
val color = if (enabled) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
}
|
||||
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
colors = ListItemDefaults.colors().copy(
|
||||
headlineColor = if (enabled) {
|
||||
ListItemDefaults.colors().headlineColor
|
||||
} else {
|
||||
ListItemDefaults.colors().headlineColor.copy(alpha = 0.5f)
|
||||
},
|
||||
supportingTextColor = if (enabled) {
|
||||
ListItemDefaults.colors().supportingTextColor
|
||||
} else {
|
||||
ListItemDefaults.colors().supportingTextColor.copy(alpha = 0.5f)
|
||||
},
|
||||
containerColor = containerColor ?: ListItemDefaults.colors().containerColor,
|
||||
),
|
||||
modifier = (padding?.let { Modifier.padding(it) } ?: modifier).toggleable(
|
||||
value = checked,
|
||||
enabled = enabled,
|
||||
onValueChange = onCheckedChange,
|
||||
),
|
||||
trailingContent = {
|
||||
Switch(
|
||||
enabled = enabled,
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = summary,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
color = color,
|
||||
)
|
||||
if (summary.isNotEmpty()) {
|
||||
Text(
|
||||
text = summary,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
color = color,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
enabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.size(48.dp)
|
||||
.padding(padding),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (enabled) {
|
||||
Color.Unspecified
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
},
|
||||
)
|
||||
Switch(
|
||||
enabled = enabled,
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SwitchPreferencePreview() {
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("LongMethod")
|
||||
|
||||
package com.geeksville.mesh.ui.radioconfig.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@@ -29,12 +31,13 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig
|
||||
@@ -43,17 +46,20 @@ import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.ui.common.components.EditPasswordPreference
|
||||
import com.geeksville.mesh.ui.common.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.common.components.PositionPrecisionPreference
|
||||
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
|
||||
|
||||
const val MapConsentPreferencesKey = "map_consent_preferences"
|
||||
|
||||
@Composable
|
||||
fun MQTTConfigScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val destNum = destNode?.num
|
||||
|
||||
if (state.responseState.isWaiting()) {
|
||||
PacketResponseStateDialog(
|
||||
@@ -63,6 +69,7 @@ fun MQTTConfigScreen(
|
||||
}
|
||||
|
||||
MQTTConfigItemList(
|
||||
nodeNum = destNum,
|
||||
mqttConfig = state.moduleConfig.mqtt,
|
||||
enabled = state.connected,
|
||||
onSaveClicked = { mqttInput ->
|
||||
@@ -74,12 +81,22 @@ fun MQTTConfigScreen(
|
||||
|
||||
@Composable
|
||||
fun MQTTConfigItemList(
|
||||
nodeNum: Int? = 0,
|
||||
mqttConfig: MQTTConfig,
|
||||
enabled: Boolean,
|
||||
onSaveClicked: (MQTTConfig) -> Unit,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
var mqttInput by rememberSaveable { mutableStateOf(mqttConfig) }
|
||||
val sharedPrefs = LocalContext.current.getSharedPreferences(
|
||||
MapConsentPreferencesKey, Context.MODE_PRIVATE
|
||||
)
|
||||
if (!mqttInput.mapReportSettings.shouldReportLocation) {
|
||||
val settings = mqttInput.mapReportSettings.copy {
|
||||
this.shouldReportLocation = sharedPrefs.getBoolean(nodeNum.toString(), false)
|
||||
}
|
||||
mqttInput = mqttInput.copy { mapReportSettings = settings }
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@@ -191,40 +208,45 @@ fun MQTTConfigItemList(
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
// mqtt map reporting opt in
|
||||
item { PreferenceCategory(text = stringResource(R.string.map_reporting)) }
|
||||
|
||||
item {
|
||||
PositionPrecisionPreference(
|
||||
title = stringResource(R.string.map_reporting),
|
||||
enabled = enabled,
|
||||
value = mqttInput.mapReportSettings.positionPrecision,
|
||||
onValueChanged = {
|
||||
val settings = mqttInput.mapReportSettings.copy { positionPrecision = it }
|
||||
MapReportingPreference(
|
||||
mapReportingEnabled = mqttInput.mapReportingEnabled,
|
||||
onMapReportingEnabledChanged = {
|
||||
mqttInput = mqttInput.copy { mapReportingEnabled = it }
|
||||
},
|
||||
shouldReportLocation = mqttInput.mapReportSettings.shouldReportLocation,
|
||||
onShouldReportLocationChanged = {
|
||||
sharedPrefs.edit { putBoolean(nodeNum.toString(), it) }
|
||||
val settings =
|
||||
mqttInput.mapReportSettings.copy { this.shouldReportLocation = it }
|
||||
mqttInput = mqttInput.copy {
|
||||
mapReportingEnabled = settings.positionPrecision > 0
|
||||
mapReportSettings = settings
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
positionPrecision = mqttInput.mapReportSettings.positionPrecision,
|
||||
onPositionPrecisionChanged = {
|
||||
val settings = mqttInput.mapReportSettings.copy { positionPrecision = it }
|
||||
mqttInput = mqttInput.copy {
|
||||
mapReportSettings = settings
|
||||
}
|
||||
},
|
||||
enabled = enabled,
|
||||
focusManager = focusManager
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.map_reporting_interval_seconds),
|
||||
value = mqttInput.mapReportSettings.publishIntervalSecs,
|
||||
enabled = enabled && mqttInput.mapReportingEnabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = {
|
||||
val settings = mqttInput.mapReportSettings.copy { publishIntervalSecs = it }
|
||||
mqttInput = mqttInput.copy { mapReportSettings = settings }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
val consentValid = if (mqttInput.mapReportingEnabled) {
|
||||
mqttInput.mapReportSettings.shouldReportLocation
|
||||
} else {
|
||||
true
|
||||
}
|
||||
PreferenceFooter(
|
||||
enabled = enabled && mqttInput != mqttConfig,
|
||||
enabled = enabled && mqttInput != mqttConfig && consentValid,
|
||||
onCancelClicked = {
|
||||
focusManager.clearFocus()
|
||||
mqttInput = mqttConfig
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.radioconfig.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
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.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.ui.common.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.common.components.PositionPrecisionMax
|
||||
import com.geeksville.mesh.ui.common.components.PositionPrecisionMin
|
||||
import com.geeksville.mesh.ui.common.components.SwitchPreference
|
||||
import com.geeksville.mesh.ui.common.components.precisionBitsToMeters
|
||||
import com.geeksville.mesh.util.DistanceUnit
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MapReportingPreference(
|
||||
mapReportingEnabled: Boolean = false,
|
||||
onMapReportingEnabledChanged: (Boolean) -> Unit = {},
|
||||
shouldReportLocation: Boolean = false,
|
||||
onShouldReportLocationChanged: (Boolean) -> Unit = {},
|
||||
positionPrecision: Int = 14,
|
||||
onPositionPrecisionChanged: (Int) -> Unit = {},
|
||||
publishIntervalSecs: Int = 3600,
|
||||
onPublishIntervalSecsChanged: (Int) -> Unit = {},
|
||||
enabled: Boolean,
|
||||
focusManager: FocusManager
|
||||
) {
|
||||
Column {
|
||||
var showMapReportingWarning by rememberSaveable { mutableStateOf(mapReportingEnabled) }
|
||||
LaunchedEffect(mapReportingEnabled) {
|
||||
showMapReportingWarning = mapReportingEnabled
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.map_reporting),
|
||||
summary = stringResource(R.string.map_reporting_summary),
|
||||
checked = showMapReportingWarning,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { checked ->
|
||||
showMapReportingWarning = checked
|
||||
if (checked && shouldReportLocation) {
|
||||
onMapReportingEnabledChanged(true)
|
||||
} else if (!checked) {
|
||||
onMapReportingEnabledChanged(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
AnimatedVisibility(showMapReportingWarning) {
|
||||
Card(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.map_reporting_consent_header),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
HorizontalDivider()
|
||||
Text(
|
||||
stringResource(R.string.map_reporting_consent_text),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.i_agree),
|
||||
summary = stringResource(R.string.i_agree_to_share_my_location),
|
||||
checked = shouldReportLocation,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
onMapReportingEnabledChanged(true)
|
||||
onShouldReportLocationChanged(true)
|
||||
} else {
|
||||
onShouldReportLocationChanged(false)
|
||||
}
|
||||
},
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
if (shouldReportLocation && mapReportingEnabled) {
|
||||
Slider(
|
||||
modifier = Modifier.Companion.padding(horizontal = 16.dp),
|
||||
value = positionPrecision.toFloat(),
|
||||
onValueChange = { onPositionPrecisionChanged(it.roundToInt()) },
|
||||
enabled = enabled,
|
||||
valueRange = PositionPrecisionMin.toFloat()..PositionPrecisionMax.toFloat(),
|
||||
steps = PositionPrecisionMax - PositionPrecisionMin - 1,
|
||||
)
|
||||
val precisionMeters = precisionBitsToMeters(positionPrecision).toInt()
|
||||
val unit = DistanceUnit.Companion.getFromLocale()
|
||||
Text(
|
||||
text = precisionMeters.toDistanceString(unit),
|
||||
modifier = Modifier.Companion.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
),
|
||||
fontSize = MaterialTheme.typography.bodyLarge.fontSize,
|
||||
overflow = TextOverflow.Companion.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
EditTextPreference(
|
||||
modifier = Modifier.Companion.padding(bottom = 16.dp),
|
||||
title = stringResource(R.string.map_reporting_interval_seconds),
|
||||
value = publishIntervalSecs,
|
||||
enabled = enabled,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = onPublishIntervalSecsChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun MapReportingPreview() {
|
||||
val focusManager = LocalFocusManager.current
|
||||
MapReportingPreference(
|
||||
mapReportingEnabled = true,
|
||||
onMapReportingEnabledChanged = {},
|
||||
shouldReportLocation = true,
|
||||
onShouldReportLocationChanged = {},
|
||||
positionPrecision = 5,
|
||||
onPositionPrecisionChanged = {},
|
||||
enabled = true,
|
||||
focusManager = focusManager
|
||||
)
|
||||
}
|
||||
@@ -662,4 +662,9 @@
|
||||
<string name="nodes">Nodes</string>
|
||||
<string name="set_your_region">Set your region</string>
|
||||
<string name="reply">Reply</string>
|
||||
<string name="map_reporting_summary">Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name.</string>
|
||||
<string name="map_reporting_consent_header">Consent to Share Unencrypted Node Data via MQTT</string>
|
||||
<string name="map_reporting_consent_text">By enabling this feature, you acknowledge and expressly consent to the transmission of your device’s real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions.</string>
|
||||
<string name="i_agree_to_share_my_location">I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT</string>
|
||||
<string name="i_agree">I agree.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user