Home | History | Annotate | Download | only in data
      1 package com.android.mms.data;
      2 
      3 import java.io.IOException;
      4 import java.io.InputStream;
      5 import java.nio.CharBuffer;
      6 import java.util.ArrayList;
      7 import java.util.Arrays;
      8 import java.util.HashMap;
      9 import java.util.HashSet;
     10 import java.util.List;
     11 
     12 import android.content.ContentUris;
     13 import android.content.Context;
     14 import android.database.ContentObserver;
     15 import android.database.Cursor;
     16 import android.graphics.Bitmap;
     17 import android.graphics.BitmapFactory;
     18 import android.graphics.drawable.BitmapDrawable;
     19 import android.graphics.drawable.Drawable;
     20 import android.net.Uri;
     21 import android.os.Handler;
     22 import android.provider.ContactsContract.Contacts;
     23 import android.provider.ContactsContract.Data;
     24 import android.provider.ContactsContract.Presence;
     25 import android.provider.ContactsContract.CommonDataKinds.Email;
     26 import android.provider.ContactsContract.CommonDataKinds.Phone;
     27 import android.provider.Telephony.Mms;
     28 import android.telephony.PhoneNumberUtils;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 
     32 import android.database.sqlite.SqliteWrapper;
     33 import com.android.mms.ui.MessageUtils;
     34 import com.android.mms.LogTag;
     35 
     36 public class Contact {
     37     private static final String TAG = "Contact";
     38     private static final boolean V = false;
     39     private static ContactsCache sContactCache;
     40 
     41 //    private static final ContentObserver sContactsObserver = new ContentObserver(new Handler()) {
     42 //        @Override
     43 //        public void onChange(boolean selfUpdate) {
     44 //            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
     45 //                log("contact changed, invalidate cache");
     46 //            }
     47 //            invalidateCache();
     48 //        }
     49 //    };
     50 
     51     private static final ContentObserver sPresenceObserver = new ContentObserver(new Handler()) {
     52         @Override
     53         public void onChange(boolean selfUpdate) {
     54             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
     55                 log("presence changed, invalidate cache");
     56             }
     57             invalidateCache();
     58         }
     59     };
     60 
     61     private final static HashSet<UpdateListener> mListeners = new HashSet<UpdateListener>();
     62 
     63     private String mNumber;
     64     private String mName;
     65     private String mNameAndNumber;   // for display, e.g. Fred Flintstone <670-782-1123>
     66     private boolean mNumberIsModified; // true if the number is modified
     67 
     68     private long mRecipientId;       // used to find the Recipient cache entry
     69     private String mLabel;
     70     private long mPersonId;
     71     private int mPresenceResId;      // TODO: make this a state instead of a res ID
     72     private String mPresenceText;
     73     private BitmapDrawable mAvatar;
     74     private byte [] mAvatarData;
     75     private boolean mIsStale;
     76     private boolean mQueryPending;
     77 
     78     public interface UpdateListener {
     79         public void onUpdate(Contact updated);
     80     }
     81 
     82     /*
     83      * Make a basic contact object with a phone number.
     84      */
     85     private Contact(String number) {
     86         mName = "";
     87         setNumber(number);
     88         mNumberIsModified = false;
     89         mLabel = "";
     90         mPersonId = 0;
     91         mPresenceResId = 0;
     92         mIsStale = true;
     93     }
     94 
     95     @Override
     96     public String toString() {
     97         return String.format("{ number=%s, name=%s, nameAndNumber=%s, label=%s, person_id=%d, hash=%d }",
     98                 (mNumber != null ? mNumber : "null"),
     99                 (mName != null ? mName : "null"),
    100                 (mNameAndNumber != null ? mNameAndNumber : "null"),
    101                 (mLabel != null ? mLabel : "null"),
    102                 mPersonId, hashCode());
    103     }
    104 
    105     private static void logWithTrace(String msg, Object... format) {
    106         Thread current = Thread.currentThread();
    107         StackTraceElement[] stack = current.getStackTrace();
    108 
    109         StringBuilder sb = new StringBuilder();
    110         sb.append("[");
    111         sb.append(current.getId());
    112         sb.append("] ");
    113         sb.append(String.format(msg, format));
    114 
    115         sb.append(" <- ");
    116         int stop = stack.length > 7 ? 7 : stack.length;
    117         for (int i = 3; i < stop; i++) {
    118             String methodName = stack[i].getMethodName();
    119             sb.append(methodName);
    120             if ((i+1) != stop) {
    121                 sb.append(" <- ");
    122             }
    123         }
    124 
    125         Log.d(TAG, sb.toString());
    126     }
    127 
    128     public static Contact get(String number, boolean canBlock) {
    129         return sContactCache.get(number, canBlock);
    130     }
    131 
    132     public static void invalidateCache() {
    133         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    134             log("invalidateCache");
    135         }
    136 
    137         // While invalidating our local Cache doesn't remove the contacts, it will mark them
    138         // stale so the next time we're asked for a particular contact, we'll return that
    139         // stale contact and at the same time, fire off an asyncUpdateContact to update
    140         // that contact's info in the background. UI elements using the contact typically
    141         // call addListener() so they immediately get notified when the contact has been
    142         // updated with the latest info. They redraw themselves when we call the
    143         // listener's onUpdate().
    144         sContactCache.invalidate();
    145     }
    146 
    147     private static String emptyIfNull(String s) {
    148         return (s != null ? s : "");
    149     }
    150 
    151     public static String formatNameAndNumber(String name, String number) {
    152         // Format like this: Mike Cleron <(650) 555-1234>
    153         //                   Erick Tseng <(650) 555-1212>
    154         //                   Tutankhamun <tutank1341 (at) gmail.com>
    155         //                   (408) 555-1289
    156         String formattedNumber = number;
    157         if (!Mms.isEmailAddress(number)) {
    158             formattedNumber = PhoneNumberUtils.formatNumber(number);
    159         }
    160 
    161         if (!TextUtils.isEmpty(name) && !name.equals(number)) {
    162             return name + " <" + formattedNumber + ">";
    163         } else {
    164             return formattedNumber;
    165         }
    166     }
    167 
    168     public synchronized void reload() {
    169         mIsStale = true;
    170         sContactCache.get(mNumber, false);
    171     }
    172 
    173     public synchronized String getNumber() {
    174         return mNumber;
    175     }
    176 
    177     public synchronized void setNumber(String number) {
    178         mNumber = number;
    179         notSynchronizedUpdateNameAndNumber();
    180         mNumberIsModified = true;
    181     }
    182 
    183     public boolean isNumberModified() {
    184         return mNumberIsModified;
    185     }
    186 
    187     public void setIsNumberModified(boolean flag) {
    188         mNumberIsModified = flag;
    189     }
    190 
    191     public synchronized String getName() {
    192         if (TextUtils.isEmpty(mName)) {
    193             return mNumber;
    194         } else {
    195             return mName;
    196         }
    197     }
    198 
    199     public synchronized String getNameAndNumber() {
    200         return mNameAndNumber;
    201     }
    202 
    203     private synchronized void updateNameAndNumber() {
    204        notSynchronizedUpdateNameAndNumber();
    205     }
    206 
    207     private void notSynchronizedUpdateNameAndNumber() {
    208         mNameAndNumber = formatNameAndNumber(mName, mNumber);
    209     }
    210 
    211     public synchronized long getRecipientId() {
    212         return mRecipientId;
    213     }
    214 
    215     public synchronized void setRecipientId(long id) {
    216         mRecipientId = id;
    217     }
    218 
    219     public synchronized String getLabel() {
    220         return mLabel;
    221     }
    222 
    223     public synchronized Uri getUri() {
    224         return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
    225     }
    226 
    227     public synchronized int getPresenceResId() {
    228         return mPresenceResId;
    229     }
    230 
    231     public synchronized boolean existsInDatabase() {
    232         return (mPersonId > 0);
    233     }
    234 
    235     public static void addListener(UpdateListener l) {
    236         synchronized (mListeners) {
    237             mListeners.add(l);
    238         }
    239     }
    240 
    241     public static void removeListener(UpdateListener l) {
    242         synchronized (mListeners) {
    243             mListeners.remove(l);
    244         }
    245     }
    246 
    247     public static synchronized void dumpListeners() {
    248         int i = 0;
    249         Log.i(TAG, "[Contact] dumpListeners; size=" + mListeners.size());
    250         for (UpdateListener listener : mListeners) {
    251             Log.i(TAG, "["+ (i++) + "]" + listener);
    252         }
    253     }
    254 
    255     public synchronized boolean isEmail() {
    256         return Mms.isEmailAddress(mNumber);
    257     }
    258 
    259     public String getPresenceText() {
    260         return mPresenceText;
    261     }
    262 
    263     public synchronized Drawable getAvatar(Context context, Drawable defaultValue) {
    264         if (mAvatar == null) {
    265             if (mAvatarData != null) {
    266                 Bitmap b = BitmapFactory.decodeByteArray(mAvatarData, 0, mAvatarData.length);
    267                 mAvatar = new BitmapDrawable(context.getResources(), b);
    268             }
    269         }
    270         return mAvatar != null ? mAvatar : defaultValue;
    271     }
    272 
    273     public static void init(final Context context) {
    274         sContactCache = new ContactsCache(context);
    275 
    276         RecipientIdCache.init(context);
    277 
    278         // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact
    279         // cache each time that occurs. Unless we can get targeted updates for the contacts we
    280         // care about(which probably won't happen for a long time), we probably should just
    281         // invalidate cache peoridically, or surgically.
    282         /*
    283         context.getContentResolver().registerContentObserver(
    284                 Contacts.CONTENT_URI, true, sContactsObserver);
    285         */
    286     }
    287 
    288     public static void dump() {
    289         sContactCache.dump();
    290     }
    291 
    292     private static class ContactsCache {
    293         private final TaskStack mTaskQueue = new TaskStack();
    294         private static final String SEPARATOR = ";";
    295 
    296         // query params for caller id lookup
    297         private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
    298                 + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
    299                 + " AND " + Data.RAW_CONTACT_ID + " IN "
    300                         + "(SELECT raw_contact_id "
    301                         + " FROM phone_lookup"
    302                         + " WHERE normalized_number GLOB('+*'))";
    303 
    304         // Utilizing private API
    305         private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI;
    306 
    307         private static final String[] CALLER_ID_PROJECTION = new String[] {
    308                 Phone.NUMBER,                   // 0
    309                 Phone.LABEL,                    // 1
    310                 Phone.DISPLAY_NAME,             // 2
    311                 Phone.CONTACT_ID,               // 3
    312                 Phone.CONTACT_PRESENCE,         // 4
    313                 Phone.CONTACT_STATUS,           // 5
    314         };
    315 
    316         private static final int PHONE_NUMBER_COLUMN = 0;
    317         private static final int PHONE_LABEL_COLUMN = 1;
    318         private static final int CONTACT_NAME_COLUMN = 2;
    319         private static final int CONTACT_ID_COLUMN = 3;
    320         private static final int CONTACT_PRESENCE_COLUMN = 4;
    321         private static final int CONTACT_STATUS_COLUMN = 5;
    322 
    323         // query params for contact lookup by email
    324         private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI;
    325 
    326         private static final String EMAIL_SELECTION = "UPPER(" + Email.DATA + ")=UPPER(?) AND "
    327                 + Data.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'";
    328 
    329         private static final String[] EMAIL_PROJECTION = new String[] {
    330                 Email.DISPLAY_NAME,           // 0
    331                 Email.CONTACT_PRESENCE,       // 1
    332                 Email.CONTACT_ID,             // 2
    333                 Phone.DISPLAY_NAME,           //
    334         };
    335         private static final int EMAIL_NAME_COLUMN = 0;
    336         private static final int EMAIL_STATUS_COLUMN = 1;
    337         private static final int EMAIL_ID_COLUMN = 2;
    338         private static final int EMAIL_CONTACT_NAME_COLUMN = 3;
    339 
    340         private final Context mContext;
    341 
    342         private final HashMap<String, ArrayList<Contact>> mContactsHash =
    343             new HashMap<String, ArrayList<Contact>>();
    344 
    345         private ContactsCache(Context context) {
    346             mContext = context;
    347         }
    348 
    349         void dump() {
    350             synchronized (ContactsCache.this) {
    351                 Log.d(TAG, "**** Contact cache dump ****");
    352                 for (String key : mContactsHash.keySet()) {
    353                     ArrayList<Contact> alc = mContactsHash.get(key);
    354                     for (Contact c : alc) {
    355                         Log.d(TAG, key + " ==> " + c.toString());
    356                     }
    357                 }
    358             }
    359         }
    360 
    361         private static class TaskStack {
    362             Thread mWorkerThread;
    363             private final ArrayList<Runnable> mThingsToLoad;
    364 
    365             public TaskStack() {
    366                 mThingsToLoad = new ArrayList<Runnable>();
    367                 mWorkerThread = new Thread(new Runnable() {
    368                     public void run() {
    369                         while (true) {
    370                             Runnable r = null;
    371                             synchronized (mThingsToLoad) {
    372                                 if (mThingsToLoad.size() == 0) {
    373                                     try {
    374                                         mThingsToLoad.wait();
    375                                     } catch (InterruptedException ex) {
    376                                         // nothing to do
    377                                     }
    378                                 }
    379                                 if (mThingsToLoad.size() > 0) {
    380                                     r = mThingsToLoad.remove(0);
    381                                 }
    382                             }
    383                             if (r != null) {
    384                                 r.run();
    385                             }
    386                         }
    387                     }
    388                 });
    389                 mWorkerThread.start();
    390             }
    391 
    392             public void push(Runnable r) {
    393                 synchronized (mThingsToLoad) {
    394                     mThingsToLoad.add(r);
    395                     mThingsToLoad.notify();
    396                 }
    397             }
    398         }
    399 
    400         public void pushTask(Runnable r) {
    401             mTaskQueue.push(r);
    402         }
    403 
    404         public Contact get(String number, boolean canBlock) {
    405             if (V) logWithTrace("get(%s, %s)", number, canBlock);
    406 
    407             if (TextUtils.isEmpty(number)) {
    408                 number = "";        // In some places (such as Korea), it's possible to receive
    409                                     // a message without the sender's address. In this case,
    410                                     // all such anonymous messages will get added to the same
    411                                     // thread.
    412             }
    413 
    414             // Always return a Contact object, if if we don't have an actual contact
    415             // in the contacts db.
    416             Contact contact = get(number);
    417             Runnable r = null;
    418 
    419             synchronized (contact) {
    420                 // If there's a query pending and we're willing to block then
    421                 // wait here until the query completes.
    422                 while (canBlock && contact.mQueryPending) {
    423                     try {
    424                         contact.wait();
    425                     } catch (InterruptedException ex) {
    426                         // try again by virtue of the loop unless mQueryPending is false
    427                     }
    428                 }
    429 
    430                 // If we're stale and we haven't already kicked off a query then kick
    431                 // it off here.
    432                 if (contact.mIsStale && !contact.mQueryPending) {
    433                     contact.mIsStale = false;
    434 
    435                     if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    436                         log("async update for " + contact.toString() + " canBlock: " + canBlock +
    437                                 " isStale: " + contact.mIsStale);
    438                     }
    439 
    440                     final Contact c = contact;
    441                     r = new Runnable() {
    442                         public void run() {
    443                             updateContact(c);
    444                         }
    445                     };
    446 
    447                     // set this to true while we have the lock on contact since we will
    448                     // either run the query directly (canBlock case) or push the query
    449                     // onto the queue.  In either case the mQueryPending will get set
    450                     // to false via updateContact.
    451                     contact.mQueryPending = true;
    452                 }
    453             }
    454             // do this outside of the synchronized so we don't hold up any
    455             // subsequent calls to "get" on other threads
    456             if (r != null) {
    457                 if (canBlock) {
    458                     r.run();
    459                 } else {
    460                     pushTask(r);
    461                 }
    462             }
    463             return contact;
    464         }
    465 
    466         private boolean contactChanged(Contact orig, Contact newContactData) {
    467             // The phone number should never change, so don't bother checking.
    468             // TODO: Maybe update it if it has gotten longer, i.e. 650-234-5678 -> +16502345678?
    469 
    470             String oldName = emptyIfNull(orig.mName);
    471             String newName = emptyIfNull(newContactData.mName);
    472             if (!oldName.equals(newName)) {
    473                 if (V) Log.d(TAG, String.format("name changed: %s -> %s", oldName, newName));
    474                 return true;
    475             }
    476 
    477             String oldLabel = emptyIfNull(orig.mLabel);
    478             String newLabel = emptyIfNull(newContactData.mLabel);
    479             if (!oldLabel.equals(newLabel)) {
    480                 if (V) Log.d(TAG, String.format("label changed: %s -> %s", oldLabel, newLabel));
    481                 return true;
    482             }
    483 
    484             if (orig.mPersonId != newContactData.mPersonId) {
    485                 if (V) Log.d(TAG, "person id changed");
    486                 return true;
    487             }
    488 
    489             if (orig.mPresenceResId != newContactData.mPresenceResId) {
    490                 if (V) Log.d(TAG, "presence changed");
    491                 return true;
    492             }
    493 
    494             if (!Arrays.equals(orig.mAvatarData, newContactData.mAvatarData)) {
    495                 if (V) Log.d(TAG, "avatar changed");
    496                 return true;
    497             }
    498 
    499             return false;
    500         }
    501 
    502         private void updateContact(final Contact c) {
    503             if (c == null) {
    504                 return;
    505             }
    506 
    507             Contact entry = getContactInfo(c.mNumber);
    508             synchronized (c) {
    509                 if (contactChanged(c, entry)) {
    510                     if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    511                         log("updateContact: contact changed for " + entry.mName);
    512                     }
    513 
    514                     c.mNumber = entry.mNumber;
    515                     c.mLabel = entry.mLabel;
    516                     c.mPersonId = entry.mPersonId;
    517                     c.mPresenceResId = entry.mPresenceResId;
    518                     c.mPresenceText = entry.mPresenceText;
    519                     c.mAvatarData = entry.mAvatarData;
    520                     c.mAvatar = entry.mAvatar;
    521 
    522                     // Check to see if this is the local ("me") number and update the name.
    523                     if (MessageUtils.isLocalNumber(c.mNumber)) {
    524                         c.mName = mContext.getString(com.android.mms.R.string.me);
    525                     } else {
    526                         c.mName = entry.mName;
    527                     }
    528 
    529                     c.notSynchronizedUpdateNameAndNumber();
    530 
    531                     // clone the list of listeners in case the onUpdate call turns around and
    532                     // modifies the list of listeners
    533                     // access to mListeners is synchronized on ContactsCache
    534                     HashSet<UpdateListener> iterator;
    535                     synchronized (mListeners) {
    536                         iterator = (HashSet<UpdateListener>)Contact.mListeners.clone();
    537                     }
    538                     for (UpdateListener l : iterator) {
    539                         if (V) Log.d(TAG, "updating " + l);
    540                         l.onUpdate(c);
    541                     }
    542                 }
    543                 synchronized (c) {
    544                     c.mQueryPending = false;
    545                     c.notifyAll();
    546                 }
    547             }
    548         }
    549 
    550         /**
    551          * Returns the caller info in Contact.
    552          */
    553         public Contact getContactInfo(String numberOrEmail) {
    554             if (Mms.isEmailAddress(numberOrEmail)) {
    555                 return getContactInfoForEmailAddress(numberOrEmail);
    556             } else {
    557                 return getContactInfoForPhoneNumber(numberOrEmail);
    558             }
    559         }
    560 
    561         /**
    562          * Queries the caller id info with the phone number.
    563          * @return a Contact containing the caller id info corresponding to the number.
    564          */
    565         private Contact getContactInfoForPhoneNumber(String number) {
    566             number = PhoneNumberUtils.stripSeparators(number);
    567             Contact entry = new Contact(number);
    568 
    569             //if (LOCAL_DEBUG) log("queryContactInfoByNumber: number=" + number);
    570 
    571             // We need to include the phone number in the selection string itself rather then
    572             // selection arguments, because SQLite needs to see the exact pattern of GLOB
    573             // to generate the correct query plan
    574             String selection = CALLER_ID_SELECTION.replace("+",
    575                     PhoneNumberUtils.toCallerIDMinMatch(number));
    576             Cursor cursor = mContext.getContentResolver().query(
    577                     PHONES_WITH_PRESENCE_URI,
    578                     CALLER_ID_PROJECTION,
    579                     selection,
    580                     new String[] { number },
    581                     null);
    582 
    583             if (cursor == null) {
    584                 Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!" +
    585                         " contact uri used " + PHONES_WITH_PRESENCE_URI);
    586                 return entry;
    587             }
    588 
    589             try {
    590                 if (cursor.moveToFirst()) {
    591                     synchronized (entry) {
    592                         entry.mLabel = cursor.getString(PHONE_LABEL_COLUMN);
    593                         entry.mName = cursor.getString(CONTACT_NAME_COLUMN);
    594                         entry.mPersonId = cursor.getLong(CONTACT_ID_COLUMN);
    595                         entry.mPresenceResId = getPresenceIconResourceId(
    596                                 cursor.getInt(CONTACT_PRESENCE_COLUMN));
    597                         entry.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN);
    598                         if (V) {
    599                             log("queryContactInfoByNumber: name=" + entry.mName +
    600                                     ", number=" + number + ", presence=" + entry.mPresenceResId);
    601                         }
    602                     }
    603 
    604                     byte[] data = loadAvatarData(entry);
    605 
    606                     synchronized (entry) {
    607                         entry.mAvatarData = data;
    608                     }
    609 
    610                 }
    611             } finally {
    612                 cursor.close();
    613             }
    614 
    615             return entry;
    616         }
    617 
    618         /*
    619          * Load the avatar data from the cursor into memory.  Don't decode the data
    620          * until someone calls for it (see getAvatar).  Hang onto the raw data so that
    621          * we can compare it when the data is reloaded.
    622          * TODO: consider comparing a checksum so that we don't have to hang onto
    623          * the raw bytes after the image is decoded.
    624          */
    625         private byte[] loadAvatarData(Contact entry) {
    626             byte [] data = null;
    627 
    628             if (entry.mPersonId == 0 || entry.mAvatar != null) {
    629                 return null;
    630             }
    631 
    632             Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId);
    633 
    634             InputStream avatarDataStream = Contacts.openContactPhotoInputStream(
    635                         mContext.getContentResolver(),
    636                         contactUri);
    637             try {
    638                 if (avatarDataStream != null) {
    639                     data = new byte[avatarDataStream.available()];
    640                     avatarDataStream.read(data, 0, data.length);
    641                 }
    642             } catch (IOException ex) {
    643                 //
    644             } finally {
    645                 try {
    646                     if (avatarDataStream != null) {
    647                         avatarDataStream.close();
    648                     }
    649                 } catch (IOException e) {
    650                 }
    651             }
    652 
    653             return data;
    654         }
    655 
    656         private int getPresenceIconResourceId(int presence) {
    657             // TODO: must fix for SDK
    658             if (presence != Presence.OFFLINE) {
    659                 return Presence.getPresenceIconResourceId(presence);
    660             }
    661 
    662             return 0;
    663         }
    664 
    665         /**
    666          * Query the contact email table to get the name of an email address.
    667          */
    668         private Contact getContactInfoForEmailAddress(String email) {
    669             Contact entry = new Contact(email);
    670 
    671             Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(),
    672                     EMAIL_WITH_PRESENCE_URI,
    673                     EMAIL_PROJECTION,
    674                     EMAIL_SELECTION,
    675                     new String[] { email },
    676                     null);
    677 
    678             if (cursor != null) {
    679                 try {
    680                     while (cursor.moveToNext()) {
    681                         boolean found = false;
    682 
    683                         synchronized (entry) {
    684                             entry.mPresenceResId = getPresenceIconResourceId(
    685                                     cursor.getInt(EMAIL_STATUS_COLUMN));
    686                             entry.mPersonId = cursor.getLong(EMAIL_ID_COLUMN);
    687 
    688                             String name = cursor.getString(EMAIL_NAME_COLUMN);
    689                             if (TextUtils.isEmpty(name)) {
    690                                 name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN);
    691                             }
    692                             if (!TextUtils.isEmpty(name)) {
    693                                 entry.mName = name;
    694                                 if (V) {
    695                                     log("getContactInfoForEmailAddress: name=" + entry.mName +
    696                                             ", email=" + email + ", presence=" +
    697                                             entry.mPresenceResId);
    698                                 }
    699                                 found = true;
    700                             }
    701                         }
    702 
    703                         if (found) {
    704                             byte[] data = loadAvatarData(entry);
    705                             synchronized (entry) {
    706                                 entry.mAvatarData = data;
    707                             }
    708 
    709                             break;
    710                         }
    711                     }
    712                 } finally {
    713                     cursor.close();
    714                 }
    715             }
    716             return entry;
    717         }
    718 
    719         // Invert and truncate to five characters the phoneNumber so that we
    720         // can use it as the key in a hashtable.  We keep a mapping of this
    721         // key to a list of all contacts which have the same key.
    722         private String key(String phoneNumber, CharBuffer keyBuffer) {
    723             keyBuffer.clear();
    724             keyBuffer.mark();
    725 
    726             int position = phoneNumber.length();
    727             int resultCount = 0;
    728             while (--position >= 0) {
    729                 char c = phoneNumber.charAt(position);
    730                 if (Character.isDigit(c)) {
    731                     keyBuffer.put(c);
    732                     if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) {
    733                         break;
    734                     }
    735                 }
    736             }
    737             keyBuffer.reset();
    738             if (resultCount > 0) {
    739                 return keyBuffer.toString();
    740             } else {
    741                 // there were no usable digits in the input phoneNumber
    742                 return phoneNumber;
    743             }
    744         }
    745 
    746         // Reuse this so we don't have to allocate each time we go through this
    747         // "get" function.
    748         static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5;
    749         static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH);
    750 
    751         public Contact get(String numberOrEmail) {
    752             synchronized (ContactsCache.this) {
    753                 // See if we can find "number" in the hashtable.
    754                 // If so, just return the result.
    755                 final boolean isNotRegularPhoneNumber = Mms.isEmailAddress(numberOrEmail) ||
    756                         MessageUtils.isAlias(numberOrEmail);
    757                 final String key = isNotRegularPhoneNumber ?
    758                         numberOrEmail : key(numberOrEmail, sStaticKeyBuffer);
    759 
    760                 ArrayList<Contact> candidates = mContactsHash.get(key);
    761                 if (candidates != null) {
    762                     int length = candidates.size();
    763                     for (int i = 0; i < length; i++) {
    764                         Contact c= candidates.get(i);
    765                         if (isNotRegularPhoneNumber) {
    766                             if (numberOrEmail.equals(c.mNumber)) {
    767                                 return c;
    768                             }
    769                         } else {
    770                             if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) {
    771                                 return c;
    772                             }
    773                         }
    774                     }
    775                 } else {
    776                     candidates = new ArrayList<Contact>();
    777                     // call toString() since it may be the static CharBuffer
    778                     mContactsHash.put(key, candidates);
    779                 }
    780                 Contact c = new Contact(numberOrEmail);
    781                 candidates.add(c);
    782                 return c;
    783             }
    784         }
    785 
    786         void invalidate() {
    787             // Don't remove the contacts. Just mark them stale so we'll update their
    788             // info, particularly their presence.
    789             synchronized (ContactsCache.this) {
    790                 for (ArrayList<Contact> alc : mContactsHash.values()) {
    791                     for (Contact c : alc) {
    792                         synchronized (c) {
    793                             c.mIsStale = true;
    794                         }
    795                     }
    796                 }
    797             }
    798         }
    799     }
    800 
    801     private static void log(String msg) {
    802         Log.d(TAG, msg);
    803     }
    804 }
    805