diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index 48200ed6..6b7e20cf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -7,7 +7,6 @@ package org.moire.ultrasonic.data -import androidx.room.Room import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -40,8 +39,7 @@ import timber.log.Timber class ActiveServerProvider(private val repository: ServerSettingDao) : CoroutineScope by CoroutinePatterns.loggingScope(Dispatchers.IO) { - private var cachedDatabase: MetaDatabase? = null - private var cachedServerId: Int? = null + private val metadataDatabases = MetaDatabaseRegistry() private val _accessState = MutableStateFlow( LibraryAccessState(Settings.activeServer, Settings.explicitOffline) @@ -308,45 +306,18 @@ class ActiveServerProvider(private val repository: ServerSettingDao) : * this rather than the effective, offline database. */ @Synchronized - fun getSelectedMetaDatabase(): MetaDatabase { - val selectedServer = getSelectedServerId() + fun getSelectedMetaDatabase(): MetaDatabase = getMetaDatabaseForServer(getSelectedServerId()) - if (selectedServer == cachedServerId && cachedDatabase != null) { - return cachedDatabase!! - } + /** Metadata storage owned by a server, independent of the current UI selection. */ + @Synchronized + fun getMetaDatabaseForServer(serverId: Int): MetaDatabase = + metadataDatabases.forServer(serverId) - if (selectedServer < 1) { - return offlineMetaDatabase - } - - Timber.i("Switching to new database, id:$selectedServer") - cachedServerId = selectedServer - cachedDatabase = initDatabase(selectedServer) - - return cachedDatabase!! - } - - val offlineMetaDatabase: MetaDatabase by lazy { - initDatabase(0) - } - - private fun initDatabase(serverId: Int): MetaDatabase = Room.databaseBuilder( - UApp.applicationContext(), - MetaDatabase::class.java, - METADATA_DB + serverId - ) - .addMigrations(META_MIGRATION_2_3, META_MIGRATION_3_4) - .fallbackToDestructiveMigrationOnDowngrade(true) - .build() + val offlineMetaDatabase: MetaDatabase get() = metadataDatabases.forServer(0) @Synchronized fun deleteMetaDatabase(id: Int) { - if (cachedServerId == id) { - cachedDatabase?.close() - cachedDatabase = null - cachedServerId = null - } - UApp.applicationContext().deleteDatabase(METADATA_DB + id) + metadataDatabases.delete(id) Timber.i("Deleted metadataBase, id:$id") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabaseRegistry.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabaseRegistry.kt new file mode 100644 index 00000000..704dda9c --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabaseRegistry.kt @@ -0,0 +1,39 @@ +package org.moire.ultrasonic.data + +import androidx.room.Room +import org.moire.ultrasonic.app.UApp +import timber.log.Timber + +/** Owns one Room handle per server for the lifetime of the application process. */ +internal class MetaDatabaseRegistry( + private val open: (Int) -> MetaDatabase = { serverId -> + Room.databaseBuilder( + UApp.applicationContext(), + MetaDatabase::class.java, + ActiveServerProvider.METADATA_DB + serverId + ) + .addMigrations(META_MIGRATION_2_3, META_MIGRATION_3_4) + .fallbackToDestructiveMigrationOnDowngrade(true) + .build() + }, + private val deleteFile: (Int) -> Unit = { serverId -> + UApp.applicationContext().deleteDatabase(ActiveServerProvider.METADATA_DB + serverId) + } +) { + private val databases = mutableMapOf() + + @Synchronized + fun forServer(serverId: Int): MetaDatabase { + val databaseId = serverId.coerceAtLeast(0) + return databases.getOrPut(databaseId) { + Timber.i("Opening metadata database, id:$databaseId") + open(databaseId) + } + } + + @Synchronized + fun delete(serverId: Int) { + databases.remove(serverId)?.close() + deleteFile(serverId) + } +} diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/MetaDatabaseRegistryTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/MetaDatabaseRegistryTest.kt new file mode 100644 index 00000000..18cb6e82 --- /dev/null +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/MetaDatabaseRegistryTest.kt @@ -0,0 +1,35 @@ +package org.moire.ultrasonic.data + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class MetaDatabaseRegistryTest { + @Test + fun `server switch reuses handles and deletion closes only its server`() { + val first: MetaDatabase = mock() + val second: MetaDatabase = mock() + val opened = mutableListOf() + val deleted = mutableListOf() + val registry = MetaDatabaseRegistry( + open = { id -> + opened += id + if (id == 1) first else second + }, + deleteFile = { deleted += it } + ) + + registry.forServer(1) shouldBeEqualTo first + registry.forServer(2) shouldBeEqualTo second + registry.forServer(1) shouldBeEqualTo first + opened shouldBeEqualTo listOf(1, 2) + + registry.delete(1) + + verify(first).close() + deleted shouldBeEqualTo listOf(1) + registry.forServer(2) shouldBeEqualTo second + opened shouldBeEqualTo listOf(1, 2) + } +}