• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onUpdate(Contact updated)79         public void onUpdate(Contact updated);
80     }
81 
82     /*
83      * Make a basic contact object with a phone number.
84      */
Contact(String number)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
toString()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 
logWithTrace(String msg, Object... format)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 
get(String number, boolean canBlock)128     public static Contact get(String number, boolean canBlock) {
129         return sContactCache.get(number, canBlock);
130     }
131 
invalidateCache()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 
emptyIfNull(String s)147     private static String emptyIfNull(String s) {
148         return (s != null ? s : "");
149     }
150 
formatNameAndNumber(String name, String number)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@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 
reload()168     public synchronized void reload() {
169         mIsStale = true;
170         sContactCache.get(mNumber, false);
171     }
172 
getNumber()173     public synchronized String getNumber() {
174         return mNumber;
175     }
176 
setNumber(String number)177     public synchronized void setNumber(String number) {
178         mNumber = number;
179         notSynchronizedUpdateNameAndNumber();
180         mNumberIsModified = true;
181     }
182 
isNumberModified()183     public boolean isNumberModified() {
184         return mNumberIsModified;
185     }
186 
setIsNumberModified(boolean flag)187     public void setIsNumberModified(boolean flag) {
188         mNumberIsModified = flag;
189     }
190 
getName()191     public synchronized String getName() {
192         if (TextUtils.isEmpty(mName)) {
193             return mNumber;
194         } else {
195             return mName;
196         }
197     }
198 
getNameAndNumber()199     public synchronized String getNameAndNumber() {
200         return mNameAndNumber;
201     }
202 
updateNameAndNumber()203     private synchronized void updateNameAndNumber() {
204        notSynchronizedUpdateNameAndNumber();
205     }
206 
notSynchronizedUpdateNameAndNumber()207     private void notSynchronizedUpdateNameAndNumber() {
208         mNameAndNumber = formatNameAndNumber(mName, mNumber);
209     }
210 
getRecipientId()211     public synchronized long getRecipientId() {
212         return mRecipientId;
213     }
214 
setRecipientId(long id)215     public synchronized void setRecipientId(long id) {
216         mRecipientId = id;
217     }
218 
getLabel()219     public synchronized String getLabel() {
220         return mLabel;
221     }
222 
getUri()223     public synchronized Uri getUri() {
224         return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
225     }
226 
getPresenceResId()227     public synchronized int getPresenceResId() {
228         return mPresenceResId;
229     }
230 
existsInDatabase()231     public synchronized boolean existsInDatabase() {
232         return (mPersonId > 0);
233     }
234 
addListener(UpdateListener l)235     public static void addListener(UpdateListener l) {
236         synchronized (mListeners) {
237             mListeners.add(l);
238         }
239     }
240 
removeListener(UpdateListener l)241     public static void removeListener(UpdateListener l) {
242         synchronized (mListeners) {
243             mListeners.remove(l);
244         }
245     }
246 
dumpListeners()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 
isEmail()255     public synchronized boolean isEmail() {
256         return Mms.isEmailAddress(mNumber);
257     }
258 
getPresenceText()259     public String getPresenceText() {
260         return mPresenceText;
261     }
262 
getAvatar(Context context, Drawable defaultValue)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 
init(final Context context)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 
dump()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 
ContactsCache(Context context)345         private ContactsCache(Context context) {
346             mContext = context;
347         }
348 
dump()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 
TaskStack()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 
push(Runnable r)392             public void push(Runnable r) {
393                 synchronized (mThingsToLoad) {
394                     mThingsToLoad.add(r);
395                     mThingsToLoad.notify();
396                 }
397             }
398         }
399 
pushTask(Runnable r)400         public void pushTask(Runnable r) {
401             mTaskQueue.push(r);
402         }
403 
get(String number, boolean canBlock)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 
contactChanged(Contact orig, Contact newContactData)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 
updateContact(final Contact c)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          */
getContactInfo(String numberOrEmail)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          */
getContactInfoForPhoneNumber(String number)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          */
loadAvatarData(Contact entry)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 
getPresenceIconResourceId(int presence)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          */
getContactInfoForEmailAddress(String email)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.
key(String phoneNumber, CharBuffer keyBuffer)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 
get(String numberOrEmail)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 
invalidate()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 
log(String msg)801     private static void log(String msg) {
802         Log.d(TAG, msg);
803     }
804 }
805