1 package com.android.mms.data;
2 
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.nio.CharBuffer;
6 import java.util.ArrayList;
7 import java.util.Arrays;
8 import java.util.HashMap;
9 import java.util.HashSet;
10 import java.util.List;
11 
12 import android.content.ContentUris;
13 import android.content.Context;
14 import android.database.ContentObserver;
15 import android.database.Cursor;
16 import android.database.sqlite.SqliteWrapper;
17 import android.graphics.Bitmap;
18 import android.graphics.BitmapFactory;
19 import android.graphics.drawable.BitmapDrawable;
20 import android.graphics.drawable.Drawable;
21 import android.net.Uri;
22 import android.os.Handler;
23 import android.os.Parcelable;
24 import android.provider.ContactsContract.CommonDataKinds.Email;
25 import android.provider.ContactsContract.CommonDataKinds.Phone;
26 import android.provider.ContactsContract.Contacts;
27 import android.provider.ContactsContract.Data;
28 import android.provider.ContactsContract.Presence;
29 import android.provider.ContactsContract.Profile;
30 import android.provider.Telephony.Mms;
31 import android.telephony.PhoneNumberUtils;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.mms.LogTag;
36 import com.android.mms.MmsApp;
37 import com.android.mms.R;
38 import com.android.mms.ui.MessageUtils;
39 
40 public 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 {
onUpdate(Contact updated)98         public void onUpdate(Contact updated);
99     }
100 
Contact(String number, String name)101     private Contact(String number, String name) {
102         init(number, name);
103     }
104     /*
105      * Make a basic contact object with a phone number.
106      */
Contact(String number)107     private Contact(String number) {
108         init(number, "");
109     }
110 
Contact(boolean isMe)111     private Contact(boolean isMe) {
112         init(SELF_ITEM_KEY, "");
113         mIsMe = isMe;
114     }
115 
init(String number, String name)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
toString()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 
logWithTrace(String tag, String msg, Object... format)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 
get(String number, boolean canBlock)161     public static Contact get(String number, boolean canBlock) {
162         return sContactCache.get(number, canBlock);
163     }
164 
getMe(boolean canBlock)165     public static Contact getMe(boolean canBlock) {
166         return sContactCache.getMe(canBlock);
167     }
168 
removeFromCache()169     public void removeFromCache() {
170         sContactCache.remove(this);
171     }
172 
getByPhoneUris(Parcelable[] uris)173     public static List<Contact> getByPhoneUris(Parcelable[] uris) {
174         return sContactCache.getContactInfoForPhoneUris(uris);
175     }
176 
invalidateCache()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 
isMe()192     public boolean isMe() {
193         return mIsMe;
194     }
195 
emptyIfNull(String s)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      */
formatNameAndNumber(String name, String number, String numberE164)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 
reload()227     public synchronized void reload() {
228         mIsStale = true;
229         sContactCache.get(mNumber, false);
230     }
231 
getNumber()232     public synchronized String getNumber() {
233         return mNumber;
234     }
235 
setNumber(String number)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 
isNumberModified()247     public boolean isNumberModified() {
248         return mNumberIsModified;
249     }
250 
getSendToVoicemail()251     public boolean getSendToVoicemail() {
252         return mSendToVoicemail;
253     }
254 
setIsNumberModified(boolean flag)255     public void setIsNumberModified(boolean flag) {
256         mNumberIsModified = flag;
257     }
258 
getName()259     public synchronized String getName() {
260         if (TextUtils.isEmpty(mName)) {
261             return mNumber;
262         } else {
263             return mName;
264         }
265     }
266 
getNameAndNumber()267     public synchronized String getNameAndNumber() {
268         return mNameAndNumber;
269     }
270 
notSynchronizedUpdateNameAndNumber()271     private void notSynchronizedUpdateNameAndNumber() {
272         mNameAndNumber = formatNameAndNumber(mName, mNumber, mNumberE164);
273     }
274 
getRecipientId()275     public synchronized long getRecipientId() {
276         return mRecipientId;
277     }
278 
setRecipientId(long id)279     public synchronized void setRecipientId(long id) {
280         mRecipientId = id;
281     }
282 
getLabel()283     public synchronized String getLabel() {
284         return mLabel;
285     }
286 
getUri()287     public synchronized Uri getUri() {
288         return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
289     }
290 
getPresenceResId()291     public synchronized int getPresenceResId() {
292         return mPresenceResId;
293     }
294 
existsInDatabase()295     public synchronized boolean existsInDatabase() {
296         return (mPersonId > 0);
297     }
298 
addListener(UpdateListener l)299     public static void addListener(UpdateListener l) {
300         synchronized (mListeners) {
301             mListeners.add(l);
302         }
303     }
304 
removeListener(UpdateListener l)305     public static void removeListener(UpdateListener l) {
306         synchronized (mListeners) {
307             mListeners.remove(l);
308         }
309     }
310 
dumpListeners()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 
isEmail()321     public synchronized boolean isEmail() {
322         return Mms.isEmailAddress(mNumber);
323     }
324 
getPresenceText()325     public String getPresenceText() {
326         return mPresenceText;
327     }
328 
getContactMethodType()329     public int getContactMethodType() {
330         return mContactMethodType;
331     }
332 
getPeopleReferenceUri()333     public Uri getPeopleReferenceUri() {
334         return mPeopleReferenceUri;
335     }
336 
getContactMethodId()337     public long getContactMethodId() {
338         return mContactMethodId;
339     }
340 
getPhoneUri()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 
getAvatar(Context context, Drawable defaultValue)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 
init(final Context context)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 
dump()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 
ContactsCache(Context context)482         private ContactsCache(Context context) {
483             mContext = context;
484         }
485 
dump()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 
TaskStack()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 
push(Runnable r)531             public void push(Runnable r) {
532                 synchronized (mThingsToLoad) {
533                     mThingsToLoad.add(r);
534                     mThingsToLoad.notify();
535                 }
536             }
537         }
538 
pushTask(Runnable r)539         public void pushTask(Runnable r) {
540             mTaskQueue.push(r);
541         }
542 
getMe(boolean canBlock)543         public Contact getMe(boolean canBlock) {
544             return get(SELF_ITEM_KEY, true, canBlock);
545         }
546 
get(String number, boolean canBlock)547         public Contact get(String number, boolean canBlock) {
548             return get(number, false, canBlock);
549         }
550 
get(String number, boolean isMe, boolean canBlock)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          */
getContactInfoForPhoneUris(Parcelable[] uris)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 
contactChanged(Contact orig, Contact newContactData)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 
updateContact(final Contact c)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          */
getContactInfo(Contact c)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"]
isAlphaNumber(String number)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          */
getContactInfoForPhoneNumber(String number)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          */
getContactInfoForSelf()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 
fillPhoneTypeContact(final Contact contact, final Cursor cursor)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 
fillSelfContact(final Contact contact, final Cursor cursor)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          */
loadAvatarData(Contact entry)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 
getPresenceIconResourceId(int presence)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          */
getContactInfoForEmailAddress(String email)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.
key(String phoneNumber, CharBuffer keyBuffer)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 
internalGet(String numberOrEmail, boolean isMe)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 
invalidate()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
remove(Contact contact)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 
log(String msg)1193     private static void log(String msg) {
1194         Log.d(TAG, msg);
1195     }
1196 }
1197