From f6dbbad7eb44a410f8803a29554cb752dbf25b68 Mon Sep 17 00:00:00 2001 From: Alex Styl <1665273+alexstyl@users.noreply.github.com> Date: Sun, 9 Jan 2022 19:10:30 +0200 Subject: [PATCH] Introduce DisplayNameStyle in ContactStore#fetchContacts The preferred style for the [Contact.displayName] to be returned. The fetched contacts' sorting order will match this option. --- .../contactstore/test/TestContactStore.kt | 4 +- .../contactstore/AndroidContactStore.kt | 9 +- .../alexstyl/contactstore/ContactQueries.kt | 211 +++++++++++++----- .../com/alexstyl/contactstore/ContactStore.kt | 4 +- .../alexstyl/contactstore/DisplayNameStyle.kt | 15 ++ .../ExistingContactOperationsFactory.kt | 3 +- .../contactstore/ContactStoreDSLKtTest.kt | 3 +- 7 files changed, 187 insertions(+), 62 deletions(-) create mode 100644 library/src/main/java/com/alexstyl/contactstore/DisplayNameStyle.kt diff --git a/library-test/src/main/java/com/alexstyl/contactstore/test/TestContactStore.kt b/library-test/src/main/java/com/alexstyl/contactstore/test/TestContactStore.kt index 111abbcb..dec8d88f 100644 --- a/library-test/src/main/java/com/alexstyl/contactstore/test/TestContactStore.kt +++ b/library-test/src/main/java/com/alexstyl/contactstore/test/TestContactStore.kt @@ -6,6 +6,7 @@ import com.alexstyl.contactstore.ContactColumn import com.alexstyl.contactstore.ContactOperation import com.alexstyl.contactstore.ContactPredicate import com.alexstyl.contactstore.ContactStore +import com.alexstyl.contactstore.DisplayNameStyle import com.alexstyl.contactstore.ExperimentalContactStoreApi import com.alexstyl.contactstore.MutableContact import com.alexstyl.contactstore.PartialContact @@ -168,7 +169,8 @@ public class TestContactStore( override fun fetchContacts( predicate: ContactPredicate?, - columnsToFetch: List + columnsToFetch: List, + displayNameStyle: DisplayNameStyle ): Flow> { return snapshot .map { contacts -> diff --git a/library/src/main/java/com/alexstyl/contactstore/AndroidContactStore.kt b/library/src/main/java/com/alexstyl/contactstore/AndroidContactStore.kt index 334781b1..ec82a0ba 100644 --- a/library/src/main/java/com/alexstyl/contactstore/AndroidContactStore.kt +++ b/library/src/main/java/com/alexstyl/contactstore/AndroidContactStore.kt @@ -2,7 +2,9 @@ package com.alexstyl.contactstore import android.content.ContentResolver import android.provider.ContactsContract -import com.alexstyl.contactstore.ContactOperation.* +import com.alexstyl.contactstore.ContactOperation.Delete +import com.alexstyl.contactstore.ContactOperation.Insert +import com.alexstyl.contactstore.ContactOperation.Update import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -28,8 +30,9 @@ internal class AndroidContactStore( override fun fetchContacts( predicate: ContactPredicate?, - columnsToFetch: List + columnsToFetch: List, + displayNameStyle: DisplayNameStyle ): Flow> { - return contactQueries.queryContacts(predicate, columnsToFetch) + return contactQueries.queryContacts(predicate, columnsToFetch, displayNameStyle) } } diff --git a/library/src/main/java/com/alexstyl/contactstore/ContactQueries.kt b/library/src/main/java/com/alexstyl/contactstore/ContactQueries.kt index 7f9e84d5..bd9eb920 100644 --- a/library/src/main/java/com/alexstyl/contactstore/ContactQueries.kt +++ b/library/src/main/java/com/alexstyl/contactstore/ContactQueries.kt @@ -58,9 +58,10 @@ internal class ContactQueries( fun queryContacts( predicate: ContactPredicate?, - columnsToFetch: List + columnsToFetch: List, + displayNameStyle: DisplayNameStyle ): Flow> { - return queryContacts(predicate) + return queryContacts(predicate, displayNameStyle) .map { contacts -> if (columnsToFetch.isEmpty()) { contacts @@ -70,78 +71,81 @@ internal class ContactQueries( } } - private fun queryContacts(predicate: ContactPredicate?): Flow> { + private fun queryContacts( + predicate: ContactPredicate?, + displayNameStyle: DisplayNameStyle + ): Flow> { return when (predicate) { - null -> queryAllContacts() - is ContactLookup -> lookupFromPredicate(predicate) - is MailLookup -> lookupFromMail(predicate.mailAddress) - is PhoneLookup -> lookupFromPhone(predicate.phoneNumber) - is NameLookup -> lookupFromName(predicate.partOfName) + null -> queryAllContacts(displayNameStyle) + is ContactLookup -> lookupFromPredicate(predicate, displayNameStyle) + is MailLookup -> lookupFromMail(predicate.mailAddress, displayNameStyle) + is PhoneLookup -> lookupFromPhone(predicate.phoneNumber, displayNameStyle) + is NameLookup -> lookupFromName(predicate.partOfName, displayNameStyle) } } - private fun lookupFromName(name: String): Flow> { + private fun lookupFromName( + name: String, + displayNameStyle: DisplayNameStyle + ): Flow> { return contentResolver.runQueryFlow( contentUri = Contacts.CONTENT_FILTER_URI.buildUpon() .appendEncodedPath(name) .build(), - projection = SimpleQuery.PROJECTION, - selection = null, - sortOrder = Contacts.SORT_KEY_PRIMARY + projection = ContactsQuery.projection(displayNameStyle), + sortOrder = ContactsQuery.sortOrder(displayNameStyle), ).map { cursor -> cursor.mapEachRow { PartialContact( - contactId = SimpleQuery.getContactId(it), - lookupKey = SimpleQuery.getLookupKey(it), - displayName = SimpleQuery.getDisplayName(it), - isStarred = SimpleQuery.getIsStarred(it), + contactId = ContactsQuery.getContactId(it), + lookupKey = ContactsQuery.getLookupKey(it), + displayName = ContactsQuery.getDisplayName(it), + isStarred = ContactsQuery.getIsStarred(it), columns = emptyList() ) } } } - private fun lookupFromPredicate(predicate: ContactLookup): Flow> { + private fun lookupFromPredicate( + predicate: ContactLookup, + displayNameStyle: DisplayNameStyle + ): Flow> { return contentResolver.runQueryFlow( contentUri = Contacts.CONTENT_URI, - projection = SimpleQuery.PROJECTION, + projection = ContactsQuery.projection(displayNameStyle), selection = buildColumnsToFetchSelection(predicate), - sortOrder = Contacts.SORT_KEY_PRIMARY + sortOrder = ContactsQuery.sortOrder(displayNameStyle) ).map { cursor -> cursor.mapEachRow { PartialContact( - contactId = SimpleQuery.getContactId(it), - lookupKey = SimpleQuery.getLookupKey(it), - displayName = SimpleQuery.getDisplayName(it), - isStarred = SimpleQuery.getIsStarred(it), + contactId = ContactsQuery.getContactId(it), + lookupKey = ContactsQuery.getLookupKey(it), + displayName = ContactsQuery.getDisplayName(it), + isStarred = ContactsQuery.getIsStarred(it), columns = emptyList() ) } } } - private fun lookupFromMail(mailAddress: MailAddress): Flow> { + private fun lookupFromMail( + mailAddress: MailAddress, + displayNameStyle: DisplayNameStyle + ): Flow> { return contentResolver.runQueryFlow( contentUri = EmailColumns.CONTENT_FILTER_URI.buildUpon() .appendEncodedPath(mailAddress.raw) .build(), - projection = arrayOf( - EmailColumns.CONTACT_ID, - EmailColumns.DISPLAY_NAME_PRIMARY, - EmailColumns.STARRED, - EmailColumns.LOOKUP_KEY, - ), - selection = null, - sortOrder = EmailColumns.SORT_KEY_PRIMARY + projection = FilterQuery.projection(displayNameStyle), + sortOrder = FilterQuery.sortOrder(displayNameStyle) ).map { cursor -> cursor.mapEachRow { PartialContact( - contactId = it.getLong(0), - displayName = it.getString(1), - isStarred = it.getInt(2) == 1, - lookupKey = it.getString(3)?.let { raw -> - LookupKey(raw) - }, + contactId = FilterQuery.getContactId(it), + displayName = FilterQuery.getDisplayName(it), + isStarred = FilterQuery.getIsStarred(it), + lookupKey = FilterQuery.getLookupKey(it), columns = emptyList() ) } @@ -162,20 +166,29 @@ internal class ContactQueries( } } - private fun lookupFromPhone(phoneNumber: PhoneNumber): Flow> { + private fun lookupFromPhone( + phoneNumber: PhoneNumber, + displayNameStyle: DisplayNameStyle + ): Flow> { return contentResolver.runQueryFlow( contentUri = ContactsContract.PhoneLookup.CONTENT_FILTER_URI.buildUpon() .appendEncodedPath(phoneNumber.raw) .build(), projection = arrayOf( PHONE_LOOKUP_CONTACT_ID, - ContactsContract.PhoneLookup.DISPLAY_NAME_PRIMARY, + if (displayNameStyle == DisplayNameStyle.Primary) { + ContactsContract.PhoneLookup.DISPLAY_NAME_PRIMARY + } else { + ContactsContract.PhoneLookup.DISPLAY_NAME_ALTERNATIVE + }, ContactsContract.PhoneLookup.STARRED, ContactsContract.PhoneLookup.LOOKUP_KEY ), - // using DISPLAY_NAME_PRIMARY as ContactsContract.PhoneLookup.SORT_KEY_PRIMARY - // throws an column name ambiguous error - sortOrder = Contacts.DISPLAY_NAME_PRIMARY + sortOrder = if (displayNameStyle == DisplayNameStyle.Primary) { + ContactsContract.PhoneLookup.DISPLAY_NAME_PRIMARY + } else { + ContactsContract.PhoneLookup.DISPLAY_NAME_ALTERNATIVE + } ).map { cursor -> cursor.mapEachRow { PartialContact( @@ -190,19 +203,20 @@ internal class ContactQueries( } - private fun queryAllContacts(): Flow> { + private fun queryAllContacts( + displayNameStyle: DisplayNameStyle + ): Flow> { return contentResolver.runQueryFlow( contentUri = Contacts.CONTENT_URI, - projection = SimpleQuery.PROJECTION, - selection = null, - sortOrder = Contacts.SORT_KEY_PRIMARY + projection = ContactsQuery.projection(displayNameStyle), + sortOrder = ContactsQuery.sortOrder(displayNameStyle) ).map { cursor -> cursor.mapEachRow { PartialContact( - contactId = SimpleQuery.getContactId(it), - lookupKey = SimpleQuery.getLookupKey(it), - displayName = SimpleQuery.getDisplayName(it), - isStarred = SimpleQuery.getIsStarred(it), + contactId = ContactsQuery.getContactId(it), + lookupKey = ContactsQuery.getLookupKey(it), + displayName = ContactsQuery.getDisplayName(it), + isStarred = ContactsQuery.getIsStarred(it), columns = emptyList() ) } @@ -648,20 +662,43 @@ internal class ContactQueries( } } - private object SimpleQuery { - val PROJECTION = arrayOf( + private object ContactsQuery { + fun projection(displayNameStyle: DisplayNameStyle): Array { + return when (displayNameStyle) { + DisplayNameStyle.Primary -> PROJECTION + DisplayNameStyle.Alternative -> PROJECTION_ALT + } + } + + private val PROJECTION = arrayOf( Contacts._ID, Contacts.DISPLAY_NAME_PRIMARY, Contacts.STARRED, Contacts.LOOKUP_KEY ) + private val PROJECTION_ALT = arrayOf( + Contacts._ID, + Contacts.DISPLAY_NAME_ALTERNATIVE, + Contacts.STARRED, + Contacts.LOOKUP_KEY + ) + + private const val SORT_ORDER = Contacts.SORT_KEY_PRIMARY + private const val SORT_ORDER_ALT = Contacts.SORT_KEY_ALTERNATIVE + fun getContactId(it: Cursor): Long { return it.getLong(0) } - fun getDisplayName(it: Cursor): String? { - return it.getString(1) + fun getDisplayName(cursor: Cursor): String? { + val indexPrimary = cursor.getColumnIndex(Contacts.DISPLAY_NAME_PRIMARY) + return if (indexPrimary == -1) { + val indexAlternative = cursor.getColumnIndex(Contacts.DISPLAY_NAME_ALTERNATIVE) + cursor.getString(indexAlternative) + } else { + cursor.getString(indexPrimary) + } } fun getIsStarred(it: Cursor): Boolean { @@ -673,6 +710,70 @@ internal class ContactQueries( LookupKey(raw) } } + + fun sortOrder(displayNameStyle: DisplayNameStyle): String { + return when (displayNameStyle) { + DisplayNameStyle.Primary -> SORT_ORDER + DisplayNameStyle.Alternative -> SORT_ORDER_ALT + } + } + } + + private object FilterQuery { + fun projection(displayNameStyle: DisplayNameStyle): Array { + return when (displayNameStyle) { + DisplayNameStyle.Primary -> PROJECTION + DisplayNameStyle.Alternative -> PROJECTION_ALT + } + } + + private val PROJECTION = arrayOf( + EmailColumns.CONTACT_ID, + EmailColumns.DISPLAY_NAME_PRIMARY, + EmailColumns.STARRED, + EmailColumns.LOOKUP_KEY + ) + + private val PROJECTION_ALT = arrayOf( + EmailColumns.CONTACT_ID, + EmailColumns.DISPLAY_NAME_ALTERNATIVE, + EmailColumns.STARRED, + EmailColumns.LOOKUP_KEY + ) + + private const val SORT_ORDER = EmailColumns.SORT_KEY_PRIMARY + private const val SORT_ORDER_ALT = EmailColumns.SORT_KEY_ALTERNATIVE + + fun getContactId(it: Cursor): Long { + return it.getLong(0) + } + + fun getDisplayName(cursor: Cursor): String? { + val indexPrimary = cursor.getColumnIndex(EmailColumns.DISPLAY_NAME_PRIMARY) + return if (indexPrimary == -1) { + val indexAlternative = cursor.getColumnIndex(EmailColumns.DISPLAY_NAME_ALTERNATIVE) + cursor.getString(indexAlternative) + } else { + cursor.getString(indexPrimary) + } + } + + fun getIsStarred(it: Cursor): Boolean { + return it.getInt(2) == 1 + } + + fun getLookupKey(it: Cursor): LookupKey? { + return it.getString(3)?.let { raw -> + LookupKey(raw) + } + } + + fun sortOrder(displayNameStyle: DisplayNameStyle): String { + return when (displayNameStyle) { + DisplayNameStyle.Primary -> SORT_ORDER + DisplayNameStyle.Alternative -> SORT_ORDER_ALT + } + } } private companion object { diff --git a/library/src/main/java/com/alexstyl/contactstore/ContactStore.kt b/library/src/main/java/com/alexstyl/contactstore/ContactStore.kt index 27f44357..397a18bf 100644 --- a/library/src/main/java/com/alexstyl/contactstore/ContactStore.kt +++ b/library/src/main/java/com/alexstyl/contactstore/ContactStore.kt @@ -28,10 +28,12 @@ public interface ContactStore { * * @param predicate The conditions that a contact need to meet in order to be fetched * @param columnsToFetch The columns of the contact you need to be fetched + * @param displayNameStyle The preferred style for the [Contact.displayName] to be returned. The fetched contacts' sorting order will match this option. */ public fun fetchContacts( predicate: ContactPredicate? = null, - columnsToFetch: List = emptyList() + columnsToFetch: List = emptyList(), + displayNameStyle: DisplayNameStyle = DisplayNameStyle.Primary ): Flow> public companion object { diff --git a/library/src/main/java/com/alexstyl/contactstore/DisplayNameStyle.kt b/library/src/main/java/com/alexstyl/contactstore/DisplayNameStyle.kt new file mode 100644 index 00000000..ef333685 --- /dev/null +++ b/library/src/main/java/com/alexstyl/contactstore/DisplayNameStyle.kt @@ -0,0 +1,15 @@ +package com.alexstyl.contactstore + +public enum class DisplayNameStyle { + /** + * The standard text shown as the contact's display name, based on the best available information for the contact (for example, it might be the email address if the name + * is not available). + */ + Primary, + + /** + * An alternative representation of the display name, such as "family name first" instead of "given name first" for Western names. + * If an alternative is not available, the values should be the same as [Primary]. + */ + Alternative +} diff --git a/library/src/main/java/com/alexstyl/contactstore/ExistingContactOperationsFactory.kt b/library/src/main/java/com/alexstyl/contactstore/ExistingContactOperationsFactory.kt index 99b42713..4627994e 100644 --- a/library/src/main/java/com/alexstyl/contactstore/ExistingContactOperationsFactory.kt +++ b/library/src/main/java/com/alexstyl/contactstore/ExistingContactOperationsFactory.kt @@ -53,7 +53,8 @@ internal class ExistingContactOperationsFactory( private suspend fun updateSuspend(contact: MutableContact): List { val existingContact = contactQueries.queryContacts( predicate = ContactPredicate.ContactLookup(inContactIds = listOf(contact.contactId)), - columnsToFetch = contact.columns + columnsToFetch = contact.columns, + displayNameStyle = DisplayNameStyle.Primary ).first().firstOrNull() ?: return emptyList() return updatesNames(contact) + replacePhoto(contact) + diff --git a/library/src/test/java/com/alexstyl/contactstore/ContactStoreDSLKtTest.kt b/library/src/test/java/com/alexstyl/contactstore/ContactStoreDSLKtTest.kt index 4cc6b96e..b20558a2 100644 --- a/library/src/test/java/com/alexstyl/contactstore/ContactStoreDSLKtTest.kt +++ b/library/src/test/java/com/alexstyl/contactstore/ContactStoreDSLKtTest.kt @@ -124,7 +124,8 @@ internal class ContactStoreDSLKtTest { override fun fetchContacts( predicate: ContactPredicate?, - columnsToFetch: List + columnsToFetch: List, + displayNameStyle: DisplayNameStyle ): Flow> { return emptyFlow() }