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