mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-14 01:36:09 -04:00
feat: per device DB manager (#3641)
This commit is contained in:
@@ -18,7 +18,16 @@
|
||||
package com.geeksville.mesh
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
@@ -33,9 +42,22 @@ class MeshUtilApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initializeMaps(this)
|
||||
// Initialize DatabaseManager asynchronously with current device address so DAO consumers have an active DB
|
||||
val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
entryPoint.databaseManager().init(entryPoint.meshPrefs().deviceAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface AppEntryPoint {
|
||||
fun databaseManager(): DatabaseManager
|
||||
|
||||
fun meshPrefs(): MeshPrefs
|
||||
}
|
||||
|
||||
fun logAssert(executeReliableWrite: Boolean) {
|
||||
if (!executeReliableWrite) {
|
||||
val ex = AssertionError("Assertion failed")
|
||||
|
||||
@@ -58,6 +58,7 @@ import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
@@ -138,6 +139,8 @@ class MeshService : Service() {
|
||||
|
||||
@Inject lateinit var nodeRepository: NodeRepository
|
||||
|
||||
@Inject lateinit var databaseManager: DatabaseManager
|
||||
|
||||
@Inject lateinit var mqttRepository: MQTTRepository
|
||||
|
||||
@Inject lateinit var serviceNotifications: MeshServiceNotifications
|
||||
@@ -2008,8 +2011,14 @@ class MeshService : Service() {
|
||||
"SetDeviceAddress: Device address changed from ${currentAddr.anonymize} to ${deviceAddr.anonymize}",
|
||||
)
|
||||
meshPrefs.deviceAddress = deviceAddr
|
||||
clearDatabases()
|
||||
clearNotifications()
|
||||
serviceScope.handledLaunch {
|
||||
// Clear only in-memory caches to avoid cross-device bleed
|
||||
discardNodeDB()
|
||||
// Switch active on-disk DB to device-specific database
|
||||
databaseManager.switchActiveDatabase(deviceAddr)
|
||||
// Do not clear packet DB here; messages are per-device and should persist
|
||||
clearNotifications()
|
||||
}
|
||||
} else {
|
||||
Timber.d("SetDeviceAddress: Device address is unchanged, ignoring.")
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ dependencies {
|
||||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
|
||||
// Needed because core:data references MeshtasticDatabase (supertype RoomDatabase)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
|
||||
implementation(libs.androidx.core.location.altitude)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.timber)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.data.datasource
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeWithRelations
|
||||
|
||||
interface NodeInfoReadDataSource {
|
||||
fun myNodeInfoFlow(): Flow<MyNodeEntity?>
|
||||
|
||||
fun nodeDBbyNumFlow(): Flow<Map<Int, NodeWithRelations>>
|
||||
|
||||
fun getNodesFlow(
|
||||
sort: String,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
hopsAwayMax: Int,
|
||||
lastHeardMin: Int,
|
||||
): Flow<List<NodeWithRelations>>
|
||||
|
||||
suspend fun getNodesOlderThan(lastHeard: Int): List<NodeEntity>
|
||||
|
||||
suspend fun getUnknownNodes(): List<NodeEntity>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.data.datasource
|
||||
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
|
||||
interface NodeInfoWriteDataSource {
|
||||
suspend fun upsert(node: NodeEntity)
|
||||
|
||||
suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>)
|
||||
|
||||
suspend fun clearNodeDB()
|
||||
|
||||
suspend fun deleteNode(num: Int)
|
||||
|
||||
suspend fun deleteNodes(nodeNums: List<Int>)
|
||||
|
||||
suspend fun deleteMetadata(num: Int)
|
||||
|
||||
suspend fun upsert(metadata: MetadataEntity)
|
||||
|
||||
suspend fun setNodeNotes(num: Int, notes: String)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.data.datasource
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeWithRelations
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SwitchingNodeInfoReadDataSource @Inject constructor(private val dbManager: DatabaseManager) :
|
||||
NodeInfoReadDataSource {
|
||||
|
||||
override fun myNodeInfoFlow(): Flow<MyNodeEntity?> =
|
||||
dbManager.currentDb.flatMapLatest { db -> db.nodeInfoDao().getMyNodeInfo() }
|
||||
|
||||
override fun nodeDBbyNumFlow(): Flow<Map<Int, NodeWithRelations>> =
|
||||
dbManager.currentDb.flatMapLatest { db -> db.nodeInfoDao().nodeDBbyNum() }
|
||||
|
||||
override fun getNodesFlow(
|
||||
sort: String,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
hopsAwayMax: Int,
|
||||
lastHeardMin: Int,
|
||||
): Flow<List<NodeWithRelations>> = dbManager.currentDb.flatMapLatest { db ->
|
||||
db.nodeInfoDao()
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter,
|
||||
includeUnknown = includeUnknown,
|
||||
hopsAwayMax = hopsAwayMax,
|
||||
lastHeardMin = lastHeardMin,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getNodesOlderThan(lastHeard: Int): List<NodeEntity> =
|
||||
dbManager.withDb { it.nodeInfoDao().getNodesOlderThan(lastHeard) }
|
||||
|
||||
override suspend fun getUnknownNodes(): List<NodeEntity> = dbManager.withDb { it.nodeInfoDao().getUnknownNodes() }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.data.datasource
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SwitchingNodeInfoWriteDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
private val dbManager: DatabaseManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : NodeInfoWriteDataSource {
|
||||
|
||||
override suspend fun upsert(node: NodeEntity) =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().upsert(node) } }
|
||||
|
||||
override suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().installConfig(mi, nodes) } }
|
||||
|
||||
override suspend fun clearNodeDB() =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().clearNodeInfo() } }
|
||||
|
||||
override suspend fun deleteNode(num: Int) =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().deleteNode(num) } }
|
||||
|
||||
override suspend fun deleteNodes(nodeNums: List<Int>) =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().deleteNodes(nodeNums) } }
|
||||
|
||||
override suspend fun deleteMetadata(num: Int) =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().deleteMetadata(num) } }
|
||||
|
||||
override suspend fun upsert(metadata: MetadataEntity) =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().upsert(metadata) } }
|
||||
|
||||
override suspend fun setNodeNotes(num: Int, notes: String) =
|
||||
withContext(dispatchers.io) { dbManager.withDb { it.nodeInfoDao().setNodeNotes(num, notes) } }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.data.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
|
||||
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
|
||||
import org.meshtastic.core.data.datasource.SwitchingNodeInfoReadDataSource
|
||||
import org.meshtastic.core.data.datasource.SwitchingNodeInfoWriteDataSource
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface NodeDataSourceModule {
|
||||
@Binds @Singleton
|
||||
fun bindNodeInfoReadDataSource(impl: SwitchingNodeInfoReadDataSource): NodeInfoReadDataSource
|
||||
|
||||
@Binds @Singleton
|
||||
fun bindNodeInfoWriteDataSource(impl: SwitchingNodeInfoWriteDataSource): NodeInfoWriteDataSource
|
||||
}
|
||||
@@ -17,14 +17,14 @@
|
||||
|
||||
package org.meshtastic.core.data.repository
|
||||
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.MeshLogDao
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
@@ -37,16 +37,16 @@ import javax.inject.Inject
|
||||
class MeshLogRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val meshLogDaoLazy: Lazy<MeshLogDao>,
|
||||
private val dbManager: DatabaseManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val meshLogDao by lazy { meshLogDaoLazy.get() }
|
||||
|
||||
fun getAllLogs(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
|
||||
meshLogDao.getAllLogs(maxItems).flowOn(dispatchers.io).conflate()
|
||||
dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogs(maxItems) }.flowOn(dispatchers.io).conflate()
|
||||
|
||||
fun getAllLogsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
|
||||
meshLogDao.getAllLogsInReceiveOrder(maxItems).flowOn(dispatchers.io).conflate()
|
||||
fun getAllLogsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getAllLogsInReceiveOrder(maxItems) }
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
private fun parseTelemetryLog(log: MeshLog): Telemetry? = runCatching {
|
||||
Telemetry.parseFrom(log.fromRadio.packet.decoded.payload)
|
||||
@@ -106,8 +106,10 @@ constructor(
|
||||
}
|
||||
.getOrNull()
|
||||
|
||||
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> = meshLogDao
|
||||
.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
|
||||
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> = dbManager.currentDb
|
||||
.flatMapLatest {
|
||||
it.meshLogDao().getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
|
||||
.flowOn(dispatchers.io)
|
||||
@@ -116,8 +118,10 @@ constructor(
|
||||
nodeNum: Int,
|
||||
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
|
||||
maxItem: Int = MAX_MESH_PACKETS,
|
||||
): Flow<List<MeshLog>> =
|
||||
meshLogDao.getLogsFrom(nodeNum, portNum, maxItem).distinctUntilChanged().flowOn(dispatchers.io)
|
||||
): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, portNum, maxItem) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
/*
|
||||
* Retrieves MeshPackets matching 'nodeNum' and 'portNum'.
|
||||
@@ -130,14 +134,16 @@ constructor(
|
||||
.mapLatest { list -> list.firstOrNull { it.myNodeInfo != null }?.myNodeInfo }
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
suspend fun insert(log: MeshLog) = withContext(dispatchers.io) { meshLogDao.insert(log) }
|
||||
suspend fun insert(log: MeshLog) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().insert(log) }
|
||||
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) { meshLogDao.deleteAll() }
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteAll() }
|
||||
|
||||
suspend fun deleteLog(uuid: String) = withContext(dispatchers.io) { meshLogDao.deleteLog(uuid) }
|
||||
suspend fun deleteLog(uuid: String) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteLog(uuid) }
|
||||
|
||||
suspend fun deleteLogs(nodeNum: Int, portNum: Int) =
|
||||
withContext(dispatchers.io) { meshLogDao.deleteLogs(nodeNum, portNum) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteLogs(nodeNum, portNum) }
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS = 500
|
||||
|
||||
@@ -30,7 +30,8 @@ import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.NodeInfoDao
|
||||
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
|
||||
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
@@ -49,13 +50,14 @@ class NodeRepository
|
||||
@Inject
|
||||
constructor(
|
||||
processLifecycle: Lifecycle,
|
||||
private val nodeInfoDao: NodeInfoDao,
|
||||
private val nodeInfoReadDataSource: NodeInfoReadDataSource,
|
||||
private val nodeInfoWriteDataSource: NodeInfoWriteDataSource,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
// hardware info about our local device (can be null)
|
||||
val myNodeInfo: StateFlow<MyNodeEntity?> =
|
||||
nodeInfoDao
|
||||
.getMyNodeInfo()
|
||||
nodeInfoReadDataSource
|
||||
.myNodeInfoFlow()
|
||||
.flowOn(dispatchers.io)
|
||||
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null)
|
||||
|
||||
@@ -69,12 +71,13 @@ constructor(
|
||||
val myId: StateFlow<String?>
|
||||
get() = _myId
|
||||
|
||||
fun getNodeDBbyNum() = nodeInfoDao.nodeDBbyNum().map { map -> map.mapValues { (_, it) -> it.toEntity() } }
|
||||
fun getNodeDBbyNum() =
|
||||
nodeInfoReadDataSource.nodeDBbyNumFlow().map { map -> map.mapValues { (_, it) -> it.toEntity() } }
|
||||
|
||||
// A map from nodeNum to Node
|
||||
val nodeDBbyNum: StateFlow<Map<Int, Node>> =
|
||||
nodeInfoDao
|
||||
.nodeDBbyNum()
|
||||
nodeInfoReadDataSource
|
||||
.nodeDBbyNumFlow()
|
||||
.mapLatest { map -> map.mapValues { (_, it) -> it.toModel() } }
|
||||
.onEach {
|
||||
val ourNodeInfo = it.values.firstOrNull()
|
||||
@@ -116,8 +119,8 @@ constructor(
|
||||
includeUnknown: Boolean = true,
|
||||
onlyOnline: Boolean = false,
|
||||
onlyDirect: Boolean = false,
|
||||
) = nodeInfoDao
|
||||
.getNodes(
|
||||
) = nodeInfoReadDataSource
|
||||
.getNodesFlow(
|
||||
sort = sort.sqlValue,
|
||||
filter = filter,
|
||||
includeUnknown = includeUnknown,
|
||||
@@ -128,40 +131,46 @@ constructor(
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoDao.upsert(node) }
|
||||
suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node) }
|
||||
|
||||
suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) =
|
||||
withContext(dispatchers.io) { nodeInfoDao.installConfig(mi, nodes) }
|
||||
withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi, nodes) }
|
||||
|
||||
suspend fun clearNodeDB() = withContext(dispatchers.io) { nodeInfoDao.clearNodeInfo() }
|
||||
suspend fun clearNodeDB() = withContext(dispatchers.io) { nodeInfoWriteDataSource.clearNodeDB() }
|
||||
|
||||
suspend fun deleteNode(num: Int) = withContext(dispatchers.io) {
|
||||
nodeInfoDao.deleteNode(num)
|
||||
nodeInfoDao.deleteMetadata(num)
|
||||
nodeInfoWriteDataSource.deleteNode(num)
|
||||
nodeInfoWriteDataSource.deleteMetadata(num)
|
||||
}
|
||||
|
||||
suspend fun deleteNodes(nodeNums: List<Int>) = withContext(dispatchers.io) {
|
||||
nodeInfoDao.deleteNodes(nodeNums)
|
||||
nodeNums.forEach { nodeInfoDao.deleteMetadata(it) }
|
||||
nodeInfoWriteDataSource.deleteNodes(nodeNums)
|
||||
nodeNums.forEach { nodeInfoWriteDataSource.deleteMetadata(it) }
|
||||
}
|
||||
|
||||
suspend fun getNodesOlderThan(lastHeard: Int): List<NodeEntity> =
|
||||
withContext(dispatchers.io) { nodeInfoDao.getNodesOlderThan(lastHeard) }
|
||||
withContext(dispatchers.io) { nodeInfoReadDataSource.getNodesOlderThan(lastHeard) }
|
||||
|
||||
suspend fun getUnknownNodes(): List<NodeEntity> = withContext(dispatchers.io) { nodeInfoDao.getUnknownNodes() }
|
||||
suspend fun getUnknownNodes(): List<NodeEntity> =
|
||||
withContext(dispatchers.io) { nodeInfoReadDataSource.getUnknownNodes() }
|
||||
|
||||
suspend fun insertMetadata(metadata: MetadataEntity) = withContext(dispatchers.io) { nodeInfoDao.upsert(metadata) }
|
||||
suspend fun insertMetadata(metadata: MetadataEntity) =
|
||||
withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(metadata) }
|
||||
|
||||
val onlineNodeCount: Flow<Int> =
|
||||
nodeInfoDao
|
||||
.nodeDBbyNum()
|
||||
nodeInfoReadDataSource
|
||||
.nodeDBbyNumFlow()
|
||||
.mapLatest { map -> map.values.count { it.node.lastHeard > onlineTimeThreshold() } }
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
val totalNodeCount: Flow<Int> =
|
||||
nodeInfoDao.nodeDBbyNum().mapLatest { map -> map.values.count() }.flowOn(dispatchers.io).conflate()
|
||||
nodeInfoReadDataSource
|
||||
.nodeDBbyNumFlow()
|
||||
.mapLatest { map -> map.values.count() }
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
suspend fun setNodeNotes(num: Int, notes: String) =
|
||||
withContext(dispatchers.io) { nodeInfoDao.setNodeNotes(num, notes) }
|
||||
withContext(dispatchers.io) { nodeInfoWriteDataSource.setNodeNotes(num, notes) }
|
||||
}
|
||||
|
||||
@@ -17,45 +17,54 @@
|
||||
|
||||
package org.meshtastic.core.data.repository
|
||||
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.PacketDao
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.ContactSettings
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.proto.ChannelProtos.ChannelSettings
|
||||
import org.meshtastic.proto.Portnums.PortNum
|
||||
import javax.inject.Inject
|
||||
|
||||
class PacketRepository @Inject constructor(private val packetDaoLazy: Lazy<PacketDao>) {
|
||||
private val packetDao by lazy { packetDaoLazy.get() }
|
||||
class PacketRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val dbManager: DatabaseManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
fun getWaypoints(): Flow<List<Packet>> =
|
||||
dbManager.currentDb.flatMapLatest { db -> db.packetDao().getAllPackets(PortNum.WAYPOINT_APP_VALUE) }
|
||||
|
||||
fun getWaypoints(): Flow<List<Packet>> = packetDao.getAllPackets(PortNum.WAYPOINT_APP_VALUE)
|
||||
|
||||
fun getContacts(): Flow<Map<String, Packet>> = packetDao.getContactKeys()
|
||||
fun getContacts(): Flow<Map<String, Packet>> =
|
||||
dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactKeys() }
|
||||
|
||||
suspend fun getMessageCount(contact: String): Int =
|
||||
withContext(Dispatchers.IO) { packetDao.getMessageCount(contact) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getMessageCount(contact) }
|
||||
|
||||
suspend fun getUnreadCount(contact: String): Int = withContext(Dispatchers.IO) { packetDao.getUnreadCount(contact) }
|
||||
suspend fun getUnreadCount(contact: String): Int =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getUnreadCount(contact) }
|
||||
|
||||
fun getUnreadCountTotal(): Flow<Int> = packetDao.getUnreadCountTotal()
|
||||
fun getUnreadCountTotal(): Flow<Int> =
|
||||
dbManager.currentDb.flatMapLatest { db -> db.packetDao().getUnreadCountTotal() }
|
||||
|
||||
suspend fun clearUnreadCount(contact: String, timestamp: Long) =
|
||||
withContext(Dispatchers.IO) { packetDao.clearUnreadCount(contact, timestamp) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().clearUnreadCount(contact, timestamp) }
|
||||
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? = withContext(Dispatchers.IO) { packetDao.getQueuedPackets() }
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getQueuedPackets() }
|
||||
|
||||
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) { packetDao.insert(packet) }
|
||||
suspend fun insert(packet: Packet) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(packet) }
|
||||
|
||||
suspend fun getMessagesFrom(contact: String, getNode: suspend (String?) -> Node) = withContext(Dispatchers.IO) {
|
||||
packetDao.getMessagesFrom(contact).mapLatest { packets ->
|
||||
suspend fun getMessagesFrom(contact: String, getNode: suspend (String?) -> Node) = withContext(dispatchers.io) {
|
||||
dbManager.currentDb.value.packetDao().getMessagesFrom(contact).mapLatest { packets ->
|
||||
packets.map { packet ->
|
||||
val message = packet.toMessage(getNode)
|
||||
message.replyId
|
||||
@@ -68,43 +77,53 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: Lazy<Packe
|
||||
}
|
||||
|
||||
suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) =
|
||||
withContext(Dispatchers.IO) { packetDao.updateMessageStatus(d, m) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageStatus(d, m) }
|
||||
|
||||
suspend fun updateMessageId(d: DataPacket, id: Int) =
|
||||
withContext(Dispatchers.IO) { packetDao.updateMessageId(d, id) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().updateMessageId(d, id) }
|
||||
|
||||
suspend fun getPacketById(requestId: Int) = withContext(Dispatchers.IO) { packetDao.getPacketById(requestId) }
|
||||
suspend fun getPacketById(requestId: Int) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketById(requestId) }
|
||||
|
||||
suspend fun getPacketByPacketId(packetId: Int) =
|
||||
withContext(Dispatchers.IO) { packetDao.getPacketByPacketId(packetId) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().getPacketByPacketId(packetId) }
|
||||
|
||||
suspend fun deleteMessages(uuidList: List<Long>) = withContext(Dispatchers.IO) {
|
||||
for (chunk in uuidList.chunked(500)) { // limit number of UUIDs per query
|
||||
packetDao.deleteMessages(chunk)
|
||||
suspend fun deleteMessages(uuidList: List<Long>) = withContext(dispatchers.io) {
|
||||
for (chunk in uuidList.chunked(500)) {
|
||||
// Fetch DAO per chunk to avoid holding a stale reference if the active DB switches
|
||||
dbManager.currentDb.value.packetDao().deleteMessages(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteContacts(contactList: List<String>) =
|
||||
withContext(Dispatchers.IO) { packetDao.deleteContacts(contactList) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteContacts(contactList) }
|
||||
|
||||
suspend fun deleteWaypoint(id: Int) = withContext(Dispatchers.IO) { packetDao.deleteWaypoint(id) }
|
||||
suspend fun deleteWaypoint(id: Int) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteWaypoint(id) }
|
||||
|
||||
suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) { packetDao.delete(packet) }
|
||||
suspend fun delete(packet: Packet) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().delete(packet) }
|
||||
|
||||
suspend fun update(packet: Packet) = withContext(Dispatchers.IO) { packetDao.update(packet) }
|
||||
suspend fun update(packet: Packet) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().update(packet) }
|
||||
|
||||
fun getContactSettings(): Flow<Map<String, ContactSettings>> = packetDao.getContactSettings()
|
||||
fun getContactSettings(): Flow<Map<String, ContactSettings>> =
|
||||
dbManager.currentDb.flatMapLatest { db -> db.packetDao().getContactSettings() }
|
||||
|
||||
suspend fun getContactSettings(contact: String) =
|
||||
withContext(Dispatchers.IO) { packetDao.getContactSettings(contact) ?: ContactSettings(contact) }
|
||||
suspend fun getContactSettings(contact: String) = withContext(dispatchers.io) {
|
||||
dbManager.currentDb.value.packetDao().getContactSettings(contact) ?: ContactSettings(contact)
|
||||
}
|
||||
|
||||
suspend fun setMuteUntil(contacts: List<String>, until: Long) =
|
||||
withContext(Dispatchers.IO) { packetDao.setMuteUntil(contacts, until) }
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().setMuteUntil(contacts, until) }
|
||||
|
||||
suspend fun insertReaction(reaction: ReactionEntity) = withContext(Dispatchers.IO) { packetDao.insert(reaction) }
|
||||
suspend fun insertReaction(reaction: ReactionEntity) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().insert(reaction) }
|
||||
|
||||
suspend fun clearPacketDB() = withContext(Dispatchers.IO) { packetDao.deleteAll() }
|
||||
suspend fun clearPacketDB() = withContext(dispatchers.io) { dbManager.currentDb.value.packetDao().deleteAll() }
|
||||
|
||||
suspend fun migrateChannelsByPSK(oldSettings: List<ChannelSettings>, newSettings: List<ChannelSettings>) =
|
||||
packetDao.migrateChannelsByPSK(oldSettings, newSettings)
|
||||
withContext(dispatchers.io) {
|
||||
dbManager.currentDb.value.packetDao().migrateChannelsByPSK(oldSettings, newSettings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
package org.meshtastic.core.data.repository
|
||||
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.QuickChatActionDao
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import javax.inject.Inject
|
||||
@@ -28,19 +28,20 @@ import javax.inject.Inject
|
||||
class QuickChatActionRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val quickChatDaoLazy: Lazy<QuickChatActionDao>,
|
||||
private val dbManager: DatabaseManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val quickChatActionDao by lazy { quickChatDaoLazy.get() }
|
||||
fun getAllActions() = dbManager.currentDb.flatMapLatest { it.quickChatActionDao().getAll() }.flowOn(dispatchers.io)
|
||||
|
||||
fun getAllActions() = quickChatActionDao.getAll().flowOn(dispatchers.io)
|
||||
suspend fun upsert(action: QuickChatAction) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.quickChatActionDao().upsert(action) }
|
||||
|
||||
suspend fun upsert(action: QuickChatAction) = withContext(dispatchers.io) { quickChatActionDao.upsert(action) }
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) { dbManager.currentDb.value.quickChatActionDao().deleteAll() }
|
||||
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) { quickChatActionDao.deleteAll() }
|
||||
suspend fun delete(action: QuickChatAction) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.quickChatActionDao().delete(action) }
|
||||
|
||||
suspend fun delete(action: QuickChatAction) = withContext(dispatchers.io) { quickChatActionDao.delete(action) }
|
||||
|
||||
suspend fun setItemPosition(uuid: Long, newPos: Int) =
|
||||
withContext(dispatchers.io) { quickChatActionDao.updateActionPosition(uuid, newPos) }
|
||||
suspend fun setItemPosition(uuid: Long, newPos: Int) = withContext(dispatchers.io) {
|
||||
dbManager.currentDb.value.quickChatActionDao().updateActionPosition(uuid, newPos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.database
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.room.Room
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Manages per-device Room database instances for node data, with LRU eviction. */
|
||||
@Singleton
|
||||
class DatabaseManager @Inject constructor(private val app: Application) {
|
||||
val prefs: SharedPreferences = app.getSharedPreferences("db-manager-prefs", Context.MODE_PRIVATE)
|
||||
private val managerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
// Expose the DB cache limit as a reactive stream so UI can observe changes.
|
||||
private val _cacheLimit = MutableStateFlow(getCacheLimit())
|
||||
val cacheLimit: StateFlow<Int> = _cacheLimit
|
||||
|
||||
// Keep cache-limit StateFlow in sync if some other component updates SharedPreferences.
|
||||
private val prefsListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == DatabaseConstants.CACHE_LIMIT_KEY) {
|
||||
_cacheLimit.value = getCacheLimit()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
prefs.registerOnSharedPreferenceChangeListener(prefsListener)
|
||||
}
|
||||
|
||||
private val _currentDb = MutableStateFlow<MeshtasticDatabase?>(null)
|
||||
val currentDb: StateFlow<MeshtasticDatabase> =
|
||||
_currentDb.filterNotNull().stateIn(managerScope, SharingStarted.Eagerly, buildRoomDb(app, defaultDbName()))
|
||||
|
||||
private val _currentAddress = MutableStateFlow<String?>(null)
|
||||
val currentAddress: StateFlow<String?> = _currentAddress
|
||||
|
||||
private val dbCache = mutableMapOf<String, MeshtasticDatabase>() // key = dbName
|
||||
|
||||
/** Initialize the active database for [address]. */
|
||||
suspend fun init(address: String?) = switchActiveDatabase(address)
|
||||
|
||||
/** Switch active database to the one associated with [address]. Serialized via mutex. */
|
||||
suspend fun switchActiveDatabase(address: String?) = mutex.withLock {
|
||||
val dbName = buildDbName(address)
|
||||
|
||||
// Fast path: no-op if already on this address
|
||||
if (_currentAddress.value == address && _currentDb.value != null) {
|
||||
markLastUsed(dbName)
|
||||
return
|
||||
}
|
||||
|
||||
// Build/open Room DB off the main thread
|
||||
val db =
|
||||
dbCache[dbName]
|
||||
?: withContext(Dispatchers.IO) { buildRoomDb(app, dbName) }.also { dbCache[dbName] = it }
|
||||
|
||||
_currentDb.value = db
|
||||
_currentAddress.value = address
|
||||
markLastUsed(dbName)
|
||||
|
||||
// Defer LRU eviction so switch is not blocked by filesystem work
|
||||
managerScope.launch(Dispatchers.IO) { enforceCacheLimit(activeDbName = dbName) }
|
||||
|
||||
Timber.i("Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}")
|
||||
}
|
||||
|
||||
/** Execute [block] with the current DB instance. */
|
||||
fun <T> withDb(block: (MeshtasticDatabase) -> T): T = block(currentDb.value)
|
||||
|
||||
private fun markLastUsed(dbName: String) {
|
||||
prefs.edit().putLong(lastUsedKey(dbName), System.currentTimeMillis()).apply()
|
||||
}
|
||||
|
||||
private fun lastUsed(dbName: String): Long {
|
||||
val k = lastUsedKey(dbName)
|
||||
val v = prefs.getLong(k, 0L)
|
||||
return if (v == 0L) getDbFile(app, dbName)?.lastModified() ?: 0L else v
|
||||
}
|
||||
|
||||
private fun listExistingDbNames(): List<String> {
|
||||
val base = app.getDatabasePath(DatabaseConstants.LEGACY_DB_NAME)
|
||||
val dir = base.parentFile ?: return emptyList()
|
||||
val names = dir.listFiles()?.mapNotNull { f -> f.name } ?: emptyList()
|
||||
return names
|
||||
.filter { it.startsWith(DatabaseConstants.DB_PREFIX) }
|
||||
.filterNot { it.endsWith("-wal") || it.endsWith("-shm") }
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private suspend fun enforceCacheLimit(activeDbName: String) = mutex.withLock {
|
||||
val limit = getCacheLimit()
|
||||
val all = listExistingDbNames()
|
||||
if (all.size <= limit) return
|
||||
val victims = all.filter { it != activeDbName }.sortedBy { lastUsed(it) }.take(all.size - limit)
|
||||
victims.forEach { name ->
|
||||
runCatching { dbCache.remove(name)?.close() }
|
||||
.onFailure { Timber.w(it, "Failed to close database %s", name) }
|
||||
app.deleteDatabase(name)
|
||||
prefs.edit().remove(lastUsedKey(name)).apply()
|
||||
Timber.i("Evicted cached DB ${anonymizeDbName(name)}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getCacheLimit(): Int = prefs
|
||||
.getInt(DatabaseConstants.CACHE_LIMIT_KEY, DatabaseConstants.DEFAULT_CACHE_LIMIT)
|
||||
.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
|
||||
|
||||
fun setCacheLimit(limit: Int) {
|
||||
val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
|
||||
if (clamped == getCacheLimit()) return
|
||||
prefs.edit().putInt(DatabaseConstants.CACHE_LIMIT_KEY, clamped).apply()
|
||||
_cacheLimit.value = clamped
|
||||
// Enforce asynchronously with current active DB protected
|
||||
val active = _currentDb.value?.openHelper?.databaseName ?: defaultDbName()
|
||||
managerScope.launch(Dispatchers.IO) { enforceCacheLimit(activeDbName = active) }
|
||||
}
|
||||
}
|
||||
|
||||
object DatabaseConstants {
|
||||
const val DB_PREFIX: String = "meshtastic_database"
|
||||
const val LEGACY_DB_NAME: String = DB_PREFIX
|
||||
const val DEFAULT_DB_NAME: String = "${DB_PREFIX}_default"
|
||||
|
||||
const val CACHE_LIMIT_KEY: String = "node_db_cache_limit"
|
||||
const val DEFAULT_CACHE_LIMIT: Int = 3
|
||||
const val MIN_CACHE_LIMIT: Int = 1
|
||||
const val MAX_CACHE_LIMIT: Int = 10
|
||||
|
||||
// Display/truncation and hash sizing for DB names
|
||||
const val DB_NAME_HASH_LEN: Int = 10
|
||||
const val DB_NAME_SEPARATOR_LEN: Int = 1
|
||||
const val DB_NAME_SUFFIX_LEN: Int = 3
|
||||
|
||||
// Address anonymization sizing
|
||||
const val ADDRESS_ANON_SHORT_LEN: Int = 4
|
||||
const val ADDRESS_ANON_EDGE_LEN: Int = 2
|
||||
}
|
||||
|
||||
// File-private helpers (kept outside the class to reduce class function count)
|
||||
private fun defaultDbName(): String = DatabaseConstants.DEFAULT_DB_NAME
|
||||
|
||||
private fun normalizeAddress(addr: String?): String = addr?.uppercase()?.replace(":", "") ?: "DEFAULT"
|
||||
|
||||
private fun shortSha1(s: String): String = MessageDigest.getInstance("SHA-1")
|
||||
.digest(s.toByteArray())
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
.take(DatabaseConstants.DB_NAME_HASH_LEN)
|
||||
|
||||
private fun buildDbName(address: String?): String = if (address.isNullOrBlank()) {
|
||||
defaultDbName()
|
||||
} else {
|
||||
"${DatabaseConstants.DB_PREFIX}_${shortSha1(normalizeAddress(address))}"
|
||||
}
|
||||
|
||||
private fun lastUsedKey(dbName: String) = "db_last_used:$dbName"
|
||||
|
||||
private fun anonymizeAddress(address: String?): String = when {
|
||||
address == null -> "null"
|
||||
address.length <= DatabaseConstants.ADDRESS_ANON_SHORT_LEN -> address
|
||||
else ->
|
||||
address.take(DatabaseConstants.ADDRESS_ANON_EDGE_LEN) +
|
||||
"…" +
|
||||
address.takeLast(DatabaseConstants.ADDRESS_ANON_EDGE_LEN)
|
||||
}
|
||||
|
||||
private fun anonymizeDbName(name: String): String =
|
||||
if (name == DatabaseConstants.LEGACY_DB_NAME || name == DatabaseConstants.DEFAULT_DB_NAME) {
|
||||
name
|
||||
} else {
|
||||
name.take(
|
||||
DatabaseConstants.DB_PREFIX.length +
|
||||
DatabaseConstants.DB_NAME_SEPARATOR_LEN +
|
||||
DatabaseConstants.DB_NAME_SUFFIX_LEN,
|
||||
) + "…"
|
||||
}
|
||||
|
||||
private fun buildRoomDb(app: Application, dbName: String): MeshtasticDatabase =
|
||||
Room.databaseBuilder(app.applicationContext, MeshtasticDatabase::class.java, dbName)
|
||||
.fallbackToDestructiveMigration(false)
|
||||
.build()
|
||||
|
||||
private fun getDbFile(app: Application, dbName: String): File? = app.getDatabasePath(dbName).takeIf { it.exists() }
|
||||
@@ -499,6 +499,8 @@
|
||||
<string name="allow_input_source">Allow input source</string>
|
||||
<string name="send_bell">Send bell</string>
|
||||
<string name="messages">Messages</string>
|
||||
<string name="device_db_cache_limit">Device DB cache limit</string>
|
||||
<string name="device_db_cache_limit_summary">Max device databases to keep on this phone</string>
|
||||
<string name="detection_sensor_config">Detection Sensor Config</string>
|
||||
<string name="detection_sensor_enabled">Detection Sensor enabled</string>
|
||||
<string name="minimum_broadcast_seconds">Minimum broadcast (seconds)</string>
|
||||
|
||||
@@ -64,7 +64,9 @@ import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.common.gpsDisabled
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.ui.component.DropDownPreference
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
|
||||
@@ -307,6 +309,22 @@ fun SettingsScreen(
|
||||
) {
|
||||
showThemePickerDialog = true
|
||||
}
|
||||
|
||||
// Node DB cache limit (App setting)
|
||||
val cacheLimit = settingsViewModel.dbCacheLimit.collectAsStateWithLifecycle().value
|
||||
val cacheItems = remember {
|
||||
(DatabaseConstants.MIN_CACHE_LIMIT..DatabaseConstants.MAX_CACHE_LIMIT).map {
|
||||
it.toLong() to it.toString()
|
||||
}
|
||||
}
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.device_db_cache_limit),
|
||||
enabled = true,
|
||||
items = cacheItems,
|
||||
selectedItem = cacheLimit.toLong(),
|
||||
onItemSelected = { selected -> settingsViewModel.setDbCacheLimit(selected.toInt()) },
|
||||
summary = stringResource(Res.string.device_db_cache_limit_summary),
|
||||
)
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val nodeName = ourNode?.user?.shortName ?: ""
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
@@ -71,6 +73,7 @@ constructor(
|
||||
private val uiPrefs: UiPrefs,
|
||||
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
||||
private val buildConfigProvider: BuildConfigProvider,
|
||||
private val databaseManager: DatabaseManager,
|
||||
) : ViewModel() {
|
||||
val myNodeInfo: StateFlow<MyNodeEntity?> = nodeRepository.myNodeInfo
|
||||
|
||||
@@ -106,6 +109,14 @@ constructor(
|
||||
val appVersionName
|
||||
get() = buildConfigProvider.versionName
|
||||
|
||||
// Device DB cache limit (bounded by DatabaseConstants)
|
||||
val dbCacheLimit: StateFlow<Int> = databaseManager.cacheLimit
|
||||
|
||||
fun setDbCacheLimit(limit: Int) {
|
||||
val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
|
||||
databaseManager.setCacheLimit(clamped)
|
||||
}
|
||||
|
||||
fun setProvideLocation(value: Boolean) {
|
||||
myNodeNum?.let { uiPrefs.setShouldProvideNodeLocation(it, value) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user