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.database.sqlite.SqliteWrapper;
17import android.graphics.Bitmap;
18import android.graphics.BitmapFactory;
19import android.graphics.drawable.BitmapDrawable;
20import android.graphics.drawable.Drawable;
21import android.net.Uri;
22import android.os.Handler;
23import android.os.Parcelable;
24import android.provider.ContactsContract.CommonDataKinds.Email;
25import android.provider.ContactsContract.CommonDataKinds.Phone;
26import android.provider.ContactsContract.Contacts;
27import android.provider.ContactsContract.Data;
28import android.provider.ContactsContract.Presence;
29import android.provider.ContactsContract.Profile;
30import android.provider.Telephony.Mms;
31import android.telephony.PhoneNumberUtils;
32import android.text.TextUtils;
33import android.util.Log;
34
35import com.android.mms.LogTag;
36import com.android.mms.MmsApp;
37import com.android.mms.R;
38import com.android.mms.ui.MessageUtils;
39
40public class Contact {
41    public static final int CONTACT_METHOD_TYPE_UNKNOWN = 0;
42    public static final int CONTACT_METHOD_TYPE_PHONE = 1;
43    public static final int CONTACT_METHOD_TYPE_EMAIL = 2;
44    public static final int CONTACT_METHOD_TYPE_SELF = 3;       // the "Me" or profile contact
45    public static final String TEL_SCHEME = "tel";
46    public static final String CONTENT_SCHEME = "content";
47    private static final int CONTACT_METHOD_ID_UNKNOWN = -1;
48    private static final String TAG = LogTag.TAG;
49    private static ContactsCache sContactCache;
50    private static final String SELF_ITEM_KEY = "Self_Item_Key";
51
52//    private static final ContentObserver sContactsObserver = new ContentObserver(new Handler()) {
53//        @Override
54//        public void onChange(boolean selfUpdate) {
55//            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
56//                log("contact changed, invalidate cache");
57//            }
58//            invalidateCache();
59//        }
60//    };
61
62    private static final ContentObserver sPresenceObserver = new ContentObserver(new Handler()) {
63        @Override
64        public void onChange(boolean selfUpdate) {
65            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
66                log("presence changed, invalidate cache");
67            }
68            invalidateCache();
69        }
70    };
71
72    private final static HashSet<UpdateListener> mListeners = new HashSet<UpdateListener>();
73
74    private long mContactMethodId;   // Id in phone or email Uri returned by provider of current
75                                     // Contact, -1 is invalid. e.g. contact method id is 20 when
76                                     // current contact has phone content://.../phones/20.
77    private int mContactMethodType;
78    private String mNumber;
79    private String mNumberE164;
80    private String mName;
81    private String mNameAndNumber;   // for display, e.g. Fred Flintstone <670-782-1123>
82    private boolean mNumberIsModified; // true if the number is modified
83
84    private long mRecipientId;       // used to find the Recipient cache entry
85    private String mLabel;
86    private long mPersonId;
87    private int mPresenceResId;      // TODO: make this a state instead of a res ID
88    private String mPresenceText;
89    private BitmapDrawable mAvatar;
90    private byte [] mAvatarData;
91    private boolean mIsStale;
92    private boolean mQueryPending;
93    private boolean mIsMe;          // true if this contact is me!
94    private boolean mSendToVoicemail;   // true if this contact should not put up notification
95    private Uri mPeopleReferenceUri;
96
97    public interface UpdateListener {
98        public void onUpdate(Contact updated);
99    }
100
101    private Contact(String number, String name) {
102        init(number, name);
103    }
104    /*
105     * Make a basic contact object with a phone number.
106     */
107    private Contact(String number) {
108        init(number, "");
109    }
110
111    private Contact(boolean isMe) {
112        init(SELF_ITEM_KEY, "");
113        mIsMe = isMe;
114    }
115
116    private void init(String number, String name) {
117        mContactMethodId = CONTACT_METHOD_ID_UNKNOWN;
118        mName = name;
119        setNumber(number);
120        mNumberIsModified = false;
121        mLabel = "";
122        mPersonId = 0;
123        mPresenceResId = 0;
124        mIsStale = true;
125        mSendToVoicemail = false;
126    }
127    @Override
128    public String toString() {
129        return String.format("{ number=%s, name=%s, nameAndNumber=%s, label=%s, person_id=%d, hash=%d method_id=%d }",
130                (mNumber != null ? mNumber : "null"),
131                (mName != null ? mName : "null"),
132                (mNameAndNumber != null ? mNameAndNumber : "null"),
133                (mLabel != null ? mLabel : "null"),
134                mPersonId, hashCode(),
135                mContactMethodId);
136    }
137
138    public static void logWithTrace(String tag, String msg, Object... format) {
139        Thread current = Thread.currentThread();
140        StackTraceElement[] stack = current.getStackTrace();
141
142        StringBuilder sb = new StringBuilder();
143        sb.append("[");
144        sb.append(current.getId());
145        sb.append("] ");
146        sb.append(String.format(msg, format));
147
148        sb.append(" <- ");
149        int stop = stack.length > 7 ? 7 : stack.length;
150        for (int i = 3; i < stop; i++) {
151            String methodName = stack[i].getMethodName();
152            sb.append(methodName);
153            if ((i+1) != stop) {
154                sb.append(" <- ");
155            }
156        }
157
158        Log.d(tag, sb.toString());
159    }
160
161    public static Contact get(String number, boolean canBlock) {
162        return sContactCache.get(number, canBlock);
163    }
164
165    public static Contact getMe(boolean canBlock) {
166        return sContactCache.getMe(canBlock);
167    }
168
169    public void removeFromCache() {
170        sContactCache.remove(this);
171    }
172
173    public static List<Contact> getByPhoneUris(Parcelable[] uris) {
174        return sContactCache.getContactInfoForPhoneUris(uris);
175    }
176
177    public static void invalidateCache() {
178        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
179            log("invalidateCache");
180        }
181
182        // While invalidating our local Cache doesn't remove the contacts, it will mark them
183        // stale so the next time we're asked for a particular contact, we'll return that
184        // stale contact and at the same time, fire off an asyncUpdateContact to update
185        // that contact's info in the background. UI elements using the contact typically
186        // call addListener() so they immediately get notified when the contact has been
187        // updated with the latest info. They redraw themselves when we call the
188        // listener's onUpdate().
189        sContactCache.invalidate();
190    }
191
192    public boolean isMe() {
193        return mIsMe;
194    }
195
196    private static String emptyIfNull(String s) {
197        return (s != null ? s : "");
198    }
199
200    /**
201     * Fomat the name and number.
202     *
203     * @param name
204     * @param number
205     * @param numberE164 the number's E.164 representation, is used to get the
206     *        country the number belongs to.
207     * @return the formatted name and number
208     */
209    public static String formatNameAndNumber(String name, String number, String numberE164) {
210        // Format like this: Mike Cleron <(650) 555-1234>
211        //                   Erick Tseng <(650) 555-1212>
212        //                   Tutankhamun <tutank1341@gmail.com>
213        //                   (408) 555-1289
214        String formattedNumber = number;
215        if (!Mms.isEmailAddress(number)) {
216            formattedNumber = PhoneNumberUtils.formatNumber(number, numberE164,
217                    MmsApp.getApplication().getCurrentCountryIso());
218        }
219
220        if (!TextUtils.isEmpty(name) && !name.equals(number)) {
221            return name + " <" + formattedNumber + ">";
222        } else {
223            return formattedNumber;
224        }
225    }
226
227    public synchronized void reload() {
228        mIsStale = true;
229        sContactCache.get(mNumber, false);
230    }
231
232    public synchronized String getNumber() {
233        return mNumber;
234    }
235
236    public synchronized void setNumber(String number) {
237        if (!Mms.isEmailAddress(number)) {
238            mNumber = PhoneNumberUtils.formatNumber(number, mNumberE164,
239                    MmsApp.getApplication().getCurrentCountryIso());
240        } else {
241            mNumber = number;
242        }
243        notSynchronizedUpdateNameAndNumber();
244        mNumberIsModified = true;
245    }
246
247    public boolean isNumberModified() {
248        return mNumberIsModified;
249    }
250
251    public boolean getSendToVoicemail() {
252        return mSendToVoicemail;
253    }
254
255    public void setIsNumberModified(boolean flag) {
256        mNumberIsModified = flag;
257    }
258
259    public synchronized String getName() {
260        if (TextUtils.isEmpty(mName)) {
261            return mNumber;
262        } else {
263            return mName;
264        }
265    }
266
267    public synchronized String getNameAndNumber() {
268        return mNameAndNumber;
269    }
270
271    private void notSynchronizedUpdateNameAndNumber() {
272        mNameAndNumber = formatNameAndNumber(mName, mNumber, mNumberE164);
273    }
274
275    public synchronized long getRecipientId() {
276        return mRecipientId;
277    }
278
279    public synchronized void setRecipientId(long id) {
280        mRecipientId = id;
281    }
282
283    public synchronized String getLabel() {
284        return mLabel;
285    }
286
287    public synchronized Uri getUri() {
288        return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
289    }
290
291    public synchronized int getPresenceResId() {
292        return mPresenceResId;
293    }
294
295    public synchronized boolean existsInDatabase() {
296        return (mPersonId > 0);
297    }
298
299    public static void addListener(UpdateListener l) {
300        synchronized (mListeners) {
301            mListeners.add(l);
302        }
303    }
304
305    public static void removeListener(UpdateListener l) {
306        synchronized (mListeners) {
307            mListeners.remove(l);
308        }
309    }
310
311    public static void dumpListeners() {
312        synchronized (mListeners) {
313            int i = 0;
314            Log.i(TAG, "[Contact] dumpListeners; size=" + mListeners.size());
315            for (UpdateListener listener : mListeners) {
316                Log.i(TAG, "["+ (i++) + "]" + listener);
317            }
318        }
319    }
320
321    public synchronized boolean isEmail() {
322        return Mms.isEmailAddress(mNumber);
323    }
324
325    public String getPresenceText() {
326        return mPresenceText;
327    }
328
329    public int getContactMethodType() {
330        return mContactMethodType;
331    }
332
333    public Uri getPeopleReferenceUri() {
334        return mPeopleReferenceUri;
335    }
336
337    public long getContactMethodId() {
338        return mContactMethodId;
339    }
340
341    public synchronized Uri getPhoneUri() {
342        if (existsInDatabase()) {
343            return ContentUris.withAppendedId(Phone.CONTENT_URI, mContactMethodId);
344        } else {
345            Uri.Builder ub = new Uri.Builder();
346            ub.scheme(TEL_SCHEME);
347            ub.encodedOpaquePart(mNumber);
348            return ub.build();
349        }
350    }
351
352    public synchronized Drawable getAvatar(Context context, Drawable defaultValue) {
353        if (mAvatar == null) {
354            if (mAvatarData != null) {
355                Bitmap b = BitmapFactory.decodeByteArray(mAvatarData, 0, mAvatarData.length);
356                mAvatar = new BitmapDrawable(context.getResources(), b);
357            }
358        }
359        return mAvatar != null ? mAvatar : defaultValue;
360    }
361
362    public static void init(final Context context) {
363        if (sContactCache != null) { // Stop previous Runnable
364            sContactCache.mTaskQueue.mWorkerThread.interrupt();
365        }
366        sContactCache = new ContactsCache(context);
367
368        RecipientIdCache.init(context);
369
370        // it maybe too aggressive to listen for *any* contact changes, and rebuild MMS contact
371        // cache each time that occurs. Unless we can get targeted updates for the contacts we
372        // care about(which probably won't happen for a long time), we probably should just
373        // invalidate cache peoridically, or surgically.
374        /*
375        context.getContentResolver().registerContentObserver(
376                Contacts.CONTENT_URI, true, sContactsObserver);
377        */
378    }
379
380    public static void dump() {
381        sContactCache.dump();
382    }
383
384    private static class ContactsCache {
385        private final TaskStack mTaskQueue = new TaskStack();
386        private static final String SEPARATOR = ";";
387
388        /**
389         * For a specified phone number, 2 rows were inserted into phone_lookup
390         * table. One is the phone number's E164 representation, and another is
391         * one's normalized format. If the phone number's normalized format in
392         * the lookup table is the suffix of the given number's one, it is
393         * treated as matched CallerId. E164 format number must fully equal.
394         *
395         * For example: Both 650-123-4567 and +1 (650) 123-4567 will match the
396         * normalized number 6501234567 in the phone lookup.
397         *
398         *  The min_match is used to narrow down the candidates for the final
399         * comparison.
400         */
401        // query params for caller id lookup
402        private static final String CALLER_ID_SELECTION = " Data._ID IN "
403                + " (SELECT DISTINCT lookup.data_id "
404                + " FROM "
405                    + " (SELECT data_id, normalized_number, length(normalized_number) as len "
406                    + " FROM phone_lookup "
407                    + " WHERE min_match = ?) AS lookup "
408                + " WHERE lookup.normalized_number = ? OR"
409                    + " (lookup.len <= ? AND "
410                        + " substr(?, ? - lookup.len + 1) = lookup.normalized_number))";
411
412        // query params for caller id lookup without E164 number as param
413        private static final String CALLER_ID_SELECTION_WITHOUT_E164 =  " Data._ID IN "
414            + " (SELECT DISTINCT lookup.data_id "
415            + " FROM "
416                + " (SELECT data_id, normalized_number, length(normalized_number) as len "
417                + " FROM phone_lookup "
418                + " WHERE min_match = ?) AS lookup "
419            + " WHERE "
420                + " (lookup.len <= ? AND "
421                    + " substr(?, ? - lookup.len + 1) = lookup.normalized_number))";
422
423        // Utilizing private API
424        private static final Uri PHONES_WITH_PRESENCE_URI = Data.CONTENT_URI;
425
426        private static final String[] CALLER_ID_PROJECTION = new String[] {
427                Phone._ID,                      // 0
428                Phone.NUMBER,                   // 1
429                Phone.LABEL,                    // 2
430                Phone.DISPLAY_NAME,             // 3
431                Phone.CONTACT_ID,               // 4
432                Phone.CONTACT_PRESENCE,         // 5
433                Phone.CONTACT_STATUS,           // 6
434                Phone.NORMALIZED_NUMBER,        // 7
435                Contacts.SEND_TO_VOICEMAIL      // 8
436        };
437
438        private static final int PHONE_ID_COLUMN = 0;
439        private static final int PHONE_NUMBER_COLUMN = 1;
440        private static final int PHONE_LABEL_COLUMN = 2;
441        private static final int CONTACT_NAME_COLUMN = 3;
442        private static final int CONTACT_ID_COLUMN = 4;
443        private static final int CONTACT_PRESENCE_COLUMN = 5;
444        private static final int CONTACT_STATUS_COLUMN = 6;
445        private static final int PHONE_NORMALIZED_NUMBER = 7;
446        private static final int SEND_TO_VOICEMAIL = 8;
447
448        private static final String[] SELF_PROJECTION = new String[] {
449                Phone._ID,                      // 0
450                Phone.DISPLAY_NAME,             // 1
451        };
452
453        private static final int SELF_ID_COLUMN = 0;
454        private static final int SELF_NAME_COLUMN = 1;
455
456        // query params for contact lookup by email
457        private static final Uri EMAIL_WITH_PRESENCE_URI = Data.CONTENT_URI;
458
459        private static final String EMAIL_SELECTION = "UPPER(" + Email.DATA + ")=UPPER(?) AND "
460                + Data.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'";
461
462        private static final String[] EMAIL_PROJECTION = new String[] {
463                Email._ID,                    // 0
464                Email.DISPLAY_NAME,           // 1
465                Email.CONTACT_PRESENCE,       // 2
466                Email.CONTACT_ID,             // 3
467                Phone.DISPLAY_NAME,           // 4
468                Contacts.SEND_TO_VOICEMAIL    // 5
469        };
470        private static final int EMAIL_ID_COLUMN = 0;
471        private static final int EMAIL_NAME_COLUMN = 1;
472        private static final int EMAIL_STATUS_COLUMN = 2;
473        private static final int EMAIL_CONTACT_ID_COLUMN = 3;
474        private static final int EMAIL_CONTACT_NAME_COLUMN = 4;
475        private static final int EMAIL_SEND_TO_VOICEMAIL_COLUMN = 5;
476
477        private final Context mContext;
478
479        private final HashMap<String, ArrayList<Contact>> mContactsHash =
480            new HashMap<String, ArrayList<Contact>>();
481
482        private ContactsCache(Context context) {
483            mContext = context;
484        }
485
486        void dump() {
487            synchronized (ContactsCache.this) {
488                Log.d(TAG, "**** Contact cache dump ****");
489                for (String key : mContactsHash.keySet()) {
490                    ArrayList<Contact> alc = mContactsHash.get(key);
491                    for (Contact c : alc) {
492                        Log.d(TAG, key + " ==> " + c.toString());
493                    }
494                }
495            }
496        }
497
498        private static class TaskStack {
499            Thread mWorkerThread;
500            private final ArrayList<Runnable> mThingsToLoad;
501
502            public TaskStack() {
503                mThingsToLoad = new ArrayList<Runnable>();
504                mWorkerThread = new Thread(new Runnable() {
505                    @Override
506                    public void run() {
507                        while (true) {
508                            Runnable r = null;
509                            synchronized (mThingsToLoad) {
510                                if (mThingsToLoad.size() == 0) {
511                                    try {
512                                        mThingsToLoad.wait();
513                                    } catch (InterruptedException ex) {
514                                        break;  // Exception sent by Contact.init() to stop Runnable
515                                    }
516                                }
517                                if (mThingsToLoad.size() > 0) {
518                                    r = mThingsToLoad.remove(0);
519                                }
520                            }
521                            if (r != null) {
522                                r.run();
523                            }
524                        }
525                    }
526                }, "Contact.ContactsCache.TaskStack worker thread");
527                mWorkerThread.setPriority(Thread.MIN_PRIORITY);
528                mWorkerThread.start();
529            }
530
531            public void push(Runnable r) {
532                synchronized (mThingsToLoad) {
533                    mThingsToLoad.add(r);
534                    mThingsToLoad.notify();
535                }
536            }
537        }
538
539        public void pushTask(Runnable r) {
540            mTaskQueue.push(r);
541        }
542
543        public Contact getMe(boolean canBlock) {
544            return get(SELF_ITEM_KEY, true, canBlock);
545        }
546
547        public Contact get(String number, boolean canBlock) {
548            return get(number, false, canBlock);
549        }
550
551        private Contact get(String number, boolean isMe, boolean canBlock) {
552            if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
553                logWithTrace(TAG, "get(%s, %s, %s)", number, isMe, canBlock);
554            }
555
556            if (TextUtils.isEmpty(number)) {
557                number = "";        // In some places (such as Korea), it's possible to receive
558                                    // a message without the sender's address. In this case,
559                                    // all such anonymous messages will get added to the same
560                                    // thread.
561            }
562
563            // Always return a Contact object, if if we don't have an actual contact
564            // in the contacts db.
565            Contact contact = internalGet(number, isMe);
566            Runnable r = null;
567
568            synchronized (contact) {
569                // If there's a query pending and we're willing to block then
570                // wait here until the query completes.
571                while (canBlock && contact.mQueryPending) {
572                    try {
573                        contact.wait();
574                    } catch (InterruptedException ex) {
575                        // try again by virtue of the loop unless mQueryPending is false
576                    }
577                }
578
579                // If we're stale and we haven't already kicked off a query then kick
580                // it off here.
581                if (contact.mIsStale && !contact.mQueryPending) {
582                    contact.mIsStale = false;
583
584                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
585                        log("async update for " + contact.toString() + " canBlock: " + canBlock +
586                                " isStale: " + contact.mIsStale);
587                    }
588
589                    final Contact c = contact;
590                    r = new Runnable() {
591                        @Override
592                        public void run() {
593                            updateContact(c);
594                        }
595                    };
596
597                    // set this to true while we have the lock on contact since we will
598                    // either run the query directly (canBlock case) or push the query
599                    // onto the queue.  In either case the mQueryPending will get set
600                    // to false via updateContact.
601                    contact.mQueryPending = true;
602                }
603            }
604            // do this outside of the synchronized so we don't hold up any
605            // subsequent calls to "get" on other threads
606            if (r != null) {
607                if (canBlock) {
608                    r.run();
609                } else {
610                    pushTask(r);
611                }
612            }
613            return contact;
614        }
615
616        /**
617         * Get CacheEntry list for given phone URIs. This method will do single one query to
618         * get expected contacts from provider. Be sure passed in URIs are not null and contains
619         * only valid URIs.
620         */
621        public List<Contact> getContactInfoForPhoneUris(Parcelable[] uris) {
622            if (uris.length == 0) {
623                return null;
624            }
625            StringBuilder idSetBuilder = new StringBuilder();
626            boolean first = true;
627            for (Parcelable p : uris) {
628                Uri uri = (Uri) p;
629                if ("content".equals(uri.getScheme())) {
630                    if (first) {
631                        first = false;
632                        idSetBuilder.append(uri.getLastPathSegment());
633                    } else {
634                        idSetBuilder.append(',').append(uri.getLastPathSegment());
635                    }
636                }
637            }
638            // Check whether there is content URI.
639            if (first) return null;
640            Cursor cursor = null;
641            if (idSetBuilder.length() > 0) {
642                final String whereClause = Phone._ID + " IN (" + idSetBuilder.toString() + ")";
643                cursor = mContext.getContentResolver().query(
644                        PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, whereClause, null, null);
645            }
646
647            if (cursor == null) {
648                return null;
649            }
650
651            List<Contact> entries = new ArrayList<Contact>();
652
653            try {
654                while (cursor.moveToNext()) {
655                    Contact entry = new Contact(cursor.getString(PHONE_NUMBER_COLUMN),
656                            cursor.getString(CONTACT_NAME_COLUMN));
657                    fillPhoneTypeContact(entry, cursor);
658                    ArrayList<Contact> value = new ArrayList<Contact>();
659                    value.add(entry);
660                    // Put the result in the cache.
661                    mContactsHash.put(key(entry.mNumber, sStaticKeyBuffer), value);
662                    entries.add(entry);
663                }
664            } finally {
665                cursor.close();
666            }
667            return entries;
668        }
669
670        private boolean contactChanged(Contact orig, Contact newContactData) {
671            // The phone number should never change, so don't bother checking.
672            // TODO: Maybe update it if it has gotten longer, i.e. 650-234-5678 -> +16502345678?
673
674            // Do the quick check first.
675            if (orig.mContactMethodType != newContactData.mContactMethodType) {
676                return true;
677            }
678
679            if (orig.mContactMethodId != newContactData.mContactMethodId) {
680                return true;
681            }
682
683            if (orig.mPersonId != newContactData.mPersonId) {
684                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
685                    Log.d(TAG, "person id changed");
686                }
687                return true;
688            }
689
690            if (orig.mPresenceResId != newContactData.mPresenceResId) {
691                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
692                    Log.d(TAG, "presence changed");
693                }
694                return true;
695            }
696
697            if (orig.mSendToVoicemail != newContactData.mSendToVoicemail) {
698                return true;
699            }
700
701            String oldName = emptyIfNull(orig.mName);
702            String newName = emptyIfNull(newContactData.mName);
703            if (!oldName.equals(newName)) {
704                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
705                    Log.d(TAG, String.format("name changed: %s -> %s", oldName, newName));
706                }
707                return true;
708            }
709
710            String oldLabel = emptyIfNull(orig.mLabel);
711            String newLabel = emptyIfNull(newContactData.mLabel);
712            if (!oldLabel.equals(newLabel)) {
713                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
714                    Log.d(TAG, String.format("label changed: %s -> %s", oldLabel, newLabel));
715                }
716                return true;
717            }
718
719            if (!Arrays.equals(orig.mAvatarData, newContactData.mAvatarData)) {
720                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
721                    Log.d(TAG, "avatar changed");
722                }
723                return true;
724            }
725
726            return false;
727        }
728
729        private void updateContact(final Contact c) {
730            if (c == null) {
731                return;
732            }
733
734            Contact entry = getContactInfo(c);
735            synchronized (c) {
736                if (contactChanged(c, entry)) {
737                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
738                        log("updateContact: contact changed for " + entry.mName);
739                    }
740
741                    c.mNumber = entry.mNumber;
742                    c.mLabel = entry.mLabel;
743                    c.mPersonId = entry.mPersonId;
744                    c.mPresenceResId = entry.mPresenceResId;
745                    c.mPresenceText = entry.mPresenceText;
746                    c.mAvatarData = entry.mAvatarData;
747                    c.mAvatar = entry.mAvatar;
748                    c.mContactMethodId = entry.mContactMethodId;
749                    c.mContactMethodType = entry.mContactMethodType;
750                    c.mNumberE164 = entry.mNumberE164;
751                    c.mName = entry.mName;
752                    c.mSendToVoicemail = entry.mSendToVoicemail;
753                    c.mPeopleReferenceUri = entry.mPeopleReferenceUri;
754
755                    c.notSynchronizedUpdateNameAndNumber();
756
757                    // We saw a bug where we were updating an empty contact. That would trigger
758                    // l.onUpdate() below, which would call ComposeMessageActivity.onUpdate,
759                    // which would call the adapter's notifyDataSetChanged, which would throw
760                    // away the message items and rebuild, eventually calling updateContact()
761                    // again -- all in a vicious and unending loop. Break the cycle and don't
762                    // notify if the number (the most important piece of information) is empty.
763                    if (!TextUtils.isEmpty(c.mNumber)) {
764                        // clone the list of listeners in case the onUpdate call turns around and
765                        // modifies the list of listeners
766                        // access to mListeners is synchronized on ContactsCache
767                        HashSet<UpdateListener> iterator;
768                        synchronized (mListeners) {
769                            iterator = (HashSet<UpdateListener>)Contact.mListeners.clone();
770                        }
771                        for (UpdateListener l : iterator) {
772                            if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
773                                Log.d(TAG, "updating " + l);
774                            }
775                            l.onUpdate(c);
776                        }
777                    }
778                }
779                synchronized (c) {
780                    c.mQueryPending = false;
781                    c.notifyAll();
782                }
783            }
784        }
785
786        /**
787         * Returns the caller info in Contact.
788         */
789        private Contact getContactInfo(Contact c) {
790            if (c.mIsMe) {
791                return getContactInfoForSelf();
792            } else if (Mms.isEmailAddress(c.mNumber)) {
793                return getContactInfoForEmailAddress(c.mNumber);
794            } else if (isAlphaNumber(c.mNumber)) {
795                // first try to look it up in the email field
796                Contact contact = getContactInfoForEmailAddress(c.mNumber);
797                if (contact.existsInDatabase()) {
798                    return contact;
799                }
800                // then look it up in the phone field
801                return getContactInfoForPhoneNumber(c.mNumber);
802            } else {
803                // it's a real phone number, so strip out non-digits and look it up
804                final String strippedNumber = PhoneNumberUtils.stripSeparators(c.mNumber);
805                return getContactInfoForPhoneNumber(strippedNumber);
806            }
807        }
808
809        // Some received sms's have addresses such as "OakfieldCPS" or "T-Mobile". This
810        // function will attempt to identify these and return true. If the number contains
811        // 3 or more digits, such as "jello123", this function will return false.
812        // Some countries have 3 digits shortcodes and we have to identify them as numbers.
813        //    http://en.wikipedia.org/wiki/Short_code
814        // Examples of input/output for this function:
815        //    "Jello123" -> false  [3 digits, it is considered to be the phone number "123"]
816        //    "T-Mobile" -> true   [it is considered to be the address "T-Mobile"]
817        //    "Mobile1"  -> true   [1 digit, it is considered to be the address "Mobile1"]
818        //    "Dogs77"   -> true   [2 digits, it is considered to be the address "Dogs77"]
819        //    "****1"    -> true   [1 digits, it is considered to be the address "****1"]
820        //    "#4#5#6#"  -> true   [it is considered to be the address "#4#5#6#"]
821        //    "AB12"     -> true   [2 digits, it is considered to be the address "AB12"]
822        //    "12"       -> true   [2 digits, it is considered to be the address "12"]
823        private boolean isAlphaNumber(String number) {
824            // TODO: PhoneNumberUtils.isWellFormedSmsAddress() only check if the number is a valid
825            // GSM SMS address. If the address contains a dialable char, it considers it a well
826            // formed SMS addr. CDMA doesn't work that way and has a different parser for SMS
827            // address (see CdmaSmsAddress.parse(String address)). We should definitely fix this!!!
828            if (!PhoneNumberUtils.isWellFormedSmsAddress(number)) {
829                // The example "T-Mobile" will exit here because there are no numbers.
830                return true;        // we're not an sms address, consider it an alpha number
831            }
832            if (MessageUtils.isAlias(number)) {
833                return true;
834            }
835            number = PhoneNumberUtils.extractNetworkPortion(number);
836            if (TextUtils.isEmpty(number)) {
837                return true;    // there are no digits whatsoever in the number
838            }
839            // At this point, anything like "Mobile1" or "Dogs77" will be stripped down to
840            // "1" and "77". "#4#5#6#" remains as "#4#5#6#" at this point.
841            return number.length() < 3;
842        }
843
844        /**
845         * Queries the caller id info with the phone number.
846         * @return a Contact containing the caller id info corresponding to the number.
847         */
848        private Contact getContactInfoForPhoneNumber(String number) {
849            Contact entry = new Contact(number);
850            entry.mContactMethodType = CONTACT_METHOD_TYPE_PHONE;
851            entry.mPeopleReferenceUri = Uri.fromParts("tel", number, null);
852
853            if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
854                log("queryContactInfoByNumber: number=" + number);
855            }
856
857            String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
858            String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber);
859            if (!TextUtils.isEmpty(normalizedNumber) && !TextUtils.isEmpty(minMatch)) {
860                String numberLen = String.valueOf(normalizedNumber.length());
861                String numberE164 = PhoneNumberUtils.formatNumberToE164(
862                        number, MmsApp.getApplication().getCurrentCountryIso());
863                String selection;
864                String[] args;
865                if (TextUtils.isEmpty(numberE164)) {
866                    selection = CALLER_ID_SELECTION_WITHOUT_E164;
867                    args = new String[] {minMatch, numberLen, normalizedNumber, numberLen};
868                } else {
869                    selection = CALLER_ID_SELECTION;
870                    args = new String[] {
871                            minMatch, numberE164, numberLen, normalizedNumber, numberLen};
872                }
873
874                Cursor cursor = mContext.getContentResolver().query(
875                        PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, selection, args, null);
876                if (cursor == null) {
877                    Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!"
878                            + " contact uri used " + PHONES_WITH_PRESENCE_URI);
879                    return entry;
880                }
881
882                try {
883                    if (cursor.moveToFirst()) {
884                        fillPhoneTypeContact(entry, cursor);
885                    }
886                } finally {
887                    cursor.close();
888                }
889            }
890            return entry;
891        }
892
893        /**
894         * @return a Contact containing the info for the profile.
895         */
896        private Contact getContactInfoForSelf() {
897            Contact entry = new Contact(true);
898            entry.mContactMethodType = CONTACT_METHOD_TYPE_SELF;
899
900            if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
901                log("getContactInfoForSelf");
902            }
903            Cursor cursor = mContext.getContentResolver().query(
904                    Profile.CONTENT_URI, SELF_PROJECTION, null, null, null);
905            if (cursor == null) {
906                Log.w(TAG, "getContactInfoForSelf() returned NULL cursor!"
907                        + " contact uri used " + Profile.CONTENT_URI);
908                return entry;
909            }
910
911            try {
912                if (cursor.moveToFirst()) {
913                    fillSelfContact(entry, cursor);
914                }
915            } finally {
916                cursor.close();
917            }
918            return entry;
919        }
920
921        private void fillPhoneTypeContact(final Contact contact, final Cursor cursor) {
922            synchronized (contact) {
923                contact.mContactMethodType = CONTACT_METHOD_TYPE_PHONE;
924                contact.mContactMethodId = cursor.getLong(PHONE_ID_COLUMN);
925                contact.mLabel = cursor.getString(PHONE_LABEL_COLUMN);
926                contact.mName = cursor.getString(CONTACT_NAME_COLUMN);
927                contact.mPersonId = cursor.getLong(CONTACT_ID_COLUMN);
928                contact.mPresenceResId = getPresenceIconResourceId(
929                        cursor.getInt(CONTACT_PRESENCE_COLUMN));
930                contact.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN);
931                contact.mNumberE164 = cursor.getString(PHONE_NORMALIZED_NUMBER);
932                contact.mSendToVoicemail = cursor.getInt(SEND_TO_VOICEMAIL) == 1;
933                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
934                    log("fillPhoneTypeContact: name=" + contact.mName + ", number="
935                            + contact.mNumber + ", presence=" + contact.mPresenceResId
936                            + " SendToVoicemail: " + contact.mSendToVoicemail);
937                }
938            }
939            byte[] data = loadAvatarData(contact);
940
941            synchronized (contact) {
942                contact.mAvatarData = data;
943            }
944        }
945
946        private void fillSelfContact(final Contact contact, final Cursor cursor) {
947            synchronized (contact) {
948                contact.mName = cursor.getString(SELF_NAME_COLUMN);
949                if (TextUtils.isEmpty(contact.mName)) {
950                    contact.mName = mContext.getString(R.string.messagelist_sender_self);
951                }
952                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
953                    log("fillSelfContact: name=" + contact.mName + ", number="
954                            + contact.mNumber);
955                }
956            }
957            byte[] data = loadAvatarData(contact);
958
959            synchronized (contact) {
960                contact.mAvatarData = data;
961            }
962        }
963        /*
964         * Load the avatar data from the cursor into memory.  Don't decode the data
965         * until someone calls for it (see getAvatar).  Hang onto the raw data so that
966         * we can compare it when the data is reloaded.
967         * TODO: consider comparing a checksum so that we don't have to hang onto
968         * the raw bytes after the image is decoded.
969         */
970        private byte[] loadAvatarData(Contact entry) {
971            byte [] data = null;
972
973            if ((!entry.mIsMe && entry.mPersonId == 0) || entry.mAvatar != null) {
974                return null;
975            }
976
977            if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
978                log("loadAvatarData: name=" + entry.mName + ", number=" + entry.mNumber);
979            }
980
981            // If the contact is "me", then use my local profile photo. Otherwise, build a
982            // uri to get the avatar of the contact.
983            Uri contactUri = entry.mIsMe ?
984                    Profile.CONTENT_URI :
985                    ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId);
986
987            InputStream avatarDataStream = Contacts.openContactPhotoInputStream(
988                        mContext.getContentResolver(),
989                        contactUri);
990            try {
991                if (avatarDataStream != null) {
992                    data = new byte[avatarDataStream.available()];
993                    avatarDataStream.read(data, 0, data.length);
994                }
995            } catch (IOException ex) {
996                //
997            } finally {
998                try {
999                    if (avatarDataStream != null) {
1000                        avatarDataStream.close();
1001                    }
1002                } catch (IOException e) {
1003                }
1004            }
1005
1006            return data;
1007        }
1008
1009        private int getPresenceIconResourceId(int presence) {
1010            // TODO: must fix for SDK
1011            if (presence != Presence.OFFLINE) {
1012                return Presence.getPresenceIconResourceId(presence);
1013            }
1014
1015            return 0;
1016        }
1017
1018        /**
1019         * Query the contact email table to get the name of an email address.
1020         */
1021        private Contact getContactInfoForEmailAddress(String email) {
1022            Contact entry = new Contact(email);
1023            entry.mContactMethodType = CONTACT_METHOD_TYPE_EMAIL;
1024            entry.mPeopleReferenceUri = Uri.fromParts("mailto", email, null);
1025
1026            Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(),
1027                    EMAIL_WITH_PRESENCE_URI,
1028                    EMAIL_PROJECTION,
1029                    EMAIL_SELECTION,
1030                    new String[] { email },
1031                    null);
1032
1033            if (cursor != null) {
1034                try {
1035                    while (cursor.moveToNext()) {
1036                        boolean found = false;
1037                        synchronized (entry) {
1038                            entry.mContactMethodId = cursor.getLong(EMAIL_ID_COLUMN);
1039                            entry.mPresenceResId = getPresenceIconResourceId(
1040                                    cursor.getInt(EMAIL_STATUS_COLUMN));
1041                            entry.mPersonId = cursor.getLong(EMAIL_CONTACT_ID_COLUMN);
1042                            entry.mSendToVoicemail =
1043                                    cursor.getInt(EMAIL_SEND_TO_VOICEMAIL_COLUMN) == 1;
1044
1045                            String name = cursor.getString(EMAIL_NAME_COLUMN);
1046                            if (TextUtils.isEmpty(name)) {
1047                                name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN);
1048                            }
1049                            if (!TextUtils.isEmpty(name)) {
1050                                entry.mName = name;
1051                                if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
1052                                    log("getContactInfoForEmailAddress: name=" + entry.mName +
1053                                            ", email=" + email + ", presence=" +
1054                                            entry.mPresenceResId);
1055                                }
1056                                found = true;
1057                            }
1058                        }
1059
1060                        if (found) {
1061                            byte[] data = loadAvatarData(entry);
1062                            synchronized (entry) {
1063                                entry.mAvatarData = data;
1064                            }
1065
1066                            break;
1067                        }
1068                    }
1069                } finally {
1070                    cursor.close();
1071                }
1072            }
1073            return entry;
1074        }
1075
1076        // Invert and truncate to five characters the phoneNumber so that we
1077        // can use it as the key in a hashtable.  We keep a mapping of this
1078        // key to a list of all contacts which have the same key.
1079        private String key(String phoneNumber, CharBuffer keyBuffer) {
1080            keyBuffer.clear();
1081            keyBuffer.mark();
1082
1083            int position = phoneNumber.length();
1084            int resultCount = 0;
1085            while (--position >= 0) {
1086                char c = phoneNumber.charAt(position);
1087                if (Character.isDigit(c)) {
1088                    keyBuffer.put(c);
1089                    if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) {
1090                        break;
1091                    }
1092                }
1093            }
1094            keyBuffer.reset();
1095            if (resultCount > 0) {
1096                return keyBuffer.toString();
1097            } else {
1098                // there were no usable digits in the input phoneNumber
1099                return phoneNumber;
1100            }
1101        }
1102
1103        // Reuse this so we don't have to allocate each time we go through this
1104        // "get" function.
1105        static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5;
1106        static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH);
1107
1108        private Contact internalGet(String numberOrEmail, boolean isMe) {
1109            synchronized (ContactsCache.this) {
1110                // See if we can find "number" in the hashtable.
1111                // If so, just return the result.
1112                final boolean isNotRegularPhoneNumber = isMe || Mms.isEmailAddress(numberOrEmail) ||
1113                        MessageUtils.isAlias(numberOrEmail);
1114                final String key = isNotRegularPhoneNumber ?
1115                        numberOrEmail : key(numberOrEmail, sStaticKeyBuffer);
1116
1117                ArrayList<Contact> candidates = mContactsHash.get(key);
1118                if (candidates != null) {
1119                    int length = candidates.size();
1120                    for (int i = 0; i < length; i++) {
1121                        Contact c= candidates.get(i);
1122                        if (isNotRegularPhoneNumber) {
1123                            if (numberOrEmail.equals(c.mNumber)) {
1124                                return c;
1125                            }
1126                        } else {
1127                            if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) {
1128                                return c;
1129                            }
1130                        }
1131                    }
1132                } else {
1133                    candidates = new ArrayList<Contact>();
1134                    // call toString() since it may be the static CharBuffer
1135                    mContactsHash.put(key, candidates);
1136                }
1137                Contact c = isMe ?
1138                        new Contact(true) :
1139                        new Contact(numberOrEmail);
1140                candidates.add(c);
1141                return c;
1142            }
1143        }
1144
1145        void invalidate() {
1146            // Don't remove the contacts. Just mark them stale so we'll update their
1147            // info, particularly their presence.
1148            synchronized (ContactsCache.this) {
1149                for (ArrayList<Contact> alc : mContactsHash.values()) {
1150                    for (Contact c : alc) {
1151                        synchronized (c) {
1152                            c.mIsStale = true;
1153                        }
1154                    }
1155                }
1156            }
1157        }
1158
1159        // Remove a contact from the ContactsCache based on the number or email address
1160        private void remove(Contact contact) {
1161            synchronized (ContactsCache.this) {
1162                String number = contact.getNumber();
1163                final boolean isNotRegularPhoneNumber = contact.isMe() ||
1164                                    Mms.isEmailAddress(number) ||
1165                                    MessageUtils.isAlias(number);
1166                final String key = isNotRegularPhoneNumber ?
1167                        number : key(number, sStaticKeyBuffer);
1168                ArrayList<Contact> candidates = mContactsHash.get(key);
1169                if (candidates != null) {
1170                    int length = candidates.size();
1171                    for (int i = 0; i < length; i++) {
1172                        Contact c = candidates.get(i);
1173                        if (isNotRegularPhoneNumber) {
1174                            if (number.equals(c.mNumber)) {
1175                                candidates.remove(i);
1176                                break;
1177                            }
1178                        } else {
1179                            if (PhoneNumberUtils.compare(number, c.mNumber)) {
1180                                candidates.remove(i);
1181                                break;
1182                            }
1183                        }
1184                    }
1185                    if (candidates.size() == 0) {
1186                        mContactsHash.remove(key);
1187                    }
1188                }
1189            }
1190        }
1191    }
1192
1193    private static void log(String msg) {
1194        Log.d(TAG, msg);
1195    }
1196}
1197