• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = "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 {
onUpdate(Contact updated)97         public void onUpdate(Contact updated);
98     }
99 
Contact(String number, String name)100     private Contact(String number, String name) {
101         init(number, name);
102     }
103     /*
104      * Make a basic contact object with a phone number.
105      */
Contact(String number)106     private Contact(String number) {
107         init(number, "");
108     }
109 
Contact(boolean isMe)110     private Contact(boolean isMe) {
111         init(SELF_ITEM_KEY, "");
112         mIsMe = isMe;
113     }
114 
init(String number, String name)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
toString()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 
logWithTrace(String tag, String msg, Object... format)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 
get(String number, boolean canBlock)160     public static Contact get(String number, boolean canBlock) {
161         return sContactCache.get(number, canBlock);
162     }
163 
getMe(boolean canBlock)164     public static Contact getMe(boolean canBlock) {
165         return sContactCache.getMe(canBlock);
166     }
167 
removeFromCache()168     public void removeFromCache() {
169         sContactCache.remove(this);
170     }
171 
getByPhoneUris(Parcelable[] uris)172     public static List<Contact> getByPhoneUris(Parcelable[] uris) {
173         return sContactCache.getContactInfoForPhoneUris(uris);
174     }
175 
invalidateCache()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 
isMe()191     public boolean isMe() {
192         return mIsMe;
193     }
194 
emptyIfNull(String s)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      */
formatNameAndNumber(String name, String number, String numberE164)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 
reload()226     public synchronized void reload() {
227         mIsStale = true;
228         sContactCache.get(mNumber, false);
229     }
230 
getNumber()231     public synchronized String getNumber() {
232         return mNumber;
233     }
234 
setNumber(String number)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 
isNumberModified()246     public boolean isNumberModified() {
247         return mNumberIsModified;
248     }
249 
getSendToVoicemail()250     public boolean getSendToVoicemail() {
251         return mSendToVoicemail;
252     }
253 
setIsNumberModified(boolean flag)254     public void setIsNumberModified(boolean flag) {
255         mNumberIsModified = flag;
256     }
257 
getName()258     public synchronized String getName() {
259         if (TextUtils.isEmpty(mName)) {
260             return mNumber;
261         } else {
262             return mName;
263         }
264     }
265 
getNameAndNumber()266     public synchronized String getNameAndNumber() {
267         return mNameAndNumber;
268     }
269 
notSynchronizedUpdateNameAndNumber()270     private void notSynchronizedUpdateNameAndNumber() {
271         mNameAndNumber = formatNameAndNumber(mName, mNumber, mNumberE164);
272     }
273 
getRecipientId()274     public synchronized long getRecipientId() {
275         return mRecipientId;
276     }
277 
setRecipientId(long id)278     public synchronized void setRecipientId(long id) {
279         mRecipientId = id;
280     }
281 
getLabel()282     public synchronized String getLabel() {
283         return mLabel;
284     }
285 
getUri()286     public synchronized Uri getUri() {
287         return ContentUris.withAppendedId(Contacts.CONTENT_URI, mPersonId);
288     }
289 
getPresenceResId()290     public synchronized int getPresenceResId() {
291         return mPresenceResId;
292     }
293 
existsInDatabase()294     public synchronized boolean existsInDatabase() {
295         return (mPersonId > 0);
296     }
297 
addListener(UpdateListener l)298     public static void addListener(UpdateListener l) {
299         synchronized (mListeners) {
300             mListeners.add(l);
301         }
302     }
303 
removeListener(UpdateListener l)304     public static void removeListener(UpdateListener l) {
305         synchronized (mListeners) {
306             mListeners.remove(l);
307         }
308     }
309 
dumpListeners()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 
isEmail()320     public synchronized boolean isEmail() {
321         return Mms.isEmailAddress(mNumber);
322     }
323 
getPresenceText()324     public String getPresenceText() {
325         return mPresenceText;
326     }
327 
getContactMethodType()328     public int getContactMethodType() {
329         return mContactMethodType;
330     }
331 
getContactMethodId()332     public long getContactMethodId() {
333         return mContactMethodId;
334     }
335 
getPhoneUri()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 
getAvatar(Context context, Drawable defaultValue)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 
init(final Context context)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 
dump()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 
ContactsCache(Context context)474         private ContactsCache(Context context) {
475             mContext = context;
476         }
477 
dump()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 
TaskStack()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 
push(Runnable r)523             public void push(Runnable r) {
524                 synchronized (mThingsToLoad) {
525                     mThingsToLoad.add(r);
526                     mThingsToLoad.notify();
527                 }
528             }
529         }
530 
pushTask(Runnable r)531         public void pushTask(Runnable r) {
532             mTaskQueue.push(r);
533         }
534 
getMe(boolean canBlock)535         public Contact getMe(boolean canBlock) {
536             return get(SELF_ITEM_KEY, true, canBlock);
537         }
538 
get(String number, boolean canBlock)539         public Contact get(String number, boolean canBlock) {
540             return get(number, false, canBlock);
541         }
542 
get(String number, boolean isMe, boolean canBlock)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          */
getContactInfoForPhoneUris(Parcelable[] uris)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 
contactChanged(Contact orig, Contact newContactData)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 
updateContact(final Contact c)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          */
getContactInfo(Contact c)780         private Contact getContactInfo(Contact c) {
781             if (c.mIsMe) {
782                 return getContactInfoForSelf();
783             } else if (Mms.isEmailAddress(c.mNumber)) {
784                 return getContactInfoForEmailAddress(c.mNumber);
785             } else if (isAlphaNumber(c.mNumber)) {
786                 // first try to look it up in the email field
787                 Contact contact = getContactInfoForEmailAddress(c.mNumber);
788                 if (contact.existsInDatabase()) {
789                     return contact;
790                 }
791                 // then look it up in the phone field
792                 return getContactInfoForPhoneNumber(c.mNumber);
793             } else {
794                 // it's a real phone number, so strip out non-digits and look it up
795                 final String strippedNumber = PhoneNumberUtils.stripSeparators(c.mNumber);
796                 return getContactInfoForPhoneNumber(strippedNumber);
797             }
798         }
799 
800         // Some received sms's have addresses such as "OakfieldCPS" or "T-Mobile". This
801         // function will attempt to identify these and return true. If the number contains
802         // 3 or more digits, such as "jello123", this function will return false.
803         // Some countries have 3 digits shortcodes and we have to identify them as numbers.
804         //    http://en.wikipedia.org/wiki/Short_code
805         // Examples of input/output for this function:
806         //    "Jello123" -> false  [3 digits, it is considered to be the phone number "123"]
807         //    "T-Mobile" -> true   [it is considered to be the address "T-Mobile"]
808         //    "Mobile1"  -> true   [1 digit, it is considered to be the address "Mobile1"]
809         //    "Dogs77"   -> true   [2 digits, it is considered to be the address "Dogs77"]
810         //    "****1"    -> true   [1 digits, it is considered to be the address "****1"]
811         //    "#4#5#6#"  -> true   [it is considered to be the address "#4#5#6#"]
812         //    "AB12"     -> true   [2 digits, it is considered to be the address "AB12"]
813         //    "12"       -> true   [2 digits, it is considered to be the address "12"]
isAlphaNumber(String number)814         private boolean isAlphaNumber(String number) {
815             // TODO: PhoneNumberUtils.isWellFormedSmsAddress() only check if the number is a valid
816             // GSM SMS address. If the address contains a dialable char, it considers it a well
817             // formed SMS addr. CDMA doesn't work that way and has a different parser for SMS
818             // address (see CdmaSmsAddress.parse(String address)). We should definitely fix this!!!
819             if (!PhoneNumberUtils.isWellFormedSmsAddress(number)) {
820                 // The example "T-Mobile" will exit here because there are no numbers.
821                 return true;        // we're not an sms address, consider it an alpha number
822             }
823             if (MessageUtils.isAlias(number)) {
824                 return true;
825             }
826             number = PhoneNumberUtils.extractNetworkPortion(number);
827             if (TextUtils.isEmpty(number)) {
828                 return true;    // there are no digits whatsoever in the number
829             }
830             // At this point, anything like "Mobile1" or "Dogs77" will be stripped down to
831             // "1" and "77". "#4#5#6#" remains as "#4#5#6#" at this point.
832             return number.length() < 3;
833         }
834 
835         /**
836          * Queries the caller id info with the phone number.
837          * @return a Contact containing the caller id info corresponding to the number.
838          */
getContactInfoForPhoneNumber(String number)839         private Contact getContactInfoForPhoneNumber(String number) {
840             Contact entry = new Contact(number);
841             entry.mContactMethodType = CONTACT_METHOD_TYPE_PHONE;
842 
843             if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
844                 log("queryContactInfoByNumber: number=" + number);
845             }
846 
847             String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
848             String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber);
849             if (!TextUtils.isEmpty(normalizedNumber) && !TextUtils.isEmpty(minMatch)) {
850                 String numberLen = String.valueOf(normalizedNumber.length());
851                 String numberE164 = PhoneNumberUtils.formatNumberToE164(
852                         number, MmsApp.getApplication().getCurrentCountryIso());
853                 String selection;
854                 String[] args;
855                 if (TextUtils.isEmpty(numberE164)) {
856                     selection = CALLER_ID_SELECTION_WITHOUT_E164;
857                     args = new String[] {minMatch, numberLen, normalizedNumber, numberLen};
858                 } else {
859                     selection = CALLER_ID_SELECTION;
860                     args = new String[] {
861                             minMatch, numberE164, numberLen, normalizedNumber, numberLen};
862                 }
863 
864                 Cursor cursor = mContext.getContentResolver().query(
865                         PHONES_WITH_PRESENCE_URI, CALLER_ID_PROJECTION, selection, args, null);
866                 if (cursor == null) {
867                     Log.w(TAG, "queryContactInfoByNumber(" + number + ") returned NULL cursor!"
868                             + " contact uri used " + PHONES_WITH_PRESENCE_URI);
869                     return entry;
870                 }
871 
872                 try {
873                     if (cursor.moveToFirst()) {
874                         fillPhoneTypeContact(entry, cursor);
875                     }
876                 } finally {
877                     cursor.close();
878                 }
879             }
880             return entry;
881         }
882 
883         /**
884          * @return a Contact containing the info for the profile.
885          */
getContactInfoForSelf()886         private Contact getContactInfoForSelf() {
887             Contact entry = new Contact(true);
888             entry.mContactMethodType = CONTACT_METHOD_TYPE_SELF;
889 
890             if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
891                 log("getContactInfoForSelf");
892             }
893             Cursor cursor = mContext.getContentResolver().query(
894                     Profile.CONTENT_URI, SELF_PROJECTION, null, null, null);
895             if (cursor == null) {
896                 Log.w(TAG, "getContactInfoForSelf() returned NULL cursor!"
897                         + " contact uri used " + Profile.CONTENT_URI);
898                 return entry;
899             }
900 
901             try {
902                 if (cursor.moveToFirst()) {
903                     fillSelfContact(entry, cursor);
904                 }
905             } finally {
906                 cursor.close();
907             }
908             return entry;
909         }
910 
fillPhoneTypeContact(final Contact contact, final Cursor cursor)911         private void fillPhoneTypeContact(final Contact contact, final Cursor cursor) {
912             synchronized (contact) {
913                 contact.mContactMethodType = CONTACT_METHOD_TYPE_PHONE;
914                 contact.mContactMethodId = cursor.getLong(PHONE_ID_COLUMN);
915                 contact.mLabel = cursor.getString(PHONE_LABEL_COLUMN);
916                 contact.mName = cursor.getString(CONTACT_NAME_COLUMN);
917                 contact.mPersonId = cursor.getLong(CONTACT_ID_COLUMN);
918                 contact.mPresenceResId = getPresenceIconResourceId(
919                         cursor.getInt(CONTACT_PRESENCE_COLUMN));
920                 contact.mPresenceText = cursor.getString(CONTACT_STATUS_COLUMN);
921                 contact.mNumberE164 = cursor.getString(PHONE_NORMALIZED_NUMBER);
922                 contact.mSendToVoicemail = cursor.getInt(SEND_TO_VOICEMAIL) == 1;
923                 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
924                     log("fillPhoneTypeContact: name=" + contact.mName + ", number="
925                             + contact.mNumber + ", presence=" + contact.mPresenceResId
926                             + " SendToVoicemail: " + contact.mSendToVoicemail);
927                 }
928             }
929             byte[] data = loadAvatarData(contact);
930 
931             synchronized (contact) {
932                 contact.mAvatarData = data;
933             }
934         }
935 
fillSelfContact(final Contact contact, final Cursor cursor)936         private void fillSelfContact(final Contact contact, final Cursor cursor) {
937             synchronized (contact) {
938                 contact.mName = cursor.getString(SELF_NAME_COLUMN);
939                 if (TextUtils.isEmpty(contact.mName)) {
940                     contact.mName = mContext.getString(R.string.messagelist_sender_self);
941                 }
942                 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
943                     log("fillSelfContact: name=" + contact.mName + ", number="
944                             + contact.mNumber);
945                 }
946             }
947             byte[] data = loadAvatarData(contact);
948 
949             synchronized (contact) {
950                 contact.mAvatarData = data;
951             }
952         }
953         /*
954          * Load the avatar data from the cursor into memory.  Don't decode the data
955          * until someone calls for it (see getAvatar).  Hang onto the raw data so that
956          * we can compare it when the data is reloaded.
957          * TODO: consider comparing a checksum so that we don't have to hang onto
958          * the raw bytes after the image is decoded.
959          */
loadAvatarData(Contact entry)960         private byte[] loadAvatarData(Contact entry) {
961             byte [] data = null;
962 
963             if ((!entry.mIsMe && entry.mPersonId == 0) || entry.mAvatar != null) {
964                 return null;
965             }
966 
967             if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
968                 log("loadAvatarData: name=" + entry.mName + ", number=" + entry.mNumber);
969             }
970 
971             // If the contact is "me", then use my local profile photo. Otherwise, build a
972             // uri to get the avatar of the contact.
973             Uri contactUri = entry.mIsMe ?
974                     Profile.CONTENT_URI :
975                     ContentUris.withAppendedId(Contacts.CONTENT_URI, entry.mPersonId);
976 
977             InputStream avatarDataStream = Contacts.openContactPhotoInputStream(
978                         mContext.getContentResolver(),
979                         contactUri);
980             try {
981                 if (avatarDataStream != null) {
982                     data = new byte[avatarDataStream.available()];
983                     avatarDataStream.read(data, 0, data.length);
984                 }
985             } catch (IOException ex) {
986                 //
987             } finally {
988                 try {
989                     if (avatarDataStream != null) {
990                         avatarDataStream.close();
991                     }
992                 } catch (IOException e) {
993                 }
994             }
995 
996             return data;
997         }
998 
getPresenceIconResourceId(int presence)999         private int getPresenceIconResourceId(int presence) {
1000             // TODO: must fix for SDK
1001             if (presence != Presence.OFFLINE) {
1002                 return Presence.getPresenceIconResourceId(presence);
1003             }
1004 
1005             return 0;
1006         }
1007 
1008         /**
1009          * Query the contact email table to get the name of an email address.
1010          */
getContactInfoForEmailAddress(String email)1011         private Contact getContactInfoForEmailAddress(String email) {
1012             Contact entry = new Contact(email);
1013             entry.mContactMethodType = CONTACT_METHOD_TYPE_EMAIL;
1014 
1015             Cursor cursor = SqliteWrapper.query(mContext, mContext.getContentResolver(),
1016                     EMAIL_WITH_PRESENCE_URI,
1017                     EMAIL_PROJECTION,
1018                     EMAIL_SELECTION,
1019                     new String[] { email },
1020                     null);
1021 
1022             if (cursor != null) {
1023                 try {
1024                     while (cursor.moveToNext()) {
1025                         boolean found = false;
1026                         synchronized (entry) {
1027                             entry.mContactMethodId = cursor.getLong(EMAIL_ID_COLUMN);
1028                             entry.mPresenceResId = getPresenceIconResourceId(
1029                                     cursor.getInt(EMAIL_STATUS_COLUMN));
1030                             entry.mPersonId = cursor.getLong(EMAIL_CONTACT_ID_COLUMN);
1031                             entry.mSendToVoicemail =
1032                                     cursor.getInt(EMAIL_SEND_TO_VOICEMAIL_COLUMN) == 1;
1033 
1034                             String name = cursor.getString(EMAIL_NAME_COLUMN);
1035                             if (TextUtils.isEmpty(name)) {
1036                                 name = cursor.getString(EMAIL_CONTACT_NAME_COLUMN);
1037                             }
1038                             if (!TextUtils.isEmpty(name)) {
1039                                 entry.mName = name;
1040                                 if (Log.isLoggable(LogTag.CONTACT, Log.DEBUG)) {
1041                                     log("getContactInfoForEmailAddress: name=" + entry.mName +
1042                                             ", email=" + email + ", presence=" +
1043                                             entry.mPresenceResId);
1044                                 }
1045                                 found = true;
1046                             }
1047                         }
1048 
1049                         if (found) {
1050                             byte[] data = loadAvatarData(entry);
1051                             synchronized (entry) {
1052                                 entry.mAvatarData = data;
1053                             }
1054 
1055                             break;
1056                         }
1057                     }
1058                 } finally {
1059                     cursor.close();
1060                 }
1061             }
1062             return entry;
1063         }
1064 
1065         // Invert and truncate to five characters the phoneNumber so that we
1066         // can use it as the key in a hashtable.  We keep a mapping of this
1067         // key to a list of all contacts which have the same key.
key(String phoneNumber, CharBuffer keyBuffer)1068         private String key(String phoneNumber, CharBuffer keyBuffer) {
1069             keyBuffer.clear();
1070             keyBuffer.mark();
1071 
1072             int position = phoneNumber.length();
1073             int resultCount = 0;
1074             while (--position >= 0) {
1075                 char c = phoneNumber.charAt(position);
1076                 if (Character.isDigit(c)) {
1077                     keyBuffer.put(c);
1078                     if (++resultCount == STATIC_KEY_BUFFER_MAXIMUM_LENGTH) {
1079                         break;
1080                     }
1081                 }
1082             }
1083             keyBuffer.reset();
1084             if (resultCount > 0) {
1085                 return keyBuffer.toString();
1086             } else {
1087                 // there were no usable digits in the input phoneNumber
1088                 return phoneNumber;
1089             }
1090         }
1091 
1092         // Reuse this so we don't have to allocate each time we go through this
1093         // "get" function.
1094         static final int STATIC_KEY_BUFFER_MAXIMUM_LENGTH = 5;
1095         static CharBuffer sStaticKeyBuffer = CharBuffer.allocate(STATIC_KEY_BUFFER_MAXIMUM_LENGTH);
1096 
internalGet(String numberOrEmail, boolean isMe)1097         private Contact internalGet(String numberOrEmail, boolean isMe) {
1098             synchronized (ContactsCache.this) {
1099                 // See if we can find "number" in the hashtable.
1100                 // If so, just return the result.
1101                 final boolean isNotRegularPhoneNumber = isMe || Mms.isEmailAddress(numberOrEmail) ||
1102                         MessageUtils.isAlias(numberOrEmail);
1103                 final String key = isNotRegularPhoneNumber ?
1104                         numberOrEmail : key(numberOrEmail, sStaticKeyBuffer);
1105 
1106                 ArrayList<Contact> candidates = mContactsHash.get(key);
1107                 if (candidates != null) {
1108                     int length = candidates.size();
1109                     for (int i = 0; i < length; i++) {
1110                         Contact c= candidates.get(i);
1111                         if (isNotRegularPhoneNumber) {
1112                             if (numberOrEmail.equals(c.mNumber)) {
1113                                 return c;
1114                             }
1115                         } else {
1116                             if (PhoneNumberUtils.compare(numberOrEmail, c.mNumber)) {
1117                                 return c;
1118                             }
1119                         }
1120                     }
1121                 } else {
1122                     candidates = new ArrayList<Contact>();
1123                     // call toString() since it may be the static CharBuffer
1124                     mContactsHash.put(key, candidates);
1125                 }
1126                 Contact c = isMe ?
1127                         new Contact(true) :
1128                         new Contact(numberOrEmail);
1129                 candidates.add(c);
1130                 return c;
1131             }
1132         }
1133 
invalidate()1134         void invalidate() {
1135             // Don't remove the contacts. Just mark them stale so we'll update their
1136             // info, particularly their presence.
1137             synchronized (ContactsCache.this) {
1138                 for (ArrayList<Contact> alc : mContactsHash.values()) {
1139                     for (Contact c : alc) {
1140                         synchronized (c) {
1141                             c.mIsStale = true;
1142                         }
1143                     }
1144                 }
1145             }
1146         }
1147 
1148         // Remove a contact from the ContactsCache based on the number or email address
remove(Contact contact)1149         private void remove(Contact contact) {
1150             synchronized (ContactsCache.this) {
1151                 String number = contact.getNumber();
1152                 final boolean isNotRegularPhoneNumber = contact.isMe() ||
1153                                     Mms.isEmailAddress(number) ||
1154                                     MessageUtils.isAlias(number);
1155                 final String key = isNotRegularPhoneNumber ?
1156                         number : key(number, sStaticKeyBuffer);
1157                 ArrayList<Contact> candidates = mContactsHash.get(key);
1158                 if (candidates != null) {
1159                     int length = candidates.size();
1160                     for (int i = 0; i < length; i++) {
1161                         Contact c = candidates.get(i);
1162                         if (isNotRegularPhoneNumber) {
1163                             if (number.equals(c.mNumber)) {
1164                                 candidates.remove(i);
1165                                 break;
1166                             }
1167                         } else {
1168                             if (PhoneNumberUtils.compare(number, c.mNumber)) {
1169                                 candidates.remove(i);
1170                                 break;
1171                             }
1172                         }
1173                     }
1174                     if (candidates.size() == 0) {
1175                         mContactsHash.remove(key);
1176                     }
1177                 }
1178             }
1179         }
1180     }
1181 
log(String msg)1182     private static void log(String msg) {
1183         Log.d(TAG, msg);
1184     }
1185 }
1186