diff --git a/app/src/androidTest/java/com/geeksville/mesh/compose/EditDeviceProfileDialogTest.kt b/app/src/androidTest/java/com/geeksville/mesh/compose/EditDeviceProfileDialogTest.kt new file mode 100644 index 000000000..c687dd588 --- /dev/null +++ b/app/src/androidTest/java/com/geeksville/mesh/compose/EditDeviceProfileDialogTest.kt @@ -0,0 +1,93 @@ +package com.geeksville.mesh.compose + +import androidx.compose.ui.test.assertIsDisplayed +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.ClientOnlyProtos.DeviceProfile +import com.geeksville.mesh.R +import com.geeksville.mesh.deviceProfile +import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EditDeviceProfileDialogTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private fun getString(id: Int): String = + InstrumentationRegistry.getInstrumentation().targetContext.getString(id) + + private val title = "Export configuration" + private val deviceProfile = deviceProfile { + longName = "Long name" + shortName = "Short name" + channelUrl = "https://meshtastic.org/e/#CgMSAQESBggBQANIAQ" + } + + private fun testEditDeviceProfileDialog( + onDismissRequest: () -> Unit = {}, + onAddClick: (DeviceProfile) -> Unit = {}, + ) = composeTestRule.setContent { + EditDeviceProfileDialog( + title = title, + deviceProfile = deviceProfile, + onAddClick = onAddClick, + onDismissRequest = onDismissRequest, + ) + } + + @Test + fun testEditDeviceProfileDialog_showsDialogTitle() { + composeTestRule.apply { + testEditDeviceProfileDialog() + + // Verify that the dialog title is displayed + onNodeWithText(title).assertIsDisplayed() + } + } + + @Test + fun testEditDeviceProfileDialog_showsCancelAndSaveButtons() { + composeTestRule.apply { + testEditDeviceProfileDialog() + + // Verify the "Cancel" and "Save" buttons are displayed + onNodeWithText(getString(R.string.cancel)).assertIsDisplayed() + onNodeWithText(getString(R.string.save)).assertIsDisplayed() + } + } + + @Test + fun testEditDeviceProfileDialog_clickCancelButton() { + var onDismissClicked = false + composeTestRule.apply { + testEditDeviceProfileDialog(onDismissRequest = { onDismissClicked = true }) + + // Click the "Cancel" button + onNodeWithText(getString(R.string.cancel)).performClick() + } + + // Verify onDismiss is called + Assert.assertTrue(onDismissClicked) + } + + @Test + fun testEditDeviceProfileDialog_addChannels() { + var actualDeviceProfile: DeviceProfile? = null + composeTestRule.apply { + testEditDeviceProfileDialog(onAddClick = { actualDeviceProfile = it }) + + onNodeWithText(getString(R.string.save)).performClick() + } + + // Verify onConfirm is called with the correct DeviceProfile + Assert.assertEquals(deviceProfile, actualDeviceProfile) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt index b90318af5..6dee65d2e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/RadioConfigViewModel.kt @@ -326,7 +326,8 @@ class RadioConfigViewModel @Inject constructor( if (hasLongName() || hasShortName()) destNode.value?.user?.let { val user = it.copy( longName = if (hasLongName()) longName else it.longName, - shortName = if (hasShortName()) shortName else it.shortName + shortName = if (hasShortName()) shortName else it.shortName, + hwModel = MeshProtos.HardwareModel.UNSET, ) if (it != user) setOwner(user.toProto()) } @@ -337,28 +338,22 @@ class RadioConfigViewModel @Inject constructor( setResponseStateError(ex.customMessage) } if (hasConfig()) { - setConfig(config { device = config.device }) - setConfig(config { position = config.position }) - setConfig(config { power = config.power }) - setConfig(config { network = config.network }) - setConfig(config { display = config.display }) - setConfig(config { lora = config.lora }) - setConfig(config { bluetooth = config.bluetooth }) + val descriptor = config.descriptorForType + config.allFields.forEach { (field, value) -> + val newConfig = ConfigProtos.Config.newBuilder() + .setField(descriptor.findFieldByName(field.name), value) + .build() + setConfig(newConfig) + } } - if (hasModuleConfig()) moduleConfig.let { - setModuleConfig(moduleConfig { mqtt = it.mqtt }) - setModuleConfig(moduleConfig { serial = it.serial }) - setModuleConfig(moduleConfig { externalNotification = it.externalNotification }) - setModuleConfig(moduleConfig { storeForward = it.storeForward }) - setModuleConfig(moduleConfig { rangeTest = it.rangeTest }) - setModuleConfig(moduleConfig { telemetry = it.telemetry }) - setModuleConfig(moduleConfig { cannedMessage = it.cannedMessage }) - setModuleConfig(moduleConfig { audio = it.audio }) - setModuleConfig(moduleConfig { remoteHardware = it.remoteHardware }) - setModuleConfig(moduleConfig { neighborInfo = it.neighborInfo }) - setModuleConfig(moduleConfig { ambientLighting = it.ambientLighting }) - setModuleConfig(moduleConfig { detectionSensor = it.detectionSensor }) - setModuleConfig(moduleConfig { paxcounter = it.paxcounter }) + if (hasModuleConfig()) { + val descriptor = moduleConfig.descriptorForType + moduleConfig.allFields.forEach { (field, value) -> + val newConfig = ModuleConfigProtos.ModuleConfig.newBuilder() + .setField(descriptor.findFieldByName(field.name), value) + .build() + setModuleConfig(newConfig) + } } meshService?.commitEditSettings() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt index 7755d925b..88854728b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/EditDeviceProfileDialog.kt @@ -1,18 +1,19 @@ package com.geeksville.mesh.ui.components.config +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.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -21,8 +22,9 @@ import com.geeksville.mesh.ClientOnlyProtos import com.geeksville.mesh.R import com.geeksville.mesh.deviceProfile import com.geeksville.mesh.ui.components.SwitchPreference -import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import com.google.protobuf.Descriptors +@OptIn(ExperimentalLayoutApi::class) @Composable fun EditDeviceProfileDialog( title: String, @@ -31,72 +33,55 @@ fun EditDeviceProfileDialog( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, ) { - var longNameInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasLongName()) } - var shortNameInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasShortName()) } - var channelUrlInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasChannelUrl()) } - var configInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasConfig()) } - var moduleConfigInput by remember(deviceProfile) { mutableStateOf(deviceProfile.hasModuleConfig()) } + val state = remember { + val fields = deviceProfile.descriptorForType.fields + mutableStateMapOf() + .apply { putAll(fields.associateWith(deviceProfile::hasField)) } + } AlertDialog( title = { Text(title) }, onDismissRequest = onDismissRequest, text = { - AppCompatTheme { - Column(modifier.fillMaxWidth()) { - SwitchPreference(title = "longName", - checked = longNameInput, - enabled = deviceProfile.hasLongName(), - onCheckedChange = { longNameInput = it } - ) - SwitchPreference(title = "shortName", - checked = shortNameInput, - enabled = deviceProfile.hasShortName(), - onCheckedChange = { shortNameInput = it } - ) - SwitchPreference(title = "channelUrl", - checked = channelUrlInput, - enabled = deviceProfile.hasChannelUrl(), - onCheckedChange = { channelUrlInput = it } - ) - SwitchPreference(title = "config", - checked = configInput, - enabled = deviceProfile.hasConfig(), - onCheckedChange = { configInput = it } - ) - SwitchPreference(title = "moduleConfig", - checked = moduleConfigInput, - enabled = deviceProfile.hasModuleConfig(), - onCheckedChange = { moduleConfigInput = it } + Column(modifier.fillMaxWidth()) { + state.keys.sortedBy { it.number }.forEach { field -> + SwitchPreference( + title = field.name, + checked = state[field] == true, + enabled = deviceProfile.hasField(field), + onCheckedChange = { state[field] = it }, + padding = PaddingValues(0.dp) ) } } }, buttons = { - Row( + FlowRow( modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - Button( + TextButton( modifier = modifier .fillMaxWidth() - .padding(start = 24.dp) + .padding(horizontal = 24.dp) .weight(1f), onClick = onDismissRequest ) { Text(stringResource(R.string.cancel)) } Button( modifier = modifier .fillMaxWidth() - .padding(end = 24.dp) + .padding(horizontal = 24.dp) .weight(1f), onClick = { - onAddClick(deviceProfile { - if (longNameInput) longName = deviceProfile.longName - if (shortNameInput) shortName = deviceProfile.shortName - if (channelUrlInput) channelUrl = deviceProfile.channelUrl - if (configInput) config = deviceProfile.config - if (moduleConfigInput) moduleConfig = deviceProfile.moduleConfig - }) + val builder = ClientOnlyProtos.DeviceProfile.newBuilder() + deviceProfile.allFields.forEach { (field, value) -> + if (state[field] == true) { + builder.setField(field, value) + } + } + onAddClick(builder.build()) }, + enabled = state.values.any { it }, ) { Text(stringResource(R.string.save)) } } }