From c665b5528cd91be8607e807563dd083fd259090f Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Tue, 22 Jul 2025 00:26:25 +1000 Subject: [PATCH] feat/2482 Make decoded payload accessible to filters/search/copies (#2483) --- .../geeksville/mesh/model/DebugViewModel.kt | 31 ++++- .../com/geeksville/mesh/ui/debug/Debug.kt | 130 +++++++++++------- 2 files changed, 114 insertions(+), 47 deletions(-) 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 2ac6c923d..d0aa7e13c 100644 --- a/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/DebugViewModel.kt @@ -48,6 +48,7 @@ import com.geeksville.mesh.TelemetryProtos import com.geeksville.mesh.AdminProtos import com.geeksville.mesh.PaxcountProtos import com.geeksville.mesh.StoreAndForwardProtos +import com.geeksville.mesh.ui.debug.FilterMode data class SearchMatch( val logIndex: Int, @@ -140,7 +141,11 @@ class LogSearchManager { .map { match -> SearchMatch(logIndex, match.range.first, match.range.last, "type") } val dateMatches = regex.findAll(log.formattedReceivedDate) .map { match -> SearchMatch(logIndex, match.range.first, match.range.last, "date") } - messageMatches + typeMatches + dateMatches + val decodedPayloadMatches = log.decodedPayload?.let { decoded -> + regex.findAll(decoded) + .map { match -> SearchMatch(logIndex, match.range.first, match.range.last, "decodedPayload") } + } ?: emptySequence() + messageMatches + typeMatches + dateMatches + decodedPayloadMatches } }.sortedBy { it.start } } @@ -160,6 +165,30 @@ class LogFilterManager { fun updateFilteredLogs(logs: List) { _filteredLogs.value = logs } + + fun filterLogs( + logs: List, + filterTexts: List, + filterMode: FilterMode + ): List { + if (filterTexts.isEmpty()) return logs + return logs.filter { log -> + when (filterMode) { + FilterMode.OR -> filterTexts.any { filterText -> + log.logMessage.contains(filterText, ignoreCase = true) || + log.messageType.contains(filterText, ignoreCase = true) || + log.formattedReceivedDate.contains(filterText, ignoreCase = true) || + (log.decodedPayload?.contains(filterText, ignoreCase = true) == true) + } + FilterMode.AND -> filterTexts.all { filterText -> + log.logMessage.contains(filterText, ignoreCase = true) || + log.messageType.contains(filterText, ignoreCase = true) || + log.formattedReceivedDate.contains(filterText, ignoreCase = true) || + (log.decodedPayload?.contains(filterText, ignoreCase = true) == true) + } + } + } + } } private const val HEX_FORMAT = "%02x" diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt index ca9010691..0c83f13e0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt @@ -110,24 +110,9 @@ internal fun DebugScreen( var filterMode by remember { mutableStateOf(FilterMode.OR) } + // Use the new filterLogs method to include decodedPayload in filtering val filteredLogs = remember(logs, filterTexts, filterMode) { - logs.filter { log -> - if (filterTexts.isEmpty()) { - true - } else { when (filterMode) { - FilterMode.OR -> filterTexts.any { filterText -> - log.logMessage.contains(filterText, ignoreCase = true) || - log.messageType.contains(filterText, ignoreCase = true) || - log.formattedReceivedDate.contains(filterText, ignoreCase = true) - } - FilterMode.AND -> filterTexts.all { filterText -> - log.logMessage.contains(filterText, ignoreCase = true) || - log.messageType.contains(filterText, ignoreCase = true) || - log.formattedReceivedDate.contains(filterText, ignoreCase = true) - } - } - } - }.toImmutableList() + viewModel.filterManager.filterLogs(logs, filterTexts, filterMode).toImmutableList() } LaunchedEffect(filteredLogs) { @@ -237,12 +222,14 @@ internal fun DebugItem( color = colorScheme.onSurface ) ) - // Show decoded payload if available + // Show decoded payload if available, with search highlighting if (!log.decodedPayload.isNullOrBlank()) { DecodedPayloadBlock( decodedPayload = log.decodedPayload, isSelected = isSelected, - colorScheme = colorScheme + colorScheme = colorScheme, + searchText = searchText, + modifier = Modifier.weight(1f) ) } } @@ -277,8 +264,20 @@ private fun DebugItemHeader( color = theme.onSurface ), ) + // Copy full log: message + decoded payload if present + val fullLogText = remember(log.logMessage, log.decodedPayload) { + buildString { + append(log.logMessage) + if (!log.decodedPayload.isNullOrBlank()) { + append("\n\nDecoded Payload:\n{") + append("\n") + append(log.decodedPayload) + append("\n}") + } + } + } CopyIconButton( - valueToCopy = log.logMessage, + valueToCopy = fullLogText, modifier = Modifier.padding(start = 8.dp) ) Icon( @@ -756,6 +755,12 @@ private suspend fun exportAllLogs(context: Context, logs: List) = wit logs.forEach { log -> writer.write("${log.formattedReceivedDate} [${log.messageType}]\n") writer.write(log.logMessage) + if (!log.decodedPayload.isNullOrBlank()) { + writer.write("\n\nDecoded Payload:\n{") + writer.write("\n") + writer.write(log.decodedPayload) + writer.write("\n}") + } writer.write("\n\n") } } @@ -793,7 +798,9 @@ private suspend fun exportAllLogs(context: Context, logs: List) = wit private fun DecodedPayloadBlock( decodedPayload: String, isSelected: Boolean, - colorScheme: ColorScheme + colorScheme: ColorScheme, + searchText: String = "", + modifier: Modifier = Modifier ) { val commonTextStyle = TextStyle( @@ -802,29 +809,60 @@ private fun DecodedPayloadBlock( color = colorScheme.primary ) - Text( - text = stringResource(id = R.string.debug_decoded_payload), - style = commonTextStyle, - modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) - ) - Text( - text = "{", - style = commonTextStyle, - modifier = Modifier.padding(start = 8.dp, bottom = 2.dp) - ) - Text( - text = decodedPayload, - softWrap = true, - style = TextStyle( - fontSize = if (isSelected) 10.sp else 8.sp, - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.8f) - ), - modifier = Modifier.padding(start = 16.dp, bottom = 0.dp) - ) - Text( - text = "}", - style = commonTextStyle, - modifier = Modifier.padding(start = 8.dp, bottom = 4.dp) - ) + Column(modifier = modifier) { + Text( + text = stringResource(id = R.string.debug_decoded_payload), + style = commonTextStyle, + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) + ) + Text( + text = "{", + style = commonTextStyle, + modifier = Modifier.padding(start = 8.dp, bottom = 2.dp) + ) + val annotatedPayload = rememberAnnotatedDecodedPayload(decodedPayload, searchText, colorScheme) + Text( + text = annotatedPayload, + softWrap = true, + style = TextStyle( + fontSize = if (isSelected) 10.sp else 8.sp, + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.8f) + ), + modifier = Modifier.padding(start = 16.dp, bottom = 0.dp) + ) + Text( + text = "}", + style = commonTextStyle, + modifier = Modifier.padding(start = 8.dp, bottom = 4.dp) + ) + } +} + +@Composable +private fun rememberAnnotatedDecodedPayload( + decodedPayload: String, + searchText: String, + colorScheme: ColorScheme +): AnnotatedString { + val highlightStyle = SpanStyle( + background = colorScheme.primary.copy(alpha = 0.3f), + color = colorScheme.onSurface + ) + return remember(decodedPayload, searchText) { + buildAnnotatedString { + append(decodedPayload) + if (searchText.isNotEmpty()) { + searchText.split(" ").forEach { term -> + Regex(Regex.escape(term), RegexOption.IGNORE_CASE).findAll(decodedPayload).forEach { match -> + addStyle( + style = highlightStyle, + start = match.range.first, + end = match.range.last + 1 + ) + } + } + } + } + } }