Redesign contact results
This commit is contained in:
parent
29f8440b24
commit
84582ebbaf
@ -1,75 +1,83 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search.contacts
|
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.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
|
||||||
import androidx.compose.animation.expandIn
|
import androidx.compose.animation.expandIn
|
||||||
import androidx.compose.animation.shrinkOut
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
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.Message
|
import androidx.compose.material.icons.automirrored.rounded.Message
|
||||||
import androidx.compose.material.icons.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.NavigateNext
|
||||||
import androidx.compose.material.icons.rounded.Call
|
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.Edit
|
||||||
import androidx.compose.material.icons.rounded.Email
|
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.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.Star
|
||||||
import androidx.compose.material.icons.rounded.StarOutline
|
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.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.roundToIntRect
|
import androidx.compose.ui.unit.roundToIntRect
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import coil.compose.AsyncImage
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.search.Contact
|
import de.mm20.launcher2.search.Contact
|
||||||
import de.mm20.launcher2.search.ContactInfoType
|
import de.mm20.launcher2.search.ContactInfoType
|
||||||
import de.mm20.launcher2.ui.R
|
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.DefaultToolbarAction
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.component.Toolbar
|
import de.mm20.launcher2.ui.component.Toolbar
|
||||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
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.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
|
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
|
||||||
import de.mm20.launcher2.ui.modifier.scale
|
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
|
@Composable
|
||||||
fun ContactItem(
|
fun ContactItem(
|
||||||
@ -87,158 +95,483 @@ fun ContactItem(
|
|||||||
viewModel.init(contact, iconSize.toInt())
|
viewModel.init(contact, iconSize.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val icon by viewModel.icon.collectAsStateWithLifecycle()
|
||||||
val snackbarHostState = LocalSnackbarHostState.current
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
|
|
||||||
val transition = updateTransition(showDetails, label = "ContactItem")
|
SharedTransitionLayout {
|
||||||
|
AnimatedContent(showDetails) { showDetails ->
|
||||||
Column(
|
if (showDetails) {
|
||||||
modifier = modifier
|
Column {
|
||||||
) {
|
|
||||||
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) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(when(type) {
|
ShapedLauncherIcon(
|
||||||
ContactInfoType.Phone -> Icons.Rounded.Call
|
size = 48.dp,
|
||||||
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(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.padding(end = 16.dp)
|
||||||
.padding(start = 16.dp),
|
.sharedElement(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
rememberSharedContentState("icon"),
|
||||||
) {
|
this@AnimatedContent,
|
||||||
items(items.toList()) {
|
),
|
||||||
Chip(
|
icon = { icon },
|
||||||
modifier = Modifier.padding(end = 16.dp),
|
badge = { badge }
|
||||||
text = it.label,
|
)
|
||||||
onClick = {
|
Text(
|
||||||
context.tryStartActivity(it.intent)
|
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) {
|
if (LocalFavoritesEnabled.current) {
|
||||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
val favAction = if (isPinned) {
|
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(
|
DefaultToolbarAction(
|
||||||
label = stringResource(R.string.menu_favorites_unpin),
|
label = stringResource(R.string.menu_contacts_open_externally),
|
||||||
icon = Icons.Rounded.Star,
|
icon = Icons.Rounded.OpenInNew,
|
||||||
action = {
|
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
|
val sheetManager = LocalBottomSheetManager.current
|
||||||
toolbarActions.add(DefaultToolbarAction(
|
toolbarActions.add(DefaultToolbarAction(
|
||||||
label = stringResource(R.string.menu_customize),
|
label = stringResource(R.string.menu_customize),
|
||||||
icon = Icons.Rounded.Edit,
|
icon = Icons.Rounded.Edit,
|
||||||
action = { sheetManager.showCustomizeSearchableModal(contact) }
|
action = { sheetManager.showCustomizeSearchableModal(contact) }
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
Toolbar(
|
Toolbar(
|
||||||
leftActions = listOf(
|
leftActions = listOf(
|
||||||
DefaultToolbarAction(
|
DefaultToolbarAction(
|
||||||
label = stringResource(id = R.string.menu_back),
|
label = stringResource(id = R.string.menu_back),
|
||||||
icon = Icons.Rounded.ArrowBack
|
icon = Icons.AutoMirrored.Rounded.ArrowBack
|
||||||
) {
|
) {
|
||||||
onBack()
|
onBack()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
rightActions = toolbarActions
|
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
|
@Composable
|
||||||
fun ContactItemGridPopup(
|
fun ContactItemGridPopup(
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
|
|||||||
@ -1,35 +1,46 @@
|
|||||||
package de.mm20.launcher2.search
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import android.content.Intent
|
import android.net.Uri
|
||||||
|
|
||||||
interface Contact: SavableSearchable {
|
interface Contact : SavableSearchable {
|
||||||
val firstName: String
|
val firstName: String
|
||||||
val lastName: String
|
val lastName: String
|
||||||
val displayName: String
|
val displayName: String
|
||||||
val summary: 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
|
override val preferDetailsOverLaunch: Boolean
|
||||||
get() = true
|
get() = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class PhoneNumber(
|
||||||
* Type of the contact info
|
val number: String,
|
||||||
* Acts as a hint for the UI, so that it can display the correct icon and group them accordingly
|
val type: ContactInfoType,
|
||||||
*/
|
)
|
||||||
enum class ContactInfoType {
|
|
||||||
Phone,
|
|
||||||
Message,
|
|
||||||
Email,
|
|
||||||
Postal,
|
|
||||||
Telegram,
|
|
||||||
Whatsapp,
|
|
||||||
Signal,
|
|
||||||
Other
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactInfo {
|
data class EmailAddress(
|
||||||
val type: ContactInfoType
|
val address: String,
|
||||||
val label: String
|
val type: ContactInfoType,
|
||||||
val intent: Intent
|
)
|
||||||
|
|
||||||
|
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_calendar_default">Calendar widget & search results</string>
|
||||||
<string name="item_visibility_search_only">Search results</string>
|
<string name="item_visibility_search_only">Search results</string>
|
||||||
<string name="item_visibility_hidden">Never</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>
|
</resources>
|
||||||
@ -14,7 +14,10 @@ import de.mm20.launcher2.icons.TextLayer
|
|||||||
import de.mm20.launcher2.ktx.asBitmap
|
import de.mm20.launcher2.ktx.asBitmap
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.search.Contact
|
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 de.mm20.launcher2.search.SearchableSerializer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -24,7 +27,10 @@ internal data class AndroidContact(
|
|||||||
override val firstName: String,
|
override val firstName: String,
|
||||||
override val lastName: String,
|
override val lastName: String,
|
||||||
override val displayName: 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,
|
internal val lookupKey: String,
|
||||||
override val labelOverride: String? = null,
|
override val labelOverride: String? = null,
|
||||||
) : Contact {
|
) : Contact {
|
||||||
@ -38,7 +44,7 @@ internal data class AndroidContact(
|
|||||||
|
|
||||||
override val summary: String
|
override val summary: String
|
||||||
get() {
|
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 {
|
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
|
package de.mm20.launcher2.contacts
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
|
import androidx.core.database.getLongOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
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
|
||||||
import de.mm20.launcher2.search.Contact
|
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 de.mm20.launcher2.search.SearchableRepository
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@ -51,25 +57,22 @@ internal class ContactRepository(
|
|||||||
|
|
||||||
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? =
|
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
val s = "${ContactsContract.Data.RAW_CONTACT_ID} IN (${rawIds.joinToString(", ")})"
|
||||||
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 dataCursor = context.contentResolver.query(
|
val dataCursor = context.contentResolver.query(
|
||||||
ContactsContract.Data.CONTENT_URI,
|
ContactsContract.Data.CONTENT_URI,
|
||||||
null, s, null, null
|
null, s, null, null
|
||||||
) ?: return@withContext null
|
) ?: return@withContext null
|
||||||
val contactInfos = mutableSetOf<ContactInfo>()
|
|
||||||
var firstName = ""
|
var firstName = ""
|
||||||
var lastName = ""
|
var lastName = ""
|
||||||
var displayName = ""
|
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 mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
|
||||||
|
val typeColumn =
|
||||||
|
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Contactables.TYPE)
|
||||||
val emailAddressColumn =
|
val emailAddressColumn =
|
||||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
||||||
val numberColumn =
|
val numberColumn =
|
||||||
@ -82,25 +85,49 @@ internal class ContactRepository(
|
|||||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||||
val familyNameColumn =
|
val familyNameColumn =
|
||||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
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 data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3)
|
||||||
val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID)
|
val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID)
|
||||||
loop@ while (dataCursor.moveToNext()) {
|
loop@ while (dataCursor.moveToNext()) {
|
||||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
||||||
dataCursor.getStringOrNull(emailAddressColumn)?.let {
|
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 ->
|
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
||||||
dataCursor.getStringOrNull(numberColumn)?.let {
|
dataCursor.getStringOrNull(numberColumn)?.let {
|
||||||
val phone = it.replace(Regex("[^+0-9]"), "")
|
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 ->
|
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
||||||
dataCursor.getStringOrNull(addressColumn)?.let {
|
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 -> {
|
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||||
@ -109,34 +136,18 @@ internal class ContactRepository(
|
|||||||
displayName = dataCursor.getStringOrNull(displayNameColumn) ?: ""
|
displayName = dataCursor.getStringOrNull(displayNameColumn) ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
TelegramContactInfo.ItemType -> {
|
else -> {
|
||||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
val mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue
|
||||||
?: continue@loop
|
contactApps += ContactApp(
|
||||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
label = dataCursor.getStringOrNull(data3Column) ?: continue,
|
||||||
?: continue@loop
|
packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue,
|
||||||
contactInfos.add(
|
mimeType = mimeType,
|
||||||
TelegramContactInfo(data3.substringAfterLast(" "), data1)
|
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()
|
dataCursor.close()
|
||||||
@ -159,7 +170,10 @@ internal class ContactRepository(
|
|||||||
firstName = firstName,
|
firstName = firstName,
|
||||||
lastName = lastName,
|
lastName = lastName,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
contactInfos = contactInfos,
|
phoneNumbers = phoneNumbers.distinct(),
|
||||||
|
emailAddresses = emailAddresses.distinct(),
|
||||||
|
postalAddresses = postalAddresses.distinct(),
|
||||||
|
contactApps = contactApps.distinct(),
|
||||||
lookupKey = lookUpKey
|
lookupKey = lookUpKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user