Add database migrtion to UART to support configuration from previous version

This commit is contained in:
Sylwester Zieliński
2022-02-21 13:33:21 +01:00
parent 1a71488d3c
commit da3235ca22
21 changed files with 358 additions and 85 deletions

View File

@@ -4,7 +4,7 @@ apply plugin: 'com.google.protobuf'
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.14.0'
artifact = 'com.google.protobuf:protoc:3.14.0:osx-x86_64'
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
@@ -27,6 +27,10 @@ dependencies {
implementation project(":lib_theme")
implementation project(":lib_utils")
implementation libs.room.runtime
implementation libs.room.ktx
kapt libs.room.compiler
implementation libs.nordic.ble.common
implementation libs.nordic.ble.ktx
@@ -45,4 +49,9 @@ dependencies {
implementation libs.compose.activity
testImplementation libs.bundles.test
implementation('org.simpleframework:simple-xml:2.7.1') {
exclude group: 'stax', module: 'stax-api'
exclude group: 'xpp3', module: 'xpp3'
}
}

View File

@@ -0,0 +1,34 @@
package no.nordicsemi.android.uart
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import no.nordicsemi.android.uart.db.ConfigurationsDao
import no.nordicsemi.android.uart.db.ConfigurationsDatabase
import no.nordicsemi.android.uart.db.MIGRATION_1_2
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class HiltModule {
@Provides
@Singleton
internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase {
return Room.databaseBuilder(
context,
ConfigurationsDatabase::class.java, "toolbox_uart.db"
).addMigrations(MIGRATION_1_2).build()
}
@Provides
@Singleton
internal fun provideDao(db: ConfigurationsDatabase): ConfigurationsDao {
return db.dao()
}
}

View File

@@ -0,0 +1,7 @@
package no.nordicsemi.android.uart.data
enum class MacroEol(val eolIndex: Int) {
LF(0),
CR(1),
CR_LF(2);
}

View File

@@ -0,0 +1,31 @@
package no.nordicsemi.android.uart.data
enum class MacroIcon(val index: Int) {
LEFT(0),
UP(1),
RIGHT(2),
DOWN(3),
SETTINGS(4),
REW(5),
PLAY(6),
PAUSE(7),
STOP(8),
FWD(9),
INFO(10),
NUMBER_1(11),
NUMBER_2(12),
NUMBER_3(13),
NUMBER_4(14),
NUMBER_5(15),
NUMBER_6(16),
NUMBER_7(17),
NUMBER_8(18),
NUMBER_9(19);
companion object {
fun create(index: Int): MacroIcon {
return values().firstOrNull { it.index == index }
?: throw IllegalArgumentException("Cannot create MacroIcon for index: $index")
}
}
}

View File

@@ -0,0 +1,6 @@
package no.nordicsemi.android.uart.data
data class UARTConfiguration(
val name: String,
val macros: List<UARTMacro>
)

View File

@@ -1,7 +1,3 @@
package no.nordicsemi.android.uart.data
data class UARTMacro(val command: String, val newLineChar: NewLineChar)
enum class NewLineChar {
LF, CR_LF, CR
}
data class UARTMacro(val icon: MacroIcon, val command: String, val newLineChar: MacroEol)

View File

@@ -1,9 +1,9 @@
package no.nordicsemi.android.uart.data
fun String.parseWithNewLineChar(newLineChar: NewLineChar): String {
fun String.parseWithNewLineChar(newLineChar: MacroEol): String {
return when (newLineChar) {
NewLineChar.LF -> this
NewLineChar.CR_LF -> this.replace("\n", "\r\n")
NewLineChar.CR -> this.replace("\n", "\r")
MacroEol.LF -> this
MacroEol.CR_LF -> this.replace("\n", "\r\n")
MacroEol.CR -> this.replace("\n", "\r")
}
}

View File

@@ -1,78 +1,51 @@
package no.nordicsemi.android.uart.data
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import no.nordicsemi.android.Macro
import no.nordicsemi.android.MacroSettings
import no.nordicsemi.android.uart.db.*
import org.simpleframework.xml.Serializer
import org.simpleframework.xml.core.Persister
import org.simpleframework.xml.strategy.Strategy
import org.simpleframework.xml.strategy.VisitorStrategy
import org.simpleframework.xml.stream.Format
import org.simpleframework.xml.stream.HyphenStyle
import java.io.StringWriter
import javax.inject.Inject
import javax.inject.Singleton
private const val MACRO_FILE = "macro.proto"
@Singleton
internal class UARTPersistentDataSource @Inject constructor(
@ApplicationContext
private val context: Context
private val configurationsDao: ConfigurationsDao,
) {
private val Context.dataStore: DataStore<MacroSettings> by dataStore(fileName = MACRO_FILE, MacroSerializer)
val macros = context.dataStore.data.map {
it.macrosList.map {
UARTMacro(it.name, it.newLineType.toNewLineChar())
fun getConfigurations(): Flow<List<UARTConfiguration>> = configurationsDao.load().map {
it.map {
val xml: String = it.xml
val format = Format(HyphenStyle())
val serializer: Serializer = Persister(format)
val configuration = serializer.read(XmlConfiguration::class.java, xml)
UARTConfiguration(configuration.name ?: "Unknown", createMacro(configuration.commands))
}
}
suspend fun saveMacros(uartMacros: List<UARTMacro>) {
context.dataStore.updateData { settings ->
val macros = uartMacros.map { it.toMacro() }
settings.toBuilder()
.clearMacros()
.addAllMacros(macros)
.build()
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) }
}
}
suspend fun addNewMacro(uartMacro: UARTMacro) {
context.dataStore.updateData { settings ->
settings.toBuilder()
.addMacros(uartMacro.toMacro())
.build()
}
}
suspend fun saveConfiguration(configuration: UARTConfiguration) {
val format = Format(HyphenStyle())
val strategy: Strategy = VisitorStrategy(CommentVisitor())
val serializer: Serializer = Persister(strategy, format)
val writer = StringWriter()
serializer.write(configuration, writer)
val xml = writer.toString()
suspend fun deleteMacro(uartMacro: UARTMacro) {
context.dataStore.updateData { settings ->
val i = settings.macrosList.map { it.name }.indexOf(uartMacro.command)
settings.toBuilder()
.removeMacros(i)
.build()
}
}
private fun UARTMacro.toMacro(): Macro {
return Macro.newBuilder()
.setName(command)
.setNewLineType(newLineChar.toMacroNewLineType())
.build()
}
private fun NewLineChar.toMacroNewLineType(): Macro.NewLineType {
return when (this) {
NewLineChar.LF -> Macro.NewLineType.LF
NewLineChar.CR_LF -> Macro.NewLineType.LF_CR
NewLineChar.CR -> Macro.NewLineType.CR
}
}
private fun Macro.NewLineType.toNewLineChar(): NewLineChar {
return when (this) {
Macro.NewLineType.LF -> NewLineChar.LF
Macro.NewLineType.LF_CR -> NewLineChar.CR_LF
Macro.NewLineType.CR -> NewLineChar.CR
Macro.NewLineType.UNRECOGNIZED -> throw IllegalArgumentException("Unrecognized NewLineChar.")
}
configurationsDao.insert(Configuration(0, configuration.name, xml, 0))
}
}

View File

@@ -0,0 +1,28 @@
package no.nordicsemi.android.uart.db
import no.nordicsemi.android.uart.data.MacroIcon
import org.simpleframework.xml.strategy.Type
import org.simpleframework.xml.strategy.Visitor
import org.simpleframework.xml.stream.InputNode
import org.simpleframework.xml.stream.NodeMap
import org.simpleframework.xml.stream.OutputNode
/**
* The comment visitor will add comments to the XML during saving.
*/
internal class CommentVisitor : Visitor {
override fun read(type: Type, node: NodeMap<InputNode>) {
// do nothing
}
override fun write(type: Type, node: NodeMap<OutputNode>) {
if (type.type == Array<XmlCommand>::class.java) {
val element = node.node
val builder =
StringBuilder("A configuration must have 9 commands, one for each button.\n Possible icons are:")
for (icon in MacroIcon.values()) builder.append("\n - ")
.append(icon.toString())
element.comment = builder.toString()
}
}
}

View File

@@ -0,0 +1,14 @@
package no.nordicsemi.android.uart.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "configurations")
internal data class Configuration(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Int,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "xml") val xml: String,
@ColumnInfo(name = "deleted") val deleted: Int
)

View File

@@ -0,0 +1,17 @@
package no.nordicsemi.android.uart.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
internal interface ConfigurationsDao {
@Query("SELECT * FROM configurations")
fun load(): Flow<List<Configuration>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(configuration: Configuration)
}

View File

@@ -0,0 +1,9 @@
package no.nordicsemi.android.uart.db
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [Configuration::class], version = 2)
internal abstract class ConfigurationsDatabase : RoomDatabase() {
abstract fun dao(): ConfigurationsDao
}

