From 7c30d86e395d5a49167c52743dfe82932298fbf5 Mon Sep 17 00:00:00 2001 From: andrekir Date: Tue, 3 Oct 2023 18:05:40 -0300 Subject: [PATCH] feat: add feedback for configuration changes --- .../com/geeksville/mesh/IMeshService.aidl | 8 +- .../java/com/geeksville/mesh/model/UIState.kt | 136 ++++++++++-------- .../geeksville/mesh/service/MeshService.kt | 68 ++++----- .../mesh/ui/DeviceSettingsFragment.kt | 43 +++--- .../config/ChannelSettingsItemList.kt | 16 +-- .../config/PacketResponseStateDialog.kt | 10 +- 6 files changed, 138 insertions(+), 143 deletions(-) diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index cac319aaa..67bb11f09 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -58,7 +58,7 @@ interface IMeshService { */ void setOwner(in MeshUser user); - void setRemoteOwner(in int destNum, in byte []payload); + void setRemoteOwner(in int requestId, in byte []payload); void getRemoteOwner(in int requestId, in int destNum); /// Return my unique user ID string @@ -91,11 +91,11 @@ interface IMeshService { void setConfig(in byte []payload); /// Set and get a Config protobuf via admin packet - void setRemoteConfig(in int destNum, in byte []payload); + void setRemoteConfig(in int requestId, in int destNum, in byte []payload); void getRemoteConfig(in int requestId, in int destNum, in int configTypeValue); /// Set and get a ModuleConfig protobuf via admin packet - void setModuleConfig(in int destNum, in byte []payload); + void setModuleConfig(in int requestId, in int destNum, in byte []payload); void getModuleConfig(in int requestId, in int destNum, in int moduleConfigTypeValue); /// Set and get the Ext Notification Ringtone string via admin packet @@ -111,7 +111,7 @@ interface IMeshService { void setChannel(in byte []payload); /// Set and get a Channel protobuf via admin packet - void setRemoteChannel(in int destNum, in byte []payload); + void setRemoteChannel(in int requestId, in int destNum, in byte []payload); void getRemoteChannel(in int requestId, in int destNum, in int channelIndex); /// Send beginEditSettings admin packet to nodeNum diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index cfb9f1a6a..2834a7311 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -135,7 +135,7 @@ class UIViewModel @Inject constructor( private val _ourNodeInfo = MutableStateFlow(null) val ourNodeInfo: StateFlow = _ourNodeInfo - private val requestId = MutableStateFlow(null) + private val requestIds = MutableStateFlow>(hashMapOf()) private val _radioConfigState = MutableStateFlow(RadioConfigState()) val radioConfigState: StateFlow = _radioConfigState @@ -169,11 +169,11 @@ class UIViewModel @Inject constructor( }.launchIn(viewModelScope) viewModelScope.launch { - combine(meshLogRepository.getAllLogs(9), requestId) { list, id -> - list.takeIf { id != null }?.firstOrNull { it.meshPacket?.decoded?.requestId == id } - }.collect(::processPacketResponse) + combine(meshLogRepository.getAllLogs(9), requestIds) { list, ids -> + val unprocessed = ids.filterValues { !it }.keys.ifEmpty { return@combine emptyList() } + list.filter { log -> log.meshPacket?.decoded?.requestId in unprocessed } + }.collect { it.forEach(::processPacketResponse) } } - debug("ViewModel created") } @@ -255,7 +255,15 @@ class UIViewModel @Inject constructor( val packetId = service.packetId try { requestAction(service, packetId, destNum) - requestId.value = packetId + requestIds.update { it.apply { put(packetId, false) } } + _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(responseState = ResponseState.Loading()) + } + } } catch (ex: RemoteException) { errormsg("$errorMessage: ${ex.message}") } @@ -268,18 +276,44 @@ class UIViewModel @Inject constructor( "Request getOwner error" ) + private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) = request( + destNum, + { service, packetId, dest -> + service.setRemoteChannel(packetId, dest, channel.toByteArray()) + }, + "Request setRemoteChannel error" + ) + fun getChannel(destNum: Int, index: Int) = request( destNum, { service, packetId, dest -> service.getRemoteChannel(packetId, dest, index) }, "Request getChannel error" ) + fun setRemoteConfig(destNum: Int, config: Config) = request( + destNum, + { service, packetId, dest -> + _radioConfigState.update { it.copy(radioConfig = config) } + service.setRemoteConfig(packetId, dest, config.toByteArray()) + }, + "Request setConfig error", + ) + fun getConfig(destNum: Int, configType: Int) = request( destNum, { service, packetId, dest -> service.getRemoteConfig(packetId, dest, configType) }, "Request getConfig error", ) + fun setModuleConfig(destNum: Int, config: ModuleConfig) = request( + destNum, + { service, packetId, dest -> + _radioConfigState.update { it.copy(moduleConfig = config) } + service.setModuleConfig(packetId, dest, config.toByteArray()) + }, + "Request setConfig error", + ) + fun getModuleConfig(destNum: Int, configType: Int) = request( destNum, { service, packetId, dest -> service.getModuleConfig(packetId, dest, configType) }, @@ -472,16 +506,6 @@ class UIViewModel @Inject constructor( meshService?.setConfig(config.toByteArray()) } - fun setRemoteConfig(destNum: Int, config: Config) { - _radioConfigState.update { it.copy(radioConfig = config) } - meshService?.setRemoteConfig(destNum, config.toByteArray()) - } - - fun setModuleConfig(destNum: Int, config: ModuleConfig) { - _radioConfigState.update { it.copy(moduleConfig = config) } - meshService?.setModuleConfig(destNum, config.toByteArray()) - } - fun setModuleConfig(config: ModuleConfig) { setModuleConfig(myNodeNum ?: return, config) } @@ -515,6 +539,7 @@ class UIViewModel @Inject constructor( if (destNum == myNodeNum) viewModelScope.launch { radioConfigRepository.replaceAllSettings(new) } + _radioConfigState.update { it.copy(channelList = new) } } private fun updateChannels( @@ -542,14 +567,6 @@ class UIViewModel @Inject constructor( this._channelSet = channelSet.protobuf } - private fun setRemoteChannel(destNum: Int, channel: ChannelProtos.Channel) { - try { - meshService?.setRemoteChannel(destNum, channel.toByteArray()) - } catch (ex: RemoteException) { - errormsg("Can't set channel on radio ${ex.message}") - } - } - val provideLocation = object : MutableLiveData(preferences.getBoolean("provide-location", false)) { override fun setValue(value: Boolean) { super.setValue(value) @@ -564,15 +581,14 @@ class UIViewModel @Inject constructor( setRemoteOwner(myNodeNum ?: return, user) } - fun setRemoteOwner(destNum: Int, user: User) { - try { - // Note: we use ?. here because we might be running in the emulator - meshService?.setRemoteOwner(destNum, user.toByteArray()) + fun setRemoteOwner(destNum: Int, user: User) = request( + destNum, + { service, packetId, _ -> _radioConfigState.update { it.copy(userConfig = user) } - } catch (ex: RemoteException) { - errormsg("Can't set username on device, is device offline? ${ex.message}") - } - } + service.setRemoteOwner(packetId, user.toByteArray()) + }, + "Request setOwner error", + ) val adminChannelIndex: Int /** matches [MeshService.adminChannelIndex] **/ get() = channelSet.settingsList.indexOfFirst { it.name.equals("admin", ignoreCase = true) } @@ -723,6 +739,7 @@ class UIViewModel @Inject constructor( message.writeTo(outputStream) } } + setResponseStateSuccess() } catch (ex: Exception) { val error = "${ex.javaClass.simpleName}: ${ex.message}" errormsg("Can't write file error: ${ex.message}") @@ -763,6 +780,7 @@ class UIViewModel @Inject constructor( setModuleConfig(moduleConfig { audio = it.audio }) setModuleConfig(moduleConfig { remoteHardware = it.remoteHardware }) } + setResponseStateSuccess() // meshService?.commitEditSettings() } @@ -806,17 +824,20 @@ class UIViewModel @Inject constructor( } fun clearPacketResponse() { + requestIds.value = hashMapOf() _radioConfigState.update { it.copy(responseState = ResponseState.Empty) } } fun setResponseStateLoading(route: String) { _radioConfigState.value = RadioConfigState( route = route, - responseState = ResponseState.Loading(total = 1), + responseState = ResponseState.Loading(), ) + // channel editor is synchronous, so we don't use requestIds as total + if (route == ConfigRoute.CHANNELS.name) setResponseStateTotal(maxChannels + 1) } - fun setResponseStateTotal(total: Int) { + private fun setResponseStateTotal(total: Int) { _radioConfigState.update { state -> if (state.responseState is ResponseState.Loading) { state.copy(responseState = state.responseState.copy(total = total)) @@ -826,6 +847,16 @@ class UIViewModel @Inject constructor( } } + private fun setResponseStateSuccess() { + _radioConfigState.update { state -> + if (state.responseState is ResponseState.Loading) { + state.copy(responseState = ResponseState.Success(true)) + } else { + state // Return the unchanged state for other response states + } + } + } + private fun setResponseStateError(error: String) { _radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) } } @@ -841,14 +872,6 @@ class UIViewModel @Inject constructor( } } - fun clearRemoteChannelList() { - _radioConfigState.update { it.copy(channelList = emptyList()) } - } - - fun setRemoteChannelList(list: List) { - _radioConfigState.update { it.copy(channelList = list) } - } - private val _tracerouteResponse = MutableLiveData(null) val tracerouteResponse: LiveData get() = _tracerouteResponse @@ -859,8 +882,7 @@ class UIViewModel @Inject constructor( private fun processPacketResponse(log: MeshLog?) { val packet = log?.meshPacket ?: return val data = packet.decoded - val fromStr = packet.from.toUInt() - requestId.value = null + requestIds.update { it.apply { put(data.requestId, true) } } if (data?.portnumValue == Portnums.PortNum.TRACEROUTE_APP_VALUE) { val parsed = MeshProtos.RouteDiscovery.parseFrom(data.payload) @@ -874,25 +896,26 @@ class UIViewModel @Inject constructor( } } val destNum = destNode.value?.num ?: return - val destStr = destNum.toUInt() + val debugMsg = "requestId: ${data.requestId.toUInt()} to: ${destNum.toUInt()} received %s from: ${packet.from.toUInt()}" if (data?.portnumValue == Portnums.PortNum.ROUTING_APP_VALUE) { val parsed = MeshProtos.Routing.parseFrom(data.payload) - debug("packet for destNum $destStr received ${parsed.errorReason} from $fromStr") + debug(debugMsg.format(parsed.errorReason.name)) if (parsed.errorReason != MeshProtos.Routing.Error.NONE) { setResponseStateError(parsed.errorReason.name) } else if (packet.from == destNum) { - _radioConfigState.update { it.copy(responseState = ResponseState.Success(true)) } + if (requestIds.value.filterValues { !it }.isEmpty()) setResponseStateSuccess() + else incrementCompleted() } } if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) { val parsed = AdminProtos.AdminMessage.parseFrom(data.payload) - debug("packet for destNum $destStr received ${parsed.payloadVariantCase} from $fromStr") + debug(debugMsg.format(parsed.payloadVariantCase.name)) if (destNum != packet.from) { - setResponseStateError("Unexpected sender: $fromStr instead of $destStr.") + setResponseStateError("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}.") return } - // check destination: lora config or channel editor + // check if destination is channel editor val goChannels = radioConfigState.value.route == ConfigRoute.CHANNELS.name when (parsed.payloadVariantCase) { AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> { @@ -900,23 +923,18 @@ class UIViewModel @Inject constructor( // Stop once we get to the first disabled entry if (response.role != ChannelProtos.Channel.Role.DISABLED) { _radioConfigState.update { state -> - val updatedList = state.channelList.toMutableList().apply { + state.copy(channelList = state.channelList.toMutableList().apply { add(response.index, response.settings) - } - state.copy(channelList = updatedList) + }) } incrementCompleted() if (response.index + 1 < maxChannels && goChannels) { // Not done yet, request next channel getChannel(destNum, response.index + 1) - } else { - // Received max channels, get lora config (for default channel names) - getConfig(destNum, AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE) } } else { - // Received last channel, get lora config (for default channel names) + // Received last channel, update total and start channel editor setResponseStateTotal(response.index + 1) - getConfig(destNum, AdminProtos.AdminMessage.ConfigType.LORA_CONFIG_VALUE) } } @@ -948,13 +966,11 @@ class UIViewModel @Inject constructor( it.copy(cannedMessageMessages = parsed.getCannedMessageModuleMessagesResponse) } incrementCompleted() - getModuleConfig(destNum, AdminProtos.AdminMessage.ModuleConfigType.CANNEDMSG_CONFIG_VALUE) } AdminProtos.AdminMessage.PayloadVariantCase.GET_RINGTONE_RESPONSE -> { _radioConfigState.update { it.copy(ringtone = parsed.getRingtoneResponse) } incrementCompleted() - getModuleConfig(destNum, AdminProtos.AdminMessage.ModuleConfigType.EXTNOTIF_CONFIG_VALUE) } else -> TODO() diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index c3a4002f3..4b7220245 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1483,35 +1483,23 @@ class MeshService : Service(), Logging { /** * Send setOwner admin packet with [MeshProtos.User] protobuf */ - fun setOwner(meshUser: MeshUser) = with(meshUser) { + private fun setOwner(packetId: Int, user: MeshProtos.User) = with(user) { val dest = nodeDBbyID[id] - if (dest != null) { - val old = dest.user - if (longName == old?.longName && shortName == old.shortName && isLicensed == old.isLicensed) - debug("Ignoring nop owner change") - else { - debug("SetOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed") + ?: throw Exception("Can't set user without a NodeInfo") // this shouldn't happen + val old = dest.user!! + if (longName == old.longName && shortName == old.shortName && isLicensed == old.isLicensed) { + debug("Ignoring nop owner change") + } else { + debug("setOwner Id: $id longName: ${longName.anonymize} shortName: $shortName isLicensed: $isLicensed") - val user = MeshProtos.User.newBuilder().also { - it.longName = longName - it.shortName = shortName - it.hwModel = hwModel - it.isLicensed = isLicensed - }.build() + // Also update our own map for our nodeNum, by handling the packet just like packets from other users + handleReceivedUser(dest.num, user) - // Also update our own map for our nodenum, by handling the packet just like packets from other users - handleReceivedUser(dest.num, user) - - // encapsulate our payload in the proper protobufs and fire it off - val packet = newMeshPacketTo(dest.num).buildAdminPacket { - setOwner = user - } - - // send the packet into the mesh - sendToRadio(packet) - } - } else - throw Exception("Can't set user without a node info") // this shouldn't happen + // encapsulate our payload in the proper protobuf and fire it off + sendToRadio(newMeshPacketTo(dest.num).buildAdminPacket(id = packetId) { + setOwner = user + }) + } } @@ -1614,14 +1602,12 @@ class MeshService : Service(), Logging { override fun getPacketId() = toRemoteExceptions { generatePacketId() } override fun setOwner(user: MeshUser) = toRemoteExceptions { - this@MeshService.setOwner(user) + setOwner(generatePacketId(), user.toProto()) } - override fun setRemoteOwner(destNum: Int, payload: ByteArray) = toRemoteExceptions { + override fun setRemoteOwner(id: Int, payload: ByteArray) = toRemoteExceptions { val parsed = MeshProtos.User.parseFrom(payload) - sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { - setOwner = parsed - }) + setOwner(id, parsed) } override fun getRemoteOwner(id: Int, destNum: Int) = toRemoteExceptions { @@ -1674,14 +1660,14 @@ class MeshService : Service(), Logging { /** Send our current radio config to the device */ override fun setConfig(payload: ByteArray) = toRemoteExceptions { - setRemoteConfig(myNodeNum, payload) + setRemoteConfig(generatePacketId(), myNodeNum, payload) } - override fun setRemoteConfig(destNum: Int, payload: ByteArray) = toRemoteExceptions { + override fun setRemoteConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions { debug("Setting new radio config!") val config = ConfigProtos.Config.parseFrom(payload) - sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setConfig = config }) - if (destNum == myNodeNum) setLocalConfig(config) // Update our local copy + sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setConfig = config }) + if (num == myNodeNum) setLocalConfig(config) // Update our local copy } override fun getRemoteConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions { @@ -1692,11 +1678,11 @@ class MeshService : Service(), Logging { /** Send our current module config to the device */ - override fun setModuleConfig(destNum: Int, payload: ByteArray) = toRemoteExceptions { + override fun setModuleConfig(id: Int, num: Int, payload: ByteArray) = toRemoteExceptions { debug("Setting new module config!") val config = ModuleConfigProtos.ModuleConfig.parseFrom(payload) - sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setModuleConfig = config }) - if (destNum == myNodeNum) setLocalModuleConfig(config) // Update our local copy + sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setModuleConfig = config }) + if (num == myNodeNum) setLocalModuleConfig(config) // Update our local copy } override fun getModuleConfig(id: Int, destNum: Int, config: Int) = toRemoteExceptions { @@ -1730,12 +1716,12 @@ class MeshService : Service(), Logging { } override fun setChannel(payload: ByteArray?) = toRemoteExceptions { - setRemoteChannel(myNodeNum, payload) + setRemoteChannel(generatePacketId(), myNodeNum, payload) } - override fun setRemoteChannel(destNum: Int, payload: ByteArray?) = toRemoteExceptions { + override fun setRemoteChannel(id: Int, num: Int, payload: ByteArray?) = toRemoteExceptions { val channel = ChannelProtos.Channel.parseFrom(payload) - sendToRadio(newMeshPacketTo(destNum).buildAdminPacket { setChannel = channel }) + sendToRadio(newMeshPacketTo(num).buildAdminPacket(id = id) { setChannel = channel }) } override fun getRemoteChannel(id: Int, destNum: Int, index: Int) = toRemoteExceptions { diff --git a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt index 0394092ae..990f6f1a5 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt @@ -191,7 +191,7 @@ private fun getName(route: Any): String = when (route) { */ sealed class ResponseState { data object Empty : ResponseState() - data class Loading(var total: Int = 0, var completed: Int = 0) : ResponseState() + data class Loading(var total: Int = 1, var completed: Int = 0) : ResponseState() data class Success(val result: T) : ResponseState() data class Error(val error: String) : ResponseState() } @@ -231,7 +231,6 @@ fun RadioConfigNavHost( val destNum = node?.num ?: 0 val isLocal = destNum == viewModel.myNodeNum - val maxChannels = viewModel.maxChannels val radioConfigState by viewModel.radioConfigState.collectAsStateWithLifecycle() var location by remember(node) { mutableStateOf(node?.position) } // FIXME @@ -314,12 +313,15 @@ fun RadioConfigNavHost( onRouteClick = { route -> viewModel.setResponseStateLoading(getName(route)) when (route) { - ConfigRoute.USER -> { viewModel.getOwner(destNum) } - ConfigRoute.CHANNELS -> { - viewModel.setResponseStateTotal(maxChannels + 1) // for lora config - viewModel.clearRemoteChannelList() - viewModel.getChannel(destNum, 0) + ConfigRoute.USER -> { + viewModel.getOwner(destNum) } + + ConfigRoute.CHANNELS -> { + viewModel.getChannel(destNum, 0) + viewModel.getConfig(destNum, ConfigRoute.LORA.configType) + } + "IMPORT" -> { viewModel.clearPacketResponse() viewModel.setDeviceProfile(null) @@ -329,6 +331,7 @@ fun RadioConfigNavHost( } importConfigLauncher.launch(intent) } + "EXPORT" -> { viewModel.clearPacketResponse() showEditDeviceProfileDialog = true @@ -350,23 +353,20 @@ fun RadioConfigNavHost( viewModel.requestNodedbReset(destNum) } - ConfigRoute.LORA -> { - viewModel.setResponseStateTotal(2) - viewModel.clearRemoteChannelList() - viewModel.getChannel(destNum, 0) - } is ConfigRoute -> { + if (route == ConfigRoute.LORA) { + viewModel.getChannel(destNum, 0) + } viewModel.getConfig(destNum, route.configType) } - ModuleRoute.CANNED_MESSAGE -> { - viewModel.setResponseStateTotal(2) - viewModel.getCannedMessages(destNum) - } - ModuleRoute.EXTERNAL_NOTIFICATION -> { - viewModel.setResponseStateTotal(2) - viewModel.getRingtone(destNum) - } + is ModuleRoute -> { + if (route == ModuleRoute.CANNED_MESSAGE) { + viewModel.getCannedMessages(destNum) + } + if (route == ModuleRoute.EXTERNAL_NOTIFICATION) { + viewModel.getRingtone(destNum) + } viewModel.getModuleConfig(destNum, route.configType) } } @@ -387,10 +387,9 @@ fun RadioConfigNavHost( settingsList = radioConfigState.channelList, modemPresetName = Channel(loraConfig = radioConfigState.radioConfig.lora).name, enabled = connected, - maxChannels = maxChannels, + maxChannels = viewModel.maxChannels, onPositiveClicked = { channelListInput -> viewModel.updateChannels(destNum, radioConfigState.channelList, channelListInput) - viewModel.setRemoteChannelList(channelListInput) }, ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt index 62f283867..f61c89a8a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/ChannelSettingsItemList.kt @@ -28,10 +28,10 @@ import androidx.compose.material.icons.twotone.Add import androidx.compose.material.icons.twotone.Close import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -94,9 +94,7 @@ fun ChannelSettingsItemList( onPositiveClicked: (List) -> Unit, ) { val focusManager = LocalFocusManager.current - val settingsListInput = remember { - mutableStateListOf().apply { addAll(settingsList) } - } + val settingsListInput = remember(settingsList) { settingsList.toMutableStateList() } val isEditing: Boolean = settingsList.size != settingsListInput.size || settingsList.zip(settingsListInput).any { (item1, item2) -> item1 != item2 } @@ -172,10 +170,12 @@ fun ChannelSettingsItemList( ) { FloatingActionButton( onClick = { - settingsListInput.add(channelSettings { - psk = Channel.default.settings.psk - }) - showEditChannelDialog = settingsListInput.lastIndex + if (maxChannels > settingsListInput.size) { + settingsListInput.add(channelSettings { + psk = Channel.default.settings.psk + }) + showEditChannelDialog = settingsListInput.lastIndex + } }, modifier = Modifier.padding(16.dp) ) { Icon(Icons.TwoTone.Add, stringResource(R.string.add)) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt index eb2fd803d..c644449b0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/PacketResponseStateDialog.kt @@ -51,7 +51,7 @@ fun PacketResponseStateDialog( if (state.total == state.completed) onComplete() } if (state is ResponseState.Success) { - Text("Success!") + Text("Delivery confirmed.") } if (state is ResponseState.Error) { Text(text = "Error\n", textAlign = TextAlign.Center) @@ -67,13 +67,7 @@ fun PacketResponseStateDialog( Button( onClick = onDismiss, modifier = Modifier.padding(top = 16.dp) - ) { - if (state is ResponseState.Loading) { - Text(stringResource(R.string.cancel)) - } else { - Text(stringResource(R.string.close)) - } - } + ) { Text(stringResource(R.string.close)) } } } )