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