Merge "Fix intermittently failing unit test."
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index c59cbf9..3383ab2 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -27,8 +27,8 @@
 import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener;
 import com.android.contacts.util.ExpirableCache;
 import com.android.contacts.voicemail.VoicemailStatusHelper;
-import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
 import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
+import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
 import com.android.internal.telephony.CallerInfo;
 import com.android.internal.telephony.ITelephony;
 import com.google.common.annotations.VisibleForTesting;
@@ -186,6 +186,42 @@
         public String lookupKey;
 
         public static ContactInfo EMPTY = new ContactInfo();
+
+        @Override
+        public int hashCode() {
+            // Uses only name and personId to determine hashcode.
+            // This should be sufficient to have a reasonable distribution of hash codes.
+            // Moreover, there should be no two people with the same personId.
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + (int) (personId ^ (personId >>> 32));
+            result = prime * result + ((name == null) ? 0 : name.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            ContactInfo other = (ContactInfo) obj;
+            if (personId != other.personId) return false;
+            if (!TextUtils.equals(name, other.name)) return false;
+            if (type != other.type) return false;
+            if (!TextUtils.equals(label, other.label)) return false;
+            if (!TextUtils.equals(number, other.number)) return false;
+            // Ignore formatted number.
+            if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
+            if (!uriEquals(thumbnailUri, other.thumbnailUri)) return false;
+            if (!TextUtils.equals(lookupKey, other.lookupKey)) return false;
+            return true;
+        }
+
+        private static boolean uriEquals(Uri thumbnailUri1, Uri thumbnailUri2) {
+            if (thumbnailUri1 == thumbnailUri2) return true;
+            if (thumbnailUri1 == null) return false;
+            return thumbnailUri1.equals(thumbnailUri2);
+        }
     }
 
     public interface GroupCreator {
@@ -363,129 +399,175 @@
             }
         }
 
