From 2479fdbc034bab45b18964b1f41c9149cbb2e1d0 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:44:41 +0100 Subject: [PATCH] Move badge settings to decentralized data store, add preference for plugin badges --- .../ui/settings/icons/IconsSettingsScreen.kt | 9 +++ .../settings/icons/IconsSettingsScreenVM.kt | 60 +++++----------- core/base/build.gradle.kts | 1 + .../mm20/launcher2/settings/BaseSettings.kt | 66 ++++++++++++++++++ core/i18n/src/main/res/values/strings.xml | 2 + .../preferences/src/main/proto/settings.proto | 2 +- .../files/settings/FileSearchSettings.kt | 51 +++----------- .../files/settings/FileSearchSettingsData.kt | 2 +- gradle/libs.versions.toml | 2 +- services/badges/build.gradle.kts | 1 + .../de/mm20/launcher2/badges/BadgeService.kt | 14 ++-- .../java/de/mm20/launcher2/badges/Module.kt | 7 +- .../badges/settings/BadgeSettings.kt | 68 +++++++++++++++++++ .../badges/settings/BadgeSettingsData.kt | 50 ++++++++++++++ .../badges/settings/migrations/Migration1.kt | 28 ++++++++ 15 files changed, 268 insertions(+), 95 deletions(-) create mode 100644 core/base/src/main/java/de/mm20/launcher2/settings/BaseSettings.kt create mode 100644 services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettings.kt create mode 100644 services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettingsData.kt create mode 100644 services/badges/src/main/java/de/mm20/launcher2/badges/settings/migrations/Migration1.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt index e3556601..93d8c57b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt @@ -89,6 +89,7 @@ fun IconsSettingsScreen() { val cloudFileBadges by viewModel.cloudFileBadges.collectAsStateWithLifecycle(null) val suspendedAppBadges by viewModel.suspendedAppBadges.collectAsStateWithLifecycle(null) val shortcutBadges by viewModel.shortcutBadges.collectAsStateWithLifecycle(null) + val pluginBadges by viewModel.pluginBadges.collectAsStateWithLifecycle(null) val previewIcons by remember(iconSize) { viewModel.getPreviewIcons(with(density) { iconSize.dp.toPx() }.toInt()) @@ -325,6 +326,14 @@ fun IconsSettingsScreen() { viewModel.setShortcuts(it) } ) + SwitchPreference( + title = stringResource(R.string.preference_plugin_badges), + summary = stringResource(R.string.preference_plugin_badges_summary), + value = pluginBadges == true, + onValueChanged = { + viewModel.setPluginBadges(it) + } + ) } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt index da73b8b3..1370ae54 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory +import de.mm20.launcher2.badges.settings.BadgeSettings import de.mm20.launcher2.icons.IconPack import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.LauncherIcon @@ -24,6 +25,7 @@ import org.koin.core.component.get class IconsSettingsScreenVM( private val dataStore: LauncherDataStore, + private val badgeSettings: BadgeSettings, private val iconService: IconService, private val favoritesService: FavoritesService, private val permissionsManager: PermissionsManager, @@ -161,64 +163,33 @@ class IconsSettingsScreenVM( val hasNotificationsPermission = permissionsManager.hasPermission(PermissionGroup.Notifications) - val notificationBadges = dataStore.data.map { it.badges.notifications } + val notificationBadges = badgeSettings.notifications fun setNotifications(notifications: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setBadges( - it.badges.toBuilder() - .setNotifications(notifications) - ) - .build() - } - } + badgeSettings.setNotifications(notifications) } fun requestNotificationsPermission(context: AppCompatActivity) { permissionsManager.requestPermission(context, PermissionGroup.Notifications) } - val cloudFileBadges = dataStore.data.map { it.badges.cloudFiles } + val cloudFileBadges = badgeSettings.cloudFiles fun setCloudFiles(cloudFiles: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setBadges( - it.badges.toBuilder() - .setCloudFiles(cloudFiles) - ) - .build() - } - } + badgeSettings.setCloudFiles(cloudFiles) } - val shortcutBadges = dataStore.data.map { it.badges.shortcuts } + val shortcutBadges = badgeSettings.shortcuts fun setShortcuts(shortcuts: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setBadges( - it.badges.toBuilder() - .setShortcuts(shortcuts) - ) - .build() - } - } + badgeSettings.setShortcuts(shortcuts) } - val suspendedAppBadges = dataStore.data.map { it.badges.suspendedApps } + val suspendedAppBadges = badgeSettings.suspendedApps fun setSuspendedApps(suspendedApps: Boolean) { - viewModelScope.launch { - dataStore.updateData { - it.toBuilder() - .setBadges( - it.badges.toBuilder() - .setSuspendedApps(suspendedApps) - ) - .build() - } - } + badgeSettings.setSuspendedApps(suspendedApps) + } + + val pluginBadges = badgeSettings.plugins + fun setPluginBadges(plugins: Boolean) { + badgeSettings.setPlugins(plugins) } fun getPreviewIcons(size: Int): Flow> { @@ -247,6 +218,7 @@ class IconsSettingsScreenVM( iconService = get(), permissionsManager = get(), favoritesService = get(), + badgeSettings = get(), ) } } diff --git a/core/base/build.gradle.kts b/core/base/build.gradle.kts index 5af99272..794b1a05 100644 --- a/core/base/build.gradle.kts +++ b/core/base/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.appcompat) + implementation(libs.androidx.datastore) implementation(libs.materialcomponents.core) implementation(libs.koin.android) diff --git a/core/base/src/main/java/de/mm20/launcher2/settings/BaseSettings.kt b/core/base/src/main/java/de/mm20/launcher2/settings/BaseSettings.kt new file mode 100644 index 00000000..83de3550 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/settings/BaseSettings.kt @@ -0,0 +1,66 @@ +package de.mm20.launcher2.settings + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataMigration +import androidx.datastore.core.Serializer +import androidx.datastore.dataStore +import de.mm20.launcher2.backup.Backupable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.serialization.SerializationException +import java.io.File + +abstract class BaseSettings( + internal val context: Context, + private val fileName: String, + private val serializer: Serializer, + migrations: List> +): Backupable { + + protected val scope = CoroutineScope(Job() + Dispatchers.Default) + + protected val Context.dataStore by dataStore( + fileName = fileName, + serializer = serializer, + produceMigrations = { + migrations + }, + ) + + protected fun updateData(block: suspend (T) -> T) { + scope.launch { + context.dataStore.updateData(block) + } + } + + override suspend fun backup(toDir: File) { + val data = context.dataStore.data.first() + val file = File(toDir, fileName) + file.outputStream().use { + serializer.writeTo(data, it) + } + } + + override suspend fun restore(fromDir: File) { + val file = File(fromDir, fileName) + if (!file.exists()) { + return + } + try { + file.inputStream().use { + val data = serializer.readFrom(it) + context.dataStore.updateData { + data + } + } + } catch (e: SerializationException) { + Log.e("MM20", "Cannot restore $fileName", e) + } catch (e: IllegalArgumentException) { + Log.e("MM20", "Cannot restore $fileName", e) + } + } +} \ No newline at end of file diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 9b91bf13..e92cecb6 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -552,6 +552,8 @@ Show a badge for files that are stored in a cloud Shortcut badges Show a badge which indicates to which app a shortcut belongs + Plugin badges + Indicate by which plugin a search result was created Microsoft Sign in with Microsoft Sign in to search OneDrive diff --git a/core/preferences/src/main/proto/settings.proto b/core/preferences/src/main/proto/settings.proto index dae3f4a0..f57fc567 100644 --- a/core/preferences/src/main/proto/settings.proto +++ b/core/preferences/src/main/proto/settings.proto @@ -221,7 +221,7 @@ message Settings { bool cloud_files = 3; bool shortcuts = 4; } - BadgeSettings badges = 18; + BadgeSettings badges = 18 [deprecated = true]; message GridSettings { uint32 column_count = 1; 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 index 3a9cdd33..ec47dea2 100644 --- 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 @@ -6,6 +6,7 @@ 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 de.mm20.launcher2.settings.BaseSettings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -19,26 +20,15 @@ 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), - ) - }, + dataStore: LauncherDataStore, +) : BaseSettings( + context = context, + fileName = "file_search.json", + serializer = FileSearchSettingsDataSerializer, + migrations = listOf( + Migration1(dataStore), ) - - private fun updateData(block: suspend (FileSearchSettingsData) -> FileSearchSettingsData) { - scope.launch { - context.dataStore.updateData(block) - } - } +) { internal val data get() = context.dataStore.data @@ -98,29 +88,6 @@ class FileSearchSettings( } } - 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) - } - } - fun setPluginEnabled(authority: String, enabled: Boolean) { updateData { if (enabled) { 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 index a538309c..1a9849e6 100644 --- 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 @@ -12,7 +12,7 @@ import java.io.InputStream import java.io.OutputStream @Serializable -internal data class FileSearchSettingsData( +data class FileSearchSettingsData( val localFiles: Boolean = true, val gdriveFiles: Boolean = false, val nextcloudFiles: Boolean = false, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28d1bbbc..01a801ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -139,7 +139,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co [bundles] -kotlin = ["kotlin-stdlib", "kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-collections-immutable"] +kotlin = ["kotlin-stdlib", "kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-collections-immutable", "kotlinx-serialization-json"] androidx-lifecycle = ["androidx-lifecycle-viewmodel", "androidx-lifecycle-common", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodelcompose", "androidx-lifecycle-runtimecompose"] retrofit = ["retrofit-core", "retrofit-gson"] tests = ["junit"] diff --git a/services/badges/build.gradle.kts b/services/badges/build.gradle.kts index 682bc43a..95bc0123 100644 --- a/services/badges/build.gradle.kts +++ b/services/badges/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 { diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt index 2303e7f4..7d1fe414 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/BadgeService.kt @@ -2,6 +2,7 @@ package de.mm20.launcher2.badges import android.content.Context import de.mm20.launcher2.badges.providers.* +import de.mm20.launcher2.badges.settings.BadgeSettings import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.Searchable import kotlinx.coroutines.* @@ -13,16 +14,17 @@ interface BadgeService { fun getBadge(searchable: Searchable): Flow } -internal class BadgeServiceImpl(private val context: Context) : BadgeService, KoinComponent { +internal class BadgeServiceImpl( + private val context: Context, + private val settings: BadgeSettings, +) : BadgeService, KoinComponent { - private val dataStore: LauncherDataStore by inject() private val scope = CoroutineScope(Job() + Dispatchers.Default) - private val badgeProviders = MutableStateFlow>(emptyList()) init { scope.launch { - dataStore.data.map { it.badges }.distinctUntilChanged().collectLatest { + settings.data.distinctUntilChanged().collectLatest { val providers = mutableListOf() providers += WorkProfileBadgeProvider() if (it.notifications) { @@ -37,7 +39,9 @@ internal class BadgeServiceImpl(private val context: Context) : BadgeService, Ko if (it.suspendedApps) { providers += SuspendedAppsBadgeProvider() } - providers += PluginBadgeProvider(context) + if (it.plugins) { + providers += PluginBadgeProvider(context) + } badgeProviders.value = providers } } diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/Module.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/Module.kt index 5f354faf..cd568b4d 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/Module.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/Module.kt @@ -1,8 +1,13 @@ package de.mm20.launcher2.badges +import de.mm20.launcher2.backup.Backupable +import de.mm20.launcher2.badges.settings.BadgeSettings import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val badgesModule = module { - single { BadgeServiceImpl(androidContext()) } + single { BadgeServiceImpl(androidContext(), get()) } + single { BadgeSettings(androidContext(), get()) } + factory(named()) { get() } } \ No newline at end of file diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettings.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettings.kt new file mode 100644 index 00000000..36afa38a --- /dev/null +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettings.kt @@ -0,0 +1,68 @@ +package de.mm20.launcher2.badges.settings + +import android.content.Context +import de.mm20.launcher2.badges.settings.migrations.Migration1 +import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.settings.BaseSettings +import kotlinx.coroutines.flow.map + +class BadgeSettings( + private val context: Context, + dataStore: LauncherDataStore, +) : BaseSettings( + context = context, + fileName = "badges.json", + serializer = BadgeSettingsDataSerializer, + migrations = listOf( + Migration1(dataStore), + ) +) { + + internal val data + get() = context.dataStore.data + + val notifications + get() = context.dataStore.data.map { it.notifications } + + fun setNotifications(notifications: Boolean) { + updateData { + it.copy(notifications = notifications) + } + } + + val suspendedApps + get() = context.dataStore.data.map { it.suspendedApps } + + fun setSuspendedApps(suspendedApps: Boolean) { + updateData { + it.copy(suspendedApps = suspendedApps) + } + } + + val cloudFiles + get() = context.dataStore.data.map { it.cloudFiles } + + fun setCloudFiles(cloudFiles: Boolean) { + updateData { + it.copy(cloudFiles = cloudFiles) + } + } + + val shortcuts + get() = context.dataStore.data.map { it.shortcuts } + + fun setShortcuts(shortcuts: Boolean) { + updateData { + it.copy(shortcuts = shortcuts) + } + } + + val plugins + get() = context.dataStore.data.map { it.plugins } + + fun setPlugins(plugins: Boolean) { + updateData { + it.copy(plugins = plugins) + } + } +} diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettingsData.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettingsData.kt new file mode 100644 index 00000000..31f0ed67 --- /dev/null +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/settings/BadgeSettingsData.kt @@ -0,0 +1,50 @@ +package de.mm20.launcher2.badges.settings + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import de.mm20.launcher2.files.settings.FileSearchSettingsData +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 +data class BadgeSettingsData( + val notifications: Boolean = true, + val suspendedApps: Boolean = true, + val cloudFiles: Boolean = true, + val shortcuts: Boolean = true, + val plugins: Boolean = true, + val schemaVersion: Int = 1, +) + +internal object BadgeSettingsDataSerializer : Serializer { + + private val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + } + + override val defaultValue: BadgeSettingsData + get() = BadgeSettingsData(schemaVersion = 0) + + override suspend fun readFrom(input: InputStream): BadgeSettingsData { + 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: BadgeSettingsData, output: OutputStream) { + json.encodeToStream(t, output) + } +} \ No newline at end of file diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/settings/migrations/Migration1.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/settings/migrations/Migration1.kt new file mode 100644 index 00000000..1c8959b0 --- /dev/null +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/settings/migrations/Migration1.kt @@ -0,0 +1,28 @@ +package de.mm20.launcher2.badges.settings.migrations + +import androidx.datastore.core.DataMigration +import de.mm20.launcher2.badges.settings.BadgeSettingsData +import de.mm20.launcher2.preferences.LauncherDataStore +import kotlinx.coroutines.flow.first + +class Migration1( + private val dataStore: LauncherDataStore, +): DataMigration { + override suspend fun cleanUp() { + } + + override suspend fun shouldMigrate(currentData: BadgeSettingsData): Boolean { + return currentData.schemaVersion < 1 + } + + override suspend fun migrate(currentData: BadgeSettingsData): BadgeSettingsData { + val data = dataStore.data.first().badges + return currentData.copy( + notifications = data.notifications, + suspendedApps = data.suspendedApps, + cloudFiles = data.cloudFiles, + shortcuts = data.shortcuts, + schemaVersion = 1, + ) + } +} \ No newline at end of file