mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: settings rework (#4678)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
b2b21e10e2
commit
fdd07f893f
27 changed files with 941 additions and 306 deletions
|
|
@ -19,6 +19,7 @@ package org.meshtastic.core.ble
|
|||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
|
@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import no.nordicsemi.android.common.core.simpleSharedFlow
|
||||
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
|
||||
|
|
@ -72,7 +74,7 @@ class BleConnection(
|
|||
*
|
||||
* @param p The peripheral to connect to.
|
||||
*/
|
||||
suspend fun connect(p: Peripheral) {
|
||||
suspend fun connect(p: Peripheral) = withContext(NonCancellable) {
|
||||
stateJob?.cancel()
|
||||
peripheral = p
|
||||
|
||||
|
|
@ -156,7 +158,7 @@ class BleConnection(
|
|||
}
|
||||
|
||||
/** Disconnects from the current peripheral. */
|
||||
suspend fun disconnect() {
|
||||
suspend fun disconnect() = withContext(NonCancellable) {
|
||||
stateJob?.cancel()
|
||||
stateJob = null
|
||||
peripheral?.disconnect()
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@
|
|||
*/
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -26,15 +29,31 @@ import javax.inject.Inject
|
|||
* 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)
|
||||
private val job = AtomicReference<Job?>()
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param timeoutMs Optional timeout in milliseconds. If > 0, the [block] is wrapped in [withTimeout] so that
|
||||
* indefinitely-suspended coroutines (e.g. blocked DataStore reads) throw [TimeoutCancellationException] instead
|
||||
* of hanging silently.
|
||||
*/
|
||||
fun launch(scope: CoroutineScope, block: suspend CoroutineScope.() -> Unit) {
|
||||
fun launch(scope: CoroutineScope, timeoutMs: Long = 0, block: suspend CoroutineScope.() -> Unit) {
|
||||
cancel()
|
||||
val newJob = scope.handledLaunch(block = block)
|
||||
val newJob =
|
||||
scope.handledLaunch {
|
||||
if (timeoutMs > 0) {
|
||||
try {
|
||||
withTimeout(timeoutMs, block)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
Logger.w { "SequentialJob timed out after ${timeoutMs}ms" }
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
||||
job.set(newJob)
|
||||
|
||||
newJob.invokeOnCompletion { job.compareAndSet(newJob, null) }
|
||||
|
|
|
|||
|
|
@ -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.datasource
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -54,7 +53,8 @@ class SwitchingNodeInfoReadDataSource @Inject constructor(private val dbManager:
|
|||
}
|
||||
|
||||
override suspend fun getNodesOlderThan(lastHeard: Int): List<NodeEntity> =
|
||||
dbManager.withDb { it.nodeInfoDao().getNodesOlderThan(lastHeard) }
|
||||
dbManager.withDb { it.nodeInfoDao().getNodesOlderThan(lastHeard) } ?: emptyList()
|
||||
|
||||
override suspend fun getUnknownNodes(): List<NodeEntity> = dbManager.withDb { it.nodeInfoDao().getUnknownNodes() }
|
||||
override suspend fun getUnknownNodes(): List<NodeEntity> =
|
||||
dbManager.withDb { it.nodeInfoDao().getUnknownNodes() } ?: emptyList()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,33 +33,43 @@ constructor(
|
|||
private val dispatchers: CoroutineDispatchers,
|
||||
) : NodeInfoWriteDataSource {
|
||||
|
||||
override suspend fun upsert(node: NodeEntity) =
|
||||
override suspend fun upsert(node: NodeEntity) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().upsert(node) } }
|
||||
}
|
||||
|
||||
override suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) =
|
||||
override suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().installConfig(mi, nodes) } }
|
||||
}
|
||||
|
||||
override suspend fun clearNodeDB(preserveFavorites: Boolean) =
|
||||
override suspend fun clearNodeDB(preserveFavorites: Boolean) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().clearNodeInfo(preserveFavorites) } }
|
||||
}
|
||||
|
||||
override suspend fun clearMyNodeInfo() =
|
||||
override suspend fun clearMyNodeInfo() {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().clearMyNodeInfo() } }
|
||||
}
|
||||
|
||||
override suspend fun deleteNode(num: Int) =
|
||||
override suspend fun deleteNode(num: Int) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().deleteNode(num) } }
|
||||
}
|
||||
|
||||
override suspend fun deleteNodes(nodeNums: List<Int>) =
|
||||
override suspend fun deleteNodes(nodeNums: List<Int>) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().deleteNodes(nodeNums) } }
|
||||
}
|
||||
|
||||
override suspend fun deleteMetadata(num: Int) =
|
||||
override suspend fun deleteMetadata(num: Int) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().deleteMetadata(num) } }
|
||||
}
|
||||
|
||||
override suspend fun upsert(metadata: MetadataEntity) =
|
||||
override suspend fun upsert(metadata: MetadataEntity) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().upsert(metadata) } }
|
||||
}
|
||||
|
||||
override suspend fun setNodeNotes(num: Int, notes: String) =
|
||||
override suspend fun setNodeNotes(num: Int, notes: String) {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().setNodeNotes(num, notes) } }
|
||||
}
|
||||
|
||||
override suspend fun backfillDenormalizedNames() =
|
||||
override suspend fun backfillDenormalizedNames() {
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().backfillDenormalizedNames() } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import androidx.room.Room
|
|||
import androidx.room.RoomDatabase
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -44,6 +45,7 @@ import javax.inject.Singleton
|
|||
/** Manages per-device Room database instances for node data, with LRU eviction. */
|
||||
@Singleton
|
||||
@Suppress("TooManyFunctions")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DatabaseManager @Inject constructor(private val app: Application, private val dispatchers: CoroutineDispatchers) {
|
||||
val prefs: SharedPreferences = app.getSharedPreferences("db-manager-prefs", Context.MODE_PRIVATE)
|
||||
private val managerScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
|
@ -114,8 +116,15 @@ class DatabaseManager @Inject constructor(private val app: Application, private
|
|||
Logger.i { "Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}" }
|
||||
}
|
||||
|
||||
private val limitedIo = dispatchers.io.limitedParallelism(4)
|
||||
|
||||
/** Execute [block] with the current DB instance. */
|
||||
inline fun <T> withDb(block: (MeshtasticDatabase) -> T): T = block(currentDb.value)
|
||||
suspend fun <T> withDb(block: suspend (MeshtasticDatabase) -> T): T? = withContext(limitedIo) {
|
||||
val active = _currentDb.value?.openHelper?.databaseName ?: return@withContext null
|
||||
markLastUsed(active)
|
||||
val db = _currentDb.value ?: return@withContext null // Use the cached current DB
|
||||
block(db)
|
||||
}
|
||||
|
||||
/** Returns true if a database exists for the given device address. */
|
||||
fun hasDatabaseFor(address: String?): Boolean {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@ object SettingsRoutes {
|
|||
|
||||
@Serializable data class Settings(val destNum: Int? = null) : Route
|
||||
|
||||
@Serializable data object DeviceConfiguration : Route
|
||||
|
||||
@Serializable data object ModuleConfiguration : Route
|
||||
|
||||
@Serializable data object Administration : Route
|
||||
|
||||
// region radio Config Routes
|
||||
|
||||
@Serializable data object User : Route
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@
|
|||
<string name="direct_message">Direct Message</string>
|
||||
<string name="nodedb_reset">NodeDB reset</string>
|
||||
<string name="delivery_confirmed">Delivery confirmed</string>
|
||||
<string name="delivery_confirmed_reboot_warning">Your device may disconnect and reboot while settings are applied.</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="ignore">Ignore</string>
|
||||
<string name="remove_ignored">Remove from ignored</string>
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class ServiceRepository @Inject constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
private val _meshPacketFlow = MutableSharedFlow<MeshPacket>()
|
||||
private val _meshPacketFlow = MutableSharedFlow<MeshPacket>(extraBufferCapacity = 64)
|
||||
val meshPacketFlow: SharedFlow<MeshPacket>
|
||||
get() = _meshPacketFlow
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue