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 cloudFileBadges by viewModel.cloudFileBadges.collectAsStateWithLifecycle(null)
val suspendedAppBadges by viewModel.suspendedAppBadges.collectAsStateWithLifecycle(null) val suspendedAppBadges by viewModel.suspendedAppBadges.collectAsStateWithLifecycle(null)
val shortcutBadges by viewModel.shortcutBadges.collectAsStateWithLifecycle(null) val shortcutBadges by viewModel.shortcutBadges.collectAsStateWithLifecycle(null)
val pluginBadges by viewModel.pluginBadges.collectAsStateWithLifecycle(null)
val previewIcons by remember(iconSize) { val previewIcons by remember(iconSize) {
viewModel.getPreviewIcons(with(density) { iconSize.dp.toPx() }.toInt()) viewModel.getPreviewIcons(with(density) { iconSize.dp.toPx() }.toInt())
@ -325,6 +326,14 @@ fun IconsSettingsScreen() {
viewModel.setShortcuts(it) 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.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import de.mm20.launcher2.badges.settings.BadgeSettings
import de.mm20.launcher2.icons.IconPack import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
@ -24,6 +25,7 @@ import org.koin.core.component.get
class IconsSettingsScreenVM( class IconsSettingsScreenVM(
private val dataStore: LauncherDataStore, private val dataStore: LauncherDataStore,
private val badgeSettings: BadgeSettings,
private val iconService: IconService, private val iconService: IconService,
private val favoritesService: FavoritesService, private val favoritesService: FavoritesService,
private val permissionsManager: PermissionsManager, private val permissionsManager: PermissionsManager,
@ -161,64 +163,33 @@ class IconsSettingsScreenVM(
val hasNotificationsPermission = permissionsManager.hasPermission(PermissionGroup.Notifications) val hasNotificationsPermission = permissionsManager.hasPermission(PermissionGroup.Notifications)
val notificationBadges = dataStore.data.map { it.badges.notifications } val notificationBadges = badgeSettings.notifications
fun setNotifications(notifications: Boolean) { fun setNotifications(notifications: Boolean) {
viewModelScope.launch { badgeSettings.setNotifications(notifications)
dataStore.updateData {
it.toBuilder()
.setBadges(
it.badges.toBuilder()
.setNotifications(notifications)
)
.build()
}
}
} }
fun requestNotificationsPermission(context: AppCompatActivity) { fun requestNotificationsPermission(context: AppCompatActivity) {
permissionsManager.requestPermission(context, PermissionGroup.Notifications) permissionsManager.requestPermission(context, PermissionGroup.Notifications)
} }
val cloudFileBadges = dataStore.data.map { it.badges.cloudFiles } val cloudFileBadges = badgeSettings.cloudFiles
fun setCloudFiles(cloudFiles: Boolean) { fun setCloudFiles(cloudFiles: Boolean) {
viewModelScope.launch { badgeSettings.setCloudFiles(cloudFiles)
dataStore.updateData {
it.toBuilder()
.setBadges(
it.badges.toBuilder()
.setCloudFiles(cloudFiles)
)
.build()
}
}
} }
val shortcutBadges = dataStore.data.map { it.badges.shortcuts } val shortcutBadges = badgeSettings.shortcuts
fun setShortcuts(shortcuts: Boolean) { fun setShortcuts(shortcuts: Boolean) {
viewModelScope.launch { badgeSettings.setShortcuts(shortcuts)
dataStore.updateData {
it.toBuilder()
.setBadges(
it.badges.toBuilder()
.setShortcuts(shortcuts)
)
.build()
}
}
} }
val suspendedAppBadges = dataStore.data.map { it.badges.suspendedApps } val suspendedAppBadges = badgeSettings.suspendedApps
fun setSuspendedApps(suspendedApps: Boolean) { fun setSuspendedApps(suspendedApps: Boolean) {
viewModelScope.launch { badgeSettings.setSuspendedApps(suspendedApps)
dataStore.updateData { }
it.toBuilder()
.setBadges( val pluginBadges = badgeSettings.plugins
it.badges.toBuilder() fun setPluginBadges(plugins: Boolean) {
.setSuspendedApps(suspendedApps) badgeSettings.setPlugins(plugins)
)
.build()
}
}
} }
fun getPreviewIcons(size: Int): Flow<List<LauncherIcon?>> { fun getPreviewIcons(size: Int): Flow<List<LauncherIcon?>> {
@ -247,6 +218,7 @@ class IconsSettingsScreenVM(
iconService = get(), iconService = get(),
permissionsManager = get(), permissionsManager = get(),
favoritesService = get(), favoritesService = get(),
badgeSettings = get(),
) )
} }
} }

View File

@ -42,6 +42,7 @@ dependencies {
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.datastore)
implementation(libs.materialcomponents.core) implementation(libs.materialcomponents.core)
implementation(libs.koin.android) 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_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">Shortcut badges</string>
<string name="preference_shortcut_badges_summary">Show a badge which indicates to which app a shortcut belongs</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_microsoft">Microsoft</string>
<string name="preference_ms_signin">Sign in with Microsoft</string> <string name="preference_ms_signin">Sign in with Microsoft</string>
<string name="preference_ms_signin_summary">Sign in to search OneDrive</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 cloud_files = 3;
bool shortcuts = 4; bool shortcuts = 4;
} }
BadgeSettings badges = 18; BadgeSettings badges = 18 [deprecated = true];
message GridSettings { message GridSettings {
uint32 column_count = 1; 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.crashreporter.CrashReporter
import de.mm20.launcher2.files.settings.migrations.Migration1 import de.mm20.launcher2.files.settings.migrations.Migration1
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.settings.BaseSettings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -19,26 +20,15 @@ import java.io.File
class FileSearchSettings( class FileSearchSettings(
private val context: Context, private val context: Context,
private val dataStore: LauncherDataStore, dataStore: LauncherDataStore,
) : Backupable { ) : BaseSettings<FileSearchSettingsData>(
context = context,
private val scope = CoroutineScope(Job() + Dispatchers.Default) fileName = "file_search.json",
serializer = FileSearchSettingsDataSerializer,
private val Context.dataStore by dataStore( migrations = listOf(
fileName = "file_search.json", Migration1(dataStore),
serializer = FileSearchSettingsDataSerializer,
produceMigrations = {
listOf(
Migration1(dataStore),
)
},
) )
) {
private fun updateData(block: suspend (FileSearchSettingsData) -> FileSearchSettingsData) {
scope.launch {
context.dataStore.updateData(block)
}
}
internal val data internal val data
get() = context.dataStore.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) { fun setPluginEnabled(authority: String, enabled: Boolean) {
updateData { updateData {
if (enabled) { if (enabled) {

View File

@ -12,7 +12,7 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@Serializable @Serializable
internal data class FileSearchSettingsData( data class FileSearchSettingsData(
val localFiles: Boolean = true, val localFiles: Boolean = true,
val gdriveFiles: Boolean = false, val gdriveFiles: Boolean = false,
val nextcloudFiles: Boolean = false, val nextcloudFiles: Boolean = false,

View File

@ -139,7 +139,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
[bundles] [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"] androidx-lifecycle = ["androidx-lifecycle-viewmodel", "androidx-lifecycle-common", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodelcompose", "androidx-lifecycle-runtimecompose"]
retrofit = ["retrofit-core", "retrofit-gson"] retrofit = ["retrofit-core", "retrofit-gson"]
tests = ["junit"] tests = ["junit"]

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 {

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.badges
import android.content.Context import android.content.Context
import de.mm20.launcher2.badges.providers.* import de.mm20.launcher2.badges.providers.*
import de.mm20.launcher2.badges.settings.BadgeSettings
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -13,16 +14,17 @@ interface BadgeService {
fun getBadge(searchable: Searchable): Flow<Badge?> 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 scope = CoroutineScope(Job() + Dispatchers.Default)
private val badgeProviders = MutableStateFlow<List<BadgeProvider>>(emptyList()) private val badgeProviders = MutableStateFlow<List<BadgeProvider>>(emptyList())
init { init {
scope.launch { scope.launch {
dataStore.data.map { it.badges }.distinctUntilChanged().collectLatest { settings.data.distinctUntilChanged().collectLatest {
val providers = mutableListOf<BadgeProvider>() val providers = mutableListOf<BadgeProvider>()
providers += WorkProfileBadgeProvider() providers += WorkProfileBadgeProvider()
if (it.notifications) { if (it.notifications) {
@ -37,7 +39,9 @@ internal class BadgeServiceImpl(private val context: Context) : BadgeService, Ko
if (it.suspendedApps) { if (it.suspendedApps) {
providers += SuspendedAppsBadgeProvider() providers += SuspendedAppsBadgeProvider()
} }
providers += PluginBadgeProvider(context) if (it.plugins) {
providers += PluginBadgeProvider(context)
}
badgeProviders.value = providers badgeProviders.value = providers
} }
} }

View File

@ -1,8 +1,13 @@
package de.mm20.launcher2.badges 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val badgesModule = 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,
)
}
}