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