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