-        private boolean queryContactInfo(String number) {
-            // First check if there was a prior request for the same number
-            // that was already satisfied
-            ContactInfo info = mContactInfoCache.get(number);
-            boolean needNotify = false;
-            if (info != null && info != ContactInfo.EMPTY) {
-                return true;
-            } else {
-                // Ok, do a fresh Contacts lookup for ciq.number.
-                boolean infoUpdated = false;
+        /**
+         * Determines the contact information for the given SIP address.
+         * <p>
+         * It returns the contact info if found.
+         * <p>
+         * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
+         * <p>
+         * If the lookup fails for some other reason, it returns null.
+         */
+        private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
+            final ContactInfo info;
 
-                if (PhoneNumberUtils.isUriNumber(number)) {
-                    // This "number" is really a SIP address.
+            // TODO: This code is duplicated from the
+            // CallerInfoAsyncQuery class.  To avoid that, could the
+            // code here just use CallerInfoAsyncQuery, rather than
+            // manually running ContentResolver.query() itself?
 
-                    // TODO: This code is duplicated from the
-                    // CallerInfoAsyncQuery class.  To avoid that, could the
-                    // code here just use CallerInfoAsyncQuery, rather than
-                    // manually running ContentResolver.query() itself?
+            // We look up SIP addresses directly in the Data table:
+            Uri contactRef = Data.CONTENT_URI;
 
-                    // We look up SIP addresses directly in the Data table:
-                    Uri contactRef = Data.CONTENT_URI;
+            // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+            //
+            // Also note we use "upper(data1)" in the WHERE clause, and
+            // uppercase the incoming SIP address, in order to do a
+            // case-insensitive match.
+            //
+            // TODO: May also need to normalize by adding "sip:" as a
+            // prefix, if we start storing SIP addresses that way in the
+            // database.
+            String selection = "upper(" + Data.DATA1 + ")=?"
+                    + " AND "
+                    + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
+            String[] selectionArgs = new String[] { sipAddress.toUpperCase() };
 
+            Cursor dataTableCursor =
+                    getActivity().getContentResolver().query(
+                            contactRef,
+                            null,  // projection
+                            selection,  // selection
+                            selectionArgs,  // selectionArgs
+                            null);  // sortOrder
+
+            if (dataTableCursor != null) {
+                if (dataTableCursor.moveToFirst()) {
+                    info = new ContactInfo();
+
+                    // TODO: we could slightly speed this up using an
+                    // explicit projection (and thus not have to do
+                    // those getColumnIndex() calls) but the benefit is
+                    // very minimal.
+
+                    // Note the Data.CONTACT_ID column here is
+                    // equivalent to the PERSON_ID_COLUMN_INDEX column
+                    // we use with "phonesCursor" below.
+                    info.personId = dataTableCursor.getLong(
+                            dataTableCursor.getColumnIndex(Data.CONTACT_ID));
+                    info.name = dataTableCursor.getString(
+                            dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
+                    // "type" and "label" are currently unused for SIP addresses
+                    info.type = SipAddress.TYPE_OTHER;
+                    info.label = null;
+
+                    // And "number" is the SIP address.
                     // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
-                    //
-                    // Also note we use "upper(data1)" in the WHERE clause, and
-                    // uppercase the incoming SIP address, in order to do a
-                    // case-insensitive match.
-                    //
-                    // TODO: May also need to normalize by adding "sip:" as a
-                    // prefix, if we start storing SIP addresses that way in the
-                    // database.
-                    String selection = "upper(" + Data.DATA1 + ")=?"
-                            + " AND "
-                            + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
-                    String[] selectionArgs = new String[] { number.toUpperCase() };
-
-                    Cursor dataTableCursor =
-                            getActivity().getContentResolver().query(
-                                    contactRef,
-                                    null,  // projection
-                                    selection,  // selection
-                                    selectionArgs,  // selectionArgs
-                                    null);  // sortOrder
-
-                    if (dataTableCursor != null) {
-                        if (dataTableCursor.moveToFirst()) {
-                            info = new ContactInfo();
-
-                            // TODO: we could slightly speed this up using an
-                            // explicit projection (and thus not have to do
-                            // those getColumnIndex() calls) but the benefit is
-                            // very minimal.
-
-                            // Note the Data.CONTACT_ID column here is
-                            // equivalent to the PERSON_ID_COLUMN_INDEX column
-                            // we use with "phonesCursor" below.
-                            info.personId = dataTableCursor.getLong(
-                                    dataTableCursor.getColumnIndex(Data.CONTACT_ID));
-                            info.name = dataTableCursor.getString(
-                                    dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
-                            // "type" and "label" are currently unused for SIP addresses
-                            info.type = SipAddress.TYPE_OTHER;
-                            info.label = null;
-
-                            // And "number" is the SIP address.
-                            // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
-                            info.number = dataTableCursor.getString(
-                                    dataTableCursor.getColumnIndex(Data.DATA1));
-                            info.normalizedNumber = null;  // meaningless for SIP addresses
-                            final String thumbnailUriString = dataTableCursor.getString(
-                                    dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
-                            info.thumbnailUri = thumbnailUriString == null
-                                    ? null
-                                    : Uri.parse(thumbnailUriString);
-                            info.lookupKey = dataTableCursor.getString(
-                                    dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
-
-                            infoUpdated = true;
-                        }
-                        dataTableCursor.close();
-                    }
+                    info.number = dataTableCursor.getString(
+                            dataTableCursor.getColumnIndex(Data.DATA1));
+                    info.normalizedNumber = null;  // meaningless for SIP addresses
+                    final String thumbnailUriString = dataTableCursor.getString(
+                            dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
+                    info.thumbnailUri = thumbnailUriString == null
+                            ? null
+                            : Uri.parse(thumbnailUriString);
+                    info.lookupKey = dataTableCursor.getString(
+                            dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
                 } else {
-                    // "number" is a regular phone number, so use the
-                    // PhoneLookup table:
-                    Cursor phonesCursor =
-                            getActivity().getContentResolver().query(
-                                Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
-                                        Uri.encode(number)),
-                                        PhoneQuery._PROJECTION, null, null, null);
-                    if (phonesCursor != null) {
-                        if (phonesCursor.moveToFirst()) {
-                            info = new ContactInfo();
-                            info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
-                            info.name = phonesCursor.getString(PhoneQuery.NAME);
-                            info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
-                            info.label = phonesCursor.getString(PhoneQuery.LABEL);
-                            info.number = phonesCursor
-                                    .getString(PhoneQuery.MATCHED_NUMBER);
-                            info.normalizedNumber = phonesCursor
-                                    .getString(PhoneQuery.NORMALIZED_NUMBER);
-                            final String thumbnailUriString = phonesCursor.getString(
-                                    PhoneQuery.THUMBNAIL_URI);
-                            info.thumbnailUri = thumbnailUriString == null
-                                    ? null
-                                    : Uri.parse(thumbnailUriString);
-                            info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
-
-                            infoUpdated = true;
-                        }
-                        phonesCursor.close();
-                    }
+                    info = ContactInfo.EMPTY;
                 }
-
-                if (infoUpdated) {
-                    // New incoming phone number invalidates our formatted
-                    // cache. Any cache fills happen only on the GUI thread.
-                    info.formattedNumber = null;
-                    mContactInfoCache.put(number, info);
-                    // Inform list to update this item, if in view
-                    needNotify = true;
-                }
+                dataTableCursor.close();
+            } else {
+                // Failed to fetch the data, ignore this request.
+                info = null;
             }
-            return needNotify;
+            return info;
+        }
+
+        /**
+         * Determines the contact information for the given phone number.
+         * <p>
+         * It returns the contact info if found.
+         * <p>
+         * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+         * <p>
+         * If the lookup fails for some other reason, it returns null.
+         */
+        private ContactInfo queryContactInfoForPhoneNumber(String number) {
+            final ContactInfo info;
+
+            // "number" is a regular phone number, so use the
+            // PhoneLookup table:
+            Cursor phonesCursor =
+                    getActivity().getContentResolver().query(
+                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+                                Uri.encode(number)),
+                                PhoneQuery._PROJECTION, null, null, null);
+            if (phonesCursor != null) {
+                if (phonesCursor.moveToFirst()) {
+                    info = new ContactInfo();
+                    info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
+                    info.name = phonesCursor.getString(PhoneQuery.NAME);
+                    info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
+                    info.label = phonesCursor.getString(PhoneQuery.LABEL);
+                    info.number = phonesCursor
+                            .getString(PhoneQuery.MATCHED_NUMBER);
+                    info.normalizedNumber = phonesCursor
+                            .getString(PhoneQuery.NORMALIZED_NUMBER);
+                    final String thumbnailUriString = phonesCursor.getString(
+                            PhoneQuery.THUMBNAIL_URI);
+                    info.thumbnailUri = thumbnailUriString == null
+                            ? null
+                            : Uri.parse(thumbnailUriString);
+                    info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
+                } else {
+                    info = ContactInfo.EMPTY;
+                }
+                phonesCursor.close();
+            } else {
+                // Failed to fetch the data, ignore this request.
+                info = null;
+            }
+            return info;
+        }
+
+        /**
+         * Queries the appropriate content provider for the contact associated with the number.
+         * <p>
+         * The number might be either a SIP address or a phone number.
+         * <p>
+         * It returns true if it updated the content of the cache and we should therefore tell the
+         * view to update its content.
+         */
+        private boolean queryContactInfo(String number) {
+            final ContactInfo info;
+
+            // Determine the contact info.
+            if (PhoneNumberUtils.isUriNumber(number)) {
+                // This "number" is really a SIP address.
+                info = queryContactInfoForSipAddress(number);
+            } else {
+                info = queryContactInfoForPhoneNumber(number);
+            }
+
+            if (info == null) {
+                // The lookup failed, just return without requesting to update the view.
+                return false;
+            }
+
+            // Check the existing entry in the cache: only if it has changed we should update the
+            // view.
+            ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(number);
+            boolean updated = !info.equals(existingInfo);
+            if (updated) {
+                // The formattedNumber is computed by the UI thread when needed. Since we updated
+                // the details of the contact, set this value to null for now.
+                info.formattedNumber = null;
+            }
+            // Store the data in the cache so that the UI thread can use to display it. Store it
+            // even if it has not changed so that it is marked as not expired.
+            mContactInfoCache.put(number, info);
+            return updated;
         }
 
         /*
@@ -766,7 +848,6 @@
         if (getActivity() == null || getActivity().isFinishing()) {
             return;
         }
-        Log.d(TAG, "updating adapter");
         mAdapter.setLoading(false);
         mAdapter.changeCursor(cursor);
         if (mScrollToTop) {