From e9f95dbf8c03b562f3e4e0e41daf670a89b342aa Mon Sep 17 00:00:00 2001
From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com>
Date: Thu, 19 Jun 2025 07:38:57 -0500
Subject: [PATCH] fix (#2076): hidden client freeze issue when viewing node
details (#2164)
---
.../geeksville/mesh/model/MetricsViewModel.kt | 31 +++++++++++++++++--
app/src/main/res/values/strings.xml | 1 +
2 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
index d3d3eb24f..ce4d5431f 100644
--- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
@@ -29,6 +29,8 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
import com.geeksville.mesh.CoroutineDispatchers
+import com.geeksville.mesh.DataPacket
+import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.Portnums.PortNum
@@ -67,6 +69,8 @@ import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject
+private const val DEFAULT_ID_SUFFIX_LENGTH = 4
+
data class MetricsState(
val isLocal: Boolean = false,
val isManaged: Boolean = true,
@@ -212,6 +216,27 @@ class MetricsViewModel @Inject constructor(
hasDecoded() && decoded.wantResponse && from == 0 && to == destNum
}
+ /**
+ * Creates a fallback node for hidden clients or nodes not yet in the database.
+ * This prevents the detail screen from freezing when viewing unknown nodes.
+ */
+ private fun createFallbackNode(nodeNum: Int): Node {
+ val userId = DataPacket.nodeNumToDefaultId(nodeNum)
+ val safeUserId = userId.padStart(DEFAULT_ID_SUFFIX_LENGTH, '0').takeLast(DEFAULT_ID_SUFFIX_LENGTH)
+ val longName = app.getString(R.string.fallback_node_name, safeUserId)
+ val defaultUser = MeshProtos.User.newBuilder()
+ .setId(userId)
+ .setLongName(longName)
+ .setShortName(safeUserId)
+ .setHwModel(MeshProtos.HardwareModel.UNSET)
+ .build()
+
+ return Node(
+ num = nodeNum,
+ user = defaultUser,
+ )
+ }
+
fun getUser(nodeNum: Int) = radioConfigRepository.getUser(nodeNum)
val tileSource get() = CustomTileSource.getTileSource(preferences.getInt(MAP_STYLE_ID, 0))
@@ -244,12 +269,14 @@ class MetricsViewModel @Inject constructor(
.mapLatest { nodes -> nodes[destNum] to nodes.keys.firstOrNull() }
.distinctUntilChanged()
.onEach { (node, ourNode) ->
- val deviceHardware = node?.user?.hwModel?.number?.let {
+ // Create a fallback node if not found in database (for hidden clients, etc.)
+ val actualNode = node ?: createFallbackNode(destNum)
+ val deviceHardware = actualNode.user.hwModel.number.let {
deviceHardwareRepository.getDeviceHardwareByModel(it)
}
_state.update { state ->
state.copy(
- node = node,
+ node = actualNode,
isLocal = destNum == ourNode,
deviceHardware = deviceHardware
)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c44df9263..e85a43ecd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -37,6 +37,7 @@
Settings
\???
+ Meshtastic %s
Filter
clear node filter
Include unknown