From cca7c274b98dbe224c3df6f511da7f3263c697ab Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sun, 31 May 2026 07:16:10 -0700 Subject: [PATCH] fix: address top Crashlytics crashes and non-fatals for build 29320984 (#5684) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../app/map/component/NodeClusterMarkers.kt | 8 +++++ .../app/node/component/InlineMap.kt | 29 ++++++++++++++++++- .../meshtastic/core/ble/KermitLogEngine.kt | 9 ++++-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt index af8704d54..3efaf68ef 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt @@ -18,9 +18,12 @@ package org.meshtastic.app.map.component import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.currentStateAsState import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.savedstate.compose.LocalSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner @@ -45,6 +48,7 @@ fun NodeClusterMarkers( val view = LocalView.current val lifecycleOwner = LocalLifecycleOwner.current val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current + val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState() // Workaround for https://github.com/googlemaps/android-maps-compose/issues/858 // and https://github.com/googlemaps/android-maps-compose/issues/875 @@ -66,6 +70,10 @@ fun NodeClusterMarkers( onDispose {} } + // Guard against the cluster renderer's async Handler trying to render markers + // after the lifecycle has stopped — the internal ComposeView requires an active lifecycle. + if (!lifecycleState.isAtLeast(Lifecycle.State.STARTED)) return + Clustering( items = nodeClusterItems, onClusterClick = onClusterClick, diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt index 993082067..c65c0bec1 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt @@ -17,10 +17,18 @@ package org.meshtastic.app.node.component import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.compose.LocalSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.Circle @@ -46,6 +54,23 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) { true -> ComposeMapColorScheme.DARK else -> ComposeMapColorScheme.LIGHT } + + // Workaround for maps-compose issue where MarkerComposable's internal ComposeView + // cannot find ViewTreeLifecycleOwner, causing crash on bitmap rendering. + val view = LocalView.current + val lifecycleOwner = LocalLifecycleOwner.current + val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current + DisposableEffect(lifecycleOwner, savedStateRegistryOwner) { + val root = view.rootView + root.setViewTreeLifecycleOwner(lifecycleOwner) + root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner) + if (view !== root) { + view.setViewTreeLifecycleOwner(lifecycleOwner) + view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner) + } + onDispose {} + } + key(node.num) { val location = LatLng(node.latitude, node.longitude) val cameraState = rememberCameraPositionState { @@ -79,7 +104,9 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) { strokeWidth = 2f, ) } - MarkerComposable(state = rememberUpdatedMarkerState(position = latLng)) { NodeChip(node = node) } + MarkerComposable(state = rememberUpdatedMarkerState(position = latLng)) { + NodeChip(node = node, modifier = Modifier.defaultMinSize(minWidth = 64.dp, minHeight = 28.dp)) + } } } } diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KermitLogEngine.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KermitLogEngine.kt index 6884dc9e1..97739a072 100644 --- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KermitLogEngine.kt +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KermitLogEngine.kt @@ -23,6 +23,10 @@ import com.juul.kable.logs.LogEngine * Bridges Kable's internal logging to [Kermit][Logger] so BLE lifecycle events (connect, disconnect, subscribe, GATT * operations) appear in the standard app logs rather than going to [System.out] via Kable's default * [com.juul.kable.logs.SystemLogEngine]. + * + * Kable logs connection failures and disconnections at error level, but these are expected BLE operational events — not + * application bugs. We downgrade error/assert to warn so these don't trigger non-fatal exception recording in + * Crashlytics (which records any Kermit Error-level log with a throwable as a non-fatal). */ internal object KermitLogEngine : LogEngine { override fun verbose(throwable: Throwable?, tag: String, message: String) { @@ -42,10 +46,11 @@ internal object KermitLogEngine : LogEngine { } override fun error(throwable: Throwable?, tag: String, message: String) { - Logger.e(throwable) { "[$tag] $message" } + // Downgrade: Kable "errors" are operational (failed connect, disconnect requested) not app bugs. + Logger.w(throwable) { "[$tag] $message" } } override fun assert(throwable: Throwable?, tag: String, message: String) { - Logger.e(throwable) { "[$tag] $message" } + Logger.w(throwable) { "[$tag] $message" } } }