mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: migrate :core:database to Room Kotlin Multiplatform (#4702)
This commit is contained in:
parent
744db2d5bd
commit
6a858acb4a
42 changed files with 406 additions and 264 deletions
|
|
@ -35,7 +35,7 @@ kotlin {
|
|||
implementation(libs.kermit)
|
||||
}
|
||||
androidMain.dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
api(libs.androidx.core.ktx)
|
||||
api(libs.nordic.common.core)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ dependencies {
|
|||
// Needed because core:data references MeshtasticDatabase (supertype RoomDatabase)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.paging)
|
||||
implementation(libs.androidx.sqlite.bundled)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@
|
|||
*/
|
||||
package org.meshtastic.core.data.datasource
|
||||
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.DeviceHardwareDao
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.DeviceHardwareEntity
|
||||
import org.meshtastic.core.database.entity.asEntity
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
|
|
@ -28,10 +27,11 @@ import javax.inject.Inject
|
|||
class DeviceHardwareLocalDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
private val deviceHardwareDaoLazy: Lazy<DeviceHardwareDao>,
|
||||
private val dbManager: DatabaseManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val deviceHardwareDao by lazy { deviceHardwareDaoLazy.get() }
|
||||
private val deviceHardwareDao
|
||||
get() = dbManager.currentDb.value.deviceHardwareDao()
|
||||
|
||||
suspend fun insertAllDeviceHardware(deviceHardware: List<NetworkDeviceHardware>) =
|
||||
withContext(dispatchers.io) { deviceHardwareDao.insertAll(deviceHardware.map { it.asEntity() }) }
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@
|
|||
*/
|
||||
package org.meshtastic.core.data.datasource
|
||||
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.FirmwareReleaseDao
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseType
|
||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
|
|
@ -30,10 +29,11 @@ import javax.inject.Inject
|
|||
class FirmwareReleaseLocalDataSource
|
||||
@Inject
|
||||
constructor(
|
||||
private val firmwareReleaseDaoLazy: Lazy<FirmwareReleaseDao>,
|
||||
private val dbManager: DatabaseManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val firmwareReleaseDao by lazy { firmwareReleaseDaoLazy.get() }
|
||||
private val firmwareReleaseDao
|
||||
get() = dbManager.currentDb.value.firmwareReleaseDao()
|
||||
|
||||
suspend fun insertFirmwareReleases(
|
||||
firmwareReleases: List<NetworkFirmwareRelease>,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.data.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
interface DatabaseModule {
|
||||
|
||||
@Binds @Singleton
|
||||
fun bindDatabaseManager(impl: DatabaseManager): org.meshtastic.core.repository.DatabaseManager
|
||||
}
|
||||
|
|
@ -201,10 +201,12 @@ constructor(
|
|||
val currentPosition =
|
||||
when {
|
||||
provideLocation && position.isValid() -> position
|
||||
else ->
|
||||
provideLocation ->
|
||||
nodeManager.nodeDBbyNodeNum[myNodeNum]?.position?.let { Position(it) }?.takeIf { it.isValid() }
|
||||
?: Position(0.0, 0.0, 0)
|
||||
else -> Position(0.0, 0.0, 0)
|
||||
}
|
||||
currentPosition?.let { commandSender.requestPosition(destNum, it) }
|
||||
commandSender.requestPosition(destNum, currentPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -440,11 +440,13 @@ constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
environment != null -> nextNode = nextNode.copy(environmentMetrics = environment)
|
||||
power != null -> nextNode = nextNode.copy(powerMetrics = power)
|
||||
}
|
||||
nextNode
|
||||
|
||||
val telemetryTime = if (t.time != 0) t.time else nextNode.lastHeard
|
||||
val newLastHeard = maxOf(nextNode.lastHeard, telemetryTime)
|
||||
nextNode.copy(lastHeard = newLastHeard)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -192,23 +192,43 @@ constructor(
|
|||
}
|
||||
|
||||
override fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long) {
|
||||
if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) {
|
||||
Logger.d { "Ignoring nop position update for the local node" }
|
||||
} else {
|
||||
updateNode(fromNum) { node ->
|
||||
node.copy(position = p.copy(time = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt()))
|
||||
}
|
||||
val isZeroPos = (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0
|
||||
@Suppress("ComplexCondition")
|
||||
if (myNodeNum == fromNum && isZeroPos && p.sats_in_view == 0 && p.time == 0) {
|
||||
Logger.d { "Ignoring empty position update for the local node" }
|
||||
return
|
||||
}
|
||||
|
||||
updateNode(fromNum) { node ->
|
||||
val posTime = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt()
|
||||
val newLastHeard = maxOf(node.lastHeard, posTime)
|
||||
|
||||
val newPos =
|
||||
if (isZeroPos) {
|
||||
p.copy(
|
||||
time = posTime,
|
||||
latitude_i = node.position.latitude_i,
|
||||
longitude_i = node.position.longitude_i,
|
||||
altitude = p.altitude ?: node.position.altitude,
|
||||
sats_in_view = p.sats_in_view,
|
||||
)
|
||||
} else {
|
||||
p.copy(time = posTime)
|
||||
}
|
||||
|
||||
node.copy(position = newPos, lastHeard = newLastHeard)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) {
|
||||
updateNode(fromNum) { node ->
|
||||
when {
|
||||
telemetry.device_metrics != null -> node.copy(deviceMetrics = telemetry.device_metrics!!)
|
||||
telemetry.environment_metrics != null -> node.copy(environmentMetrics = telemetry.environment_metrics!!)
|
||||
telemetry.power_metrics != null -> node.copy(powerMetrics = telemetry.power_metrics!!)
|
||||
else -> node
|
||||
}
|
||||
var nextNode = node
|
||||
telemetry.device_metrics?.let { nextNode = nextNode.copy(deviceMetrics = it) }
|
||||
telemetry.environment_metrics?.let { nextNode = nextNode.copy(environmentMetrics = it) }
|
||||
telemetry.power_metrics?.let { nextNode = nextNode.copy(powerMetrics = it) }
|
||||
val telemetryTime = if (telemetry.time != 0) telemetry.time else node.lastHeard
|
||||
val newLastHeard = maxOf(node.lastHeard, telemetryTime)
|
||||
nextNode.copy(lastHeard = newLastHeard)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,8 @@ constructor(
|
|||
num = num,
|
||||
user = user,
|
||||
position = position,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,85 @@ class NodeManagerImplTest {
|
|||
assertEquals(90.0, result.longitude, 0.0001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedPosition with zero coordinates preserves last known location but updates satellites`() {
|
||||
val nodeNum = 1234
|
||||
val initialPosition = Position(latitude_i = 450000000, longitude_i = 900000000, sats_in_view = 10)
|
||||
nodeManager.handleReceivedPosition(nodeNum, 9999, initialPosition, 1000000L)
|
||||
|
||||
// Receive "zero" position with new satellite count
|
||||
val zeroPosition = Position(latitude_i = 0, longitude_i = 0, sats_in_view = 5, time = 1001)
|
||||
nodeManager.handleReceivedPosition(nodeNum, 9999, zeroPosition, 1001000L)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertEquals(45.0, result!!.latitude, 0.0001)
|
||||
assertEquals(90.0, result.longitude, 0.0001)
|
||||
assertEquals(5, result.position.sats_in_view)
|
||||
assertEquals(1001, result.lastHeard)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedPosition for local node ignores purely empty packets`() {
|
||||
val myNum = 1111
|
||||
val emptyPos = Position(latitude_i = 0, longitude_i = 0, sats_in_view = 0, time = 0)
|
||||
|
||||
nodeManager.handleReceivedPosition(myNum, myNum, emptyPos, 0)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[myNum]
|
||||
// Should still be a default/unset node if it didn't exist, or shouldn't have position
|
||||
assertTrue(result == null || result.position.latitude_i == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedTelemetry updates lastHeard`() {
|
||||
val nodeNum = 1234
|
||||
nodeManager.updateNode(nodeNum) { it.copy(lastHeard = 1000) }
|
||||
|
||||
val telemetry =
|
||||
org.meshtastic.proto.Telemetry(
|
||||
time = 2000,
|
||||
device_metrics = org.meshtastic.proto.DeviceMetrics(battery_level = 50),
|
||||
)
|
||||
|
||||
nodeManager.handleReceivedTelemetry(nodeNum, telemetry)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertEquals(2000, result!!.lastHeard)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedTelemetry updates device metrics`() {
|
||||
val nodeNum = 1234
|
||||
val telemetry =
|
||||
org.meshtastic.proto.Telemetry(
|
||||
device_metrics = org.meshtastic.proto.DeviceMetrics(battery_level = 75, voltage = 3.8f),
|
||||
)
|
||||
|
||||
nodeManager.handleReceivedTelemetry(nodeNum, telemetry)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertNotNull(result!!.deviceMetrics)
|
||||
assertEquals(75, result.deviceMetrics.battery_level)
|
||||
assertEquals(3.8f, result.deviceMetrics.voltage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleReceivedTelemetry updates environment metrics`() {
|
||||
val nodeNum = 1234
|
||||
val telemetry =
|
||||
org.meshtastic.proto.Telemetry(
|
||||
environment_metrics =
|
||||
org.meshtastic.proto.EnvironmentMetrics(temperature = 22.5f, relative_humidity = 45.0f),
|
||||
)
|
||||
|
||||
nodeManager.handleReceivedTelemetry(nodeNum, telemetry)
|
||||
|
||||
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
|
||||
assertNotNull(result!!.environmentMetrics)
|
||||
assertEquals(22.5f, result.environmentMetrics.temperature)
|
||||
assertEquals(45.0f, result.environmentMetrics.relative_humidity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear resets internal state`() {
|
||||
nodeManager.updateNode(1234) { it.copy(user = it.user.copy(long_name = "Test")) }
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
# `:core:database`
|
||||
|
||||
This module provides the local Room database persistence layer for the application.
|
||||
This module provides the local Room database persistence layer for the application using Room Kotlin Multiplatform (KMP).
|
||||
|
||||
## Key Components
|
||||
|
||||
- **`MeshtasticDatabase`**: The main Room database class.
|
||||
- **`MeshtasticDatabase`**: The main Room database class, defined in `commonMain`.
|
||||
- **DAOs (Data Access Objects)**:
|
||||
- `NodeInfoDao`: Manages storage and retrieval of node information (`NodeEntity`). Contains critical logic for handling Public Key Conflict (PKC) resolution and preventing identity wiping attacks.
|
||||
- `PacketDao`: Handles storage of mesh packets.
|
||||
- `ChatMessageDao`: Manages chat message history.
|
||||
- `PacketDao`: Handles storage of mesh packets, including text messages, waypoints, and reactions.
|
||||
- **Entities**:
|
||||
- `NodeEntity`: Represents a node on the mesh.
|
||||
- `PacketEntity`: Represents a stored packet.
|
||||
- `Packet`: Represents a stored packet.
|
||||
- `ReactionEntity`: Represents emoji reactions to packets.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
|
|
|
|||
|
|
@ -14,45 +14,61 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.android.library)
|
||||
alias(libs.plugins.meshtastic.kmp.library)
|
||||
alias(libs.plugins.meshtastic.android.room)
|
||||
alias(libs.plugins.meshtastic.hilt)
|
||||
alias(libs.plugins.meshtastic.kotlinx.serialization)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
}
|
||||
|
||||
configure<LibraryExtension> {
|
||||
namespace = "org.meshtastic.core.database"
|
||||
kotlin {
|
||||
android {
|
||||
namespace = "org.meshtastic.core.database"
|
||||
withHostTest { isIncludeAndroidResources = true }
|
||||
withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
// Adds exported schema location as test app assets.
|
||||
named("androidTest") { assets.directories.add("$projectDir/schemas") }
|
||||
commonMain.dependencies {
|
||||
implementation(libs.androidx.sqlite.bundled)
|
||||
implementation(projects.core.repository)
|
||||
api(projects.core.common)
|
||||
implementation(projects.core.di)
|
||||
api(projects.core.model)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.resources)
|
||||
implementation(libs.androidx.room.paging)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kermit)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.androidx.room.testing)
|
||||
}
|
||||
androidMain.dependencies { implementation(libs.javax.inject) }
|
||||
|
||||
val androidHostTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.androidx.room.testing)
|
||||
implementation(libs.androidx.test.core)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
implementation(libs.junit)
|
||||
implementation(libs.robolectric)
|
||||
}
|
||||
}
|
||||
val androidDeviceTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.androidx.room.testing)
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
implementation(libs.androidx.test.runner)
|
||||
}
|
||||
resources.srcDir("$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.repository)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.di)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.resources)
|
||||
|
||||
implementation(libs.androidx.room.paging)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kermit)
|
||||
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
testImplementation(libs.androidx.room.testing)
|
||||
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
androidTestImplementation(libs.androidx.room.testing)
|
||||
"kspAndroidHostTest"(libs.androidx.room.compiler)
|
||||
"kspAndroidDeviceTest"(libs.androidx.room.compiler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.database
|
||||
|
||||
import androidx.room.Room
|
||||
|
|
@ -24,6 +23,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.database.MeshtasticDatabase.Companion.configureCommon
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
|
@ -40,17 +40,23 @@ class MeshtasticDatabaseTest {
|
|||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrateAll() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
// Create earliest version of the database.
|
||||
helper.createDatabase(TEST_DB, 3).apply { close() }
|
||||
|
||||
// Open latest version of the database. Room validates the schema
|
||||
// once all migrations execute.
|
||||
Room.databaseBuilder(
|
||||
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||
MeshtasticDatabase::class.java,
|
||||
TEST_DB,
|
||||
Room.databaseBuilder<MeshtasticDatabase>(
|
||||
context = context,
|
||||
name = context.getDatabasePath(TEST_DB).absolutePath,
|
||||
factory = { MeshtasticDatabaseConstructor.initialize() },
|
||||
)
|
||||
.configureCommon()
|
||||
.build()
|
||||
.apply { openHelper.writableDatabase.close() }
|
||||
.apply {
|
||||
openHelper.writableDatabase
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import org.junit.Before
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.database.MeshtasticDatabase
|
||||
import org.meshtastic.core.database.MeshtasticDatabaseConstructor
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.model.Node
|
||||
|
|
@ -197,7 +198,12 @@ class NodeInfoDaoTest {
|
|||
@Before
|
||||
fun createDb(): Unit = runBlocking {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
database = Room.inMemoryDatabaseBuilder(context, MeshtasticDatabase::class.java).build()
|
||||
database =
|
||||
Room.inMemoryDatabaseBuilder<MeshtasticDatabase>(
|
||||
context = context,
|
||||
factory = { MeshtasticDatabaseConstructor.initialize() },
|
||||
)
|
||||
.build()
|
||||
nodeInfoDao = database.nodeInfoDao()
|
||||
|
||||
nodeInfoDao.apply {
|
||||
|
|
@ -33,6 +33,7 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.database.MeshtasticDatabase
|
||||
import org.meshtastic.core.database.MeshtasticDatabaseConstructor
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
|
|
@ -82,7 +83,12 @@ class PacketDaoTest {
|
|||
@Before
|
||||
fun createDb(): Unit = runBlocking {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
database = Room.inMemoryDatabaseBuilder(context, MeshtasticDatabase::class.java).build()
|
||||
database =
|
||||
Room.inMemoryDatabaseBuilder<MeshtasticDatabase>(
|
||||
context = context,
|
||||
factory = { MeshtasticDatabaseConstructor.initialize() },
|
||||
)
|
||||
.build()
|
||||
|
||||
nodeInfoDao = database.nodeInfoDao().apply { setMyNodeInfo(myNodeInfo) }
|
||||
|
||||
|
|
@ -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.database
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
package org.meshtastic.core.database.dao
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.ByteString.Companion.toByteString
|
||||
|
|
@ -29,13 +29,16 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.database.MeshtasticDatabase
|
||||
import org.meshtastic.core.database.MeshtasticDatabaseConstructor
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(sdk = [34])
|
||||
class MigrationTest {
|
||||
private lateinit var database: MeshtasticDatabase
|
||||
private lateinit var packetDao: PacketDao
|
||||
|
|
@ -57,8 +60,13 @@ class MigrationTest {
|
|||
|
||||
@Before
|
||||
fun createDb(): Unit = runBlocking {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
database = Room.inMemoryDatabaseBuilder(context, MeshtasticDatabase::class.java).build()
|
||||
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
|
||||
database =
|
||||
Room.inMemoryDatabaseBuilder<MeshtasticDatabase>(
|
||||
context = context,
|
||||
factory = { MeshtasticDatabaseConstructor.initialize() },
|
||||
)
|
||||
.build()
|
||||
nodeInfoDao = database.nodeInfoDao().apply { setMyNodeInfo(myNodeInfo) }
|
||||
packetDao = database.packetDao()
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -36,9 +35,9 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.database.MeshtasticDatabase.Companion.configureCommon
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import org.meshtastic.core.repository.DatabaseManager as SharedDatabaseManager
|
||||
|
|
@ -93,7 +92,7 @@ constructor(
|
|||
val dbName = buildDbName(address)
|
||||
|
||||
// Remember the previously active DB name (any) so we can record its last-used time as well.
|
||||
val previousDbName = _currentDb.value?.openHelper?.databaseName
|
||||
val previousDbName = _currentDb.value?.let { buildDbName(_currentAddress.value) }
|
||||
|
||||
// Fast path: no-op if already on this address
|
||||
if (_currentAddress.value == address && _currentDb.value != null) {
|
||||
|
|
@ -126,9 +125,9 @@ constructor(
|
|||
|
||||
/** Execute [block] with the current DB instance. */
|
||||
suspend fun <T> withDb(block: suspend (MeshtasticDatabase) -> T): T? = withContext(limitedIo) {
|
||||
val active = _currentDb.value?.openHelper?.databaseName ?: return@withContext null
|
||||
val db = _currentDb.value ?: return@withContext null
|
||||
val active = buildDbName(_currentAddress.value)
|
||||
markLastUsed(active)
|
||||
val db = _currentDb.value ?: return@withContext null // Use the cached current DB
|
||||
block(db)
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +199,7 @@ constructor(
|
|||
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()
|
||||
val active = _currentDb.value?.let { buildDbName(_currentAddress.value) } ?: defaultDbName()
|
||||
managerScope.launch(dispatchers.io) { enforceCacheLimit(activeDbName = active) }
|
||||
}
|
||||
|
||||
|
|
@ -235,113 +234,18 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const val LEGACY_DB_CLEANED_KEY: String = "legacy_db_cleaned"
|
||||
|
||||
// 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)
|
||||
// File-private helpers
|
||||
private fun defaultDbName(): String = DatabaseConstants.DEFAULT_DB_NAME
|
||||
|
||||
private fun normalizeAddress(addr: String?): String {
|
||||
val u = addr?.trim()?.uppercase()
|
||||
val normalized =
|
||||
when {
|
||||
u.isNullOrBlank() -> "DEFAULT"
|
||||
u == "N" || u == "NULL" -> "DEFAULT"
|
||||
else -> u.replace(":", "")
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
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)
|
||||
.setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)
|
||||
.fallbackToDestructiveMigration(false)
|
||||
Room.databaseBuilder<MeshtasticDatabase>(
|
||||
context = app.applicationContext,
|
||||
name = app.getDatabasePath(dbName).absolutePath,
|
||||
factory = { MeshtasticDatabaseConstructor.initialize() },
|
||||
)
|
||||
.configureCommon()
|
||||
.build()
|
||||
|
||||
private fun getDbFile(app: Application, dbName: String): File? = app.getDatabasePath(dbName).takeIf { it.exists() }
|
||||
|
||||
/**
|
||||
* Compute which DBs to evict using LRU policy.
|
||||
*
|
||||
* Rules:
|
||||
* - Only consider device-specific DBs (exclude legacy and default)
|
||||
* - Never evict the active DB
|
||||
* - If number of device DBs is within the limit, evict none
|
||||
* - Otherwise evict the (size - limit) least-recently-used DBs
|
||||
*
|
||||
* Pass a precomputed [lastUsedMsByDb] snapshot to avoid redundant IO/lookups.
|
||||
*/
|
||||
internal fun selectEvictionVictims(
|
||||
dbNames: List<String>,
|
||||
activeDbName: String,
|
||||
limit: Int,
|
||||
lastUsedMsByDb: Map<String, Long>,
|
||||
): List<String> {
|
||||
val deviceDbNames =
|
||||
dbNames.filterNot { it == DatabaseConstants.LEGACY_DB_NAME || it == DatabaseConstants.DEFAULT_DB_NAME }
|
||||
val victims =
|
||||
if (limit < 1 || deviceDbNames.size <= limit) {
|
||||
emptyList()
|
||||
} else {
|
||||
val candidates = deviceDbNames.filter { it != activeDbName }
|
||||
if (candidates.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
val toEvict = deviceDbNames.size - limit
|
||||
candidates.sortedBy { lastUsedMsByDb[it] ?: 0L }.take(toEvict)
|
||||
}
|
||||
}
|
||||
return victims
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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.database
|
||||
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
|
||||
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
|
||||
|
||||
const val LEGACY_DB_CLEANED_KEY: String = "legacy_db_cleaned"
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
fun normalizeAddress(addr: String?): String {
|
||||
val u = addr?.trim()?.uppercase()
|
||||
val normalized =
|
||||
when {
|
||||
u.isNullOrBlank() -> "DEFAULT"
|
||||
u == "N" || u == "NULL" -> "DEFAULT"
|
||||
else -> u.replace(":", "")
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
fun shortSha1(s: String): String = s.encodeUtf8().sha1().hex().take(DatabaseConstants.DB_NAME_HASH_LEN)
|
||||
|
||||
fun buildDbName(address: String?): String = if (address.isNullOrBlank()) {
|
||||
DatabaseConstants.DEFAULT_DB_NAME
|
||||
} else {
|
||||
"${DatabaseConstants.DB_PREFIX}_${shortSha1(normalizeAddress(address))}"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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,
|
||||
) + "…"
|
||||
}
|
||||
|
||||
/** Compute which DBs to evict using LRU policy. */
|
||||
internal fun selectEvictionVictims(
|
||||
dbNames: List<String>,
|
||||
activeDbName: String,
|
||||
limit: Int,
|
||||
lastUsedMsByDb: Map<String, Long>,
|
||||
): List<String> {
|
||||
val deviceDbNames =
|
||||
dbNames.filterNot { it == DatabaseConstants.LEGACY_DB_NAME || it == DatabaseConstants.DEFAULT_DB_NAME }
|
||||
val victims =
|
||||
if (limit < 1 || deviceDbNames.size <= limit) {
|
||||
emptyList()
|
||||
} else {
|
||||
val candidates = deviceDbNames.filter { it != activeDbName }
|
||||
if (candidates.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
val toEvict = deviceDbNames.size - limit
|
||||
candidates.sortedBy { lastUsedMsByDb[it] ?: 0L }.take(toEvict)
|
||||
}
|
||||
}
|
||||
return victims
|
||||
}
|
||||
|
|
@ -16,15 +16,15 @@
|
|||
*/
|
||||
package org.meshtastic.core.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.meshtastic.core.database.dao.DeviceHardwareDao
|
||||
import org.meshtastic.core.database.dao.FirmwareReleaseDao
|
||||
import org.meshtastic.core.database.dao.MeshLogDao
|
||||
|
|
@ -99,6 +99,7 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
|
|||
version = 37,
|
||||
exportSchema = true,
|
||||
)
|
||||
@androidx.room.ConstructedBy(MeshtasticDatabaseConstructor::class)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class MeshtasticDatabase : RoomDatabase() {
|
||||
abstract fun nodeInfoDao(): NodeInfoDao
|
||||
|
|
@ -116,11 +117,11 @@ abstract class MeshtasticDatabase : RoomDatabase() {
|
|||
abstract fun tracerouteNodePositionDao(): TracerouteNodePositionDao
|
||||
|
||||
companion object {
|
||||
fun getDatabase(context: Context): MeshtasticDatabase =
|
||||
Room.databaseBuilder(context.applicationContext, MeshtasticDatabase::class.java, "meshtastic_database")
|
||||
.setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)
|
||||
.fallbackToDestructiveMigration(false)
|
||||
.build()
|
||||
/** Configures a [RoomDatabase.Builder] with standard settings for this project. */
|
||||
fun <T : RoomDatabase> RoomDatabase.Builder<T>.configureCommon(): RoomDatabase.Builder<T> =
|
||||
this.fallbackToDestructiveMigration(dropAllTables = false)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.setQueryCoroutineContext(Dispatchers.IO)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.database
|
||||
|
||||
import androidx.room.RoomDatabaseConstructor
|
||||
|
||||
@Suppress("NO_ACTUAL_FOR_EXPECT", "KotlinNoActualForExpect")
|
||||
expect object MeshtasticDatabaseConstructor : RoomDatabaseConstructor<MeshtasticDatabase> {
|
||||
override fun initialize(): MeshtasticDatabase
|
||||
}
|
||||
|
|
@ -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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
|
|
@ -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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
|
|
@ -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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.database.di
|
||||
|
||||
import android.app.Application
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.MeshtasticDatabase
|
||||
import org.meshtastic.core.database.dao.DeviceHardwareDao
|
||||
import org.meshtastic.core.database.dao.FirmwareReleaseDao
|
||||
import org.meshtastic.core.database.dao.MeshLogDao
|
||||
import org.meshtastic.core.database.dao.NodeInfoDao
|
||||
import org.meshtastic.core.database.dao.PacketDao
|
||||
import org.meshtastic.core.database.dao.QuickChatActionDao
|
||||
import org.meshtastic.core.database.dao.TracerouteNodePositionDao
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
abstract class DatabaseModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindDatabaseManager(impl: DatabaseManager): org.meshtastic.core.repository.DatabaseManager
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(app: Application): MeshtasticDatabase = MeshtasticDatabase.getDatabase(app)
|
||||
|
||||
@Provides fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao = database.nodeInfoDao()
|
||||
|
||||
@Provides fun providePacketDao(database: MeshtasticDatabase): PacketDao = database.packetDao()
|
||||
|
||||
@Provides fun provideMeshLogDao(database: MeshtasticDatabase): MeshLogDao = database.meshLogDao()
|
||||
|
||||
@Provides
|
||||
fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao = database.quickChatActionDao()
|
||||
|
||||
@Provides
|
||||
fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao = database.deviceHardwareDao()
|
||||
|
||||
@Provides
|
||||
fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao = database.firmwareReleaseDao()
|
||||
|
||||
@Provides
|
||||
fun provideTracerouteNodePositionDao(database: MeshtasticDatabase): TracerouteNodePositionDao =
|
||||
database.tracerouteNodePositionDao()
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ constructor(
|
|||
|
||||
val applicationId = buildConfigProvider.applicationId
|
||||
|
||||
private val ourNodeNumFlow = nodeRepository.nodeDBbyNum.map { it.keys.firstOrNull() }.distinctUntilChanged()
|
||||
private val ourNodeNumFlow = nodeRepository.myNodeInfo.map { it?.myNodeNum }.distinctUntilChanged()
|
||||
|
||||
val positionLogs: StateFlow<List<Position>> =
|
||||
ourNodeNumFlow
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref =
|
|||
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "room" }
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
|
||||
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version = "2.5.0-alpha13" }
|
||||
androidx-savedstate-compose = { module = "androidx.savedstate:savedstate-compose", version.ref = "savedstate" }
|
||||
androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "savedstate" }
|
||||
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version = "2.11.1" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue