From 02cf1f10340e7a33d268e3723f47b46455cc60b0 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Thu, 8 Jan 2026 12:43:50 -0600
Subject: [PATCH] refactor(datetime): Standardize date/time formatting with
`DateUtils` (#4164)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
---
.../meshtastic/core/model/util/Extensions.kt | 20 +++++-----
.../composeResources/values/strings.xml | 5 +++
.../org/meshtastic/core/ui/util/FormatAgo.kt | 40 +++++++++++++++++++
.../core/ui/util/ProtoExtensions.kt | 14 ++++---
.../org/meshtastic/feature/map/MapView.kt | 40 +++++++++----------
.../map/component/EditWaypointDialog.kt | 27 ++-----------
.../org/meshtastic/feature/map/MapView.kt | 20 ++--------
.../map/component/EditWaypointDialog.kt | 23 ++---------
.../feature/node/component/LastHeardInfo.kt | 5 +--
.../node/component/LinkedCoordinatesItem.kt | 5 +--
.../node/component/NodeDetailsSection.kt | 2 +-
.../feature/node/metrics/PositionLog.kt | 22 +++-------
.../feature/node/metrics/TracerouteLog.kt | 26 +++++++-----
.../feature/settings/debugging/Debug.kt | 9 +++--
.../settings/debugging/DebugFilters.kt | 9 +++--
15 files changed, 131 insertions(+), 136 deletions(-)
create mode 100644 core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt
index 4886f333f..05ff8018a 100644
--- a/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/util/Extensions.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,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.core.model.util
import android.widget.EditText
@@ -69,15 +68,14 @@ fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
@Suppress("MagicNumber")
fun formatAgo(lastSeenUnix: Int, currentTimeMillis: Long = System.currentTimeMillis()): String {
- val currentTime = (currentTimeMillis / 1000).toInt()
- val diffMin = (currentTime - lastSeenUnix) / 60
- return when {
- diffMin < 1 -> "now"
- diffMin < 60 -> diffMin.toString() + " min"
- diffMin < 2880 -> (diffMin / 60).toString() + " h"
- diffMin < 1440000 -> (diffMin / (60 * 24)).toString() + " d"
- else -> "?"
- }
+ val timeInMillis = lastSeenUnix * 1000L
+ return android.text.format.DateUtils.getRelativeTimeSpanString(
+ timeInMillis,
+ currentTimeMillis,
+ android.text.format.DateUtils.MINUTE_IN_MILLIS,
+ android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE,
+ )
+ .toString()
}
private const val MPS_TO_KMPH = 3.6f
diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml
index 82684dff5..af3f2b548 100644
--- a/core/strings/src/commonMain/composeResources/values/strings.xml
+++ b/core/strings/src/commonMain/composeResources/values/strings.xml
@@ -251,6 +251,10 @@
Add filter
Filter included
Clear all filters
+ Add custom filter
+ Preset Filters
+ Store mesh logs
+ Disable to skip writing mesh logs to disk
Clear Logs
Match Any | All
Match All | Any
@@ -1082,4 +1086,5 @@
Estimated area: \u00b1%1$s (\u00b1%2$s)
Estimated area: unknown accuracy
Mark as read
+ now
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt
new file mode 100644
index 000000000..c76d09bbf
--- /dev/null
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/FormatAgo.kt
@@ -0,0 +1,40 @@
+/*
+ * 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
+ * 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 .
+ */
+package org.meshtastic.core.ui.util
+
+import android.text.format.DateUtils
+import com.meshtastic.core.strings.getString
+import org.meshtastic.core.strings.Res
+import org.meshtastic.core.strings.now
+
+@Suppress("MagicNumber")
+fun formatAgo(lastSeenUnix: Int, currentTimeMillis: Long = System.currentTimeMillis()): String {
+ val timeInMillis = lastSeenUnix * 1000L
+ val diff = currentTimeMillis - timeInMillis
+
+ return if (diff < 60_000L) {
+ getString(Res.string.now)
+ } else {
+ DateUtils.getRelativeTimeSpanString(
+ timeInMillis,
+ currentTimeMillis,
+ DateUtils.MINUTE_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_RELATIVE,
+ )
+ .toString()
+ }
+}
diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
index 8d080a5bd..71ab1b46a 100644
--- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.kt
+++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/util/ProtoExtensions.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,10 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.core.ui.util
+import android.text.format.DateUtils
import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.unknown_age
@@ -28,13 +29,12 @@ import org.meshtastic.proto.MeshProtos.MeshPacket
import org.meshtastic.proto.MeshProtos.Position
import org.meshtastic.proto.channel
import org.meshtastic.proto.channelSettings
-import java.text.DateFormat
import kotlin.time.Duration.Companion.days
private const val SECONDS_TO_MILLIS = 1000L
@Composable
-fun MeshProtos.Position.formatPositionTime(dateFormat: DateFormat): String {
+fun MeshProtos.Position.formatPositionTime(): String {
val currentTime = System.currentTimeMillis()
val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds
val isOlderThanSixMonths = time * SECONDS_TO_MILLIS < sixMonthsAgo
@@ -42,7 +42,11 @@ fun MeshProtos.Position.formatPositionTime(dateFormat: DateFormat): String {
if (isOlderThanSixMonths) {
stringResource(Res.string.unknown_age)
} else {
- dateFormat.format(time * SECONDS_TO_MILLIS)
+ DateUtils.formatDateTime(
+ LocalContext.current,
+ time * SECONDS_TO_MILLIS,
+ DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
+ )
}
return timeText
}
diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt
index e87821579..895f60768 100644
--- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt
+++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapView.kt
@@ -18,6 +18,7 @@ package org.meshtastic.feature.map
import android.Manifest // Added for Accompanist
import android.graphics.Paint
+import android.text.format.DateUtils
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -88,7 +89,6 @@ import org.meshtastic.core.common.hasGps
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
-import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.calculating
import org.meshtastic.core.strings.cancel
@@ -123,6 +123,7 @@ import org.meshtastic.core.strings.you
import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.theme.TracerouteColors
+import org.meshtastic.core.ui.util.formatAgo
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer
import org.meshtastic.feature.map.component.CacheLayout
@@ -157,7 +158,6 @@ import org.osmdroid.views.overlay.Polyline
import org.osmdroid.views.overlay.infowindow.InfoWindow
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import java.io.File
-import java.text.DateFormat
import kotlin.math.abs
import kotlin.math.asin
import kotlin.math.atan2
@@ -514,34 +514,32 @@ fun MapView(
@Suppress("MagicNumber")
fun MapView.onWaypointChanged(waypoints: Collection, selectedWaypointId: Int?): List {
- val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
return waypoints.mapNotNull { waypoint ->
val pt = waypoint.data.waypoint ?: return@mapNotNull null
if (!mapFilterState.showWaypoints) return@mapNotNull null // Use collected mapFilterState
val lock = if (pt.lockedTo != 0) "\uD83D\uDD12" else ""
- val time = dateFormat.format(waypoint.received_time)
+ val time =
+ DateUtils.formatDateTime(
+ context,
+ waypoint.received_time,
+ DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
+ )
val label = pt.name + " " + formatAgo((waypoint.received_time / 1000).toInt())
val emoji = String(Character.toChars(if (pt.icon == 0) 128205 else pt.icon))
- val timeLeft = pt.expire * 1000L - System.currentTimeMillis()
+ val now = System.currentTimeMillis()
+ val expireTimeMillis = pt.expire * 1000L
val expireTimeStr =
when {
pt.expire == 0 || pt.expire == Int.MAX_VALUE -> "Never"
- timeLeft <= 0 -> "Expired"
- timeLeft < 60_000 -> "${timeLeft / 1000} seconds"
- timeLeft < 3_600_000 -> "${timeLeft / 60_000} minute${if (timeLeft / 60_000 != 1L) "s" else ""}"
- timeLeft < 86_400_000 -> {
- val hours = (timeLeft / 3_600_000).toInt()
- val minutes = ((timeLeft % 3_600_000) / 60_000).toInt()
- if (minutes >= 30) {
- "${hours + 1} hour${if (hours + 1 != 1) "s" else ""}"
- } else if (minutes > 0) {
- "$hours hour${if (hours != 1) "s" else ""}, $minutes minute${if (minutes != 1) "s" else ""}"
- } else {
- "$hours hour${if (hours != 1) "s" else ""}"
- }
- }
-
- else -> "${timeLeft / 86_400_000} day${if (timeLeft / 86_400_000 != 1L) "s" else ""}"
+ expireTimeMillis <= now -> "Expired"
+ else ->
+ DateUtils.getRelativeTimeSpanString(
+ expireTimeMillis,
+ now,
+ DateUtils.MINUTE_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_RELATIVE,
+ )
+ .toString()
}
MarkerWithLabel(this, label, emoji).apply {
id = "${pt.id}"
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 611781d81..63c9d4c23 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
@@ -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,7 +14,6 @@
* 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 android.app.DatePickerDialog
@@ -82,9 +81,7 @@ import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.MeshProtos.Waypoint
import org.meshtastic.proto.copy
import org.meshtastic.proto.waypoint
-import java.text.SimpleDateFormat
import java.util.Calendar
-import java.util.Locale
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@@ -119,21 +116,10 @@ fun EditWaypointDialog(
val minute = calendar.get(Calendar.MINUTE)
// Determine locale-specific date format
- val locale = Locale.getDefault()
- val dateFormat =
- if (locale.country == "US") {
- SimpleDateFormat("MM/dd/yyyy", locale)
- } else {
- SimpleDateFormat("dd/MM/yyyy", locale)
- }
+ val dateFormat = android.text.format.DateFormat.getDateFormat(context)
// Check if 24-hour format is preferred
val is24Hour = android.text.format.DateFormat.is24HourFormat(context)
- val timeFormat =
- if (is24Hour) {
- SimpleDateFormat("HH:mm", locale)
- } else {
- SimpleDateFormat("hh:mm a", locale)
- }
+ val timeFormat = android.text.format.DateFormat.getTimeFormat(context)
// State to hold selected date and time
var selectedDate by remember { mutableStateOf(dateFormat.format(calendar.time)) }
@@ -205,12 +191,9 @@ fun EditWaypointDialog(
DatePickerDialog(
context,
{ _: DatePicker, selectedYear: Int, selectedMonth: Int, selectedDay: Int ->
- selectedDate = "$selectedDay/${selectedMonth + 1}/$selectedYear"
calendar.set(selectedYear, selectedMonth, selectedDay)
epochTime = calendar.timeInMillis
- if (epochTime != null) {
- selectedDate = dateFormat.format(calendar.time)
- }
+ selectedDate = dateFormat.format(calendar.time)
},
year,
month,
@@ -221,8 +204,6 @@ fun EditWaypointDialog(
android.app.TimePickerDialog(
context,
{ _: TimePicker, selectedHour: Int, selectedMinute: Int ->
- selectedTime =
- String.format(Locale.getDefault(), "%02d:%02d", selectedHour, selectedMinute)
calendar.set(Calendar.HOUR_OF_DAY, selectedHour)
calendar.set(Calendar.MINUTE, selectedMinute)
epochTime = calendar.timeInMillis
diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt
index 5494020ed..2c301302e 100644
--- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt
+++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapView.kt
@@ -92,7 +92,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
-import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.mpsToKmph
import org.meshtastic.core.model.util.mpsToMph
@@ -109,6 +108,7 @@ import org.meshtastic.core.strings.timestamp
import org.meshtastic.core.strings.track_point
import org.meshtastic.core.ui.component.NodeChip
import org.meshtastic.core.ui.theme.TracerouteColors
+import org.meshtastic.core.ui.util.formatAgo
import org.meshtastic.core.ui.util.formatPositionTime
import org.meshtastic.feature.map.component.ClusterItemsListDialog
import org.meshtastic.feature.map.component.CustomMapLayersSheet
@@ -124,7 +124,6 @@ import org.meshtastic.proto.MeshProtos.Position
import org.meshtastic.proto.MeshProtos.Waypoint
import org.meshtastic.proto.copy
import org.meshtastic.proto.waypoint
-import java.text.DateFormat
import kotlin.math.abs
import kotlin.math.max
@@ -487,9 +486,6 @@ fun MapView(
?.let { focusedNode ->
sortedPositions.forEachIndexed { index, position ->
val markerState = rememberUpdatedMarkerState(position = position.toLatLng())
- val dateFormat = remember {
- DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
- }
val alpha = (index.toFloat() / (sortedPositions.size.toFloat() - 1))
val color = Color(focusedNode.colors.second).copy(alpha = alpha)
if (index == sortedPositions.lastIndex) {
@@ -501,11 +497,7 @@ fun MapView(
snippet = formatAgo(position.time),
zIndex = alpha,
infoContent = {
- PositionInfoWindowContent(
- position = position,
- dateFormat = dateFormat,
- displayUnits = displayUnits,
- )
+ PositionInfoWindowContent(position = position, displayUnits = displayUnits)
},
) {
Icon(
@@ -759,11 +751,7 @@ fun Uri.getFileName(context: android.content.Context): String {
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Suppress("LongMethod")
-private fun PositionInfoWindowContent(
- position: Position,
- dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM),
- displayUnits: DisplayUnits = DisplayUnits.METRIC,
-) {
+private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayUnits = DisplayUnits.METRIC) {
@Composable
fun PositionRow(label: String, value: String) {
Row(modifier = Modifier.padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) {
@@ -796,7 +784,7 @@ private fun PositionInfoWindowContent(
value = "%.0f°".format(position.groundTrack * HEADING_DEG),
)
- PositionRow(label = stringResource(Res.string.timestamp), value = position.formatPositionTime(dateFormat))
+ PositionRow(label = stringResource(Res.string.timestamp), value = position.formatPositionTime())
}
}
}
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 d8a588f73..cde9825ab 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
@@ -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,7 +14,6 @@
* 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 android.app.DatePickerDialog
@@ -77,9 +76,7 @@ import org.meshtastic.core.strings.waypoint_new
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.proto.MeshProtos.Waypoint
import org.meshtastic.proto.copy
-import java.text.SimpleDateFormat
import java.util.Calendar
-import java.util.Locale
import java.util.TimeZone
@OptIn(ExperimentalMaterial3Api::class)
@@ -108,22 +105,8 @@ fun EditWaypointDialog(
mutableStateOf(waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE)
}
- val locale = Locale.getDefault()
- val dateFormat = remember {
- if (locale.country.equals("US", ignoreCase = true)) {
- SimpleDateFormat("MM/dd/yyyy", locale)
- } else {
- SimpleDateFormat("dd/MM/yyyy", locale)
- }
- }
- val timeFormat = remember {
- val is24Hour = android.text.format.DateFormat.is24HourFormat(context)
- if (is24Hour) {
- SimpleDateFormat("HH:mm", locale)
- } else {
- SimpleDateFormat("hh:mm a", locale)
- }
- }
+ val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
+ val timeFormat = remember { android.text.format.DateFormat.getTimeFormat(context) }
dateFormat.timeZone = TimeZone.getDefault()
timeFormat.timeZone = TimeZone.getDefault()
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt
index e46a9aac1..64c333cfd 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LastHeardInfo.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,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.node.component
import androidx.compose.material3.MaterialTheme
@@ -25,11 +24,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import org.jetbrains.compose.resources.stringResource
-import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.node_sort_last_heard
import org.meshtastic.core.ui.R
import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.core.ui.util.formatAgo
@Composable
fun LastHeardInfo(
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt
index edc0e5a0d..84a6084e2 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.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,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.node.component
import android.content.ActivityNotFoundException
@@ -37,7 +36,6 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.util.GPSFormat
-import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.model.util.metersIn
import org.meshtastic.core.model.util.toString
import org.meshtastic.core.strings.Res
@@ -46,6 +44,7 @@ import org.meshtastic.core.strings.last_position_update
import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.component.icon
import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.core.ui.util.formatAgo
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits
import java.net.URLEncoder
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
index 9d1314465..1159f445d 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt
@@ -49,7 +49,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.database.model.Node
-import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.details
@@ -61,6 +60,7 @@ import org.meshtastic.core.strings.role
import org.meshtastic.core.strings.short_name
import org.meshtastic.core.strings.uptime
import org.meshtastic.core.strings.user_id
+import org.meshtastic.core.ui.util.formatAgo
@Composable
fun NodeDetailsSection(node: Node, modifier: Modifier = Modifier) {
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt
index 07086c27f..d1916722c 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/PositionLog.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,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.node.metrics
import android.app.Activity
@@ -49,7 +48,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -79,7 +77,6 @@ import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.formatPositionTime
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits
import org.meshtastic.proto.MeshProtos
-import java.text.DateFormat
@Composable
private fun RowScope.PositionText(text: String, weight: Float) {
@@ -116,7 +113,7 @@ const val DEG_D = 1e-7
const val HEADING_DEG = 1e-5
@Composable
-fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateFormat: DateFormat, system: DisplayUnits) {
+fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, system: DisplayUnits) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -129,7 +126,7 @@ fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateForma
PositionText("${position.groundSpeed} Km/h", WEIGHT_15)
PositionText("%.0f°".format(position.groundTrack * HEADING_DEG), WEIGHT_15)
}
- PositionText(position.formatPositionTime(dateFormat), WEIGHT_40)
+ PositionText(position.formatPositionTime(), WEIGHT_40)
}
}
@@ -233,10 +230,8 @@ private fun ColumnScope.PositionList(
positions: List,
displayUnits: DisplayUnits,
) {
- val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) }
-
LazyColumn(modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
- items(positions) { position -> PositionItem(compactWidth, position, dateFormat, displayUnits) }
+ items(positions) { position -> PositionItem(compactWidth, position, displayUnits) }
}
}
@@ -255,14 +250,7 @@ private val testPosition =
@Preview(showBackground = true)
@Composable
private fun PositionItemPreview() {
- AppTheme {
- PositionItem(
- compactWidth = false,
- position = testPosition,
- dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM),
- system = DisplayUnits.METRIC,
- )
- }
+ AppTheme { PositionItem(compactWidth = false, position = testPosition, system = DisplayUnits.METRIC) }
}
@PreviewScreenSizes
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
index a77af41fd..edd92dab3 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.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,9 +14,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.node.metrics
+import android.text.format.DateUtils
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
@@ -52,6 +52,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@@ -88,7 +89,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.proto.MeshProtos
-import java.text.DateFormat
private data class TracerouteDialog(
val message: AnnotatedString,
@@ -107,12 +107,12 @@ fun TracerouteLogScreen(
onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit = { _, _ -> },
) {
val state by viewModel.state.collectAsStateWithLifecycle()
- val dateFormat = remember { DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) }
fun getUsername(nodeNum: Int): String = with(viewModel.getUser(nodeNum)) { "$longName ($shortName)" }
var showDialog by remember { mutableStateOf(null) }
var errorMessageRes by remember { mutableStateOf(null) }
+ val context = LocalContext.current
TracerouteLogDialogs(
dialog = showDialog,
@@ -150,7 +150,12 @@ fun TracerouteLogScreen(
}
val route = remember(result) { result?.fromRadio?.packet?.fullRouteDiscovery }
- val time = dateFormat.format(log.received_date)
+ val time =
+ DateUtils.formatDateTime(
+ context,
+ log.received_date,
+ DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
+ )
val (text, icon) = route.getTextAndIcon()
var expanded by remember { mutableStateOf(false) }
@@ -359,8 +364,11 @@ fun annotateTraceroute(inString: String?): AnnotatedString {
@PreviewLightDark
@Composable
private fun TracerouteItemPreview() {
- val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
- AppTheme {
- TracerouteItem(icon = Icons.Default.Group, text = "${dateFormat.format(System.currentTimeMillis())} - Direct")
- }
+ val time =
+ DateUtils.formatDateTime(
+ LocalContext.current,
+ System.currentTimeMillis(),
+ DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
+ )
+ AppTheme { TracerouteItem(icon = Icons.Default.Group, text = "$time - Direct") }
}
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
index 97ba58510..327d888b7 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
+++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/Debug.kt
@@ -87,11 +87,14 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_clear
import org.meshtastic.core.strings.debug_clear_logs_confirm
import org.meshtastic.core.strings.debug_decoded_payload
+import org.meshtastic.core.strings.debug_default_search
import org.meshtastic.core.strings.debug_export_failed
import org.meshtastic.core.strings.debug_export_success
import org.meshtastic.core.strings.debug_filters
import org.meshtastic.core.strings.debug_logs_export
import org.meshtastic.core.strings.debug_panel
+import org.meshtastic.core.strings.debug_store_logs_summary
+import org.meshtastic.core.strings.debug_store_logs_title
import org.meshtastic.core.strings.log_retention_days
import org.meshtastic.core.strings.log_retention_days_quantity
import org.meshtastic.core.strings.log_retention_days_summary
@@ -250,11 +253,11 @@ private fun DebugLogSettings(viewModel: DebugViewModel) {
)
SwitchPreference(
- title = "Store mesh logs",
+ title = stringResource(Res.string.debug_store_logs_title),
enabled = true,
checked = loggingEnabled,
onCheckedChange = { viewModel.setLoggingEnabled(it) },
- summary = "Disable to skip writing mesh logs to disk",
+ summary = stringResource(Res.string.debug_store_logs_summary),
)
}
}
@@ -691,7 +694,7 @@ private fun DebugScreenEmptyPreview() {
value = "",
onValueChange = {},
modifier = Modifier.weight(1f).padding(end = 8.dp),
- placeholder = { Text("Search in logs...") },
+ placeholder = { Text(stringResource(Res.string.debug_default_search)) },
singleLine = true,
)
TextButton(onClick = {}) {
diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
index c4c72cc62..98d8cf757 100644
--- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt
+++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.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,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package org.meshtastic.feature.settings.debugging
import androidx.compose.foundation.background
@@ -59,8 +58,10 @@ import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_active_filters
import org.meshtastic.core.strings.debug_filter_add
+import org.meshtastic.core.strings.debug_filter_add_custom
import org.meshtastic.core.strings.debug_filter_clear
import org.meshtastic.core.strings.debug_filter_included
+import org.meshtastic.core.strings.debug_filter_preset_title
import org.meshtastic.core.strings.debug_filters
import org.meshtastic.core.strings.match_all
import org.meshtastic.core.strings.match_any
@@ -79,7 +80,7 @@ fun DebugCustomFilterInput(
value = customFilterText,
onValueChange = onCustomFilterTextChange,
modifier = Modifier.weight(1f),
- placeholder = { Text("Add custom filter") },
+ placeholder = { Text(stringResource(Res.string.debug_filter_add_custom)) },
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions =
@@ -125,7 +126,7 @@ internal fun DebugPresetFilters(
}
Column(modifier = modifier) {
Text(
- text = "Preset Filters",
+ text = stringResource(Res.string.debug_filter_preset_title),
style = TextStyle(fontWeight = FontWeight.Bold),
modifier = Modifier.padding(vertical = 4.dp),
)