diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt index efacd775..43d05374 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt @@ -62,12 +62,10 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.ShapedLauncherIcon @@ -85,6 +83,9 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import androidx.core.net.toUri 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 fun ContactItem( @@ -275,48 +276,34 @@ fun ContactItem( ) } val apps = remember(contact) { - contact.contactApps.groupBy { it.packageName } + contact.contactChannels.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 + val packageName = app.key + val packageInfo = remember(packageName) { + context.packageManager.getApplicationInfoOrNull(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 appIcon = remember(packageName) { + context.packageManager.getApplicationIconOrNull(packageName) + } val label = remember(app) { - try { - context.packageManager.getApplicationInfo(packageName, 0) - .loadLabel(context.packageManager).toString() - } catch (e: PackageManager.NameNotFoundException) { - app.key - } + packageInfo.loadLabel(context.packageManager).toString() } val itemsWithPermission = remember(app) { app.value.filter { // exclude activities we have no permission for val resolvedActivityInfo = context.packageManager.resolveActivity( - Intent(Intent.ACTION_VIEW).setDataAndType(it.uri, it.mimeType), + Intent(Intent.ACTION_VIEW).setPackage(it.packageName).setDataAndType(it.uri, it.mimeType), 0 )?.activityInfo ?: return@filter false resolvedActivityInfo.permission == null || context.checkPermission(resolvedActivityInfo.permission) } } + + if (itemsWithPermission.isEmpty()) continue + ContactInfo( icon = Icons.AutoMirrored.Rounded.OpenInNew, customIcon = appIcon, @@ -334,6 +321,7 @@ fun ContactItem( viewModel.reportUsage(contact) context.tryStartActivity( Intent(Intent.ACTION_VIEW).apply { + setPackage(packageName) setDataAndType( it.uri, it.mimeType diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt b/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt index 4db6316b..339e9f72 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt @@ -8,13 +8,17 @@ import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TextLayer 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 { val name: String val phoneNumbers: List val emailAddresses: List val postalAddresses: List - val contactApps: List + val contactChannels: List val summary: String get() { @@ -41,32 +45,3 @@ interface Contact : SavableSearchable { override val preferDetailsOverLaunch: Boolean 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, -} diff --git a/core/ktx/src/main/java/de/mm20/launcher2/ktx/PackageManager.kt b/core/ktx/src/main/java/de/mm20/launcher2/ktx/PackageManager.kt new file mode 100644 index 00000000..b6d8470f --- /dev/null +++ b/core/ktx/src/main/java/de/mm20/launcher2/ktx/PackageManager.kt @@ -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 + } +} \ No newline at end of file diff --git a/core/shared/src/main/java/de/mm20/launcher2/plugin/contracts/ContactPluginContract.kt b/core/shared/src/main/java/de/mm20/launcher2/plugin/contracts/ContactPluginContract.kt new file mode 100644 index 00000000..983de362 --- /dev/null +++ b/core/shared/src/main/java/de/mm20/launcher2/plugin/contracts/ContactPluginContract.kt @@ -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("id") + + /** + * The display name of the contact. + * First name + last name, if applicable. + */ + val Name = column("name") + + val PhoneNumbers = column>("phone_numbers") + val EmailAddresses = column>("email_addresses") + val PostalAddresses = column>("postal_addresses") + val ContactChannels = column>("contact_channels") + + val PhotoUri = column("photo_uri") + } +} \ No newline at end of file diff --git a/core/shared/src/main/java/de/mm20/launcher2/search/contact/ContactInfo.kt b/core/shared/src/main/java/de/mm20/launcher2/search/contact/ContactInfo.kt new file mode 100644 index 00000000..1a93a26a --- /dev/null +++ b/core/shared/src/main/java/de/mm20/launcher2/search/contact/ContactInfo.kt @@ -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, +} diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/AndroidContact.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/AndroidContact.kt index 3c5eeb95..c7643254 100644 --- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/AndroidContact.kt +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/AndroidContact.kt @@ -13,11 +13,11 @@ import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.ktx.asBitmap import de.mm20.launcher2.ktx.tryStartActivity 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.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.withContext @@ -27,8 +27,7 @@ internal data class AndroidContact( override val phoneNumbers: List, override val emailAddresses: List, override val postalAddresses: List, - override val contactApps: List, - internal val lookupKey: String, + override val contactChannels: List, internal val lookupKey: String, override val labelOverride: String? = null, ) : Contact { diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt index fd810c82..29c1eaf5 100644 --- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt @@ -12,12 +12,12 @@ 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.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.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.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -71,7 +71,7 @@ internal class ContactRepository( val phoneNumbers = mutableListOf() val emailAddresses = mutableListOf() val postalAddresses = mutableListOf() - val contactApps = mutableListOf() + val contactChannels = mutableListOf() val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE) val typeColumn = @@ -139,7 +139,7 @@ internal class ContactRepository( } else -> { - contactApps += ContactApp( + contactChannels += CustomContactChannel( label = dataCursor.getStringOrNull(data3Column) ?: continue, packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue, mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue, @@ -189,7 +189,7 @@ internal class ContactRepository( }, emailAddresses = emailAddresses.distinct(), postalAddresses = postalAddresses.distinct(), - contactApps = contactApps.distinct(), + contactChannels = contactChannels.distinct(), lookupKey = lookUpKey ) }