mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
chore: Upgrade to Room 3.0 and refactor related components
This commit is contained in:
parent
38591f4c0b
commit
785b698542
31 changed files with 121 additions and 415 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/)*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue