ContactItem improvements (#1324)
* ContactRepository: try to deduplicate phoneNumbers in a smart way * AndroidManifest: use CALL_PHONE permission to allow for making phone calls * Implement CallOnTap with contact results - contact activities that require permissions we do not have are not listed - add CallOnTap setting for phone number results on search behind contacts settings * utilize PhoneNumberUtils * navroute settings/search/contacts * queryIntentActivities -> resolveActivity * localization * Code formatting * Wrap contact search settings in preference category --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
parent
21894d8df2
commit
bceae1aa58
@ -2,6 +2,10 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.telephony"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.SET_ALARM" />
|
<uses-permission android:name="android.permission.SET_ALARM" />
|
||||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
@ -23,6 +27,7 @@
|
|||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import de.mm20.launcher2.notifications.Notification
|
|||||||
import de.mm20.launcher2.notifications.NotificationRepository
|
import de.mm20.launcher2.notifications.NotificationRepository
|
||||||
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.LocationSearchSettings
|
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||||
import de.mm20.launcher2.search.AppShortcut
|
import de.mm20.launcher2.search.AppShortcut
|
||||||
import de.mm20.launcher2.search.Application
|
import de.mm20.launcher2.search.Application
|
||||||
@ -53,6 +54,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
|||||||
private val appShortcutRepository: AppShortcutRepository by inject()
|
private val appShortcutRepository: AppShortcutRepository by inject()
|
||||||
private val permissionsManager: PermissionsManager by inject()
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
private val locationSearchSettings: LocationSearchSettings by inject()
|
private val locationSearchSettings: LocationSearchSettings by inject()
|
||||||
|
private val contactSearchSettings: ContactSearchSettings by inject()
|
||||||
|
|
||||||
val isUpToDate = MutableStateFlow(true)
|
val isUpToDate = MutableStateFlow(true)
|
||||||
|
|
||||||
@ -247,6 +249,9 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
|||||||
.map { it ?: LocationSearchSettings.DefaultTileServerUrl }
|
.map { it ?: LocationSearchSettings.DefaultTileServerUrl }
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, "")
|
.stateIn(viewModelScope, SharingStarted.Lazily, "")
|
||||||
|
|
||||||
|
val callOnTap = contactSearchSettings.callOnTap
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||||
|
|
||||||
fun reportUsage(searchable: SavableSearchable) {
|
fun reportUsage(searchable: SavableSearchable) {
|
||||||
favoritesService.reportLaunch(searchable)
|
favoritesService.reportLaunch(searchable)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,8 @@ import de.mm20.launcher2.ui.modifier.scale
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import de.mm20.launcher2.ktx.checkPermission
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ContactItem(
|
fun ContactItem(
|
||||||
@ -101,6 +103,7 @@ fun ContactItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||||
|
val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(false)
|
||||||
val badge by viewModel.badge.collectAsState(null)
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
|
|
||||||
SharedTransitionLayout {
|
SharedTransitionLayout {
|
||||||
@ -183,9 +186,12 @@ fun ContactItem(
|
|||||||
onContact = {
|
onContact = {
|
||||||
viewModel.reportUsage(contact)
|
viewModel.reportUsage(contact)
|
||||||
context.tryStartActivity(
|
context.tryStartActivity(
|
||||||
Intent(Intent.ACTION_DIAL).apply {
|
Intent(
|
||||||
data = Uri.parse("tel:${it.number}")
|
if (callOnTap)
|
||||||
}
|
Intent.ACTION_CALL
|
||||||
|
else
|
||||||
|
Intent.ACTION_DIAL
|
||||||
|
).setData("tel:${it.number}".toUri())
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
copyText = { it.number },
|
copyText = { it.number },
|
||||||
@ -300,11 +306,22 @@ fun ContactItem(
|
|||||||
app.key
|
app.key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val itemsWithPermission = remember(app) {
|
||||||
|
app.value.filter {
|
||||||
|
// exclude activities we have no permission for
|
||||||
|
val resolvedActivityInfo = context.packageManager.resolveActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW).setDataAndType(it.uri, it.mimeType),
|
||||||
|
0
|
||||||
|
)?.activityInfo ?: return@filter false
|
||||||
|
|
||||||
|
resolvedActivityInfo.permission == null || context.checkPermission(resolvedActivityInfo.permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
ContactInfo(
|
ContactInfo(
|
||||||
icon = Icons.AutoMirrored.Rounded.OpenInNew,
|
icon = Icons.AutoMirrored.Rounded.OpenInNew,
|
||||||
customIcon = appIcon,
|
customIcon = appIcon,
|
||||||
label = label,
|
label = label,
|
||||||
items = app.value,
|
items = itemsWithPermission,
|
||||||
itemLabel = { it.label },
|
itemLabel = { it.label },
|
||||||
expanded = expandedSection == 3 + i,
|
expanded = expandedSection == 3 + i,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen
|
|||||||
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
|
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
|
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
|
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
|
||||||
|
import de.mm20.launcher2.ui.settings.contacts.ContactsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
|
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
|
||||||
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
|
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
|
||||||
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
|
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
|
||||||
@ -227,6 +228,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/favorites") {
|
composable("settings/favorites") {
|
||||||
FavoritesSettingsScreen()
|
FavoritesSettingsScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/search/contacts") {
|
||||||
|
ContactsSettingsScreen()
|
||||||
|
}
|
||||||
composable("settings/integrations") {
|
composable("settings/integrations") {
|
||||||
IntegrationsSettingsScreen()
|
IntegrationsSettingsScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
package de.mm20.launcher2.ui.settings.contacts
|
||||||
|
|
||||||
|
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.runtime.Composable
|
||||||
|
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.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
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.SwitchPreference
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContactsSettingsScreen() {
|
||||||
|
val viewModel: ContactsSettingsScreenVM = viewModel()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val hasCallPermission by viewModel.hasCallPermission.collectAsStateWithLifecycle(null)
|
||||||
|
val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
PreferenceScreen(
|
||||||
|
title = stringResource(R.string.preference_search_contacts)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
PreferenceCategory {
|
||||||
|
AnimatedVisibility(hasCallPermission == false) {
|
||||||
|
MissingPermissionBanner(
|
||||||
|
text = stringResource(R.string.missing_permission_call_contacts_settings),
|
||||||
|
onClick = {
|
||||||
|
viewModel.requestCallPermission(context as AppCompatActivity)
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SwitchPreference(
|
||||||
|
title = stringResource(R.string.preference_contacts_call_on_tap),
|
||||||
|
summary = stringResource(R.string.preference_contacts_call_on_tap_summary),
|
||||||
|
icon = Icons.Rounded.Call,
|
||||||
|
value = callOnTap == true && hasCallPermission == true,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.setCallOnTap(it)
|
||||||
|
},
|
||||||
|
enabled = hasCallPermission == true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package de.mm20.launcher2.ui.settings.contacts
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
|
import de.mm20.launcher2.preferences.search.ContactSearchSettings
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class ContactsSettingsScreenVM : ViewModel(), KoinComponent {
|
||||||
|
|
||||||
|
private val settings: ContactSearchSettings by inject()
|
||||||
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
|
|
||||||
|
val hasCallPermission = permissionsManager.hasPermission(PermissionGroup.Call)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
|
fun requestCallPermission(activity: AppCompatActivity) =
|
||||||
|
permissionsManager.requestPermission(activity, PermissionGroup.Call)
|
||||||
|
|
||||||
|
val callOnTap = settings.callOnTap
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
|
fun setCallOnTap(callOnTap: Boolean) =
|
||||||
|
settings.setCallOnTap(callOnTap)
|
||||||
|
}
|
||||||
@ -119,14 +119,17 @@ fun SearchSettingsScreen() {
|
|||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SwitchPreference(
|
PreferenceWithSwitch(
|
||||||
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 = contacts == true && hasContactsPermission == true,
|
switchValue = contacts == true && hasContactsPermission == true,
|
||||||
onValueChanged = {
|
onSwitchChanged = {
|
||||||
viewModel.setContacts(it)
|
viewModel.setContacts(it)
|
||||||
},
|
},
|
||||||
|
onClick = {
|
||||||
|
navController?.navigate("settings/search/contacts")
|
||||||
|
},
|
||||||
enabled = hasContactsPermission == true
|
enabled = hasContactsPermission == true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -818,4 +818,6 @@
|
|||||||
<item quantity="one">in einer Minute</item>
|
<item quantity="one">in einer Minute</item>
|
||||||
<item quantity="other">in %1$d Minuten</item>
|
<item quantity="other">in %1$d Minuten</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="preference_contacts_call_on_tap">Zum Anruf Tippen</string>
|
||||||
|
<string name="preference_contacts_call_on_tap_summary">Beim Tippen auf eine Telefonnummer diese direkt Anrufen</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -390,6 +390,8 @@
|
|||||||
<string name="missing_permission_music_widget">Notification access is required to control media playback</string>
|
<string name="missing_permission_music_widget">Notification access is required to control media playback</string>
|
||||||
<!-- Missing contact permission in search settings screen -->
|
<!-- Missing contact permission in search settings screen -->
|
||||||
<string name="missing_permission_contact_search_settings">Contact permission is required to search contacts</string>
|
<string name="missing_permission_contact_search_settings">Contact permission is required to search contacts</string>
|
||||||
|
<!-- Missing call permission in contacts settings screen -->
|
||||||
|
<string name="missing_permission_call_contacts_settings">Call permission is required to start calls</string>
|
||||||
<!-- Missing calendar permission in search settings screen -->
|
<!-- Missing calendar permission in search settings screen -->
|
||||||
<string name="missing_permission_calendar_search_settings">Calendar permission is required to search calendar</string>
|
<string name="missing_permission_calendar_search_settings">Calendar permission is required to search calendar</string>
|
||||||
<!-- Missing calendar permission in calendar widget settings screen -->
|
<!-- Missing calendar permission in calendar widget settings screen -->
|
||||||
@ -668,6 +670,8 @@
|
|||||||
<string name="preference_category_accounts">Accounts</string>
|
<string name="preference_category_accounts">Accounts</string>
|
||||||
<string name="preference_weather_integration">Weather</string>
|
<string name="preference_weather_integration">Weather</string>
|
||||||
<string name="preference_media_integration">Media control</string>
|
<string name="preference_media_integration">Media control</string>
|
||||||
|
<string name="preference_contacts_call_on_tap">Call on tap</string>
|
||||||
|
<string name="preference_contacts_call_on_tap_summary">Directly start calls when tapping phone numbers</string>
|
||||||
<!-- Used in an info banner if a specific feature requires a Nextcloud account -->
|
<!-- Used in an info banner if a specific feature requires a Nextcloud account -->
|
||||||
<string name="no_account_nextcloud">You haven\'t connected a Nextcloud account yet</string>
|
<string name="no_account_nextcloud">You haven\'t connected a Nextcloud account yet</string>
|
||||||
<!-- Used in an info banner if a specific feature requires an Owncloud account -->
|
<!-- Used in an info banner if a specific feature requires an Owncloud account -->
|
||||||
|
|||||||
@ -15,3 +15,15 @@ fun <T> List<T>.randomElementOrNull(): T? {
|
|||||||
fun <T> List<T>?.ifNullOrEmpty(block: () -> List<T>): List<T> {
|
fun <T> List<T>?.ifNullOrEmpty(block: () -> List<T>): List<T> {
|
||||||
return if (this.isNullOrEmpty()) block() else this
|
return if (this.isNullOrEmpty()) block() else this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> List<T>.distinctByEquality(equalityPredicate: (T, T) -> Boolean): List<T> {
|
||||||
|
if (size < 2) return this
|
||||||
|
|
||||||
|
val ret = mutableListOf<T>()
|
||||||
|
|
||||||
|
for (item in this) {
|
||||||
|
if (ret.none { equalityPredicate(it, item) }) ret.add(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|||||||
@ -65,6 +65,7 @@ enum class PermissionGroup {
|
|||||||
AppShortcuts,
|
AppShortcuts,
|
||||||
Accessibility,
|
Accessibility,
|
||||||
ManageProfiles,
|
ManageProfiles,
|
||||||
|
Call,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PermissionsManagerImpl(
|
internal class PermissionsManagerImpl(
|
||||||
@ -93,6 +94,9 @@ internal class PermissionsManagerImpl(
|
|||||||
private val manageProfilesPermissionState = MutableStateFlow(
|
private val manageProfilesPermissionState = MutableStateFlow(
|
||||||
checkPermissionOnce(PermissionGroup.ManageProfiles)
|
checkPermissionOnce(PermissionGroup.ManageProfiles)
|
||||||
)
|
)
|
||||||
|
private val callPermissionState = MutableStateFlow(
|
||||||
|
checkPermissionOnce(PermissionGroup.Call)
|
||||||
|
)
|
||||||
|
|
||||||
override fun requestPermission(context: AppCompatActivity, permissionGroup: PermissionGroup) {
|
override fun requestPermission(context: AppCompatActivity, permissionGroup: PermissionGroup) {
|
||||||
when (permissionGroup) {
|
when (permissionGroup) {
|
||||||
@ -167,6 +171,14 @@ internal class PermissionsManagerImpl(
|
|||||||
CrashReporter.logException(e)
|
CrashReporter.logException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionGroup.Call -> {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
context,
|
||||||
|
callPermissions,
|
||||||
|
permissionGroup.ordinal
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,6 +221,10 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.Accessibility -> {
|
PermissionGroup.Accessibility -> {
|
||||||
accessibilityPermissionState.value
|
accessibilityPermissionState.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PermissionGroup.Call -> {
|
||||||
|
callPermissions.all { context.checkPermission(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +238,7 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
PermissionGroup.AppShortcuts -> appShortcutsPermissionState
|
||||||
PermissionGroup.Accessibility -> accessibilityPermissionState
|
PermissionGroup.Accessibility -> accessibilityPermissionState
|
||||||
PermissionGroup.ManageProfiles -> manageProfilesPermissionState
|
PermissionGroup.ManageProfiles -> manageProfilesPermissionState
|
||||||
|
PermissionGroup.Call -> callPermissionState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +258,7 @@ internal class PermissionsManagerImpl(
|
|||||||
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
|
PermissionGroup.AppShortcuts -> appShortcutsPermissionState.value = granted
|
||||||
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
|
PermissionGroup.Accessibility -> accessibilityPermissionState.value = granted
|
||||||
PermissionGroup.ManageProfiles -> manageProfilesPermissionState.value = granted
|
PermissionGroup.ManageProfiles -> manageProfilesPermissionState.value = granted
|
||||||
|
PermissionGroup.Call -> callPermissionState.value = granted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,5 +287,6 @@ internal class PermissionsManagerImpl(
|
|||||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
)
|
)
|
||||||
|
private val callPermissions = arrayOf(Manifest.permission.CALL_PHONE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,7 @@ data class LauncherSettingsData internal constructor(
|
|||||||
val fileSearchProviders: Set<String> = setOf("local"),
|
val fileSearchProviders: Set<String> = setOf("local"),
|
||||||
|
|
||||||
val contactSearchEnabled: Boolean = true,
|
val contactSearchEnabled: Boolean = true,
|
||||||
|
val contactSearchCallOnTap: Boolean = false,
|
||||||
|
|
||||||
@Deprecated("Use calendarSearchProviders `local` instead")
|
@Deprecated("Use calendarSearchProviders `local` instead")
|
||||||
val calendarSearchEnabled: Boolean = true,
|
val calendarSearchEnabled: Boolean = true,
|
||||||
|
|||||||
@ -12,4 +12,11 @@ class ContactSearchSettings internal constructor(private val dataStore: Launcher
|
|||||||
fun setEnabled(enabled: Boolean) {
|
fun setEnabled(enabled: Boolean) {
|
||||||
dataStore.update { it.copy(contactSearchEnabled = enabled) }
|
dataStore.update { it.copy(contactSearchEnabled = enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val callOnTap: Flow<Boolean>
|
||||||
|
get() = dataStore.data.map { it.contactSearchCallOnTap }.distinctUntilChanged()
|
||||||
|
|
||||||
|
fun setCallOnTap(callOnTap: Boolean) {
|
||||||
|
dataStore.update { it.copy(contactSearchCallOnTap = callOnTap) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,9 +2,12 @@ package de.mm20.launcher2.contacts
|
|||||||
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
|
import android.telephony.PhoneNumberUtils
|
||||||
import androidx.core.database.getLongOrNull
|
import androidx.core.database.getLongOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
|
import de.mm20.launcher2.ktx.distinctByEquality
|
||||||
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
|
||||||
@ -136,11 +139,10 @@ internal class ContactRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue
|
|
||||||
contactApps += ContactApp(
|
contactApps += ContactApp(
|
||||||
label = dataCursor.getStringOrNull(data3Column) ?: continue,
|
label = dataCursor.getStringOrNull(data3Column) ?: continue,
|
||||||
packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue,
|
packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue,
|
||||||
mimeType = mimeType,
|
mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue,
|
||||||
uri = ContentUris.withAppendedId(
|
uri = ContentUris.withAppendedId(
|
||||||
ContactsContract.Data.CONTENT_URI,
|
ContactsContract.Data.CONTENT_URI,
|
||||||
dataCursor.getLongOrNull(idColumn) ?: continue
|
dataCursor.getLongOrNull(idColumn) ?: continue
|
||||||
@ -164,12 +166,24 @@ internal class ContactRepository(
|
|||||||
}
|
}
|
||||||
lookupKeyCursor.close()
|
lookupKeyCursor.close()
|
||||||
|
|
||||||
|
val mainLocaleISO3 = context.resources.configuration.locales[0].isO3Country
|
||||||
|
|
||||||
return@withContext AndroidContact(
|
return@withContext AndroidContact(
|
||||||
id = id,
|
id = id,
|
||||||
firstName = firstName,
|
firstName = firstName,
|
||||||
lastName = lastName,
|
lastName = lastName,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
phoneNumbers = phoneNumbers.distinct(),
|
phoneNumbers = phoneNumbers.sortedByDescending {
|
||||||
|
it.number.count { !PhoneNumberUtils.isReallyDialable(it) }
|
||||||
|
}.distinctByEquality { a, b ->
|
||||||
|
if (Build.VERSION.SDK_INT < 31) {
|
||||||
|
PhoneNumberUtils.compare(context, a.number, b.number)
|
||||||
|
} else {
|
||||||
|
PhoneNumberUtils.areSamePhoneNumber(a.number, b.number, mainLocaleISO3)
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
|
it.copy(number = PhoneNumberUtils.formatNumber(it.number, mainLocaleISO3))
|
||||||
|
},
|
||||||
emailAddresses = emailAddresses.distinct(),
|
emailAddresses = emailAddresses.distinct(),
|
||||||
postalAddresses = postalAddresses.distinct(),
|
postalAddresses = postalAddresses.distinct(),
|
||||||
contactApps = contactApps.distinct(),
|
contactApps = contactApps.distinct(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user