Add contact plugin settings
This commit is contained in:
parent
02cec92d72
commit
ee9312a887
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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<String?>(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)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -158,5 +158,5 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
|
||||
val plugins = pluginService.getPluginsWithState(enabled = true)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
}
|
||||
@ -849,7 +849,8 @@
|
||||
<string name="plugin_state_error">This plugin isn\'t working correctly</string>
|
||||
<string name="plugin_state_setup_required">You need to setup this plugin first</string>
|
||||
<string name="plugin_action_setup">Set up</string>
|
||||
<string name="plugin_type_filesearch">File search</string>
|
||||
<string name="plugin_type_filesearch">Files</string>
|
||||
<string name="plugin_type_contacts">Contacts</string>
|
||||
<string name="plugin_type_weather">Weather provider</string>
|
||||
<string name="plugin_weather_provider_enable">Set as weather provider</string>
|
||||
<string name="plugin_weather_provider_enabled">Currently set as weather provider</string>
|
||||
|
||||
@ -22,6 +22,13 @@ class ContactSearchSettings internal constructor(private val dataStore: Launcher
|
||||
}
|
||||
}
|
||||
|
||||
val enabledPlugins: Flow<Set<String>>
|
||||
get() = dataStore.data.map { it.contactSearchProviders - "local" }
|
||||
|
||||
fun setPluginEnabled(authority: String, enabled: Boolean) {
|
||||
setProviderEnabled(authority, enabled)
|
||||
}
|
||||
|
||||
val callOnTap: Flow<Boolean>
|
||||
get() = dataStore.data.map { it.contactSearchCallOnTap }.distinctUntilChanged()
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user