diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts
index 78f2849c6..8380bc81c 100644
--- a/feature/map/build.gradle.kts
+++ b/feature/map/build.gradle.kts
@@ -16,23 +16,6 @@
*/
import com.android.build.api.dsl.LibraryExtension
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.compose)
@@ -62,8 +45,11 @@ dependencies {
implementation(libs.androidx.compose.ui.text)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.lifecycle.runtime.compose)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.common)
+ implementation(libs.androidx.savedstate.ktx)
implementation(libs.material)
implementation(libs.kermit)
diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
index a8dc3091f..bc7851d90 100644
--- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
+++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
@@ -81,7 +81,7 @@ import org.meshtastic.proto.copy
import org.meshtastic.proto.waypoint
import java.util.Calendar
-@Suppress("LongMethod")
+@Suppress("LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun EditWaypointDialog(
@@ -100,29 +100,52 @@ fun EditWaypointDialog(
// Get current context for dialogs
val context = LocalContext.current
- val calendar = Calendar.getInstance()
- val currentTime = System.currentTimeMillis()
- calendar.timeInMillis = currentTime
- @Suppress("MagicNumber")
- calendar.add(Calendar.HOUR_OF_DAY, 8)
-
- // Current time for initializing pickers
- val year = calendar.get(Calendar.YEAR)
- val month = calendar.get(Calendar.MONTH)
- val day = calendar.get(Calendar.DAY_OF_MONTH)
- val hour = calendar.get(Calendar.HOUR_OF_DAY)
- val minute = calendar.get(Calendar.MINUTE)
+ val calendar = remember {
+ Calendar.getInstance().apply {
+ if (waypoint.expire != 0 && waypoint.expire != Int.MAX_VALUE) {
+ timeInMillis = waypoint.expire * 1000L
+ } else {
+ timeInMillis = System.currentTimeMillis()
+ @Suppress("MagicNumber")
+ add(Calendar.HOUR_OF_DAY, 8)
+ }
+ }
+ }
// Determine locale-specific date format
- val dateFormat = android.text.format.DateFormat.getDateFormat(context)
+ val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
// Check if 24-hour format is preferred
- val is24Hour = android.text.format.DateFormat.is24HourFormat(context)
- val timeFormat = android.text.format.DateFormat.getTimeFormat(context)
+ val is24Hour = remember { android.text.format.DateFormat.is24HourFormat(context) }
+ val timeFormat = remember { android.text.format.DateFormat.getTimeFormat(context) }
// State to hold selected date and time
- var selectedDate by remember { mutableStateOf(dateFormat.format(calendar.time)) }
- var selectedTime by remember { mutableStateOf(timeFormat.format(calendar.time)) }
- var epochTime by remember { mutableStateOf(null) }
+ var selectedDate by remember {
+ mutableStateOf(
+ if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
+ dateFormat.format(calendar.time)
+ } else {
+ ""
+ },
+ )
+ }
+ var selectedTime by remember {
+ mutableStateOf(
+ if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
+ timeFormat.format(calendar.time)
+ } else {
+ ""
+ },
+ )
+ }
+ var epochTime by remember {
+ mutableStateOf(
+ if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
+ waypointInput.expire * 1000L
+ } else {
+ null
+ },
+ )
+ }
if (!showEmojiPickerView) {
AlertDialog(
@@ -193,9 +216,9 @@ fun EditWaypointDialog(
epochTime = calendar.timeInMillis
selectedDate = dateFormat.format(calendar.time)
},
- year,
- month,
- day,
+ calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH),
+ calendar.get(Calendar.DAY_OF_MONTH),
)
val timePickerDialog =
@@ -206,11 +229,10 @@ fun EditWaypointDialog(
calendar.set(Calendar.MINUTE, selectedMinute)
epochTime = calendar.timeInMillis
selectedTime = timeFormat.format(calendar.time)
- @Suppress("MagicNumber")
- waypointInput = waypointInput.copy { expire = (epochTime!! / 1000).toInt() }
+ waypointInput = waypointInput.copy { expire = (calendar.timeInMillis / 1000).toInt() }
},
- hour,
- minute,
+ calendar.get(Calendar.HOUR_OF_DAY),
+ calendar.get(Calendar.MINUTE),
is24Hour,
)
@@ -227,23 +249,19 @@ fun EditWaypointDialog(
modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.End),
checked = waypointInput.expire != Int.MAX_VALUE && waypointInput.expire != 0,
onCheckedChange = { isChecked ->
- waypointInput =
- waypointInput.copy {
- expire =
- if (isChecked) {
- @Suppress("MagicNumber")
- calendar.timeInMillis / 1000
- } else {
- Int.MAX_VALUE
- }
- .toInt()
- }
if (isChecked) {
+ // Default to now if not already set
+ if (epochTime == null) {
+ epochTime = calendar.timeInMillis
+ }
selectedDate = dateFormat.format(calendar.time)
selectedTime = timeFormat.format(calendar.time)
+ waypointInput =
+ waypointInput.copy { expire = (calendar.timeInMillis / 1000).toInt() }
} else {
selectedDate = ""
selectedTime = ""
+ waypointInput = waypointInput.copy { expire = Int.MAX_VALUE }
}
},
)
diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
index bdecbd2f5..0dea2a46f 100644
--- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
@@ -246,10 +246,15 @@ fun EditWaypointDialog(
DatePickerDialog(
context,
{ _: DatePicker, selectedYear: Int, selectedMonth: Int, selectedDay: Int ->
- calendar.clear()
- calendar.set(selectedYear, selectedMonth, selectedDay, hour, minute)
+ val tempCal = Calendar.getInstance()
+ if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
+ tempCal.timeInMillis = waypointInput.expire * 1000L
+ } else {
+ tempCal.add(Calendar.HOUR_OF_DAY, 8)
+ }
+ tempCal.set(selectedYear, selectedMonth, selectedDay)
waypointInput =
- waypointInput.copy { expire = (calendar.timeInMillis / 1000).toInt() }
+ waypointInput.copy { expire = (tempCal.timeInMillis / 1000).toInt() }
},
year,
month,
@@ -262,7 +267,11 @@ fun EditWaypointDialog(
{ _: TimePicker, selectedHour: Int, selectedMinute: Int ->
// Keep the existing date part
val tempCal = Calendar.getInstance()
- tempCal.timeInMillis = waypointInput.expire * 1000L
+ if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
+ tempCal.timeInMillis = waypointInput.expire * 1000L
+ } else {
+ tempCal.add(Calendar.HOUR_OF_DAY, 8)
+ }
tempCal.set(Calendar.HOUR_OF_DAY, selectedHour)
tempCal.set(Calendar.MINUTE, selectedMinute)
waypointInput =
diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt
index b1ad47f75..7f85d74e3 100644
--- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/component/NodeClusterMarkers.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,12 +14,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.map.component
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.SavedStateRegistryOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.view.DefaultClusterRenderer
import com.google.maps.android.compose.Circle
@@ -37,6 +44,23 @@ fun NodeClusterMarkers(
navigateToNodeDetails: (Int) -> Unit,
onClusterClick: (Cluster) -> Boolean,
) {
+ val context = LocalContext.current
+ // Workaround for https://github.com/googlemaps/android-maps-compose/issues/858
+ // Ensure owners are set on the Activity decor view so the internal ComposeView created by
+ // the clustering renderer can find them when walking up the view tree.
+ LaunchedEffect(Unit) {
+ val activity = context as? android.app.Activity
+ if (activity != null) {
+ val decorView = activity.window.decorView
+ if (decorView.findViewTreeLifecycleOwner() == null && activity is LifecycleOwner) {
+ decorView.setViewTreeLifecycleOwner(activity)
+ }
+ if (decorView.findViewTreeSavedStateRegistryOwner() == null && activity is SavedStateRegistryOwner) {
+ decorView.setViewTreeSavedStateRegistryOwner(activity)
+ }
+ }
+ }
+
if (mapFilterState.showPrecisionCircle) {
nodeClusterItems.forEach { clusterItem ->
key(clusterItem.node.num) {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ba9bdb1ed..215043c4d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,6 +14,7 @@ navigation = "2.9.6"
navigation3 = "1.0.0"
paging = "3.3.6"
room = "2.8.4"
+savedstate = "1.4.0"
# Kotlin
kotlin = "2.3.0"
@@ -65,7 +66,9 @@ androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-lived
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
+androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
+androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "navigation" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }
androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "navigation" }
@@ -78,6 +81,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
+androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "savedstate" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.0" }
# AndroidX Compose