diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 41a0c8a3dd..a902d1cc1b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -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 { diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 90a438478e..1279e4a5c0 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -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) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt index 852c56e04e..a73a658990 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/DeviceHardwareLocalDataSource.kt @@ -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, + 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) = withContext(dispatchers.io) { deviceHardwareDao.insertAll(deviceHardware.map { it.asEntity() }) } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt index dff3b0171e..3f1a05c7fe 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/datasource/FirmwareReleaseLocalDataSource.kt @@ -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, + 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, diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/DatabaseModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/DatabaseModule.kt new file mode 100644 index 0000000000..c21be19202 --- /dev/null +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/DatabaseModule.kt @@ -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 . + */ +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 +} diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt index 0adf6a80ee..15b3e8b908 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt @@ -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) } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index e84af354c7..98b492c6a7 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -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) } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index e9172809b0..6f9c615d55 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -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) } } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt index a6af8c51e8..0b08c806f3 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt @@ -260,6 +260,8 @@ constructor( num = num, user = user, position = position, + latitude = latitude, + longitude = longitude, snr = snr, rssi = rssi, lastHeard = lastHeard, diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt index 4748663ba3..b9eca56ded 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt @@ -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")) } diff --git a/core/database/README.md b/core/database/README.md index 3ee07f244c..850d3ecf54 100644 --- a/core/database/README.md +++ b/core/database/README.md @@ -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 diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index cb85f50174..e97a8d3ed2 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -14,45 +14,61 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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 { - 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) } diff --git a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/DatabaseManagerLegacyCleanupTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/DatabaseManagerLegacyCleanupTest.kt similarity index 100% rename from core/database/src/androidTest/kotlin/org/meshtastic/core/database/DatabaseManagerLegacyCleanupTest.kt rename to core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/DatabaseManagerLegacyCleanupTest.kt diff --git a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt similarity index 74% rename from core/database/src/androidTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt rename to core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt index c1eb8f8403..2e7c783c3d 100644 --- a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt +++ b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt @@ -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 . */ - 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( + context = context, + name = context.getDatabasePath(TEST_DB).absolutePath, + factory = { MeshtasticDatabaseConstructor.initialize() }, ) + .configureCommon() .build() - .apply { openHelper.writableDatabase.close() } + .apply { + openHelper.writableDatabase + close() + } } } diff --git a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt similarity index 98% rename from core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt rename to core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt index e59e01c37e..2777135edf 100644 --- a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt +++ b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt @@ -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( + context = context, + factory = { MeshtasticDatabaseConstructor.initialize() }, + ) + .build() nodeInfoDao = database.nodeInfoDao() nodeInfoDao.apply { diff --git a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt similarity index 98% rename from core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt rename to core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt index 30a980d0fd..71bd06e242 100644 --- a/core/database/src/androidTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt +++ b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt @@ -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( + context = context, + factory = { MeshtasticDatabaseConstructor.initialize() }, + ) + .build() nodeInfoDao = database.nodeInfoDao().apply { setMyNodeInfo(myNodeInfo) } diff --git a/core/database/src/test/kotlin/org/meshtastic/core/database/DatabaseManagerEvictionTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/DatabaseManagerEvictionTest.kt similarity index 98% rename from core/database/src/test/kotlin/org/meshtastic/core/database/DatabaseManagerEvictionTest.kt rename to core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/DatabaseManagerEvictionTest.kt index 872b3021ab..7fb7fb8623 100644 --- a/core/database/src/test/kotlin/org/meshtastic/core/database/DatabaseManagerEvictionTest.kt +++ b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/DatabaseManagerEvictionTest.kt @@ -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 . */ - package org.meshtastic.core.database import org.junit.Assert.assertEquals diff --git a/core/database/src/test/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt similarity index 92% rename from core/database/src/test/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt rename to core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt index 11ee9ba4ad..507490c34c 100644 --- a/core/database/src/test/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt +++ b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt @@ -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() + database = + Room.inMemoryDatabaseBuilder( + context = context, + factory = { MeshtasticDatabaseConstructor.initialize() }, + ) + .build() nodeInfoDao = database.nodeInfoDao().apply { setMyNodeInfo(myNodeInfo) } packetDao = database.packetDao() } diff --git a/core/database/src/test/kotlin/org/meshtastic/core/database/model/NodeTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/model/NodeTest.kt similarity index 100% rename from core/database/src/test/kotlin/org/meshtastic/core/database/model/NodeTest.kt rename to core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/model/NodeTest.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt similarity index 71% rename from core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt rename to core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt index e935a88e21..1a6181f92a 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -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 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( + 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, - activeDbName: String, - limit: Int, - lastUsedMsByDb: Map, -): List { - 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 -} diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/Converters.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/Converters.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseConstants.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseConstants.kt new file mode 100644 index 0000000000..c917ee066a --- /dev/null +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseConstants.kt @@ -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 . + */ +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, + activeDbName: String, + limit: Int, + lastUsedMsByDb: Map, +): List { + 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 +} diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt similarity index 90% rename from core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt index de950c15a2..95a43db007 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt @@ -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 RoomDatabase.Builder.configureCommon(): RoomDatabase.Builder = + this.fallbackToDestructiveMigration(dropAllTables = false) + .setDriver(BundledSQLiteDriver()) + .setQueryCoroutineContext(Dispatchers.IO) } } diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabaseConstructor.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabaseConstructor.kt new file mode 100644 index 0000000000..997fa9cc37 --- /dev/null +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabaseConstructor.kt @@ -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 . + */ +package org.meshtastic.core.database + +import androidx.room.RoomDatabaseConstructor + +@Suppress("NO_ACTUAL_FOR_EXPECT", "KotlinNoActualForExpect") +expect object MeshtasticDatabaseConstructor : RoomDatabaseConstructor { + override fun initialize(): MeshtasticDatabase +} diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt similarity index 97% rename from core/database/src/main/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt index ee8b15adcd..dfaa30eea5 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt @@ -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 . */ - package org.meshtastic.core.database.dao import androidx.room.Dao diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/PacketDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/dao/PacketDao.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt similarity index 97% rename from core/database/src/main/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt index 5d3ebe0160..863a42440d 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt @@ -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 . */ - package org.meshtastic.core.database.dao import androidx.room.Dao diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/MeshLog.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/MeshLog.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt similarity index 96% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt index ef2e9e8a61..fbcaba95dd 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt @@ -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 . */ - package org.meshtastic.core.database.entity import androidx.room.ColumnInfo diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt similarity index 100% rename from core/database/src/main/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt rename to core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt deleted file mode 100644 index 8a722aa6c9..0000000000 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/di/DatabaseModule.kt +++ /dev/null @@ -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 . - */ -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() - } -} diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt index 7a971417fa..535c872270 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt @@ -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> = ourNodeNumFlow diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 86446fc332..c686cf872f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }