Contact stuff
This commit is contained in:
parent
0ebce91a43
commit
04dd12ee96
@ -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
|
||||||
|
|||||||
@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user