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