From fac8d85a8e935e41063abcd0bd98787586d99929 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Wed, 6 Dec 2023 14:34:47 +0100 Subject: [PATCH 1/4] feature(TestApp): Remove Field.METADATA from "casual" queries Splitting up fetching the FIELD_TYPE and fetching the actual query from the user into two separate ones. Unfortunately this did not fix the crash when logging vehicle because the json contained to many lineBreaks ("\n"). Closes: #24 Signed-off-by: Andre Weber --- .../kuksa/testapp/KuksaDataBrokerActivity.kt | 27 ++++++++++++++----- .../viewmodel/VssPropertiesViewModel.kt | 8 ++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt index e79e9f95..6b7465fd 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -162,6 +162,7 @@ class KuksaDataBrokerActivity : ComponentActivity() { } vssPropertiesViewModel.onGetProperty = { property: Property -> + fetchPropertyFieldType(property) fetchProperty(property) } @@ -212,6 +213,26 @@ class KuksaDataBrokerActivity : ComponentActivity() { dataBrokerEngine.unregisterDisconnectListener(onDisconnectListener) } + private fun fetchPropertyFieldType(property: Property) { + dataBrokerEngine.fetch( + property.copy(fields = listOf(Field.FIELD_METADATA)), + object : CoroutineCallback() { + override fun onSuccess(result: GetResponse?) { + val automaticValueType = result?.metadata?.valueType ?: Datapoint.ValueCase.VALUE_NOT_SET + Log.d(TAG, "Fetched automatic value type from meta data: $automaticValueType") + + val vssProperties = vssPropertiesViewModel.vssProperties + .copy(valueType = automaticValueType) + vssPropertiesViewModel.updateVssProperties(vssProperties) + } + + override fun onError(error: Throwable) { + // intentionally ignored + } + }, + ) + } + private fun fetchProperty(property: Property) { Log.d(TAG, "Fetch property: $property") @@ -219,9 +240,6 @@ class KuksaDataBrokerActivity : ComponentActivity() { property, object : CoroutineCallback() { override fun onSuccess(result: GetResponse?) { - val automaticValueType = result?.metadata?.valueType ?: Datapoint.ValueCase.VALUE_NOT_SET - Log.d(TAG, "Fetched automatic value type from meta data: $automaticValueType") - val errorsList = result?.errorsList errorsList?.forEach { outputViewModel.appendOutput(it.toString()) @@ -229,9 +247,6 @@ class KuksaDataBrokerActivity : ComponentActivity() { return } - val vssProperties = vssPropertiesViewModel.vssProperties - .copy(valueType = automaticValueType) - vssPropertiesViewModel.updateVssProperties(vssProperties) outputViewModel.appendOutput(result.toString()) } diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/VssPropertiesViewModel.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/VssPropertiesViewModel.kt index cf153143..486d8000 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/VssPropertiesViewModel.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/VssPropertiesViewModel.kt @@ -48,14 +48,18 @@ class VSSPropertiesViewModel : ViewModel() { private set val valueTypes: List = ValueCase.values().toList() - val fieldTypes: List = listOf(Field.FIELD_VALUE, Field.FIELD_ACTUATOR_TARGET) + val fieldTypes: List = listOf( + Field.FIELD_VALUE, + Field.FIELD_ACTUATOR_TARGET, + Field.FIELD_METADATA, + ) val datapoint: Datapoint get() = vssProperties.valueType.createDatapoint(vssProperties.value) // Meta data are always part of the properties val property: Property - get() = Property(vssProperties.vssPath, listOf(vssProperties.fieldType, Field.FIELD_METADATA)) + get() = Property(vssProperties.vssPath, listOf(vssProperties.fieldType)) fun updateVssProperties(vssProperties: VSSProperties = VSSProperties()) { this.vssProperties = vssProperties From 480f17fba59d0660aa0e56f7a2a6baa08c1229ab Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Wed, 6 Dec 2023 14:35:50 +0100 Subject: [PATCH 2/4] chore: Improve Log Output Fixing the crash when logging "Vehicle". Instead of outputting one big log chunk outputting multiple smaller ones. Signed-off-by: Andre Weber --- .../kuksa/testapp/KuksaDataBrokerActivity.kt | 8 ++++- .../testapp/databroker/view/DataBrokerView.kt | 2 +- .../databroker/viewmodel/OutputViewModel.kt | 30 +++++-------------- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt index 6b7465fd..7acdc2d4 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -247,8 +247,14 @@ class KuksaDataBrokerActivity : ComponentActivity() { return } + result?.entriesList?.withIndex()?.forEach { + val dataEntry = it.value + val index = it.index - outputViewModel.appendOutput(result.toString()) + val text = dataEntry.toString().substringAfter("\n") + val withTimestamp = index == 0 + outputViewModel.appendOutput(text, withTimestamp) + } } override fun onError(error: Throwable) { diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt index 7542c4b8..288ed51c 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt @@ -392,7 +392,7 @@ fun DataBrokerOutput(viewModel: OutputViewModel, modifier: Modifier = Modifier) .fillMaxHeight() .fillMaxWidth() .padding(start = DefaultElementPadding, end = DefaultElementPadding), - text = outputElement, + text = "\n" + outputElement, fontSize = 14.sp, textAlign = TextAlign.Start, onTextLayout = { diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt index 8bb6c549..460a3fd9 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt @@ -39,35 +39,23 @@ class OutputViewModel : ViewModel() { var output: List by mutableStateOf(listOf()) private set - fun appendOutput(text: String) { + fun appendOutput(text: String, withTimestamp: Boolean = true) { viewModelScope.launch { withContext(Dispatchers.Main) { - val sanitizedText = sanitizeString(text) - - val emptyLines = if (logEntries.isEmpty()) "\n" else "\n\n" val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS") val date = LocalDateTime.now().format(dateFormatter) - logEntries += "$emptyLines- $date\n $sanitizedText" + var msg = "" + if (withTimestamp) { + msg += "$date\n" + } + msg += text + logEntries += msg output = logEntries.toList() } } } - // fixes a crash when outputting VssPath(Vehicle). The ScrollBar can't handle input with more than 3971 line breaks - private fun sanitizeString(text: String): String { - var sanitizedText = text - val isTextTooLong = sanitizedText.length >= MAX_LENGTH_LOG_ENTRY - if (isTextTooLong) { - sanitizedText = sanitizedText.substring(0, MAX_LENGTH_LOG_ENTRY) + "…" - sanitizedText += System.lineSeparator() - sanitizedText += System.lineSeparator() - sanitizedText += "Text is too long and was truncated" - } - - return sanitizedText - } - fun clear() { viewModelScope.launch { withContext(Dispatchers.Main) { @@ -77,8 +65,4 @@ class OutputViewModel : ViewModel() { } } } - - private companion object { - private const val MAX_LENGTH_LOG_ENTRY = 90_000 - } } From 4594406515f68d7b31d5fe9017aca810381ccce3 Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Mon, 15 Jan 2024 14:12:35 +0100 Subject: [PATCH 3/4] chore: Change metadata Extension --- .../kuksa/extension/KuksaValResponseExtension.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/KuksaValResponseExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/KuksaValResponseExtension.kt index 486acecf..9c674649 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/KuksaValResponseExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/KuksaValResponseExtension.kt @@ -25,16 +25,11 @@ import org.eclipse.kuksa.proto.v1.Types /** * Convenience property which returns any [Types.Metadata] if available. */ -val KuksaValV1.GetResponse.metadata: Types.Metadata? +val KuksaValV1.GetResponse.entriesMetadata: List get() { - if (entriesList.isEmpty()) return null - - val entry = entriesList.first() - if (!entry.hasMetadata()) { - return null - } + if (entriesList.isEmpty()) return emptyList() - return entry.metadata + return entriesList.map { it.metadata } } /** From a1e4ef29f4aac7e76b8aa381fc257f058899fd8d Mon Sep 17 00:00:00 2001 From: Andre Weber Date: Mon, 15 Jan 2024 15:00:57 +0100 Subject: [PATCH 4/4] chore: Refactor Output Log Handling --- .../kuksa/testapp/KuksaDataBrokerActivity.kt | 58 +++++++++++-------- .../testapp/databroker/view/DataBrokerView.kt | 55 ++++++++++++------ .../databroker/viewmodel/OutputViewModel.kt | 42 +++++++++----- 3 files changed, 99 insertions(+), 56 deletions(-) diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt index 7acdc2d4..e660dac2 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/KuksaDataBrokerActivity.kt @@ -34,11 +34,12 @@ import org.eclipse.kuksa.DataBrokerConnection import org.eclipse.kuksa.DisconnectListener import org.eclipse.kuksa.PropertyListener import org.eclipse.kuksa.VssSpecificationListener -import org.eclipse.kuksa.extension.metadata +import org.eclipse.kuksa.extension.entriesMetadata import org.eclipse.kuksa.extension.valueType import org.eclipse.kuksa.model.Property import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.KuksaValV1.GetResponse +import org.eclipse.kuksa.proto.v1.Types import org.eclipse.kuksa.proto.v1.Types.DataEntry import org.eclipse.kuksa.proto.v1.Types.Datapoint import org.eclipse.kuksa.proto.v1.Types.Field @@ -49,6 +50,7 @@ import org.eclipse.kuksa.testapp.databroker.model.ConnectionInfo import org.eclipse.kuksa.testapp.databroker.view.DataBrokerView import org.eclipse.kuksa.testapp.databroker.viewmodel.ConnectionViewModel import org.eclipse.kuksa.testapp.databroker.viewmodel.ConnectionViewModel.ConnectionViewState +import org.eclipse.kuksa.testapp.databroker.viewmodel.OutputEntry import org.eclipse.kuksa.testapp.databroker.viewmodel.OutputViewModel import org.eclipse.kuksa.testapp.databroker.viewmodel.TopAppBarViewModel import org.eclipse.kuksa.testapp.databroker.viewmodel.VSSPropertiesViewModel @@ -73,12 +75,12 @@ class KuksaDataBrokerActivity : ComponentActivity() { private val dataBrokerConnectionCallback = object : CoroutineCallback() { override fun onSuccess(result: DataBrokerConnection?) { - outputViewModel.appendOutput("Connection to DataBroker successful established") + outputViewModel.addOutputEntry("Connection to DataBroker successful established") connectionViewModel.updateConnectionState(ConnectionViewState.CONNECTED) } override fun onError(error: Throwable) { - outputViewModel.appendOutput("Connection to DataBroker failed: ${error.message}") + outputViewModel.addOutputEntry("Connection to DataBroker failed: ${error.message}") connectionViewModel.updateConnectionState(ConnectionViewState.DISCONNECTED) } } @@ -86,27 +88,27 @@ class KuksaDataBrokerActivity : ComponentActivity() { private val onDisconnectListener = DisconnectListener { connectionViewModel.updateConnectionState(ConnectionViewState.DISCONNECTED) outputViewModel.clear() - outputViewModel.appendOutput("DataBroker disconnected") + outputViewModel.addOutputEntry("DataBroker disconnected") } private val propertyListener = object : PropertyListener { override fun onPropertyChanged(vssPath: String, field: Field, updatedValue: DataEntry) { Log.d(TAG, "onPropertyChanged path: vssPath = $vssPath, field = $field, changedValue = $updatedValue") - outputViewModel.appendOutput("Updated value: $updatedValue") + outputViewModel.addOutputEntry("Updated value: $updatedValue") } override fun onError(throwable: Throwable) { - outputViewModel.appendOutput("${throwable.message}") + outputViewModel.addOutputEntry("${throwable.message}") } } private val specificationListener = object : VssSpecificationListener { override fun onSpecificationChanged(vssSpecification: VssSpecification) { - outputViewModel.appendOutput("Updated specification: $vssSpecification") + outputViewModel.addOutputEntry("Updated specification: $vssSpecification") } override fun onError(throwable: Throwable) { - outputViewModel.appendOutput("Updated specification: ${throwable.message}") + outputViewModel.addOutputEntry("Updated specification: ${throwable.message}") } } @@ -150,7 +152,7 @@ class KuksaDataBrokerActivity : ComponentActivity() { kotlinDataBrokerEngine } val enabledState = if (isCompatibilityModeEnabled) "enabled" else "disabled" - outputViewModel.appendOutput("Java Compatibility Mode $enabledState") + outputViewModel.addOutputEntry("Java Compatibility Mode $enabledState") } connectionViewModel.onConnect = { connectionInfo -> @@ -200,7 +202,7 @@ class KuksaDataBrokerActivity : ComponentActivity() { private fun connect(connectionInfo: ConnectionInfo) { Log.d(TAG, "Connecting to DataBroker: $connectionInfo") - outputViewModel.appendOutput("Connecting to data broker - Please wait") + outputViewModel.addOutputEntry("Connecting to data broker - Please wait") connectionViewModel.updateConnectionState(ConnectionViewState.CONNECTING) dataBrokerEngine.registerDisconnectListener(onDisconnectListener) @@ -214,11 +216,19 @@ class KuksaDataBrokerActivity : ComponentActivity() { } private fun fetchPropertyFieldType(property: Property) { + val propertyWithMetaData = property.copy(fields = listOf(Field.FIELD_METADATA)) + dataBrokerEngine.fetch( - property.copy(fields = listOf(Field.FIELD_METADATA)), + propertyWithMetaData, object : CoroutineCallback() { override fun onSuccess(result: GetResponse?) { - val automaticValueType = result?.metadata?.valueType ?: Datapoint.ValueCase.VALUE_NOT_SET + val entriesMetadata = result?.entriesMetadata ?: emptyList() + val automaticValueType = if (entriesMetadata.size == 1) { + entriesMetadata.first().valueType + } else { + Types.Datapoint.ValueCase.VALUE_NOT_SET + } + Log.d(TAG, "Fetched automatic value type from meta data: $automaticValueType") val vssProperties = vssPropertiesViewModel.vssProperties @@ -227,7 +237,7 @@ class KuksaDataBrokerActivity : ComponentActivity() { } override fun onError(error: Throwable) { - // intentionally ignored + Log.w(TAG, "Could not resolve type of value for $property") } }, ) @@ -242,23 +252,23 @@ class KuksaDataBrokerActivity : ComponentActivity() { override fun onSuccess(result: GetResponse?) { val errorsList = result?.errorsList errorsList?.forEach { - outputViewModel.appendOutput(it.toString()) + outputViewModel.addOutputEntry(it.toString()) return } + val outputEntry = OutputEntry() result?.entriesList?.withIndex()?.forEach { val dataEntry = it.value - val index = it.index - val text = dataEntry.toString().substringAfter("\n") - val withTimestamp = index == 0 - outputViewModel.appendOutput(text, withTimestamp) + + outputEntry.addMessage(text) } + outputViewModel.addOutputEntry(outputEntry) } override fun onError(error: Throwable) { - outputViewModel.appendOutput("Connection to data broker failed: ${error.message}") + outputViewModel.addOutputEntry("Connection to data broker failed: ${error.message}") } }, ) @@ -274,15 +284,15 @@ class KuksaDataBrokerActivity : ComponentActivity() { override fun onSuccess(result: KuksaValV1.SetResponse?) { val errorsList = result?.errorsList errorsList?.forEach { - outputViewModel.appendOutput(it.toString()) + outputViewModel.addOutputEntry(it.toString()) return } - outputViewModel.appendOutput(result.toString()) + outputViewModel.addOutputEntry(result.toString()) } override fun onError(error: Throwable) { - outputViewModel.appendOutput("Connection to data broker failed: ${error.message}") + outputViewModel.addOutputEntry("Connection to data broker failed: ${error.message}") } }, ) @@ -294,11 +304,11 @@ class KuksaDataBrokerActivity : ComponentActivity() { object : CoroutineCallback() { override fun onSuccess(result: VssSpecification?) { Log.d(TAG, "Fetched specification: $result") - outputViewModel.appendOutput("Fetched specification: $result") + outputViewModel.addOutputEntry("Fetched specification: $result") } override fun onError(error: Throwable) { - outputViewModel.appendOutput("Could not fetch specification: ${error.message}") + outputViewModel.addOutputEntry("Could not fetch specification: ${error.message}") } }, ) diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt index 288ed51c..823c53cd 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/view/DataBrokerView.kt @@ -67,6 +67,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign @@ -76,7 +77,6 @@ import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import org.eclipse.kuksa.proto.v1.Types.Datapoint.ValueCase import org.eclipse.kuksa.testapp.databroker.viewmodel.ConnectionViewModel -import org.eclipse.kuksa.testapp.databroker.viewmodel.ConnectionViewModel.* import org.eclipse.kuksa.testapp.databroker.viewmodel.OutputViewModel import org.eclipse.kuksa.testapp.databroker.viewmodel.TopAppBarViewModel import org.eclipse.kuksa.testapp.databroker.viewmodel.TopAppBarViewModel.DataBrokerMode @@ -88,6 +88,7 @@ import org.eclipse.kuksa.testapp.extension.compose.OverflowMenu import org.eclipse.kuksa.testapp.extension.compose.SimpleExposedDropdownMenuBox import org.eclipse.kuksa.testapp.preferences.ConnectionInfoRepository import org.eclipse.kuksa.testapp.ui.theme.KuksaAppAndroidTheme +import java.time.format.DateTimeFormatter val DefaultEdgePadding = 25.dp val DefaultElementPadding = 10.dp @@ -377,7 +378,7 @@ fun DataBrokerOutput(viewModel: OutputViewModel, modifier: Modifier = Modifier) val shape = RoundedCornerShape(20.dp, 20.dp, 0.dp, 0.dp) val scrollState = rememberScrollState(0) - val output = viewModel.output + val outputEntries = viewModel.output Surface( modifier = modifier.height(500.dp), @@ -385,27 +386,47 @@ fun DataBrokerOutput(viewModel: OutputViewModel, modifier: Modifier = Modifier) shape = shape, ) { Column(modifier = Modifier.verticalScroll(scrollState)) { + val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS") Headline(name = "Output", color = Color.White) - output.forEach { outputElement -> - Text( - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth() - .padding(start = DefaultElementPadding, end = DefaultElementPadding), - text = "\n" + outputElement, - fontSize = 14.sp, - textAlign = TextAlign.Start, - onTextLayout = { - scope.launch { - scrollState.animateScrollTo(scrollState.maxValue) - } - }, - ) + outputEntries.forEach { outputEntry -> + val date = outputEntry.localDateTime.format(dateFormatter) + val newLine = System.lineSeparator() + + val onTextLayout: ((TextLayoutResult) -> Unit) = { + scope.launch { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + + val logTextModifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .padding(start = DefaultElementPadding, end = DefaultElementPadding) + + OutputText(date, logTextModifier, onTextLayout) + outputEntry.messages.forEach { + OutputText(it + newLine, logTextModifier, onTextLayout) + } } } } } +@Composable +private fun OutputText( + text: String, + modifier: Modifier = Modifier, + onTextLayout: (TextLayoutResult) -> Unit = {}, +) { + Text( + modifier = modifier, + text = text, + fontSize = 14.sp, + textAlign = TextAlign.Start, + onTextLayout = onTextLayout, + ) +} + @Preview(showBackground = true) @Composable fun Preview() { diff --git a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt index 460a3fd9..36bb1a02 100644 --- a/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt +++ b/app/src/main/kotlin/org/eclipse/kuksa/testapp/databroker/viewmodel/OutputViewModel.kt @@ -29,29 +29,28 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.eclipse.kuksa.testapp.collection.MaxElementSet import java.time.LocalDateTime -import java.time.format.DateTimeFormatter private const val MAX_NUMBER_LOG_ENTRIES = 100 class OutputViewModel : ViewModel() { - private val logEntries = MaxElementSet(MAX_NUMBER_LOG_ENTRIES) + private val outputEntries = MaxElementSet(MAX_NUMBER_LOG_ENTRIES) - var output: List by mutableStateOf(listOf()) + var output: List by mutableStateOf(listOf()) private set - fun appendOutput(text: String, withTimestamp: Boolean = true) { + fun addOutputEntry(message: String) { + val messages = listOf(message) + val outputEntry = OutputEntry(messages = messages) + + addOutputEntry(outputEntry) + } + + fun addOutputEntry(outputEntry: OutputEntry) { viewModelScope.launch { withContext(Dispatchers.Main) { - val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS") - val date = LocalDateTime.now().format(dateFormatter) - var msg = "" - if (withTimestamp) { - msg += "$date\n" - } - msg += text - logEntries += msg + outputEntries.add(outputEntry) - output = logEntries.toList() + output = outputEntries.toList() } } } @@ -59,10 +58,23 @@ class OutputViewModel : ViewModel() { fun clear() { viewModelScope.launch { withContext(Dispatchers.Main) { - logEntries.clear() + outputEntries.clear() - output = logEntries.toList() + output = outputEntries.toList() } } } } + +class OutputEntry( + val localDateTime: LocalDateTime = LocalDateTime.now(), + messages: List = mutableListOf(), +) { + private var _messages: MutableList = messages.toMutableList() + val messages: List + get() = _messages + + fun addMessage(message: String) { + _messages.add(message) + } +}