Skip to content

Instantly share code, notes, and snippets.

@tarunmahe
Forked from bugraoral/Contact.java
Created August 31, 2017 14:44
Show Gist options
  • Save tarunmahe/043500d501e21dc8aa1765c2c82dbfec to your computer and use it in GitHub Desktop.
Save tarunmahe/043500d501e21dc8aa1765c2c82dbfec 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);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment