Add contact plugin settings

This commit is contained in:
MM20 2025-04-06 14:10:48 +02:00
parent 02cec92d72
commit ee9312a887
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
11 changed files with 200 additions and 46 deletions

View File

@ -1,23 +1,34 @@
package de.mm20.launcher2.ui.settings.contacts package de.mm20.launcher2.ui.settings.contacts
import android.app.PendingIntent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Call import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.Person 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.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel 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.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@Composable @Composable
@ -25,9 +36,10 @@ fun ContactsSettingsScreen() {
val viewModel: ContactsSettingsScreenVM = viewModel() val viewModel: ContactsSettingsScreenVM = viewModel()
val context = LocalContext.current val context = LocalContext.current
val localContacts by viewModel.localContacts.collectAsStateWithLifecycle(null)
val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null) val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null)
val hasCallPermission by viewModel.hasCallPermission.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) val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(null)
PreferenceScreen( PreferenceScreen(
@ -48,11 +60,44 @@ fun ContactsSettingsScreen() {
title = stringResource(R.string.preference_search_contacts), title = stringResource(R.string.preference_search_contacts),
summary = stringResource(R.string.preference_search_contacts_summary), summary = stringResource(R.string.preference_search_contacts_summary),
icon = Icons.Rounded.Person, icon = Icons.Rounded.Person,
value = localContacts == true, value = enabledProviders.contains("local"),
onValueChanged = { 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 { PreferenceCategory {
AnimatedVisibility(hasCallPermission == false) { AnimatedVisibility(hasCallPermission == false) {

View File

@ -5,6 +5,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager 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 de.mm20.launcher2.preferences.search.ContactSearchSettings
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@ -15,6 +17,7 @@ class ContactsSettingsScreenVM : ViewModel(), KoinComponent {
private val settings: ContactSearchSettings by inject() private val settings: ContactSearchSettings by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val pluginService: PluginService by inject()
val hasCallPermission = permissionsManager.hasPermission(PermissionGroup.Call) val hasCallPermission = permissionsManager.hasPermission(PermissionGroup.Call)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@ -34,10 +37,14 @@ class ContactsSettingsScreenVM : ViewModel(), KoinComponent {
permissionsManager.requestPermission(activity, PermissionGroup.Contacts) permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
} }
val localContacts = settings.isProviderEnabled("local") val availablePlugins = pluginService.getPluginsWithState(
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) type = PluginType.ContactSearch,
enabled = true,
)
fun setLocalContacts(enabled: Boolean) { val enabledProviders = settings.enabledProviders
settings.setProviderEnabled("local", enabled)
fun setProviderEnabled(providerId: String, enabled: Boolean) {
settings.setProviderEnabled(providerId, enabled)
} }
} }

View File

@ -6,11 +6,8 @@ import android.content.Intent
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -23,15 +20,11 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack 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.automirrored.rounded.OpenInNew
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Error import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.Info 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.Settings
import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.Verified import androidx.compose.material.icons.rounded.Verified
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -64,7 +57,6 @@ import coil.compose.AsyncImage
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.sendWithBackgroundPermission import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.themes.atTone import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.Banner
@ -109,6 +101,11 @@ fun PluginSettingsScreen(pluginId: String) {
minActiveState = Lifecycle.State.RESUMED minActiveState = Lifecycle.State.RESUMED
) )
val contactPlugins by viewModel.contactPlugins.collectAsStateWithLifecycle(
emptyList(),
minActiveState = Lifecycle.State.RESUMED
)
val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle( val weatherPlugins by viewModel.weatherPlugins.collectAsStateWithLifecycle(
emptyList(), emptyList(),
minActiveState = Lifecycle.State.RESUMED minActiveState = Lifecycle.State.RESUMED
@ -125,6 +122,10 @@ fun PluginSettingsScreen(pluginId: String) {
null null
) )
val enabledContactPlugins by viewModel.enabledContactPlugins.collectAsStateWithLifecycle(
null
)
val enabledLocationSearchPlugins by viewModel.enabledLocationSearchPlugins.collectAsStateWithLifecycle( val enabledLocationSearchPlugins by viewModel.enabledLocationSearchPlugins.collectAsStateWithLifecycle(
null 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()) { if (locationPlugins.isNotEmpty()) {
PreferenceCategory( PreferenceCategory(
stringResource(R.string.plugin_type_locationsearch), stringResource(R.string.plugin_type_locationsearch),

View File

@ -17,6 +17,7 @@ import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.plugins.PluginWithState import de.mm20.launcher2.plugins.PluginWithState
import de.mm20.launcher2.preferences.search.CalendarSearchSettings 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.FileSearchSettings
import de.mm20.launcher2.preferences.search.LocationSearchSettings import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.preferences.weather.WeatherSettings import de.mm20.launcher2.preferences.weather.WeatherSettings
@ -39,6 +40,7 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
private val fileSearchSettings: FileSearchSettings by inject() private val fileSearchSettings: FileSearchSettings by inject()
private val locationSearchSettings: LocationSearchSettings by inject() private val locationSearchSettings: LocationSearchSettings by inject()
private val calendarSearchSettings: CalendarSearchSettings by inject() private val calendarSearchSettings: CalendarSearchSettings by inject()
private val contactSearchSettings: ContactSearchSettings by inject()
private val weatherSettings: WeatherSettings by inject() private val weatherSettings: WeatherSettings by inject()
private var pluginPackageName = MutableStateFlow<String?>(null) private var pluginPackageName = MutableStateFlow<String?>(null)
@ -90,6 +92,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
it.filter { it.plugin.type == PluginType.Calendar } it.filter { it.plugin.type == PluginType.Calendar }
} }
val contactPlugins = states
.map {
it.filter { it.plugin.type == PluginType.ContactSearch }
}
val weatherPlugins = states val weatherPlugins = states
.map { .map {
it.filter { it.plugin.type == PluginType.Weather } it.filter { it.plugin.type == PluginType.Weather }
@ -127,6 +134,11 @@ class PluginSettingsScreenVM : ViewModel(), KoinComponent {
fileSearchSettings.setPluginEnabled(authority, enabled) fileSearchSettings.setPluginEnabled(authority, enabled)
} }
val enabledContactPlugins = contactSearchSettings.enabledPlugins
fun setContactPluginEnabled(authority: String, enabled: Boolean) {
contactSearchSettings.setPluginEnabled(authority, enabled)
}
val enabledLocationSearchPlugins = locationSearchSettings.enabledPlugins val enabledLocationSearchPlugins = locationSearchSettings.enabledPlugins
fun setLocationSearchPluginEnabled(authority: String, enabled: Boolean) { fun setLocationSearchPluginEnabled(authority: String, enabled: Boolean) {
locationSearchSettings.setPluginEnabled(authority, enabled) locationSearchSettings.setPluginEnabled(authority, enabled)

View File

@ -59,9 +59,10 @@ fun SearchSettingsScreen() {
var showFilterEditor by remember { mutableStateOf(false) } var showFilterEditor by remember { mutableStateOf(false) }
val plugins by viewModel.plugins.collectAsStateWithLifecycle(emptyList()) val plugins by viewModel.plugins.collectAsStateWithLifecycle(null)
val hasCalendarPlugins by remember { derivedStateOf { plugins.any { it.plugin.type == PluginType.Calendar } } } 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 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) val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(null)
@ -110,30 +111,41 @@ fun SearchSettingsScreen() {
} }
) )
AnimatedVisibility(hasContactsPermission == false) { if (hasLocationPlugins != false) {
MissingPermissionBanner( Preference(
text = stringResource(R.string.missing_permission_contact_search_settings), title = stringResource(R.string.preference_search_contacts),
summary = stringResource(R.string.preference_search_contacts_summary),
icon = Icons.Rounded.Person,
onClick = { 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( Preference(
title = stringResource(R.string.preference_search_calendar), title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary), summary = stringResource(R.string.preference_search_calendar_summary),
@ -247,7 +259,7 @@ fun SearchSettingsScreen() {
) )
} }
if (hasLocationPlugins) { if (hasLocationPlugins != false) {
Preference( Preference(
title = stringResource(R.string.preference_search_locations), title = stringResource(R.string.preference_search_locations),
summary = stringResource(R.string.preference_search_locations_summary), summary = stringResource(R.string.preference_search_locations_summary),

View File

@ -158,5 +158,5 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
} }
val plugins = pluginService.getPluginsWithState(enabled = true) val plugins = pluginService.getPluginsWithState(enabled = true)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
} }

View File

@ -849,7 +849,8 @@
<string name="plugin_state_error">This plugin isn\'t working correctly</string> <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_state_setup_required">You need to setup this plugin first</string>
<string name="plugin_action_setup">Set up</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_type_weather">Weather provider</string>
<string name="plugin_weather_provider_enable">Set as 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> <string name="plugin_weather_provider_enabled">Currently set as weather provider</string>

View File

@ -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> val callOnTap: Flow<Boolean>
get() = dataStore.data.map { it.contactSearchCallOnTap }.distinctUntilChanged() get() = dataStore.data.map { it.contactSearchCallOnTap }.distinctUntilChanged()

View File

@ -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 @Serializable
data class CustomContactAction( data class CustomContactAction(
@ -35,18 +35,33 @@ data class CustomContactAction(
/** /**
* Type that is passed to the Intent. * Type that is passed to the Intent.
*/ */
val mimeType: String, val mimeType: String? = null,
/** /**
* Package name of the app that handles this channel. * Package name of the app that handles this channel.
* Used to get the app icon, and label, and to group channels by app. * Used to get the app icon, and label, and to group actions by app.
* If the app is not installed, the channel will be ignored. * If the app is not installed, the action will be ignored.
*/ */
val packageName: String, val packageName: String,
) )
enum class ContactInfoType { enum class ContactInfoType {
/**
* Home or private number/address.
*/
Home, Home,
/**
* Cell phone number, only applicable for phone numbers.
*/
Mobile, Mobile,
/**
* Work or business number/address.
*/
Work, Work,
/**
* Other
*/
Other, Other,
} }

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.contacts
import android.content.Context import android.content.Context
import de.mm20.launcher2.contacts.providers.AndroidContactProvider 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.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.search.ContactSearchSettings import de.mm20.launcher2.preferences.search.ContactSearchSettings
@ -36,7 +37,7 @@ internal class ContactRepository(
val providers = providerIds.mapNotNull { val providers = providerIds.mapNotNull {
when (it) { when (it) {
"local" -> if (perm) AndroidContactProvider(context) else null "local" -> if (perm) AndroidContactProvider(context) else null
else -> null else -> PluginContactProvider(context, it)
} }
} }

View File

@ -6,7 +6,7 @@ minSdk = "26"
compileSdk = "35" compileSdk = "35"
targetSdk = "35" targetSdk = "35"
pluginSdk = "2.1.2" pluginSdk = "2.2.0-SNAPSHOT"
gradle = "8.1.2" gradle = "8.1.2"
android-gradle-plugin = "8.6.0" android-gradle-plugin = "8.6.0"