refactor: KMP Migration, Messaging Modularization, and Handshake Robustness (#4631)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-24 06:37:33 -06:00 committed by GitHub
parent b3f88bd94f
commit d408964f07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
144 changed files with 1460 additions and 664 deletions

View file

@ -17,6 +17,7 @@
package org.meshtastic.core.common
import android.Manifest
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.IntentFilter
@ -25,6 +26,11 @@ import android.location.LocationManager
import android.os.Build
import androidx.core.content.ContextCompat
/** Global accessor for Android Application. Must be initialized at app startup. */
object ContextServices {
lateinit var app: Application
}
/** Checks if the device has a GPS receiver. */
fun Context.hasGps(): Boolean {
val lm = getSystemService(Context.LOCATION_SERVICE) as? LocationManager

View file

@ -19,9 +19,9 @@ package org.meshtastic.core.common.util
import android.os.Build
/** Utility for checking build properties, such as emulator detection. */
object BuildUtils {
actual object BuildUtils {
/** Whether the app is currently running on an emulator. */
val isEmulator: Boolean
actual val isEmulator: Boolean
get() =
Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
@ -32,4 +32,7 @@ object BuildUtils {
Build.MODEL.contains("Android SDK built for") ||
Build.MANUFACTURER.contains("Genymotion") ||
Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
actual val sdkInt: Int
get() = Build.VERSION.SDK_INT
}

View file

@ -0,0 +1,45 @@
/*
* 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.common.util
import android.net.Uri
actual class CommonUri(private val uri: Uri) {
actual val host: String?
get() = uri.host
actual val fragment: String?
get() = uri.fragment
actual val pathSegments: List<String>
get() = uri.pathSegments
actual fun getQueryParameter(key: String): String? = uri.getQueryParameter(key)
actual fun getBooleanQueryParameter(key: String, defaultValue: Boolean): Boolean =
uri.getBooleanQueryParameter(key, defaultValue)
actual override fun toString(): String = uri.toString()
actual companion object {
actual fun parse(uriString: String): CommonUri = CommonUri(Uri.parse(uriString))
}
fun toUri(): Uri = uri
}
actual fun CommonUri.toPlatformUri(): Any = this.toUri()

View file

@ -0,0 +1,48 @@
/*
* 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.common.util
import android.text.format.DateUtils
import org.meshtastic.core.common.ContextServices
import java.text.DateFormat
actual object DateFormatter {
actual fun formatRelativeTime(timestampMillis: Long): String = DateUtils.getRelativeTimeSpanString(
timestampMillis,
nowMillis,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
)
.toString()
actual fun formatDateTime(timestampMillis: Long): String = DateUtils.formatDateTime(
ContextServices.app,
timestampMillis,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
)
actual fun formatShortDate(timestampMillis: Long): String {
val now = nowMillis
val isWithin24Hours = (now - timestampMillis) <= DateUtils.DAY_IN_MILLIS
return if (isWithin24Hours) {
DateFormat.getTimeInstance(DateFormat.SHORT).format(timestampMillis)
} else {
DateFormat.getDateInstance(DateFormat.SHORT).format(timestampMillis)
}
}
}

View file

@ -19,18 +19,6 @@ package org.meshtastic.core.common.util
import android.os.RemoteException
import co.touchlab.kermit.Logger
/**
* Wraps and discards exceptions, but reports them to the crash reporter before logging. Use this for operations that
* should not crash the process but are still unexpected.
*/
fun exceptionReporter(inner: () -> Unit) {
try {
inner()
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
Exceptions.report(ex, "exceptionReporter", "Uncaught Exception")
}
}
/**
* Wraps an operation and converts any thrown exceptions into [RemoteException] for safe return through an AIDL
* interface.

View file

@ -0,0 +1,61 @@
/*
* 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.common.util
import android.icu.util.LocaleData
import android.icu.util.ULocale
import android.os.Build
import java.util.Locale
@Suppress("MagicNumber")
actual fun getSystemMeasurementSystem(): MeasurementSystem {
val locale = Locale.getDefault()
// Android 14+ (API 34) introduced user-settable locale preferences.
if (Build.VERSION.SDK_INT >= 34) {
try {
val localePrefsClass = Class.forName("androidx.core.text.util.LocalePreferences")
val getMeasurementSystemMethod =
localePrefsClass.getMethod("getMeasurementSystem", Locale::class.java, Boolean::class.javaPrimitiveType)
val result = getMeasurementSystemMethod.invoke(null, locale, true) as String
return when (result) {
"us",
"uk",
-> MeasurementSystem.IMPERIAL
else -> MeasurementSystem.METRIC
}
} catch (@Suppress("TooGenericExceptionCaught") ignored: Exception) {
// Fallback
}
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) {
LocaleData.MeasurementSystem.SI -> MeasurementSystem.METRIC
else -> MeasurementSystem.IMPERIAL
}
} else {
when (locale.country.uppercase(locale)) {
"US",
"LR",
"MM",
"GB",
-> MeasurementSystem.IMPERIAL
else -> MeasurementSystem.METRIC
}
}
}

View file

@ -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.common.util
import android.net.InetAddresses
import android.os.Build
import android.util.Patterns
actual fun String?.isValidAddress(): Boolean = if (this.isNullOrBlank()) {
false
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@Suppress("DEPRECATION")
Patterns.IP_ADDRESS.matcher(this).matches() || Patterns.DOMAIN_NAME.matcher(this).matches()
} else {
InetAddresses.isNumericAddress(this) || Patterns.DOMAIN_NAME.matcher(this).matches()
}

View file

@ -0,0 +1,31 @@
/*
* 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.common.util
import android.os.Parcelable
actual typealias CommonParcelable = Parcelable
actual typealias CommonParcelize = kotlinx.parcelize.Parcelize
actual typealias CommonIgnoredOnParcel = kotlinx.parcelize.IgnoredOnParcel
actual typealias CommonParceler<T> = kotlinx.parcelize.Parceler<T>
actual typealias CommonTypeParceler<T, P> = kotlinx.parcelize.TypeParceler<T, P>
actual typealias CommonParcel = android.os.Parcel

View file

@ -0,0 +1,26 @@
/*
* 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.common.util
/** Utility for checking build properties, such as emulator detection. */
expect object BuildUtils {
/** Whether the app is currently running on an emulator. */
val isEmulator: Boolean
/** The SDK version of the current platform. On non-Android platforms, this returns 0. */
val sdkInt: Int
}

View file

@ -0,0 +1,37 @@
/*
* 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.common.util
/** Platform-agnostic URI representation to decouple core logic from android.net.Uri. */
expect class CommonUri {
val host: String?
val fragment: String?
val pathSegments: List<String>
fun getQueryParameter(key: String): String?
fun getBooleanQueryParameter(key: String, defaultValue: Boolean): Boolean
override fun toString(): String
companion object {
fun parse(uriString: String): CommonUri
}
}
/** Extension to convert platform Uri to CommonUri in Android source sets. */
expect fun CommonUri.toPlatformUri(): Any

View file

@ -0,0 +1,33 @@
/*
* 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.common.util
/** Platform-agnostic Date formatter utility. */
expect object DateFormatter {
/** Formats a timestamp into a relative "time ago" string. */
fun formatRelativeTime(timestampMillis: Long): String
/** Formats a timestamp into a localized date and time string. */
fun formatDateTime(timestampMillis: Long): String
/**
* Formats a timestamp into a short date or time string.
*
* Typically shows time if within the last 24 hours, otherwise the date.
*/
fun formatShortDate(timestampMillis: Long): String
}

View file

@ -46,3 +46,15 @@ fun ignoreException(silent: Boolean = false, inner: () -> Unit) {
}
}
}
/**
* Wraps and discards exceptions, but reports them to the crash reporter before logging. Use this for operations that
* should not crash the process but are still unexpected.
*/
fun exceptionReporter(inner: () -> Unit) {
try {
inner()
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
Exceptions.report(ex, "exceptionReporter", "Uncaught Exception")
}
}

View file

@ -0,0 +1,90 @@
/*
* 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/>.
*/
@file:Suppress("MatchingDeclarationName")
package org.meshtastic.core.common.util
import kotlin.math.PI
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
@Suppress("MagicNumber")
object GPSFormat {
fun toDec(latitude: Double, longitude: Double): String {
// Simple decimal formatting for KMP
fun Double.format(digits: Int): String {
val multiplier = 10.0.pow(digits)
val rounded = (this * multiplier).toLong() / multiplier
return rounded.toString()
}
return "${latitude.format(5)}, ${longitude.format(5)}"
}
}
private const val EARTH_RADIUS_METERS = 6371e3
@Suppress("MagicNumber")
private fun Double.toRadians(): Double = this * PI / 180.0
@Suppress("MagicNumber")
private fun Double.toDegrees(): Double = this * 180.0 / PI
/** @return distance in meters along the surface of the earth (ish) */
@Suppress("MagicNumber")
fun latLongToMeter(latitudeA: Double, longitudeA: Double, latitudeB: Double, longitudeB: Double): Double {
val lat1 = latitudeA.toRadians()
val lon1 = longitudeA.toRadians()
val lat2 = latitudeB.toRadians()
val lon2 = longitudeB.toRadians()
val dLat = lat2 - lat1
val dLon = lon2 - lon1
val a = sin(dLat / 2).pow(2) + cos(lat1) * cos(lat2) * sin(dLon / 2).pow(2)
val c = 2 * asin(sqrt(a))
return EARTH_RADIUS_METERS * c
}
/**
* Computes the bearing in degrees between two points on Earth.
*
* @param lat1 Latitude of the first point
* @param lon1 Longitude of the first point
* @param lat2 Latitude of the second point
* @param lon2 Longitude of the second point
* @return Bearing between the two points in degrees. A value of 0 means due north.
*/
@Suppress("MagicNumber")
fun bearing(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
val lat1Rad = lat1.toRadians()
val lon1Rad = lon1.toRadians()
val lat2Rad = lat2.toRadians()
val lon2Rad = lon2.toRadians()
val dLon = lon2Rad - lon1Rad
val y = sin(dLon) * cos(lat2Rad)
val x = cos(lat1Rad) * sin(lat2Rad) - sin(lat1Rad) * cos(lat2Rad) * cos(dLon)
val bearing = atan2(y, x).toDegrees()
return (bearing + 360) % 360
}

View file

@ -0,0 +1,26 @@
/*
* 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.common.util
/** Represents the system's preferred measurement system. */
enum class MeasurementSystem {
METRIC,
IMPERIAL,
}
/** returns the system's preferred measurement system. */
expect fun getSystemMeasurementSystem(): MeasurementSystem

View file

@ -0,0 +1,20 @@
/*
* 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.common.util
/** Validates if the given string is a valid network address (IP or domain). */
expect fun String?.isValidAddress(): Boolean

View file

@ -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.common.util
/** Platform-agnostic Parcelable interface. */
expect interface CommonParcelable
/** Platform-agnostic Parcelize annotation. */
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class CommonParcelize()
/** Platform-agnostic IgnoredOnParcel annotation. */
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
expect annotation class CommonIgnoredOnParcel()
/** Platform-agnostic Parceler interface. */
expect interface CommonParceler<T> {
fun create(parcel: CommonParcel): T
fun T.write(parcel: CommonParcel, flags: Int)
}
/** Platform-agnostic TypeParceler annotation. */
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
@Repeatable
expect annotation class CommonTypeParceler<T, P : CommonParceler<in T>>()
/** Platform-agnostic Parcel representation for manual parceling (e.g. AIDL support). */
expect class CommonParcel {
fun readString(): String?
fun readInt(): Int
fun readLong(): Long
fun readFloat(): Float
fun createByteArray(): ByteArray?
fun writeByteArray(b: ByteArray?)
}