mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
chore: KMP audit — commonize code, centralize utilities, eliminate dead abstractions (#5133)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
50ade01e55
commit
72b981f73b
132 changed files with 2186 additions and 916 deletions
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* 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 org.meshtastic.core.common.util.nowInstant
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import java.text.DateFormat
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
private val DAY_DURATION = 24.hours
|
||||
|
||||
/**
|
||||
* 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? {
|
||||
if (time == 0L) return null
|
||||
val instant = time.toInstant()
|
||||
val isWithin24Hours = (nowInstant - instant) <= DAY_DURATION
|
||||
|
||||
return if (isWithin24Hours) {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(instant.toDate())
|
||||
} else {
|
||||
DateFormat.getDateInstance(DateFormat.SHORT).format(instant.toDate())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the remaining mute time in days and hours.
|
||||
*
|
||||
* @param remainingMillis The remaining time in milliseconds
|
||||
* @return Pair of (days, hours), where days is Int and hours is Double
|
||||
*/
|
||||
|
|
@ -17,12 +17,13 @@
|
|||
package org.meshtastic.core.model.util
|
||||
|
||||
import android.net.Uri
|
||||
import com.eygraber.uri.toKmpUri
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
||||
/** Extension to bridge android.net.Uri to CommonUri for shared dispatch logic. */
|
||||
fun Uri.toCommonUri(): CommonUri = CommonUri.parse(this.toString())
|
||||
fun Uri.toCommonUri(): CommonUri = this.toKmpUri()
|
||||
|
||||
/** Bridge extension for Android clients. */
|
||||
fun Uri.dispatchMeshtasticUri(
|
||||
|
|
|
|||
|
|
@ -19,10 +19,9 @@ package org.meshtastic.core.model
|
|||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.GPSFormat
|
||||
import org.meshtastic.core.common.util.MetricFormatter
|
||||
import org.meshtastic.core.common.util.bearing
|
||||
import org.meshtastic.core.common.util.formatString
|
||||
import org.meshtastic.core.common.util.latLongToMeter
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
@ -143,34 +142,26 @@ data class Node(
|
|||
private fun EnvironmentMetrics.getDisplayStrings(isFahrenheit: Boolean): List<String> {
|
||||
val temp =
|
||||
if ((temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
formatString("%.1f°F", celsiusToFahrenheit(temperature ?: 0f))
|
||||
} else {
|
||||
formatString("%.1f°C", temperature)
|
||||
}
|
||||
MetricFormatter.temperature(temperature ?: 0f, isFahrenheit)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val humidity = if ((relative_humidity ?: 0f) != 0f) formatString("%.0f%%", relative_humidity) else null
|
||||
val humidity = if ((relative_humidity ?: 0f) != 0f) MetricFormatter.humidity(relative_humidity ?: 0f) else null
|
||||
val soilTemperatureStr =
|
||||
if ((soil_temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
formatString("%.1f°F", celsiusToFahrenheit(soil_temperature ?: 0f))
|
||||
} else {
|
||||
formatString("%.1f°C", soil_temperature)
|
||||
}
|
||||
MetricFormatter.temperature(soil_temperature ?: 0f, isFahrenheit)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val soilMoistureRange = 0..100
|
||||
val soilMoisture =
|
||||
if ((soil_moisture ?: Int.MIN_VALUE) in soilMoistureRange && (soil_temperature ?: 0f) != 0f) {
|
||||
formatString("%d%%", soil_moisture)
|
||||
MetricFormatter.percent(soil_moisture ?: 0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val voltage = if ((this.voltage ?: 0f) != 0f) formatString("%.2fV", this.voltage) else null
|
||||
val current = if ((current ?: 0f) != 0f) formatString("%.1fmA", current) else null
|
||||
val voltage = if ((this.voltage ?: 0f) != 0f) MetricFormatter.voltage(this.voltage ?: 0f) else null
|
||||
val current = if ((current ?: 0f) != 0f) MetricFormatter.current(current ?: 0f) else null
|
||||
val iaq = if ((iaq ?: 0) != 0) "IAQ: $iaq" else null
|
||||
|
||||
return listOfNotNull(
|
||||
|
|
@ -199,9 +190,12 @@ data class Node(
|
|||
fun getRelayNode(relayNodeId: Int, nodes: List<Node>, ourNodeNum: Int?): Node? {
|
||||
val relayNodeIdSuffix = relayNodeId and RELAY_NODE_SUFFIX_MASK
|
||||
|
||||
val candidateRelayNodes = nodes.filter {
|
||||
it.num != ourNodeNum && it.lastHeard != 0 && (it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix
|
||||
}
|
||||
val candidateRelayNodes =
|
||||
nodes.filter {
|
||||
it.num != ourNodeNum &&
|
||||
it.lastHeard != 0 &&
|
||||
(it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix
|
||||
}
|
||||
|
||||
val closestRelayNode =
|
||||
if (candidateRelayNodes.size == 1) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ val Any?.anonymize: String
|
|||
get() = this.anonymize()
|
||||
|
||||
/** A version of anonymize that allows passing in a custom minimum length */
|
||||
fun Any?.anonymize(maxLen: Int = 3) = if (this != null) ("..." + this.toString().takeLast(maxLen)) else "null"
|
||||
fun Any?.anonymize(maxLen: Int = 3) = if (this != null) "...${this.toString().takeLast(maxLen)}" else "null"
|
||||
|
||||
// A toString that makes sure all newlines are removed (for nice logging).
|
||||
fun Any.toOneLineString() = this.toString().replace('\n', ' ')
|
||||
|
|
|
|||
|
|
@ -16,7 +16,27 @@
|
|||
*/
|
||||
package org.meshtastic.core.model.util
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
|
||||
/** Computes SFPP (Store-Forward-Plus-Plus) message hashes for deduplication. */
|
||||
expect object SfppHasher {
|
||||
fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray
|
||||
object SfppHasher {
|
||||
private const val HASH_SIZE = 16
|
||||
private const val INT_BYTES = 4
|
||||
private const val INT_COUNT = 3
|
||||
private const val SHIFT_8 = 8
|
||||
private const val SHIFT_16 = 16
|
||||
private const val SHIFT_24 = 24
|
||||
|
||||
fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray {
|
||||
val input = ByteArray(encryptedPayload.size + INT_BYTES * INT_COUNT)
|
||||
encryptedPayload.copyInto(input)
|
||||
var offset = encryptedPayload.size
|
||||
for (value in intArrayOf(to, from, id)) {
|
||||
input[offset++] = value.toByte()
|
||||
input[offset++] = (value shr SHIFT_8).toByte()
|
||||
input[offset++] = (value shr SHIFT_16).toByte()
|
||||
input[offset++] = (value shr SHIFT_24).toByte()
|
||||
}
|
||||
return input.toByteString().sha256().toByteArray().copyOf(HASH_SIZE)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ fun compareUsers(oldUser: User, newUser: User): String {
|
|||
return if (changes.isEmpty()) {
|
||||
"No changes detected."
|
||||
} else {
|
||||
"Changes:\n" + changes.joinToString("\n")
|
||||
"Changes:\n${changes.joinToString("\n")}"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 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.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CommonUtilsTest {
|
||||
|
||||
@Test
|
||||
fun testByteArrayOfInts() {
|
||||
val bytes = byteArrayOfInts(0x01, 0xFF, 0x80)
|
||||
assertEquals(3, bytes.size)
|
||||
assertEquals(1, bytes[0])
|
||||
assertEquals(-1, bytes[1]) // 0xFF as signed byte
|
||||
assertEquals(-128, bytes[2].toInt()) // 0x80 as signed byte
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testXorHash() {
|
||||
val data = byteArrayOfInts(0x01, 0x02, 0x03)
|
||||
assertEquals(0 xor 1 xor 2 xor 3, xorHash(data))
|
||||
|
||||
val data2 = byteArrayOfInts(0xFF, 0xFF)
|
||||
assertEquals(0xFF xor 0xFF, xorHash(data2))
|
||||
assertEquals(0, xorHash(data2))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (c) 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.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class SfppHasherTest {
|
||||
|
||||
@Test
|
||||
fun outputIsAlways16Bytes() {
|
||||
val hash = SfppHasher.computeMessageHash(byteArrayOf(1, 2, 3), to = 100, from = 200, id = 1)
|
||||
assertEquals(16, hash.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyPayloadProduces16Bytes() {
|
||||
val hash = SfppHasher.computeMessageHash(byteArrayOf(), to = 0, from = 0, id = 0)
|
||||
assertEquals(16, hash.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deterministicOutput() {
|
||||
val a = SfppHasher.computeMessageHash(byteArrayOf(0xAB.toByte()), to = 1, from = 2, id = 3)
|
||||
val b = SfppHasher.computeMessageHash(byteArrayOf(0xAB.toByte()), to = 1, from = 2, id = 3)
|
||||
assertEquals(a.toList(), b.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun differentPayloadsProduceDifferentHashes() {
|
||||
val a = SfppHasher.computeMessageHash(byteArrayOf(1), to = 1, from = 2, id = 3)
|
||||
val b = SfppHasher.computeMessageHash(byteArrayOf(2), to = 1, from = 2, id = 3)
|
||||
assertNotEquals(a.toList(), b.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun differentIdsProduceDifferentHashes() {
|
||||
val payload = byteArrayOf(0x10, 0x20)
|
||||
val a = SfppHasher.computeMessageHash(payload, to = 1, from = 2, id = 100)
|
||||
val b = SfppHasher.computeMessageHash(payload, to = 1, from = 2, id = 101)
|
||||
assertNotEquals(a.toList(), b.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun differentFromProduceDifferentHashes() {
|
||||
val payload = byteArrayOf(0x10, 0x20)
|
||||
val a = SfppHasher.computeMessageHash(payload, to = 1, from = 2, id = 3)
|
||||
val b = SfppHasher.computeMessageHash(payload, to = 1, from = 99, id = 3)
|
||||
assertNotEquals(a.toList(), b.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun maxIntValues() {
|
||||
val hash =
|
||||
SfppHasher.computeMessageHash(
|
||||
byteArrayOf(0xFF.toByte()),
|
||||
to = Int.MAX_VALUE,
|
||||
from = Int.MAX_VALUE,
|
||||
id = Int.MAX_VALUE,
|
||||
)
|
||||
assertEquals(16, hash.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun littleEndianByteOrder() {
|
||||
// Verify the integer 0x04030201 is encoded as [01, 02, 03, 04] (little-endian)
|
||||
val hashA = SfppHasher.computeMessageHash(byteArrayOf(), to = 0x04030201, from = 0, id = 0)
|
||||
val hashB = SfppHasher.computeMessageHash(byteArrayOf(), to = 0x01020304, from = 0, id = 0)
|
||||
// Different byte orderings must produce different hashes
|
||||
assertNotEquals(hashA.toList(), hashB.toList())
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,3 @@ package org.meshtastic.core.model.util
|
|||
actual fun getShortDateTime(time: Long): String = ""
|
||||
|
||||
actual fun platformRandomBytes(size: Int): ByteArray = ByteArray(size)
|
||||
|
||||
actual object SfppHasher {
|
||||
actual fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray = ByteArray(32)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* 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 java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.MessageDigest
|
||||
|
||||
actual object SfppHasher {
|
||||
private const val HASH_SIZE = 16
|
||||
private const val INT_BYTES = 4
|
||||
|
||||
actual fun computeMessageHash(encryptedPayload: ByteArray, to: Int, from: Int, id: Int): ByteArray {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.update(encryptedPayload)
|
||||
digest.update(ByteBuffer.allocate(INT_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(to).array())
|
||||
digest.update(ByteBuffer.allocate(INT_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(from).array())
|
||||
digest.update(ByteBuffer.allocate(INT_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(id).array())
|
||||
return digest.digest().copyOf(HASH_SIZE)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue