Redesign contact results
This commit is contained in:
parent
29f8440b24
commit
84582ebbaf
@ -1,75 +1,83 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.contacts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.expandIn
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.rounded.Message
|
||||
import androidx.compose.material.icons.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.Call
|
||||
import androidx.compose.material.icons.automirrored.rounded.NavigateNext
|
||||
import androidx.compose.material.icons.automirrored.rounded.OpenInNew
|
||||
import androidx.compose.material.icons.rounded.Directions
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Email
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.MoreHoriz
|
||||
import androidx.compose.material.icons.rounded.OpenInNew
|
||||
import androidx.compose.material.icons.rounded.Phone
|
||||
import androidx.compose.material.icons.rounded.Place
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.StarOutline
|
||||
import androidx.compose.material.icons.rounded.Visibility
|
||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||
import androidx.compose.material.icons.rounded.Whatsapp
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.roundToIntRect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfoType
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.Chip
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||
import de.mm20.launcher2.icons.Signal
|
||||
import de.mm20.launcher2.icons.Telegram
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.ui.modifier.scale
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
@Composable
|
||||
fun ContactItem(
|
||||
@ -87,158 +95,483 @@ fun ContactItem(
|
||||
viewModel.init(contact, iconSize.toInt())
|
||||
}
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||
val badge by viewModel.badge.collectAsState(null)
|
||||
|
||||
val transition = updateTransition(showDetails, label = "ContactItem")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||
val padding = 16.dp
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
modifier = Modifier
|
||||
.padding(start = padding, top = padding, bottom = padding),
|
||||
icon = { icon },
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
val textStyle by animateTextStyleAsState(
|
||||
if (showDetails) MaterialTheme.typography.titleLarge
|
||||
else MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = contact.labelOverride ?: contact.label,
|
||||
style = textStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
AnimatedVisibility(!showDetails) {
|
||||
Text(
|
||||
contact.summary,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(showDetails) {
|
||||
val tags by viewModel.tags.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 1.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(showDetails) {
|
||||
val groups = remember {
|
||||
contact.contactInfos.groupBy { it.type }
|
||||
}
|
||||
Column {
|
||||
|
||||
for ((type, items) in groups) {
|
||||
SharedTransitionLayout {
|
||||
AnimatedContent(showDetails) { showDetails ->
|
||||
if (showDetails) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(when(type) {
|
||||
ContactInfoType.Phone -> Icons.Rounded.Call
|
||||
ContactInfoType.Message -> Icons.AutoMirrored.Rounded.Message
|
||||
ContactInfoType.Email -> Icons.Rounded.Email
|
||||
ContactInfoType.Postal -> Icons.Rounded.Home
|
||||
ContactInfoType.Telegram -> Icons.Rounded.Telegram
|
||||
ContactInfoType.Whatsapp -> Icons.Rounded.Whatsapp
|
||||
ContactInfoType.Signal -> Icons.Rounded.Signal
|
||||
ContactInfoType.Other -> Icons.Rounded.MoreHoriz
|
||||
}, contentDescription = null)
|
||||
LazyRow(
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(items.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
context.tryStartActivity(it.intent)
|
||||
.padding(end = 16.dp)
|
||||
.sharedElement(
|
||||
rememberSharedContentState("icon"),
|
||||
this@AnimatedContent,
|
||||
),
|
||||
icon = { icon },
|
||||
badge = { badge }
|
||||
)
|
||||
Text(
|
||||
text = contact.labelOverride ?: contact.label,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.sharedBounds(
|
||||
rememberSharedContentState("label"),
|
||||
this@AnimatedContent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val canNavigate = remember {
|
||||
context.packageManager.queryIntentActivities(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("google.navigation:q=")
|
||||
),
|
||||
0
|
||||
).isNotEmpty()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
var expandedSection by remember { mutableStateOf(-1) }
|
||||
if (contact.phoneNumbers.isNotEmpty()) {
|
||||
ContactInfo(
|
||||
icon = Icons.Rounded.Phone,
|
||||
label = pluralStringResource(
|
||||
R.plurals.contact_phone_numbers,
|
||||
contact.phoneNumbers.size,
|
||||
contact.phoneNumbers.size
|
||||
),
|
||||
items = contact.phoneNumbers,
|
||||
itemLabel = { it.number },
|
||||
itemSubLabel = { it.type.toString(context) },
|
||||
expanded = expandedSection == 0,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
secondaryAction = {
|
||||
IconButton(onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = Uri.parse("smsto:${it.number}")
|
||||
}
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.Message,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
onExpand = {
|
||||
expandedSection = if (it) 0 else -1
|
||||
},
|
||||
onContact = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_DIAL).apply {
|
||||
data = Uri.parse("tel:${it.number}")
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (contact.emailAddresses.isNotEmpty()) {
|
||||
ContactInfo(
|
||||
icon = Icons.Rounded.Email,
|
||||
label = pluralStringResource(
|
||||
R.plurals.contact_email_addresses,
|
||||
contact.emailAddresses.size,
|
||||
contact.emailAddresses.size
|
||||
),
|
||||
items = contact.emailAddresses,
|
||||
itemLabel = { it.address },
|
||||
itemSubLabel = { it.type.toString(context) },
|
||||
expanded = expandedSection == 1,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
onExpand = {
|
||||
expandedSection = if (it) 1 else -1
|
||||
},
|
||||
onContact = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = Uri.parse("mailto:${it.address}")
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (contact.postalAddresses.isNotEmpty()) {
|
||||
ContactInfo(
|
||||
icon = Icons.Rounded.Place,
|
||||
label = pluralStringResource(
|
||||
R.plurals.contact_postal_addresses,
|
||||
contact.postalAddresses.size,
|
||||
contact.postalAddresses.size
|
||||
),
|
||||
items = contact.postalAddresses,
|
||||
itemLabel = { it.address },
|
||||
itemSubLabel = { it.type.toString(context) },
|
||||
secondaryAction = if (canNavigate) {
|
||||
{
|
||||
IconButton(onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("google.navigation:q=${it.address}")
|
||||
}
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Rounded.Directions,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
expanded = expandedSection == 2,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
onExpand = {
|
||||
expandedSection = if (it) 2 else -1
|
||||
},
|
||||
onContact = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse("geo:0,0?q=${it.address}")
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
val apps = remember(contact) {
|
||||
contact.contactApps.groupBy { it.packageName }
|
||||
}
|
||||
for ((i, app) in apps.entries.withIndex()) {
|
||||
val packageName = remember(app) {
|
||||
context.packageManager.queryIntentActivities(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(
|
||||
app.value.first().uri,
|
||||
app.value.first().mimeType
|
||||
)
|
||||
},
|
||||
0,
|
||||
).firstOrNull()?.activityInfo?.packageName
|
||||
} ?: continue
|
||||
val appIcon by remember(app) {
|
||||
flow {
|
||||
try {
|
||||
emit(context.packageManager.getApplicationIcon(packageName))
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
emit(null)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}.collectAsState(null)
|
||||
val label = remember(app) {
|
||||
try {
|
||||
context.packageManager.getApplicationInfo(packageName, 0)
|
||||
.loadLabel(context.packageManager).toString()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
app.key
|
||||
}
|
||||
}
|
||||
ContactInfo(
|
||||
icon = Icons.AutoMirrored.Rounded.OpenInNew,
|
||||
customIcon = appIcon,
|
||||
label = label,
|
||||
items = app.value,
|
||||
itemLabel = { it.label },
|
||||
expanded = expandedSection == 3 + i,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
onExpand = {
|
||||
expandedSection = if (it) 3 + i else -1
|
||||
},
|
||||
onContact = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(
|
||||
it.uri,
|
||||
it.mimeType
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
label = stringResource(R.string.menu_contacts_open_externally),
|
||||
icon = Icons.Rounded.OpenInNew,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
viewModel.launch(context)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_contacts_open_externally),
|
||||
icon = Icons.Rounded.OpenInNew,
|
||||
action = {
|
||||
viewModel.launch(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(contact) }
|
||||
))
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(contact) }
|
||||
))
|
||||
|
||||
|
||||
Toolbar(
|
||||
leftActions = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
Toolbar(
|
||||
leftActions = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(id = R.string.menu_back),
|
||||
icon = Icons.AutoMirrored.Rounded.ArrowBack
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 8.dp,
|
||||
top = 8.dp,
|
||||
bottom = 8.dp,
|
||||
end = 16.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.sharedElement(
|
||||
rememberSharedContentState("icon"),
|
||||
this@AnimatedContent,
|
||||
),
|
||||
icon = { icon },
|
||||
badge = { badge }
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.sharedBounds(
|
||||
rememberSharedContentState("label"),
|
||||
this@AnimatedContent,
|
||||
),
|
||||
text = contact.labelOverride ?: contact.label,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
contact.summary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> ContactInfo(
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
items: List<T>,
|
||||
itemLabel: (T) -> String,
|
||||
itemSubLabel: (T) -> String? = { null },
|
||||
itemIcon: (T) -> ImageVector? = { null },
|
||||
secondaryAction: (@Composable (T) -> Unit)? = null,
|
||||
icon: ImageVector,
|
||||
customIcon: Drawable? = null,
|
||||
expanded: Boolean,
|
||||
onExpand: (Boolean) -> Unit,
|
||||
onContact: (T) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable(
|
||||
enabled = !expanded && items.size != 1
|
||||
) {
|
||||
if (items.size > 1) {
|
||||
onExpand(true)
|
||||
} else {
|
||||
onContact(items.first())
|
||||
}
|
||||
}
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
MaterialTheme.shapes.small
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AnimatedContent(expanded || items.size == 1) { exp ->
|
||||
if (exp) {
|
||||
Column {
|
||||
for (item in items) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = if (secondaryAction != null) 8.dp else 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
onContact(item)
|
||||
}
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (customIcon != null) {
|
||||
AsyncImage(
|
||||
model = customIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
itemIcon(item) ?: icon,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
itemSubLabel(item)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(bottom = 2.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = itemLabel(item),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (secondaryAction != null) {
|
||||
VerticalDivider(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.padding(end = 4.dp)
|
||||
)
|
||||
secondaryAction(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (customIcon != null) {
|
||||
AsyncImage(
|
||||
model = customIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.NavigateNext,
|
||||
null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun ContactInfoType.toString(context: Context): String? {
|
||||
return when (this) {
|
||||
ContactInfoType.Home -> context.getString(R.string.contact_info_home)
|
||||
ContactInfoType.Mobile -> context.getString(R.string.contact_info_mobile)
|
||||
ContactInfoType.Work -> context.getString(R.string.contact_info_work)
|
||||
ContactInfoType.Other -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ContactItemGridPopup(
|
||||
contact: Contact,
|
||||
|
||||
@ -1,35 +1,46 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
||||
interface Contact: SavableSearchable {
|
||||
interface Contact : SavableSearchable {
|
||||
val firstName: String
|
||||
val lastName: String
|
||||
val displayName: String
|
||||
val summary: String
|
||||
val contactInfos: Iterable<ContactInfo>
|
||||
val phoneNumbers: List<PhoneNumber>
|
||||
val emailAddresses: List<EmailAddress>
|
||||
val postalAddresses: List<PostalAddress>
|
||||
val contactApps: List<ContactApp>
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the contact info
|
||||
* Acts as a hint for the UI, so that it can display the correct icon and group them accordingly
|
||||
*/
|
||||
enum class ContactInfoType {
|
||||
Phone,
|
||||
Message,
|
||||
Email,
|
||||
Postal,
|
||||
Telegram,
|
||||
Whatsapp,
|
||||
Signal,
|
||||
Other
|
||||
}
|
||||
data class PhoneNumber(
|
||||
val number: String,
|
||||
val type: ContactInfoType,
|
||||
)
|
||||
|
||||
interface ContactInfo {
|
||||
val type: ContactInfoType
|
||||
val label: String
|
||||
val intent: Intent
|
||||
}
|
||||
data class EmailAddress(
|
||||
val address: String,
|
||||
val type: ContactInfoType,
|
||||
)
|
||||
|
||||
data class PostalAddress(
|
||||
val address: String,
|
||||
val type: ContactInfoType,
|
||||
)
|
||||
|
||||
data class ContactApp(
|
||||
val label: String,
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
val packageName: String,
|
||||
)
|
||||
|
||||
enum class ContactInfoType {
|
||||
Home,
|
||||
Mobile,
|
||||
Work,
|
||||
Other,
|
||||
}
|
||||
|
||||
@ -949,4 +949,19 @@
|
||||
<string name="item_visibility_calendar_default">Calendar widget & search results</string>
|
||||
<string name="item_visibility_search_only">Search results</string>
|
||||
<string name="item_visibility_hidden">Never</string>
|
||||
<string name="contact_info_home">Home</string>
|
||||
<string name="contact_info_mobile">Home</string>
|
||||
<string name="contact_info_work">Home</string>
|
||||
<plurals name="contact_phone_numbers">
|
||||
<item quantity="one">%1$s phone number</item>
|
||||
<item quantity="other">%1$s phone numbers</item>
|
||||
</plurals>
|
||||
<plurals name="contact_email_addresses">
|
||||
<item quantity="one">%1$s email address</item>
|
||||
<item quantity="other">%1$s email addresses</item>
|
||||
</plurals>
|
||||
<plurals name="contact_postal_addresses">
|
||||
<item quantity="one">%1$s postal address</item>
|
||||
<item quantity="other">%1$s postal addresses</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@ -14,7 +14,10 @@ import de.mm20.launcher2.icons.TextLayer
|
||||
import de.mm20.launcher2.ktx.asBitmap
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.ContactApp
|
||||
import de.mm20.launcher2.search.EmailAddress
|
||||
import de.mm20.launcher2.search.PhoneNumber
|
||||
import de.mm20.launcher2.search.PostalAddress
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -24,7 +27,10 @@ internal data class AndroidContact(
|
||||
override val firstName: String,
|
||||
override val lastName: String,
|
||||
override val displayName: String,
|
||||
override val contactInfos: Iterable<ContactInfo>,
|
||||
override val phoneNumbers: List<PhoneNumber>,
|
||||
override val emailAddresses: List<EmailAddress>,
|
||||
override val postalAddresses: List<PostalAddress>,
|
||||
override val contactApps: List<ContactApp>,
|
||||
internal val lookupKey: String,
|
||||
override val labelOverride: String? = null,
|
||||
) : Contact {
|
||||
@ -38,7 +44,7 @@ internal data class AndroidContact(
|
||||
|
||||
override val summary: String
|
||||
get() {
|
||||
return contactInfos.distinctBy { it.label }.take(5).joinToString(separator = ", ") { it.label }
|
||||
return (phoneNumbers.map { it.number } + emailAddresses.map { it.address }).joinToString(", ")
|
||||
}
|
||||
|
||||
override fun overrideLabel(label: String): Contact {
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.ContactInfoType
|
||||
import java.net.URLEncoder
|
||||
|
||||
|
||||
internal data class PhoneContactInfo(
|
||||
val number: String,
|
||||
) : ContactInfo {
|
||||
override val label: String
|
||||
get() = number
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Phone
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("tel:$number"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
internal data class MailContactInfo(
|
||||
val address: String,
|
||||
) : ContactInfo {
|
||||
override val label: String
|
||||
get() = address
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Email
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("mailto:$address"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
internal data class PostalContactInfo(
|
||||
val address: String,
|
||||
) : ContactInfo {
|
||||
override val label: String
|
||||
get() = address.replace("\n", ", ")
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Postal
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("geo:0,0?q=${URLEncoder.encode(address, "utf8")}"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
internal data class TelegramContactInfo(
|
||||
override val label: String,
|
||||
val userId: String,
|
||||
) : ContactInfo {
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Telegram
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("tg:openmessage?user_id=$userId"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
internal companion object {
|
||||
const val ItemType = "vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile"
|
||||
}
|
||||
}
|
||||
|
||||
internal data class SignalContactInfo(
|
||||
override val label: String,
|
||||
val dataId: Long,
|
||||
) : ContactInfo {
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Signal
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(
|
||||
Uri.withAppendedPath(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataId.toString()
|
||||
)
|
||||
)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
internal companion object {
|
||||
const val ItemType = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact"
|
||||
}
|
||||
}
|
||||
|
||||
internal data class WhatsAppContactInfo(
|
||||
override val label: String,
|
||||
val dataId: Long,
|
||||
) : ContactInfo {
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Whatsapp
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(
|
||||
Uri.withAppendedPath(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataId.toString()
|
||||
)
|
||||
)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
internal companion object {
|
||||
const val ItemType = "vnd.android.cursor.item/vnd.com.whatsapp.profile"
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,19 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.search.ContactSearchSettings
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.ContactApp
|
||||
import de.mm20.launcher2.search.ContactInfoType
|
||||
import de.mm20.launcher2.search.EmailAddress
|
||||
import de.mm20.launcher2.search.PhoneNumber
|
||||
import de.mm20.launcher2.search.PostalAddress
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@ -51,25 +57,22 @@ internal class ContactRepository(
|
||||
|
||||
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
||||
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${TelegramContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${WhatsAppContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${SignalContactInfo.ItemType}\"" +
|
||||
")"
|
||||
val s = "${ContactsContract.Data.RAW_CONTACT_ID} IN (${rawIds.joinToString(", ")})"
|
||||
val dataCursor = context.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
null, s, null, null
|
||||
) ?: return@withContext null
|
||||
val contactInfos = mutableSetOf<ContactInfo>()
|
||||
var firstName = ""
|
||||
var lastName = ""
|
||||
var displayName = ""
|
||||
val phoneNumbers = mutableListOf<PhoneNumber>()
|
||||
val emailAddresses = mutableListOf<EmailAddress>()
|
||||
val postalAddresses = mutableListOf<PostalAddress>()
|
||||
val contactApps = mutableListOf<ContactApp>()
|
||||
|
||||
val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
|
||||
val typeColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Contactables.TYPE)
|
||||
val emailAddressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
||||
val numberColumn =
|
||||
@ -82,25 +85,49 @@ internal class ContactRepository(
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||
val familyNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
||||
val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1)
|
||||
val accountTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.ACCOUNT_TYPE_AND_DATA_SET)
|
||||
|
||||
val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3)
|
||||
val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID)
|
||||
loop@ while (dataCursor.moveToNext()) {
|
||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(emailAddressColumn)?.let {
|
||||
contactInfos.add(MailContactInfo(it))
|
||||
emailAddresses += EmailAddress(
|
||||
it,
|
||||
when (dataCursor.getInt(typeColumn)) {
|
||||
ContactsContract.CommonDataKinds.Email.TYPE_HOME -> ContactInfoType.Home
|
||||
ContactsContract.CommonDataKinds.Email.TYPE_WORK -> ContactInfoType.Work
|
||||
ContactsContract.CommonDataKinds.Email.TYPE_MOBILE -> ContactInfoType.Mobile
|
||||
else -> ContactInfoType.Other
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(numberColumn)?.let {
|
||||
val phone = it.replace(Regex("[^+0-9]"), "")
|
||||
contactInfos.add(PhoneContactInfo(phone))
|
||||
phoneNumbers += PhoneNumber(
|
||||
phone,
|
||||
when (dataCursor.getInt(typeColumn)) {
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> ContactInfoType.Home
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> ContactInfoType.Work
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> ContactInfoType.Mobile
|
||||
else -> ContactInfoType.Other
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(addressColumn)?.let {
|
||||
contactInfos.add(PostalContactInfo(it))
|
||||
postalAddresses += PostalAddress(
|
||||
it,
|
||||
when (dataCursor.getInt(typeColumn)) {
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME -> ContactInfoType.Home
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK -> ContactInfoType.Work
|
||||
else -> ContactInfoType.Other
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||
@ -109,34 +136,18 @@ internal class ContactRepository(
|
||||
displayName = dataCursor.getStringOrNull(displayNameColumn) ?: ""
|
||||
}
|
||||
|
||||
TelegramContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
||||
?: continue@loop
|
||||
contactInfos.add(
|
||||
TelegramContactInfo(data3.substringAfterLast(" "), data1)
|
||||
else -> {
|
||||
val mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue
|
||||
contactApps += ContactApp(
|
||||
label = dataCursor.getStringOrNull(data3Column) ?: continue,
|
||||
packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue,
|
||||
mimeType = mimeType,
|
||||
uri = ContentUris.withAppendedId(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataCursor.getLongOrNull(idColumn) ?: continue
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
WhatsAppContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(
|
||||
WhatsAppContactInfo(
|
||||
"+${data1.substringBefore('@')}",
|
||||
dataId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SignalContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(SignalContactInfo(data1, dataId))
|
||||
}
|
||||
}
|
||||
}
|
||||
dataCursor.close()
|
||||
@ -159,7 +170,10 @@ internal class ContactRepository(
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
displayName = displayName,
|
||||
contactInfos = contactInfos,
|
||||
phoneNumbers = phoneNumbers.distinct(),
|
||||
emailAddresses = emailAddresses.distinct(),
|
||||
postalAddresses = postalAddresses.distinct(),
|
||||
contactApps = contactApps.distinct(),
|
||||
lookupKey = lookUpKey
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user