mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
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:
parent
da1fbc7963
commit
33c5391a67
3 changed files with 222 additions and 162 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue