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