Add UI for handling configurations for UART profile

This commit is contained in:
Sylwester Zieliński
2022-02-22 11:03:03 +01:00
parent da3235ca22
commit ded529410f
39 changed files with 810 additions and 104 deletions

View File

@@ -1,6 +1,17 @@
package no.nordicsemi.android.uart.data
import no.nordicsemi.android.uart.db.XmlCommand
private const val MACROS_SIZES = 9
data class UARTConfiguration(
val name: String,
val macros: List<UARTMacro>
)
val macros: List<UARTMacro?> = List<UARTMacro?>(9) { null }
) {
init {
if (macros.size < 9) {
throw IllegalArgumentException("Macros should always have 9 positions.")
}
}
}

View File

@@ -1,6 +1,6 @@
package no.nordicsemi.android.uart.data
import dagger.hilt.android.qualifiers.ApplicationContext
import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import no.nordicsemi.android.uart.db.*
@@ -16,7 +16,6 @@ import javax.inject.Singleton
@Singleton
internal class UARTPersistentDataSource @Inject constructor(
@ApplicationContext
private val configurationsDao: ConfigurationsDao,
) {
@@ -31,10 +30,14 @@ internal class UARTPersistentDataSource @Inject constructor(
}
}
private fun createMacro(macros: Array<XmlCommand?>): List<UARTMacro> {
return macros.filterNotNull().mapNotNull {
val icon = MacroIcon.create(it.iconIndex)
it.command?.let { c -> UARTMacro(icon, c, it.eol) }
private fun createMacro(macros: Array<XmlCommand?>): List<UARTMacro?> {
return macros.map {
if (it == null) {
null
} else {
val icon = MacroIcon.create(it.iconIndex)
it.command?.let { c -> UARTMacro(icon, c, it.eol) }
}
}
}
@@ -48,4 +51,8 @@ internal class UARTPersistentDataSource @Inject constructor(
configurationsDao.insert(Configuration(0, configuration.name, xml, 0))
}
suspend fun deleteConfiguration(configuration: UARTConfiguration) {
configurationsDao.delete(configuration.name)
}
}

View File

@@ -7,8 +7,8 @@ import androidx.room.PrimaryKey
@Entity(tableName = "configurations")
internal data class Configuration(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Int,
@ColumnInfo(name = "_id") val _id: Int?,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "xml") val xml: String,
@ColumnInfo(name = "deleted") val deleted: Int
@ColumnInfo(name = "deleted", defaultValue = "0") val deleted: Int
)

View File

@@ -14,4 +14,7 @@ internal interface ConfigurationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(configuration: Configuration)
@Query("DELETE FROM configurations WHERE name = :name")
suspend fun delete(name: String)
}

View File

