Skip to content

Instantly share code, notes, and snippets.

@bugraoral
Created May 10, 2016 07:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bugraoral/a4d36d79621455fa3dd860ff994ae796 to your computer and use it in GitHub Desktop.
Save bugraoral/a4d36d79621455fa3dd860ff994ae796 to your computer and use it in GitHub Desktop.
Loading contacts with thumbnail images faster. Normally, with any loader fetching 500 contacts with images can take up to 3 seconds. With this task you can load them in ~500ms.
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.text.TextUtils;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class Contact {
public static final int EMAIL = 0;
public static final int PHONE_NUMBER = 1;
/**
* The interface Contact type.
*/
@IntDef({EMAIL, PHONE_NUMBER})
@Retention(RetentionPolicy.SOURCE)
public @interface ContactType {
}
private String firstName;
private String lastName;
private String profileImageUrl;
private String contactItem;
@ContactType
private int contactType;
/**
* Instantiates a new Contact.
*
* @param displayName the display name
* @param contactItem the contact item
*/
public Contact(String displayName, String contactItem) {
this(displayName, contactItem, null);
}
/**
* Instantiates a new Contact.
*
* @param displayName the display name
* @param contactItem the contact item
* @param profileImageUrl the profile image url
*/
public Contact(String displayName, String contactItem, String profileImageUrl) {
if (TextUtils.isEmpty(displayName)) {
this.firstName = "";
this.lastName = "";
} else {
final int splitIndex = displayName.lastIndexOf(" ");
this.firstName = splitIndex == -1 ? displayName : displayName.substring(0, splitIndex);
this.lastName = splitIndex == -1 ? "" : displayName.substring(splitIndex + 1);
}
this.contactItem = contactItem;
this.contactType = !TextUtils.isEmpty(email)
&& android.util.Patterns.EMAIL_ADDRESS.matcher(contactItem).matches() ? EMAIL : PHONE_NUMBER;
this.profileImageUrl = profileImageUrl;
}
/**
* Gets display name.
*
* @return the display name
*/
public String getDisplayName() {
String displayName = "";
if (!TextUtils.isEmpty(firstName)) {
displayName = firstName;
}
if (!TextUtils.isEmpty(lastName)) {
displayName = displayName + " " + lastName;
}
return displayName;
}
/**
* Gets profile image url.
*
* @return the profile image url
*/
public String getProfileImageUrl() {
return profileImageUrl;
}
/**
* Gets contact type.
*
* @return the contact type
*/
@ContactType
public int getContactType() {
return contactType;
}
public String getContactItem() {
return contactItem;
}
}
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.regex.Pattern;
class ContactsAsyncTask extends AsyncTask<Void, Void, ArrayList<Contact>> {
private ContentResolver resolver;
private final ArrayList<Contact> contacts;
private ContactsTaskListener listener;
private ArrayList<Thumbnail> thumbUrls;
private Thumbnail compareThumb = new Thumbnail();
@Nullable
private final Pattern validEmailPattern;
@Nullable
private final Pattern validPhoneNumberPattern;
/**
* Callback for result delivery.
*/
interface ContactsTaskListener {
/**
* Triggered when contacts are loaded and sorted by their name from content provider.
*
* @param contacts the contacts
*/
void onLocalContactsFetched(ArrayList<Contact> contacts);
}
/**
* Const.
*
* @param resolver to fetch contacts.
* @param listener to deliver result.
* @param validEmailRegex the valid email regex
* @param validPhoneNumberRegex the valid phone number regex
*/
public ContactsAsyncTask(
ContentResolver resolver,
ContactsTaskListener listener,
String validEmailRegex,
String validPhoneNumberRegex) {
this.contacts = new ArrayList<>();
this.resolver = resolver;
this.listener = listener;
this.validEmailPattern = TextUtils.isEmpty(validEmailRegex)
? null
: Pattern.compile(validEmailRegex);
this.validPhoneNumberPattern = TextUtils.isEmpty(validPhoneNumberRegex)
? null
: Pattern.compile(validPhoneNumberRegex);
}
@Override
protected ArrayList<Contact> doInBackground(Void... params) {
long start = System.currentTimeMillis();
Log.e(getClass().getSimpleName(), "Contacts started to load ");
loadThumbUrls();
Log.e(getClass().getSimpleName(), "Url's loaded in " + (System.currentTimeMillis() - start));
addEmails(resolver);
addPhoneNumbers(resolver);
sortContacts();
Log.e(getClass().getSimpleName(), "Contacts loaded in " + (System.currentTimeMillis() - start));
return contacts;
}
@Override
protected void onPostExecute(ArrayList<Contact> contacts) {
if (listener != null) {
listener.onLocalContactsFetched(contacts);
}
}
/**
* Sorts contacts by their name.
*/
private void sortContacts() {
Collections.sort(contacts, new Comparator<Contact>() {
@Override
public int compare(Contact lhs, Contact rhs) {
// TODO: Might need to sort based on emailOrPhoneNumber.
return getContactNameForSorting(lhs)
.compareToIgnoreCase(getContactNameForSorting(rhs));
}
});
}
/**
* Creates a string from the name of a contact which is suitable for sorting.
* Names which does not start with an alphabetic character should be at the
* bottom of the list. This is achieved by concatenating a "~" at the start
* of the name.
*
* @param contact contact
* @return name suitable for sorting
*/
private String getContactNameForSorting(Contact contact) {
final String name = contact.getDisplayName();
if (TextUtils.isEmpty(name)) {
return "~";
}
return Character.isAlphabetic(name.charAt(0)) ? name : "~" + name;
}
/**
* Adds phone numbers as contacts to the list.
*
* @param contentResolver data source
*/
private void addPhoneNumbers(ContentResolver contentResolver) {
final Cursor phoneCursor = contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null, null, null, null
);
if (phoneCursor == null) {
return;
}
while (phoneCursor.moveToNext()) {
final String phoneNumber = phoneCursor.getString(
phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
);
if (validPhoneNumberPattern != null
&& !validPhoneNumberPattern.matcher(phoneNumber).matches()) {
continue;
}
final String contactId = phoneCursor.getString(
phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
);
String photoUrl = getPhotoUrl(contactId);
Contact contact = new Contact(getContactName(phoneCursor), phoneNumber, photoUrl);
contacts.add(contact);
}
phoneCursor.close();
}
/**
* Loads thumbnail urls' to memory to be used for faster look up when contacts
* are being added to the list.
*/
private void loadThumbUrls() {
thumbUrls = new ArrayList<>();
Cursor cursor = resolver.query(
ContactsContract.Contacts.CONTENT_URI,
new String[]{ContactsContract.Contacts._ID, ContactsContract.Contacts.PHOTO_THUMBNAIL_URI},
null,
null,
ContactsContract.Contacts._ID
);
if (cursor == null) {
return;
}
int idColumn = cursor.getColumnIndex(ContactsContract.Contacts._ID);
int urlColumn = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI);
while (cursor.moveToNext()) {
String url = cursor.getString(urlColumn);
if (url == null) {
continue;
}
Thumbnail thumbnail = new Thumbnail();
thumbnail.contactId = Long.parseLong(cursor.getString(idColumn));
thumbnail.url = url;
thumbUrls.add(thumbnail);
}
cursor.close();
}
/**
* Finds the url of photos from local kept thumb list.
* <p/>
* Uses binary search for lookup
*
* @param contactId id of contact.
* @return thumb url of content
*/
private String getPhotoUrl(String contactId) {
compareThumb.contactId = Long.parseLong(contactId);
int position = Collections.binarySearch(thumbUrls, compareThumb);
if (position < 0) {
return null;
}
return thumbUrls.get(position).url;
}
/**
* Adds emails as contacts to the list.
*
* @param contentResolver data source
*/
private void addEmails(ContentResolver contentResolver) {
final Cursor emailCursor = contentResolver.query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
null, null, null, null
);
if (emailCursor == null) {
return;
}
while (emailCursor.moveToNext()) {
final String email = emailCursor.getString(
emailCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
);
if (validEmailPattern != null && !validEmailPattern.matcher(email).matches()) {
continue;
}
final String contactId = emailCursor.getString(
emailCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
);
String url = getPhotoUrl(contactId);
Contact contact = new Contact(getContactName(emailCursor), email, url);
contacts.add(contact);
}
emailCursor.close();
}
/**
* Finds the contacts name from cursor column.
*
* @param cursor with position.
* @return name in the current row.
*/
private String getContactName(Cursor cursor) {
final int nameSource = cursor.getInt(
cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_SOURCE)
);
return ContactsContract.DisplayNameSources.STRUCTURED_NAME == nameSource
? cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
: "";
}
/**
* Wrapper class for local storing.
*/
private static class Thumbnail implements Comparable<Thumbnail> {
private long contactId;
private String url;
@Override
public int compareTo(Thumbnail another) {
return another == null ? 1 : Long.compare(contactId, another.contactId);
}
}
}
@ward459
Copy link

ward459 commented Feb 1, 2017

This is very useful, thanks for sharing. Would you mind offering how to use it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment