kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: migrate `DebugFragment` RecyclerView to Compose
rodzic
c7a3488a78
commit
a543bcbfcd
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
Ładowanie…
Reference in New Issue