@@ -79,6 +79,9 @@ class DatabaseManager @Inject constructor(private val app: Application) {
7979 suspend fun switchActiveDatabase (address : String? ) = mutex.withLock {
8080 val dbName = buildDbName(address)
8181
82+ // Remember the previously active DB name (if any) so we can record its last-used time as well.
83+ val previousDbName = _currentDb .value?.openHelper?.databaseName
84+
8285 // Fast path: no-op if already on this address
8386 if (_currentAddress .value == address && _currentDb .value != null ) {
8487 markLastUsed(dbName)
@@ -93,10 +96,16 @@ class DatabaseManager @Inject constructor(private val app: Application) {
9396 _currentDb .value = db
9497 _currentAddress .value = address
9598 markLastUsed(dbName)
99+ // Also mark the previous DB as used "just now" so LRU has an accurate, recent timestamp
100+ // even on first run after upgrade where no timestamp might exist yet.
101+ previousDbName?.let { markLastUsed(it) }
96102
97103 // Defer LRU eviction so switch is not blocked by filesystem work
98104 managerScope.launch(Dispatchers .IO ) { enforceCacheLimit(activeDbName = dbName) }
99105
106+ // One-time cleanup: remove legacy DB if present and not active
107+ managerScope.launch(Dispatchers .IO ) { cleanupLegacyDbIfNeeded(activeDbName = dbName) }
108+
100109 Timber .i(" Switched active DB to ${anonymizeDbName(dbName)} for address ${anonymizeAddress(address)} " )
101110 }
102111
@@ -126,8 +135,23 @@ class DatabaseManager @Inject constructor(private val app: Application) {
126135 private suspend fun enforceCacheLimit (activeDbName : String ) = mutex.withLock {
127136 val limit = getCacheLimit()
128137 val all = listExistingDbNames()
129- if (all.size <= limit) return
130- val victims = all.filter { it != activeDbName }.sortedBy { lastUsed(it) }.take(all.size - limit)
138+ // Only enforce the limit over device-specific DBs; exclude legacy and default DBs
139+ val deviceDbs =
140+ all.filterNot { it == DatabaseConstants .LEGACY_DB_NAME || it == DatabaseConstants .DEFAULT_DB_NAME }
141+ Timber .d(
142+ " LRU check: limit=%d, active=%s, deviceDbs=%s" ,
143+ limit,
144+ anonymizeDbName(activeDbName),
145+ deviceDbs.joinToString(" , " ) { anonymizeDbName(it) },
146+ )
147+ if (deviceDbs.size <= limit) return
148+ val usageSnapshot = deviceDbs.associateWith { lastUsed(it) }
149+ Timber .d(
150+ " LRU lastUsed(ms): %s" ,
151+ usageSnapshot.entries.joinToString(" , " ) { (name, ts) -> " ${anonymizeDbName(name)} =$ts " },
152+ )
153+ val victims = selectEvictionVictims(deviceDbs, activeDbName, limit, usageSnapshot)
154+ Timber .i(" LRU victims: %s" , victims.joinToString(" , " ) { anonymizeDbName(it) })
131155 victims.forEach { name ->
132156 runCatching { dbCache.remove(name)?.close() }
133157 .onFailure { Timber .w(it, " Failed to close database %s" , name) }
@@ -150,6 +174,28 @@ class DatabaseManager @Inject constructor(private val app: Application) {
150174 val active = _currentDb .value?.openHelper?.databaseName ? : defaultDbName()
151175 managerScope.launch(Dispatchers .IO ) { enforceCacheLimit(activeDbName = active) }
152176 }
177+
178+ private suspend fun cleanupLegacyDbIfNeeded (activeDbName : String ) = mutex.withLock {
179+ if (prefs.getBoolean(DatabaseConstants .LEGACY_DB_CLEANED_KEY , false )) return
180+ val legacy = DatabaseConstants .LEGACY_DB_NAME
181+ if (legacy == activeDbName) {
182+ // Never delete the active DB; mark as cleaned to avoid repeated checks
183+ prefs.edit().putBoolean(DatabaseConstants .LEGACY_DB_CLEANED_KEY , true ).apply ()
184+ return
185+ }
186+ val legacyFile = getDbFile(app, legacy)
187+ if (legacyFile != null ) {
188+ runCatching { dbCache.remove(legacy)?.close() }
189+ .onFailure { Timber .w(it, " Failed to close legacy database %s before deletion" , legacy) }
190+ val deleted = app.deleteDatabase(legacy)
191+ if (deleted) {
192+ Timber .i(" Deleted legacy DB ${anonymizeDbName(legacy)} " )
193+ } else {
194+ Timber .w(" Attempted to delete legacy DB %s but deleteDatabase returned false" , legacy)
195+ }
196+ }
197+ prefs.edit().putBoolean(DatabaseConstants .LEGACY_DB_CLEANED_KEY , true ).apply ()
198+ }
153199}
154200
155201object DatabaseConstants {
@@ -162,6 +208,8 @@ object DatabaseConstants {
162208 const val MIN_CACHE_LIMIT : Int = 1
163209 const val MAX_CACHE_LIMIT : Int = 10
164210
211+ const val LEGACY_DB_CLEANED_KEY : String = " legacy_db_cleaned"
212+
165213 // Display/truncation and hash sizing for DB names
166214 const val DB_NAME_HASH_LEN : Int = 10
167215 const val DB_NAME_SEPARATOR_LEN : Int = 1
@@ -175,7 +223,16 @@ object DatabaseConstants {
175223// File-private helpers (kept outside the class to reduce class function count)
176224private fun defaultDbName (): String = DatabaseConstants .DEFAULT_DB_NAME
177225
178- private fun normalizeAddress (addr : String? ): String = addr?.uppercase()?.replace(" :" , " " ) ? : " DEFAULT"
226+ private fun normalizeAddress (addr : String? ): String {
227+ val u = addr?.trim()?.uppercase()
228+ val normalized =
229+ when {
230+ u.isNullOrBlank() -> " DEFAULT"
231+ u == " N" || u == " NULL" -> " DEFAULT"
232+ else -> u.replace(" :" , " " )
233+ }
234+ return normalized
235+ }
179236
180237private fun shortSha1 (s : String ): String = MessageDigest .getInstance(" SHA-1" )
181238 .digest(s.toByteArray())
@@ -216,3 +273,37 @@ private fun buildRoomDb(app: Application, dbName: String): MeshtasticDatabase =
216273 .build()
217274
218275private fun getDbFile (app : Application , dbName : String ): File ? = app.getDatabasePath(dbName).takeIf { it.exists() }
276+
277+ /* *
278+ * Compute which DBs to evict using LRU policy.
279+ *
280+ * Rules:
281+ * - Only consider device-specific DBs (exclude legacy and default)
282+ * - Never evict the active DB
283+ * - If number of device DBs is within the limit, evict none
284+ * - Otherwise evict the (size - limit) least-recently-used DBs
285+ *
286+ * Pass a precomputed [lastUsedMsByDb] snapshot to avoid redundant IO/lookups.
287+ */
288+ internal fun selectEvictionVictims (
289+ dbNames : List <String >,
290+ activeDbName : String ,
291+ limit : Int ,
292+ lastUsedMsByDb : Map <String , Long >,
293+ ): List <String > {
294+ val deviceDbNames =
295+ dbNames.filterNot { it == DatabaseConstants .LEGACY_DB_NAME || it == DatabaseConstants .DEFAULT_DB_NAME }
296+ val victims =
297+ if (limit < 1 || deviceDbNames.size <= limit) {
298+ emptyList()
299+ } else {
300+ val candidates = deviceDbNames.filter { it != activeDbName }
301+ if (candidates.isEmpty()) {
302+ emptyList()
303+ } else {
304+ val toEvict = deviceDbNames.size - limit
305+ candidates.sortedBy { lastUsedMsByDb[it] ? : 0L }.take(toEvict)
306+ }
307+ }
308+ return victims
309+ }
0 commit comments