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:
@@ -16,6 +16,7 @@ import de.mm20.launcher2.notifications.Notification
|
||||
import de.mm20.launcher2.notifications.NotificationRepository
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.search.ContactSearchSettings
|
||||
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.Application
|
||||
@@ -53,6 +54,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||
private val appShortcutRepository: AppShortcutRepository by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val locationSearchSettings: LocationSearchSettings by inject()
|
||||
private val contactSearchSettings: ContactSearchSettings by inject()
|
||||
|
||||
val isUpToDate = MutableStateFlow(true)
|
||||
|
||||
@@ -247,6 +249,9 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||
.map { it ?: LocationSearchSettings.DefaultTileServerUrl }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, "")
|
||||
|
||||
val callOnTap = contactSearchSettings.callOnTap
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
|
||||
fun reportUsage(searchable: SavableSearchable) {
|
||||
favoritesService.reportLaunch(searchable)
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ import de.mm20.launcher2.ui.modifier.scale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import androidx.core.net.toUri
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
|
||||
@Composable
|
||||
fun ContactItem(
|
||||
@@ -101,6 +103,7 @@ fun ContactItem(
|
||||
}
|
||||
|
||||
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||
val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(false)
|
||||
val badge by viewModel.badge.collectAsState(null)
|
||||
|
||||
SharedTransitionLayout {
|
||||
@@ -183,9 +186,12 @@ fun ContactItem(
|
||||
onContact = {
|
||||
viewModel.reportUsage(contact)
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_DIAL).apply {
|
||||
data = Uri.parse("tel:${it.number}")
|
||||
}
|
||||
Intent(
|
||||
if (callOnTap)
|
||||
Intent.ACTION_CALL
|
||||
else
|
||||
Intent.ACTION_DIAL
|
||||
).setData("tel:${it.number}".toUri())
|
||||
)
|
||||
},
|
||||
copyText = { it.number },
|
||||
@@ -300,11 +306,22 @@ fun ContactItem(
|
||||
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(
|
||||
icon = Icons.AutoMirrored.Rounded.OpenInNew,
|
||||
customIcon = appIcon,
|
||||
label = label,
|
||||
items = app.value,
|
||||
items = itemsWithPermission,
|
||||
itemLabel = { it.label },
|
||||
expanded = expandedSection == 3 + i,
|
||||
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.colorscheme.ThemeSettingsScreen
|
||||
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.CrashReporterScreen
|
||||
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
|
||||
@@ -227,6 +228,9 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/favorites") {
|
||||
FavoritesSettingsScreen()
|
||||
}
|
||||
composable("settings/search/contacts") {
|
||||
ContactsSettingsScreen()
|
||||
}
|
||||
composable("settings/integrations") {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -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)
|
||||
)
|
||||
}
|
||||
SwitchPreference(
|
||||
PreferenceWithSwitch(
|
||||
title = stringResource(R.string.preference_search_contacts),
|
||||
summary = stringResource(R.string.preference_search_contacts_summary),
|
||||
icon = Icons.Rounded.Person,
|
||||
value = contacts == true && hasContactsPermission == true,
|
||||
onValueChanged = {
|
||||
switchValue = contacts == true && hasContactsPermission == true,
|
||||
onSwitchChanged = {
|
||||
viewModel.setContacts(it)
|
||||
},
|
||||
onClick = {
|
||||
navController?.navigate("settings/search/contacts")
|
||||
},
|
||||
enabled = hasContactsPermission == true
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user