chore: Upgrade to Room 3.0 and refactor related components

This commit is contained in:
James Rich 2026-03-20 15:25:32 -05:00
parent 38591f4c0b
commit 785b698542
31 changed files with 121 additions and 415 deletions

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import androidx.room.gradle.RoomExtension
import androidx.room3.gradle.RoomExtension
import com.google.devtools.ksp.gradle.KspExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -30,7 +30,7 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "androidx.room")
apply(plugin = "androidx.room3")
apply(plugin = "com.google.devtools.ksp")
extensions.configure<KspExtension> {
@ -55,7 +55,7 @@ class AndroidRoomConventionPlugin : Plugin<Project> {
}
}
dependencies {
"kspAndroid"(roomCompiler)
add("kspAndroid", roomCompiler)
}
}

View file

@ -6,3 +6,9 @@ This file tracks all major tracks for the project. Each track has its own detail
- [x] **Track: Migrate to room3, prepare to support all targets (Android, Desktop, iOS) with bundled SQLite driver and full idiomatic migration.**
*Link: [./tracks/migrate_room3_20260320/](./tracks/migrate_room3_20260320/)*
- [x] **Track: Extract DatabaseManager to KMP**
*Link: [./tracks/extract_database_manager_kmp_20260320/](./tracks/extract_database_manager_kmp_20260320/)*
- [ ] **Track: Extract RadioInterfaceService to KMP**
*Link: [./tracks/extract_radio_interface_kmp_20260320/](./tracks/extract_radio_interface_kmp_20260320/)*

View file

@ -4,33 +4,33 @@
- Update `libs.versions.toml` to Room 3.0.
- Update `AndroidRoomConventionPlugin.kt` to align with Room 3 best practices (e.g., ensuring `room.generateKotlin` is correctly set and using the `androidx.room` Gradle plugin).
- Verify all modules (`core:database`, `core:data`, `app`, etc.) can build with the new dependencies.
- [ ] Task: Update `libs.versions.toml` with Room 3.0 and related dependencies.
- [ ] Task: Refactor `AndroidRoomConventionPlugin.kt` for Room 3.0.
- [ ] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md)
- [x] Task: Update `libs.versions.toml` with Room 3.0 and related dependencies.
- [x] Task: Refactor `AndroidRoomConventionPlugin.kt` for Room 3.0.
- [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md)
## Phase 2: Core Database Implementation (KMP)
- Refactor `MeshtasticDatabase.kt` and `MeshtasticDatabaseConstructor.kt` to use the new Room 3 `RoomDatabase.Builder` for KMP.
- Configure the `BundledSQLiteDriver` in `commonMain` to ensure consistent SQL behavior across all targets.
- Ensure that DAOs and Entities are using `room-runtime` in `commonMain` correctly.
- Implement platform-specific database setup for Android, Desktop, and iOS in their respective `Main` source sets.
- [ ] Task: Refactor `MeshtasticDatabase.kt` for Room 3.0 KMP APIs.
- [ ] Task: Configure `BundledSQLiteDriver` in `DatabaseProvider.kt`.
- [ ] Task: Implement platform-specific database path logic for Desktop and iOS.
- [ ] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md)
- [x] Task: Refactor `MeshtasticDatabase.kt` for Room 3.0 KMP APIs.
- [x] Task: Configure `BundledSQLiteDriver` in `DatabaseProvider.kt`.
- [x] Task: Implement platform-specific database path logic for Desktop and iOS.
- [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md)
## Phase 3: Multi-target Support (iOS)
- Add iOS targets (`iosX64`, `iosArm64`, `iosSimulatorArm64`) to `core:database/build.gradle.kts`.
- Configure the database file path logic for iOS.
- Verify that the `core:database` module compiles for iOS.
- [ ] Task: Add iOS targets to `core:database/build.gradle.kts`.
- [ ] Task: Verify iOS compilation.
- [ ] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md)
- [x] Task: Add iOS targets to `core:database/build.gradle.kts`.
- [x] Task: Verify iOS compilation (Skipped: Linux host).
- [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md)
## Phase 4: Verification and Testing
- Update existing database tests in `commonTest`, `androidHostTest`, and `androidDeviceTest` to Room 3.
- Run tests on Android and Desktop to ensure no regressions in behavior.
- Perform manual verification on Android and Desktop apps to ensure the database initializes and functions correctly.
- [ ] Task: Update and run DAO unit tests in `commonTest`.
- [ ] Task: Run Android instrumented tests (`androidDeviceTest`).
- [ ] Task: Manual verification on Desktop.
- [ ] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md)
- [x] Task: Update and run DAO unit tests in `commonTest`.
- [x] Task: Run Android instrumented tests (`androidDeviceTest`).
- [x] Task: Manual verification on Desktop.
- [x] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md)

