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