mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(ble): Centralize BLE logic into a core module (#4550)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
7a68802bc2
commit
6bfa5b5f70
214 changed files with 3471 additions and 2405 deletions
|
|
@ -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,11 +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.common
|
||||
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
|
|
@ -73,3 +74,18 @@ fun Context.hasLocationPermission(): Boolean {
|
|||
val perms = listOf(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
return perms.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension for Context to register a BroadcastReceiver in a compatible way across Android versions.
|
||||
*
|
||||
* @param receiver The receiver to register.
|
||||
* @param filter The intent filter.
|
||||
* @param flag The export flag (defaults to [ContextCompat.RECEIVER_EXPORTED]).
|
||||
*/
|
||||
fun Context.registerReceiverCompat(
|
||||
receiver: BroadcastReceiver,
|
||||
filter: IntentFilter,
|
||||
flag: Int = ContextCompat.RECEIVER_EXPORTED,
|
||||
) {
|
||||
ContextCompat.registerReceiver(this, receiver, filter, flag)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.content.Context
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* A specialized [FileOutputStream] that writes data to a file in the application's external files directory. Primarily
|
||||
* used for low-level protocol debugging and packet logging.
|
||||
*
|
||||
* @param context The context used to locate the external files directory.
|
||||
* @param name The name of the log file.
|
||||
*/
|
||||
class BinaryLogFile(context: Context, name: String) :
|
||||
FileOutputStream(File(context.getExternalFilesDir(null), name), true)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.Build
|
||||
|
||||
/** Utility for checking build properties, such as emulator detection. */
|
||||
object BuildUtils {
|
||||
/** Whether the app is currently running on an emulator. */
|
||||
val isEmulator: Boolean
|
||||
get() =
|
||||
Build.FINGERPRINT.startsWith("generic") ||
|
||||
Build.FINGERPRINT.startsWith("unknown") ||
|
||||
Build.FINGERPRINT.contains("emulator") ||
|
||||
setOf(Build.MODEL, Build.PRODUCT).contains("google_sdk") ||
|
||||
Build.MODEL.contains("sdk_gphone64") ||
|
||||
Build.MODEL.contains("Emulator") ||
|
||||
Build.MODEL.contains("Android SDK built for") ||
|
||||
Build.MANUFACTURER.contains("Genymotion") ||
|
||||
Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.os.ParcelCompat
|
||||
|
||||
/** Reads a [Parcelable] from a [Parcel] in a backward-compatible way. */
|
||||
inline fun <reified T : Parcelable> Parcel.readParcelableCompat(loader: ClassLoader?): T? =
|
||||
ParcelCompat.readParcelable(this, loader, T::class.java)
|
||||
|
||||
/** Retrieves a [Parcelable] extra from an [Intent] in a backward-compatible way. */
|
||||
inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String?): T? =
|
||||
IntentCompat.getParcelableExtra(this, key, T::class.java)
|
||||
|
||||
/** Retrieves [PackageInfo] for a given package name in a backward-compatible way. */
|
||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getPackageInfo(packageName, flags)
|
||||
}
|
||||
|
||||
/** Registers a [BroadcastReceiver] using [ContextCompat] to ensure consistent behavior across Android versions. */
|
||||
fun Context.registerReceiverCompat(
|
||||
receiver: BroadcastReceiver,
|
||||
filter: IntentFilter,
|
||||
flag: Int = ContextCompat.RECEIVER_EXPORTED,
|
||||
) {
|
||||
ContextCompat.registerReceiver(this, receiver, filter, flag)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.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.
|
||||
*/
|
||||
fun <T> toRemoteExceptions(inner: () -> T): T = try {
|
||||
inner()
|
||||
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
|
||||
Logger.e(ex) { "Uncaught exception in service call, returning RemoteException to client" }
|
||||
when (ex) {
|
||||
is RemoteException -> throw ex
|
||||
else -> throw RemoteException(ex.message).apply { initCause(ex) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 java.util.Date
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
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)
|
||||
|
||||
/** Converts this [Instant] to a legacy [Date]. */
|
||||
fun Instant.toDate(): Date = Date(this.toEpochMilliseconds())
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.InputFilter
|
||||
import android.text.Spanned
|
||||
|
||||
/**
|
||||
* An [InputFilter] that constrains text length based on UTF-8 byte count instead of character count. This is
|
||||
* particularly useful for fields that must be stored in byte-limited buffers, such as hardware configuration fields.
|
||||
*
|
||||
* @param maxBytes The maximum allowed length in UTF-8 bytes.
|
||||
*/
|
||||
class Utf8ByteLengthFilter(private val maxBytes: Int) : InputFilter {
|
||||
|
||||
private companion object {
|
||||
const val ONE_BYTE_LIMIT = '\u0080'
|
||||
const val TWO_BYTE_LIMIT = '\u0800'
|
||||
const val BYTES_1 = 1
|
||||
const val BYTES_2 = 2
|
||||
const val BYTES_3 = 3
|
||||
}
|
||||
|
||||
override fun filter(
|
||||
source: CharSequence,
|
||||
start: Int,
|
||||
end: Int,
|
||||
dest: Spanned,
|
||||
dstart: Int,
|
||||
dend: Int,
|
||||
): CharSequence? {
|
||||
val srcByteCount = countUtf8Bytes(source, start, end)
|
||||
|
||||
// Calculate bytes in dest excluding the range being replaced
|
||||
val destLen = dest.length
|
||||
var destByteCount = 0
|
||||
destByteCount += countUtf8Bytes(dest, 0, dstart)
|
||||
destByteCount += countUtf8Bytes(dest, dend, destLen)
|
||||
|
||||
var keepBytes = maxBytes - destByteCount
|
||||
return when {
|
||||
keepBytes <= 0 -> ""
|
||||
keepBytes >= srcByteCount -> null
|
||||
else -> {
|
||||
for (i in start until end) {
|
||||
val c = source[i]
|
||||
keepBytes -= getByteCount(c)
|
||||
if (keepBytes < 0) {
|
||||
return source.subSequence(start, i)
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun countUtf8Bytes(seq: CharSequence, start: Int, end: Int): Int {
|
||||
var count = 0
|
||||
for (i in start until end) {
|
||||
count += getByteCount(seq[i])
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private fun getByteCount(c: Char): Int = when {
|
||||
c < ONE_BYTE_LIMIT -> BYTES_1
|
||||
c < TWO_BYTE_LIMIT -> BYTES_2
|
||||
else -> BYTES_3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
private val errorHandler = CoroutineExceptionHandler { _, exception ->
|
||||
Exceptions.report(exception, "coroutine-exception-handler", "Uncaught coroutine exception")
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a new coroutine with a central [CoroutineExceptionHandler] that reports errors to [Exceptions].
|
||||
*
|
||||
* @param context Additional to [CoroutineExceptionHandler] context.
|
||||
* @param start Coroutine start option.
|
||||
* @param block The coroutine code block.
|
||||
* @return The launched [Job].
|
||||
*/
|
||||
fun CoroutineScope.handledLaunch(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
): Job = launch(context = context + errorHandler, start = start, block = block)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 co.touchlab.kermit.Logger
|
||||
|
||||
/**
|
||||
* Sometimes when starting services we face situations where messages come in that require computation but we can't do
|
||||
* that computation yet because we are still waiting for some long running init to complete.
|
||||
*
|
||||
* This class lets you queue up closures to run at a later date and later on you can call run() to run all the
|
||||
* previously queued work.
|
||||
*/
|
||||
class DeferredExecution {
|
||||
private val queue = mutableListOf<() -> Unit>()
|
||||
|
||||
/** Queues new work to be executed later. */
|
||||
fun add(fn: () -> Unit) {
|
||||
queue.add(fn)
|
||||
}
|
||||
|
||||
/** Runs all work in the queue and clears it. */
|
||||
fun run() {
|
||||
Logger.d { "Running deferred execution, numJobs=${queue.size}" }
|
||||
queue.forEach { it() }
|
||||
queue.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 co.touchlab.kermit.Logger
|
||||
|
||||
object Exceptions {
|
||||
/** Set by the application to provide a custom crash reporting implementation. */
|
||||
var reporter: ((Throwable, String?, String?) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Report an exception to the configured reporter (if any) after logging it.
|
||||
*
|
||||
* @param exception The exception to report.
|
||||
* @param tag An optional tag for the report.
|
||||
* @param message An optional message providing context.
|
||||
*/
|
||||
fun report(exception: Throwable, tag: String? = null, message: String? = null) {
|
||||
// Log locally first
|
||||
Logger.e(exception) { "Exceptions.report: $tag $message" }
|
||||
reporter?.invoke(exception, tag, message)
|
||||
}
|
||||
}
|
||||
|
||||
/** Wraps and discards exceptions, optionally logging them. */
|
||||
fun ignoreException(silent: Boolean = false, inner: () -> Unit) {
|
||||
try {
|
||||
inner()
|
||||
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
|
||||
if (!silent) {
|
||||
Logger.w(ex) { "Ignoring exception" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A helper class that manages a single [Job]. When a new job is launched, the previous one is cancelled. This is useful
|
||||
* for ensuring that only one operation of a certain type is running at a time.
|
||||
*/
|
||||
class SequentialJob @Inject constructor() {
|
||||
private val job = AtomicReference<Job?>(null)
|
||||
|
||||
/**
|
||||
* Cancels the previous job (if any) and launches a new one in the given [scope]. The new job uses [handledLaunch]
|
||||
* to ensure exceptions are reported.
|
||||
*/
|
||||
fun launch(scope: CoroutineScope, block: suspend CoroutineScope.() -> Unit) {
|
||||
cancel()
|
||||
val newJob = scope.handledLaunch(block = block)
|
||||
job.set(newJob)
|
||||
|
||||
newJob.invokeOnCompletion { job.compareAndSet(newJob, null) }
|
||||
}
|
||||
|
||||
/** Cancels the current job. */
|
||||
fun cancel() {
|
||||
job.getAndSet(null)?.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -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.common.util
|
||||
|
||||
/** A deferred execution object (with various possible implementations) */
|
||||
interface Continuation<in T> {
|
||||
fun resume(res: Result<T>)
|
||||
|
||||
/** Syntactic sugar for resuming with success. */
|
||||
fun resumeSuccess(res: T) = resume(Result.success(res))
|
||||
|
||||
/** Syntactic sugar for resuming with failure. */
|
||||
fun resumeWithException(ex: Throwable) = resume(Result.failure(ex))
|
||||
}
|
||||
|
||||
/** An async continuation that calls a callback when the result is available. */
|
||||
class CallbackContinuation<in T>(private val cb: (Result<T>) -> Unit) : Continuation<T> {
|
||||
override fun resume(res: Result<T>) = cb(res)
|
||||
}
|
||||
|
||||
/**
|
||||
* A blocking version of coroutine Continuation using traditional threading primitives.
|
||||
*
|
||||
* This is useful in contexts where coroutine suspension is not desirable or when bridging with legacy threaded code.
|
||||
*/
|
||||
class SyncContinuation<T> : Continuation<T> {
|
||||
|
||||
private val lock = java.util.concurrent.locks.ReentrantLock()
|
||||
private val condition = lock.newCondition()
|
||||
private var result: Result<T>? = null
|
||||
|
||||
override fun resume(res: Result<T>) {
|
||||
lock.lock()
|
||||
try {
|
||||
result = res
|
||||
condition.signal()
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks the current thread until the result is available or the timeout expires.
|
||||
*
|
||||
* @param timeoutMsecs Maximum time to wait in milliseconds. If 0, waits indefinitely.
|
||||
* @return The result of the operation.
|
||||
* @throws IllegalStateException if a timeout occurs or if an internal error happens.
|
||||
*/
|
||||
@Suppress("NestedBlockDepth")
|
||||
fun await(timeoutMsecs: Long = 0): T {
|
||||
lock.lock()
|
||||
try {
|
||||
val startT = nowMillis
|
||||
while (result == null) {
|
||||
if (timeoutMsecs > 0) {
|
||||
val remaining = timeoutMsecs - (nowMillis - startT)
|
||||
check(remaining > 0) { "SyncContinuation timeout" }
|
||||
condition.await(remaining, java.util.concurrent.TimeUnit.MILLISECONDS)
|
||||
} else {
|
||||
condition.await()
|
||||
}
|
||||
}
|
||||
|
||||
val r = result
|
||||
checkNotNull(r) { "Unexpected null result in SyncContinuation" }
|
||||
return r.getOrThrow()
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls an [initfn] that is responsible for starting an operation and saving the [SyncContinuation]. Then blocks the
|
||||
* current thread until the operation completes or times out.
|
||||
*
|
||||
* Essentially a blocking version of [kotlinx.coroutines.suspendCancellableCoroutine].
|
||||
*/
|
||||
fun <T> suspend(timeoutMsecs: Long = -1, initfn: (SyncContinuation<T>) -> Unit): T {
|
||||
val cont = SyncContinuation<T>()
|
||||
initfn(cont)
|
||||
return cont.await(timeoutMsecs)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 kotlinx.datetime.TimeZone
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
/** 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 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,80 @@
|
|||
/*
|
||||
* 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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SequentialJobTest {
|
||||
|
||||
private val sequentialJob = SequentialJob()
|
||||
|
||||
@Test
|
||||
fun `launch cancels previous job`() = runTest {
|
||||
var job1Active = false
|
||||
var job1Cancelled = false
|
||||
|
||||
// Launch first job
|
||||
sequentialJob.launch(this) {
|
||||
try {
|
||||
job1Active = true
|
||||
delay(1000)
|
||||
} finally {
|
||||
job1Cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
advanceTimeBy(100)
|
||||
assertTrue(job1Active, "Job 1 should be active")
|
||||
|
||||
// Launch second job
|
||||
sequentialJob.launch(this) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
advanceTimeBy(100)
|
||||
assertTrue(job1Cancelled, "Job 1 should be cancelled")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cancel stops the job`() = runTest {
|
||||
var jobActive = false
|
||||
var jobCancelled = false
|
||||
|
||||
sequentialJob.launch(this) {
|
||||
try {
|
||||
jobActive = true
|
||||
delay(1000)
|
||||
} finally {
|
||||
jobCancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
advanceTimeBy(100)
|
||||
assertTrue(jobActive, "Job should be active")
|
||||
|
||||
sequentialJob.cancel()
|
||||
|
||||
advanceTimeBy(100)
|
||||
assertTrue(jobCancelled, "Job should be cancelled")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TimeUtilsTest {
|
||||
@Test
|
||||
fun testNowMillis() {
|
||||
val start = nowMillis
|
||||
// Just verify it returns something sensible (not 0)
|
||||
assertTrue(start > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNowSeconds() {
|
||||
val start = nowSeconds
|
||||
assertTrue(start > 0)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue