diff --git a/app/build.gradle b/app/build.gradle index 356d9597a..4d695b146 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -198,6 +198,9 @@ dependencies { implementation 'androidx.activity:activity-compose' implementation 'androidx.compose.runtime:runtime-livedata' + // Immutable Collections (for Compose UI State) + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8") + // Android Studio Preview support implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' diff --git a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt index 75e4a0f24..801a1df6e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt @@ -17,25 +17,34 @@ package com.geeksville.mesh.model +import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.geeksville.mesh.android.Logging import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.entity.MeshLog import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.text.DateFormat +import java.util.Locale import javax.inject.Inject @HiltViewModel class DebugViewModel @Inject constructor( private val meshLogRepository: MeshLogRepository, ) : ViewModel(), Logging { - val meshLog: StateFlow> = meshLogRepository.getAllLogs() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + + val meshLog: StateFlow> = meshLogRepository.getAllLogs() + .map(::toUiState) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), persistentListOf()) init { debug("DebugViewModel created") @@ -46,7 +55,83 @@ class DebugViewModel @Inject constructor( debug("DebugViewModel cleared") } + private fun toUiState(databaseLogs: List) = databaseLogs.map { log -> + UiMeshLog( + uuid = log.uuid, + messageType = log.message_type, + formattedReceivedDate = TIME_FORMAT.format(log.received_date), + logMessage = annotateMeshLogMessage(log), + ) + }.toImmutableList() + + /** + * Transform the input [MeshLog] by enhancing the raw message with annotations. + */ + private fun annotateMeshLogMessage(meshLog: MeshLog): String { + val annotated = when (meshLog.message_type) { + "Packet" -> meshLog.meshPacket?.let { packet -> + annotateRawMessage(meshLog.raw_message, packet.from, packet.to) + } + + "NodeInfo" -> meshLog.nodeInfo?.let { nodeInfo -> + annotateRawMessage(meshLog.raw_message, nodeInfo.num) + } + + "MyNodeInfo" -> meshLog.myNodeInfo?.let { nodeInfo -> + annotateRawMessage(meshLog.raw_message, nodeInfo.myNodeNum) + } + + else -> null + } + return annotated ?: meshLog.raw_message + } + + /** + * Annotate the raw message string with the node IDs provided, in hex, if they are present. + */ + private fun annotateRawMessage(rawMessage: String, vararg nodeIds: Int): String { + val msg = StringBuilder(rawMessage) + var mutated = false + nodeIds.forEach { nodeId -> + mutated = mutated or msg.annotateNodeId(nodeId) + } + return if (mutated) { + return msg.toString() + } else { + rawMessage + } + } + + /** + * Look for a single node ID integer in the string and annotate it with the hex equivalent + * if found. + */ + private fun StringBuilder.annotateNodeId(nodeId: Int): Boolean { + val nodeIdStr = nodeId.toUInt().toString() + indexOf(nodeIdStr).takeIf { it >= 0 }?.let { idx -> + insert(idx + nodeIdStr.length, " (${nodeId.asNodeId()})") + return true + } + return false + } + + private fun Int.asNodeId(): String { + return "!%08x".format(Locale.getDefault(), this) + } + fun deleteAllLogs() = viewModelScope.launch(Dispatchers.IO) { meshLogRepository.deleteAll() } + + @Immutable + data class UiMeshLog( + val uuid: String, + val messageType: String, + val formattedReceivedDate: String, + val logMessage: String, + ) + + companion object { + private val TIME_FORMAT = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) + } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt index 6cdba9ad1..3d1666007 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt @@ -44,12 +44,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString @@ -63,13 +65,11 @@ import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.R -import com.geeksville.mesh.database.entity.MeshLog import com.geeksville.mesh.model.DebugViewModel +import com.geeksville.mesh.model.DebugViewModel.UiMeshLog import com.geeksville.mesh.ui.components.BaseScaffold import com.geeksville.mesh.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint -import java.text.DateFormat -import java.util.Locale @AndroidEntryPoint class DebugFragment : Fragment() { @@ -91,65 +91,6 @@ class DebugFragment : Fragment() { private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE) -/** - * Transform the input [MeshLog] by enhancing the raw message with annotations. - */ -private fun annotateMeshLog(meshLog: MeshLog): MeshLog { - val annotated = when (meshLog.message_type) { - "Packet" -> meshLog.meshPacket?.let { packet -> - annotateRawMessage(meshLog.raw_message, packet.from, packet.to) - } - - "NodeInfo" -> meshLog.nodeInfo?.let { nodeInfo -> - annotateRawMessage(meshLog.raw_message, nodeInfo.num) - } - - "MyNodeInfo" -> meshLog.myNodeInfo?.let { nodeInfo -> - annotateRawMessage(meshLog.raw_message, nodeInfo.myNodeNum) - } - - else -> null - } - return if (annotated == null) { - meshLog - } else { - meshLog.copy(raw_message = annotated) - } -} - -/** - * Annotate the raw message string with the node IDs provided, in hex, if they are present. - */ -private fun annotateRawMessage(rawMessage: String, vararg nodeIds: Int): String { - val msg = StringBuilder(rawMessage) - var mutated = false - nodeIds.forEach { nodeId -> - mutated = mutated or msg.annotateNodeId(nodeId) - } - return if (mutated) { - return msg.toString() - } else { - rawMessage - } -} - -/** - * Look for a single node ID integer in the string and annotate it with the hex equivalent - * if found. - */ -private fun StringBuilder.annotateNodeId(nodeId: Int): Boolean { - val nodeIdStr = nodeId.toUInt().toString() - indexOf(nodeIdStr).takeIf { it >= 0 }?.let { idx -> - insert(idx + nodeIdStr.length, " (${nodeId.asNodeId()})") - return true - } - return false -} - -private fun Int.asNodeId(): String { - return "!%08x".format(Locale.getDefault(), this) -} - @Composable internal fun DebugScreen( viewModel: DebugViewModel = hiltViewModel(), @@ -162,7 +103,7 @@ internal fun DebugScreen( if (shouldAutoScroll) { LaunchedEffect(logs) { if (!listState.isScrollInProgress) { - listState.scrollToItem(0) + listState.animateScrollToItem(0) } } } @@ -181,18 +122,24 @@ internal fun DebugScreen( modifier = Modifier.fillMaxSize(), state = listState, ) { - items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) } + items(logs, key = { it.uuid }) { log -> + DebugItem( + modifier = Modifier.animateItem(), + log = log, + ) + } } } } } @Composable -internal fun DebugItem(log: MeshLog) { - val timeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) - +internal fun DebugItem( + log: UiMeshLog, + modifier: Modifier = Modifier, +) { Card( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(4.dp), elevation = 4.dp, @@ -207,9 +154,10 @@ internal fun DebugItem(log: MeshLog) { .fillMaxWidth() .padding(bottom = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { Text( - text = log.message_type, + text = log.messageType, modifier = Modifier.weight(1f), style = TextStyle(fontWeight = FontWeight.Bold), ) @@ -220,22 +168,12 @@ internal fun DebugItem(log: MeshLog) { modifier = Modifier.padding(end = 8.dp), ) Text( - text = timeFormat.format(log.received_date), + text = log.formattedReceivedDate, style = TextStyle(fontWeight = FontWeight.Bold), ) } - val style = SpanStyle( - color = colorResource(id = R.color.colorAnnotation), - fontStyle = FontStyle.Italic, - ) - val annotatedString = buildAnnotatedString { - append(log.raw_message) - REGEX_ANNOTATED_NODE_ID.findAll(log.raw_message).toList().reversed().forEach { - addStyle(style = style, start = it.range.first, end = it.range.last + 1) - } - } - + val annotatedString = rememberAnnotatedLogMessage(log) Text( text = annotatedString, softWrap = false, @@ -249,16 +187,37 @@ internal fun DebugItem(log: MeshLog) { } } +@Composable +private fun rememberAnnotatedLogMessage(log: UiMeshLog): AnnotatedString { + val style = SpanStyle( + color = colorResource(id = R.color.colorAnnotation), + fontStyle = FontStyle.Italic, + ) + return remember(log.uuid) { + buildAnnotatedString { + append(log.logMessage) + REGEX_ANNOTATED_NODE_ID.findAll(log.logMessage).toList().reversed() + .forEach { + addStyle( + style = style, + start = it.range.first, + end = it.range.last + 1 + ) + } + } + } +} + @PreviewLightDark @Composable private fun DebugScreenPreview() { AppTheme { DebugItem( - MeshLog( + UiMeshLog( uuid = "", - message_type = "NodeInfo", - received_date = 1601251258000L, - raw_message = "from: 2885173132\n" + + messageType = "NodeInfo", + formattedReceivedDate = "9/27/20, 8:00:58 PM", + logMessage = "from: 2885173132\n" + "decoded {\n" + " position {\n" + " altitude: 60\n" +