refactor: migrate `DebugPanel` to Compose

pull/1423/head
andrekir 2024-11-19 16:50:42 -03:00 zatwierdzone przez Andre K
rodzic 91c8c7809a
commit e33cf85df6
7 zmienionych plików z 156 dodań i 271 usunięć

Wyświetl plik

@ -1,31 +1,36 @@
package com.geeksville.mesh.database
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.database.dao.MeshLogDao
import com.geeksville.mesh.database.entity.MeshLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
import javax.inject.Inject
class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.Lazy<MeshLogDao>) {
class MeshLogRepository @Inject constructor(
private val meshLogDaoLazy: dagger.Lazy<MeshLogDao>,
private val dispatchers: CoroutineDispatchers,
) {
private val meshLogDao by lazy {
meshLogDaoLazy.get()
}
suspend fun getAllLogs(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> = withContext(Dispatchers.IO) {
meshLogDao.getAllLogs(maxItems)
}
fun getAllLogs(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> = meshLogDao.getAllLogs(maxItems)
.flowOn(dispatchers.io)
.conflate()
suspend fun getAllLogsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> = withContext(Dispatchers.IO) {
fun getAllLogsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
meshLogDao.getAllLogsInReceiveOrder(maxItems)
}
.flowOn(dispatchers.io)
.conflate()
private fun parseTelemetryLog(log: MeshLog): Telemetry? = runCatching {
Telemetry.parseFrom(log.fromRadio.packet.decoded.payload)
@ -37,7 +42,7 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
meshLogDao.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
.distinctUntilChanged()
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
.flowOn(Dispatchers.IO)
.flowOn(dispatchers.io)
fun getLogsFrom(
nodeNum: Int,
@ -45,7 +50,7 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
maxItem: Int = MAX_MESH_PACKETS,
): Flow<List<MeshLog>> = meshLogDao.getLogsFrom(nodeNum, portNum, maxItem)
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.flowOn(dispatchers.io)
/*
* Retrieves MeshPackets matching 'nodeNum' and 'portNum'.
@ -57,21 +62,21 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
): Flow<List<MeshPacket>> = getLogsFrom(nodeNum, portNum)
.mapLatest { list -> list.map { it.fromRadio.packet } }
.flowOn(Dispatchers.IO)
.flowOn(dispatchers.io)
suspend fun insert(log: MeshLog) = withContext(Dispatchers.IO) {
suspend fun insert(log: MeshLog) = withContext(dispatchers.io) {
meshLogDao.insert(log)
}
suspend fun deleteAll() = withContext(Dispatchers.IO) {
suspend fun deleteAll() = withContext(dispatchers.io) {
meshLogDao.deleteAll()
}
suspend fun deleteLog(uuid: String) = withContext(Dispatchers.IO) {
suspend fun deleteLog(uuid: String) = withContext(dispatchers.io) {
meshLogDao.deleteLog(uuid)
}
suspend fun deleteLogs(nodeNum: Int, portNum: Int) = withContext(Dispatchers.IO) {
suspend fun deleteLogs(nodeNum: Int, portNum: Int) = withContext(dispatchers.io) {
meshLogDao.deleteLogs(nodeNum, portNum)
}

Wyświetl plik

@ -7,8 +7,9 @@ import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.entity.MeshLog
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -16,15 +17,10 @@ import javax.inject.Inject
class DebugViewModel @Inject constructor(
private val meshLogRepository: MeshLogRepository,
) : ViewModel(), Logging {
private val _meshLog = MutableStateFlow<List<MeshLog>>(emptyList())
val meshLog: StateFlow<List<MeshLog>> = _meshLog
val meshLog: StateFlow<List<MeshLog>> = meshLogRepository.getAllLogs()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
init {
viewModelScope.launch {
meshLogRepository.getAllLogs().collect { _meshLog.value = it }
}
debug("DebugViewModel created")
}

Wyświetl plik

@ -6,6 +6,7 @@ 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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -15,10 +16,17 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Button
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.CloudDownload
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@ -26,8 +34,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
@ -37,12 +47,12 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
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.databinding.FragmentDebugBinding
import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
@ -51,133 +61,138 @@ import java.util.Locale
@AndroidEntryPoint
class DebugFragment : Fragment() {
private var _binding: FragmentDebugBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private val model: DebugViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDebugBinding.inflate(inflater, container, false)
return binding.root
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
setContent {
val viewModel: DebugViewModel = hiltViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.clearButton.setOnClickListener {
model.deleteAllLogs()
}
binding.closeButton.setOnClickListener {
parentFragmentManager.popBackStack()
}
binding.debugListView.setContent {
val listState = rememberLazyListState()
val logs by model.meshLog.collectAsStateWithLifecycle()
val shouldAutoScroll by remember { derivedStateOf { listState.firstVisibleItemIndex < 3 } }
if (shouldAutoScroll) {
LaunchedEffect(logs) {
if (!listState.isScrollInProgress) {
listState.scrollToItem(0)
}
}
}
AppTheme {
SelectionContainer {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
) {
items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) }
AppTheme {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.debug_panel)) },
navigationIcon = {
IconButton(onClick = { parentFragmentManager.popBackStack() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
stringResource(id = R.string.navigate_back),
)
}
},
actions = {
Button(onClick = viewModel::deleteAllLogs) {
Text(text = stringResource(R.string.clear))
}
}
)
},
) { innerPadding ->
DebugScreen(
viewModel = viewModel,
contentPadding = innerPadding,
)
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* 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)
}
}
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(),
contentPadding: PaddingValues,
) {
val listState = rememberLazyListState()
val logs by viewModel.meshLog.collectAsStateWithLifecycle()
val shouldAutoScroll by remember { derivedStateOf { listState.firstVisibleItemIndex < 3 } }
if (shouldAutoScroll) {
LaunchedEffect(logs) {
if (!listState.isScrollInProgress) {
listState.scrollToItem(0)
}
}
}
SelectionContainer {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = contentPadding,
) {
items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) }
}
}
}
@Composable
internal fun DebugItem(log: MeshLog) {
val timeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
@ -205,8 +220,8 @@ internal fun DebugItem(log: MeshLog) {
style = TextStyle(fontWeight = FontWeight.Bold),
)
Icon(
painterResource(R.drawable.cloud_download_outline_24),
contentDescription = null,
imageVector = Icons.Outlined.CloudDownload,
contentDescription = stringResource(id = R.string.logs),
tint = Color.Gray.copy(alpha = 0.6f),
modifier = Modifier.padding(end = 8.dp),
)

Wyświetl plik

@ -1,7 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M8,13H10.55V10H13.45V13H16L12,17L8,13M19.35,10.04C21.95,10.22 24,12.36 24,15A5,5 0 0,1 19,20H6A6,6 0 0,1 0,14C0,10.91 2.34,8.36 5.35,8.04C6.6,5.64 9.11,4 12,4C15.64,4 18.67,6.59 19.35,10.04M19,18A3,3 0 0,0 22,15C22,13.45 20.78,12.14 19.22,12.04L17.69,11.93L17.39,10.43C16.88,7.86 14.62,6 12,6C9.94,6 8.08,7.14 7.13,8.97L6.63,9.92L5.56,10.03C3.53,10.24 2,11.95 2,14A4,4 0 0,0 6,18H19Z" />
</vector>

Wyświetl plik

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAdvancedBackground">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/debugListView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/clearButton" />
<Button
android:id="@+id/clearButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/clear"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/closeButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:backgroundTint="#D1D1D1"
app:icon="@android:drawable/ic_menu_close_clear_cancel"
app:iconPadding="0dp"
app:iconSize="24dp"
app:iconTint="#000000"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/Meshtastic.Button.Rounded" />
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/debug_last_messages"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/debugListView"
app:layout_constraintEnd_toStartOf="@+id/clearButton"
app:layout_constraintStart_toEndOf="@+id/closeButton"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAdvancedBackground">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/quickChatSettingsToolbar"
style="@style/MyToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/toolbarBackground"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="?android:attr/homeAsUpIndicator">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/quickChatSettingsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/quick_chat"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.MaterialToolbar>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/quickChatSettingsView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/quickChatSettingsToolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/quickChatSettingsCreateButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_twotone_add_24" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -115,7 +115,6 @@
<ID>FinalNewline:PreferenceCategory.kt$com.geeksville.mesh.ui.components.PreferenceCategory.kt</ID>
<ID>FinalNewline:PreviewParameterProviders.kt$com.geeksville.mesh.ui.preview.PreviewParameterProviders.kt</ID>
<ID>FinalNewline:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>FinalNewline:QuickChatActionDao.kt$com.geeksville.mesh.database.dao.QuickChatActionDao.kt</ID>
<ID>FinalNewline:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt</ID>
<ID>FinalNewline:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt</ID>
<ID>FinalNewline:RadioRepositoryModule.kt$com.geeksville.mesh.repository.radio.RadioRepositoryModule.kt</ID>
@ -219,7 +218,7 @@
<ID>MagicNumber:ContactsFragment.kt$ContactsFragment.ActionModeCallback$8</ID>
<ID>MagicNumber:ContextServices.kt$33</ID>
<ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID>
<ID>MagicNumber:DebugFragment.kt$DebugFragment$3</ID>
<ID>MagicNumber:DebugFragment.kt$3</ID>
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$100</ID>
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$10000</ID>
<ID>MagicNumber:DownloadButton.kt$1.25f</ID>
@ -262,7 +261,6 @@
<ID>MagicNumber:MainActivity.kt$MainActivity$5</ID>
<ID>MagicNumber:MapFragment.kt$0.5f</ID>
<ID>MagicNumber:MapFragment.kt$1.3</ID>
<ID>MagicNumber:MapFragment.kt$1.5</ID>
<ID>MagicNumber:MapFragment.kt$1000</ID>
<ID>MagicNumber:MapFragment.kt$1024.0</ID>
<ID>MagicNumber:MapFragment.kt$128205</ID>
@ -341,14 +339,12 @@
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1000</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e-7</ID>
<ID>MagicNumber:NodeInfo.kt$Position.Companion$1e7</ID>
<ID>MagicNumber:NodeMenu.kt$3</ID>
<ID>MagicNumber:PacketRepository.kt$PacketRepository$500</ID>
<ID>MagicNumber:PacketResponseStateDialog.kt$100</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$21972</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$32809</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114</ID>
<ID>MagicNumber:QuickChatSettingsFragment.kt$QuickChatSettingsFragment$3</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$10</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$100</ID>
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$1000</ID>
@ -577,7 +573,6 @@
<ID>NewLineAtEndOfFile:PreferenceCategory.kt$com.geeksville.mesh.ui.components.PreferenceCategory.kt</ID>
<ID>NewLineAtEndOfFile:PreviewParameterProviders.kt$com.geeksville.mesh.ui.preview.PreviewParameterProviders.kt</ID>
<ID>NewLineAtEndOfFile:ProbeTableProvider.kt$com.geeksville.mesh.repository.usb.ProbeTableProvider.kt</ID>
<ID>NewLineAtEndOfFile:QuickChatActionDao.kt$com.geeksville.mesh.database.dao.QuickChatActionDao.kt</ID>
<ID>NewLineAtEndOfFile:QuickChatActionRepository.kt$com.geeksville.mesh.database.QuickChatActionRepository.kt</ID>
<ID>NewLineAtEndOfFile:RadioNotConnectedException.kt$com.geeksville.mesh.service.RadioNotConnectedException.kt</ID>
<ID>NewLineAtEndOfFile:RadioRepositoryModule.kt$com.geeksville.mesh.repository.radio.RadioRepositoryModule.kt</ID>
@ -602,7 +597,6 @@
<ID>NoBlankLineBeforeRbrace:OnlineTileSourceAuth.kt$OnlineTileSourceAuth$ </ID>
<ID>NoBlankLineBeforeRbrace:PositionTest.kt$PositionTest$ </ID>
<ID>NoBlankLineBeforeRbrace:PreviewParameterProviders.kt$NodeInfoPreviewParameterProvider$ </ID>
<ID>NoBlankLineBeforeRbrace:QuickChatActionDao.kt$QuickChatActionDao$ </ID>
<ID>NoConsecutiveBlankLines:AppIntroduction.kt$AppIntroduction$ </ID>
<ID>NoConsecutiveBlankLines:AppPrefs.kt$ </ID>
<ID>NoConsecutiveBlankLines:BluetoothInterface.kt$ </ID>
@ -637,7 +631,6 @@
<ID>NoWildcardImports:MockInterface.kt$import com.geeksville.mesh.*</ID>
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.material.*</ID>
<ID>NoWildcardImports:QuickChatActionDao.kt$import androidx.room.*</ID>
<ID>NoWildcardImports:SafeBluetooth.kt$import android.bluetooth.*</ID>
<ID>NoWildcardImports:SafeBluetooth.kt$import kotlinx.coroutines.*</ID>
<ID>NoWildcardImports:SettingsFragment.kt$import com.geeksville.mesh.android.*</ID>
@ -761,7 +754,6 @@
<ID>WildcardImport:MockInterface.kt$import com.geeksville.mesh.*</ID>
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.material.*</ID>
<ID>WildcardImport:QuickChatActionDao.kt$import androidx.room.*</ID>
<ID>WildcardImport:SafeBluetooth.kt$import android.bluetooth.*</ID>
<ID>WildcardImport:SafeBluetooth.kt$import kotlinx.coroutines.*</ID>
<ID>WildcardImport:SettingsFragment.kt$import com.geeksville.mesh.android.*</ID>