fix: address top Crashlytics crashes in beta 2.7.14 (#5672)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James RichandCopilot authored and GitHub committed 2026-05-30 15:01:29 +00:00
1 parent ad6144189f
commit c7fbc6e9d3
6 files changed
+66 -19

No files matched your search

@@ -17,14 +17,12 @@
package org.meshtastic.app.map.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.view.DefaultClusterRenderer
@@ -54,15 +52,18 @@ fun NodeClusterMarkers(
// If that view is not attached to the hierarchy (which it often isn't during rendering),
// it fails to find the Lifecycle and SavedState owners. We propagate them to the root view
// so the internal snapshot view can find them when walking up the tree.
// We do this in a SideEffect to ensure it happens before or during composition of children.
SideEffect {
// DisposableEffect runs at composition time (not post-composition like SideEffect),
// ensuring owners are set before the Clustering composable triggers marker rendering.
DisposableEffect(lifecycleOwner, savedStateRegistryOwner) {
val root = view.rootView
if (root.findViewTreeLifecycleOwner() == null) {
root.setViewTreeLifecycleOwner(lifecycleOwner)
}
if (root.findViewTreeSavedStateRegistryOwner() == null) {
root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
root.setViewTreeLifecycleOwner(lifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
// Also set on the view itself in case the internal renderer walks from a child
if (view !== root) {
view.setViewTreeLifecycleOwner(lifecycleOwner)
view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
}
onDispose {}
}
Clustering(
@@ -23,13 +23,17 @@ import kotlin.math.roundToLong
object NumberFormatter {
/** Formats a double value with the specified number of decimal places. */
fun format(value: Double, decimalPlaces: Int): String {
if (value.isNaN() || value.isInfinite()) return "—"
val factor = 10.0.pow(decimalPlaces)
val rounded = (value * factor).roundToLong()
return formatFixedPoint(rounded, decimalPlaces)
}
/** Formats a float value with the specified number of decimal places. */
fun format(value: Float, decimalPlaces: Int): String = format(value.toDouble(), decimalPlaces)
fun format(value: Float, decimalPlaces: Int): String {
if (value.isNaN() || value.isInfinite()) return "—"
return format(value.toDouble(), decimalPlaces)
}
private fun formatFixedPoint(scaledValue: Long, decimalPlaces: Int): String {
if (decimalPlaces == 0) return scaledValue.toString()
@@ -35,4 +35,17 @@ class NumberFormatterTest {
assertEquals("1", NumberFormatter.format(1.23, 0))
assertEquals("-1", NumberFormatter.format(-1.23, 0))
}
@Test
fun testFormatNaN() {
assertEquals("—", NumberFormatter.format(Double.NaN, 2))
assertEquals("—", NumberFormatter.format(Float.NaN, 1))
}
@Test
fun testFormatInfinity() {
assertEquals("—", NumberFormatter.format(Double.POSITIVE_INFINITY, 2))
assertEquals("—", NumberFormatter.format(Double.NEGATIVE_INFINITY, 2))
assertEquals("—", NumberFormatter.format(Float.POSITIVE_INFINITY, 1))
}
}
@@ -51,7 +51,13 @@ class StoreForwardPacketHandlerImpl(
override fun handleStoreAndForward(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
val payload = packet.decoded?.payload ?: return
val u = StoreAndForward.ADAPTER.decode(payload)
val u =
try {
StoreAndForward.ADAPTER.decode(payload)
} catch (e: IOException) {
Logger.e(e) { "Failed to parse StoreAndForward packet" }
return
}
handleReceivedStoreAndForward(dataPacket, u, myNodeNum)
}
@@ -338,4 +338,18 @@ class StoreForwardPacketHandlerImplTest {
verifySuspend { packetRepository.updateSFPPStatus(any(), any(), any(), any(), any(), any(), any()) }
}
// ---------- Legacy S&F: malformed proto ----------
@Test
fun `handleStoreAndForward with malformed payload does not crash`() = testScope.runTest {
val malformedPayload = ByteString.of(0xFF.toByte(), 0xFE.toByte(), 0x07, 0x0E)
val packet =
MeshPacket(from = 999, decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = malformedPayload))
val dataPacket = makeDataPacket(999)
// Should not throw — the handler catches the IOException from proto decoding
handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
advanceUntilIdle()
}
}
@@ -86,14 +86,23 @@ internal class SerialConnectionImpl(
}
port.open(usbDeviceConnection)
port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
try {
port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
// Assert DTR/RTS so native USB-CDC firmware (RAK4631 / nRF52840) recognizes the host as
// present and starts its serial-side Meshtastic protocol. Empirically, omitting these
// signals causes the firmware to never respond to WAKE_BYTES, stalling the handshake at
// Stage 1. Bridge-chip boards (CH340, CP210x, FTDI) tolerate the assertion.
port.dtr = true
port.rts = true
// Assert DTR/RTS so native USB-CDC firmware (RAK4631 / nRF52840) recognizes the host as
// present and starts its serial-side Meshtastic protocol. Empirically, omitting these
// signals causes the firmware to never respond to WAKE_BYTES, stalling the handshake at
// Stage 1. Bridge-chip boards (CH340, CP210x, FTDI) tolerate the assertion.
port.dtr = true
port.rts = true
} catch (e: java.io.IOException) {
Logger.w(e) { "USB control transfer failed during port setup — device may have disconnected" }
closed.set(true)
ignoreException(silent = true) { port.close() }
closedLatch.countDown()
listener.onDisconnected(e)
return
}
Logger.d { "Starting serial reader thread" }
val io =