mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(time): Centralize time handling with kotlinx-datetime (#4545)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
da04448dee
commit
5ca2ab4695
86 changed files with 993 additions and 663 deletions
|
|
@ -35,8 +35,9 @@ import kotlinx.coroutines.launch
|
|||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
/**
|
||||
* The main application class for Meshtastic.
|
||||
|
|
@ -67,11 +68,7 @@ class MeshUtilApplication :
|
|||
|
||||
private fun scheduleMeshLogCleanup() {
|
||||
val cleanupRequest =
|
||||
PeriodicWorkRequestBuilder<MeshLogCleanupWorker>(
|
||||
repeatInterval = 1,
|
||||
repeatIntervalTimeUnit = TimeUnit.HOURS,
|
||||
)
|
||||
.build()
|
||||
PeriodicWorkRequestBuilder<MeshLogCleanupWorker>(repeatInterval = 1.hours.toJavaDuration()).build()
|
||||
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.geeksville.mesh.concurrent
|
||||
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
|
||||
/** A deferred execution object (with various possible implementations) */
|
||||
interface Continuation<in T> {
|
||||
abstract fun resume(res: Result<T>)
|
||||
|
|
@ -63,10 +66,10 @@ class SyncContinuation<T> : Continuation<T> {
|
|||
fun await(timeoutMsecs: Long = 0): T {
|
||||
lock.lock()
|
||||
try {
|
||||
val startT = System.currentTimeMillis()
|
||||
val startT = nowMillis
|
||||
while (result == null) {
|
||||
if (timeoutMsecs > 0) {
|
||||
val remaining = timeoutMsecs - (System.currentTimeMillis() - startT)
|
||||
val remaining = timeoutMsecs - (nowMillis - startT)
|
||||
if (remaining <= 0) {
|
||||
throw Exception("SyncContinuation timeout")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import okio.ByteString.Companion.encodeUtf8
|
|||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.Data
|
||||
|
|
@ -146,7 +147,7 @@ constructor(
|
|||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_time = nowSeconds.toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
|
|
@ -162,7 +163,7 @@ constructor(
|
|||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_time = nowSeconds.toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
|
|
@ -177,13 +178,13 @@ constructor(
|
|||
Neighbor(
|
||||
node_id = numIn + 1,
|
||||
snr = 10.0f,
|
||||
last_rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
last_rx_time = nowSeconds.toInt(),
|
||||
node_broadcast_interval_secs = 60,
|
||||
),
|
||||
Neighbor(
|
||||
node_id = numIn + 2,
|
||||
snr = 12.0f,
|
||||
last_rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
last_rx_time = nowSeconds.toInt(),
|
||||
node_broadcast_interval_secs = 60,
|
||||
),
|
||||
),
|
||||
|
|
@ -200,7 +201,7 @@ constructor(
|
|||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_time = nowSeconds.toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
|
|
@ -210,7 +211,7 @@ constructor(
|
|||
latitude_i = org.meshtastic.core.model.Position.degI(32.776665),
|
||||
longitude_i = org.meshtastic.core.model.Position.degI(-96.796989),
|
||||
altitude = 150,
|
||||
time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
time = nowSeconds.toInt(),
|
||||
precision_bits = 15,
|
||||
)
|
||||
.encode()
|
||||
|
|
@ -225,14 +226,14 @@ constructor(
|
|||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_time = nowSeconds.toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.TELEMETRY_APP,
|
||||
payload =
|
||||
Telemetry(
|
||||
time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
time = nowSeconds.toInt(),
|
||||
device_metrics =
|
||||
DeviceMetrics(
|
||||
battery_level = 85,
|
||||
|
|
@ -254,7 +255,7 @@ constructor(
|
|||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_time = nowSeconds.toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
|
|
@ -271,7 +272,7 @@ constructor(
|
|||
id = packetIdSequence.next(),
|
||||
from = fromIn,
|
||||
to = toIn,
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_time = nowSeconds.toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded = data,
|
||||
),
|
||||
|
|
@ -326,7 +327,7 @@ constructor(
|
|||
latitude_i = org.meshtastic.core.model.Position.degI(lat),
|
||||
longitude_i = org.meshtastic.core.model.Position.degI(lon),
|
||||
altitude = 35,
|
||||
time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
time = nowSeconds.toInt(),
|
||||
precision_bits = Random.nextInt(10, 19),
|
||||
),
|
||||
),
|
||||
|
|
@ -345,6 +346,7 @@ constructor(
|
|||
makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas
|
||||
makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson
|
||||
FromRadio(config = Config(lora = defaultLoRaConfig)),
|
||||
FromRadio(config = Config(lora = defaultLoRaConfig)),
|
||||
FromRadio(channel = defaultChannel),
|
||||
FromRadio(config_complete_id = configId),
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import no.nordicsemi.kotlin.ble.client.exception.InvalidAttributeException
|
|||
import no.nordicsemi.kotlin.ble.core.CharacteristicProperty
|
||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import java.util.UUID
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
|
@ -170,19 +171,19 @@ constructor(
|
|||
private fun connect() {
|
||||
connectionScope.launch {
|
||||
try {
|
||||
connectionStartTime = System.currentTimeMillis()
|
||||
connectionStartTime = nowMillis
|
||||
Logger.i { "[$address] BLE connection attempt started at $connectionStartTime" }
|
||||
|
||||
peripheral = retryCall { findAndConnectPeripheral() }
|
||||
peripheral?.let {
|
||||
val connectionTime = System.currentTimeMillis() - connectionStartTime
|
||||
val connectionTime = nowMillis - connectionStartTime
|
||||
Logger.i { "[$address] BLE peripheral connected in ${connectionTime}ms" }
|
||||
onConnected()
|
||||
observePeripheralChanges()
|
||||
discoverServicesAndSetupCharacteristics(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val failureTime = System.currentTimeMillis() - connectionStartTime
|
||||
val failureTime = nowMillis - connectionStartTime
|
||||
// BLE connection errors are common and often transient
|
||||
Logger.w(e) { "[$address] Failed to connect to peripheral after ${failureTime}ms" }
|
||||
service.onDisconnect(BleError.from(e))
|
||||
|
|
@ -230,7 +231,7 @@ constructor(
|
|||
|
||||
val uptime =
|
||||
if (connectionStartTime > 0) {
|
||||
System.currentTimeMillis() - connectionStartTime
|
||||
nowMillis - connectionStartTime
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
@ -417,7 +418,7 @@ constructor(
|
|||
runBlocking {
|
||||
val uptime =
|
||||
if (connectionStartTime > 0) {
|
||||
System.currentTimeMillis() - connectionStartTime
|
||||
nowMillis - connectionStartTime
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
|||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.di.ProcessLifecycle
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.prefs.radio.RadioPrefs
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
|
|
@ -146,7 +147,7 @@ constructor(
|
|||
|
||||
private var lastHeartbeatMillis = 0L
|
||||
|
||||
fun keepAlive(now: Long = System.currentTimeMillis()) {
|
||||
fun keepAlive(now: Long = nowMillis) {
|
||||
if (now - lastHeartbeatMillis > HEARTBEAT_INTERVAL_MILLIS) {
|
||||
if (radioIf is SerialInterface) {
|
||||
Logger.i { "Sending ToRadio heartbeat" }
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.geeksville.mesh.repository.usb.SerialConnectionListener
|
|||
import com.geeksville.mesh.repository.usb.UsbRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/** An interface that assumes we are talking to a meshtastic device via USB serial */
|
||||
|
|
@ -49,7 +50,7 @@ constructor(
|
|||
if (device == null) {
|
||||
Logger.e { "[$address] Serial device not found at address" }
|
||||
} else {
|
||||
val connectStart = System.currentTimeMillis()
|
||||
val connectStart = nowMillis
|
||||
Logger.i { "[$address] Opening serial device: $device" }
|
||||
|
||||
var packetsReceived = 0
|
||||
|
|
@ -57,7 +58,7 @@ constructor(
|
|||
var connectionStartTime = 0L
|
||||
|
||||
val onConnect: () -> Unit = {
|
||||
connectionStartTime = System.currentTimeMillis()
|
||||
connectionStartTime = nowMillis
|
||||
val connectionTime = connectionStartTime - connectStart
|
||||
Logger.i { "[$address] Serial device connected in ${connectionTime}ms" }
|
||||
super.connect()
|
||||
|
|
@ -90,7 +91,7 @@ constructor(
|
|||
override fun onDisconnected(thrown: Exception?) {
|
||||
val uptime =
|
||||
if (connectionStartTime > 0) {
|
||||
System.currentTimeMillis() - connectionStartTime
|
||||
nowMillis - connectionStartTime
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import dagger.assisted.AssistedInject
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import java.io.BufferedInputStream
|
||||
|
|
@ -104,7 +105,7 @@ constructor(
|
|||
if (s != null) {
|
||||
val uptime =
|
||||
if (connectionStartTime > 0) {
|
||||
System.currentTimeMillis() - connectionStartTime
|
||||
nowMillis - connectionStartTime
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
@ -130,7 +131,7 @@ constructor(
|
|||
} catch (ex: IOException) {
|
||||
val uptime =
|
||||
if (connectionStartTime > 0) {
|
||||
System.currentTimeMillis() - connectionStartTime
|
||||
nowMillis - connectionStartTime
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
@ -140,7 +141,7 @@ constructor(
|
|||
} catch (ex: Throwable) {
|
||||
val uptime =
|
||||
if (connectionStartTime > 0) {
|
||||
System.currentTimeMillis() - connectionStartTime
|
||||
nowMillis - connectionStartTime
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
@ -175,7 +176,7 @@ constructor(
|
|||
|
||||
// Create a socket to make the connection with the server
|
||||
private suspend fun startConnect() = withContext(dispatchers.io) {
|
||||
val attemptStart = System.currentTimeMillis()
|
||||
val attemptStart = nowMillis
|
||||
Logger.i { "[$address] TCP connection attempt starting..." }
|
||||
|
||||
val parts = address.split(":", limit = 2)
|
||||
|
|
@ -190,8 +191,8 @@ constructor(
|
|||
socket.soTimeout = SOCKET_TIMEOUT
|
||||
this@TCPInterface.socket = socket
|
||||
|
||||
val connectTime = System.currentTimeMillis() - attemptStart
|
||||
connectionStartTime = System.currentTimeMillis()
|
||||
val connectTime = nowMillis - attemptStart
|
||||
connectionStartTime = nowMillis
|
||||
Logger.i {
|
||||
"[$address] TCP socket connected in ${connectTime}ms - " +
|
||||
"Local: ${socket.localSocketAddress}, Remote: ${socket.remoteSocketAddress}"
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ import com.geeksville.mesh.util.ignoreException
|
|||
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
||||
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||
import com.hoho.android.usbserial.util.SerialInputOutputManager
|
||||
import org.meshtastic.core.model.util.await
|
||||
import java.nio.BufferOverflowException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class SerialConnectionImpl(
|
||||
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
|
||||
|
|
@ -62,7 +63,7 @@ internal class SerialConnectionImpl(
|
|||
// Allow a short amount of time for the manager to quit (so the port can be cleanly closed)
|
||||
if (waitForStopped) {
|
||||
Logger.d { "Waiting for USB manager to stop..." }
|
||||
closedLatch.await(1, TimeUnit.SECONDS)
|
||||
closedLatch.await(1.seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ class MarkAsReadReceiver : BroadcastReceiver() {
|
|||
val pendingResult = goAsync()
|
||||
scope.launch {
|
||||
try {
|
||||
packetRepository.clearUnreadCount(contactKey, System.currentTimeMillis())
|
||||
packetRepository.clearUnreadCount(contactKey, nowMillis)
|
||||
meshServiceNotifications.cancelMessageNotification(contactKey)
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import org.meshtastic.core.database.entity.ReactionEntity
|
|||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
|
|
@ -160,7 +161,7 @@ constructor(
|
|||
replyId = action.replyId,
|
||||
userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL,
|
||||
emoji = action.emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
timestamp = nowMillis,
|
||||
snr = 0f,
|
||||
rssi = 0,
|
||||
hopsAway = 0,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import org.meshtastic.core.model.MessageStatus
|
|||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.isWithinSizeLimit
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
|
|
@ -64,7 +66,7 @@ constructor(
|
|||
private val radioConfigRepository: RadioConfigRepository?,
|
||||
) {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val currentPacketId = AtomicLong(java.util.Random(System.currentTimeMillis()).nextLong().absoluteValue)
|
||||
private val currentPacketId = AtomicLong(java.util.Random(nowMillis).nextLong().absoluteValue)
|
||||
private val sessionPasskey = AtomicReference(ByteString.EMPTY)
|
||||
private val offlineSentPackets = CopyOnWriteArrayList<DataPacket>()
|
||||
val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
|
||||
|
|
@ -175,7 +177,7 @@ constructor(
|
|||
emoji = p.emoji,
|
||||
),
|
||||
)
|
||||
p.time = System.currentTimeMillis()
|
||||
p.time = nowMillis
|
||||
packetHandler?.sendToRadio(meshPacket)
|
||||
}
|
||||
|
||||
|
|
@ -240,7 +242,7 @@ constructor(
|
|||
latitude_i = Position.degI(currentPosition.latitude),
|
||||
longitude_i = Position.degI(currentPosition.longitude),
|
||||
altitude = currentPosition.altitude,
|
||||
time = (System.currentTimeMillis() / TIME_MS_TO_S).toInt(),
|
||||
time = nowSeconds.toInt(),
|
||||
)
|
||||
packetHandler?.sendToRadio(
|
||||
buildMeshPacket(
|
||||
|
|
@ -292,7 +294,7 @@ constructor(
|
|||
}
|
||||
|
||||
fun requestTraceroute(requestId: Int, destNum: Int) {
|
||||
tracerouteStartTimes[requestId] = System.currentTimeMillis()
|
||||
tracerouteStartTimes[requestId] = nowMillis
|
||||
packetHandler?.sendToRadio(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
|
|
@ -343,7 +345,7 @@ constructor(
|
|||
}
|
||||
|
||||
fun requestNeighborInfo(requestId: Int, destNum: Int) {
|
||||
neighborInfoStartTimes[requestId] = System.currentTimeMillis()
|
||||
neighborInfoStartTimes[requestId] = nowMillis
|
||||
val myNum = nodeManager?.myNodeNum ?: 0
|
||||
if (destNum == myNum) {
|
||||
val neighborInfoToSend =
|
||||
|
|
@ -360,7 +362,7 @@ constructor(
|
|||
Neighbor(
|
||||
node_id = 0, // Dummy node ID that can be intercepted
|
||||
snr = 0f,
|
||||
last_rx_time = (System.currentTimeMillis() / TIME_MS_TO_S).toInt(),
|
||||
last_rx_time = nowSeconds.toInt(),
|
||||
node_broadcast_interval_secs = oneHour,
|
||||
),
|
||||
),
|
||||
|
|
@ -471,7 +473,6 @@ constructor(
|
|||
companion object {
|
||||
private const val PACKET_ID_MASK = 0xffffffffL
|
||||
private const val PACKET_ID_SHIFT_BITS = 32
|
||||
private const val TIME_MS_TO_S = 1000L
|
||||
|
||||
private const val ADMIN_CHANNEL_NAME = "admin"
|
||||
private const val NODE_ID_PREFIX = "!"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import org.meshtastic.core.analytics.DataPair
|
|||
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
|
|
@ -48,7 +50,9 @@ import org.meshtastic.proto.Telemetry
|
|||
import org.meshtastic.proto.ToRadio
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@Singleton
|
||||
|
|
@ -140,7 +144,7 @@ constructor(
|
|||
}
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
Logger.d { "Starting connect" }
|
||||
connectTimeMsec = System.currentTimeMillis()
|
||||
connectTimeMsec = nowMillis
|
||||
scope.handledLaunch { nodeRepository.clearMyNodeInfo() }
|
||||
startConfigOnly()
|
||||
}
|
||||
|
|
@ -152,12 +156,12 @@ constructor(
|
|||
mqttManager.stop()
|
||||
|
||||
if (connectTimeMsec != 0L) {
|
||||
val now = System.currentTimeMillis()
|
||||
val now = nowMillis
|
||||
val duration = now - connectTimeMsec
|
||||
connectTimeMsec = 0L
|
||||
analytics.track(
|
||||
EVENT_CONNECTED_SECONDS,
|
||||
DataPair(EVENT_CONNECTED_SECONDS, duration / MILLISECONDS_IN_SECOND),
|
||||
DataPair(EVENT_CONNECTED_SECONDS, duration.milliseconds.toDouble(DurationUnit.SECONDS)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -207,9 +211,7 @@ constructor(
|
|||
|
||||
val myNodeNum = nodeManager.myNodeNum ?: 0
|
||||
// Set time
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
AdminMessage(set_time_only = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt())
|
||||
}
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_time_only = nowSeconds.toInt()) }
|
||||
}
|
||||
|
||||
fun onNodeDbReady() {
|
||||
|
|
@ -268,7 +270,6 @@ constructor(
|
|||
companion object {
|
||||
private const val CONFIG_ONLY_NONCE = 69420
|
||||
private const val NODE_INFO_NONCE = 69421
|
||||
private const val MILLISECONDS_IN_SECOND = 1000.0
|
||||
private const val DEVICE_SLEEP_TIMEOUT_SECONDS = 30
|
||||
|
||||
private const val EVENT_CONNECTED_SECONDS = "connected_seconds"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ import org.meshtastic.core.model.DataPacket
|
|||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.SfppHasher
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.toOneLiner
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
|
|
@ -325,7 +327,7 @@ constructor(
|
|||
val payload = packet.decoded?.payload ?: return
|
||||
val u = Waypoint.ADAPTER.decode(payload)
|
||||
if (u.locked_to != 0 && u.locked_to != packet.from) return
|
||||
val currentSecond = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt()
|
||||
val currentSecond = nowSeconds.toInt()
|
||||
rememberDataPacket(dataPacket, myNodeNum, updateNotification = u.expire > currentSecond)
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +445,7 @@ constructor(
|
|||
isRemote -> shouldDisplay = true
|
||||
}
|
||||
if (shouldDisplay) {
|
||||
val now = System.currentTimeMillis() / MILLISECONDS_IN_SECOND
|
||||
val now = nowSeconds
|
||||
if (!batteryPercentCooldowns.containsKey(fromNum)) batteryPercentCooldowns[fromNum] = 0L
|
||||
if ((now - batteryPercentCooldowns[fromNum]!!) >= BATTERY_PERCENT_COOLDOWN_SECONDS || forceDisplay) {
|
||||
batteryPercentCooldowns[fromNum] = now
|
||||
|
|
@ -461,7 +463,7 @@ constructor(
|
|||
}
|
||||
handleAckNak(
|
||||
packet.decoded?.request_id ?: 0,
|
||||
dataMapper.toNodeID(packet.from),
|
||||
nodeManager.toNodeID(packet.from),
|
||||
r.error_reason?.value ?: 0,
|
||||
dataPacket.relayNode,
|
||||
)
|
||||
|
|
@ -599,7 +601,7 @@ constructor(
|
|||
packetId = dataPacket.id,
|
||||
port_num = dataPacket.dataType,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = fromLocal || isFiltered,
|
||||
data = dataPacket,
|
||||
snr = dataPacket.snr,
|
||||
|
|
@ -693,8 +695,8 @@ constructor(
|
|||
private fun rememberReaction(packet: MeshPacket) = scope.handledLaunch {
|
||||
val decoded = packet.decoded ?: return@handledLaunch
|
||||
val emoji = decoded.payload.toByteArray().decodeToString()
|
||||
val fromId = dataMapper.toNodeID(packet.from)
|
||||
val toId = dataMapper.toNodeID(packet.to)
|
||||
val fromId = nodeManager.toNodeID(packet.from)
|
||||
val toId = nodeManager.toNodeID(packet.to)
|
||||
|
||||
val reaction =
|
||||
ReactionEntity(
|
||||
|
|
@ -702,7 +704,7 @@ constructor(
|
|||
replyId = decoded.reply_id,
|
||||
userId = fromId,
|
||||
emoji = emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
timestamp = nowMillis,
|
||||
snr = packet.rx_snr,
|
||||
rssi = packet.rx_rssi,
|
||||
hopsAway =
|
||||
|
|
@ -790,7 +792,6 @@ constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val MILLISECONDS_IN_SECOND = 1000L
|
||||
private const val HOPS_AWAY_UNAVAILABLE = -1
|
||||
|
||||
private const val BATTERY_PERCENT_UNSUPPORTED = 0.0
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.onEach
|
|||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.util.isLora
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.LogRecord
|
||||
|
|
@ -41,7 +43,6 @@ import java.util.UUID
|
|||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Singleton
|
||||
|
|
@ -126,7 +127,7 @@ constructor(
|
|||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = type,
|
||||
received_date = System.currentTimeMillis(),
|
||||
received_date = nowMillis,
|
||||
raw_message = message,
|
||||
fromRadio = proto,
|
||||
),
|
||||
|
|
@ -136,7 +137,7 @@ constructor(
|
|||
fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) {
|
||||
val rxTime =
|
||||
if (packet.rx_time == 0) {
|
||||
(System.currentTimeMillis().milliseconds.inWholeSeconds).toInt()
|
||||
nowSeconds.toInt()
|
||||
} else {
|
||||
packet.rx_time
|
||||
}
|
||||
|
|
@ -186,7 +187,7 @@ constructor(
|
|||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = "Packet",
|
||||
received_date = System.currentTimeMillis(),
|
||||
received_date = nowMillis,
|
||||
raw_message = packet.toString(),
|
||||
fromNum = packet.from,
|
||||
portNum = decoded.portnum.value,
|
||||
|
|
@ -201,9 +202,7 @@ constructor(
|
|||
myNodeNum?.let { myNum ->
|
||||
val from = packet.from
|
||||
val isOtherNode = myNum != from
|
||||
nodeManager.updateNodeInfo(myNum, withBroadcast = isOtherNode) {
|
||||
it.lastHeard = (System.currentTimeMillis().milliseconds.inWholeSeconds).toInt()
|
||||
}
|
||||
nodeManager.updateNodeInfo(myNum, withBroadcast = isOtherNode) { it.lastHeard = nowSeconds.toInt() }
|
||||
nodeManager.updateNodeInfo(from, withBroadcast = false, channel = packet.channel) {
|
||||
it.lastHeard = packet.rx_time
|
||||
it.viaMqtt = packet.via_mqtt == true
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.meshtastic.core.strings.getString
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
|
|
@ -74,7 +75,7 @@ constructor(
|
|||
|
||||
val responseText =
|
||||
if (start != null) {
|
||||
val elapsedMs = System.currentTimeMillis() - start
|
||||
val elapsedMs = nowMillis - start
|
||||
val seconds = elapsedMs / MILLIS_PER_SECOND
|
||||
Logger.i { "Neighbor info $requestId complete in $seconds s" }
|
||||
String.format(Locale.US, "%s\n\nDuration: %.1f s", formatted, seconds)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import org.meshtastic.core.model.DataPacket
|
|||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
|
|
@ -186,12 +187,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleReceivedPosition(
|
||||
fromNum: Int,
|
||||
myNodeNum: Int,
|
||||
p: ProtoPosition,
|
||||
defaultTime: Long = System.currentTimeMillis(),
|
||||
) {
|
||||
fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long = nowMillis) {
|
||||
if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) {
|
||||
Logger.d { "Ignoring nop position update for the local node" }
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import org.meshtastic.core.database.entity.NodeEntity
|
|||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.SERVICE_NOTIFY_ID
|
||||
|
|
@ -78,6 +79,8 @@ import org.meshtastic.proto.DeviceMetrics
|
|||
import org.meshtastic.proto.LocalStats
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Manages the creation and display of all app notifications.
|
||||
|
|
@ -86,6 +89,7 @@ import javax.inject.Inject
|
|||
* notifications for various events like new messages, alerts, and service status changes.
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
@Singleton
|
||||
class MeshServiceNotificationsImpl
|
||||
@Inject
|
||||
constructor(
|
||||
|
|
@ -97,7 +101,6 @@ constructor(
|
|||
private val notificationManager = context.getSystemService<NotificationManager>()!!
|
||||
|
||||
companion object {
|
||||
private const val FIFTEEN_MINUTES_IN_MILLIS = 15L * 60 * 1000
|
||||
const val MAX_BATTERY_LEVEL = 100
|
||||
private val NOTIFICATION_LIGHT_COLOR = Color.BLUE
|
||||
private const val MAX_HISTORY_MESSAGES = 10
|
||||
|
|
@ -107,6 +110,8 @@ constructor(
|
|||
private const val SUMMARY_ID = 1
|
||||
private const val PERSON_ICON_SIZE = 128
|
||||
private const val PERSON_ICON_TEXT_SIZE_RATIO = 0.5f
|
||||
private const val STATS_UPDATE_MINUTES = 15
|
||||
private val STATS_UPDATE_INTERVAL = STATS_UPDATE_MINUTES.minutes
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -279,7 +284,7 @@ constructor(
|
|||
hasLocalStats -> {
|
||||
val localStatsMessage = telemetry?.local_stats?.formatToString()
|
||||
cachedTelemetry = telemetry
|
||||
nextStatsUpdateMillis = System.currentTimeMillis() + FIFTEEN_MINUTES_IN_MILLIS
|
||||
nextStatsUpdateMillis = nowMillis + STATS_UPDATE_INTERVAL.inWholeMilliseconds
|
||||
localStatsMessage
|
||||
}
|
||||
cachedTelemetry == null && hasDeviceMetrics -> {
|
||||
|
|
@ -287,7 +292,7 @@ constructor(
|
|||
if (cachedLocalStats == null) {
|
||||
cachedTelemetry = telemetry
|
||||
}
|
||||
nextStatsUpdateMillis = System.currentTimeMillis()
|
||||
nextStatsUpdateMillis = nowMillis
|
||||
deviceMetricsMessage
|
||||
}
|
||||
else -> null
|
||||
|
|
@ -471,7 +476,7 @@ constructor(
|
|||
}
|
||||
|
||||
nextUpdateAt
|
||||
?.takeIf { it > System.currentTimeMillis() }
|
||||
?.takeIf { it > nowMillis }
|
||||
?.let {
|
||||
builder.setWhen(it)
|
||||
builder.setUsesChronometer(true)
|
||||
|
|
@ -579,7 +584,7 @@ constructor(
|
|||
isSilent: Boolean,
|
||||
): Notification {
|
||||
val person = Person.Builder().setName(name).build()
|
||||
val style = NotificationCompat.MessagingStyle(person).addMessage(message, System.currentTimeMillis(), person)
|
||||
val style = NotificationCompat.MessagingStyle(person).addMessage(message, nowMillis, person)
|
||||
|
||||
val builder =
|
||||
commonBuilder(NotificationType.Waypoint, createOpenWaypointIntent(waypointId))
|
||||
|
|
@ -588,7 +593,7 @@ constructor(
|
|||
.setStyle(style)
|
||||
.setGroup(GROUP_KEY_MESSAGES)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setWhen(nowMillis)
|
||||
.setShowWhen(true)
|
||||
|
||||
if (isSilent) {
|
||||
|
|
@ -600,7 +605,7 @@ constructor(
|
|||
|
||||
private fun createAlertNotification(contactKey: String, name: String, alert: String): Notification {
|
||||
val person = Person.Builder().setName(name).build()
|
||||
val style = NotificationCompat.MessagingStyle(person).addMessage(alert, System.currentTimeMillis(), person)
|
||||
val style = NotificationCompat.MessagingStyle(person).addMessage(alert, nowMillis, person)
|
||||
|
||||
return commonBuilder(NotificationType.Alert, createOpenMessageIntent(contactKey))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
|
@ -617,7 +622,7 @@ constructor(
|
|||
.setCategory(Notification.CATEGORY_STATUS)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(title)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setWhen(nowMillis)
|
||||
.setShowWhen(true)
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
|
|
@ -639,7 +644,7 @@ constructor(
|
|||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setWhen(nowMillis)
|
||||
.setShowWhen(true)
|
||||
.build()
|
||||
}
|
||||
|
|
@ -738,7 +743,7 @@ constructor(
|
|||
Intent(context, ReactionReceiver::class.java).apply {
|
||||
action = REACT_ACTION
|
||||
putExtra(ReactionReceiver.EXTRA_CONTACT_KEY, contactKey)
|
||||
putExtra(ReactionReceiver.EXTRA_PACKET_ID, packetId)
|
||||
putExtra(ReactionReceiver.EXTRA_REPLY_ID, packetId)
|
||||
putExtra(ReactionReceiver.EXTRA_TO_ID, toId)
|
||||
putExtra(ReactionReceiver.EXTRA_CHANNEL_INDEX, channelIndex)
|
||||
putExtra(ReactionReceiver.EXTRA_EMOJI, "👍")
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import org.meshtastic.core.data.repository.NodeRepository
|
|||
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getFullTracerouteResponse
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.service.TracerouteResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
|
|
@ -83,8 +84,8 @@ constructor(
|
|||
val start = commandSender.tracerouteStartTimes.remove(requestId)
|
||||
val responseText =
|
||||
if (start != null) {
|
||||
val elapsedMs = System.currentTimeMillis() - start
|
||||
val seconds = elapsedMs / MILLISECONDS_IN_SECOND
|
||||
val elapsedMs = nowMillis - start
|
||||
val seconds = elapsedMs / MILLIS_PER_SECOND
|
||||
Logger.i { "Traceroute $requestId complete in $seconds s" }
|
||||
val durationText = getString(Res.string.traceroute_duration, "%.1f".format(Locale.US, seconds))
|
||||
"$full\n\n$durationText"
|
||||
|
|
@ -108,6 +109,6 @@ constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val MILLISECONDS_IN_SECOND = 1000.0
|
||||
private const val MILLIS_PER_SECOND = 1000.0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import org.meshtastic.core.data.repository.PacketRepository
|
|||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.toOneLineString
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
|
|
@ -45,6 +46,8 @@ import java.util.concurrent.ConcurrentHashMap
|
|||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Singleton
|
||||
|
|
@ -59,7 +62,7 @@ constructor(
|
|||
) {
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MS = 5000L // Increased from 250ms to be more tolerant
|
||||
private val TIMEOUT = 5.seconds // Increased from 250ms to be more tolerant
|
||||
}
|
||||
|
||||
private var queueJob: Job? = null
|
||||
|
|
@ -89,7 +92,7 @@ constructor(
|
|||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = "Packet",
|
||||
received_date = System.currentTimeMillis(),
|
||||
received_date = nowMillis,
|
||||
raw_message = packet.toString(),
|
||||
fromNum = packet.from ?: 0,
|
||||
portNum = packet.decoded?.portnum?.value ?: 0,
|
||||
|
|
@ -148,7 +151,7 @@ constructor(
|
|||
// send packet to the radio and wait for response
|
||||
val response = sendPacket(packet)
|
||||
Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" }
|
||||
val success = withTimeout(TIMEOUT_MS) { response.await() }
|
||||
val success = withTimeout(TIMEOUT) { response.await() }
|
||||
Logger.d { "queueJob packet id=${packet.id.toUInt()} success $success" }
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
Logger.d { "queueJob packet id=${packet.id.toUInt()} timeout" }
|
||||
|
|
@ -173,11 +176,11 @@ constructor(
|
|||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1000) {
|
||||
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1.seconds) {
|
||||
var dataPacket: DataPacket? = null
|
||||
while (dataPacket == null) {
|
||||
dataPacket = packetRepository.get().getPacketById(packetId)?.data
|
||||
if (dataPacket == null) delay(100)
|
||||
if (dataPacket == null) delay(100.milliseconds)
|
||||
}
|
||||
dataPacket
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,91 +19,48 @@ package com.geeksville.mesh.service
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ReactionReceiver : BroadcastReceiver() {
|
||||
@Inject lateinit var commandSender: MeshCommandSender
|
||||
|
||||
@Inject lateinit var meshServiceNotifications: MeshServiceNotifications
|
||||
@Inject lateinit var serviceRepository: ServiceRepository
|
||||
|
||||
@Inject lateinit var packetRepository: PacketRepository
|
||||
|
||||
@Inject lateinit var nodeManager: MeshNodeManager
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
companion object {
|
||||
const val REACT_ACTION = "com.geeksville.mesh.REACT_ACTION"
|
||||
const val EXTRA_PACKET_ID = "packetId"
|
||||
const val EXTRA_EMOJI = "emoji"
|
||||
const val EXTRA_CONTACT_KEY = "contactKey"
|
||||
const val EXTRA_TO_ID = "toId"
|
||||
const val EXTRA_CHANNEL_INDEX = "channelIndex"
|
||||
private const val EMOJI_INDICATOR = 1
|
||||
}
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "ReturnCount")
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != REACT_ACTION) return
|
||||
|
||||
val pendingResult = goAsync()
|
||||
val contactKey = intent.getStringExtra(EXTRA_CONTACT_KEY) ?: return
|
||||
val reaction = intent.getStringExtra(EXTRA_EMOJI) ?: intent.getStringExtra(EXTRA_REACTION) ?: return
|
||||
val replyId = intent.getIntExtra(EXTRA_REPLY_ID, intent.getIntExtra(EXTRA_PACKET_ID, 0))
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val packetId = intent.getIntExtra(EXTRA_PACKET_ID, 0)
|
||||
val emoji = intent.getStringExtra(EXTRA_EMOJI)
|
||||
val toId = intent.getStringExtra(EXTRA_TO_ID)
|
||||
val channelIndex = intent.getIntExtra(EXTRA_CHANNEL_INDEX, 0)
|
||||
val contactKey = intent.getStringExtra(EXTRA_CONTACT_KEY)
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
if (packetId == 0 || emoji.isNullOrEmpty() || toId.isNullOrEmpty() || contactKey.isNullOrEmpty()) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Reactions are text messages with a replyId and emoji set
|
||||
val reactionPacket =
|
||||
DataPacket(
|
||||
to = toId,
|
||||
channel = channelIndex,
|
||||
bytes = emoji.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
replyId = packetId,
|
||||
wantAck = true,
|
||||
emoji = EMOJI_INDICATOR,
|
||||
)
|
||||
commandSender.sendData(reactionPacket)
|
||||
|
||||
val reaction =
|
||||
ReactionEntity(
|
||||
myNodeNum = nodeManager.myNodeNum ?: 0,
|
||||
replyId = packetId,
|
||||
userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL,
|
||||
emoji = emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
packetId = reactionPacket.id,
|
||||
status = MessageStatus.QUEUED,
|
||||
to = toId,
|
||||
channel = channelIndex,
|
||||
)
|
||||
packetRepository.insertReaction(reaction)
|
||||
|
||||
// Dismiss the notification after reacting
|
||||
meshServiceNotifications.cancelMessageNotification(contactKey)
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
serviceRepository.onServiceAction(ServiceAction.Reaction(reaction, replyId, contactKey))
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Error sending reaction" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REACT_ACTION = "com.geeksville.mesh.REACT_ACTION"
|
||||
const val EXTRA_CONTACT_KEY = "extra_contact_key"
|
||||
const val EXTRA_REACTION = "extra_reaction"
|
||||
const val EXTRA_REPLY_ID = "extra_reply_id"
|
||||
const val EXTRA_PACKET_ID = "extra_packet_id"
|
||||
const val EXTRA_TO_ID = "extra_to_id"
|
||||
const val EXTRA_CHANNEL_INDEX = "extra_channel_index"
|
||||
const val EXTRA_EMOJI = "extra_emoji"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -66,8 +67,10 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.entity.ContactSettings
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.model.util.formatMuteRemainingTime
|
||||
import org.meshtastic.core.model.util.getChannel
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.are_you_sure
|
||||
import org.meshtastic.core.strings.cancel
|
||||
|
|
@ -104,7 +107,7 @@ import org.meshtastic.core.ui.icon.VolumeUpTwoTone
|
|||
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalPermissionsApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -172,7 +175,7 @@ fun ContactsScreen(
|
|||
.filter { it.contactKey in selectedContactKeys }
|
||||
}
|
||||
// Get message count directly from repository for selected contacts
|
||||
var selectedCount by remember { mutableStateOf(0) }
|
||||
var selectedCount by remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(selectedContactKeys.size, selectedContactKeys.joinToString(",")) {
|
||||
selectedCount = viewModel.getTotalMessageCount(selectedContactKeys.toList())
|
||||
}
|
||||
|
|
@ -323,14 +326,14 @@ private fun MuteNotificationsDialog(
|
|||
val muteOptions = remember {
|
||||
listOf(
|
||||
Res.string.unmute to 0L,
|
||||
Res.string.mute_8_hours to TimeUnit.HOURS.toMillis(8),
|
||||
Res.string.mute_1_week to TimeUnit.DAYS.toMillis(7),
|
||||
Res.string.mute_8_hours to TimeConstants.EIGHT_HOURS.inWholeMilliseconds,
|
||||
Res.string.mute_1_week to 7.days.inWholeMilliseconds,
|
||||
Res.string.mute_always to Long.MAX_VALUE,
|
||||
)
|
||||
}
|
||||
|
||||
// State to hold the selected mute duration index
|
||||
var selectedOptionIndex by remember { mutableStateOf(2) } // Default to "Always"
|
||||
var selectedOptionIndex by remember { mutableIntStateOf(2) } // Default to "Always"
|
||||
|
||||
MeshtasticDialog(
|
||||
onDismiss = onDismiss, // Dismiss the dialog when clicked outside
|
||||
|
|
@ -347,7 +350,7 @@ private fun MuteNotificationsDialog(
|
|||
// Show current mute status
|
||||
selectedContactKeys.forEach { contactKey ->
|
||||
contactSettings[contactKey]?.let { settings ->
|
||||
val now = System.currentTimeMillis()
|
||||
val now = nowMillis
|
||||
val statusText =
|
||||
when {
|
||||
settings.muteUntil > 0 && settings.muteUntil != Long.MAX_VALUE -> {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
|||
freeCompilerArgs.addAll(
|
||||
// Enable experimental coroutines APIs, including Flow
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlin.time.ExperimentalTime",
|
||||
"-Xcontext-parameters",
|
||||
"-Xannotation-default-target=param-property"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ import org.meshtastic.core.database.entity.asExternalModel
|
|||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.BootloaderOtaQuirk
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -210,7 +211,7 @@ constructor(
|
|||
* automatically healed from newer JSON snapshots even if their timestamp is recent.
|
||||
*/
|
||||
private fun DeviceHardwareEntity.isStale(): Boolean =
|
||||
isIncomplete() || (System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
|
||||
isIncomplete() || (nowMillis - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
|
||||
|
||||
private fun loadQuirks(): List<BootloaderOtaQuirk> {
|
||||
val quirks = bootloaderOtaQuirksJsonDataSource.loadBootloaderOtaQuirksFromJsonAsset()
|
||||
|
|
@ -252,6 +253,6 @@ constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val CACHE_EXPIRATION_TIME_MS = TimeUnit.DAYS.toMillis(1)
|
||||
private val CACHE_EXPIRATION_TIME_MS = TimeConstants.ONE_DAY.inWholeMilliseconds
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.data.repository
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
|
|
@ -26,8 +25,9 @@ import org.meshtastic.core.database.entity.FirmwareRelease
|
|||
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseType
|
||||
import org.meshtastic.core.database.entity.asExternalModel
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -124,10 +124,9 @@ constructor(
|
|||
}
|
||||
|
||||
/** Extension function to check if the cached entity is stale. */
|
||||
private fun FirmwareReleaseEntity.isStale(): Boolean =
|
||||
(System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
|
||||
private fun FirmwareReleaseEntity.isStale(): Boolean = (nowMillis - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
|
||||
|
||||
companion object {
|
||||
private val CACHE_EXPIRATION_TIME_MS = TimeUnit.HOURS.toMillis(1)
|
||||
private val CACHE_EXPIRATION_TIME_MS = TimeConstants.ONE_HOUR.inWholeMilliseconds
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import kotlinx.coroutines.withContext
|
|||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.MyNodeInfo
|
||||
|
|
@ -126,9 +128,9 @@ constructor(
|
|||
|
||||
val cutoffTimestamp =
|
||||
if (retentionDays == MeshLogPrefs.ONE_HOUR_RETENTION_DAYS) {
|
||||
System.currentTimeMillis() - (60 * 60 * 1000L)
|
||||
nowMillis - TimeConstants.ONE_HOUR.inWholeMilliseconds
|
||||
} else {
|
||||
System.currentTimeMillis() - (retentionDays * 24 * 60 * 60 * 1000L)
|
||||
nowMillis - (retentionDays * TimeConstants.ONE_DAY.inWholeMilliseconds)
|
||||
}
|
||||
dbManager.currentDb.value.meshLogDao().deleteOlderThan(cutoffTimestamp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import org.meshtastic.core.data.datasource.DeviceHardwareJsonDataSource
|
|||
import org.meshtastic.core.data.datasource.DeviceHardwareLocalDataSource
|
||||
import org.meshtastic.core.database.entity.DeviceHardwareEntity
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
|
||||
|
||||
class DeviceHardwareRepositoryTest {
|
||||
|
|
@ -120,6 +121,6 @@ class DeviceHardwareRepositoryTest {
|
|||
requiresDfu = false,
|
||||
supportLevel = 0,
|
||||
tags = emptyList(),
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
lastUpdated = nowMillis,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import org.meshtastic.core.database.MeshtasticDatabase
|
|||
import org.meshtastic.core.database.dao.MeshLogDao
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
|
|
@ -67,7 +68,7 @@ class MeshLogRepositoryTest {
|
|||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = "telemetry",
|
||||
received_date = System.currentTimeMillis(),
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromRadio = FromRadio(packet = meshPacket),
|
||||
)
|
||||
|
|
@ -94,7 +95,7 @@ class MeshLogRepositoryTest {
|
|||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = "telemetry",
|
||||
received_date = System.currentTimeMillis(),
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromRadio = FromRadio(packet = meshPacket),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import org.meshtastic.core.database.entity.Packet
|
|||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -71,7 +72,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = false,
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, "Message $it!"),
|
||||
)
|
||||
|
|
@ -153,7 +154,7 @@ class PacketDaoTest {
|
|||
|
||||
@Test
|
||||
fun test_clearUnreadCount() = runBlocking {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val timestamp = nowMillis
|
||||
testContactKeys.forEach { contactKey ->
|
||||
packetDao.clearUnreadCount(contactKey, timestamp)
|
||||
val unreadCount = packetDao.getUnreadCount(contactKey)
|
||||
|
|
@ -180,7 +181,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = true,
|
||||
data = DataPacket(to = DataPacket.ID_BROADCAST, channel = 0, text = "Test").copy(id = packetId),
|
||||
packetId = packetId,
|
||||
|
|
@ -203,7 +204,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = true,
|
||||
data = DataPacket(to = DataPacket.ID_BROADCAST, channel = 0, text = "Test"),
|
||||
sfpp_hash = hashByteString,
|
||||
|
|
@ -227,7 +228,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = true,
|
||||
data = DataPacket(to = DataPacket.ID_BROADCAST, channel = 0, text = "Test"),
|
||||
sfpp_hash = hashByteString,
|
||||
|
|
@ -262,7 +263,7 @@ class PacketDaoTest {
|
|||
replyId = 123,
|
||||
userId = "sender",
|
||||
emoji = "👍",
|
||||
timestamp = System.currentTimeMillis(),
|
||||
timestamp = nowMillis,
|
||||
sfpp_hash = hashByteString,
|
||||
)
|
||||
|
||||
|
|
@ -290,7 +291,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = true,
|
||||
data = DataPacket(to = "target", channel = 0, text = "Hello").copy(id = initialId),
|
||||
packetId = initialId,
|
||||
|
|
@ -322,7 +323,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "test",
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = true,
|
||||
data = DataPacket(to = toId, channel = 0, text = "Match me").copy(from = fromId, id = packetId),
|
||||
packetId = packetId,
|
||||
|
|
@ -356,7 +357,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = filteredContactKey,
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = false,
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, "Filtered message"),
|
||||
filtered = true,
|
||||
|
|
@ -380,7 +381,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + i,
|
||||
received_time = nowMillis + i,
|
||||
read = false,
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, "Filtered $i"),
|
||||
filtered = true,
|
||||
|
|
@ -428,7 +429,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + index,
|
||||
received_time = nowMillis + index,
|
||||
read = false,
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, text),
|
||||
filtered = false,
|
||||
|
|
@ -443,7 +444,7 @@ class PacketDaoTest {
|
|||
myNodeNum = myNodeNum,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = contactKey,
|
||||
received_time = System.currentTimeMillis() + normalMessages.size + index,
|
||||
received_time = nowMillis + normalMessages.size + index,
|
||||
read = true, // Filtered messages are marked as read
|
||||
data = DataPacket(DataPacket.ID_BROADCAST, 0, text),
|
||||
filtered = true,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import javax.inject.Inject
|
||||
|
|
@ -115,7 +116,7 @@ class DatabaseManager @Inject constructor(private val app: Application, private
|
|||
inline fun <T> withDb(block: (MeshtasticDatabase) -> T): T = block(currentDb.value)
|
||||
|
||||
private fun markLastUsed(dbName: String) {
|
||||
prefs.edit().putLong(lastUsedKey(dbName), System.currentTimeMillis()).apply()
|
||||
prefs.edit().putLong(lastUsedKey(dbName), nowMillis).apply()
|
||||
}
|
||||
|
||||
private fun lastUsed(dbName: String): Long {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import org.meshtastic.core.database.entity.PacketEntity
|
|||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
@ -347,7 +348,7 @@ interface PacketDao {
|
|||
} else if (until == 0L) { // unmute
|
||||
0L
|
||||
} else {
|
||||
System.currentTimeMillis() + until
|
||||
nowMillis + until
|
||||
}
|
||||
|
||||
getContactSettings(contact)?.copy(muteUntil = absoluteMuteUntil)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.room.PrimaryKey
|
|||
import kotlinx.serialization.Serializable
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.NetworkDeviceHardware
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "device_hardware")
|
||||
|
|
@ -34,7 +35,7 @@ data class DeviceHardwareEntity(
|
|||
val hwModel: Int,
|
||||
@ColumnInfo(name = "hw_model_slug") val hwModelSlug: String,
|
||||
val images: List<String>?,
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long = nowMillis,
|
||||
@ColumnInfo(name = "partition_scheme") val partitionScheme: String? = null,
|
||||
@PrimaryKey @ColumnInfo(name = "platformio_target") val platformioTarget: String,
|
||||
@ColumnInfo(name = "requires_dfu") val requiresDfu: Boolean?,
|
||||
|
|
@ -51,7 +52,7 @@ fun NetworkDeviceHardware.asEntity() = DeviceHardwareEntity(
|
|||
hwModel = hwModel,
|
||||
hwModelSlug = hwModelSlug,
|
||||
images = images,
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
lastUpdated = nowMillis,
|
||||
partitionScheme = partitionScheme,
|
||||
platformioTarget = platformioTarget,
|
||||
requiresDfu = requiresDfu,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.room.PrimaryKey
|
|||
import kotlinx.serialization.Serializable
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.NetworkFirmwareRelease
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "firmware_release")
|
||||
|
|
@ -31,7 +32,7 @@ data class FirmwareReleaseEntity(
|
|||
@ColumnInfo(name = "release_notes") val releaseNotes: String = "",
|
||||
@ColumnInfo(name = "title") val title: String = "",
|
||||
@ColumnInfo(name = "zip_url") val zipUrl: String = "",
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long = nowMillis,
|
||||
@ColumnInfo(name = "release_type") val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
|
||||
)
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ fun NetworkFirmwareRelease.asEntity(releaseType: FirmwareReleaseType) = Firmware
|
|||
releaseNotes = releaseNotes,
|
||||
title = title,
|
||||
zipUrl = zipUrl,
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
lastUpdated = nowMillis,
|
||||
releaseType = releaseType,
|
||||
)
|
||||
|
||||
|
|
@ -61,7 +62,7 @@ data class FirmwareRelease(
|
|||
val releaseNotes: String = "",
|
||||
val title: String = "",
|
||||
val zipUrl: String = "",
|
||||
val lastUpdated: Long = System.currentTimeMillis(),
|
||||
val lastUpdated: Long = nowMillis,
|
||||
val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import org.meshtastic.core.model.EnvironmentMetrics
|
|||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
|
|
@ -100,7 +102,7 @@ data class NodeWithRelations(
|
|||
data class MetadataEntity(
|
||||
@PrimaryKey val num: Int,
|
||||
@ColumnInfo(name = "proto", typeAffinity = ColumnInfo.BLOB) val proto: DeviceMetadata,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val timestamp: Long = nowMillis,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
@ -181,7 +183,7 @@ data class NodeEntity(
|
|||
|
||||
val ERROR_BYTE_STRING: ByteString = ByteArray(32) { 0 }.toByteString()
|
||||
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
}
|
||||
|
||||
fun toModel() = Node(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.getShortDateTime
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
data class PacketEntity(
|
||||
|
|
@ -126,7 +127,7 @@ data class ContactSettings(
|
|||
@ColumnInfo(name = "filtering_disabled", defaultValue = "0") val filteringDisabled: Boolean = false,
|
||||
) {
|
||||
val isMuted
|
||||
get() = System.currentTimeMillis() <= muteUntil
|
||||
get() = nowMillis <= muteUntil
|
||||
}
|
||||
|
||||
data class Reaction(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import org.meshtastic.core.database.MeshtasticDatabase
|
|||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
|
|
@ -155,7 +156,7 @@ class MigrationTest {
|
|||
myNodeNum = 42424242,
|
||||
port_num = PortNum.TEXT_MESSAGE_APP.value,
|
||||
contact_key = "$channel!broadcast",
|
||||
received_time = System.currentTimeMillis(),
|
||||
received_time = nowMillis,
|
||||
read = false,
|
||||
data = DataPacket(to = DataPacket.ID_BROADCAST, channel = channel, text = text),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ dependencies {
|
|||
|
||||
api(libs.androidx.annotation)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.kotlinx.datetime)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.zxing.core)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import okio.ByteString
|
|||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.util.ByteStringParceler
|
||||
import org.meshtastic.core.model.util.ByteStringSerializer
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Waypoint
|
||||
|
|
@ -54,7 +55,7 @@ data class DataPacket(
|
|||
// A port number for this packet
|
||||
var dataType: Int,
|
||||
var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
|
||||
var time: Long = System.currentTimeMillis(), // msecs since 1970
|
||||
var time: Long = nowMillis, // msecs since 1970
|
||||
var id: Int = 0, // 0 means unassigned
|
||||
var status: MessageStatus? = MessageStatus.UNKNOWN,
|
||||
var hopLimit: Int = 0,
|
||||
|
|
@ -128,7 +129,7 @@ data class DataPacket(
|
|||
bytes = text.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
channel = channel,
|
||||
replyId = replyId ?: 0,
|
||||
replyId = replyId,
|
||||
)
|
||||
|
||||
/** If this is a text message, return the string, otherwise null */
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import kotlinx.parcelize.Parcelize
|
|||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.model.util.bearing
|
||||
import org.meshtastic.core.model.util.latLongToMeter
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
|
|
@ -84,7 +85,7 @@ data class Position(
|
|||
|
||||
fun degI(d: Double) = (d * 1e7).toInt()
|
||||
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -134,7 +135,7 @@ data class DeviceMetrics(
|
|||
) : Parcelable {
|
||||
companion object {
|
||||
@Suppress("MagicNumber")
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
}
|
||||
|
||||
/** Create our model object from a protobuf. */
|
||||
|
|
@ -168,7 +169,7 @@ data class EnvironmentMetrics(
|
|||
) : Parcelable {
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
fun currentTime() = nowSeconds.toInt()
|
||||
|
||||
fun fromTelemetryProto(proto: org.meshtastic.proto.EnvironmentMetrics, time: Int): EnvironmentMetrics =
|
||||
EnvironmentMetrics(
|
||||
|
|
|
|||
|
|
@ -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,60 +14,89 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import org.meshtastic.core.model.util.TimeConstants.HOURS_PER_DAY
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
private const val ONLINE_WINDOW_HOURS = 2
|
||||
private val ONLINE_WINDOW_HOURS = 2.hours
|
||||
private val DAY_DURATION = 24.hours
|
||||
|
||||
// return time if within 24 hours, otherwise date
|
||||
/**
|
||||
* Returns a short string representing the time if it's within the last 24 hours, otherwise returns a short string
|
||||
* representing the date.
|
||||
*
|
||||
* @param time The time in milliseconds
|
||||
* @return Formatted date or time string, or null if time is 0
|
||||
*/
|
||||
fun getShortDate(time: Long): String? {
|
||||
val date = if (time != 0L) Date(time) else return null
|
||||
val isWithin24Hours = System.currentTimeMillis() - date.time <= TimeUnit.DAYS.toMillis(1)
|
||||
if (time == 0L) return null
|
||||
val instant = time.toInstant()
|
||||
val isWithin24Hours = (nowInstant - instant) <= DAY_DURATION
|
||||
|
||||
return if (isWithin24Hours) {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(instant.toDate())
|
||||
} else {
|
||||
DateFormat.getDateInstance(DateFormat.SHORT).format(date)
|
||||
DateFormat.getDateInstance(DateFormat.SHORT).format(instant.toDate())
|
||||
}
|
||||
}
|
||||
|
||||
// return time if within 24 hours, otherwise date/time
|
||||
/**
|
||||
* Returns a short string representing the time if it's within the last 24 hours, otherwise returns a combined short
|
||||
* date/time string.
|
||||
*
|
||||
* @param time The time in milliseconds
|
||||
* @return Formatted date/time string
|
||||
*/
|
||||
fun getShortDateTime(time: Long): String {
|
||||
val date = Date(time)
|
||||
val isWithin24Hours = System.currentTimeMillis() - date.time <= TimeUnit.DAYS.toMillis(1)
|
||||
val instant = time.toInstant()
|
||||
val isWithin24Hours = (nowInstant - instant) <= DAY_DURATION
|
||||
|
||||
return if (isWithin24Hours) {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(date)
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(instant.toDate())
|
||||
} else {
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date)
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(instant.toDate())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in seconds as a human-readable uptime string (e.g., "1d 2h 3m 4s").
|
||||
*
|
||||
* @param seconds The duration in seconds.
|
||||
* @return A formatted uptime string.
|
||||
*/
|
||||
fun formatUptime(seconds: Int): String = formatUptime(seconds.toLong())
|
||||
|
||||
/**
|
||||
* Formats a duration in seconds as a human-readable uptime string (e.g., "1d 2h 3m 4s").
|
||||
*
|
||||
* @param seconds The duration in seconds.
|
||||
* @return A formatted uptime string.
|
||||
*/
|
||||
private fun formatUptime(seconds: Long): String {
|
||||
val days = TimeUnit.SECONDS.toDays(seconds)
|
||||
val hours = TimeUnit.SECONDS.toHours(seconds) % TimeUnit.DAYS.toHours(1)
|
||||
val minutes = TimeUnit.SECONDS.toMinutes(seconds) % TimeUnit.HOURS.toMinutes(1)
|
||||
val secs = seconds % TimeUnit.MINUTES.toSeconds(1)
|
||||
|
||||
return listOfNotNull(
|
||||
"${days}d".takeIf { days > 0 },
|
||||
"${hours}h".takeIf { hours > 0 },
|
||||
"${minutes}m".takeIf { minutes > 0 },
|
||||
"${secs}s".takeIf { secs > 0 },
|
||||
)
|
||||
.joinToString(" ")
|
||||
if (seconds == 0L) return "0s"
|
||||
return seconds.seconds.toComponents { days, hours, minutes, secs, _ ->
|
||||
listOfNotNull(
|
||||
"${days}d".takeIf { days > 0 },
|
||||
"${hours}h".takeIf { hours > 0 },
|
||||
"${minutes}m".takeIf { minutes > 0 },
|
||||
"${secs}s".takeIf { secs > 0 },
|
||||
)
|
||||
.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
fun onlineTimeThreshold(): Int {
|
||||
val currentSeconds = System.currentTimeMillis() / TimeUnit.SECONDS.toMillis(1)
|
||||
return (currentSeconds - TimeUnit.HOURS.toSeconds(ONLINE_WINDOW_HOURS.toLong())).toInt()
|
||||
}
|
||||
/**
|
||||
* Calculates the threshold in seconds for considering a node "online".
|
||||
*
|
||||
* @return The epoch seconds threshold.
|
||||
*/
|
||||
fun onlineTimeThreshold(): Int = (nowInstant - ONLINE_WINDOW_HOURS).epochSeconds.toInt()
|
||||
|
||||
/**
|
||||
* Calculates the remaining mute time in days and hours.
|
||||
|
|
@ -76,9 +105,8 @@ fun onlineTimeThreshold(): Int {
|
|||
* @return Pair of (days, hours), where days is Int and hours is Double
|
||||
*/
|
||||
fun formatMuteRemainingTime(remainingMillis: Long): Pair<Int, Double> {
|
||||
if (remainingMillis <= 0) return Pair(0, 0.0)
|
||||
val totalHours = remainingMillis.toDouble() / TimeUnit.HOURS.toMillis(1)
|
||||
val days = (totalHours / TimeUnit.DAYS.toHours(1)).toInt()
|
||||
val hours = totalHours % TimeUnit.DAYS.toHours(1)
|
||||
return Pair(days, hours)
|
||||
val duration = remainingMillis.milliseconds
|
||||
if (duration <= Duration.ZERO) return 0 to 0.0
|
||||
val totalHours = duration.toDouble(DurationUnit.HOURS)
|
||||
return (totalHours / HOURS_PER_DAY).toInt() to (totalHours % HOURS_PER_DAY)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toJavaZoneId
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.zone.ZoneOffsetTransitionRule
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
/** Generates a POSIX time zone string from a [TimeZone]. */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun TimeZone.toPosixString(): String = this.toJavaZoneId().toPosixString()
|
||||
|
||||
/**
|
||||
* Generates a POSIX time zone string from a [ZoneId]. Uses the specification found
|
||||
* [here](https://www.postgresql.org/docs/current/datetime-posix-timezone-specs.html).
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Suppress("ReturnCount", "MagicNumber")
|
||||
fun ZoneId.toPosixString(): String {
|
||||
val rules = this.rules
|
||||
|
||||
if (rules.isFixedOffset || rules.transitionRules.isEmpty()) {
|
||||
val now = java.time.Instant.ofEpochMilli(nowMillis)
|
||||
val zdt = ZonedDateTime.ofInstant(now, this)
|
||||
return "${formatAbbreviation(zdt.timeZoneShortName())}${formatPosixOffset(zdt.offset)}"
|
||||
}
|
||||
|
||||
val springRule = rules.transitionRules.lastOrNull { it.offsetAfter.totalSeconds > it.offsetBefore.totalSeconds }
|
||||
val fallRule = rules.transitionRules.lastOrNull { it.offsetAfter.totalSeconds < it.offsetBefore.totalSeconds }
|
||||
|
||||
if (springRule == null || fallRule == null) {
|
||||
val now = java.time.Instant.ofEpochMilli(nowMillis)
|
||||
val zdt = ZonedDateTime.ofInstant(now, this)
|
||||
return "${formatAbbreviation(zdt.timeZoneShortName())}${formatPosixOffset(zdt.offset)}"
|
||||
}
|
||||
|
||||
return buildString {
|
||||
val stdAbbrev = getTransitionAbbreviation(this@toPosixString, fallRule)
|
||||
val dstAbbrev = getTransitionAbbreviation(this@toPosixString, springRule)
|
||||
|
||||
append(formatAbbreviation(stdAbbrev))
|
||||
append(formatPosixOffset(springRule.offsetBefore))
|
||||
append(formatAbbreviation(dstAbbrev))
|
||||
|
||||
if (springRule.offsetAfter.totalSeconds - springRule.offsetBefore.totalSeconds != 3600) {
|
||||
append(formatPosixOffset(springRule.offsetAfter))
|
||||
}
|
||||
|
||||
append(formatTransitionRule(springRule))
|
||||
append(formatTransitionRule(fallRule))
|
||||
}
|
||||
}
|
||||
|
||||
/** Formats the time zone short name for a [ZonedDateTime]. */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
internal fun ZonedDateTime.timeZoneShortName(): String {
|
||||
val formatter = DateTimeFormatter.ofPattern("zzz", Locale.ENGLISH)
|
||||
val shortName = format(formatter)
|
||||
return if (shortName.startsWith("GMT")) "GMT" else shortName
|
||||
}
|
||||
|
||||
/** Formats an abbreviation for POSIX. If it contains non-letters, it's wrapped in angle brackets. */
|
||||
private fun formatAbbreviation(abbrev: String): String = if (abbrev.all { it.isLetter() }) abbrev else "<$abbrev>"
|
||||
|
||||
/** Gets the abbreviation for a given zone and transition rule. */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
internal fun getTransitionAbbreviation(zone: ZoneId, rule: ZoneOffsetTransitionRule): String {
|
||||
val year = nowInstant.toLocalDateTime(systemTimeZone).year
|
||||
val transition = rule.createTransition(year)
|
||||
return ZonedDateTime.ofInstant(transition.instant, zone).timeZoneShortName()
|
||||
}
|
||||
|
||||
/** Formats a [ZoneOffset] for use in a POSIX string. */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Suppress("MagicNumber")
|
||||
internal fun formatPosixOffset(offset: ZoneOffset): String {
|
||||
val offsetSeconds = -offset.totalSeconds
|
||||
val hours = offsetSeconds / 3600
|
||||
val remainingSeconds = abs(offsetSeconds) % 3600
|
||||
val minutes = remainingSeconds / 60
|
||||
val seconds = remainingSeconds % 60
|
||||
|
||||
return buildString {
|
||||
if (offsetSeconds < 0 && hours == 0) append("-")
|
||||
append(hours)
|
||||
if (minutes != 0 || seconds != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, minutes))
|
||||
if (seconds != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, seconds))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Formats a [ZoneOffsetTransitionRule] for use in a POSIX string. */
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Suppress("MagicNumber")
|
||||
internal fun formatTransitionRule(rule: ZoneOffsetTransitionRule): String {
|
||||
val month = rule.month.value
|
||||
val dayOfWeek = rule.dayOfWeek.value % 7
|
||||
val dayIndicator = rule.dayOfMonthIndicator
|
||||
|
||||
val occurrence =
|
||||
when {
|
||||
dayIndicator < 0 -> 5
|
||||
dayIndicator > rule.month.length(false) - 7 -> 5
|
||||
else -> ((dayIndicator - 1) / 7) + 1
|
||||
}
|
||||
|
||||
val wallTime =
|
||||
when (rule.timeDefinition) {
|
||||
ZoneOffsetTransitionRule.TimeDefinition.UTC ->
|
||||
rule.localTime.plusSeconds(rule.offsetBefore.totalSeconds.toLong())
|
||||
|
||||
ZoneOffsetTransitionRule.TimeDefinition.STANDARD -> {
|
||||
if (rule.offsetAfter.totalSeconds > rule.offsetBefore.totalSeconds) {
|
||||
rule.localTime
|
||||
} else {
|
||||
rule.localTime.plusSeconds(
|
||||
(rule.offsetBefore.totalSeconds - rule.offsetAfter.totalSeconds).toLong(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> rule.localTime
|
||||
}
|
||||
|
||||
return buildString {
|
||||
append(",M$month.$occurrence.$dayOfWeek")
|
||||
if (wallTime.hour != 2 || wallTime.minute != 0 || wallTime.second != 0) {
|
||||
append("/${wallTime.hour}")
|
||||
if (wallTime.minute != 0 || wallTime.second != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, wallTime.minute))
|
||||
if (wallTime.second != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, wallTime.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
/** Common time-related constants. */
|
||||
object TimeConstants {
|
||||
val ONE_HOUR = 1.hours
|
||||
val EIGHT_HOURS = 8.hours
|
||||
val ONE_DAY = 1.days
|
||||
val TWO_DAYS = 2.days
|
||||
|
||||
const val HOURS_PER_DAY = 24
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import kotlinx.datetime.TimeZone
|
||||
import java.util.Date
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* Awaits the latch for the given [Duration].
|
||||
*
|
||||
* @param timeout The maximum time to wait.
|
||||
* @return `true` if the count reached zero and `false` if the waiting time elapsed before the count reached zero.
|
||||
*/
|
||||
fun CountDownLatch.await(timeout: Duration): Boolean = this.await(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS)
|
||||
|
||||
/** Accessor for the current time in milliseconds. */
|
||||
val nowMillis: Long
|
||||
get() = nowInstant.toEpochMilliseconds()
|
||||
|
||||
/** Accessor for the current time as a stable [Instant]. */
|
||||
val nowInstant: Instant
|
||||
get() = Clock.System.now()
|
||||
|
||||
/** Accessor for the current time in seconds. */
|
||||
val nowSeconds: Long
|
||||
get() = nowInstant.epochSeconds
|
||||
|
||||
/** Accessor for the system default time zone. */
|
||||
val systemTimeZone: TimeZone
|
||||
get() = TimeZone.currentSystemDefault()
|
||||
|
||||
/** Converts this [Instant] to a legacy [Date]. */
|
||||
fun Instant.toDate(): Date = Date(this.toEpochMilliseconds())
|
||||
|
||||
/** Converts these milliseconds to an [Instant]. */
|
||||
fun Long.toInstant(): Instant = Instant.fromEpochMilliseconds(this)
|
||||
|
||||
/** Converts these seconds to an [Instant]. */
|
||||
fun Int.secondsToInstant(): Instant = Instant.fromEpochSeconds(this.toLong())
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import kotlinx.datetime.TimeZone
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
class TimeExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun testNowMillis() {
|
||||
val start = Clock.System.now().toEpochMilliseconds()
|
||||
val now = nowMillis
|
||||
val end = Clock.System.now().toEpochMilliseconds()
|
||||
assertTrue(now in start..end)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNowSeconds() {
|
||||
val start = Clock.System.now().epochSeconds
|
||||
val now = nowSeconds
|
||||
val end = Clock.System.now().epochSeconds
|
||||
assertTrue(now in start..end)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToDate() {
|
||||
val instant = Instant.fromEpochMilliseconds(1234567890L)
|
||||
val date = instant.toDate()
|
||||
assertEquals(1234567890L, date.time)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongToInstant() {
|
||||
val millis = 1234567890L
|
||||
val instant = millis.toInstant()
|
||||
assertEquals(millis, instant.toEpochMilliseconds())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIntSecondsToInstant() {
|
||||
val seconds = 1234567890
|
||||
val instant = seconds.secondsToInstant()
|
||||
assertEquals(seconds.toLong(), instant.epochSeconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDurationInWholeSeconds() {
|
||||
assertEquals(60L, 60.seconds.inWholeSeconds)
|
||||
assertEquals(3600L, TimeConstants.ONE_HOUR.inWholeSeconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongSecondsProperty() {
|
||||
assertEquals(60.seconds, 60L.seconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCountDownLatchAwaitWithDuration() {
|
||||
val latch = CountDownLatch(1)
|
||||
// This should timeout quickly
|
||||
val result = latch.await(10.milliseconds)
|
||||
assertEquals(false, result)
|
||||
|
||||
val latch2 = CountDownLatch(1)
|
||||
latch2.countDown()
|
||||
val result2 = latch2.await(1.seconds)
|
||||
assertEquals(true, result2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTimeZoneToPosixString() {
|
||||
val tz = TimeZone.of("UTC")
|
||||
assertEquals("UTC0", tz.toPosixString())
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ 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.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.node_sort_last_heard
|
||||
import org.meshtastic.core.ui.R
|
||||
|
|
@ -50,5 +51,5 @@ fun LastHeardInfo(
|
|||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun LastHeardInfoPreview() {
|
||||
AppTheme { LastHeardInfo(lastHeard = (System.currentTimeMillis() / 1000).toInt() - 8600) }
|
||||
AppTheme { LastHeardInfo(lastHeard = nowSeconds.toInt() - 8600) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.component
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
|
|
@ -28,16 +27,17 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
|
||||
@Composable
|
||||
fun rememberTimeTickWithLifecycle(): Long {
|
||||
val context = LocalContext.current
|
||||
var value by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
val receiver = TimeBroadcastReceiver { value = System.currentTimeMillis() }
|
||||
var value by remember { mutableLongStateOf(nowMillis) }
|
||||
val receiver = TimeBroadcastReceiver { value = nowMillis }
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
receiver.register(context)
|
||||
value = System.currentTimeMillis()
|
||||
value = nowMillis
|
||||
|
||||
onPauseOrDispose { receiver.unregister(context) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,135 +14,18 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:Suppress("Wrapping", "UnusedImports", "SpacingAroundColon")
|
||||
|
||||
package org.meshtastic.core.ui.timezone
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.Year
|
||||
import org.meshtastic.core.model.util.toPosixString
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.zone.ZoneOffsetTransitionRule
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Generates a POSIX time zone string from a [ZoneId]. Uses the specification found
|
||||
* [here](https://www.postgresql.org/docs/current/datetime-posix-timezone-specs.html).
|
||||
* Generates a POSIX time zone string from a [ZoneId].
|
||||
*
|
||||
* @deprecated Use [org.meshtastic.core.model.util.toPosixString] instead.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
fun ZoneId.toPosixString(): String {
|
||||
val rules = this.rules
|
||||
|
||||
if (rules.isFixedOffset || rules.transitionRules.isEmpty()) {
|
||||
val now = Instant.now()
|
||||
val zdt = ZonedDateTime.ofInstant(now, this)
|
||||
return "${formatAbbreviation(zdt.timeZoneShortName())}${formatPosixOffset(zdt.offset)}"
|
||||
}
|
||||
|
||||
val springRule = rules.transitionRules.lastOrNull { it.offsetAfter.totalSeconds > it.offsetBefore.totalSeconds }
|
||||
val fallRule = rules.transitionRules.lastOrNull { it.offsetAfter.totalSeconds < it.offsetBefore.totalSeconds }
|
||||
|
||||
if (springRule == null || fallRule == null) {
|
||||
val now = Instant.now()
|
||||
val zdt = ZonedDateTime.ofInstant(now, this)
|
||||
return "${formatAbbreviation(zdt.timeZoneShortName())}${formatPosixOffset(zdt.offset)}"
|
||||
}
|
||||
|
||||
return buildString {
|
||||
val stdAbbrev = getTransitionAbbreviation(this@toPosixString, fallRule)
|
||||
val dstAbbrev = getTransitionAbbreviation(this@toPosixString, springRule)
|
||||
|
||||
append(formatAbbreviation(stdAbbrev))
|
||||
append(formatPosixOffset(springRule.offsetBefore))
|
||||
append(formatAbbreviation(dstAbbrev))
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
if (springRule.offsetAfter.totalSeconds - springRule.offsetBefore.totalSeconds != 3600) {
|
||||
append(formatPosixOffset(springRule.offsetAfter))
|
||||
}
|
||||
|
||||
append(formatTransitionRule(springRule))
|
||||
append(formatTransitionRule(fallRule))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ZonedDateTime.timeZoneShortName(): String {
|
||||
val formatter = DateTimeFormatter.ofPattern("zzz", Locale.ENGLISH)
|
||||
val shortName = format(formatter)
|
||||
return if (shortName.startsWith("GMT")) "GMT" else shortName
|
||||
}
|
||||
|
||||
fun formatAbbreviation(abbrev: String): String = if (abbrev.all { it.isLetter() }) abbrev else "<$abbrev>"
|
||||
|
||||
internal fun getTransitionAbbreviation(zone: ZoneId, rule: ZoneOffsetTransitionRule): String {
|
||||
val transition = rule.createTransition(Year.now().value)
|
||||
return ZonedDateTime.ofInstant(transition.instant, zone).timeZoneShortName()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun formatPosixOffset(offset: ZoneOffset): String {
|
||||
val offsetSeconds = -offset.totalSeconds
|
||||
val hours = offsetSeconds / 3600
|
||||
val remainingSeconds = abs(offsetSeconds) % 3600
|
||||
val minutes = remainingSeconds / 60
|
||||
val seconds = remainingSeconds % 60
|
||||
|
||||
return buildString {
|
||||
if (offsetSeconds < 0 && hours == 0) append("-")
|
||||
append(hours)
|
||||
if (minutes != 0 || seconds != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, minutes))
|
||||
if (seconds != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, seconds))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal fun formatTransitionRule(rule: ZoneOffsetTransitionRule): String {
|
||||
val month = rule.month.value
|
||||
val dayOfWeek = rule.dayOfWeek.value % 7
|
||||
val dayIndicator = rule.dayOfMonthIndicator
|
||||
|
||||
val occurrence =
|
||||
when {
|
||||
dayIndicator < 0 -> 5
|
||||
dayIndicator > rule.month.length(false) - 7 -> 5
|
||||
else -> ((dayIndicator - 1) / 7) + 1
|
||||
}
|
||||
|
||||
val wallTime =
|
||||
when (rule.timeDefinition) {
|
||||
ZoneOffsetTransitionRule.TimeDefinition.UTC ->
|
||||
rule.localTime.plusSeconds(rule.offsetBefore.totalSeconds.toLong())
|
||||
|
||||
ZoneOffsetTransitionRule.TimeDefinition.STANDARD -> {
|
||||
if (rule.offsetAfter.totalSeconds > rule.offsetBefore.totalSeconds) {
|
||||
rule.localTime
|
||||
} else {
|
||||
rule.localTime.plusSeconds(
|
||||
(rule.offsetBefore.totalSeconds - rule.offsetAfter.totalSeconds).toLong(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> rule.localTime
|
||||
}
|
||||
|
||||
return buildString {
|
||||
append(",M$month.$occurrence.$dayOfWeek")
|
||||
if (wallTime.hour != 2 || wallTime.minute != 0 || wallTime.second != 0) {
|
||||
append("/${wallTime.hour}")
|
||||
if (wallTime.minute != 0 || wallTime.second != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, wallTime.minute))
|
||||
if (wallTime.second != 0) {
|
||||
append(":%02d".format(Locale.ENGLISH, wallTime.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Deprecated(
|
||||
message = "Use org.meshtastic.core.model.util.toPosixString instead",
|
||||
replaceWith = ReplaceWith("this.toPosixString()", "org.meshtastic.core.model.util.toPosixString"),
|
||||
)
|
||||
fun ZoneId.toPosixString(): String = this.toPosixString()
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ package org.meshtastic.core.ui.util
|
|||
|
||||
import android.text.format.DateUtils
|
||||
import com.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.now
|
||||
import org.meshtastic.core.strings.unknown
|
||||
import java.lang.System.currentTimeMillis
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
@ -40,7 +40,7 @@ fun formatAgo(lastSeenUnixSeconds: Int): String {
|
|||
if (lastSeenUnixSeconds <= 0) return getString(Res.string.unknown)
|
||||
|
||||
val lastSeenDuration = lastSeenUnixSeconds.seconds
|
||||
val currentDuration = currentTimeMillis().milliseconds
|
||||
val currentDuration = nowMillis.milliseconds
|
||||
val diff = (currentDuration - lastSeenDuration).absoluteValue
|
||||
|
||||
return if (diff < 1.minutes) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ 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.model.util.nowMillis
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.unknown_age
|
||||
import org.meshtastic.proto.Channel
|
||||
|
|
@ -32,7 +33,7 @@ private const val SECONDS_TO_MILLIS = 1000L
|
|||
|
||||
@Composable
|
||||
fun Position.formatPositionTime(): String {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val currentTime = nowMillis
|
||||
val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds
|
||||
val isOlderThanSixMonths = (time ?: 0) * SECONDS_TO_MILLIS < sixMonthsAgo
|
||||
val timeText =
|
||||
|
|
|
|||
|
|
@ -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,13 +14,12 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.ui.timezone
|
||||
|
||||
import kotlinx.datetime.TimeZone
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import org.meshtastic.core.model.util.toPosixString
|
||||
|
||||
class ZoneIdExtensionsTest {
|
||||
|
||||
|
|
@ -51,21 +50,6 @@ class ZoneIdExtensionsTest {
|
|||
"Pacific/Auckland" to "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
)
|
||||
|
||||
zoneMap.forEach { (tz, expected) -> assertEquals(expected, ZoneId.of(tz).toPosixString()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test formatAbbreviation`() {
|
||||
assertEquals("PST", formatAbbreviation("PST"))
|
||||
assertEquals("<GMT-8>", formatAbbreviation("GMT-8"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test formatPosixOffset`() {
|
||||
assertEquals("8", formatPosixOffset(ZoneOffset.ofHours(-8)))
|
||||
assertEquals("-1", formatPosixOffset(ZoneOffset.ofHours(1)))
|
||||
assertEquals("-5:30", formatPosixOffset(ZoneOffset.ofHoursMinutes(5, 30)))
|
||||
assertEquals("0", formatPosixOffset(ZoneOffset.ofHours(0)))
|
||||
assertEquals("-0:30", formatPosixOffset(ZoneOffset.ofTotalSeconds(30 * 60)))
|
||||
zoneMap.forEach { (tz, expected) -> assertEquals(expected, TimeZone.of(tz).toPosixString()) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
|||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.firmware_update_connecting_attempt
|
||||
|
|
@ -302,13 +303,13 @@ constructor(
|
|||
WifiOtaTransport.RECOMMENDED_CHUNK_SIZE
|
||||
}
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
val startTime = nowMillis
|
||||
transport
|
||||
.streamFirmware(
|
||||
data = firmwareData,
|
||||
chunkSize = chunkSize,
|
||||
onProgress = { progress ->
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val currentTime = nowMillis
|
||||
val elapsedSeconds = (currentTime - startTime) / MILLIS_PER_SECOND
|
||||
val percent = (progress * PERCENT_MAX).toInt()
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
|
|
@ -262,9 +263,9 @@ class WifiOtaTransport(private val deviceIpAddress: String, private val port: In
|
|||
|
||||
// Listen for responses
|
||||
val receiveBuffer = ByteArray(RECEIVE_BUFFER_SIZE)
|
||||
val startTime = System.currentTimeMillis()
|
||||
val startTime = nowMillis
|
||||
|
||||
while (System.currentTimeMillis() - startTime < timeoutMs) {
|
||||
while (nowMillis - startTime < timeoutMs) {
|
||||
try {
|
||||
val receivePacket = DatagramPacket(receiveBuffer, receiveBuffer.size)
|
||||
socket.receive(receivePacket)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -89,6 +90,7 @@ 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.nowMillis
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.calculating
|
||||
import org.meshtastic.core.strings.cancel
|
||||
|
|
@ -510,7 +512,7 @@ fun MapView(
|
|||
)
|
||||
val label = (pt.name ?: "") + " " + formatAgo((waypoint.received_time / 1000).toInt())
|
||||
val emoji = String(Character.toChars(if ((pt.icon ?: 0) == 0) 128205 else pt.icon!!))
|
||||
val now = System.currentTimeMillis()
|
||||
val now = nowMillis
|
||||
val expireTimeMillis = (pt.expire ?: 0) * 1000L
|
||||
val expireTimeStr =
|
||||
when {
|
||||
|
|
|
|||
|
|
@ -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,10 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map
|
||||
|
||||
import android.database.Cursor
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.osmdroid.tileprovider.modules.DatabaseFileArchive
|
||||
import org.osmdroid.tileprovider.modules.SqlTileWriter
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ class SqlTileWriterExt : SqlTileWriter() {
|
|||
}
|
||||
|
||||
val rowCountExpired: Long
|
||||
get() = getRowCount("$COLUMN_EXPIRES<?", arrayOf(System.currentTimeMillis().toString()))
|
||||
get() = getRowCount("$COLUMN_EXPIRES<?", arrayOf(nowMillis.toString()))
|
||||
|
||||
class SourceCount {
|
||||
var rowCount: Long = 0
|
||||
|
|
|
|||
|
|
@ -60,7 +60,15 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.nowInstant
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.systemTimeZone
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.date
|
||||
|
|
@ -77,7 +85,8 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.util.Calendar
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
|
|
@ -98,18 +107,7 @@ fun EditWaypointDialog(
|
|||
|
||||
// Get current context for dialogs
|
||||
val context = LocalContext.current
|
||||
val calendar = remember {
|
||||
Calendar.getInstance().apply {
|
||||
val expire = waypoint.expire ?: 0
|
||||
if (expire != 0 && expire != Int.MAX_VALUE) {
|
||||
timeInMillis = expire * 1000L
|
||||
} else {
|
||||
timeInMillis = System.currentTimeMillis()
|
||||
@Suppress("MagicNumber")
|
||||
add(Calendar.HOUR_OF_DAY, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
val tz = systemTimeZone
|
||||
|
||||
// Determine locale-specific date format
|
||||
val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
|
||||
|
|
@ -117,34 +115,37 @@ fun EditWaypointDialog(
|
|||
val is24Hour = remember { android.text.format.DateFormat.is24HourFormat(context) }
|
||||
val timeFormat = remember { android.text.format.DateFormat.getTimeFormat(context) }
|
||||
|
||||
val currentInstant =
|
||||
remember(waypointInput.expire) {
|
||||
val expire = waypointInput.expire ?: 0
|
||||
if (expire != 0 && expire != Int.MAX_VALUE) {
|
||||
Instant.fromEpochSeconds(expire.toLong())
|
||||
} else {
|
||||
nowInstant + 8.hours
|
||||
}
|
||||
}
|
||||
|
||||
// State to hold selected date and time
|
||||
var selectedDate by remember {
|
||||
mutableStateOf(
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
dateFormat.format(calendar.time)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
var selectedTime by remember {
|
||||
mutableStateOf(
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
timeFormat.format(calendar.time)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
var epochTime by remember {
|
||||
mutableStateOf<Long?>(
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
(waypointInput.expire ?: 0) * 1000L
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
var selectedDate by
|
||||
remember(currentInstant) {
|
||||
mutableStateOf(
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
dateFormat.format(currentInstant.toDate())
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
var selectedTime by
|
||||
remember(currentInstant) {
|
||||
mutableStateOf(
|
||||
if ((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE) {
|
||||
timeFormat.format(currentInstant.toDate())
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (!showEmojiPickerView) {
|
||||
AlertDialog(
|
||||
|
|
@ -207,31 +208,47 @@ fun EditWaypointDialog(
|
|||
onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) },
|
||||
)
|
||||
}
|
||||
|
||||
val ldt = currentInstant.toLocalDateTime(tz)
|
||||
val datePickerDialog =
|
||||
DatePickerDialog(
|
||||
context,
|
||||
{ _: DatePicker, selectedYear: Int, selectedMonth: Int, selectedDay: Int ->
|
||||
calendar.set(selectedYear, selectedMonth, selectedDay)
|
||||
epochTime = calendar.timeInMillis
|
||||
selectedDate = dateFormat.format(calendar.time)
|
||||
val newLdt =
|
||||
LocalDateTime(
|
||||
year = selectedYear,
|
||||
month = Month(selectedMonth + 1),
|
||||
day = selectedDay,
|
||||
hour = ldt.hour,
|
||||
minute = ldt.minute,
|
||||
second = ldt.second,
|
||||
nanosecond = ldt.nanosecond,
|
||||
)
|
||||
waypointInput = waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt())
|
||||
},
|
||||
calendar.get(Calendar.YEAR),
|
||||
calendar.get(Calendar.MONTH),
|
||||
calendar.get(Calendar.DAY_OF_MONTH),
|
||||
ldt.year,
|
||||
ldt.monthNumber - 1,
|
||||
ldt.day,
|
||||
)
|
||||
|
||||
val timePickerDialog =
|
||||
android.app.TimePickerDialog(
|
||||
context,
|
||||
{ _: TimePicker, selectedHour: Int, selectedMinute: Int ->
|
||||
calendar.set(Calendar.HOUR_OF_DAY, selectedHour)
|
||||
calendar.set(Calendar.MINUTE, selectedMinute)
|
||||
epochTime = calendar.timeInMillis
|
||||
selectedTime = timeFormat.format(calendar.time)
|
||||
waypointInput = waypointInput.copy(expire = (calendar.timeInMillis / 1000).toInt())
|
||||
val newLdt =
|
||||
LocalDateTime(
|
||||
year = ldt.year,
|
||||
month = ldt.month,
|
||||
day = ldt.day,
|
||||
hour = selectedHour,
|
||||
minute = selectedMinute,
|
||||
second = ldt.second,
|
||||
nanosecond = ldt.nanosecond,
|
||||
)
|
||||
waypointInput = waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt())
|
||||
},
|
||||
calendar.get(Calendar.HOUR_OF_DAY),
|
||||
calendar.get(Calendar.MINUTE),
|
||||
ldt.hour,
|
||||
ldt.minute,
|
||||
is24Hour,
|
||||
)
|
||||
|
||||
|
|
@ -249,16 +266,8 @@ fun EditWaypointDialog(
|
|||
checked = waypointInput.expire != Int.MAX_VALUE && (waypointInput.expire ?: 0) != 0,
|
||||
onCheckedChange = { isChecked ->
|
||||
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())
|
||||
waypointInput = waypointInput.copy(expire = currentInstant.epochSeconds.toInt())
|
||||
} else {
|
||||
selectedDate = ""
|
||||
selectedTime = ""
|
||||
waypointInput = waypointInput.copy(expire = Int.MAX_VALUE)
|
||||
}
|
||||
},
|
||||
|
|
@ -275,7 +284,7 @@ fun EditWaypointDialog(
|
|||
Button(onClick = { datePickerDialog.show() }) { Text(stringResource(Res.string.date)) }
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = "$selectedDate",
|
||||
text = selectedDate,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
|
@ -284,7 +293,7 @@ fun EditWaypointDialog(
|
|||
Button(onClick = { timePickerDialog.show() }) { Text(stringResource(Res.string.time)) }
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = "$selectedTime",
|
||||
text = selectedTime,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
|
@ -337,7 +346,7 @@ private fun EditWaypointFormPreview() {
|
|||
name = "Test 123",
|
||||
description = "This is only a test",
|
||||
icon = 128169,
|
||||
expire = (System.currentTimeMillis() / 1000 + 8 * 3600).toInt(),
|
||||
expire = (nowSeconds.toInt() + 8 * 3600),
|
||||
),
|
||||
onSendClicked = {},
|
||||
onDeleteClicked = {},
|
||||
|
|
|
|||
|
|
@ -88,13 +88,14 @@ import com.google.maps.android.compose.Polyline
|
|||
import com.google.maps.android.compose.TileOverlay
|
||||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||
import com.google.maps.android.compose.widgets.ScaleBar
|
||||
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.metersIn
|
||||
import org.meshtastic.core.model.util.mpsToKmph
|
||||
import org.meshtastic.core.model.util.mpsToMph
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.alt
|
||||
|
|
@ -253,10 +254,7 @@ fun MapView(
|
|||
}
|
||||
}
|
||||
|
||||
val allNodes by
|
||||
mapViewModel.nodes
|
||||
.map { nodes -> nodes.filter { node -> node.validPosition != null } }
|
||||
.collectAsStateWithLifecycle(listOf())
|
||||
val allNodes by mapViewModel.nodesWithPosition.collectAsStateWithLifecycle(listOf())
|
||||
val waypoints by mapViewModel.waypoints.collectAsStateWithLifecycle(emptyMap())
|
||||
val displayableWaypoints = waypoints.values.mapNotNull { it.data.waypoint }
|
||||
val selectedWaypointId by mapViewModel.selectedWaypointId.collectAsStateWithLifecycle()
|
||||
|
|
@ -275,7 +273,7 @@ fun MapView(
|
|||
.filter { node -> !mapFilterState.onlyFavorites || node.isFavorite || node.num == ourNodeInfo?.num }
|
||||
.filter { node ->
|
||||
mapFilterState.lastHeardFilter.seconds == 0L ||
|
||||
(System.currentTimeMillis() / 1000 - node.lastHeard) <= mapFilterState.lastHeardFilter.seconds ||
|
||||
(nowSeconds - node.lastHeard) <= mapFilterState.lastHeardFilter.seconds ||
|
||||
node.num == ourNodeInfo?.num
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +475,7 @@ fun MapView(
|
|||
val timeFilteredPositions =
|
||||
nodeTracks.filter {
|
||||
lastHeardTrackFilter == LastHeardFilter.Any ||
|
||||
it.time > System.currentTimeMillis() / 1000 - lastHeardTrackFilter.seconds
|
||||
it.time > nowSeconds - lastHeardTrackFilter.seconds
|
||||
}
|
||||
val sortedPositions = timeFilteredPositions.sortedBy { it.time }
|
||||
allNodes
|
||||
|
|
@ -537,7 +535,12 @@ fun MapView(
|
|||
cluster.items.forEach { bounds.include(it.position) }
|
||||
coroutineScope.launch {
|
||||
cameraPositionState.animate(
|
||||
CameraUpdateFactory.newLatLngBounds(bounds.build(), 100),
|
||||
CameraUpdateFactory.newCameraPosition(
|
||||
CameraPosition.Builder()
|
||||
.target(bounds.build().center)
|
||||
.zoom(cameraPositionState.position.zoom + 1)
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
}
|
||||
Logger.d { "Cluster clicked! $cluster" }
|
||||
|
|
@ -733,7 +736,7 @@ internal fun unicodeEmojiToBitmap(icon: Int): BitmapDescriptor {
|
|||
|
||||
@Suppress("NestedBlockDepth")
|
||||
fun Uri.getFileName(context: android.content.Context): String {
|
||||
var name = this.lastPathSegment ?: "layer_${System.currentTimeMillis()}"
|
||||
var name = this.lastPathSegment ?: "layer_$nowMillis"
|
||||
if (this.scheme == "content") {
|
||||
context.contentResolver.query(this, null, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
|
|
@ -800,7 +803,7 @@ private fun speedFromPosition(position: Position, displayUnits: DisplayUnits): S
|
|||
when (displayUnits) {
|
||||
DisplayUnits.METRIC -> "%.1f Km/h".format(speedInMps.mpsToKmph())
|
||||
DisplayUnits.IMPERIAL -> "%.1f mph".format(speedInMps.mpsToMph())
|
||||
else -> mpsText // Fallback or handle UNRECOGNIZED
|
||||
else -> mpsText
|
||||
}
|
||||
} else {
|
||||
mpsText
|
||||
|
|
|
|||
|
|
@ -60,7 +60,17 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.atTime
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.nowInstant
|
||||
import org.meshtastic.core.model.util.systemTimeZone
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.date
|
||||
|
|
@ -75,8 +85,7 @@ import org.meshtastic.core.strings.waypoint_edit
|
|||
import org.meshtastic.core.strings.waypoint_new
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
|
||||
|
|
@ -95,7 +104,7 @@ fun EditWaypointDialog(
|
|||
var showEmojiPickerView by remember { mutableStateOf(false) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val calendar = remember { Calendar.getInstance() }
|
||||
val tz = systemTimeZone
|
||||
|
||||
// Initialize date and time states from waypointInput.expire
|
||||
var selectedDateString by remember { mutableStateOf("") }
|
||||
|
|
@ -106,20 +115,23 @@ fun EditWaypointDialog(
|
|||
|
||||
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()
|
||||
dateFormat.timeZone = java.util.TimeZone.getDefault()
|
||||
timeFormat.timeZone = java.util.TimeZone.getDefault()
|
||||
|
||||
LaunchedEffect(waypointInput.expire, isExpiryEnabled) {
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (isExpiryEnabled) {
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
calendar.timeInMillis = expireValue * 1000L
|
||||
selectedDateString = dateFormat.format(calendar.time)
|
||||
selectedTimeString = timeFormat.format(calendar.time)
|
||||
val instant = Instant.fromEpochSeconds(expireValue.toLong())
|
||||
val date = instant.toDate()
|
||||
selectedDateString = dateFormat.format(date)
|
||||
selectedTimeString = timeFormat.format(date)
|
||||
} else { // If enabled but not set, default to 8 hours from now
|
||||
calendar.timeInMillis = System.currentTimeMillis()
|
||||
calendar.add(Calendar.HOUR_OF_DAY, 8)
|
||||
waypointInput = waypointInput.copy(expire = (calendar.timeInMillis / 1000).toInt())
|
||||
val futureInstant = nowInstant + 8.hours
|
||||
val date = futureInstant.toDate()
|
||||
selectedDateString = dateFormat.format(date)
|
||||
selectedTimeString = timeFormat.format(date)
|
||||
waypointInput = waypointInput.copy(expire = futureInstant.epochSeconds.toInt())
|
||||
}
|
||||
} else {
|
||||
selectedDateString = ""
|
||||
|
|
@ -213,12 +225,9 @@ fun EditWaypointDialog(
|
|||
val expireValue = waypointInput.expire ?: 0
|
||||
// Default to 8 hours from now if not already set
|
||||
if (expireValue == 0 || expireValue == Int.MAX_VALUE) {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.timeInMillis = System.currentTimeMillis()
|
||||
cal.add(Calendar.HOUR_OF_DAY, 8)
|
||||
waypointInput = waypointInput.copy(expire = (cal.timeInMillis / 1000).toInt())
|
||||
val futureInstant = nowInstant + 8.hours
|
||||
waypointInput = waypointInput.copy(expire = futureInstant.epochSeconds.toInt())
|
||||
}
|
||||
// LaunchedEffect will update date/time strings
|
||||
} else {
|
||||
waypointInput = waypointInput.copy(expire = Int.MAX_VALUE)
|
||||
}
|
||||
|
|
@ -227,59 +236,83 @@ fun EditWaypointDialog(
|
|||
}
|
||||
|
||||
if (isExpiryEnabled) {
|
||||
val currentCalendar =
|
||||
Calendar.getInstance().apply {
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
timeInMillis = expireValue * 1000L
|
||||
val currentInstant =
|
||||
(waypointInput.expire ?: 0).let {
|
||||
if (it != 0 && it != Int.MAX_VALUE) {
|
||||
Instant.fromEpochSeconds(it.toLong())
|
||||
} else {
|
||||
timeInMillis = System.currentTimeMillis()
|
||||
add(Calendar.HOUR_OF_DAY, 8) // Default if re-enabling
|
||||
nowInstant + 8.hours
|
||||
}
|
||||
}
|
||||
val year = currentCalendar.get(Calendar.YEAR)
|
||||
val month = currentCalendar.get(Calendar.MONTH)
|
||||
val day = currentCalendar.get(Calendar.DAY_OF_MONTH)
|
||||
val hour = currentCalendar.get(Calendar.HOUR_OF_DAY)
|
||||
val minute = currentCalendar.get(Calendar.MINUTE)
|
||||
val ldt = currentInstant.toLocalDateTime(tz)
|
||||
|
||||
val datePickerDialog =
|
||||
DatePickerDialog(
|
||||
context,
|
||||
{ _: DatePicker, selectedYear: Int, selectedMonth: Int, selectedDay: Int ->
|
||||
val tempCal = Calendar.getInstance()
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
tempCal.timeInMillis = expireValue * 1000L
|
||||
} else {
|
||||
tempCal.add(Calendar.HOUR_OF_DAY, 8)
|
||||
}
|
||||
tempCal.set(selectedYear, selectedMonth, selectedDay)
|
||||
waypointInput = waypointInput.copy(expire = (tempCal.timeInMillis / 1000).toInt())
|
||||
val currentLdt =
|
||||
(waypointInput.expire ?: 0)
|
||||
.let {
|
||||
if (it != 0 && it != Int.MAX_VALUE) {
|
||||
Instant.fromEpochSeconds(it.toLong())
|
||||
} else {
|
||||
nowInstant + 8.hours
|
||||
}
|
||||
}
|
||||
.toLocalDateTime(tz)
|
||||
|
||||
val newLdt =
|
||||
LocalDate(
|
||||
year = selectedYear,
|
||||
month = Month(selectedMonth + 1),
|
||||
day = selectedDay,
|
||||
)
|
||||
.atTime(
|
||||
hour = currentLdt.hour,
|
||||
minute = currentLdt.minute,
|
||||
second = currentLdt.second,
|
||||
nanosecond = currentLdt.nanosecond,
|
||||
)
|
||||
waypointInput =
|
||||
waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt())
|
||||
},
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
ldt.year,
|
||||
ldt.month.number - 1,
|
||||
ldt.day,
|
||||
)
|
||||
|
||||
val timePickerDialog =
|
||||
TimePickerDialog(
|
||||
context,
|
||||
{ _: TimePicker, selectedHour: Int, selectedMinute: Int ->
|
||||
// Keep the existing date part
|
||||
val tempCal = Calendar.getInstance()
|
||||
val expireValue = waypointInput.expire ?: 0
|
||||
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
|
||||
tempCal.timeInMillis = expireValue * 1000L
|
||||
} else {
|
||||
tempCal.add(Calendar.HOUR_OF_DAY, 8)
|
||||
}
|
||||
tempCal.set(Calendar.HOUR_OF_DAY, selectedHour)
|
||||
tempCal.set(Calendar.MINUTE, selectedMinute)
|
||||
waypointInput = waypointInput.copy(expire = (tempCal.timeInMillis / 1000).toInt())
|
||||
val currentLdt =
|
||||
(waypointInput.expire ?: 0)
|
||||
.let {
|
||||
if (it != 0 && it != Int.MAX_VALUE) {
|
||||
Instant.fromEpochSeconds(it.toLong())
|
||||
} else {
|
||||
nowInstant + 8.hours
|
||||
}
|
||||
}
|
||||
.toLocalDateTime(tz)
|
||||
|
||||
val newLdt =
|
||||
LocalDate(
|
||||
year = currentLdt.year,
|
||||
month = currentLdt.month,
|
||||
day = currentLdt.day,
|
||||
)
|
||||
.atTime(
|
||||
hour = selectedHour,
|
||||
minute = selectedMinute,
|
||||
second = currentLdt.second,
|
||||
nanosecond = currentLdt.nanosecond,
|
||||
)
|
||||
waypointInput =
|
||||
waypointInput.copy(expire = newLdt.toInstant(tz).epochSeconds.toInt())
|
||||
},
|
||||
hour,
|
||||
minute,
|
||||
ldt.hour,
|
||||
ldt.minute,
|
||||
android.text.format.DateFormat.is24HourFormat(context),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
|
|
@ -31,16 +30,15 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun PulsingNodeChip(node: Node, modifier: Modifier = Modifier) {
|
||||
val animatedProgress = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(node) {
|
||||
if ((System.currentTimeMillis().milliseconds.inWholeSeconds - node.lastHeard.seconds.inWholeSeconds) <= 5) {
|
||||
if ((nowSeconds - node.lastHeard) <= 5) {
|
||||
launch {
|
||||
animatedProgress.snapTo(0f)
|
||||
animatedProgress.animateTo(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import org.meshtastic.core.data.repository.PacketRepository
|
|||
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.TimeConstants
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
|
|
@ -46,19 +48,18 @@ import org.meshtastic.feature.map.model.TracerouteOverlay
|
|||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
sealed class LastHeardFilter(val seconds: Long, val label: StringResource) {
|
||||
data object Any : LastHeardFilter(0L, Res.string.any)
|
||||
|
||||
data object OneHour : LastHeardFilter(TimeUnit.HOURS.toSeconds(1), Res.string.one_hour)
|
||||
data object OneHour : LastHeardFilter(TimeConstants.ONE_HOUR.inWholeSeconds, Res.string.one_hour)
|
||||
|
||||
data object EightHours : LastHeardFilter(TimeUnit.HOURS.toSeconds(8), Res.string.eight_hours)
|
||||
data object EightHours : LastHeardFilter(TimeConstants.EIGHT_HOURS.inWholeSeconds, Res.string.eight_hours)
|
||||
|
||||
data object OneDay : LastHeardFilter(TimeUnit.DAYS.toSeconds(1), Res.string.one_day)
|
||||
data object OneDay : LastHeardFilter(TimeConstants.ONE_DAY.inWholeSeconds, Res.string.one_day)
|
||||
|
||||
data object TwoDays : LastHeardFilter(TimeUnit.DAYS.toSeconds(2), Res.string.two_days)
|
||||
data object TwoDays : LastHeardFilter(TimeConstants.TWO_DAYS.inWholeSeconds, Res.string.two_days)
|
||||
|
||||
companion object {
|
||||
fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any
|
||||
|
|
@ -88,6 +89,11 @@ abstract class BaseMapViewModel(
|
|||
.map { nodes -> nodes.filterNot { node -> node.isIgnored } }
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val nodesWithPosition: StateFlow<List<Node>> =
|
||||
nodes
|
||||
.map { nodes -> nodes.filter { node -> node.validPosition != null } }
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val waypoints: StateFlow<Map<Int, Packet>> =
|
||||
packetRepository
|
||||
.getWaypoints()
|
||||
|
|
@ -96,7 +102,7 @@ abstract class BaseMapViewModel(
|
|||
.associateBy { packet -> packet.data.waypoint!!.id }
|
||||
.filterValues {
|
||||
val expire = it.data.waypoint!!.expire ?: 0
|
||||
expire == 0 || expire > System.currentTimeMillis() / 1000
|
||||
expire == 0 || expire > nowSeconds
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = emptyMap())
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -45,7 +46,7 @@ class MessageItemTest {
|
|||
rssi = 90,
|
||||
hopsAway = 0,
|
||||
uuid = 1L,
|
||||
receivedTime = System.currentTimeMillis(),
|
||||
receivedTime = nowMillis,
|
||||
node = testNode,
|
||||
read = false,
|
||||
routingError = 0,
|
||||
|
|
@ -84,7 +85,7 @@ class MessageItemTest {
|
|||
rssi = 90,
|
||||
hopsAway = 0,
|
||||
uuid = 1L,
|
||||
receivedTime = System.currentTimeMillis(),
|
||||
receivedTime = nowMillis,
|
||||
node = testNode,
|
||||
read = false,
|
||||
routingError = 0,
|
||||
|
|
@ -123,7 +124,7 @@ class MessageItemTest {
|
|||
rssi = 90,
|
||||
hopsAway = 0,
|
||||
uuid = 1L,
|
||||
receivedTime = System.currentTimeMillis(),
|
||||
receivedTime = nowMillis,
|
||||
node = testNode,
|
||||
read = false,
|
||||
routingError = 0,
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ fun MessageScreen(
|
|||
val selectedMessageIds = rememberSaveable { mutableStateOf(emptySet<Long>()) }
|
||||
val messageInputState = rememberTextFieldState(message)
|
||||
val showQuickChat by viewModel.showQuickChat.collectAsStateWithLifecycle()
|
||||
val filteredCount by viewModel.getFilteredCount(contactKey).collectAsStateWithLifecycle(initialValue = 0)
|
||||
val filteredCount by viewModel.filteredCount.collectAsStateWithLifecycle()
|
||||
val showFiltered by viewModel.showFiltered.collectAsStateWithLifecycle()
|
||||
val filteringDisabled = contactSettings[contactKey]?.filteringDisabled ?: false
|
||||
|
||||
|
|
@ -220,9 +220,8 @@ fun MessageScreen(
|
|||
val listState = rememberLazyListState()
|
||||
|
||||
// Track unread messages using lightweight metadata queries
|
||||
val hasUnreadMessages by viewModel.hasUnreadMessages(contactKey).collectAsStateWithLifecycle(initialValue = false)
|
||||
val firstUnreadMessageUuid by
|
||||
viewModel.getFirstUnreadMessageUuid(contactKey).collectAsStateWithLifecycle(initialValue = null)
|
||||
val hasUnreadMessages by viewModel.hasUnreadMessages.collectAsStateWithLifecycle()
|
||||
val firstUnreadMessageUuid by viewModel.firstUnreadMessageUuid.collectAsStateWithLifecycle()
|
||||
|
||||
var hasPerformedInitialScroll by rememberSaveable(contactKey) { mutableStateOf(false) }
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,24 @@ constructor(
|
|||
|
||||
val homoglyphEncodingEnabled = homoglyphEncodingPrefs.getHomoglyphEncodingEnabledChangesFlow()
|
||||
|
||||
val firstUnreadMessageUuid: StateFlow<Long?> =
|
||||
contactKeyForPagedMessages
|
||||
.filterNotNull()
|
||||
.flatMapLatest { packetRepository.getFirstUnreadMessageUuid(it) }
|
||||
.stateInWhileSubscribed(null)
|
||||
|
||||
val hasUnreadMessages: StateFlow<Boolean> =
|
||||
contactKeyForPagedMessages
|
||||
.filterNotNull()
|
||||
.flatMapLatest { packetRepository.hasUnreadMessages(it) }
|
||||
.stateInWhileSubscribed(false)
|
||||
|
||||
val filteredCount: StateFlow<Int> =
|
||||
contactKeyForPagedMessages
|
||||
.filterNotNull()
|
||||
.flatMapLatest { packetRepository.getFilteredCountFlow(it) }
|
||||
.stateInWhileSubscribed(0)
|
||||
|
||||
init {
|
||||
val contactKey = savedStateHandle.get<String>("contactKey")
|
||||
if (contactKey != null) {
|
||||
|
|
@ -142,19 +160,12 @@ constructor(
|
|||
return pagedMessagesForContactKey
|
||||
}
|
||||
|
||||
fun getFirstUnreadMessageUuid(contactKey: String): Flow<Long?> =
|
||||
packetRepository.getFirstUnreadMessageUuid(contactKey)
|
||||
|
||||
fun hasUnreadMessages(contactKey: String): Flow<Boolean> = packetRepository.hasUnreadMessages(contactKey)
|
||||
|
||||
fun toggleShowQuickChat() = toggle(_showQuickChat) { uiPrefs.showQuickChat = it }
|
||||
|
||||
fun toggleShowFiltered() {
|
||||
_showFiltered.update { !it }
|
||||
}
|
||||
|
||||
fun getFilteredCount(contactKey: String): Flow<Int> = packetRepository.getFilteredCountFlow(contactKey)
|
||||
|
||||
fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) { packetRepository.setContactFilteringDisabled(contactKey, disabled) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import org.meshtastic.core.database.entity.Reaction
|
|||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.filter_message_label
|
||||
import org.meshtastic.core.strings.message_delivery_status
|
||||
|
|
@ -459,7 +460,7 @@ private fun MessageItemPreview() {
|
|||
rssi = 90,
|
||||
hopsAway = 0,
|
||||
uuid = 1L,
|
||||
receivedTime = System.currentTimeMillis(),
|
||||
receivedTime = nowMillis,
|
||||
node = NodePreviewParameterProvider().mickeyMouse,
|
||||
read = false,
|
||||
routingError = 0,
|
||||
|
|
@ -478,7 +479,7 @@ private fun MessageItemPreview() {
|
|||
rssi = 90,
|
||||
hopsAway = 0,
|
||||
uuid = 2L,
|
||||
receivedTime = System.currentTimeMillis(),
|
||||
receivedTime = nowMillis,
|
||||
node = NodePreviewParameterProvider().minnieMouse,
|
||||
read = false,
|
||||
routingError = 0,
|
||||
|
|
@ -497,7 +498,7 @@ private fun MessageItemPreview() {
|
|||
rssi = 90,
|
||||
hopsAway = 2,
|
||||
uuid = 2L,
|
||||
receivedTime = System.currentTimeMillis(),
|
||||
receivedTime = nowMillis,
|
||||
node = NodePreviewParameterProvider().minnieMouse,
|
||||
read = false,
|
||||
routingError = 0,
|
||||
|
|
@ -517,7 +518,7 @@ private fun MessageItemPreview() {
|
|||
rssi = 70,
|
||||
hopsAway = 1,
|
||||
uuid = 3L,
|
||||
receivedTime = System.currentTimeMillis(),
|
||||
receivedTime = nowMillis,
|
||||
node = NodePreviewParameterProvider().minnieMouse,
|
||||
read = false,
|
||||
routingError = 0,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.util.bearing
|
||||
import org.meshtastic.core.model.util.latLongToMeter
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.ui.component.precisionBitsToMeters
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
@ -47,7 +49,6 @@ import kotlin.math.sqrt
|
|||
private const val ALIGNMENT_TOLERANCE_DEGREES = 5f
|
||||
private const val FULL_CIRCLE_DEGREES = 360f
|
||||
private const val BEARING_FORMAT = "%.0f°"
|
||||
private const val MILLIS_PER_SECOND = 1000
|
||||
private const val SECONDS_PER_HOUR = 3600
|
||||
private const val SECONDS_PER_MINUTE = 60
|
||||
private const val HUNDRED = 100f
|
||||
|
|
@ -192,17 +193,12 @@ constructor(
|
|||
val loc = locationState.location ?: return heading
|
||||
val baseHeading = heading ?: return null
|
||||
val geomagnetic =
|
||||
GeomagneticField(
|
||||
loc.latitude.toFloat(),
|
||||
loc.longitude.toFloat(),
|
||||
loc.altitude.toFloat(),
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
GeomagneticField(loc.latitude.toFloat(), loc.longitude.toFloat(), loc.altitude.toFloat(), nowMillis)
|
||||
return (baseHeading + geomagnetic.declination + FULL_CIRCLE_DEGREES) % FULL_CIRCLE_DEGREES
|
||||
}
|
||||
|
||||
private fun formatElapsed(timestampSec: Long): String {
|
||||
val nowSec = System.currentTimeMillis() / MILLIS_PER_SECOND
|
||||
val nowSec = nowSeconds
|
||||
val diff = maxOf(0, nowSec - timestampSec)
|
||||
val hours = diff / SECONDS_PER_HOUR
|
||||
val minutes = (diff % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
|
|||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Refresh
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
|
@ -56,7 +57,7 @@ fun CooldownIconButton(
|
|||
progress.snapTo(0f)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
val timeSinceLast = System.currentTimeMillis() - cooldownTimestamp
|
||||
val timeSinceLast = nowMillis - cooldownTimestamp
|
||||
if (timeSinceLast < cooldownDuration) {
|
||||
val remainingTime = cooldownDuration - timeSinceLast
|
||||
progress.snapTo(remainingTime / cooldownDuration.toFloat())
|
||||
|
|
@ -106,7 +107,7 @@ fun CooldownOutlinedIconButton(
|
|||
progress.snapTo(0f)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
val timeSinceLast = System.currentTimeMillis() - cooldownTimestamp
|
||||
val timeSinceLast = nowMillis - cooldownTimestamp
|
||||
if (timeSinceLast < cooldownDuration) {
|
||||
val remainingTime = cooldownDuration - timeSinceLast
|
||||
progress.snapTo(remainingTime / cooldownDuration.toFloat())
|
||||
|
|
@ -146,7 +147,7 @@ fun CooldownOutlinedIconButton(
|
|||
@Composable
|
||||
private fun CooldownOutlinedIconButtonPreview() {
|
||||
AppTheme {
|
||||
CooldownOutlinedIconButton(onClick = {}, cooldownTimestamp = System.currentTimeMillis() - 15000L) {
|
||||
CooldownOutlinedIconButton(onClick = {}, cooldownTimestamp = nowMillis - 15000L) {
|
||||
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ 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.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.node_sort_last_heard
|
||||
import org.meshtastic.core.ui.R
|
||||
|
|
@ -34,13 +35,14 @@ import org.meshtastic.core.ui.util.formatAgo
|
|||
fun LastHeardInfo(
|
||||
modifier: Modifier = Modifier,
|
||||
lastHeard: Int,
|
||||
showLabel: Boolean = true,
|
||||
contentColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24),
|
||||
contentDescription = stringResource(Res.string.node_sort_last_heard),
|
||||
label = stringResource(Res.string.node_sort_last_heard),
|
||||
label = if (showLabel) stringResource(Res.string.node_sort_last_heard) else null,
|
||||
text = formatAgo(lastHeard),
|
||||
contentColor = contentColor,
|
||||
)
|
||||
|
|
@ -49,5 +51,5 @@ fun LastHeardInfo(
|
|||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun LastHeardInfoPreview() {
|
||||
AppTheme { LastHeardInfo(lastHeard = (System.currentTimeMillis() / 1000).toInt() - 8600) }
|
||||
AppTheme { LastHeardInfo(lastHeard = nowSeconds.toInt() - 8600) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.neighbor_info
|
||||
|
|
@ -83,7 +84,7 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return@launch
|
||||
serviceRepository.meshService?.requestNeighborInfo(packetId, destNum)
|
||||
_lastRequestNeighborTimes.update { it + (destNum to System.currentTimeMillis()) }
|
||||
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
Res.string.requesting_from,
|
||||
|
|
@ -146,7 +147,7 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return@launch
|
||||
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
|
||||
_lastTracerouteTimes.update { it + (destNum to System.currentTimeMillis()) }
|
||||
_lastTracerouteTimes.update { it + (destNum to nowMillis) }
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(Res.string.traceroute, longName)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ import com.patrykandpatrick.vico.compose.cartesian.CartesianDrawingContext
|
|||
import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.model.util.toInstant
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.strings.delete
|
||||
|
|
@ -70,7 +73,7 @@ import org.meshtastic.core.strings.snr
|
|||
import org.meshtastic.core.ui.icon.Delete
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
object CommonCharts {
|
||||
val DATE_TIME_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
|
|
@ -99,15 +102,15 @@ object CommonCharts {
|
|||
|
||||
/** A dynamic [CartesianValueFormatter] that adjusts the time format based on the visible X range. */
|
||||
val dynamicTimeFormatter = CartesianValueFormatter { context, value, _ ->
|
||||
val date = Date((value * MS_PER_SEC.toDouble()).toLong())
|
||||
val date = (value * MS_PER_SEC.toDouble()).toLong().toInstant().toDate()
|
||||
val xLength = context.ranges.xLength
|
||||
val zoom = if (context is CartesianDrawingContext) context.zoom else 1f
|
||||
val visibleSpan = xLength / zoom
|
||||
|
||||
when {
|
||||
visibleSpan <= 3600 -> TIME_SECONDS_FORMAT.format(date) // < 1 hour visible
|
||||
visibleSpan <= 86400 * 2 -> TIME_MINUTE_FORMAT.format(date) // < 2 days visible
|
||||
visibleSpan <= 86400 * 14 -> {
|
||||
visibleSpan <= TimeConstants.ONE_HOUR.inWholeSeconds -> TIME_SECONDS_FORMAT.format(date) // < 1 hour visible
|
||||
visibleSpan <= 2.days.inWholeSeconds -> TIME_MINUTE_FORMAT.format(date) // < 2 days visible
|
||||
visibleSpan <= 14.days.inWholeSeconds -> {
|
||||
// < 2 weeks visible: separate date and time with a newline
|
||||
val dateStr = DATE_FORMAT.format(date)
|
||||
val timeStr = TIME_MINUTE_FORMAT.format(date)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.air_util_definition
|
||||
import org.meshtastic.core.strings.air_utilization
|
||||
|
|
@ -352,7 +353,7 @@ private fun DeviceMetricsChart(
|
|||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DeviceMetricsChartPreview() {
|
||||
val now = (System.currentTimeMillis() / 1000).toInt()
|
||||
val now = nowSeconds.toInt()
|
||||
val telemetries =
|
||||
List(20) { i ->
|
||||
Telemetry(
|
||||
|
|
@ -468,7 +469,7 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
|
|||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DeviceMetricsCardPreview() {
|
||||
val now = (System.currentTimeMillis() / 1000).toInt()
|
||||
val now = nowSeconds.toInt()
|
||||
val telemetry =
|
||||
Telemetry(
|
||||
time = now,
|
||||
|
|
@ -488,7 +489,7 @@ private fun DeviceMetricsCardPreview() {
|
|||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun DeviceMetricsScreenPreview() {
|
||||
val now = (System.currentTimeMillis() / 1000).toInt()
|
||||
val now = nowSeconds.toInt()
|
||||
val telemetries =
|
||||
List(24) { i ->
|
||||
Telemetry(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.meshtastic.core.strings.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.env_metrics_log
|
||||
|
|
@ -431,8 +432,7 @@ private fun PreviewEnvironmentMetricsContent() {
|
|||
radiation = 0.15f,
|
||||
gas_resistance = 1200.0f,
|
||||
)
|
||||
val fakeTelemetry =
|
||||
Telemetry(time = (System.currentTimeMillis() / 1000).toInt(), environment_metrics = fakeEnvMetrics)
|
||||
val fakeTelemetry = Telemetry(time = nowSeconds.toInt(), environment_metrics = fakeEnvMetrics)
|
||||
MaterialTheme {
|
||||
Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import com.meshtastic.core.strings.getString
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.disk_free_indexed
|
||||
import org.meshtastic.core.strings.free_memory
|
||||
|
|
@ -129,7 +130,7 @@ fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), o
|
|||
@Composable
|
||||
fun HostMetricsItem(modifier: Modifier = Modifier, telemetry: Telemetry) {
|
||||
val hostMetrics = telemetry.host_metrics
|
||||
val time = telemetry.time * CommonCharts.MS_PER_SEC
|
||||
val time = telemetry.time.toLong() * CommonCharts.MS_PER_SEC
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth().padding(vertical = 4.dp).combinedClickable(onClick = { /* Handle click */ }),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
|
|
@ -281,6 +282,6 @@ private fun HostMetricsItemPreview() {
|
|||
load15 = 19,
|
||||
user_string = "test",
|
||||
)
|
||||
val logs = Telemetry(time = (System.currentTimeMillis() / 1000).toInt(), host_metrics = hostMetrics)
|
||||
val logs = Telemetry(time = nowSeconds.toInt(), host_metrics = hostMetrics)
|
||||
AppTheme { HostMetricsItem(telemetry = logs) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ import org.meshtastic.core.model.DataPacket
|
|||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.model.util.toInstant
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
|
|
@ -207,7 +210,7 @@ constructor(
|
|||
combine(_state, environmentState) { state, envState ->
|
||||
val stateOldest = state.oldestTimestampSeconds()
|
||||
val envOldest = envState.environmentMetrics.minOfOrNull { (it.time ?: 0).toLong() }?.takeIf { it > 0 }
|
||||
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: (System.currentTimeMillis() / 1000L)
|
||||
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds
|
||||
TimeFrame.entries.filter { it.isAvailable(oldest) }
|
||||
}
|
||||
.stateInWhileSubscribed(TimeFrame.entries)
|
||||
|
|
@ -519,7 +522,7 @@ constructor(
|
|||
val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
|
||||
|
||||
positions.forEach { position ->
|
||||
val rxDateTime = dateFormat.format((position.time ?: 0).toLong() * 1000L)
|
||||
val rxDateTime = dateFormat.format(((position.time ?: 0).toLong() * 1000L).toInstant().toDate())
|
||||
val latitude = (position.latitude_i ?: 0) * 1e-7
|
||||
val longitude = (position.longitude_i ?: 0) * 1e-7
|
||||
val altitude = position.altitude
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.model.util.toInstant
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.ble_devices
|
||||
import org.meshtastic.core.strings.no_pax_metrics_logs
|
||||
|
|
@ -72,7 +74,6 @@ import org.meshtastic.core.ui.theme.GraphColors.Orange
|
|||
import org.meshtastic.core.ui.theme.GraphColors.Purple
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import org.meshtastic.proto.Paxcount as ProtoPaxcount
|
||||
|
||||
private enum class PaxSeries(val color: Color, val legendRes: StringResource) {
|
||||
|
|
@ -198,7 +199,7 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
|
|||
val graphData =
|
||||
paxMetrics
|
||||
.map {
|
||||
val t = (it.first.received_date / 1000).toInt()
|
||||
val t = (it.first.received_date / CommonCharts.MS_PER_SEC).toInt()
|
||||
Triple(t, it.second.ble ?: 0, it.second.wifi ?: 0)
|
||||
}
|
||||
.sortedBy { it.first }
|
||||
|
|
@ -212,7 +213,7 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
|
|||
titleRes = Res.string.pax_metrics_log,
|
||||
nodeName = state.node?.user?.long_name ?: "",
|
||||
data = paxMetrics,
|
||||
timeProvider = { (it.first.received_date / 1000).toDouble() },
|
||||
timeProvider = { (it.first.received_date / CommonCharts.MS_PER_SEC).toDouble() },
|
||||
snackbarHostState = snackbarHostState,
|
||||
onRequestTelemetry = { metricsViewModel.requestTelemetry(TelemetryType.PAX) },
|
||||
controlPart = {
|
||||
|
|
@ -254,8 +255,8 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
|
|||
log = log,
|
||||
pax = pax,
|
||||
dateFormat = dateFormat,
|
||||
isSelected = (log.received_date / 1000).toDouble() == selectedX,
|
||||
onClick = { onCardClick((log.received_date / 1000).toDouble()) },
|
||||
isSelected = (log.received_date / CommonCharts.MS_PER_SEC).toDouble() == selectedX,
|
||||
onClick = { onCardClick((log.received_date / CommonCharts.MS_PER_SEC).toDouble()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -297,7 +298,7 @@ fun PaxMetricsItem(log: MeshLog, pax: ProtoPaxcount, dateFormat: DateFormat, isS
|
|||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(12.dp)) {
|
||||
Text(
|
||||
text = dateFormat.format(Date(log.received_date)),
|
||||
text = dateFormat.format(log.received_date.toInstant().toDate()),
|
||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.meshtastic.core.strings.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.alt
|
||||
|
|
@ -270,7 +271,7 @@ private val testPosition =
|
|||
longitude_i = -953698040,
|
||||
altitude = 1230,
|
||||
sats_in_view = 7,
|
||||
time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
time = nowSeconds.toInt(),
|
||||
)
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import org.jetbrains.compose.resources.pluralStringResource
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
|
|
@ -279,7 +280,7 @@ private fun TracerouteItemPreview() {
|
|||
val time =
|
||||
DateUtils.formatDateTime(
|
||||
LocalContext.current,
|
||||
System.currentTimeMillis(),
|
||||
nowMillis,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
|
||||
)
|
||||
AppTheme {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package org.meshtastic.feature.node.model
|
||||
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.all_time
|
||||
import org.meshtastic.core.strings.one_hour_short
|
||||
|
|
@ -35,7 +36,7 @@ enum class TimeFrame(val strRes: StringResource, val seconds: Long) {
|
|||
ALL_TIME(Res.string.all_time, 0),
|
||||
;
|
||||
|
||||
fun timeThreshold(now: Long = System.currentTimeMillis() / 1000L): Long {
|
||||
fun timeThreshold(now: Long = nowSeconds): Long {
|
||||
if (this == ALL_TIME) return 0
|
||||
return now - seconds
|
||||
}
|
||||
|
|
@ -44,7 +45,7 @@ enum class TimeFrame(val strRes: StringResource, val seconds: Long) {
|
|||
* Checks if this time frame is relevant given the oldest available data point. We show the option if the data
|
||||
* extends at least into this timeframe.
|
||||
*/
|
||||
fun isAvailable(oldestTimestampSeconds: Long, now: Long = System.currentTimeMillis() / 1000L): Boolean {
|
||||
fun isAvailable(oldestTimestampSeconds: Long, now: Long = nowSeconds): Boolean {
|
||||
if (this == ALL_TIME || this == ONE_HOUR) return true
|
||||
val rangeSeconds = now - oldestTimestampSeconds
|
||||
return rangeSeconds >= seconds
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package org.meshtastic.feature.node.metrics
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ class EnvironmentMetricsStateTest {
|
|||
|
||||
@Test
|
||||
fun `environmentMetricsForGraphing correctly calculates times`() {
|
||||
val now = (System.currentTimeMillis() / 1000).toInt()
|
||||
val now = nowSeconds.toInt()
|
||||
val metrics =
|
||||
listOf(
|
||||
Telemetry(time = now - 100, environment_metrics = EnvironmentMetrics(temperature = 20f)),
|
||||
|
|
@ -42,7 +43,7 @@ class EnvironmentMetricsStateTest {
|
|||
|
||||
@Test
|
||||
fun `environmentMetricsForGraphing handles valid zero temperatures`() {
|
||||
val now = (System.currentTimeMillis() / 1000).toInt()
|
||||
val now = nowSeconds.toInt()
|
||||
val metrics = listOf(Telemetry(time = now, environment_metrics = EnvironmentMetrics(temperature = 0.0f)))
|
||||
val state = EnvironmentMetricsState(metrics)
|
||||
val result = state.environmentMetricsForGraphing()
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ import org.jetbrains.compose.resources.StringResource
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.gpsDisabled
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.model.util.toInstant
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.strings.Res
|
||||
|
|
@ -112,7 +115,6 @@ import org.meshtastic.feature.settings.util.LanguageUtils
|
|||
import org.meshtastic.feature.settings.util.LanguageUtils.languageMap
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
|
@ -184,8 +186,8 @@ fun SettingsScreen(
|
|||
} else {
|
||||
deviceProfile = it
|
||||
val nodeName = (it.short_name ?: "").ifBlank { "node" }
|
||||
val dateFormat = java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.getDefault())
|
||||
val dateStr = dateFormat.format(java.util.Date())
|
||||
val dateFormat = SimpleDateFormat("yyyyMMdd", Locale.getDefault())
|
||||
val dateStr = dateFormat.format(nowMillis.toInstant().toDate())
|
||||
val fileName = "Meshtastic_${nodeName}_${dateStr}_nodeConfig.cfg"
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
|
|
@ -366,7 +368,7 @@ fun SettingsScreen(
|
|||
summary = stringResource(Res.string.device_db_cache_limit_summary),
|
||||
)
|
||||
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(nowMillis.toInstant().toDate())
|
||||
val nodeName = ourNode?.user?.short_name ?: ""
|
||||
|
||||
val exportRangeTestLauncher =
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.model.util.toInstant
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.debug_clear
|
||||
import org.meshtastic.core.strings.debug_decoded_payload
|
||||
|
|
@ -111,7 +114,6 @@ import java.io.IOException
|
|||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE)
|
||||
|
|
@ -199,7 +201,8 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewMo
|
|||
filterMode = filterMode,
|
||||
onFilterModeChange = { filterMode = it },
|
||||
onExportLogs = {
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val timestamp =
|
||||
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(nowMillis.toInstant().toDate())
|
||||
val fileName = "meshtastic_debug_$timestamp.txt"
|
||||
exportLogsLauncher.launch(fileName)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ import org.meshtastic.core.database.entity.MeshLog
|
|||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.core.model.util.nowInstant
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.model.util.toInstant
|
||||
import org.meshtastic.core.model.util.toReadableString
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
import org.meshtastic.core.strings.Res
|
||||
|
|
@ -60,7 +63,6 @@ import org.meshtastic.proto.Telemetry
|
|||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -306,7 +308,7 @@ constructor(
|
|||
UiMeshLog(
|
||||
uuid = log.uuid,
|
||||
messageType = log.message_type,
|
||||
formattedReceivedDate = TIME_FORMAT.format(log.received_date),
|
||||
formattedReceivedDate = TIME_FORMAT.format(log.received_date.toInstant().toDate()),
|
||||
logMessage = annotateMeshLogMessage(log),
|
||||
decodedPayload = decodePayloadFromMeshLog(log),
|
||||
)
|
||||
|
|
@ -430,7 +432,7 @@ constructor(
|
|||
// decoded
|
||||
add("decoded")
|
||||
// today (locale-dependent short date format)
|
||||
add(DateFormat.getDateInstance(DateFormat.SHORT).format(Date()))
|
||||
add(DateFormat.getDateInstance(DateFormat.SHORT).format(nowInstant.toDate()))
|
||||
// Each app name
|
||||
addAll(PortNum.entries.map { it.name })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.model.util.nowSeconds
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.are_you_sure
|
||||
|
|
@ -33,7 +34,6 @@ import org.meshtastic.core.strings.clean_now
|
|||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private const val MIN_DAYS_THRESHOLD = 7f
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ constructor(
|
|||
fun getNodesToDelete() {
|
||||
viewModelScope.launch {
|
||||
val onlyUnknownEnabled = _onlyUnknownNodes.value
|
||||
val currentTimeSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds
|
||||
val currentTimeSeconds = nowSeconds
|
||||
val sevenDaysAgoSeconds = currentTimeSeconds - 7.days.inWholeSeconds
|
||||
val olderThanTimestamp = currentTimeSeconds - _olderThanDays.value.toInt().days.inWholeSeconds
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import org.meshtastic.core.database.entity.MyNodeEntity
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.getStringResFrom
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.toChannelSet
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
|
|
@ -520,7 +521,7 @@ constructor(
|
|||
// Create a JSON object
|
||||
val jsonObject =
|
||||
JSONObject().apply {
|
||||
put("timestamp", System.currentTimeMillis())
|
||||
put("timestamp", nowMillis)
|
||||
put("public_key", publicKeyBase64)
|
||||
put("private_key", privateKeyBase64)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.systemTimeZone
|
||||
import org.meshtastic.core.model.util.toPosixString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.accept
|
||||
import org.meshtastic.core.strings.are_you_sure
|
||||
|
|
@ -115,12 +117,10 @@ import org.meshtastic.core.ui.component.EditTextPreference
|
|||
import org.meshtastic.core.ui.component.InsetDivider
|
||||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.core.ui.timezone.toPosixString
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.Config
|
||||
import java.time.ZoneId
|
||||
|
||||
private val Config.DeviceConfig.Role.description: StringResource
|
||||
get() =
|
||||
|
|
@ -259,12 +259,12 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
|||
TitledCard(title = stringResource(Res.string.time_zone)) {
|
||||
val context = LocalContext.current
|
||||
val appTzPosixString by
|
||||
produceState(initialValue = ZoneId.systemDefault().toPosixString()) {
|
||||
produceState(initialValue = systemTimeZone.toPosixString()) {
|
||||
val receiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) {
|
||||
value = ZoneId.systemDefault().toPosixString()
|
||||
value = systemTimeZone.toPosixString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import okio.ByteString
|
|||
import okio.ByteString.Companion.toByteString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.encodeToString
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.admin_key
|
||||
import org.meshtastic.core.strings.admin_keys
|
||||
|
|
@ -122,10 +123,7 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/*"
|
||||
putExtra(
|
||||
Intent.EXTRA_TITLE,
|
||||
"${node?.user?.short_name}_keys_${System.currentTimeMillis()}.json",
|
||||
)
|
||||
putExtra(Intent.EXTRA_TITLE, "${node?.user?.short_name}_keys_$nowMillis.json")
|
||||
}
|
||||
exportConfigLauncher.launch(intent)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,20 +14,22 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("MagicNumber")
|
||||
|
||||
package org.meshtastic.feature.settings.util
|
||||
|
||||
import org.jetbrains.compose.resources.PluralStringResource
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.interval_always_on
|
||||
import org.meshtastic.core.strings.interval_unset
|
||||
import org.meshtastic.core.strings.plurals_hours
|
||||
import org.meshtastic.core.strings.plurals_minutes
|
||||
import org.meshtastic.core.strings.plurals_seconds
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Defines a set of fixed time intervals in seconds, commonly used for configuration settings.
|
||||
|
|
@ -42,39 +44,39 @@ enum class FixedUpdateIntervals(
|
|||
val quantity: Int? = null,
|
||||
) {
|
||||
UNSET(0L, textRes = Res.string.interval_unset),
|
||||
ONE_SECOND(1L, pluralRes = Res.plurals.plurals_seconds, quantity = 1),
|
||||
TWO_SECONDS(2L, pluralRes = Res.plurals.plurals_seconds, quantity = 2),
|
||||
THREE_SECONDS(3L, pluralRes = Res.plurals.plurals_seconds, quantity = 3),
|
||||
FOUR_SECONDS(4L, pluralRes = Res.plurals.plurals_seconds, quantity = 4),
|
||||
FIVE_SECONDS(5L, pluralRes = Res.plurals.plurals_seconds, quantity = 5),
|
||||
TEN_SECONDS(10L, pluralRes = Res.plurals.plurals_seconds, quantity = 10),
|
||||
FIFTEEN_SECONDS(15L, pluralRes = Res.plurals.plurals_seconds, quantity = 15),
|
||||
TWENTY_SECONDS(20L, pluralRes = Res.plurals.plurals_seconds, quantity = 20),
|
||||
THIRTY_SECONDS(30L, pluralRes = Res.plurals.plurals_seconds, quantity = 30),
|
||||
FORTY_FIVE_SECONDS(45L, pluralRes = Res.plurals.plurals_seconds, quantity = 45),
|
||||
ONE_MINUTE(TimeUnit.MINUTES.toSeconds(1), pluralRes = Res.plurals.plurals_minutes, quantity = 1),
|
||||
TWO_MINUTES(TimeUnit.MINUTES.toSeconds(2), pluralRes = Res.plurals.plurals_minutes, quantity = 2),
|
||||
FIVE_MINUTES(TimeUnit.MINUTES.toSeconds(5), pluralRes = Res.plurals.plurals_minutes, quantity = 5),
|
||||
TEN_MINUTES(TimeUnit.MINUTES.toSeconds(10), pluralRes = Res.plurals.plurals_minutes, quantity = 10),
|
||||
FIFTEEN_MINUTES(TimeUnit.MINUTES.toSeconds(15), pluralRes = Res.plurals.plurals_minutes, quantity = 15),
|
||||
THIRTY_MINUTES(TimeUnit.MINUTES.toSeconds(30), pluralRes = Res.plurals.plurals_minutes, quantity = 30),
|
||||
ONE_HOUR(TimeUnit.HOURS.toSeconds(1), pluralRes = Res.plurals.plurals_hours, quantity = 1),
|
||||
TWO_HOURS(TimeUnit.HOURS.toSeconds(2), pluralRes = Res.plurals.plurals_hours, quantity = 2),
|
||||
THREE_HOURS(TimeUnit.HOURS.toSeconds(3), pluralRes = Res.plurals.plurals_hours, quantity = 3),
|
||||
FOUR_HOURS(TimeUnit.HOURS.toSeconds(4), pluralRes = Res.plurals.plurals_hours, quantity = 4),
|
||||
FIVE_HOURS(TimeUnit.HOURS.toSeconds(5), pluralRes = Res.plurals.plurals_hours, quantity = 5),
|
||||
SIX_HOURS(TimeUnit.HOURS.toSeconds(6), pluralRes = Res.plurals.plurals_hours, quantity = 6),
|
||||
TWELVE_HOURS(TimeUnit.HOURS.toSeconds(12), pluralRes = Res.plurals.plurals_hours, quantity = 12),
|
||||
EIGHTEEN_HOURS(TimeUnit.HOURS.toSeconds(18), pluralRes = Res.plurals.plurals_hours, quantity = 18),
|
||||
TWENTY_FOUR_HOURS(TimeUnit.HOURS.toSeconds(24), pluralRes = Res.plurals.plurals_hours, quantity = 24),
|
||||
THIRTY_SIX_HOURS(TimeUnit.HOURS.toSeconds(36), pluralRes = Res.plurals.plurals_hours, quantity = 36),
|
||||
FORTY_EIGHT_HOURS(TimeUnit.HOURS.toSeconds(48), pluralRes = Res.plurals.plurals_hours, quantity = 48),
|
||||
SEVENTY_TWO_HOURS(TimeUnit.HOURS.toSeconds(72), pluralRes = Res.plurals.plurals_hours, quantity = 72),
|
||||
ONE_SECOND(1.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 1),
|
||||
TWO_SECONDS(2.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 2),
|
||||
THREE_SECONDS(3.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 3),
|
||||
FOUR_SECONDS(4.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 4),
|
||||
FIVE_SECONDS(5.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 5),
|
||||
TEN_SECONDS(10.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 10),
|
||||
FIFTEEN_SECONDS(15.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 15),
|
||||
TWENTY_SECONDS(20.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 20),
|
||||
THIRTY_SECONDS(30.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 30),
|
||||
FORTY_FIVE_SECONDS(45.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 45),
|
||||
ONE_MINUTE(1.minutes.inWholeSeconds, pluralRes = Res.plurals.plurals_minutes, quantity = 1),
|
||||
TWO_MINUTES(2.minutes.inWholeSeconds, pluralRes = Res.plurals.plurals_minutes, quantity = 2),
|
||||
FIVE_MINUTES(5.minutes.inWholeSeconds, pluralRes = Res.plurals.plurals_minutes, quantity = 5),
|
||||
TEN_MINUTES(10.minutes.inWholeSeconds, pluralRes = Res.plurals.plurals_minutes, quantity = 10),
|
||||
FIFTEEN_MINUTES(15.minutes.inWholeSeconds, pluralRes = Res.plurals.plurals_minutes, quantity = 15),
|
||||
THIRTY_MINUTES(30.minutes.inWholeSeconds, pluralRes = Res.plurals.plurals_minutes, quantity = 30),
|
||||
ONE_HOUR(TimeConstants.ONE_HOUR.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 1),
|
||||
TWO_HOURS(2.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 2),
|
||||
THREE_HOURS(3.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 3),
|
||||
FOUR_HOURS(4.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 4),
|
||||
FIVE_HOURS(5.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 5),
|
||||
SIX_HOURS(6.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 6),
|
||||
TWELVE_HOURS(12.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 12),
|
||||
EIGHTEEN_HOURS(18.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 18),
|
||||
TWENTY_FOUR_HOURS(24.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 24),
|
||||
THIRTY_SIX_HOURS(36.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 36),
|
||||
FORTY_EIGHT_HOURS(48.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 48),
|
||||
SEVENTY_TWO_HOURS(72.hours.inWholeSeconds, pluralRes = Res.plurals.plurals_hours, quantity = 72),
|
||||
ALWAYS_ON(Int.MAX_VALUE.toLong(), textRes = Res.string.interval_always_on),
|
||||
EIGHTY_SECONDS(TimeUnit.SECONDS.toSeconds(80), pluralRes = Res.plurals.plurals_seconds, quantity = 80),
|
||||
NINETY_SECONDS(TimeUnit.SECONDS.toSeconds(90), pluralRes = Res.plurals.plurals_seconds, quantity = 90),
|
||||
EIGHT_SECONDS(TimeUnit.SECONDS.toSeconds(8), pluralRes = Res.plurals.plurals_seconds, quantity = 8),
|
||||
FORTY_SECONDS(TimeUnit.SECONDS.toSeconds(40), pluralRes = Res.plurals.plurals_seconds, quantity = 40),
|
||||
EIGHTY_SECONDS(80.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 80),
|
||||
NINETY_SECONDS(90.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 90),
|
||||
EIGHT_SECONDS(8.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 8),
|
||||
FORTY_SECONDS(40.seconds.inWholeSeconds, pluralRes = Res.plurals.plurals_seconds, quantity = 40),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ savedstate = "1.4.0"
|
|||
# Kotlin
|
||||
kotlin = "2.3.10"
|
||||
kotlinx-coroutines-android = "1.10.2"
|
||||
kotlinx-datetime = "0.7.1"
|
||||
kotlinx-serialization = "1.10.0"
|
||||
ktlint = "1.7.1"
|
||||
kover = "0.9.7"
|
||||
|
|
@ -147,6 +148,7 @@ dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", versi
|
|||
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.4.0" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-android" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-android" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,12 @@ import org.meshtastic.core.model.MessageStatus
|
|||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.model.util.toDate
|
||||
import org.meshtastic.core.model.util.toInstant
|
||||
import org.meshtastic.core.service.IMeshService
|
||||
import org.meshtastic.proto.PortNum
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.random.Random
|
||||
|
||||
|
|
@ -118,7 +120,7 @@ class MeshServiceViewModel : ViewModel() {
|
|||
bytes = text.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
from = DataPacket.ID_LOCAL,
|
||||
time = System.currentTimeMillis(),
|
||||
time = nowMillis,
|
||||
id = service.packetId,
|
||||
status = MessageStatus.UNKNOWN,
|
||||
hopLimit = 3,
|
||||
|
|
@ -144,7 +146,7 @@ class MeshServiceViewModel : ViewModel() {
|
|||
bytes = "Special Payload for ${portNum.name}".encodeToByteArray().toByteString(),
|
||||
dataType = portNum.value,
|
||||
from = DataPacket.ID_LOCAL,
|
||||
time = System.currentTimeMillis(),
|
||||
time = nowMillis,
|
||||
id = service.packetId,
|
||||
status = MessageStatus.UNKNOWN,
|
||||
hopLimit = 3,
|
||||
|
|
@ -341,7 +343,8 @@ class MeshServiceViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private fun addToLog(entry: String) {
|
||||
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
|
||||
val date = nowMillis.toInstant().toDate()
|
||||
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(date)
|
||||
val logEntry = "[$timestamp] $entry"
|
||||
Log.d(TAG, "Log: $logEntry")
|
||||
@Suppress("MagicNumber")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue