diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt index 1d5d77c42..5f96b8175 100644 --- a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -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 { override fun apply(target: Project) { with(target) { - apply(plugin = "androidx.room") + apply(plugin = "androidx.room3") apply(plugin = "com.google.devtools.ksp") extensions.configure { @@ -55,7 +55,7 @@ class AndroidRoomConventionPlugin : Plugin { } } dependencies { - "kspAndroid"(roomCompiler) + add("kspAndroid", roomCompiler) } } diff --git a/conductor/tracks.md b/conductor/tracks.md index dc117455f..431ed9884 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -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/)* diff --git a/conductor/tracks/migrate_room3_20260320/plan.md b/conductor/tracks/migrate_room3_20260320/plan.md index d5a581832..a67023655 100644 --- a/conductor/tracks/migrate_room3_20260320/plan.md +++ b/conductor/tracks/migrate_room3_20260320/plan.md @@ -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) diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 1815335f2..386bf58b3 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -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) diff --git a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt index 0d46627fd..fcff867b0 100644 --- a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt +++ b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/MeshtasticDatabaseTest.kt @@ -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 diff --git a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt index 2777135ed..155f7fcee 100644 --- a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt +++ b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/NodeInfoDaoTest.kt @@ -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 diff --git a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt index a75bfa07c..c67e9bc35 100644 --- a/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt +++ b/core/database/src/androidDeviceTest/kotlin/org/meshtastic/core/database/dao/PacketDaoTest.kt @@ -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 diff --git a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt index 507490c34..b1e99d974 100644 --- a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt +++ b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt @@ -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 diff --git a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt deleted file mode 100644 index 913524381..000000000 --- a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ /dev/null @@ -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 . - */ -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 = _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(null) - override val currentDb: StateFlow = - _currentDb.filterNotNull().stateIn(managerScope, SharingStarted.Eagerly, buildRoomDb(app, defaultDbName())) - - private val _currentAddress = MutableStateFlow(null) - val currentAddress: StateFlow = _currentAddress - - private val dbCache = mutableMapOf() // 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 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 { - 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( - 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() } diff --git a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt index 26b56484c..cffb8a67e 100644 --- a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt +++ b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseAndroidModule.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt index 3de320ae5..35746f68f 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/Converters.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt index 95a43db00..29d9b17ec 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.kt @@ -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 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 index 997fa9cc3..f98adcab1 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabaseConstructor.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabaseConstructor.kt @@ -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 { diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt index 5d6b4ea94..fcdc079f2 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/DeviceHardwareDao.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt index dfaa30eea..0a5520a07 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/FirmwareReleaseDao.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt index 669f86aee..967a97ec5 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/MeshLogDao.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt index 9a09c3bdf..752619014 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt index f8d6947ad..ae6506cbf 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/PacketDao.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt index 8a8f6ded7..177d71dfb 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/QuickChatActionDao.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt index 863a42440..2e7f6c549 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/TracerouteNodePositionDao.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt index 5626c6269..acae365da 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/di/CoreDatabaseModule.kt @@ -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") +} diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt index 101b62255..09af174fe 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/DeviceHardwareEntity.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt index 113435616..c3eabaf77 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/FirmwareReleaseEntity.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt index db23720cd..2f102c0ea 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MeshLog.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt index 9140754f2..ef2226ffc 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/MyNodeEntity.kt @@ -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") diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt index cb4bf06d2..13d10193c 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt index 5529b9606..859913c23 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -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 diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt index fbcaba95d..afa565cc1 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/QuickChatAction.kt @@ -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( diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt index 3712978a2..ddae980fa 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/TracerouteNodePositionEntity.kt @@ -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( diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt index c5f5a33f8..384a102ac 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt @@ -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(name = dbName) { MeshtasticDatabaseConstructor.initialize() } - .configureCommon() - .build() - } - - override val currentDb: StateFlow by lazy { MutableStateFlow(db) } - - override suspend fun withDb(block: suspend (MeshtasticDatabase) -> T): T? = block(db) - - private val _cacheLimit = MutableStateFlow(DEFAULT_CACHE_LIMIT) - override val cacheLimit: StateFlow = _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 { 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 { get() } - single { get() } } /** Named [DataStore]<[Preferences]> instances for all preference domains. */ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a1672e78..6d0b32372 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }