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, shortcuts = settings.appShortcutSearch,
contacts = settings.contactsSearch, contacts = settings.contactsSearch,
calendars = settings.calendarSearch, calendars = settings.calendarSearch,
files = settings.fileSearch,
).collectLatest { ).collectLatest {
if (searchQuery != query) return@collectLatest if (searchQuery != query) return@collectLatest
items = withContext(Dispatchers.Default) { items = withContext(Dispatchers.Default) {

View File

@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.files.settings.FileSearchSettings
import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
@ -47,6 +48,7 @@ class SearchVM : ViewModel(), KoinComponent {
private val searchableRepository: SavableSearchableRepository by inject() private val searchableRepository: SavableSearchableRepository by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val fileSearchSettings: FileSearchSettings by inject()
val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter } val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }
.stateIn(viewModelScope, SharingStarted.Eagerly, false) .stateIn(viewModelScope, SharingStarted.Eagerly, false)
@ -122,7 +124,6 @@ class SearchVM : ViewModel(), KoinComponent {
unitConverter = settings.unitConverterSearch, unitConverter = settings.unitConverterSearch,
calendars = settings.calendarSearch, calendars = settings.calendarSearch,
contacts = settings.contactsSearch, contacts = settings.contactsSearch,
files = settings.fileSearch,
shortcuts = settings.appShortcutSearch, shortcuts = settings.appShortcutSearch,
websites = settings.websiteSearch, websites = settings.websiteSearch,
wikipedia = settings.wikipediaSearch, wikipedia = settings.wikipediaSearch,
@ -293,7 +294,7 @@ class SearchVM : ViewModel(), KoinComponent {
val missingFilesPermission = combine( val missingFilesPermission = combine(
permissionsManager.hasPermission(PermissionGroup.ExternalStorage), permissionsManager.hasPermission(PermissionGroup.ExternalStorage),
dataStore.data.map { it.fileSearch.localFiles }.distinctUntilChanged() fileSearchSettings.localFiles.distinctUntilChanged()
) { perm, enabled -> !perm && enabled } ) { perm, enabled -> !perm && enabled }
fun requestFilesPermission(context: AppCompatActivity) { 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.Account
import de.mm20.launcher2.accounts.AccountType import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.accounts.AccountsRepository import de.mm20.launcher2.accounts.AccountsRepository
import de.mm20.launcher2.files.settings.FileSearchSettings
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
@ -18,7 +19,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
class FileSearchSettingsScreenVM : ViewModel(), KoinComponent { class FileSearchSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val fileSearchSettings: FileSearchSettings by inject()
private val accountsRepository: AccountsRepository by inject() private val accountsRepository: AccountsRepository by inject()
private val permissionsManager: PermissionsManager 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) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setLocalFiles(localFiles: Boolean) { fun setLocalFiles(localFiles: Boolean) {
viewModelScope.launch { fileSearchSettings.setLocalFiles(localFiles)
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setLocalFiles(localFiles)
)
.build()
}
}
} }
val nextcloud = dataStore.data.map { it.fileSearch.nextcloud } val nextcloud = fileSearchSettings.nextcloudFiles
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setNextcloud(nextcloud: Boolean) { fun setNextcloud(nextcloud: Boolean) {
viewModelScope.launch { fileSearchSettings.setNextcloudFiles(nextcloud)
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setNextcloud(nextcloud)
)
.build()
}
}
} }
val gdrive = dataStore.data.map { it.fileSearch.gdrive } val gdrive = fileSearchSettings.gdriveFiles
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setGdrive(gdrive: Boolean) { fun setGdrive(gdrive: Boolean) {
viewModelScope.launch { fileSearchSettings.setGdriveFiles(gdrive)
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setGdrive(gdrive)
)
.build()
}
}
} }
val onedrive = dataStore.data.map { it.fileSearch.onedrive } val owncloud = fileSearchSettings.owncloudFiles
.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 }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setOwncloud(owncloud: Boolean) { fun setOwncloud(owncloud: Boolean) {
viewModelScope.launch { fileSearchSettings.setOwncloudFiles(owncloud)
dataStore.updateData {
it.toBuilder()
.setFileSearch(
it.fileSearch
.toBuilder()
.setOwncloud(owncloud)
)
.build()
}
}
} }
fun requestFilePermission(context: AppCompatActivity) { fun requestFilePermission(context: AppCompatActivity) {

View File

@ -169,7 +169,7 @@ message Settings {
bool nextcloud = 4; bool nextcloud = 4;
bool owncloud = 5; bool owncloud = 5;
} }
FilesSearchSettings file_search = 9; FilesSearchSettings file_search = 9 [deprecated = true];
message ContactsSearchSettings { message ContactsSearchSettings {
bool enabled = 1; bool enabled = 1;

View File

@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
} }
android { android {
@ -35,9 +36,11 @@ android {
dependencies { dependencies {
implementation(libs.bundles.kotlin) implementation(libs.bundles.kotlin)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.exifinterface) implementation(libs.androidx.exifinterface)
implementation(libs.androidx.datastore)
implementation(libs.bundles.androidx.lifecycle) 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.NextcloudFileProvider
import de.mm20.launcher2.files.providers.OwncloudFileProvider import de.mm20.launcher2.files.providers.OwncloudFileProvider
import de.mm20.launcher2.files.providers.PluginFileProvider import de.mm20.launcher2.files.providers.PluginFileProvider
import de.mm20.launcher2.files.settings.FileSearchSettings
import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
@ -28,7 +29,7 @@ import kotlinx.coroutines.flow.map
internal class FileRepository( internal class FileRepository(
private val context: Context, private val context: Context,
private val permissionsManager: PermissionsManager, private val permissionsManager: PermissionsManager,
private val dataStore: LauncherDataStore, private val settings: FileSearchSettings,
private val pluginRepository: PluginRepository, private val pluginRepository: PluginRepository,
) : SearchableRepository<File> { ) : SearchableRepository<File> {
@ -52,8 +53,7 @@ internal class FileRepository(
enabled = true, enabled = true,
) )
dataStore.data.map { it.fileSearch } settings.data.combine(filePlugins) { settings, plugins ->
.combine(filePlugins) { settings, plugins ->
settings to plugins settings to plugins
}.collectLatest { (settings, plugins) -> }.collectLatest { (settings, plugins) ->
val providers = mutableListOf<FileProvider>() val providers = mutableListOf<FileProvider>()
@ -64,9 +64,9 @@ internal class FileRepository(
permissionsManager permissionsManager
) )
) )
if (settings.gdrive) providers.add(GDriveFileProvider(context)) if (settings.gdriveFiles) providers.add(GDriveFileProvider(context))
if (settings.nextcloud) providers.add(NextcloudFileProvider(nextcloudClient)) if (settings.nextcloudFiles) providers.add(NextcloudFileProvider(nextcloudClient))
if (settings.owncloud) providers.add(OwncloudFileProvider(owncloudClient)) if (settings.owncloudFiles) providers.add(OwncloudFileProvider(owncloudClient))
for (plugin in plugins) { for (plugin in plugins) {
providers.add(PluginFileProvider(context, plugin)) providers.add(PluginFileProvider(context, plugin))

View File

@ -1,11 +1,13 @@
package de.mm20.launcher2.files package de.mm20.launcher2.files
import de.mm20.launcher2.backup.Backupable
import de.mm20.launcher2.files.providers.GDriveFile import de.mm20.launcher2.files.providers.GDriveFile
import de.mm20.launcher2.files.providers.LocalFile import de.mm20.launcher2.files.providers.LocalFile
import de.mm20.launcher2.files.providers.NextcloudFile import de.mm20.launcher2.files.providers.NextcloudFile
import de.mm20.launcher2.files.providers.OneDriveFile import de.mm20.launcher2.files.providers.OneDriveFile
import de.mm20.launcher2.files.providers.OwncloudFile import de.mm20.launcher2.files.providers.OwncloudFile
import de.mm20.launcher2.files.providers.PluginFile 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.File
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableRepository import de.mm20.launcher2.search.SearchableRepository
@ -14,11 +16,25 @@ import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val filesModule = 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(LocalFile.Domain)) { LocalFileDeserializer(androidContext()) }
factory<SearchableDeserializer>(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() } factory<SearchableDeserializer>(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() }
factory<SearchableDeserializer>(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() } factory<SearchableDeserializer>(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() }
factory<SearchableDeserializer>(named(OneDriveFile.Domain)) { OneDriveFileDeserializer() } factory<SearchableDeserializer>(named(OneDriveFile.Domain)) { OneDriveFileDeserializer() }
factory<SearchableDeserializer>(named(GDriveFile.Domain)) { GDriveFileDeserializer() } 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() calendars: CalendarSearchSettings = Settings.CalendarSearchSettings.newBuilder()
.setEnabled(false) .setEnabled(false)
.build(), .build(),
files: FilesSearchSettings = Settings.FilesSearchSettings.newBuilder()
.setLocalFiles(false)
.setGdrive(false)
.setOnedrive(false)
.setOwncloud(false)
.setNextcloud(false)
.build(),
calculator: CalculatorSearchSettings = Settings.CalculatorSearchSettings.newBuilder() calculator: CalculatorSearchSettings = Settings.CalculatorSearchSettings.newBuilder()
.setEnabled(false) .setEnabled(false)
.build(), .build(),
@ -82,7 +75,6 @@ internal class SearchServiceImpl(
shortcuts: AppShortcutSearchSettings, shortcuts: AppShortcutSearchSettings,
contacts: ContactsSearchSettings, contacts: ContactsSearchSettings,
calendars: CalendarSearchSettings, calendars: CalendarSearchSettings,
files: FilesSearchSettings,
calculator: CalculatorSearchSettings, calculator: CalculatorSearchSettings,
unitConverter: UnitConverterSearchSettings, unitConverter: UnitConverterSearchSettings,
websites: WebsiteSearchSettings, websites: WebsiteSearchSettings,
@ -184,18 +176,16 @@ internal class SearchServiceImpl(
} }
} }
} }
if (files.localFiles || files.owncloud || files.onedrive || files.gdrive || files.nextcloud) { launch {
launch { fileRepository.search(
fileRepository.search( query,
query, )
) .withCustomLabels(customAttributesRepository)
.withCustomLabels(customAttributesRepository) .collectLatest { r ->
.collectLatest { r -> results.update {
results.update { it.copy(files = r.toImmutableList())
it.copy(files = r.toImmutableList())
}
} }
} }
} }
launch { launch {
customAttributesRepository.search(query) customAttributesRepository.search(query)