Restructure contacts search for plugins
This commit is contained in:
@@ -1,32 +1,21 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import android.telephony.PhoneNumberUtils
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.ktx.distinctByEquality
|
||||
import de.mm20.launcher2.contacts.providers.AndroidContactProvider
|
||||
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.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
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
|
||||
internal class ContactRepository(
|
||||
private val context: Context,
|
||||
@@ -34,167 +23,7 @@ internal class ContactRepository(
|
||||
private val settings: ContactSearchSettings,
|
||||
) : SearchableRepository<Contact> {
|
||||
|
||||
fun get(id: Long): Flow<Contact?> = flow {
|
||||
val rawContactsCursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.RawContacts._ID),
|
||||
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
)
|
||||
if (rawContactsCursor == null) {
|
||||
emit(null)
|
||||
return@flow
|
||||
}
|
||||
val rawContacts = mutableSetOf<Long>()
|
||||
while (rawContactsCursor.moveToNext()) {
|
||||
rawContacts.add(rawContactsCursor.getLong(0))
|
||||
}
|
||||
rawContactsCursor.close()
|
||||
if (rawContacts.isEmpty()) {
|
||||
emit(null)
|
||||
return@flow
|
||||
}
|
||||
emit(getWithRawIds(id, rawContacts))
|
||||
}
|
||||
|
||||
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? =
|
||||
withContext(Dispatchers.IO) {
|
||||
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
|
||||
var firstName: String? = null
|
||||
var lastName: String? = null
|
||||
var displayName: String? = null
|
||||
val phoneNumbers = mutableListOf<PhoneNumber>()
|
||||
val emailAddresses = mutableListOf<EmailAddress>()
|
||||
val postalAddresses = mutableListOf<PostalAddress>()
|
||||
val contactChannels = mutableListOf<CustomContactChannel>()
|
||||
|
||||
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 =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
val addressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
|
||||
val displayNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)
|
||||
val givenNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||
val familyNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
||||
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 {
|
||||
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 { 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 {
|
||||
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 -> {
|
||||
firstName = dataCursor.getStringOrNull(givenNameColumn)
|
||||
lastName = dataCursor.getStringOrNull(familyNameColumn)
|
||||
displayName = dataCursor.getStringOrNull(displayNameColumn)
|
||||
}
|
||||
|
||||
else -> {
|
||||
contactChannels += CustomContactChannel(
|
||||
label = dataCursor.getStringOrNull(data3Column) ?: continue,
|
||||
packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue,
|
||||
mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue,
|
||||
uri = ContentUris.withAppendedId(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataCursor.getLongOrNull(idColumn) ?: continue
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
dataCursor.close()
|
||||
|
||||
val lookupKeyCursor = context.contentResolver.query(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.Contacts.LOOKUP_KEY),
|
||||
"${ContactsContract.Contacts._ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return@withContext null
|
||||
var lookUpKey = ""
|
||||
if (lookupKeyCursor.moveToNext()) {
|
||||
lookUpKey = lookupKeyCursor.getString(0)
|
||||
}
|
||||
lookupKeyCursor.close()
|
||||
|
||||
val defaultCountryIso = context.resources.configuration.locales[0].country
|
||||
|
||||
return@withContext AndroidContact(
|
||||
id = id,
|
||||
name = displayName
|
||||
?: listOfNotNull(firstName, lastName).joinToString(" ").takeIf { it.isNotBlank() }
|
||||
?: return@withContext null,
|
||||
phoneNumbers = phoneNumbers.sortedByDescending {
|
||||
it.number.count { !PhoneNumberUtils.isReallyDialable(it) }
|
||||
}.map {
|
||||
val formattedNumber =
|
||||
PhoneNumberUtils.formatNumber(it.number, defaultCountryIso)
|
||||
?: return@map it
|
||||
it.copy(number = formattedNumber)
|
||||
}.distinctByEquality { a, b ->
|
||||
if (Build.VERSION.SDK_INT < 31) {
|
||||
PhoneNumberUtils.compare(context, a.number, b.number)
|
||||
} else {
|
||||
PhoneNumberUtils.areSamePhoneNumber(a.number, b.number, defaultCountryIso)
|
||||
}
|
||||
},
|
||||
emailAddresses = emailAddresses.distinct(),
|
||||
postalAddresses = postalAddresses.distinct(),
|
||||
contactChannels = contactChannels.distinct(),
|
||||
lookupKey = lookUpKey
|
||||
)
|
||||
}
|
||||
|
||||
override fun search(query: String, allowNetwork: Boolean): Flow<ImmutableList<Contact>> {
|
||||
override fun search(query: String, allowNetwork: Boolean): Flow<List<Contact>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
||||
|
||||
if (query.length < 2) {
|
||||
@@ -203,40 +32,28 @@ internal class ContactRepository(
|
||||
}
|
||||
}
|
||||
|
||||
return hasPermission.combine(settings.enabled) { perm, en -> perm && en }.map {
|
||||
if (it) {
|
||||
queryContacts(query)
|
||||
} else {
|
||||
persistentListOf()
|
||||
return hasPermission.combineTransform(settings.enabledProviders) { perm, providerIds ->
|
||||
val providers = providerIds.mapNotNull {
|
||||
when (it) {
|
||||
"local" -> if (perm) AndroidContactProvider(context) else null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryContacts(query: String): ImmutableList<Contact> {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
val proj = arrayOf(
|
||||
ContactsContract.RawContacts.CONTACT_ID,
|
||||
ContactsContract.RawContacts._ID
|
||||
)
|
||||
val sel =
|
||||
"${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ? OR ${ContactsContract.RawContacts.SORT_KEY_PRIMARY} LIKE ?"
|
||||
val selArgs = arrayOf("%$query%", "%$query%", "%$query%", "%$query%")
|
||||
val cursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
|
||||
) ?: return@withContext mutableListOf()
|
||||
//Maps raw contact ids to contact ids
|
||||
val contactMap = mutableMapOf<Long, MutableSet<Long>>()
|
||||
while (cursor.moveToNext()) {
|
||||
contactMap.getOrPut(cursor.getLong(0)) { mutableSetOf() }.add(cursor.getLong(1))
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(listOf<Contact>())
|
||||
|
||||
for (provider in providers) {
|
||||
launch {
|
||||
val r = provider.search(
|
||||
query,
|
||||
allowNetwork = allowNetwork,
|
||||
)
|
||||
result.update { it + r }
|
||||
}
|
||||
}
|
||||
emitAll(result)
|
||||
}
|
||||
cursor.close()
|
||||
val results = mutableListOf<Contact>()
|
||||
for ((id, rawIds) in contactMap) {
|
||||
getWithRawIds(id, rawIds)?.let { results.add(it) }
|
||||
if (results.size > 15) break
|
||||
}
|
||||
results
|
||||
}
|
||||
return results.toImmutableList()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.contacts.providers.AndroidContact
|
||||
import de.mm20.launcher2.contacts.providers.AndroidContactProvider
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
@@ -22,7 +25,7 @@ internal class ContactSerializer : SearchableSerializer {
|
||||
}
|
||||
|
||||
internal class ContactDeserializer(
|
||||
private val contactRepository: ContactRepository,
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager
|
||||
) : SearchableDeserializer {
|
||||
|
||||
@@ -30,6 +33,8 @@ internal class ContactDeserializer(
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Contacts)) return null
|
||||
val id = JSONObject(serialized).getLong("id")
|
||||
|
||||
return contactRepository.get(id).first()
|
||||
val androidContactProvider = AndroidContactProvider(context)
|
||||
|
||||
return androidContactProvider.get(id)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import de.mm20.launcher2.contacts.providers.AndroidContact
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@@ -10,5 +11,5 @@ import org.koin.dsl.module
|
||||
val contactsModule = module {
|
||||
factory { ContactRepository(androidContext(), get(), get()) }
|
||||
factory<SearchableRepository<Contact>>(named<Contact>()) { get<ContactRepository>() }
|
||||
factory<SearchableDeserializer>(named(AndroidContact.Domain)) { ContactDeserializer(get(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidContact.Domain)) { ContactDeserializer(androidContext(), get()) }
|
||||
}
|
||||
+4
-3
@@ -1,4 +1,4 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
package de.mm20.launcher2.contacts.providers
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import de.mm20.launcher2.contacts.ContactSerializer
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.icons.StaticIconLayer
|
||||
@@ -14,7 +15,7 @@ import de.mm20.launcher2.ktx.asBitmap
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.contact.CustomContactChannel
|
||||
import de.mm20.launcher2.search.contact.CustomContactAction
|
||||
import de.mm20.launcher2.search.contact.EmailAddress
|
||||
import de.mm20.launcher2.search.contact.PhoneNumber
|
||||
import de.mm20.launcher2.search.contact.PostalAddress
|
||||
@@ -27,7 +28,7 @@ internal data class AndroidContact(
|
||||
override val phoneNumbers: List<PhoneNumber>,
|
||||
override val emailAddresses: List<EmailAddress>,
|
||||
override val postalAddresses: List<PostalAddress>,
|
||||
override val contactChannels: List<CustomContactChannel>, internal val lookupKey: String,
|
||||
override val customActions: List<CustomContactAction>, internal val lookupKey: String,
|
||||
override val labelOverride: String? = null,
|
||||
) : Contact {
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package de.mm20.launcher2.contacts.providers
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import android.telephony.PhoneNumberUtils
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.contacts.providers.AndroidContact
|
||||
import de.mm20.launcher2.ktx.distinctByEquality
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.contact.ContactInfoType
|
||||
import de.mm20.launcher2.search.contact.CustomContactAction
|
||||
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
|
||||
|
||||
/**
|
||||
* A contact provider that uses the Android ContactsContract API to search for contacts.
|
||||
*/
|
||||
class AndroidContactProvider(
|
||||
private val context: Context,
|
||||
) : ContactProvider {
|
||||
override suspend fun search(
|
||||
query: String,
|
||||
allowNetwork: Boolean
|
||||
): List<Contact> {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
val proj = arrayOf(
|
||||
ContactsContract.RawContacts.CONTACT_ID,
|
||||
ContactsContract.RawContacts._ID
|
||||
)
|
||||
val sel =
|
||||
"${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ? OR ${ContactsContract.RawContacts.SORT_KEY_PRIMARY} LIKE ?"
|
||||
val selArgs = arrayOf("%$query%", "%$query%", "%$query%", "%$query%")
|
||||
val cursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
|
||||
) ?: return@withContext mutableListOf()
|
||||
//Maps raw contact ids to contact ids
|
||||
val contactMap = mutableMapOf<Long, MutableSet<Long>>()
|
||||
while (cursor.moveToNext()) {
|
||||
contactMap.getOrPut(cursor.getLong(0)) { mutableSetOf() }.add(cursor.getLong(1))
|
||||
}
|
||||
cursor.close()
|
||||
val results = mutableListOf<Contact>()
|
||||
for ((id, rawIds) in contactMap) {
|
||||
getWithRawIds(id, rawIds)?.let { results.add(it) }
|
||||
if (results.size > 15) break
|
||||
}
|
||||
results
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine the given raw contact ids into a single contact.
|
||||
*/
|
||||
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? =
|
||||
withContext(Dispatchers.IO) {
|
||||
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
|
||||
var firstName: String? = null
|
||||
var lastName: String? = null
|
||||
var displayName: String? = null
|
||||
val phoneNumbers = mutableListOf<PhoneNumber>()
|
||||
val emailAddresses = mutableListOf<EmailAddress>()
|
||||
val postalAddresses = mutableListOf<PostalAddress>()
|
||||
val customActions = mutableListOf<CustomContactAction>()
|
||||
|
||||
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 =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
val addressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
|
||||
val displayNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)
|
||||
val givenNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||
val familyNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
||||
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 {
|
||||
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 { 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 {
|
||||
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 -> {
|
||||
firstName = dataCursor.getStringOrNull(givenNameColumn)
|
||||
lastName = dataCursor.getStringOrNull(familyNameColumn)
|
||||
displayName = dataCursor.getStringOrNull(displayNameColumn)
|
||||
}
|
||||
|
||||
else -> {
|
||||
customActions += CustomContactAction(
|
||||
label = dataCursor.getStringOrNull(data3Column) ?: continue,
|
||||
packageName = dataCursor.getStringOrNull(accountTypeColumn) ?: continue,
|
||||
mimeType = dataCursor.getStringOrNull(mimeTypeColumn) ?: continue,
|
||||
uri = ContentUris.withAppendedId(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataCursor.getLongOrNull(idColumn) ?: continue
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
dataCursor.close()
|
||||
|
||||
val lookupKeyCursor = context.contentResolver.query(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.Contacts.LOOKUP_KEY),
|
||||
"${ContactsContract.Contacts._ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return@withContext null
|
||||
var lookUpKey = ""
|
||||
if (lookupKeyCursor.moveToNext()) {
|
||||
lookUpKey = lookupKeyCursor.getString(0)
|
||||
}
|
||||
lookupKeyCursor.close()
|
||||
|
||||
val defaultCountryIso = context.resources.configuration.locales[0].country
|
||||
|
||||
return@withContext AndroidContact(
|
||||
id = id,
|
||||
name = displayName
|
||||
?: listOfNotNull(firstName, lastName).joinToString(" ")
|
||||
.takeIf { it.isNotBlank() }
|
||||
?: return@withContext null,
|
||||
phoneNumbers = phoneNumbers.sortedByDescending {
|
||||
it.number.count { !PhoneNumberUtils.isReallyDialable(it) }
|
||||
}.map {
|
||||
val formattedNumber =
|
||||
PhoneNumberUtils.formatNumber(it.number, defaultCountryIso)
|
||||
?: return@map it
|
||||
it.copy(number = formattedNumber)
|
||||
}.distinctByEquality { a, b ->
|
||||
if (Build.VERSION.SDK_INT < 31) {
|
||||
PhoneNumberUtils.compare(context, a.number, b.number)
|
||||
} else {
|
||||
PhoneNumberUtils.areSamePhoneNumber(a.number, b.number, defaultCountryIso)
|
||||
}
|
||||
},
|
||||
emailAddresses = emailAddresses.distinct(),
|
||||
postalAddresses = postalAddresses.distinct(),
|
||||
customActions = customActions.distinct(),
|
||||
lookupKey = lookUpKey
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a contact by its id, or null if it doesn't exist.
|
||||
*/
|
||||
suspend fun get(id: Long): Contact? = withContext(Dispatchers.IO) {
|
||||
val rawContactsCursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.RawContacts._ID),
|
||||
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
)
|
||||
if (rawContactsCursor == null) {
|
||||
return@withContext null
|
||||
}
|
||||
val rawContacts = mutableSetOf<Long>()
|
||||
while (rawContactsCursor.moveToNext()) {
|
||||
rawContacts.add(rawContactsCursor.getLong(0))
|
||||
}
|
||||
rawContactsCursor.close()
|
||||
if (rawContacts.isEmpty()) {
|
||||
return@withContext null
|
||||
}
|
||||
return@withContext getWithRawIds(id, rawContacts)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.mm20.launcher2.contacts.providers
|
||||
|
||||
import de.mm20.launcher2.search.Contact
|
||||
|
||||
interface ContactProvider {
|
||||
suspend fun search(query: String, allowNetwork: Boolean): List<Contact>
|
||||
}
|
||||
Reference in New Issue
Block a user