@@ -28,13 +28,13 @@ import org.simpleframework.xml.core.PersistenceException
import org.simpleframework.xml.core.Validate
@Root
internal class XmlConfiguration {
internal class XmlConfiguration @JvmOverloads constructor(
@field:Attribute(required = false, empty = "Unnamed")
var name: String? = "",
@Attribute(required = false, empty = "Unnamed")
var name: String? = null
@ElementArray
val commands: Array<XmlCommand?> = arrayOfNulls(COMMANDS_COUNT)
@field:ElementArray
var commands: Array<XmlCommand?> = arrayOfNulls(COMMANDS_COUNT)
) {
@Validate
@Throws(PersistenceException::class)

View File

@@ -0,0 +1,81 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.TextField
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.utils.EMPTY
@Composable
internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit) {
val name = remember { mutableStateOf(String.EMPTY) }
val isError = remember { mutableStateOf(false) }
Column {
NameInput(name, isError)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onEvent(OnEditFinish) }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = {
if (isNameValid(name.value)) {
onEvent(OnEditFinish)
onEvent(OnAddConfiguration(name.value))
} else {
isError.value = true
}
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
}
}
}
@Composable
private fun NameInput(
name: MutableState<String>,
isError: MutableState<Boolean>
) {
Column {
TextField(
text = name.value,
hint = stringResource(id = R.string.uart_macro_dialog_command)
) {
isError.value = false
name.value = it
}
if (isError.value) {
Text(
text = stringResource(id = R.string.uart_name_empty),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(16.dp))
}
}
private fun isNameValid(name: String): Boolean {
return name.isNotBlank()
}

View File

@@ -1,18 +1,26 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.GridItemSpan
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -26,13 +34,17 @@ import no.nordicsemi.android.uart.data.MacroIcon
import no.nordicsemi.android.uart.data.UARTMacro
import no.nordicsemi.android.utils.EMPTY
@Composable
internal fun UARTAddMacroDialog(onDismiss: () -> Unit, onEvent: (UARTViewEvent) -> Unit) {
val command = remember { mutableStateOf(String.EMPTY) }
val newLineChar = remember { mutableStateOf(MacroEol.LF) }
val isError = remember { mutableStateOf(false) }
private const val GRID_SIZE = 5
Dialog(onDismissRequest = { onDismiss() }) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun UARTAddMacroDialog(onEvent: (UARTViewEvent) -> Unit) {
val newLineChar = remember { mutableStateOf(MacroEol.LF) }
val command = remember { mutableStateOf(String.EMPTY) }
val isError = remember { mutableStateOf(false) }
val selectedIcon = remember { mutableStateOf(MacroIcon.values()[0]) }
Dialog(onDismissRequest = { onEvent(OnEditFinish) }) {
Surface(
color = MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(10.dp),
@@ -47,59 +59,105 @@ internal fun UARTAddMacroDialog(onDismiss: () -> Unit, onEvent: (UARTViewEvent)
.padding(16.dp)
)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Column(modifier = Modifier.padding(bottom = 8.dp, start = 16.dp, end = 16.dp)) {
NewLineCharSection(newLineChar.value) { newLineChar.value = it }
Spacer(modifier = Modifier.size(16.dp))
TextField(
text = command.value,
hint = stringResource(id = R.string.uart_macro_dialog_command)
) {
isError.value = false
command.value = it
}
if (isError.value) {
Text(
text = stringResource(id = R.string.uart_macro_error),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
LazyVerticalGrid(
cells = GridCells.Fixed(GRID_SIZE),
modifier = Modifier
.padding(horizontal = 16.dp)
.wrapContentHeight()
) {
item(span = { GridItemSpan(GRID_SIZE) }) {
Column {
NewLineCharSection(newLineChar.value) { newLineChar.value = it }
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = {
if (isCommandValid(command.value)) {
onDismiss()
onEvent(OnCreateMacro(UARTMacro(MacroIcon.DOWN, command.value, newLineChar.value)))
} else {
isError.value = true
}
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
}
}
item(span = { GridItemSpan(GRID_SIZE) }) {
CommandInput(command, isError)
}
items(20) { item ->
val icon = MacroIcon.create(item)
val background = if (selectedIcon.value == icon) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.secondaryContainer
}
Image(
painter = painterResource(id = icon.toResId()),
contentDescription = stringResource(id = R.string.uart_macro_icon),
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(10.dp))
.clickable { selectedIcon.value = icon }
.background(background)
)
}
}
Spacer(modifier = Modifier.size(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onEvent(OnEditFinish) }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = {
if (isCommandValid(command.value)) {
onEvent(
OnCreateMacro(
UARTMacro(
selectedIcon.value,
command.value,
newLineChar.value
)
)
)
} else {
isError.value = true
}
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
}
}
}
}
}
@Composable
private fun CommandInput(
command: MutableState<String>,
isError: MutableState<Boolean>
) {
Column {
TextField(
text = command.value,
hint = stringResource(id = R.string.uart_macro_dialog_command)
) {
isError.value = false
command.value = it
}
if (isError.value) {
Text(
text = stringResource(id = R.string.uart_macro_error),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(16.dp))
}
}
@Composable
private fun NewLineCharSection(checkedItem: MacroEol, onItemClick: (MacroEol) -> Unit) {
val items = MacroEol.values().map {

View File

@@ -0,0 +1,83 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.theme.view.dialog.*
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.utils.exhaustive
@Composable
internal fun UARTConfigurationPicker(state: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
val showDialog = rememberSaveable { mutableStateOf(false) }
UARTConfigurationButton(state.selectedConfiguration) {
showDialog.value = true
}
if (showDialog.value) {
SelectWheelSizeDialog(state) {
when (it) {
FlowCanceled -> showDialog.value = false
is ItemSelectedResult -> {
onEvent(OnConfigurationSelected(state.configurations[it.index]))
showDialog.value = false
}
}.exhaustive
}
}
}
@Composable
internal fun SelectWheelSizeDialog(state: UARTViewState, onEvent: (StringListDialogResult) -> Unit) {
val wheelEntries = state.configurations.map { it.name }
StringListDialog(createConfig(wheelEntries) {
onEvent(it)
})
}
@Composable
private fun createConfig(entries: List<String>, onResult: (StringListDialogResult) -> Unit): StringListDialogConfig {
return StringListDialogConfig(
title = stringResource(id = R.string.uart_configuration_picker_dialog).toAnnotatedString(),
items = entries,
onResult = onResult
)
}
@Composable
internal fun UARTConfigurationButton(configuration: UARTConfiguration?, onClick: () -> Unit) {
OutlinedButton(onClick = { onClick() }) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
){
Column {
Text(
text = stringResource(id = R.string.uart_configuration_picker_hint),
style = MaterialTheme.typography.labelSmall
)
val text = configuration?.name ?: stringResource(id = R.string.uart_configuration_picker_not_selected)
Text(text = text, style = MaterialTheme.typography.bodyMedium)
}
Icon(Icons.Default.ArrowDropDown, contentDescription = "")
}
}
}

View File

@@ -1,11 +1,13 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -18,10 +20,9 @@ import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTMacro
@Composable
internal fun UARTContentView(state: UARTData, macros: List<UARTMacro>, onEvent: (UARTViewEvent) -> Unit) {
internal fun UARTContentView(state: UARTData, viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
@@ -30,7 +31,7 @@ internal fun UARTContentView(state: UARTData, macros: List<UARTMacro>, onEvent:
Spacer(modifier = Modifier.height(16.dp))
InputSection(macros, onEvent)
InputSection(viewState, onEvent)
Spacer(modifier = Modifier.height(16.dp))
@@ -43,11 +44,11 @@ internal fun UARTContentView(state: UARTData, macros: List<UARTMacro>, onEvent:
}
@Composable
private fun InputSection(macros: List<UARTMacro>, onEvent: (UARTViewEvent) -> Unit) {
val showSearchDialog = remember { mutableStateOf(false) }
private fun InputSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
val showDialog = remember { mutableStateOf(false) }
if (showSearchDialog.value) {
UARTAddMacroDialog(onDismiss = { showSearchDialog.value = false }, onEvent = onEvent)
if (showDialog.value) {
UARTAddConfigurationDialog(onEvent)
}
ScreenSection {
@@ -58,25 +59,31 @@ private fun InputSection(macros: List<UARTMacro>, onEvent: (UARTViewEvent) -> Un
Spacer(modifier = Modifier.height(16.dp))
macros.forEach {
MacroItem(macro = it, onEvent = onEvent)
Row {
Box(modifier = Modifier.weight(1f)) {
UARTConfigurationPicker(viewState, onEvent)
}
IconButton(onClick = { showDialog.value = true }) {
Icon(Icons.Default.Add, stringResource(id = R.string.uart_configuration_add))
}
Spacer(modifier = Modifier.height(16.dp))
viewState.selectedConfiguration?.let {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(Icons.Default.Edit, stringResource(id = R.string.uart_configuration_edit))
}
IconButton(onClick = { onEvent(OnDeleteConfiguration) }) {
Icon(Icons.Default.Delete, stringResource(id = R.string.uart_configuration_delete))
}
}
}
if (macros.isEmpty()) {
Text(
text = stringResource(id = R.string.uart_no_macros_info),
style = MaterialTheme.typography.bodyMedium
)
viewState.selectedConfiguration?.let {
Spacer(modifier = Modifier.height(16.dp))
}
Button(
onClick = { showSearchDialog.value = true }
) {
Text(text = stringResource(id = R.string.uart_add_macro))
UARTMacroView(it, viewState.isConfigurationEdited, onEvent)
}
}
}

View File

@@ -0,0 +1,125 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTMacro
private val divider = 4.dp
private val buttonSize = 80.dp
@Composable
internal fun UARTMacroView(
configuration: UARTConfiguration,
isEdited: Boolean,
onEvent: (UARTViewEvent) -> Unit
) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Row {
Item(configuration, isEdited, 0, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 1, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 2, onEvent)
}
Spacer(modifier = Modifier.size(divider))
Row {
Item(configuration, isEdited, 3, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 4, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 5, onEvent)
}
Spacer(modifier = Modifier.size(divider))
Row {
Item(configuration, isEdited, 6, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 7, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 8, onEvent)
}
}
}
@Composable
private fun Item(
configuration: UARTConfiguration,
isEdited: Boolean,
position: Int,
onEvent: (UARTViewEvent) -> Unit
) {
val macro = configuration.macros.getOrNull(position)
if (macro == null) {
EmptyButton(isEdited, position, onEvent)
} else {
MacroButton(macro, position, isEdited, onEvent)
}
}
@Composable
private fun MacroButton(
macro: UARTMacro,
position: Int,
isEdited: Boolean,
onEvent: (UARTViewEvent) -> Unit
) {
Image(
painter = painterResource(id = macro.icon.toResId()),
contentDescription = stringResource(id = R.string.uart_macro_icon),
modifier = Modifier
.size(buttonSize)
.clip(RoundedCornerShape(10.dp))
.clickable {
if (isEdited) {
onEvent(OnEditMacro(position))
} else {
onEvent(OnRunMacro(macro))
}
}
.background(getBackground(isEdited))
)
}
@Composable
private fun EmptyButton(
isEdited: Boolean,
position: Int,
onEvent: (UARTViewEvent) -> Unit
) {
Box(modifier = Modifier
.size(buttonSize)
.clip(RoundedCornerShape(10.dp))
.clickable {
if (isEdited) {
onEvent(OnEditMacro(position))
}
}
.background(getBackground(isEdited)))
}
@Composable
private fun getBackground(isEdited: Boolean): Color {
return if (!isEdited) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondary
}
}

View File

@@ -1,9 +1,11 @@
package no.nordicsemi.android.uart.view
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.uart.data.MacroIcon
@Composable
fun MacroEol.toDisplayString(): String {
@@ -13,3 +15,29 @@ fun MacroEol.toDisplayString(): String {
MacroEol.CR -> stringResource(id = R.string.uart_macro_dialog_cr)
}
}
@DrawableRes
fun MacroIcon.toResId(): Int {
return when (this) {
MacroIcon.LEFT -> R.drawable.ic_uart_left
MacroIcon.UP -> R.drawable.ic_uart_up
MacroIcon.RIGHT -> R.drawable.ic_uart_right
MacroIcon.DOWN -> R.drawable.ic_uart_down
MacroIcon.SETTINGS -> R.drawable.ic_uart_settings
MacroIcon.REW -> R.drawable.ic_uart_rewind
MacroIcon.PLAY -> R.drawable.ic_uart_play
MacroIcon.PAUSE -> R.drawable.ic_uart_pause
MacroIcon.STOP -> R.drawable.ic_uart_stop
MacroIcon.FWD -> R.drawable.ic_uart_forward
MacroIcon.INFO -> R.drawable.ic_uart_about
MacroIcon.NUMBER_1 -> R.drawable.ic_uart_1
MacroIcon.NUMBER_2 -> R.drawable.ic_uart_2
MacroIcon.NUMBER_3 -> R.drawable.ic_uart_3
MacroIcon.NUMBER_4 -> R.drawable.ic_uart_4
MacroIcon.NUMBER_5 -> R.drawable.ic_uart_5
MacroIcon.NUMBER_6 -> R.drawable.ic_uart_6
MacroIcon.NUMBER_7 -> R.drawable.ic_uart_7
MacroIcon.NUMBER_8 -> R.drawable.ic_uart_8
MacroIcon.NUMBER_9 -> R.drawable.ic_uart_9
}
}

View File

@@ -5,6 +5,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
@@ -23,6 +25,10 @@ fun UARTScreen() {
val viewModel: UARTViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
if (state.showEditDialog) {
UARTAddMacroDialog { viewModel.onEvent(it) }
}
Column {
val navigateUp = { viewModel.onEvent(NavigateUp) }
@@ -37,7 +43,7 @@ fun UARTScreen() {
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN, navigateUp)
is SuccessResult -> UARTContentView(state.uartManagerState.result.data, state.configuration) { viewModel.onEvent(it) }
is SuccessResult -> UARTContentView(state.uartManagerState.result.data, state) { viewModel.onEvent(it) }
}
}.exhaustive
}

View File

@@ -5,9 +5,16 @@ import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTData
internal data class UARTViewState(
val configuration: List<UARTConfiguration> = emptyList(),
val editedPosition: Int? = null,
val selectedConfigurationIndex: Int? = null,
val isConfigurationEdited: Boolean = false,
val configurations: List<UARTConfiguration> = emptyList(),
val uartManagerState: HTSManagerState = NoDeviceState
)
) {
val showEditDialog: Boolean = editedPosition != null
val selectedConfiguration: UARTConfiguration? = selectedConfigurationIndex?.let { configurations[it] }
}
internal sealed class HTSManagerState

View File

@@ -1,12 +1,19 @@
package no.nordicsemi.android.uart.view
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTMacro
internal sealed class UARTViewEvent
internal data class OnEditMacro(val position: Int) : UARTViewEvent()
internal data class OnCreateMacro(val macro: UARTMacro) : UARTViewEvent()
internal data class OnDeleteMacro(val macro: UARTMacro) : UARTViewEvent()
internal object OnEditFinish : UARTViewEvent()
internal data class OnConfigurationSelected(val configuration: UARTConfiguration) : UARTViewEvent()
internal data class OnAddConfiguration(val name: String) : UARTViewEvent()
internal object OnEditConfiguration : UARTViewEvent()
internal object OnDeleteConfiguration : UARTViewEvent()
internal data class OnRunMacro(val macro: UARTMacro) : UARTViewEvent()
internal object DisconnectEvent : UARTViewEvent()

View File

@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import no.nordicsemi.android.navigation.*
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTMacro
import no.nordicsemi.android.uart.data.UARTPersistentDataSource
import no.nordicsemi.android.uart.data.UART_SERVICE_UUID
@@ -39,7 +40,7 @@ internal class UARTViewModel @Inject constructor(
}.launchIn(viewModelScope)
dataSource.getConfigurations().onEach {
_state.value = _state.value.copy(configuration = it)
_state.value = _state.value.copy(configurations = it)
}.launchIn(viewModelScope)
}
@@ -67,18 +68,68 @@ internal class UARTViewModel @Inject constructor(
DisconnectEvent -> disconnect()
is OnRunMacro -> repository.runMacro(event.macro)
NavigateUp -> navigationManager.navigateUp()
is OnEditMacro -> onEditMacro(event)
OnEditFinish -> onEditFinish()
is OnConfigurationSelected -> onConfigurationSelected(event)
is OnAddConfiguration -> onAddConfiguration(event)
OnDeleteConfiguration -> deleteConfiguration()
OnEditConfiguration -> onEditConfiguration()
}.exhaustive
}
private fun onEditConfiguration() {
_state.value = _state.value.copy(isConfigurationEdited = true)
}
private fun onAddConfiguration(event: OnAddConfiguration) {
viewModelScope.launch(Dispatchers.IO) {
dataSource.saveConfiguration(UARTConfiguration(event.name))
}
}
private fun onEditMacro(event: OnEditMacro) {
_state.value = _state.value.copy(editedPosition = event.position)
}
private fun onEditFinish() {
_state.value = _state.value.copy(editedPosition = null)
}
private fun onConfigurationSelected(event: OnConfigurationSelected) {
_state.value = _state.value.copy(selectedConfigurationIndex = _state.value.configurations.indexOf(event.configuration))
}
private fun addNewMacro(macro: UARTMacro) {
viewModelScope.launch(Dispatchers.IO) {
dataSource.addNewMacro(macro)
_state.value.selectedConfiguration?.let {
val macros = it.macros.toMutableList().apply {
set(_state.value.editedPosition!!, macro)
}
val newConf = it.copy(macros = macros)
val newConfs = _state.value.configurations.map {
if (it.name == newConf.name) {
newConf
} else {
it
}
}
dataSource.saveConfiguration(newConf)
_state.value = _state.value.copy(editedPosition = null)
}
}
}
private fun deleteConfiguration() {
viewModelScope.launch(Dispatchers.IO) {
_state.value.selectedConfiguration?.let {
dataSource.deleteConfiguration(it)
}
}
}
private fun deleteMacro(macro: UARTMacro) {
viewModelScope.launch(Dispatchers.IO) {
dataSource.deleteMacro(macro)
// dataSource.deleteMacro(macro)
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M10,7V9H12V17H14V7H10Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V9H13V11H11A2,2 0,0 0,9 13V17H11L15,17V15H11V13H13A2,2 0,0 0,15 11V9A2,2 0,0 0,13 7H9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15,15V13.5A1.5,1.5 0,0 0,13.5 12A1.5,1.5 0,0 0,15 10.5V9C15,7.89 14.1,7 13,7H9V9H13V11H11V13H13V15H9V17H13A2,2 0,0 0,15 15"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V17H15V7H13V11H11V7H9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9,7V13H13V15H9V17H13A2,2 0,0 0,15 15V13A2,2 0,0 0,13 11H11V9H15V7H9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0,0 0,2 12A10,10 0,0 0,12 22A10,10 0,0 0,22 12A10,10 0,0 0,12 2M11,17H13V11H11V17Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M13,6V18L21.5,12M4,18L12.5,12L4,6V18Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M14,19H18V5H14M6,19H10V5H6V19Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8,5.14V19.14L19,12.14L8,5.14Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M11.5,12L20,18V6M11,18V6L2.5,12L11,18Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,15.5A3.5,3.5 0,0 1,8.5 12A3.5,3.5 0,0 1,12 8.5A3.5,3.5 0,0 1,15.5 12A3.5,3.5 0,0 1,12 15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18,18H6V6H18V18Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z"/>
</vector>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2015, Nordic Semiconductor
~ All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
~
~ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
~
~ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
~ documentation and/or other materials provided with the distribution.
~
~ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
~ software without specific prior written permission.
~
~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
~ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
~ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
~ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
~ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
~ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-->
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:maxLevel="0" android:drawable="@drawable/ic_uart_left" />
<item android:maxLevel="1" android:drawable="@drawable/ic_uart_up" />
<item android:maxLevel="2" android:drawable="@drawable/ic_uart_right" />
<item android:maxLevel="3" android:drawable="@drawable/ic_uart_down" />
<item android:maxLevel="4" android:drawable="@drawable/ic_uart_settings" />
<item android:maxLevel="5" android:drawable="@drawable/ic_uart_rewind" />
<item android:maxLevel="6" android:drawable="@drawable/ic_uart_play" />
<item android:maxLevel="7" android:drawable="@drawable/ic_uart_pause" />
<item android:maxLevel="8" android:drawable="@drawable/ic_uart_stop" />
<item android:maxLevel="9" android:drawable="@drawable/ic_uart_forward" />
<item android:maxLevel="10" android:drawable="@drawable/ic_uart_about" />
<item android:maxLevel="11" android:drawable="@drawable/ic_uart_1" />
<item android:maxLevel="12" android:drawable="@drawable/ic_uart_2" />
<item android:maxLevel="13" android:drawable="@drawable/ic_uart_3" />
<item android:maxLevel="14" android:drawable="@drawable/ic_uart_4" />
<item android:maxLevel="15" android:drawable="@drawable/ic_uart_5" />
<item android:maxLevel="16" android:drawable="@drawable/ic_uart_6" />
<item android:maxLevel="17" android:drawable="@drawable/ic_uart_7" />
<item android:maxLevel="18" android:drawable="@drawable/ic_uart_8" />
<item android:maxLevel="19" android:drawable="@drawable/ic_uart_9" />
</level-list>

View File

@@ -4,8 +4,15 @@
<string name="uart_no_macros_info">Please define a macro to send command to the device.</string>
<string name="uart_configuration_add">Add selected configuration.</string>
<string name="uart_configuration_delete">Delete selected configuration.</string>
<string name="uart_configuration_edit">Edit selected configuration.</string>
<string name="uart_input">Macros</string>
<string name="uart_output">Output</string>
<string name="uart_configuration_picker_hint">Select configuration</string>
<string name="uart_configuration_picker_not_selected">Not selected.</string>
<string name="uart_configuration_picker_dialog">Select configuration</string>
<string name="uart_command_field">Command: %s</string>
@@ -29,4 +36,9 @@
<string name="uart_output_placeholder">The incoming messages will be displayed here.</string>
<string name="uart_macro_error">Provided command cannot be empty.</string>
<string name="uart_name">Name</string>
<string name="uart_name_empty">Provided name cannot be empty.</string>
<string name="uart_macro_icon">Icon representing defined command.</string>
</resources>