Migrate file search settings to decentralized json data store

This commit is contained in:
MM20 2023-11-25 18:54:27 +01:00
parent 25cd9b707e
commit 966d43d4d1
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
11 changed files with 244 additions and 96 deletions

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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;

View File

@ -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)

View File

@ -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<File> {
@ -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<FileProvider>()
@ -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))

View File

@ -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<SearchableRepository<File>>(named<File>()) { FileRepository(androidContext(), get(), get(), get()) }
factory<SearchableRepository<File>>(named<File>()) {
FileRepository(
androidContext(),
get(),
get(),
get()
)
}
factory<SearchableDeserializer>(named(LocalFile.Domain)) { LocalFileDeserializer(androidContext()) }
factory<SearchableDeserializer>(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() }
factory<SearchableDeserializer>(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() }
factory<SearchableDeserializer>(named(OneDriveFile.Domain)) { OneDriveFileDeserializer() }
factory<SearchableDeserializer>(named(GDriveFile.Domain)) { GDriveFileDeserializer() }
factory<SearchableDeserializer>(named(PluginFile.Domain)) { PluginFileDeserializer(androidContext(), get()) }
factory<SearchableDeserializer>(named(PluginFile.Domain)) {
PluginFileDeserializer(
androidContext(),
get()
)
}
single<FileSearchSettings> { FileSearchSettings(androidContext(), get()) }
factory<Backupable>(named<FileSearchSettings>()) { get<FileSearchSettings>() }
}

View File

@ -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<Boolean> {
return context.dataStore.data.map { it.localFiles }
}
fun setLocalFiles(localFiles: Boolean) {
updateData {
it.copy(localFiles = localFiles)
}
}
val gdriveFiles
get(): Flow<Boolean> {
return context.dataStore.data.map { it.gdriveFiles }
}
fun setGdriveFiles(gdriveFiles: Boolean) {
updateData {
it.copy(gdriveFiles = gdriveFiles)
}
}
val nextcloudFiles
get(): Flow<Boolean> {
return context.dataStore.data.map { it.nextcloudFiles }
}
fun setNextcloudFiles(nextcloudFiles: Boolean) {
updateData {
it.copy(nextcloudFiles = nextcloudFiles)
}
}
val owncloudFiles
get(): Flow<Boolean> {
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)
}
}
}

View File

@ -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<String> = emptySet(),
val schemaVersion: Int = 1,
)
internal object FileSearchSettingsDataSerializer : Serializer<FileSearchSettingsData> {
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)
}
}

View File

@ -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<FileSearchSettingsData> {
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,
)
}
}

View File

@ -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)