View File

@@ -0,0 +1,10 @@
package no.nordicsemi.android.uart.db
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// Empty implementation, because the schema isn't changing.
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.
*/
package no.nordicsemi.android.uart.db
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.uart.data.MacroIcon
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.Root
import org.simpleframework.xml.Text
@Root
internal class XmlCommand {
/**
* Returns the command that will be sent to UART device.
* @return the command
*/
/**
* Sets the command.
* @param command the command that will be sent to UART device
*/
@Text(required = false)
var command: String? = null
/**
* Returns whether the icon is active.
* @return true if it's active
*/
/**
* Sets whether the command is active.
* @param active true to make it active
*/
@Attribute(required = false)
var isActive = false
/**
* Returns the new line type.
* @return end of line terminator
*/
@Attribute(required = false)
var eol = MacroEol.LF
private set
@Attribute(required = false)
private var icon = MacroIcon.LEFT
/**
* Sets the new line type.
* @param eol end of line terminator
*/
fun setEol(eol: Int) {
this.eol = MacroEol.values()[eol]
}
/**
* Returns the icon index.
* @return the icon index
*/
/**
* Sets the icon index.
* @param index index of the icon.
*/
var iconIndex: Int
get() = icon.index
set(index) {
icon = MacroIcon.values()[index]
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.
*/
package no.nordicsemi.android.uart.db
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.ElementArray
import org.simpleframework.xml.Root
import org.simpleframework.xml.core.PersistenceException
import org.simpleframework.xml.core.Validate
@Root
internal class XmlConfiguration {
@Attribute(required = false, empty = "Unnamed")
var name: String? = null
@ElementArray
val commands: Array<XmlCommand?> = arrayOfNulls(COMMANDS_COUNT)
@Validate
@Throws(PersistenceException::class)
private fun validate() {
if (commands.size != COMMANDS_COUNT) throw PersistenceException("There must be always $COMMANDS_COUNT commands in a configuration.")
}
companion object {
const val COMMANDS_COUNT = 9
}
}

View File

@@ -16,19 +16,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.HorizontalLabelRadioButtonGroup
import no.nordicsemi.android.material.you.RadioButtonGroup
import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity
import no.nordicsemi.android.material.you.TextField
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.NewLineChar
import no.nordicsemi.android.uart.data.MacroEol
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(NewLineChar.LF) }
val newLineChar = remember { mutableStateOf(MacroEol.LF) }
val isError = remember { mutableStateOf(false) }
Dialog(onDismissRequest = { onDismiss() }) {
@@ -84,7 +85,7 @@ internal fun UARTAddMacroDialog(onDismiss: () -> Unit, onEvent: (UARTViewEvent)
TextButton(onClick = {
if (isCommandValid(command.value)) {
onDismiss()
onEvent(OnCreateMacro(UARTMacro(command.value, newLineChar.value)))
onEvent(OnCreateMacro(UARTMacro(MacroIcon.DOWN, command.value, newLineChar.value)))
} else {
isError.value = true
}
@@ -100,8 +101,8 @@ internal fun UARTAddMacroDialog(onDismiss: () -> Unit, onEvent: (UARTViewEvent)
}
@Composable
private fun NewLineCharSection(checkedItem: NewLineChar, onItemClick: (NewLineChar) -> Unit) {
val items = NewLineChar.values().map {
private fun NewLineCharSection(checkedItem: MacroEol, onItemClick: (MacroEol) -> Unit) {
val items = MacroEol.values().map {
RadioButtonItem(it.toDisplayString(), it == checkedItem)
}
val viewEntity = RadioGroupViewEntity(items)
@@ -112,9 +113,9 @@ private fun NewLineCharSection(checkedItem: NewLineChar, onItemClick: (NewLineCh
style = MaterialTheme.typography.labelLarge
)
HorizontalLabelRadioButtonGroup(viewEntity) {
RadioButtonGroup(viewEntity) {
val i = items.indexOf(it)
onItemClick(NewLineChar.values()[i])
onItemClick(MacroEol.values()[i])
}
}
}

View File

@@ -3,13 +3,13 @@ package no.nordicsemi.android.uart.view
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.NewLineChar
import no.nordicsemi.android.uart.data.MacroEol
@Composable
fun NewLineChar.toDisplayString(): String {
fun MacroEol.toDisplayString(): String {
return when (this) {
NewLineChar.LF -> stringResource(id = R.string.uart_macro_dialog_lf)
NewLineChar.CR_LF -> stringResource(id = R.string.uart_macro_dialog_cr_lf)
NewLineChar.CR -> stringResource(id = R.string.uart_macro_dialog_cr)
MacroEol.LF -> stringResource(id = R.string.uart_macro_dialog_lf)
MacroEol.CR_LF -> stringResource(id = R.string.uart_macro_dialog_cr_lf)
MacroEol.CR -> stringResource(id = R.string.uart_macro_dialog_cr)
}
}

View File

@@ -37,7 +37,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.macros) { viewModel.onEvent(it) }
is SuccessResult -> UARTContentView(state.uartManagerState.result.data, state.configuration) { viewModel.onEvent(it) }
}
}.exhaustive
}

View File

@@ -1,11 +1,11 @@
package no.nordicsemi.android.uart.view
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTMacro
internal data class UARTViewState(
val macros: List<UARTMacro> = emptyList(),
val configuration: List<UARTConfiguration> = emptyList(),
val uartManagerState: HTSManagerState = NoDeviceState
)

View File

@@ -38,8 +38,8 @@ internal class UARTViewModel @Inject constructor(
_state.value = _state.value.copy(uartManagerState = WorkingState(it))
}.launchIn(viewModelScope)
dataSource.macros.onEach {
_state.value = _state.value.copy(macros = it)
dataSource.getConfigurations().onEach {
_state.value = _state.value.copy(configuration = it)
}.launchIn(viewModelScope)
}

View File

@@ -66,6 +66,11 @@ dependencyResolutionManagement {
alias('google-permissions').to('com.google.accompanist:accompanist-permissions:0.20.0')
alias('chart').to('com.github.PhilJay:MPAndroidChart:v3.1.0')
version('room', '2.4.1')
alias('room-runtime').to('androidx.room', 'room-runtime').versionRef('room')
alias('room-ktx').to('androidx.room', 'room-ktx').versionRef('room')
alias('room-compiler').to('androidx.room', 'room-compiler').versionRef('room')
//-- Test ------------------------------------------------------------------------------
alias('test-junit').to('junit:junit:4.13.2')
alias('android-test-junit').to('androidx.test.ext:junit:1.1.3')