mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-10-06 10:31:21 -04:00
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:
1 parent
ad6144189f
commit
c7fbc6e9d3
6 files changed
+66
-19
No files matched your search
+11
-10
@@ -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(
|
||||
|
||||
+5
-1
@@ -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()
|
||||
|
||||
+13
@@ -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))
|
||||
}
|
||||
}
|
||||
+7
-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)
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -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()
|
||||
}
|
||||
}
|
||||
+16
-7
@@ -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 =
|
||||
|
||||
Reference in new issue
Block a user