refactor: migrate `DebugFragment` RecyclerView to Compose

pull/1123/head
andrekir 2024-07-01 23:04:15 -03:00
rodzic c7a3488a78
commit a543bcbfcd
4 zmienionych plików z 175 dodań i 223 usunięć

Wyświetl plik

@ -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<DebugAdapter.DebugViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val colorAnnotation = ContextCompat.getColor(context, R.color.colorAnnotation)
private var logs = emptyList<MeshLog>()
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<MeshLog>) {
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)
}
}

Wyświetl plik

@ -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<RecyclerView>(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<MeshLog>): List<MeshLog> {
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)
}
}
}
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",
)
)
}
}

Wyświetl plik

@ -1,86 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/adapterDebugLayout"
style="@style/Widget.App.CardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="@+id/cloudDownloadIcon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/cloudDownloadIcon"
tools:text="NodeInfo" />
<TextView
android:id="@+id/dateReceived"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@+id/cloudDownloadIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/cloudDownloadIcon"
tools:text="9/27/20 21:00:58" />
<TextView
android:id="@+id/rawMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:fontFamily="monospace"
android:singleLine="false"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textIsSelectable="true"
android:textSize="8sp"
android:typeface="monospace"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cloudDownloadIcon"
app:layout_constraintVertical_weight="1"
tools:text="# com.geeksville.mesh.MeshProtos$MeshPacket@1b1ea594\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
from: -1409794164\n
hop_limit: 3\n
id: 1737414295\n
rx_snr: 9.5\n
rx_time: 316400569\n
to: -1409790708" />
<ImageView
android:id="@+id/cloudDownloadIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:alpha="0.4"
app:layout_constraintEnd_toStartOf="@+id/dateReceived"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/cloud_download_outline_24"
android:contentDescription="TODO"
app:tint="@color/colorIconTint" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

Wyświetl plik

@ -7,19 +7,15 @@
android:background="@color/colorAdvancedBackground"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/debug_recyclerview"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/debugListView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:scrollbarAlwaysDrawVerticalTrack="true"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/clearButton"
tools:itemCount="8"
tools:listitem="@layout/adapter_debug_layout" />
app:layout_constraintTop_toBottomOf="@+id/clearButton" />
<Button
android:id="@+id/clearButton"