diff --git a/app/src/main/java/com/geeksville/mesh/ui/DebugAdapter.kt b/app/src/main/java/com/geeksville/mesh/ui/DebugAdapter.kt deleted file mode 100644 index f81316a30..000000000 --- a/app/src/main/java/com/geeksville/mesh/ui/DebugAdapter.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.geeksville.mesh.ui - -import android.content.Context -import android.text.SpannedString -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.text.toSpannable -import androidx.recyclerview.widget.RecyclerView -import com.geeksville.mesh.R -import com.geeksville.mesh.database.entity.MeshLog -import java.text.DateFormat -import java.util.Date - -class DebugAdapter internal constructor( - context: Context -) : RecyclerView.Adapter() { - - private val inflater: LayoutInflater = LayoutInflater.from(context) - private val colorAnnotation = ContextCompat.getColor(context, R.color.colorAnnotation) - private var logs = emptyList() - - private val timeFormat: DateFormat = - DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) - - inner class DebugViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val logTypeView: TextView = itemView.findViewById(R.id.type) - val logDateReceivedView: TextView = itemView.findViewById(R.id.dateReceived) - val logRawMessage: TextView = itemView.findViewById(R.id.rawMessage) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DebugViewHolder { - val itemView = inflater.inflate(R.layout.adapter_debug_layout, parent, false) - return DebugViewHolder(itemView) - } - - override fun onBindViewHolder(holder: DebugViewHolder, position: Int) { - val current = logs[position] - holder.logTypeView.text = current.message_type - holder.logRawMessage.text = annotateMessage(current) - val date = Date(current.received_date) - holder.logDateReceivedView.text = timeFormat.format(date) - } - - /** - * Enhance the raw message by visually distinguishing the annotations prior to when - * the data was added to the database. - * - * @see com.geeksville.mesh.ui.DebugFragment.annotateMeshLogs - */ - private fun annotateMessage(current: MeshLog): CharSequence { - val spannable = current.raw_message.toSpannable() - REGEX_ANNOTATED_NODE_ID.findAll(spannable).toList().reversed().forEach { - spannable.setSpan( - android.text.style.StyleSpan(android.graphics.Typeface.ITALIC), - it.range.first, - it.range.last + 1, - SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE - ) - spannable.setSpan( - android.text.style.ForegroundColorSpan(colorAnnotation), - it.range.first, - it.range.last + 1, - SpannedString.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - return spannable - } - - internal fun setLogs(logs: List) { - this.logs = logs - notifyDataSetChanged() - } - - override fun getItemCount() = logs.size - - private companion object { - /** - * Regex to match the node ID annotations in the MeshLog raw message text. - */ - val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE) - } -} \ No newline at end of file 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 66cf03fce..df9ee8616 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DebugFragment.kt @@ -1,24 +1,51 @@ package com.geeksville.mesh.ui +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.asLiveData -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.geeksville.mesh.CoroutineDispatchers +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.R import com.geeksville.mesh.database.entity.MeshLog import com.geeksville.mesh.databinding.FragmentDebugBinding import com.geeksville.mesh.model.DebugViewModel +import com.geeksville.mesh.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map +import java.text.DateFormat import java.util.Locale -import javax.inject.Inject @AndroidEntryPoint class DebugFragment : Fragment() { @@ -30,9 +57,6 @@ class DebugFragment : Fragment() { private val model: DebugViewModel by viewModels() - @Inject - lateinit var dispatchers: CoroutineDispatchers - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -43,11 +67,6 @@ class DebugFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val recyclerView = view.findViewById(R.id.debug_recyclerview) - val adapter = DebugAdapter(requireContext()) - - recyclerView.adapter = adapter - recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.clearButton.setOnClickListener { model.deleteAllLogs() @@ -56,12 +75,25 @@ class DebugFragment : Fragment() { binding.closeButton.setOnClickListener { parentFragmentManager.popBackStack() } - model.meshLog - .map(this::annotateMeshLogs) - .flowOn(dispatchers.default) - .asLiveData() - .observe(viewLifecycleOwner) { logs -> - logs?.let { adapter.setLogs(it) } + + binding.debugListView.setContent { + val listState = rememberLazyListState() + val logs by model.meshLog.collectAsStateWithLifecycle() + + LaunchedEffect(logs) { + if (listState.firstVisibleItemIndex < 3 && !listState.isScrollInProgress) { + listState.scrollToItem(0) + } + } + + AppTheme { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + ) { + items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) } + } + } } } @@ -71,33 +103,34 @@ class DebugFragment : Fragment() { } /** - * Transform the input list by enhancing the raw message with annotations. + * Transform the input [MeshLog] by enhancing the raw message with annotations. */ - private fun annotateMeshLogs(logs: List): List { - return logs.map { meshLog -> - val annotated = when (meshLog.message_type) { - "Packet" -> { - meshLog.meshPacket?.let { packet -> - annotateRawMessage(meshLog.raw_message, packet.from, packet.to) - } + 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 } - if (annotated == null) { - meshLog - } else { - meshLog.copy(raw_message = annotated) + + "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) } } @@ -133,4 +166,98 @@ class DebugFragment : Fragment() { private fun Int.asNodeId(): String { return "!%08x".format(Locale.getDefault(), this) } -} \ No newline at end of file +} + +private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE) + +@Composable +internal fun DebugItem(log: MeshLog) { + val timeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + elevation = 4.dp, + shape = RoundedCornerShape(12.dp), + ) { + Surface { + Column( + modifier = Modifier.padding(8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = log.message_type, + modifier = Modifier.weight(1f), + style = TextStyle(fontWeight = FontWeight.Bold), + ) + Icon( + painterResource(R.drawable.cloud_download_outline_24), + contentDescription = null, + tint = Color.Gray.copy(alpha = 0.6f), + modifier = Modifier.padding(end = 8.dp), + ) + Text( + text = timeFormat.format(log.received_date), + 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) + } + } + + Text( + text = annotatedString, + softWrap = false, + style = TextStyle( + fontSize = 9.sp, + fontFamily = FontFamily.Monospace, + ) + ) + } + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DebugScreenPreview() { + AppTheme { + DebugItem( + MeshLog( + uuid = "", + message_type = "NodeInfo", + received_date = 1601251258000L, + raw_message = "from: 2885173132\n" + + "decoded {\n" + + " position {\n" + + " altitude: 60\n" + + " battery_level: 81\n" + + " latitude_i: 411111136\n" + + " longitude_i: -711111805\n" + + " time: 1600390966\n" + + " }\n" + + "}\n" + + "hop_limit: 3\n" + + "id: 1737414295\n" + + "rx_snr: 9.5\n" + + "rx_time: 316400569\n" + + "to: -1409790708", + ) + ) + } +} diff --git a/app/src/main/res/layout/adapter_debug_layout.xml b/app/src/main/res/layout/adapter_debug_layout.xml deleted file mode 100644 index d890e2636..000000000 --- a/app/src/main/res/layout/adapter_debug_layout.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_debug.xml b/app/src/main/res/layout/fragment_debug.xml index 5b0e87e82..83401e8a8 100644 --- a/app/src/main/res/layout/fragment_debug.xml +++ b/app/src/main/res/layout/fragment_debug.xml @@ -7,19 +7,15 @@ android:background="@color/colorAdvancedBackground" > - + app:layout_constraintTop_toBottomOf="@+id/clearButton" />