From 966d43d4d189c0011417b88ba242712bb4fc5f67 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 25 Nov 2023 18:54:27 +0100 Subject: [PATCH] Migrate file search settings to decentralized json data store --- .../launcher2/ui/common/SearchablePickerVM.kt | 1 - .../launcher2/ui/launcher/search/SearchVM.kt | 5 +- .../filesearch/FileSearchSettingsScreenVM.kt | 75 ++---------- .../preferences/src/main/proto/settings.proto | 2 +- data/files/build.gradle.kts | 3 + .../mm20/launcher2/files/FilesRepository.kt | 12 +- .../java/de/mm20/launcher2/files/Module.kt | 20 +++- .../files/settings/FileSearchSettings.kt | 112 ++++++++++++++++++ .../files/settings/FileSearchSettingsData.kt | 49 ++++++++ .../files/settings/migrations/Migration1.kt | 33 ++++++ .../de/mm20/launcher2/search/SearchService.kt | 28 ++--- 11 files changed, 244 insertions(+), 96 deletions(-) create mode 100644 data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettings.kt create mode 100644 data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettingsData.kt create mode 100644 data/files/src/main/java/de/mm20/launcher2/files/settings/migrations/Migration1.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt index 18d1c93b..366aea3f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePickerVM.kt @@ -47,7 +47,6 @@ class SearchablePickerVM: ViewModel(), KoinComponent { shortcuts = settings.appShortcutSearch, contacts = settings.contactsSearch, calendars = settings.calendarSearch, - files = settings.fileSearch, ).collectLatest { if (searchQuery != query) return@collectLatest items = withContext(Dispatchers.Default) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index a27de003..ffb3dabc 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.files.settings.FileSearchSettings import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager @@ -47,6 +48,7 @@ class SearchVM : ViewModel(), KoinComponent { private val searchableRepository: SavableSearchableRepository by inject() private val permissionsManager: PermissionsManager by inject() private val dataStore: LauncherDataStore by inject() + private val fileSearchSettings: FileSearchSettings by inject() val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter } .stateIn(viewModelScope, SharingStarted.Eagerly, false) @@ -122,7 +124,6 @@ class SearchVM : ViewModel(), KoinComponent { unitConverter = settings.unitConverterSearch, calendars = settings.calendarSearch, contacts = settings.contactsSearch, - files = settings.fileSearch, shortcuts = settings.appShortcutSearch, websites = settings.websiteSearch, wikipedia = settings.wikipediaSearch, @@ -293,7 +294,7 @@ class SearchVM : ViewModel(), KoinComponent { val missingFilesPermission = combine( permissionsManager.hasPermission(PermissionGroup.ExternalStorage), - dataStore.data.map { it.fileSearch.localFiles }.distinctUntilChanged() + fileSearchSettings.localFiles.distinctUntilChanged() ) { perm, enabled -> !perm && enabled } fun requestFilesPermission(context: AppCompatActivity) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt index af6c07d2..a1181d9a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreenVM.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import de.mm20.launcher2.accounts.Account import de.mm20.launcher2.accounts.AccountType import de.mm20.launcher2.accounts.AccountsRepository +import de.mm20.launcher2.files.settings.FileSearchSettings import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore @@ -18,7 +19,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject class FileSearchSettingsScreenVM : ViewModel(), KoinComponent { - private val dataStore: LauncherDataStore by inject() + private val fileSearchSettings: FileSearchSettings by inject() private val accountsRepository: AccountsRepository by inject() private val permissionsManager: PermissionsManager by inject() @@ -43,84 +44,28 @@ class FileSearchSettingsScreenVM : ViewModel(), KoinComponent { } } - val localFiles = dataStore.data.map { it.fileSearch.localFiles } + val localFiles = fileSearchSettings.localFiles .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) fun setLocalFiles(localFiles: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setFileSearch( - it.fileSearch - .toBuilder() - .setLocalFiles(localFiles) - ) - .build() - } - } + fileSearchSettings.setLocalFiles(localFiles) } - val nextcloud = dataStore.data.map { it.fileSearch.nextcloud } + val nextcloud = fileSearchSettings.nextcloudFiles .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) fun setNextcloud(nextcloud: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setFileSearch( - it.fileSearch - .toBuilder() - .setNextcloud(nextcloud) - ) - .build() - } - } + fileSearchSettings.setNextcloudFiles(nextcloud) } - val gdrive = dataStore.data.map { it.fileSearch.gdrive } + val gdrive = fileSearchSettings.gdriveFiles .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) fun setGdrive(gdrive: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setFileSearch( - it.fileSearch - .toBuilder() - .setGdrive(gdrive) - ) - .build() - } - } + fileSearchSettings.setGdriveFiles(gdrive) } - val onedrive = dataStore.data.map { it.fileSearch.onedrive } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - fun setOneDrive(onedrive: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setFileSearch( - it.fileSearch - .toBuilder() - .setOnedrive(onedrive) - ) - .build() - } - } - } - - val owncloud = dataStore.data.map { it.fileSearch.owncloud } + val owncloud = fileSearchSettings.owncloudFiles .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) fun setOwncloud(owncloud: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setFileSearch( - it.fileSearch - .toBuilder() - .setOwncloud(owncloud) - ) - .build() - } - } + fileSearchSettings.setOwncloudFiles(owncloud) } fun requestFilePermission(context: AppCompatActivity) { diff --git a/core/preferences/src/main/proto/settings.proto b/core/preferences/src/main/proto/settings.proto index 0d3e6439..dae3f4a0 100644 --- a/core/preferences/src/main/proto/settings.proto +++ b/core/preferences/src/main/proto/settings.proto @@ -169,7 +169,7 @@ message Settings { bool nextcloud = 4; bool owncloud = 5; } - FilesSearchSettings file_search = 9; + FilesSearchSettings file_search = 9 [deprecated = true]; message ContactsSearchSettings { bool enabled = 1; diff --git a/data/files/build.gradle.kts b/data/files/build.gradle.kts index 6c987e1e..0aa2590b 100644 --- a/data/files/build.gradle.kts +++ b/data/files/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.plugin.serialization) } android { @@ -35,9 +36,11 @@ android { dependencies { implementation(libs.bundles.kotlin) + implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.core) implementation(libs.androidx.appcompat) implementation(libs.androidx.exifinterface) + implementation(libs.androidx.datastore) implementation(libs.bundles.androidx.lifecycle) diff --git a/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt b/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt index a7265c98..0aa8006f 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt @@ -7,6 +7,7 @@ import de.mm20.launcher2.files.providers.LocalFileProvider import de.mm20.launcher2.files.providers.NextcloudFileProvider import de.mm20.launcher2.files.providers.OwncloudFileProvider import de.mm20.launcher2.files.providers.PluginFileProvider +import de.mm20.launcher2.files.settings.FileSearchSettings import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.permissions.PermissionsManager @@ -28,7 +29,7 @@ import kotlinx.coroutines.flow.map internal class FileRepository( private val context: Context, private val permissionsManager: PermissionsManager, - private val dataStore: LauncherDataStore, + private val settings: FileSearchSettings, private val pluginRepository: PluginRepository, ) : SearchableRepository { @@ -52,8 +53,7 @@ internal class FileRepository( enabled = true, ) - dataStore.data.map { it.fileSearch } - .combine(filePlugins) { settings, plugins -> + settings.data.combine(filePlugins) { settings, plugins -> settings to plugins }.collectLatest { (settings, plugins) -> val providers = mutableListOf() @@ -64,9 +64,9 @@ internal class FileRepository( permissionsManager ) ) - if (settings.gdrive) providers.add(GDriveFileProvider(context)) - if (settings.nextcloud) providers.add(NextcloudFileProvider(nextcloudClient)) - if (settings.owncloud) providers.add(OwncloudFileProvider(owncloudClient)) + if (settings.gdriveFiles) providers.add(GDriveFileProvider(context)) + if (settings.nextcloudFiles) providers.add(NextcloudFileProvider(nextcloudClient)) + if (settings.owncloudFiles) providers.add(OwncloudFileProvider(owncloudClient)) for (plugin in plugins) { providers.add(PluginFileProvider(context, plugin)) diff --git a/data/files/src/main/java/de/mm20/launcher2/files/Module.kt b/data/files/src/main/java/de/mm20/launcher2/files/Module.kt index 1c0526eb..8b86a310 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/Module.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/Module.kt @@ -1,11 +1,13 @@ package de.mm20.launcher2.files +import de.mm20.launcher2.backup.Backupable import de.mm20.launcher2.files.providers.GDriveFile import de.mm20.launcher2.files.providers.LocalFile import de.mm20.launcher2.files.providers.NextcloudFile import de.mm20.launcher2.files.providers.OneDriveFile import de.mm20.launcher2.files.providers.OwncloudFile import de.mm20.launcher2.files.providers.PluginFile +import de.mm20.launcher2.files.settings.FileSearchSettings import de.mm20.launcher2.search.File import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableRepository @@ -14,11 +16,25 @@ import org.koin.core.qualifier.named import org.koin.dsl.module val filesModule = module { - factory>(named()) { FileRepository(androidContext(), get(), get(), get()) } + factory>(named()) { + FileRepository( + androidContext(), + get(), + get(), + get() + ) + } factory(named(LocalFile.Domain)) { LocalFileDeserializer(androidContext()) } factory(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() } factory(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() } factory(named(OneDriveFile.Domain)) { OneDriveFileDeserializer() } factory(named(GDriveFile.Domain)) { GDriveFileDeserializer() } - factory(named(PluginFile.Domain)) { PluginFileDeserializer(androidContext(), get()) } + factory(named(PluginFile.Domain)) { + PluginFileDeserializer( + androidContext(), + get() + ) + } + single { FileSearchSettings(androidContext(), get()) } + factory(named()) { get() } } \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettings.kt b/data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettings.kt new file mode 100644 index 00000000..e631a283 --- /dev/null +++ b/data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettings.kt @@ -0,0 +1,112 @@ +package de.mm20.launcher2.files.settings + +import android.content.Context +import androidx.datastore.dataStore +import de.mm20.launcher2.backup.Backupable +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.files.settings.migrations.Migration1 +import de.mm20.launcher2.preferences.LauncherDataStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import java.io.File + +class FileSearchSettings( + private val context: Context, + private val dataStore: LauncherDataStore, +) : Backupable { + + private val scope = CoroutineScope(Job() + Dispatchers.Default) + + private val Context.dataStore by dataStore( + fileName = "file_search.json", + serializer = FileSearchSettingsDataSerializer, + produceMigrations = { + listOf( + Migration1(dataStore), + ) + }, + ) + + private fun updateData(block: suspend (FileSearchSettingsData) -> FileSearchSettingsData) { + scope.launch { + context.dataStore.updateData(block) + } + } + + internal val data + get() = context.dataStore.data + + val localFiles + get(): Flow { + return context.dataStore.data.map { it.localFiles } + } + + fun setLocalFiles(localFiles: Boolean) { + updateData { + it.copy(localFiles = localFiles) + } + } + + val gdriveFiles + get(): Flow { + return context.dataStore.data.map { it.gdriveFiles } + } + + fun setGdriveFiles(gdriveFiles: Boolean) { + updateData { + it.copy(gdriveFiles = gdriveFiles) + } + } + + val nextcloudFiles + get(): Flow { + return context.dataStore.data.map { it.nextcloudFiles } + } + + fun setNextcloudFiles(nextcloudFiles: Boolean) { + updateData { + it.copy(nextcloudFiles = nextcloudFiles) + } + } + + val owncloudFiles + get(): Flow { + return context.dataStore.data.map { it.owncloudFiles } + } + + fun setOwncloudFiles(owncloudFiles: Boolean) { + updateData { + it.copy(owncloudFiles = owncloudFiles) + } + } + + override suspend fun backup(toDir: File) { + val data = context.dataStore.data.first() + val file = File(toDir, "file_search.json") + file.writeText(FileSearchSettingsDataSerializer.json.encodeToString(FileSearchSettingsData.serializer(), data)) + } + + override suspend fun restore(fromDir: File) { + val file = File(fromDir, "file_search.json") + if (!file.exists()) { + return + } + try { + val data = FileSearchSettingsDataSerializer.json.decodeFromString(FileSearchSettingsData.serializer(), file.readText()) + context.dataStore.updateData { + data + } + } catch (e: SerializationException) { + CrashReporter.logException(e) + } catch (e: IllegalArgumentException) { + CrashReporter.logException(e) + } + } +} \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettingsData.kt b/data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettingsData.kt new file mode 100644 index 00000000..a538309c --- /dev/null +++ b/data/files/src/main/java/de/mm20/launcher2/files/settings/FileSearchSettingsData.kt @@ -0,0 +1,49 @@ +package de.mm20.launcher2.files.settings + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +@Serializable +internal data class FileSearchSettingsData( + val localFiles: Boolean = true, + val gdriveFiles: Boolean = false, + val nextcloudFiles: Boolean = false, + val owncloudFiles: Boolean = false, + val plugins: Set = emptySet(), + val schemaVersion: Int = 1, +) + +internal object FileSearchSettingsDataSerializer : Serializer { + + internal val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + } + + override val defaultValue: FileSearchSettingsData + get() = FileSearchSettingsData(schemaVersion = 0) + + override suspend fun readFrom(input: InputStream): FileSearchSettingsData { + try { + return json.decodeFromStream(input) + } catch (e: IllegalArgumentException) { + throw (CorruptionException("Cannot read json.", e)) + } catch (e: SerializationException) { + throw (CorruptionException("Cannot read json.", e)) + } catch (e: IOException) { + throw (CorruptionException("Cannot read json.", e)) + } + } + + override suspend fun writeTo(t: FileSearchSettingsData, output: OutputStream) { + json.encodeToStream(t, output) + } +} \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/files/settings/migrations/Migration1.kt b/data/files/src/main/java/de/mm20/launcher2/files/settings/migrations/Migration1.kt new file mode 100644 index 00000000..c9911dbf --- /dev/null +++ b/data/files/src/main/java/de/mm20/launcher2/files/settings/migrations/Migration1.kt @@ -0,0 +1,33 @@ +package de.mm20.launcher2.files.settings.migrations + +import androidx.datastore.core.DataMigration +import de.mm20.launcher2.files.settings.FileSearchSettingsData +import de.mm20.launcher2.preferences.LauncherDataStore +import kotlinx.coroutines.flow.first + +/** + * This migration is used to migrate the data from the old proto data store. + * TODO: remove after a few releases + */ +internal class Migration1( + private val dataStore: LauncherDataStore, +): DataMigration { + override suspend fun cleanUp() { + + } + + override suspend fun shouldMigrate(currentData: FileSearchSettingsData): Boolean { + return currentData.schemaVersion < 1 + } + + override suspend fun migrate(currentData: FileSearchSettingsData): FileSearchSettingsData { + val data = dataStore.data.first().fileSearch + return currentData.copy( + localFiles = data.localFiles, + gdriveFiles = data.gdrive, + nextcloudFiles = data.nextcloud, + owncloudFiles = data.owncloud, + schemaVersion = 1, + ) + } +} \ No newline at end of file diff --git a/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt b/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt index 15366fb7..62ee3e05 100644 --- a/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt +++ b/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt @@ -41,13 +41,6 @@ interface SearchService { calendars: CalendarSearchSettings = Settings.CalendarSearchSettings.newBuilder() .setEnabled(false) .build(), - files: FilesSearchSettings = Settings.FilesSearchSettings.newBuilder() - .setLocalFiles(false) - .setGdrive(false) - .setOnedrive(false) - .setOwncloud(false) - .setNextcloud(false) - .build(), calculator: CalculatorSearchSettings = Settings.CalculatorSearchSettings.newBuilder() .setEnabled(false) .build(), @@ -82,7 +75,6 @@ internal class SearchServiceImpl( shortcuts: AppShortcutSearchSettings, contacts: ContactsSearchSettings, calendars: CalendarSearchSettings, - files: FilesSearchSettings, calculator: CalculatorSearchSettings, unitConverter: UnitConverterSearchSettings, websites: WebsiteSearchSettings, @@ -184,18 +176,16 @@ internal class SearchServiceImpl( } } } - if (files.localFiles || files.owncloud || files.onedrive || files.gdrive || files.nextcloud) { - launch { - fileRepository.search( - query, - ) - .withCustomLabels(customAttributesRepository) - .collectLatest { r -> - results.update { - it.copy(files = r.toImmutableList()) - } + launch { + fileRepository.search( + query, + ) + .withCustomLabels(customAttributesRepository) + .collectLatest { r -> + results.update { + it.copy(files = r.toImmutableList()) } - } + } } launch { customAttributesRepository.search(query)