From ee9312a887f348a1bd139f3eac62d93053baf807 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 6 Apr 2025 14:10:48 +0200 Subject: [PATCH] Add contact plugin settings --- .../contacts/ContactsSettingsScreen.kt | 51 +++++++++++++- .../contacts/ContactsSettingsScreenVM.kt | 15 ++-- .../settings/plugins/PluginSettingsScreen.kt | 70 ++++++++++++++++--- .../plugins/PluginSettingsScreenVM.kt | 12 ++++ .../settings/search/SearchSettingsScreen.kt | 58 +++++++++------ .../settings/search/SearchSettingsScreenVM.kt | 2 +- core/i18n/src/main/res/values/strings.xml | 3 +- .../search/ContactSearchSettings.kt | 7 ++ .../launcher2/search/contact/ContactInfo.kt | 23 ++++-- .../launcher2/contacts/ContactRepository.kt | 3 +- gradle/libs.versions.toml | 2 +- 11 files changed, 200 insertions(+), 46 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt index 5bbfd49a..9346d17a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt @@ -1,23 +1,34 @@ package de.mm20.launcher2.ui.settings.contacts +import android.app.PendingIntent import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Call +import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material.icons.rounded.Person +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.ktx.sendWithBackgroundPermission +import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen +import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch import de.mm20.launcher2.ui.component.preferences.SwitchPreference @Composable @@ -25,9 +36,10 @@ fun ContactsSettingsScreen() { val viewModel: ContactsSettingsScreenVM = viewModel() val context = LocalContext.current - val localContacts by viewModel.localContacts.collectAsStateWithLifecycle(null) val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null) val hasCallPermission by viewModel.hasCallPermission.collectAsStateWithLifecycle(null) + val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(emptyList(), minActiveState = Lifecycle.State.RESUMED) + val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet()) val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(null) PreferenceScreen( @@ -48,11 +60,44 @@ fun ContactsSettingsScreen() { title = stringResource(R.string.preference_search_contacts), summary = stringResource(R.string.preference_search_contacts_summary), icon = Icons.Rounded.Person, - value = localContacts == true, + value = enabledProviders.contains("local"), onValueChanged = { - viewModel.setLocalContacts(it) + viewModel.setProviderEnabled("local", it) } ) + for (plugin in plugins) { + val state = plugin.state + if (state is PluginState.SetupRequired) { + Banner( + modifier = Modifier.padding(16.dp), + text = state.message + ?: stringResource(id = R.string.plugin_state_setup_required), + icon = Icons.Rounded.ErrorOutline, + primaryAction = { + TextButton(onClick = { + try { + state.setupActivity.sendWithBackgroundPermission(context) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } + }) { + Text(stringResource(id = R.string.plugin_action_setup)) + } + } + ) + } + SwitchPreference( + title = plugin.plugin.label, + enabled = state is PluginState.Ready, + summary = (state as? PluginState.SetupRequired)?.message + ?: (state as? PluginState.Ready)?.text + ?: plugin.plugin.description, + value = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready, + onValueChanged = { + viewModel.setProviderEnabled(plugin.plugin.authority, it) + }, + ) + } } PreferenceCategory { AnimatedVisibility(hasCallPermission == false) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreenVM.kt index 5c438c03..c44f18df 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreenVM.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.plugin.PluginType +import de.mm20.launcher2.plugins.PluginService import de.mm20.launcher2.preferences.search.ContactSearchSettings import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn @@ -15,6 +17,7 @@ class ContactsSettingsScreenVM : ViewModel(), KoinComponent { private val settings: ContactSearchSettings by inject() private val permissionsManager: PermissionsManager by inject() + private val pluginService: PluginService by inject() val hasCallPermission = permissionsManager.hasPermission(PermissionGroup.Call) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) @@ -34,10 +37,14 @@ class ContactsSettingsScreenVM : ViewModel(), KoinComponent { permissionsManager.requestPermission(activity, PermissionGroup.Contacts) } - val localContacts = settings.isProviderEnabled("local") - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + val availablePlugins = pluginService.getPluginsWithState( + type = PluginType.ContactSearch, + enabled = true, + ) - fun setLocalContacts(enabled: Boolean) { - settings.setProviderEnabled("local", enabled) + val enabledProviders = settings.enabledProviders + + fun setProviderEnabled(providerId: String, enabled: Boolean) { + settings.setProviderEnabled(providerId, enabled) } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt index ab7470fa..3303a63b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt @@ -6,11 +6,8 @@ import android.content.Intent import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -23,15 +20,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile import androidx.compose.material.icons.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Error import androidx.compose.material.icons.rounded.Info -import androidx.compose.material.icons.rounded.LightMode -import androidx.compose.material.icons.rounded.Place import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material.icons.rounded.Today import androidx.compose.material.icons.rounded.Verified import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.Icon @@ -64,7 +57,6 @@ import coil.compose.AsyncImage import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.ktx.sendWithBackgroundPermission import de.mm20.launcher2.plugin.PluginState -import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.themes.atTone import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.Banner @@ -109,6 +101,11 @@ fun PluginSettingsScreen(pluginId: String) { minActiveState = Lifecycle.State.RESUMED ) + val contactPlugins by viewModel.contactPlugins.collectAsStateWithLifecycle( + emptyList(), + minActiveState = Lifecycle.State.RESUMED + ) + val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle( emptyList(), minActiveState = Lifecycle.State.RESUMED @@ -125,6 +122,10 @@ fun PluginSettingsScreen(pluginId: String) { null ) + val enabledContactPlugins by viewModel.enabledContactPlugins.collectAsStateWithLifecycle( + null + ) + val enabledLocationSearchPlugins by viewModel.enabledLocationSearchPlugins.collectAsStateWithLifecycle( null ) @@ -340,6 +341,59 @@ fun PluginSettingsScreen(pluginId: String) { } } } + if (contactPlugins.isNotEmpty()) { + PreferenceCategory( + stringResource(R.string.plugin_type_contacts), + iconPadding = false, + ) { + for (plugin in contactPlugins) { + val state = plugin.state + if (state is PluginState.SetupRequired) { + Banner( + modifier = Modifier.padding(16.dp), + text = state.message + ?: stringResource(R.string.plugin_state_setup_required), + icon = Icons.Rounded.Info, + primaryAction = { + TextButton(onClick = { + try { + state.setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } + }) { + Text(stringResource(R.string.plugin_action_setup)) + } + } + ) + } else if (state is PluginState.Error) { + Banner( + modifier = Modifier.padding(16.dp), + text = stringResource(R.string.plugin_state_error), + icon = Icons.Rounded.Error, + color = MaterialTheme.colorScheme.errorContainer, + ) + } + SwitchPreference( + title = plugin.plugin.label, + enabled = enabledContactPlugins != null && state is PluginState.Ready, + summary = (state as? PluginState.Ready)?.text + ?: (state as? PluginState.SetupRequired)?.message + ?: plugin.plugin.description, + value = enabledContactPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, + onValueChanged = { + viewModel.setContactPluginEnabled( + plugin.plugin.authority, + it + ) + }, + iconPadding = false, + ) + } + } + } if (locationPlugins.isNotEmpty()) { PreferenceCategory( stringResource(R.string.plugin_type_locationsearch), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt index 339a6bdb..b39f00e6 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreenVM.kt @@ -17,6 +17,7 @@ import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.plugins.PluginService import de.mm20.launcher2.plugins.PluginWithState import de.mm20.launcher2.preferences.search.CalendarSearchSettings +import de.mm20.launcher2.preferences.search.ContactSearchSettings import de.mm20.launcher2.preferences.search.FileSearchSettings import de.mm20.launcher2.preferences.search.LocationSearchSettings import de.mm20.launcher2.preferences.weather.WeatherSettings @@ -39,6 +40,7 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent { private val fileSearchSettings: FileSearchSettings by inject() private val locationSearchSettings: LocationSearchSettings by inject() private val calendarSearchSettings: CalendarSearchSettings by inject() + private val contactSearchSettings: ContactSearchSettings by inject() private val weatherSettings: WeatherSettings by inject() private var pluginPackageName = MutableStateFlow(null) @@ -90,6 +92,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent { it.filter { it.plugin.type == PluginType.Calendar } } + val contactPlugins = states + .map { + it.filter { it.plugin.type == PluginType.ContactSearch } + } + val weatherPlugins = states .map { it.filter { it.plugin.type == PluginType.Weather } @@ -127,6 +134,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent { fileSearchSettings.setPluginEnabled(authority, enabled) } + val enabledContactPlugins = contactSearchSettings.enabledPlugins + fun setContactPluginEnabled(authority: String, enabled: Boolean) { + contactSearchSettings.setPluginEnabled(authority, enabled) + } + val enabledLocationSearchPlugins = locationSearchSettings.enabledPlugins fun setLocationSearchPluginEnabled(authority: String, enabled: Boolean) { locationSearchSettings.setPluginEnabled(authority, enabled) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index 903b47a7..9914e039 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -59,9 +59,10 @@ fun SearchSettingsScreen() { var showFilterEditor by remember { mutableStateOf(false) } - val plugins by viewModel.plugins.collectAsStateWithLifecycle(emptyList()) - val hasCalendarPlugins by remember { derivedStateOf { plugins.any { it.plugin.type == PluginType.Calendar } } } - val hasLocationPlugins by remember { derivedStateOf { plugins.any { it.plugin.type == PluginType.LocationSearch } } } + val plugins by viewModel.plugins.collectAsStateWithLifecycle(null) + val hasCalendarPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.Calendar } } } + val hasLocationPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.LocationSearch } } } + val hasContactPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.ContactSearch } } } val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(null) @@ -110,30 +111,41 @@ fun SearchSettingsScreen() { } ) - AnimatedVisibility(hasContactsPermission == false) { - MissingPermissionBanner( - text = stringResource(R.string.missing_permission_contact_search_settings), + if (hasLocationPlugins != false) { + Preference( + title = stringResource(R.string.preference_search_contacts), + summary = stringResource(R.string.preference_search_contacts_summary), + icon = Icons.Rounded.Person, onClick = { - viewModel.requestContactsPermission(context as AppCompatActivity) + navController?.navigate("settings/search/contacts") }, - modifier = Modifier.padding(16.dp) + ) + } else { + AnimatedVisibility(hasContactsPermission == false) { + MissingPermissionBanner( + text = stringResource(R.string.missing_permission_contact_search_settings), + onClick = { + viewModel.requestContactsPermission(context as AppCompatActivity) + }, + modifier = Modifier.padding(16.dp) + ) + } + PreferenceWithSwitch( + title = stringResource(R.string.preference_search_contacts), + summary = stringResource(R.string.preference_search_contacts_summary), + icon = Icons.Rounded.Person, + switchValue = contacts == true && hasContactsPermission == true, + onSwitchChanged = { + viewModel.setContacts(it) + }, + onClick = { + navController?.navigate("settings/search/contacts") + }, + enabled = hasContactsPermission == true ) } - PreferenceWithSwitch( - title = stringResource(R.string.preference_search_contacts), - summary = stringResource(R.string.preference_search_contacts_summary), - icon = Icons.Rounded.Person, - switchValue = contacts == true && hasContactsPermission == true, - onSwitchChanged = { - viewModel.setContacts(it) - }, - onClick = { - navController?.navigate("settings/search/contacts") - }, - enabled = hasContactsPermission == true - ) - if (hasCalendarPlugins) { + if (hasCalendarPlugins != false) { Preference( title = stringResource(R.string.preference_search_calendar), summary = stringResource(R.string.preference_search_calendar_summary), @@ -247,7 +259,7 @@ fun SearchSettingsScreen() { ) } - if (hasLocationPlugins) { + if (hasLocationPlugins != false) { Preference( title = stringResource(R.string.preference_search_locations), summary = stringResource(R.string.preference_search_locations_summary), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt index c53f42c8..0c6bf587 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt @@ -158,5 +158,5 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent { } val plugins = pluginService.getPluginsWithState(enabled = true) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) } \ 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 1e199ee2..f460f86c 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -849,7 +849,8 @@ This plugin isn\'t working correctly You need to setup this plugin first Set up - File search + Files + Contacts Weather provider Set as weather provider Currently set as weather provider diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ContactSearchSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ContactSearchSettings.kt index 7fcd99f7..c7892edc 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ContactSearchSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/search/ContactSearchSettings.kt @@ -22,6 +22,13 @@ class ContactSearchSettings internal constructor(private val dataStore: Launcher } } + val enabledPlugins: Flow> + get() = dataStore.data.map { it.contactSearchProviders - "local" } + + fun setPluginEnabled(authority: String, enabled: Boolean) { + setProviderEnabled(authority, enabled) + } + val callOnTap: Flow get() = dataStore.data.map { it.contactSearchCallOnTap }.distinctUntilChanged() diff --git a/core/shared/src/main/java/de/mm20/launcher2/search/contact/ContactInfo.kt b/core/shared/src/main/java/de/mm20/launcher2/search/contact/ContactInfo.kt index a296fd26..f80a35b9 100644 --- a/core/shared/src/main/java/de/mm20/launcher2/search/contact/ContactInfo.kt +++ b/core/shared/src/main/java/de/mm20/launcher2/search/contact/ContactInfo.kt @@ -23,7 +23,7 @@ data class PostalAddress( ) /** - * Custom contact channel, for example, WhatsApp message, Telegram video call, etc. + * Custom contact action, for example, WhatsApp message, Telegram video call, etc. */ @Serializable data class CustomContactAction( @@ -35,18 +35,33 @@ data class CustomContactAction( /** * Type that is passed to the Intent. */ - val mimeType: String, + val mimeType: String? = null, /** * Package name of the app that handles this channel. - * Used to get the app icon, and label, and to group channels by app. - * If the app is not installed, the channel will be ignored. + * Used to get the app icon, and label, and to group actions by app. + * If the app is not installed, the action will be ignored. */ val packageName: String, ) enum class ContactInfoType { + /** + * Home or private number/address. + */ Home, + + /** + * Cell phone number, only applicable for phone numbers. + */ Mobile, + + /** + * Work or business number/address. + */ Work, + + /** + * Other + */ Other, } diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt index 32ba58a6..8044700c 100644 --- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt @@ -2,6 +2,7 @@ package de.mm20.launcher2.contacts import android.content.Context import de.mm20.launcher2.contacts.providers.AndroidContactProvider +import de.mm20.launcher2.contacts.providers.PluginContactProvider import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.search.ContactSearchSettings @@ -36,7 +37,7 @@ internal class ContactRepository( val providers = providerIds.mapNotNull { when (it) { "local" -> if (perm) AndroidContactProvider(context) else null - else -> null + else -> PluginContactProvider(context, it) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a986f78..5cf67bb6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ minSdk = "26" compileSdk = "35" targetSdk = "35" -pluginSdk = "2.1.2" +pluginSdk = "2.2.0-SNAPSHOT" gradle = "8.1.2" android-gradle-plugin = "8.6.0"