Created
May 10, 2016 07:13
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
This is very useful, thanks for sharing. Would you mind offering how to use it?