Contact stuff

This commit is contained in:
MM20 2025-04-04 17:29:26 +02:00
parent 0ebce91a43
commit 04dd12ee96
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 141 additions and 73 deletions

View File

@ -62,12 +62,10 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage 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.ui.R import de.mm20.launcher2.ui.R
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
@ -85,6 +83,9 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import androidx.core.net.toUri import androidx.core.net.toUri
import de.mm20.launcher2.ktx.checkPermission import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.getApplicationIconOrNull
import de.mm20.launcher2.ktx.getApplicationInfoOrNull
import de.mm20.launcher2.search.contact.ContactInfoType
@Composable @Composable
fun ContactItem( fun ContactItem(
@ -275,48 +276,34 @@ fun ContactItem(
) )
} }
val apps = remember(contact) { val apps = remember(contact) {
contact.contactApps.groupBy { it.packageName } contact.contactChannels.groupBy { it.packageName }
} }
for ((i, app) in apps.entries.withIndex()) { for ((i, app) in apps.entries.withIndex()) {
val packageName = remember(app) { val packageName = app.key
context.packageManager.queryIntentActivities( val packageInfo = remember(packageName) {
Intent(Intent.ACTION_VIEW).apply { context.packageManager.getApplicationInfoOrNull(packageName)
setDataAndType(
app.value.first().uri,
app.value.first().mimeType
)
},
0,
).firstOrNull()?.activityInfo?.packageName
} ?: continue } ?: continue
val appIcon by remember(app) {
flow { val appIcon = remember(packageName) {
try { context.packageManager.getApplicationIconOrNull(packageName)
emit(context.packageManager.getApplicationIcon(packageName)) }
} catch (e: PackageManager.NameNotFoundException) {
emit(null)
}
}.flowOn(Dispatchers.IO)
}.collectAsState(null)
val label = remember(app) { val label = remember(app) {
try { packageInfo.loadLabel(context.packageManager).toString()
context.packageManager.getApplicationInfo(packageName, 0)
.loadLabel(context.packageManager).toString()
} catch (e: PackageManager.NameNotFoundException) {
app.key
}
} }
val itemsWithPermission = remember(app) { val itemsWithPermission = remember(app) {
app.value.filter { app.value.filter {
// exclude activities we have no permission for // exclude activities we have no permission for
val resolvedActivityInfo = context.packageManager.resolveActivity( val resolvedActivityInfo = context.packageManager.resolveActivity(
Intent(Intent.ACTION_VIEW).setDataAndType(it.uri, it.mimeType), Intent(Intent.ACTION_VIEW).setPackage(it.packageName).setDataAndType(it.uri, it.mimeType),
0 0
)?.activityInfo ?: return@filter false )?.activityInfo ?: return@filter false
resolvedActivityInfo.permission == null || context.checkPermission(resolvedActivityInfo.permission) resolvedActivityInfo.permission == null || context.checkPermission(resolvedActivityInfo.permission)
} }
} }
if (itemsWithPermission.isEmpty()) continue
ContactInfo( ContactInfo(
icon = Icons.AutoMirrored.Rounded.OpenInNew, icon = Icons.AutoMirrored.Rounded.OpenInNew,
customIcon = appIcon, customIcon = appIcon,
@ -334,6 +321,7 @@ fun ContactItem(
viewModel.reportUsage(contact) viewModel.reportUsage(contact)
context.tryStartActivity( context.tryStartActivity(
Intent(Intent.ACTION_VIEW).apply { Intent(Intent.ACTION_VIEW).apply {
setPackage(packageName)
setDataAndType( setDataAndType(
it.uri, it.uri,
it.mimeType it.mimeType

View File

@ -8,13 +8,17 @@ import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.icons.VectorLayer import de.mm20.launcher2.icons.VectorLayer
import de.mm20.launcher2.search.contact.CustomContactChannel
import de.mm20.launcher2.search.contact.EmailAddress
import de.mm20.launcher2.search.contact.PhoneNumber
import de.mm20.launcher2.search.contact.PostalAddress
interface Contact : SavableSearchable { interface Contact : SavableSearchable {
val name: String val name: String
val phoneNumbers: List<PhoneNumber> val phoneNumbers: List<PhoneNumber>
val emailAddresses: List<EmailAddress> val emailAddresses: List<EmailAddress>
val postalAddresses: List<PostalAddress> val postalAddresses: List<PostalAddress>
val contactApps: List<ContactApp> val contactChannels: List<CustomContactChannel>
val summary: String val summary: String
get() { get() {
@ -41,32 +45,3 @@ interface Contact : SavableSearchable {
override val preferDetailsOverLaunch: Boolean override val preferDetailsOverLaunch: Boolean
get() = true get() = true
} }
data class PhoneNumber(
val number: String,
val type: ContactInfoType,
)
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,
}

View File

@ -0,0 +1,26 @@
package de.mm20.launcher2.ktx
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
fun PackageManager.getApplicationInfoOrNull(
packageName: String,
flags: Int = 0,
): ApplicationInfo? {
return try {
getApplicationInfo(packageName, flags)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
fun PackageManager.getApplicationIconOrNull(
packageName: String,
): Drawable? {
return try {
getApplicationIcon(packageName)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}

View File

@ -0,0 +1,28 @@
package de.mm20.launcher2.plugin.contracts
import de.mm20.launcher2.search.contact.CustomContactChannel
import de.mm20.launcher2.search.contact.EmailAddress
import de.mm20.launcher2.search.contact.PhoneNumber
import de.mm20.launcher2.search.contact.PostalAddress
abstract class ContactPluginContract {
object ContactColumns: Columns() {
/**
* The unique ID of the contact.
*/
val Id = column<String>("id")
/**
* The display name of the contact.
* First name + last name, if applicable.
*/
val Name = column<String>("name")
val PhoneNumbers = column<List<PhoneNumber>>("phone_numbers")
val EmailAddresses = column<List<EmailAddress>>("email_addresses")
val PostalAddresses = column<List<PostalAddress>>("postal_addresses")
val ContactChannels = column<List<CustomContactChannel>>("contact_channels")
val PhotoUri = column<String>("photo_uri")
}
}

View File

@ -0,0 +1,52 @@
package de.mm20.launcher2.search.contact
import android.net.Uri
import de.mm20.launcher2.serialization.UriSerializer
import kotlinx.serialization.Serializable
@Serializable
data class PhoneNumber(
val number: String,
val type: ContactInfoType = ContactInfoType.Other,
)
@Serializable
data class EmailAddress(
val address: String,
val type: ContactInfoType = ContactInfoType.Other,
)
@Serializable
data class PostalAddress(
val address: String,
val type: ContactInfoType = ContactInfoType.Other,
)
/**
* Custom contact channel, for example, WhatsApp message, Telegram video call, etc.
*/
@Serializable
data class CustomContactChannel(
val label: String,
/**
* The data URI that is passed to the Intent.
*/
@Serializable(with = UriSerializer::class) val uri: Uri,
/**
* Type that is passed to the Intent.
*/
val mimeType: String,
/**
* Package name of the app that handles this channel.
* Used to get the app icon, and label, and to group channels by app.
* If the app is not installed, the channel will be ignored.
*/
val packageName: String,
)
enum class ContactInfoType {
Home,
Mobile,
Work,
Other,
}

View File

@ -13,11 +13,11 @@ import de.mm20.launcher2.icons.StaticLauncherIcon
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.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 de.mm20.launcher2.search.contact.CustomContactChannel
import de.mm20.launcher2.search.contact.EmailAddress
import de.mm20.launcher2.search.contact.PhoneNumber
import de.mm20.launcher2.search.contact.PostalAddress
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -27,8 +27,7 @@ internal data class AndroidContact(
override val phoneNumbers: List<PhoneNumber>, override val phoneNumbers: List<PhoneNumber>,
override val emailAddresses: List<EmailAddress>, override val emailAddresses: List<EmailAddress>,
override val postalAddresses: List<PostalAddress>, override val postalAddresses: List<PostalAddress>,
override val contactApps: List<ContactApp>, override val contactChannels: List<CustomContactChannel>, internal val lookupKey: String,
internal val lookupKey: String,
override val labelOverride: String? = null, override val labelOverride: String? = null,
) : Contact { ) : Contact {

View File

@ -12,12 +12,12 @@ 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.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 de.mm20.launcher2.search.contact.ContactInfoType
import de.mm20.launcher2.search.contact.CustomContactChannel
import de.mm20.launcher2.search.contact.EmailAddress
import de.mm20.launcher2.search.contact.PhoneNumber
import de.mm20.launcher2.search.contact.PostalAddress
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -71,7 +71,7 @@ internal class ContactRepository(
val phoneNumbers = mutableListOf<PhoneNumber>() val phoneNumbers = mutableListOf<PhoneNumber>()
val emailAddresses = mutableListOf<EmailAddress>() val emailAddresses = mutableListOf<EmailAddress>()
val postalAddresses = mutableListOf<PostalAddress>() val postalAddresses = mutableListOf<PostalAddress>()
val contactApps = mutableListOf<ContactApp>() val contactChannels = mutableListOf<CustomContactChannel>()
val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE) val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
val typeColumn = val typeColumn =
@ -139,7 +139,7 @@ internal class ContactRepository(
} }
else -> { else -> {
contactApps += ContactApp( contactChannels += CustomContactChannel(
label = dataCursor.getStringOrNull(data3Column) ?: continue, label = dataCursor.getStringOrNull(data3Column) ?: continue,
packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue, packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue,
mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue, mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue,
@ -189,7 +189,7 @@ internal class ContactRepository(
}, },
emailAddresses = emailAddresses.distinct(), emailAddresses = emailAddresses.distinct(),
postalAddresses = postalAddresses.distinct(), postalAddresses = postalAddresses.distinct(),
contactApps = contactApps.distinct(), contactChannels = contactChannels.distinct(),
lookupKey = lookUpKey lookupKey = lookUpKey
) )
} }