PageRenderTime 58ms CodeModel.GetById 29ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 0ms

/src/com/android/mms/data/Contact.java

https://github.com/greg-pe/android_packages_apps_Mms
Java | 804 lines | 598 code | 115 blank | 91 comment | 100 complexity | d44786a54d8c4f058f88fb49eb8bed08 MD5 | raw file
  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 android.provider.Telephony.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 != 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@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}