View file

@ -35,6 +35,8 @@ kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.androidx.sqlite.bundled)
implementation(libs.androidx.datastore.preferences)
implementation(libs.okio)
api(projects.core.common)
implementation(projects.core.di)

View file

@ -16,8 +16,8 @@
*/
package org.meshtastic.core.database
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.room3.Room
import androidx.room3.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Room
import androidx.room3.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.flow.first

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Room
import androidx.room3.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.flow.first

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Room
import androidx.room3.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first

View file

@ -1,247 +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
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
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 org.meshtastic.core.common.database.DatabaseManager as SharedDatabaseManager
/** Manages per-device Room database instances for node data, with LRU eviction. */
@Single(binds = [DatabaseProvider::class, SharedDatabaseManager::class])
@Suppress("TooManyFunctions")
@OptIn(ExperimentalCoroutinesApi::class)
open class DatabaseManager(private val app: Application, private val dispatchers: CoroutineDispatchers) :
DatabaseProvider,
SharedDatabaseManager {
val prefs: SharedPreferences = app.getSharedPreferences("db-manager-prefs", Context.MODE_PRIVATE)
private val managerScope = CoroutineScope(SupervisorJob() + dispatchers.default)
private val mutex = Mutex()
// Expose the DB cache limit as a reactive stream so UI can observe changes.
private val _cacheLimit = MutableStateFlow(getCurrentCacheLimit())
override val cacheLimit: StateFlow<Int> = _cacheLimit
// Keep cache-limit StateFlow in sync if some other component updates SharedPreferences.
private val prefsListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == DatabaseConstants.CACHE_LIMIT_KEY) {
_cacheLimit.value = getCurrentCacheLimit()
}
}
init {
prefs.registerOnSharedPreferenceChangeListener(prefsListener)
}
private val _currentDb = MutableStateFlow<MeshtasticDatabase?>(null)
override val currentDb: StateFlow<MeshtasticDatabase> =
_currentDb.filterNotNull().stateIn(managerScope, SharingStarted.Eagerly, buildRoomDb(app, defaultDbName()))
private val _currentAddress = MutableStateFlow<String?>(null)
val currentAddress: StateFlow<String?> = _currentAddress
private val dbCache = mutableMapOf<String, MeshtasticDatabase>() // key = dbName
/** Initialize the active database for [address]. */
suspend fun init(address: String?) {
switchActiveDatabase(address)
}
/** Switch active database to the one associated with [address]. Serialized via mutex. */
override suspend fun switchActiveDatabase(address: String?) = mutex.withLock {
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?.let { buildDbName(_currentAddress.value) }
// Fast path: no-op if already on this address
if (_currentAddress.value == address && _currentDb.value != null) {
markLastUsed(dbName)
return@withLock
}
// Build/open Room DB off the main thread
val db =
dbCache[dbName]
?: withContext(dispatchers.io) { buildRoomDb(app, dbName) }.also { dbCache[dbName] = it }
_currentDb.value = db
_currentAddress.value = address
markLastUsed(dbName)
// Also mark the previous DB as used "just now" so LRU has an accurate, recent timestamp
// even on first run after upgrade where no timestamp might exist yet.
previousDbName?.let { markLastUsed(it) }
// Defer LRU eviction so switch is not blocked by filesystem work
managerScope.launch(dispatchers.io) { enforceCacheLimit(activeDbName = dbName) }
// One-time cleanup: remove legacy DB if present and not active
managerScope.launch(dispatchers.io) { cleanupLegacyDbIfNeeded(activeDbName = dbName) }
Logger.i { "Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)}" }
}
private val limitedIo = dispatchers.io.limitedParallelism(4)
/** Execute [block] with the current DB instance. */
override suspend fun <T> withDb(block: suspend (MeshtasticDatabase) -> T): T? = withContext(limitedIo) {
val db = _currentDb.value ?: return@withContext null
val active = buildDbName(_currentAddress.value)
markLastUsed(active)
block(db)
}
/** Returns true if a database exists for the given device address. */
override fun hasDatabaseFor(address: String?): Boolean {
if (address.isNullOrBlank() || address == "n") return false
val dbName = buildDbName(address)
return getDbFile(app, dbName) != null
}
private fun markLastUsed(dbName: String) {
prefs.edit().putLong(lastUsedKey(dbName), nowMillis).apply()
}
private fun lastUsed(dbName: String): Long {
val k = lastUsedKey(dbName)
val v = prefs.getLong(k, 0L)
return if (v == 0L) getDbFile(app, dbName)?.lastModified() ?: 0L else v
}
private fun listExistingDbNames(): List<String> {
val base = app.getDatabasePath(DatabaseConstants.LEGACY_DB_NAME)
val dir = base.parentFile ?: return emptyList()
val names = dir.listFiles()?.mapNotNull { f -> f.name } ?: emptyList()
return names
.filter { it.startsWith(DatabaseConstants.DB_PREFIX) }
.filterNot { it.endsWith("-wal") || it.endsWith("-shm") }
.distinct()
}
private suspend fun enforceCacheLimit(activeDbName: String) = mutex.withLock {
val limit = getCurrentCacheLimit()
val all = listExistingDbNames()
// Only enforce the limit over device-specific DBs; exclude legacy and default DBs
val deviceDbs =
all.filterNot { it == DatabaseConstants.LEGACY_DB_NAME || it == DatabaseConstants.DEFAULT_DB_NAME }
Logger.d {
"LRU check: limit=$limit, active=${anonymizeDbName(
activeDbName,
)}, deviceDbs=${deviceDbs.joinToString(", ") {
anonymizeDbName(it)
}}"
}
if (deviceDbs.size <= limit) return@withLock
val usageSnapshot = deviceDbs.associateWith { lastUsed(it) }
Logger.d {
"LRU lastUsed(ms): ${usageSnapshot.entries.joinToString(", ") { (name, ts) ->
"${anonymizeDbName(name)}=$ts"
}}"
}
val victims = selectEvictionVictims(deviceDbs, activeDbName, limit, usageSnapshot)
Logger.i { "LRU victims: ${victims.joinToString(", ") { anonymizeDbName(it) }}" }
victims.forEach { name ->
runCatching { dbCache.remove(name)?.close() }
.onFailure { Logger.w(it) { "Failed to close database $name" } }
app.deleteDatabase(name)
prefs.edit().remove(lastUsedKey(name)).apply()
Logger.i { "Evicted cached DB ${anonymizeDbName(name)}" }
}
}
override fun getCurrentCacheLimit(): Int = prefs
.getInt(DatabaseConstants.CACHE_LIMIT_KEY, DatabaseConstants.DEFAULT_CACHE_LIMIT)
.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
override fun setCacheLimit(limit: Int) {
val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
if (clamped == getCurrentCacheLimit()) return
prefs.edit().putInt(DatabaseConstants.CACHE_LIMIT_KEY, clamped).apply()
_cacheLimit.value = clamped
// Enforce asynchronously with current active DB protected
val active = _currentDb.value?.let { buildDbName(_currentAddress.value) } ?: defaultDbName()
managerScope.launch(dispatchers.io) { enforceCacheLimit(activeDbName = active) }
}
private suspend fun cleanupLegacyDbIfNeeded(activeDbName: String) = mutex.withLock {
if (prefs.getBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, false)) return@withLock
val legacy = DatabaseConstants.LEGACY_DB_NAME
if (legacy == activeDbName) {
// Never delete the active DB; mark as cleaned to avoid repeated checks
prefs.edit().putBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, true).apply()
return@withLock
}
val legacyFile = getDbFile(app, legacy)
if (legacyFile != null) {
runCatching { dbCache.remove(legacy)?.close() }
.onFailure { Logger.w(it) { "Failed to close legacy database $legacy before deletion" } }
val deleted = app.deleteDatabase(legacy)
if (deleted) {
Logger.i { "Deleted legacy DB ${anonymizeDbName(legacy)}" }
} else {
Logger.w { "Attempted to delete legacy DB $legacy but deleteDatabase returned false" }
}
}
prefs.edit().putBoolean(DatabaseConstants.LEGACY_DB_CLEANED_KEY, true).apply()
}
/** Closes all open databases and cancels background work. */
fun close() {
managerScope.cancel()
dbCache.values.forEach { it.close() }
dbCache.clear()
_currentDb.value = null
}
}
// File-private helpers
private fun defaultDbName(): String = DatabaseConstants.DEFAULT_DB_NAME
private fun lastUsedKey(dbName: String) = "db_last_used:$dbName"
private fun buildRoomDb(app: Application, dbName: String): MeshtasticDatabase =
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() }

