Skip to content

Commit

Permalink
Introduce DisplayNameStyle in ContactStore#fetchContacts
Browse files Browse the repository at this point in the history
The preferred style for the [Contact.displayName] to be returned. The fetched contacts' sorting order will match this option.
  • Loading branch information
alexstyl committed Jan 9, 2022
1 parent 1c6b0ee commit c704803
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -168,7 +169,8 @@ public class TestContactStore(

override fun fetchContacts(
predicate: ContactPredicate?,
columnsToFetch: List<ContactColumn>
columnsToFetch: List<ContactColumn>,
displayNameStyle: DisplayNameStyle
): Flow<List<Contact>> {
return snapshot
.map { contacts ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,8 +30,9 @@ internal class AndroidContactStore(

override fun fetchContacts(
predicate: ContactPredicate?,
columnsToFetch: List<ContactColumn>
columnsToFetch: List<ContactColumn>,
displayNameStyle: DisplayNameStyle
): Flow<List<Contact>> {
return contactQueries.queryContacts(predicate, columnsToFetch)
return contactQueries.queryContacts(predicate, columnsToFetch, displayNameStyle)
}
}
211 changes: 156 additions & 55 deletions library/src/main/java/com/alexstyl/contactstore/ContactQueries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ internal class ContactQueries(

fun queryContacts(
predicate: ContactPredicate?,
columnsToFetch: List<ContactColumn>
columnsToFetch: List<ContactColumn>,
displayNameStyle: DisplayNameStyle
): Flow<List<Contact>> {
return queryContacts(predicate)
return queryContacts(predicate, displayNameStyle)
.map { contacts ->
if (columnsToFetch.isEmpty()) {
contacts
Expand All @@ -70,78 +71,81 @@ internal class ContactQueries(
}
}

private fun queryContacts(predicate: ContactPredicate?): Flow<List<PartialContact>> {
private fun queryContacts(
predicate: ContactPredicate?,
displayNameStyle: DisplayNameStyle
): Flow<List<PartialContact>> {
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<List<PartialContact>> {
private fun lookupFromName(
name: String,
displayNameStyle: DisplayNameStyle
): Flow<List<PartialContact>> {
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<List<PartialContact>> {
private fun lookupFromPredicate(
predicate: ContactLookup,
displayNameStyle: DisplayNameStyle
): Flow<List<PartialContact>> {
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<List<PartialContact>> {
private fun lookupFromMail(
mailAddress: MailAddress,
displayNameStyle: DisplayNameStyle
): Flow<List<PartialContact>> {
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()
)
}
Expand All @@ -162,20 +166,29 @@ internal class ContactQueries(
}
}

private fun lookupFromPhone(phoneNumber: PhoneNumber): Flow<List<PartialContact>> {
private fun lookupFromPhone(
phoneNumber: PhoneNumber,
displayNameStyle: DisplayNameStyle
): Flow<List<PartialContact>> {
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(
Expand All @@ -190,19 +203,20 @@ internal class ContactQueries(

}

private fun queryAllContacts(): Flow<List<PartialContact>> {
private fun queryAllContacts(
displayNameStyle: DisplayNameStyle
): Flow<List<PartialContact>> {
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()
)
}
Expand Down Expand Up @@ -648,20 +662,43 @@ internal class ContactQueries(
}
}

private object SimpleQuery {
val PROJECTION = arrayOf(
private object ContactsQuery {
fun projection(displayNameStyle: DisplayNameStyle): Array<String> {
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 {
Expand All @@ -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<String> {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContactColumn> = emptyList()
columnsToFetch: List<ContactColumn> = emptyList(),
displayNameStyle: DisplayNameStyle = DisplayNameStyle.Primary
): Flow<List<Contact>>

public companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ internal class ExistingContactOperationsFactory(
private suspend fun updateSuspend(contact: MutableContact): List<ContentProviderOperation> {
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) +
Expand Down
Loading

0 comments on commit c704803

Please sign in to comment.