Move badge settings to decentralized data store, add preference for plugin badges

This commit is contained in:
MM20 2023-12-07 22:44:41 +01:00
parent 0f6d636ca0
commit 2479fdbc03
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
15 changed files with 268 additions and 95 deletions

View File

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

View File

@ -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<List<LauncherIcon?>> {
@ -247,6 +218,7 @@ class IconsSettingsScreenVM(
iconService = get(),
permissionsManager = get(),
favoritesService = get(),
badgeSettings = get(),
)
}
}

View File

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

View File

@ -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<T>(
internal val context: Context,
private val fileName: String,
private val serializer: Serializer<T>,
migrations: List<DataMigration<T>>
): 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)
}
}
}

View File

@ -552,6 +552,8 @@
<string name="preference_cloud_badges_summary">Show a badge for files that are stored in a cloud</string>
<string name="preference_shortcut_badges">Shortcut badges</string>
<string name="preference_shortcut_badges_summary">Show a badge which indicates to which app a shortcut belongs</string>
<string name="preference_plugin_badges">Plugin badges</string>
<string name="preference_plugin_badges_summary">Indicate by which plugin a search result was created</string>
<string name="preference_microsoft">Microsoft</string>
<string name="preference_ms_signin">Sign in with Microsoft</string>
<string name="preference_ms_signin_summary">Sign in to search OneDrive</string>

View File

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

View File

@ -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(
dataStore: LauncherDataStore,
) : BaseSettings<FileSearchSettingsData>(
context = context,
fileName = "file_search.json",
serializer = FileSearchSettingsDataSerializer,
produceMigrations = {
listOf(
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) {

View File

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

View File

@ -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"]

View File

@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
}
android {

View File

@ -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<Badge?>
}
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<List<BadgeProvider>>(emptyList())
init {
scope.launch {
dataStore.data.map { it.badges }.distinctUntilChanged().collectLatest {
settings.data.distinctUntilChanged().collectLatest {
val providers = mutableListOf<BadgeProvider>()
providers += WorkProfileBadgeProvider()
if (it.notifications) {
@ -37,7 +39,9 @@ internal class BadgeServiceImpl(private val context: Context) : BadgeService, Ko
if (it.suspendedApps) {
providers += SuspendedAppsBadgeProvider()
}
if (it.plugins) {
providers += PluginBadgeProvider(context)
}
badgeProviders.value = providers
}
}

View File

@ -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<BadgeService> { BadgeServiceImpl(androidContext()) }
single<BadgeService> { BadgeServiceImpl(androidContext(), get()) }
single<BadgeSettings> { BadgeSettings(androidContext(), get()) }
factory<Backupable>(named<BadgeSettings>()) { get<BadgeSettings>() }
}

View File

@ -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<BadgeSettingsData>(
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)
}
}
}

View File

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

View File

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