View file

@ -16,9 +16,7 @@
*/
package org.meshtastic.core.database.di
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@Module
@ComponentScan("org.meshtastic.core.database")
class CoreDatabaseAndroidModule

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.core.database
import androidx.room.TypeConverter
import androidx.room3.TypeConverter
import co.touchlab.kermit.Logger
import kotlinx.serialization.json.Json
import okio.ByteString

View file

@ -16,13 +16,13 @@
*/
package org.meshtastic.core.database
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteColumn
import androidx.room.DeleteTable
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.AutoMigrationSpec
import androidx.room3.AutoMigration
import androidx.room3.Database
import androidx.room3.DeleteColumn
import androidx.room3.DeleteTable
import androidx.room3.RoomDatabase
import androidx.room3.TypeConverters
import androidx.room3.migration.AutoMigrationSpec
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import kotlinx.coroutines.Dispatchers
import org.meshtastic.core.database.dao.DeviceHardwareDao
@ -99,8 +99,9 @@ import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
version = 37,
exportSchema = true,
)
@androidx.room.ConstructedBy(MeshtasticDatabaseConstructor::class)
@androidx.room3.ConstructedBy(MeshtasticDatabaseConstructor::class)
@TypeConverters(Converters::class)
@androidx.room3.DaoReturnTypeConverters(androidx.room3.paging.PagingSourceDaoReturnTypeConverter::class)
abstract class MeshtasticDatabase : RoomDatabase() {
abstract fun nodeInfoDao(): NodeInfoDao

View file

@ -16,7 +16,7 @@
*/
package org.meshtastic.core.database
import androidx.room.RoomDatabaseConstructor
import androidx.room3.RoomDatabaseConstructor
@Suppress("NO_ACTUAL_FOR_EXPECT", "KotlinNoActualForExpect")
expect object MeshtasticDatabaseConstructor : RoomDatabaseConstructor<MeshtasticDatabase> {

View file

@ -16,10 +16,10 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.OnConflictStrategy
import androidx.room3.Query
import org.meshtastic.core.database.entity.DeviceHardwareEntity
@Dao

View file

@ -16,10 +16,10 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.OnConflictStrategy
import androidx.room3.Query
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.FirmwareReleaseType

View file

@ -16,9 +16,9 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.Query
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.database.entity.MeshLog

View file

@ -16,13 +16,13 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.MapColumn
import androidx.room3.OnConflictStrategy
import androidx.room3.Query
import androidx.room3.Transaction
import androidx.room3.Upsert
import kotlinx.coroutines.flow.Flow
import okio.ByteString
import org.meshtastic.core.database.entity.MetadataEntity

View file

@ -17,12 +17,12 @@
package org.meshtastic.core.database.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.Upsert
import androidx.room3.Dao
import androidx.room3.MapColumn
import androidx.room3.Query
import androidx.room3.Transaction
import androidx.room3.Update
import androidx.room3.Upsert
import kotlinx.coroutines.flow.Flow
import okio.ByteString
import org.meshtastic.core.common.util.nowMillis

View file

@ -16,10 +16,10 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import androidx.room3.Dao
import androidx.room3.Query
import androidx.room3.Transaction
import androidx.room3.Upsert
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.database.entity.QuickChatAction

View file

@ -16,10 +16,10 @@
*/
package org.meshtastic.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room3.Dao
import androidx.room3.Insert
import androidx.room3.OnConflictStrategy
import androidx.room3.Query
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.database.entity.TracerouteNodePositionEntity

View file

@ -18,7 +18,14 @@ package org.meshtastic.core.database.di
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.core.database.createDatabaseDataStore
@Module
@ComponentScan("org.meshtastic.core.database")
class CoreDatabaseModule
class CoreDatabaseModule {
@Single
@Named("DatabaseDataStore")
fun provideDatabaseDataStore() = createDatabaseDataStore("db-manager-prefs")
}

View file

@ -16,9 +16,9 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room3.ColumnInfo
import androidx.room3.Entity
import androidx.room3.PrimaryKey
import kotlinx.serialization.Serializable
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.DeviceHardware

View file

@ -16,9 +16,9 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room3.ColumnInfo
import androidx.room3.Entity
import androidx.room3.PrimaryKey
import kotlinx.serialization.Serializable
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.DeviceVersion

View file

@ -16,10 +16,10 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room3.ColumnInfo
import androidx.room3.Entity
import androidx.room3.Index
import androidx.room3.PrimaryKey
import co.touchlab.kermit.Logger
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.proto.FromRadio

View file

@ -16,8 +16,8 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room3.Entity
import androidx.room3.PrimaryKey
import org.meshtastic.core.model.MyNodeInfo
@Entity(tableName = "my_node")

View file

@ -16,12 +16,12 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.Relation
import androidx.room3.ColumnInfo
import androidx.room3.Embedded
import androidx.room3.Entity
import androidx.room3.Index
import androidx.room3.PrimaryKey
import androidx.room3.Relation
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.meshtastic.core.common.util.nowMillis

View file

@ -16,12 +16,12 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.Relation
import androidx.room3.ColumnInfo
import androidx.room3.Embedded
import androidx.room3.Entity
import androidx.room3.Index
import androidx.room3.PrimaryKey
import androidx.room3.Relation
import okio.ByteString
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.DataPacket

View file

@ -16,9 +16,9 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room3.ColumnInfo
import androidx.room3.Entity
import androidx.room3.PrimaryKey
@Entity(tableName = "quick_chat")
data class QuickChatAction(

View file

@ -16,10 +16,10 @@
*/
package org.meshtastic.core.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room3.ColumnInfo
import androidx.room3.Entity
import androidx.room3.ForeignKey
import androidx.room3.Index
import org.meshtastic.proto.Position
@Entity(

View file

@ -26,22 +26,14 @@ import androidx.datastore.preferences.core.emptyPreferences
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.room.Room
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import okio.FileSystem
import okio.Path.Companion.toPath
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.MeshtasticDatabase
import org.meshtastic.core.database.MeshtasticDatabase.Companion.configureCommon
import org.meshtastic.core.database.MeshtasticDatabaseConstructor
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.LocalStatsSerializer
@ -50,6 +42,7 @@ import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
import java.io.File
/**
* Resolves the desktop data directory for persistent storage (DataStore files, Room database). Defaults to
@ -72,52 +65,6 @@ private fun createPreferencesDataStore(name: String, scope: CoroutineScope): Dat
)
}
/**
* Desktop Room KMP database provider. Builds a single file-backed SQLite database using [MeshtasticDatabaseConstructor]
* and [BundledSQLiteDriver] (both KMP-ready).
*/
class DesktopDatabaseManager :
DatabaseProvider,
DatabaseManager {
private val dir = desktopDataDir()
private val dbName = "$dir/meshtastic.db"
private val db: MeshtasticDatabase by lazy {
FileSystem.SYSTEM.createDirectories(dir.toPath())
Room.databaseBuilder<MeshtasticDatabase>(name = dbName) { MeshtasticDatabaseConstructor.initialize() }
.configureCommon()
.build()
}
override val currentDb: StateFlow<MeshtasticDatabase> by lazy { MutableStateFlow(db) }
override suspend fun <T> withDb(block: suspend (MeshtasticDatabase) -> T): T? = block(db)
private val _cacheLimit = MutableStateFlow(DEFAULT_CACHE_LIMIT)
override val cacheLimit: StateFlow<Int> = _cacheLimit
override fun getCurrentCacheLimit(): Int = _cacheLimit.value
override fun setCacheLimit(limit: Int) {
_cacheLimit.value = limit.coerceIn(MIN_LIMIT, MAX_LIMIT)
}
override suspend fun switchActiveDatabase(address: String?) {
// Desktop uses a single database — no per-device switching
}
override fun hasDatabaseFor(address: String?): Boolean {
// Desktop always has the single database available
return !address.isNullOrBlank() && address != "n"
}
companion object {
private const val DEFAULT_CACHE_LIMIT = 100
private const val MIN_LIMIT = 1
private const val MAX_LIMIT = 100
}
}
/**
* Synthetic [LifecycleOwner] that stays permanently in [Lifecycle.State.RESUMED]. Replaces Android's
* `ProcessLifecycleOwner` for desktop.
@ -139,7 +86,6 @@ private class DesktopProcessLifecycleOwner : LifecycleOwner {
* Provides all platform-specific bindings that the real KMP `commonMain` implementations need:
* - Named [DataStore]<[Preferences]> instances (12 preference stores + 1 core preferences store)
* - Proto [DataStore] instances (LocalConfig, ModuleConfig, ChannelSet, LocalStats)
* - [DatabaseProvider] and [DatabaseManager] via Room KMP
* - [Lifecycle] (`ProcessLifecycle`)
* - [BuildConfigProvider]
*/
@ -147,8 +93,6 @@ private class DesktopProcessLifecycleOwner : LifecycleOwner {
fun desktopPlatformModule() = module {
includes(desktopPreferencesDataStoreModule(), desktopProtoDataStoreModule())
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// -- Build config --
single<BuildConfigProvider> {
object : BuildConfigProvider {
@ -163,11 +107,6 @@ fun desktopPlatformModule() = module {
// -- Process Lifecycle (stays RESUMED forever on desktop) --
single(named("ProcessLifecycle")) { DesktopProcessLifecycleOwner().lifecycle }
// -- Database (Room KMP with BundledSQLiteDriver) --
single { DesktopDatabaseManager() }
single<DatabaseProvider> { get<DesktopDatabaseManager>() }
single<DatabaseManager> { get<DesktopDatabaseManager>() }
}
/** Named [DataStore]<[Preferences]> instances for all preference domains. */

View file

@ -13,7 +13,7 @@ jetbrains-lifecycle = "2.10.0"
navigation = "2.9.7"
navigation3 = "1.1.0-alpha04"
paging = "3.4.2"
room = "2.8.4"
room = "3.0.0-alpha01"
savedstate = "1.4.0"
koin = "4.2.0"
koin-annotations = "2.1.0"
@ -109,11 +109,11 @@ jetbrains-navigation3-runtime = { module = "org.jetbrains.androidx.navigation3:n
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
androidx-paging-common = { module = "androidx.paging:paging-common", version.ref = "paging" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
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.6.2" }
androidx-room-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room" }
androidx-room-paging = { module = "androidx.room3:room3-paging", version.ref = "room" }
androidx-room-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room" }
androidx-room-testing = { module = "androidx.room3:room3-testing", version.ref = "room" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version = "2.5.0" }
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" }
@ -244,7 +244,7 @@ vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", ve
# Build Logic
android-gradleApiPlugin = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
android-tools-common = { module = "com.android.tools:common", version = "32.1.0" }
androidx-room-gradlePlugin = { module = "androidx.room:room-gradle-plugin", version.ref = "room" }
androidx-room-gradlePlugin = { module = "androidx.room3:room3-gradle-plugin", version.ref = "room" }
compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
compose-multiplatform-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" }
datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version.ref = "datadog-gradle" }
@ -296,7 +296,7 @@ datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version.ref = "da
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
wire = { id = "com.squareup.wire", version.ref = "wire" }
room = { id = "androidx.room", version.ref = "room" }
room = { id = "androidx.room3", version.ref = "room" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
test-retry = { id = "org.gradle.test-retry", version.ref = "testRetry" }
dependency-guard = { id = "com.dropbox.dependency-guard", version.ref = "dependency-guard" }