From 6b1b75186f96372cc39e93f8deb0bbbe18f4b320 Mon Sep 17 00:00:00 2001 From: Christian Nagel Date: Fri, 7 Aug 2026 17:36:42 +0200 Subject: [PATCH] Migrate ShownMessages from DataStore to Room --- .../scanbridge/AndroidBuildInfoProvider.kt | 18 ++ .../chrisimx/scanbridge/ScanBridgeApp.kt | 8 +- .../scanbridge/ScanBridgeApplication.kt | 19 +- .../DatastoreShownMessagesRepository.kt | 19 +- .../6.json | 286 ++++++++++++++++++ .../scanbridge/ScanProtocolRegistry.kt | 2 +- .../scanbridge/ShownMessagesRepository.kt | 12 - .../scanbridge/buildinfo/BuildInfoProvider.kt | 12 + .../scanbridge/buildinfo/ScanBridgeEdition.kt | 7 + .../chrisimx/scanbridge/db/ScanBridgeDb.kt | 14 +- .../scanbridge/db/ScanBridgeDbFactory.kt | 8 +- .../db/daos/ShownStartupMessageDao.kt | 38 +++ .../db/entities/ShownStartupMessage.kt | 11 + .../db/migrations/KoinRoomMigrationsModule.kt | 8 + .../db/migrations/RoomMigrationV5To6.kt | 42 +++ ...ion4To5.kt => RoomMigrationVersion4To5.kt} | 2 +- .../chrisimx/scanbridge/model/Platform.kt | 7 + .../RoomShownStartupMessagesRepository.kt | 26 ++ .../ShownStartupMessagesRepository.kt | 8 + .../startupmessages/StartupMessage.kt | 7 + 20 files changed, 515 insertions(+), 39 deletions(-) create mode 100644 androidApp/src/main/java/io/github/chrisimx/scanbridge/AndroidBuildInfoProvider.kt create mode 100644 core/schemas/io.github.chrisimx.scanbridge.db.ScanBridgeDb/6.json delete mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ShownMessagesRepository.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/BuildInfoProvider.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/ScanBridgeEdition.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/ShownStartupMessageDao.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/ShownStartupMessage.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationV5To6.kt rename core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/{Version4To5.kt => RoomMigrationVersion4To5.kt} (98%) create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/model/Platform.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/RoomShownStartupMessagesRepository.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/ShownStartupMessagesRepository.kt create mode 100644 core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/StartupMessage.kt diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/AndroidBuildInfoProvider.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/AndroidBuildInfoProvider.kt new file mode 100644 index 00000000..d0b6b612 --- /dev/null +++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/AndroidBuildInfoProvider.kt @@ -0,0 +1,18 @@ +package io.github.chrisimx.scanbridge + +import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider +import io.github.chrisimx.scanbridge.buildinfo.ScanBridgeEdition +import io.github.chrisimx.scanbridge.model.Platform + +class AndroidBuildInfoProvider : BuildInfoProvider { + override val versionName: String = BuildConfig.VERSION_NAME + override val versionCode: Int = BuildConfig.VERSION_CODE + override val commit: String = BuildConfig.GIT_COMMIT_HASH + override val edition: ScanBridgeEdition = when (BuildConfig.FLAVOR) { + "play" -> ScanBridgeEdition.PLAYSTORE + "fdroid" -> ScanBridgeEdition.FDROID + else -> throw IllegalStateException("Unknown build flavor: ${BuildConfig.FLAVOR}") + } + override val isDebugBuild: Boolean = BuildConfig.DEBUG + override val platform = Platform.ANDROID +} diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApp.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApp.kt index a15ffb83..fc4f7c45 100644 --- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApp.kt +++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApp.kt @@ -41,6 +41,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import io.github.chrisimx.scanbridge.startupmessages.ShownStartupMessagesRepository +import io.github.chrisimx.scanbridge.startupmessages.StartupMessage import io.github.chrisimx.scanbridge.theme.ScanBridgeTheme import io.github.chrisimx.scanbridge.uicomponents.CrashFileHandler import kotlinx.coroutines.launch @@ -59,10 +61,10 @@ fun ScanBridgeApp() { val typedRoute = currentBackStackEntry?.toTypedRoute() val lastRouteRepository = koinInject() - val shownMessagesRepository = koinInject() + val shownMessagesRepository = koinInject() val thanksForPurchaseAlreadyShown by shownMessagesRepository - .getWasShownFlow(UserInformationMessage.THANKS_FOR_PURCHASE) + .getWasShownFlow(StartupMessage.THANKS_FOR_PURCHASE) .collectAsState(true) LaunchedEffect(Unit) { @@ -96,7 +98,7 @@ fun ScanBridgeApp() { val markThanksMessageAsRead = { coroutineScope.launch { - shownMessagesRepository.setShown(UserInformationMessage.THANKS_FOR_PURCHASE, true) + shownMessagesRepository.setShown(StartupMessage.THANKS_FOR_PURCHASE, true) } } diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt index 66f06c25..4ec1e1e6 100644 --- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt +++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/ScanBridgeApplication.kt @@ -11,6 +11,7 @@ import coil3.ImageLoader import coil3.network.ktor3.KtorNetworkFetcherFactory import io.github.chrisimx.scanbridge.adapters.KoinBasedScanningProtocolManager import io.github.chrisimx.scanbridge.adapters.RoomBackedCustomScannerRepository +import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider import io.github.chrisimx.scanbridge.data.ui.ScanSettingsComposableStateHolder import io.github.chrisimx.scanbridge.data.ui.ScanningScreenViewModel import io.github.chrisimx.scanbridge.datastore.appSettingsStore @@ -19,6 +20,7 @@ import io.github.chrisimx.scanbridge.db.DefaultScanBridgeDbFactory import io.github.chrisimx.scanbridge.db.ScanBridgeDb import io.github.chrisimx.scanbridge.db.ScanBridgeDbBuilderFactory import io.github.chrisimx.scanbridge.db.ScanBridgeDbFactory +import io.github.chrisimx.scanbridge.db.migrations.ROOM_MIGRATIONS import io.github.chrisimx.scanbridge.infrastructure.KmLogScanBridgeLoggerFactory import io.github.chrisimx.scanbridge.migrations.MigrationExecutor import io.github.chrisimx.scanbridge.migrations.RoomBackedMigrationExecutor @@ -44,6 +46,8 @@ import io.github.chrisimx.scanbridge.services.AndroidLocaleProvider import io.github.chrisimx.scanbridge.services.DebugLogService import io.github.chrisimx.scanbridge.services.FileDebugLogService import io.github.chrisimx.scanbridge.services.ScanJobRepository +import io.github.chrisimx.scanbridge.startupmessages.RoomShownStartupMessagesRepository +import io.github.chrisimx.scanbridge.startupmessages.ShownStartupMessagesRepository import io.github.chrisimx.scanbridge.usecases.StartScanUseCase import io.ktor.client.HttpClient import kotlinx.coroutines.CoroutineScope @@ -114,9 +118,13 @@ val appModule = module { factory() bind MdnsDiscoverService::class single() single() bind LastRouteRepository::class - single { - DatastoreShownMessagesRepository(get(named())) - } bind ShownMessagesRepository::class + single(named("legacyDatastoreShownMessages")) { + DatastoreShownMessagesRepository( + get(named()), + get() + ) + } bind ShownStartupMessagesRepository::class + single() bind ShownStartupMessagesRepository::class single() bind ScanningProtocolManager::class single() bind InitialScanSettingsProvider::class single() bind PaperFormatProvider::class @@ -127,7 +135,10 @@ val appModule = module { viewModel() single() bind MulticastLockHandler::class single() bind StartScanUseCase::class - includes(scanProtocols) + + single() bind BuildInfoProvider::class + + includes(SCAN_PROTOCOLS, ROOM_MIGRATIONS) } class ScanBridgeApplication : Application() { diff --git a/androidApp/src/main/java/io/github/chrisimx/scanbridge/repositories/DatastoreShownMessagesRepository.kt b/androidApp/src/main/java/io/github/chrisimx/scanbridge/repositories/DatastoreShownMessagesRepository.kt index a2fa17e3..9df8479b 100644 --- a/androidApp/src/main/java/io/github/chrisimx/scanbridge/repositories/DatastoreShownMessagesRepository.kt +++ b/androidApp/src/main/java/io/github/chrisimx/scanbridge/repositories/DatastoreShownMessagesRepository.kt @@ -1,26 +1,27 @@ package io.github.chrisimx.scanbridge.repositories import androidx.datastore.core.DataStore -import io.github.chrisimx.scanbridge.BuildConfig -import io.github.chrisimx.scanbridge.ShownMessagesRepository -import io.github.chrisimx.scanbridge.UserInformationMessage +import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider import io.github.chrisimx.scanbridge.proto.ShownMessages import io.github.chrisimx.scanbridge.proto.copy +import io.github.chrisimx.scanbridge.startupmessages.ShownStartupMessagesRepository +import io.github.chrisimx.scanbridge.startupmessages.StartupMessage import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -class DatastoreShownMessagesRepository(val shownMessagesDataStore: DataStore) : ShownMessagesRepository { - override fun getWasShownFlow(message: UserInformationMessage): Flow = shownMessagesDataStore.data.map { - (BuildConfig.FLAVOR != "play" && message.playOnly) || !when (message) { - UserInformationMessage.THANKS_FOR_PURCHASE -> it.thankPlayOne +class DatastoreShownMessagesRepository(val shownMessagesDataStore: DataStore, val buildInfoProvider: BuildInfoProvider) : + ShownStartupMessagesRepository { + override fun getWasShownFlow(message: StartupMessage): Flow = shownMessagesDataStore.data.map { + !message.editions.contains(buildInfoProvider.edition) || when (message) { + StartupMessage.THANKS_FOR_PURCHASE -> it.thankPlayOne } } - override suspend fun setShown(message: UserInformationMessage, shown: Boolean) { + override suspend fun setShown(message: StartupMessage, shown: Boolean) { shownMessagesDataStore.updateData { it.copy { when (message) { - UserInformationMessage.THANKS_FOR_PURCHASE -> { + StartupMessage.THANKS_FOR_PURCHASE -> { thankPlayOne = shown } } diff --git a/core/schemas/io.github.chrisimx.scanbridge.db.ScanBridgeDb/6.json b/core/schemas/io.github.chrisimx.scanbridge.db.ScanBridgeDb/6.json new file mode 100644 index 00000000..bd98d192 --- /dev/null +++ b/core/schemas/io.github.chrisimx.scanbridge.db.ScanBridgeDb/6.json @@ -0,0 +1,286 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "e825fe52357f0eef9cb4a7033180b96b", + "entities": [ + { + "tableName": "customscanners", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `protocolIdentifier` TEXT NOT NULL DEFAULT 'eSCL', PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protocolIdentifier", + "columnName": "protocolIdentifier", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'eSCL'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + } + }, + { + "tableName": "scannedpages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scanId` TEXT NOT NULL, `ownerSessionId` TEXT NOT NULL, `filePath` TEXT NOT NULL, `originalScanSettings` TEXT NOT NULL, `rotation` TEXT NOT NULL, `orderIndex` INTEGER NOT NULL, `outputName` TEXT, PRIMARY KEY(`scanId`), FOREIGN KEY(`ownerSessionId`) REFERENCES `sessions`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "scanId", + "columnName": "scanId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerSessionId", + "columnName": "ownerSessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filePath", + "columnName": "filePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalScanSettings", + "columnName": "originalScanSettings", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rotation", + "columnName": "rotation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderIndex", + "columnName": "orderIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "outputName", + "columnName": "outputName", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "scanId" + ] + }, + "indices": [ + { + "name": "index_scannedpages_ownerSessionId", + "unique": false, + "columnNames": [ + "ownerSessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_scannedpages_ownerSessionId` ON `${TABLE_NAME}` (`ownerSessionId`)" + }, + { + "name": "index_scannedpages_ownerSessionId_orderIndex", + "unique": true, + "columnNames": [ + "ownerSessionId", + "orderIndex" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_scannedpages_ownerSessionId_orderIndex` ON `${TABLE_NAME}` (`ownerSessionId`, `orderIndex`)" + } + ], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerSessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "sessions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `currentScanSettings` TEXT, `currentSettingsUIData` TEXT DEFAULT null, `currentPage` INTEGER NOT NULL, PRIMARY KEY(`sessionId`))", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentScanSettings", + "columnName": "currentScanSettings", + "affinity": "TEXT" + }, + { + "fieldPath": "currentSettingsUIData", + "columnName": "currentSettingsUIData", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "currentPage", + "columnName": "currentPage", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sessionId" + ] + } + }, + { + "tableName": "tempfiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tempFileId` TEXT NOT NULL, `ownerSessionId` TEXT NOT NULL, `path` TEXT NOT NULL, PRIMARY KEY(`tempFileId`), FOREIGN KEY(`ownerSessionId`) REFERENCES `sessions`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tempFileId", + "columnName": "tempFileId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerSessionId", + "columnName": "ownerSessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tempFileId" + ] + }, + "indices": [ + { + "name": "index_tempfiles_ownerSessionId", + "unique": false, + "columnNames": [ + "ownerSessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tempfiles_ownerSessionId` ON `${TABLE_NAME}` (`ownerSessionId`)" + } + ], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "ownerSessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "lastroute", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`route` TEXT NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "route", + "columnName": "route", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "executedmigrationtoroom", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`migrationId` TEXT NOT NULL, PRIMARY KEY(`migrationId`))", + "fields": [ + { + "fieldPath": "migrationId", + "columnName": "migrationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "migrationId" + ] + } + }, + { + "tableName": "shownstartupmessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message` TEXT NOT NULL, PRIMARY KEY(`message`))", + "fields": [ + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e825fe52357f0eef9cb4a7033180b96b')" + ] + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ScanProtocolRegistry.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ScanProtocolRegistry.kt index a6ea6a01..07b004a2 100644 --- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ScanProtocolRegistry.kt +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ScanProtocolRegistry.kt @@ -7,7 +7,7 @@ import org.koin.dsl.bind import org.koin.dsl.module import org.koin.plugin.module.dsl.single -val scanProtocols = module { +val SCAN_PROTOCOLS = module { single() bind ScanningProtocol::class single() bind ScanningProtocol::class } diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ShownMessagesRepository.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ShownMessagesRepository.kt deleted file mode 100644 index af6b8460..00000000 --- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/ShownMessagesRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.chrisimx.scanbridge - -import kotlinx.coroutines.flow.Flow - -interface ShownMessagesRepository { - fun getWasShownFlow(message: UserInformationMessage): Flow - suspend fun setShown(message: UserInformationMessage, shown: Boolean) -} - -enum class UserInformationMessage(val playOnly: Boolean) { - THANKS_FOR_PURCHASE(true) -} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/BuildInfoProvider.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/BuildInfoProvider.kt new file mode 100644 index 00000000..a7226125 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/BuildInfoProvider.kt @@ -0,0 +1,12 @@ +package io.github.chrisimx.scanbridge.buildinfo + +import io.github.chrisimx.scanbridge.model.Platform + +interface BuildInfoProvider { + val versionName: String + val versionCode: Int + val commit: String + val edition: ScanBridgeEdition + val isDebugBuild: Boolean + val platform: Platform +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/ScanBridgeEdition.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/ScanBridgeEdition.kt new file mode 100644 index 00000000..b16665d6 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/buildinfo/ScanBridgeEdition.kt @@ -0,0 +1,7 @@ +package io.github.chrisimx.scanbridge.buildinfo + +enum class ScanBridgeEdition { + FDROID, + PLAYSTORE, + IOS +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt index 680f24f6..d016f35f 100644 --- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDb.kt @@ -9,12 +9,14 @@ import io.github.chrisimx.scanbridge.db.daos.ExecutedMigrationsDao import io.github.chrisimx.scanbridge.db.daos.LastRouteDao import io.github.chrisimx.scanbridge.db.daos.ScannedPageDao import io.github.chrisimx.scanbridge.db.daos.SessionDao +import io.github.chrisimx.scanbridge.db.daos.ShownStartupMessageDao import io.github.chrisimx.scanbridge.db.daos.TempFileDao import io.github.chrisimx.scanbridge.db.entities.CustomScanner import io.github.chrisimx.scanbridge.db.entities.ExecutedMigrationToRoom import io.github.chrisimx.scanbridge.db.entities.LastRoute import io.github.chrisimx.scanbridge.db.entities.ScannedPage import io.github.chrisimx.scanbridge.db.entities.Session +import io.github.chrisimx.scanbridge.db.entities.ShownStartupMessage import io.github.chrisimx.scanbridge.db.entities.TempFile import io.github.chrisimx.scanbridge.db.typeconverters.CommonScanSettingsTypeConverter import io.github.chrisimx.scanbridge.db.typeconverters.ScanSettingsTypeConverter @@ -25,9 +27,12 @@ import io.github.chrisimx.scanbridge.db.typeconverters.UuidTypeConverter @Database( entities = [ - CustomScanner::class, ScannedPage::class, Session::class, TempFile::class, LastRoute::class, ExecutedMigrationToRoom::class + CustomScanner::class, ScannedPage::class, + Session::class, TempFile::class, + LastRoute::class, ExecutedMigrationToRoom::class, + ShownStartupMessage::class ], - version = 5, + version = 6, autoMigrations = [ AutoMigration( from = 1, @@ -40,10 +45,6 @@ import io.github.chrisimx.scanbridge.db.typeconverters.UuidTypeConverter AutoMigration( from = 3, to = 4 - ), - AutoMigration( - from = 4, - to = 5 ) ] ) @@ -63,4 +64,5 @@ abstract class ScanBridgeDb : RoomDatabase() { abstract fun tmpFileDao(): TempFileDao abstract fun lastRouteDao(): LastRouteDao abstract fun executedMigrationsDao(): ExecutedMigrationsDao + abstract fun shownStartupMessageDao(): ShownStartupMessageDao } diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt index d12fc57d..fbc043d9 100644 --- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/ScanBridgeDbFactory.kt @@ -1,7 +1,8 @@ package io.github.chrisimx.scanbridge.db -import MIGRATION_4_5 +import ROOM_MIGRATION_4_5 import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import io.github.chrisimx.scanbridge.db.migrations.RoomMigrationV5To6 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -9,13 +10,14 @@ interface ScanBridgeDbFactory { fun createInstance(): ScanBridgeDb } -class DefaultScanBridgeDbFactory(val builderFactory: ScanBridgeDbBuilderFactory) : ScanBridgeDbFactory { +class DefaultScanBridgeDbFactory(val builderFactory: ScanBridgeDbBuilderFactory, val migrationV5To6: RoomMigrationV5To6) : + ScanBridgeDbFactory { override fun createInstance(): ScanBridgeDb { val dbBuilder = builderFactory.getBuilder() return dbBuilder .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) - .addMigrations(MIGRATION_4_5) + .addMigrations(ROOM_MIGRATION_4_5, migrationV5To6) .build() } } diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/ShownStartupMessageDao.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/ShownStartupMessageDao.kt new file mode 100644 index 00000000..1e6dc6e0 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/daos/ShownStartupMessageDao.kt @@ -0,0 +1,38 @@ +package io.github.chrisimx.scanbridge.db.daos + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.github.chrisimx.scanbridge.db.entities.ShownStartupMessage +import io.github.chrisimx.scanbridge.startupmessages.StartupMessage +import kotlinx.coroutines.flow.Flow + +@Dao +interface ShownStartupMessageDao { + @Query("SELECT * FROM shownstartupmessages") + fun getAllFlow(): Flow> + + @Query( + """ + SELECT EXISTS( + SELECT 1 + FROM shownstartupmessages + WHERE message = :message + ) +""" + ) + fun exists(message: StartupMessage): Flow + + @Query("SELECT * FROM shownstartupmessages") + suspend fun getAll(): List + + @Delete + suspend fun delete(startupMessage: ShownStartupMessage) + + @Insert( + onConflict = OnConflictStrategy.IGNORE + ) + suspend fun insertAll(vararg startupMessages: ShownStartupMessage) +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/ShownStartupMessage.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/ShownStartupMessage.kt new file mode 100644 index 00000000..131cb7f2 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/entities/ShownStartupMessage.kt @@ -0,0 +1,11 @@ +package io.github.chrisimx.scanbridge.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import io.github.chrisimx.scanbridge.startupmessages.StartupMessage + +@Entity(tableName = "shownstartupmessages") +data class ShownStartupMessage( + @PrimaryKey + val message: StartupMessage +) diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt new file mode 100644 index 00000000..82491b9f --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/KoinRoomMigrationsModule.kt @@ -0,0 +1,8 @@ +package io.github.chrisimx.scanbridge.db.migrations + +import org.koin.dsl.module +import org.koin.plugin.module.dsl.single + +val ROOM_MIGRATIONS = module { + single() +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationV5To6.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationV5To6.kt new file mode 100644 index 00000000..2d492691 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationV5To6.kt @@ -0,0 +1,42 @@ +package io.github.chrisimx.scanbridge.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.SQLiteConnection +import androidx.sqlite.execSQL +import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider +import io.github.chrisimx.scanbridge.model.Platform +import io.github.chrisimx.scanbridge.ports.ScanBridgeLoggerFactory +import io.github.chrisimx.scanbridge.startupmessages.ShownStartupMessagesRepository +import io.github.chrisimx.scanbridge.startupmessages.StartupMessage +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.koin.core.qualifier.named +import org.koin.core.scope.Scope + +class RoomMigrationV5To6(val buildInfoProvider: BuildInfoProvider, val koinScope: Scope, val loggerFactory: ScanBridgeLoggerFactory) : + Migration(5, 6) { + + private val logger = loggerFactory.withClass(this::class) + + override fun migrate(connection: SQLiteConnection): Unit = runBlocking { + val oldShownMessages = if (buildInfoProvider.platform == Platform.ANDROID) { + // On Android, we need to get the old ShownStartupMessages from the DataStore + logger.debug { "Android: Migration of old shown startup messages from DataStore to Room" } + val oldDataStoreRepo = koinScope.get(named("legacyDatastoreShownMessages")) + + StartupMessage.entries.map { + it to oldDataStoreRepo.getWasShownFlow(it).first() + }.filter { (_, wasShown) -> wasShown } + .map { (message, _) -> message } + } else { + logger.debug { "Other Platform (not Android): Migration of old shown startup messages from DataStore to Room not necessary" } + null + } + + connection.execSQL("CREATE TABLE IF NOT EXISTS `shownstartupmessages` (`message` TEXT NOT NULL, PRIMARY KEY(`message`))") + + oldShownMessages?.forEach { message -> + connection.execSQL("INSERT INTO shownstartupmessages (message) VALUES ('${message.name}')") + } + } +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/Version4To5.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion4To5.kt similarity index 98% rename from core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/Version4To5.kt rename to core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion4To5.kt index e0bf8606..552a7474 100644 --- a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/Version4To5.kt +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/db/migrations/RoomMigrationVersion4To5.kt @@ -6,7 +6,7 @@ import io.github.chrisimx.esclkt.anyscancompat.toCommonAbstraction import io.github.chrisimx.scanbridge.ScanSettingsJson import io.github.chrisimx.scanbridge.model.ScanSettingsEnterableDataV0 -val MIGRATION_4_5 = object : Migration(4, 5) { +val ROOM_MIGRATION_4_5 = object : Migration(4, 5) { override fun migrate(connection: SQLiteConnection) { migrateCustomScanners(connection) diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/model/Platform.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/model/Platform.kt new file mode 100644 index 00000000..dd3cd600 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/model/Platform.kt @@ -0,0 +1,7 @@ +package io.github.chrisimx.scanbridge.model + +enum class Platform { + ANDROID, + IOS, + DESKTOP +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/RoomShownStartupMessagesRepository.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/RoomShownStartupMessagesRepository.kt new file mode 100644 index 00000000..409bbb83 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/RoomShownStartupMessagesRepository.kt @@ -0,0 +1,26 @@ +package io.github.chrisimx.scanbridge.startupmessages + +import io.github.chrisimx.scanbridge.buildinfo.BuildInfoProvider +import io.github.chrisimx.scanbridge.db.ScanBridgeDb +import io.github.chrisimx.scanbridge.db.entities.ShownStartupMessage +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class RoomShownStartupMessagesRepository(scanBridgeDb: ScanBridgeDb, private val buildInfoProvider: BuildInfoProvider) : + ShownStartupMessagesRepository { + private val shownMessagesDao = scanBridgeDb.shownStartupMessageDao() + + override fun getWasShownFlow(message: StartupMessage): Flow = shownMessagesDao.exists(message).map { + it || !message.editions.contains(buildInfoProvider.edition) + } + + override suspend fun setShown(message: StartupMessage, shown: Boolean) { + if (shown) { + shownMessagesDao.insertAll( + ShownStartupMessage(message) + ) + } else { + shownMessagesDao.delete(ShownStartupMessage(message)) + } + } +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/ShownStartupMessagesRepository.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/ShownStartupMessagesRepository.kt new file mode 100644 index 00000000..91271e2e --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/ShownStartupMessagesRepository.kt @@ -0,0 +1,8 @@ +package io.github.chrisimx.scanbridge.startupmessages + +import kotlinx.coroutines.flow.Flow + +interface ShownStartupMessagesRepository { + fun getWasShownFlow(message: StartupMessage): Flow + suspend fun setShown(message: StartupMessage, shown: Boolean) +} diff --git a/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/StartupMessage.kt b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/StartupMessage.kt new file mode 100644 index 00000000..d9440165 --- /dev/null +++ b/core/src/commonMain/kotlin/io/github/chrisimx/scanbridge/startupmessages/StartupMessage.kt @@ -0,0 +1,7 @@ +package io.github.chrisimx.scanbridge.startupmessages + +import io.github.chrisimx.scanbridge.buildinfo.ScanBridgeEdition + +enum class StartupMessage(val editions: Set) { + THANKS_FOR_PURCHASE(setOf(ScanBridgeEdition.PLAYSTORE)) +}