Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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" }
}
}