Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce DisplayNameStyle in ContactStore#fetchContacts #39

Merged
merged 1 commit into from Jan 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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
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
@@ -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
}
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