Merge "Use hardware layer for SearchFragment fade in"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 638c2c4..5b7944d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -61,7 +61,8 @@
         android:icon="@mipmap/ic_launcher_phone"
         android:hardwareAccelerated="true"
         android:supportsRtl="true"
-        android:backupAgent='com.android.dialer.DialerBackupAgent'>
+        android:backupAgent='com.android.dialer.DialerBackupAgent'
+        android:usesCleartextTraffic="false">
 
         <meta-data android:name="com.google.android.backup.api_key"
             android:value="AEdPqrEAAAAIBXgtCEKQ6W0PXVnW-ZVia2KmlV2AxsTw3GjAeQ" />
diff --git a/src/com/android/dialer/DialerApplication.java b/src/com/android/dialer/DialerApplication.java
index 45457c6..7bc3bb4 100644
--- a/src/com/android/dialer/DialerApplication.java
+++ b/src/com/android/dialer/DialerApplication.java
@@ -40,18 +40,4 @@
         Trace.endSection();
         Trace.endSection();
     }
-
-    @Override
-    public Object getSystemService(String name) {
-        if (ContactPhotoManager.CONTACT_PHOTO_SERVICE.equals(name)) {
-            if (mContactPhotoManager == null) {
-                mContactPhotoManager = ContactPhotoManager.createContactPhotoManager(this);
-                registerComponentCallbacks(mContactPhotoManager);
-                mContactPhotoManager.preloadPhotosInBackground();
-            }
-            return mContactPhotoManager;
-        }
-
-        return super.getSystemService(name);
-    }
 }
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 1733068..8ea861f 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -16,16 +16,13 @@
 
 package com.android.dialer.calllog;
 
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.database.sqlite.SQLiteFullException;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Message;
-import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.PhoneLookup;
 import android.telecom.PhoneAccountHandle;
@@ -42,17 +39,16 @@
 import android.widget.TextView;
 
 import com.android.common.widget.GroupingListAdapter;
-import com.android.contacts.common.ContactPhotoManager;
-import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
 import com.android.contacts.common.util.UriUtils;
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.PhoneCallDetailsHelper;
 import com.android.dialer.R;
+import com.android.dialer.contactinfo.ContactInfoRequest;
+import com.android.dialer.contactinfo.NumberWithCountryIso;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.ExpirableCache;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Objects;
 
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -64,8 +60,6 @@
         implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
     private static final String TAG = CallLogAdapter.class.getSimpleName();
 
-    private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10;
-
     /** The enumeration of {@link android.os.AsyncTask} objects used in this class. */
     public enum Tasks {
         REMOVE_CALL_LOG_ENTRIES,
@@ -99,37 +93,6 @@
         public void onReportButtonClick(String number);
     }
 
-    /**
-     * Stores a phone number of a call with the country code where it originally occurred.
-     * <p>
-     * Note the country does not necessarily specifies the country of the phone number itself, but
-     * it is the country in which the user was in when the call was placed or received.
-     */
-    private static final class NumberWithCountryIso {
-        public final String number;
-        public final String countryIso;
-
-        public NumberWithCountryIso(String number, String countryIso) {
-            this.number = number;
-            this.countryIso = countryIso;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (o == null) return false;
-            if (!(o instanceof NumberWithCountryIso)) return false;
-            NumberWithCountryIso other = (NumberWithCountryIso) o;
-            return TextUtils.equals(number, other.number)
-                    && TextUtils.equals(countryIso, other.countryIso);
-        }
-
-        @Override
-        public int hashCode() {
-            return (number == null ? 0 : number.hashCode())
-                    ^ (countryIso == null ? 0 : countryIso.hashCode());
-        }
-    }
-
     /** The time in millis to delay starting the thread processing requests. */
     private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
 
@@ -181,49 +144,6 @@
     private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>();
 
     /**
-     * A request for contact details for the given number.
-     */
-    private static final class ContactInfoRequest {
-        /** The number to look-up. */
-        public final String number;
-        /** The country in which a call to or from this number was placed or received. */
-        public final String countryIso;
-        /** The cached contact information stored in the call log. */
-        public final ContactInfo callLogInfo;
-
-        public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
-            this.number = number;
-            this.countryIso = countryIso;
-            this.callLogInfo = callLogInfo;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) return true;
-            if (obj == null) return false;
-            if (!(obj instanceof ContactInfoRequest)) return false;
-
-            ContactInfoRequest other = (ContactInfoRequest) obj;
-
-            if (!TextUtils.equals(number, other.number)) return false;
-            if (!TextUtils.equals(countryIso, other.countryIso)) return false;
-            if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
-
-            return true;
-        }
-
-        @Override
-        public int hashCode() {
-            final int prime = 31;
-            int result = 1;
-            result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
-            result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
-            result = prime * result + ((number == null) ? 0 : number.hashCode());
-            return result;
-        }
-    }
-
-    /**
      * List of requests to update contact details.
      * <p>
      * Each request is made of a phone number to look up, and the contact info currently stored in
@@ -243,8 +163,6 @@
     /** Instance of helper class for managing views. */
     private final CallLogListItemHelper mCallLogViewsHelper;
 
-    /** Helper to set up contact photos. */
-    private final ContactPhotoManager mContactPhotoManager;
     /** Helper to parse and process phone numbers. */
     private PhoneNumberDisplayHelper mPhoneNumberHelper;
     /** Helper to access Telephony phone number utils class */
@@ -257,11 +175,6 @@
     /** Can be set to true by tests to disable processing of requests. */
     private volatile boolean mRequestProcessingDisabled = false;
 
-    private int mCallLogBackgroundColor;
-    private int mExpandedBackgroundColor;
-    private float mExpandedTranslationZ;
-    private int mPhotoSize;
-
     /** Listener for the primary or secondary actions in the list.
      *  Primary opens the call details.
      *  Secondary calls or plays.
@@ -349,12 +262,7 @@
 
         Resources resources = mContext.getResources();
         CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
-        mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items);
-        mExpandedBackgroundColor = resources.getColor(R.color.call_log_expanded_background_color);
-        mExpandedTranslationZ = resources.getDimension(R.dimen.call_log_expanded_translation_z);
-        mPhotoSize = resources.getDimensionPixelSize(R.dimen.contact_photo_size);
 
-        mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
         mPhoneNumberHelper = new PhoneNumberDisplayHelper(mContext, resources);
         mPhoneNumberUtilsWrapper = new PhoneNumberUtilsWrapper(mContext);
         PhoneCallDetailsHelper phoneCallDetailsHelper =
@@ -494,9 +402,10 @@
         // 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(numberCountryIso, info);
+
         // Update the call log even if the cache it is up-to-date: it is possible that the cache
         // contains the value from a different call log entry.
-        updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
+        mContactInfoHelper.updateCallLogContactInfo(number, countryIso, info, callLogInfo);
         return updated;
     }
 
@@ -656,7 +565,7 @@
         // Stash away the Ids of the calls so that we can support deleting a row in the call log.
         views.callIds = getCallIds(c, count);
 
-        final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
+        final ContactInfo cachedContactInfo = mContactInfoHelper.getContactInfo(c);
 
         final boolean isVoicemailNumber =
                 mPhoneNumberUtilsWrapper.isVoicemailNumber(accountHandle, number);
@@ -733,7 +642,12 @@
 
         // Restore expansion state of the row on rebind.  Inflate the actions ViewStub if required,
         // and set its visibility state accordingly.
-        expandOrCollapseActions(callLogItemView, isExpanded(rowId));
+        views.expandOrCollapseActions(
+                isExpanded(rowId),
+                mOnReportButtonClickListener,
+                mActionListener,
+                mPhoneNumberUtilsWrapper,
+                mCallLogViewsHelper);
 
         if (TextUtils.isEmpty(name)) {
             details = new PhoneCallDetails(number, numberPresentation, formattedNumber, countryIso,
@@ -747,17 +661,6 @@
 
         mCallLogViewsHelper.setPhoneCallDetails(mContext, views, details);
 
-        int contactType = ContactPhotoManager.TYPE_DEFAULT;
-
-        if (isVoicemailNumber) {
-            contactType = ContactPhotoManager.TYPE_VOICEMAIL;
-        } else if (mContactInfoHelper.isBusiness(info.sourceType)) {
-            contactType = ContactPhotoManager.TYPE_BUSINESS;
-        }
-
-        String lookupKey = lookupUri == null ? null
-                : ContactInfoHelper.getLookupKeyFromUri(lookupUri);
-
         String nameForDefaultImage = null;
         if (TextUtils.isEmpty(name)) {
             nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(details.accountHandle,
@@ -766,11 +669,8 @@
             nameForDefaultImage = name;
         }
 
-        if (photoId == 0 && photoUri != null) {
-            setPhoto(views, photoUri, lookupUri, nameForDefaultImage, lookupKey, contactType);
-        } else {
-            setPhoto(views, photoId, lookupUri, nameForDefaultImage, lookupKey, contactType);
-        }
+        views.setPhoto(photoId, photoUri, lookupUri, nameForDefaultImage, isVoicemailNumber,
+                mContactInfoHelper.isBusiness(info.sourceType));
         views.quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
 
         // Listen for the first draw
@@ -812,6 +712,7 @@
         }
         return CallLogGroupBuilder.DAY_GROUP_NONE;
     }
+
     /**
      * Determines if a call log row with the given Id is expanded.
      * @param rowId The row Id of the call.
@@ -845,54 +746,6 @@
         }
     }
 
-    /**
-     * Expands or collapses the view containing the CALLBACK/REDIAL, VOICEMAIL and DETAILS action
-     * buttons.
-     *
-     * @param callLogItem The call log entry parent view.
-     * @param isExpanded The new expansion state of the view.
-     */
-    private void expandOrCollapseActions(View callLogItem, boolean isExpanded) {
-        final CallLogListItemViews views = (CallLogListItemViews) callLogItem.getTag();
-
-        expandVoicemailTranscriptionView(views, isExpanded);
-        if (isExpanded) {
-            // Inflate the view stub if necessary, and wire up the event handlers.
-            views.inflateActionViewStub(callLogItem, mOnReportButtonClickListener, mActionListener,
-                    mPhoneNumberUtilsWrapper, mCallLogViewsHelper);
-
-            views.actionsView.setVisibility(View.VISIBLE);
-            views.actionsView.setAlpha(1.0f);
-            views.callLogEntryView.setBackgroundColor(mExpandedBackgroundColor);
-            views.callLogEntryView.setTranslationZ(mExpandedTranslationZ);
-            callLogItem.setTranslationZ(mExpandedTranslationZ); // WAR
-        } else {
-            // When recycling a view, it is possible the actionsView ViewStub was previously
-            // inflated so we should hide it in this case.
-            if (views.actionsView != null) {
-                views.actionsView.setVisibility(View.GONE);
-            }
-
-            views.callLogEntryView.setBackgroundColor(mCallLogBackgroundColor);
-            views.callLogEntryView.setTranslationZ(0);
-            callLogItem.setTranslationZ(0); // WAR
-        }
-    }
-
-    public static void expandVoicemailTranscriptionView(CallLogListItemViews views,
-            boolean isExpanded) {
-        if (views.callType != Calls.VOICEMAIL_TYPE) {
-            return;
-        }
-
-        final TextView view = views.phoneCallDetailsViews.voicemailTranscriptionView;
-        if (TextUtils.isEmpty(view.getText())) {
-            return;
-        }
-        view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1);
-        view.setSingleLine(!isExpanded);
-    }
-
     /** Checks whether the contact info from the call log matches the one from the contacts db. */
     private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
         // The call log only contains a subset of the fields in the contacts db.
@@ -902,105 +755,6 @@
                 && TextUtils.equals(callLogInfo.label, info.label);
     }
 
-    /** Stores the updated contact info in the call log if it is different from the current one. */
-    private void updateCallLogContactInfoCache(String number, String countryIso,
-            ContactInfo updatedInfo, ContactInfo callLogInfo) {
-        final ContentValues values = new ContentValues();
-        boolean needsUpdate = false;
-
-        if (callLogInfo != null) {
-            if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
-                values.put(Calls.CACHED_NAME, updatedInfo.name);
-                needsUpdate = true;
-            }
-
-            if (updatedInfo.type != callLogInfo.type) {
-                values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
-                needsUpdate = true;
-            }
-
-            if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
-                values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
-                needsUpdate = true;
-            }
-            if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
-                values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
-                needsUpdate = true;
-            }
-            // Only replace the normalized number if the new updated normalized number isn't empty.
-            if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) &&
-                    !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
-                values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
-                needsUpdate = true;
-            }
-            if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
-                values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
-                needsUpdate = true;
-            }
-            if (updatedInfo.photoId != callLogInfo.photoId) {
-                values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
-                needsUpdate = true;
-            }
-            final Uri updatedPhotoUriContactsOnly =
-                    UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
-            if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
-                values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(
-                        updatedPhotoUriContactsOnly));
-                needsUpdate = true;
-            }
-            if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
-                values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
-                needsUpdate = true;
-            }
-        } else {
-            // No previous values, store all of them.
-            values.put(Calls.CACHED_NAME, updatedInfo.name);
-            values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
-            values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
-            values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
-            values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
-            values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
-            values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
-            values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(
-                    UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
-            values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
-            needsUpdate = true;
-        }
-
-        if (!needsUpdate) return;
-
-        try {
-            if (countryIso == null) {
-                mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
-                        Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
-                        new String[]{ number });
-            } else {
-                mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
-                        Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
-                        new String[]{ number, countryIso });
-            }
-        } catch (SQLiteFullException e) {
-            Log.e(TAG, "Unable to update contact info in call log db", e);
-        }
-    }
-
-    /** Returns the contact information as stored in the call log. */
-    private ContactInfo getContactInfoFromCallLog(Cursor c) {
-        ContactInfo info = new ContactInfo();
-        info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
-        info.name = c.getString(CallLogQuery.CACHED_NAME);
-        info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
-        info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
-        String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
-        info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
-        info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
-        info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
-        info.photoUri = UriUtils.nullForNonContactsUri(
-                UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)));
-        info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
-        return info;
-    }
-
     /**
      * Returns the call types for the given number of items in the cursor.
      * <p>
@@ -1038,26 +792,6 @@
         return features;
     }
 
-    private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri,
-            String displayName, String identifier, int contactType) {
-        views.quickContactView.assignContactUri(contactUri);
-        views.quickContactView.setOverlay(null);
-        DefaultImageRequest request = new DefaultImageRequest(displayName, identifier,
-                contactType, true /* isCircular */);
-        mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */,
-                true /* isCircular */, request);
-    }
-
-    private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri,
-            String displayName, String identifier, int contactType) {
-        views.quickContactView.assignContactUri(contactUri);
-        views.quickContactView.setOverlay(null);
-        DefaultImageRequest request = new DefaultImageRequest(displayName, identifier,
-                contactType, true /* isCircular */);
-        mContactPhotoManager.loadPhoto(views.quickContactView, photoUri, mPhotoSize,
-                false /* darkTheme */, true /* isCircular */, request);
-    }
-
     /**
      * Bind a call log entry view for testing purposes.  Also inflates the action view stub so
      * unit tests can access the buttons contained within.
@@ -1070,7 +804,7 @@
     void bindViewForTest(View view, Context context, Cursor cursor) {
         bindStandAloneView(view, context, cursor);
         CallLogListItemViews views = CallLogListItemViews.fromView(context, view);
-        views.inflateActionViewStub(view, mOnReportButtonClickListener, mActionListener,
+        views.inflateActionViewStub(mOnReportButtonClickListener, mActionListener,
                 mPhoneNumberUtilsWrapper, mCallLogViewsHelper);
     }
 
@@ -1172,7 +906,12 @@
         boolean expanded = toggleExpansion(views.rowId);
 
         // Trigger loading of the viewstub and visual expand or collapse.
-        expandOrCollapseActions(view, expanded);
+        views.expandOrCollapseActions(
+                expanded,
+                mOnReportButtonClickListener,
+                mActionListener,
+                mPhoneNumberUtilsWrapper,
+                mCallLogViewsHelper);
 
         // Animate the expansion or collapse.
         if (mCallItemExpandedListener != null) {
@@ -1182,11 +921,15 @@
 
             // Animate the collapse of the previous item if it is still visible on screen.
             if (mPreviouslyExpanded != NONE_EXPANDED) {
-                View previousItem = mCallItemExpandedListener.getViewForCallId(
-                        mPreviouslyExpanded);
+                View previousItem = mCallItemExpandedListener.getViewForCallId(mPreviouslyExpanded);
 
                 if (previousItem != null) {
-                    expandOrCollapseActions(previousItem, false);
+                    ((CallLogListItemViews) previousItem.getTag()).expandOrCollapseActions(
+                            false /* isExpanded */,
+                            mOnReportButtonClickListener,
+                            mActionListener,
+                            mPhoneNumberUtilsWrapper,
+                            mCallLogViewsHelper);
                     if (animate) {
                         mCallItemExpandedListener.onItemExpanded(previousItem);
                     }
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index c4e453c..7b5907c 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -556,7 +556,7 @@
                 if (!isExpand) {
                     viewHolder.actionsView.setVisibility(View.VISIBLE);
                 }
-                CallLogAdapter.expandVoicemailTranscriptionView(viewHolder, !isExpand);
+                viewHolder.expandVoicemailTranscriptionView(!isExpand);
 
                 // Set up the fade effect for the action buttons.
                 if (isExpand) {
@@ -625,7 +625,7 @@
                             // is defaulting to the value (0) at the start of the expand animation.
                             viewHolder.actionsView.setAlpha(1);
                         }
-                        CallLogAdapter.expandVoicemailTranscriptionView(viewHolder, isExpand);
+                        viewHolder.expandVoicemailTranscriptionView(isExpand);
                     }
                 });
 
diff --git a/src/com/android/dialer/calllog/CallLogListItemViews.java b/src/com/android/dialer/calllog/CallLogListItemViews.java
index b9a76a8..9d11a3a 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViews.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViews.java
@@ -17,8 +17,11 @@
 package com.android.dialer.calllog;
 
 import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
 import android.provider.CallLog.Calls;
 import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
@@ -26,6 +29,8 @@
 import android.widget.TextView;
 
 import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
 import com.android.contacts.common.testing.NeededForTesting;
 import com.android.dialer.PhoneCallDetailsViews;
 import com.android.dialer.R;
@@ -40,6 +45,8 @@
  * if the call log list item is eventually represented as a UI component.
  */
 public final class CallLogListItemViews {
+    /** The root view of the call log list item */
+    public final View rootView;
     /** The quick contact badge for the contact. */
     public final QuickContactBadge quickContactView;
     /** The primary action view of the entry. */
@@ -123,10 +130,18 @@
      */
     public boolean canBeReportedAsInvalid;
 
+    private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10;
+
     private Context mContext;
+    private int mPhotoSize;
+
+    private int mCallLogBackgroundColor;
+    private int mExpandedBackgroundColor;
+    private float mExpandedTranslationZ;
 
     private CallLogListItemViews(
             Context context,
+            View rootView,
             QuickContactBadge quickContactView,
             View primaryActionView,
             PhoneCallDetailsViews phoneCallDetailsViews,
@@ -134,11 +149,29 @@
             TextView dayGroupHeader) {
         mContext = context;
 
+        this.rootView = rootView;
         this.quickContactView = quickContactView;
         this.primaryActionView = primaryActionView;
         this.phoneCallDetailsViews = phoneCallDetailsViews;
         this.callLogEntryView = callLogEntryView;
         this.dayGroupHeader = dayGroupHeader;
+
+        Resources resources = mContext.getResources();
+        mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items);
+        mExpandedBackgroundColor = resources.getColor(R.color.call_log_expanded_background_color);
+        mExpandedTranslationZ = resources.getDimension(R.dimen.call_log_expanded_translation_z);
+        mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
+    }
+
+    public static CallLogListItemViews fromView(Context context, View view) {
+        return new CallLogListItemViews(
+                context,
+                view,
+                (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
+                view.findViewById(R.id.primary_action_view),
+                PhoneCallDetailsViews.fromView(view),
+                view.findViewById(R.id.call_log_row),
+                (TextView) view.findViewById(R.id.call_log_day_group_label));
     }
 
     /**
@@ -149,12 +182,11 @@
      * @param callLogItem The call log list item view.
      */
     public void inflateActionViewStub(
-            final View callLogItem,
             final CallLogAdapter.OnReportButtonClickListener onReportButtonClickListener,
             View.OnClickListener actionListener,
             PhoneNumberUtilsWrapper phoneNumberUtilsWrapper,
             CallLogListItemHelper callLogViewsHelper) {
-        ViewStub stub = (ViewStub) callLogItem.findViewById(R.id.call_log_entry_actions_stub);
+        ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub);
         if (stub != null) {
             actionsView = (ViewGroup) stub.inflate();
         }
@@ -266,20 +298,93 @@
         callLogViewsHelper.setActionContentDescriptions(this);
     }
 
-    public static CallLogListItemViews fromView(Context context, View view) {
-        return new CallLogListItemViews(
-                context,
-                (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
-                view.findViewById(R.id.primary_action_view),
-                PhoneCallDetailsViews.fromView(view),
-                view.findViewById(R.id.call_log_row),
-                (TextView) view.findViewById(R.id.call_log_day_group_label));
+    /**
+     * Expands or collapses the view containing the CALLBACK/REDIAL, VOICEMAIL and DETAILS action
+     * buttons.
+     *
+     * TODO: Reduce number of classes which need to be passed in to inflate the action view stub.
+     *     1) Instantiate them in this class, and store local references.
+     *     2) Set them on the CallLogListItemHelper and use it for inflation.
+     *     3) Implement a parent view for a call log list item, and store references in that class.
+     */
+    public void expandOrCollapseActions(
+            boolean isExpanded,
+            final CallLogAdapter.OnReportButtonClickListener onReportButtonClickListener,
+            View.OnClickListener actionListener,
+            PhoneNumberUtilsWrapper phoneNumberUtilsWrapper,
+            CallLogListItemHelper callLogViewsHelper) {
+        expandVoicemailTranscriptionView(isExpanded);
+
+        if (isExpanded) {
+            // Inflate the view stub if necessary, and wire up the event handlers.
+            inflateActionViewStub(onReportButtonClickListener, actionListener,
+                    phoneNumberUtilsWrapper, callLogViewsHelper);
+
+            actionsView.setVisibility(View.VISIBLE);
+            actionsView.setAlpha(1.0f);
+            callLogEntryView.setBackgroundColor(mExpandedBackgroundColor);
+            callLogEntryView.setTranslationZ(mExpandedTranslationZ);
+            rootView.setTranslationZ(mExpandedTranslationZ); // WAR
+        } else {
+            // When recycling a view, it is possible the actionsView ViewStub was previously
+            // inflated so we should hide it in this case.
+            if (actionsView != null) {
+                actionsView.setVisibility(View.GONE);
+            }
+
+            callLogEntryView.setBackgroundColor(mCallLogBackgroundColor);
+            callLogEntryView.setTranslationZ(0);
+            rootView.setTranslationZ(0); // WAR
+        }
+    }
+
+    public void expandVoicemailTranscriptionView(boolean isExpanded) {
+        if (callType != Calls.VOICEMAIL_TYPE) {
+            return;
+        }
+
+        final TextView view = phoneCallDetailsViews.voicemailTranscriptionView;
+        if (TextUtils.isEmpty(view.getText())) {
+            return;
+        }
+        view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1);
+        view.setSingleLine(!isExpanded);
+    }
+
+    public void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName,
+            boolean isVoicemail, boolean isBusiness) {
+        quickContactView.assignContactUri(contactUri);
+        quickContactView.setOverlay(null);
+
+        int contactType = ContactPhotoManager.TYPE_DEFAULT;
+        if (isVoicemail) {
+            contactType = ContactPhotoManager.TYPE_VOICEMAIL;
+        } else if (isBusiness) {
+            contactType = ContactPhotoManager.TYPE_BUSINESS;
+        }
+
+        String lookupKey = null;
+        if (contactUri != null) {
+            lookupKey = ContactInfoHelper.getLookupKeyFromUri(contactUri);
+        }
+
+        DefaultImageRequest request = new DefaultImageRequest(
+                displayName, lookupKey, contactType, true /* isCircular */);
+
+        if (photoId == 0 && photoUri != null) {
+            ContactPhotoManager.getInstance(mContext).loadPhoto(quickContactView, photoUri,
+                    mPhotoSize, false /* darkTheme */, true /* isCircular */, request);
+        } else {
+            ContactPhotoManager.getInstance(mContext).loadThumbnail(quickContactView, photoId,
+                    false /* darkTheme */, true /* isCircular */, request);
+        }
     }
 
     @NeededForTesting
     public static CallLogListItemViews createForTest(Context context) {
         CallLogListItemViews views = new CallLogListItemViews(
                 context,
+                new View(context),
                 new QuickContactBadge(context),
                 new View(context),
                 PhoneCallDetailsViews.createForTest(context),
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
index da03b07..8e8aa3c 100644
--- a/src/com/android/dialer/calllog/ContactInfoHelper.java
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -14,9 +14,12 @@
 
 package com.android.dialer.calllog;
 
+import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteFullException;
 import android.net.Uri;
+import android.provider.CallLog.Calls;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Contacts;
@@ -24,6 +27,7 @@
 import android.provider.ContactsContract.PhoneLookup;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.contacts.common.util.Constants;
 import com.android.contacts.common.util.PhoneNumberHelper;
@@ -41,6 +45,8 @@
  * Utility class to look up the contact information for a given number.
  */
 public class ContactInfoHelper {
+    private static final String TAG = ContactInfoHelper.class.getSimpleName();
+
     private final Context mContext;
     private final String mCurrentCountryIso;
 
@@ -278,6 +284,107 @@
     }
 
     /**
+     * Stores differences between the updated contact info and the current call log contact info.
+     *
+     * @param number The number of the contact.
+     * @param countryIso The country associated with this number.
+     * @param updatedInfo The updated contact info.
+     * @param callLogInfo The call log entry's current contact info.
+     */
+    public void updateCallLogContactInfo(String number, String countryIso, ContactInfo updatedInfo,
+            ContactInfo callLogInfo) {
+        final ContentValues values = new ContentValues();
+        boolean needsUpdate = false;
+
+        if (callLogInfo != null) {
+            if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
+                values.put(Calls.CACHED_NAME, updatedInfo.name);
+                needsUpdate = true;
+            }
+
+            if (updatedInfo.type != callLogInfo.type) {
+                values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+                needsUpdate = true;
+            }
+
+            if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
+                values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+                needsUpdate = true;
+            }
+
+            if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
+                values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+                needsUpdate = true;
+            }
+
+            // Only replace the normalized number if the new updated normalized number isn't empty.
+            if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) &&
+                    !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
+                values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+                needsUpdate = true;
+            }
+
+            if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
+                values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+                needsUpdate = true;
+            }
+
+            if (updatedInfo.photoId != callLogInfo.photoId) {
+                values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+                needsUpdate = true;
+            }
+
+            final Uri updatedPhotoUriContactsOnly =
+                    UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
+            if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
+                values.put(Calls.CACHED_PHOTO_URI,
+                        UriUtils.uriToString(updatedPhotoUriContactsOnly));
+                needsUpdate = true;
+            }
+
+            if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
+                values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+                needsUpdate = true;
+            }
+        } else {
+            // No previous values, store all of them.
+            values.put(Calls.CACHED_NAME, updatedInfo.name);
+            values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+            values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+            values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+            values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+            values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+            values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+            values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(
+                    UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
+            values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+            needsUpdate = true;
+        }
+
+        if (!needsUpdate) {
+            return;
+        }
+
+        try {
+            if (countryIso == null) {
+                mContext.getContentResolver().update(
+                        Calls.CONTENT_URI_WITH_VOICEMAIL,
+                        values,
+                        Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
+                        new String[]{ number });
+            } else {
+                mContext.getContentResolver().update(
+                        Calls.CONTENT_URI_WITH_VOICEMAIL,
+                        values,
+                        Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
+                        new String[]{ number, countryIso });
+            }
+        } catch (SQLiteFullException e) {
+            Log.e(TAG, "Unable to update contact info in call log db", e);
+        }
+    }
+
+    /**
      * Parses the given URI to determine the original lookup key of the contact.
      */
     public static String getLookupKeyFromUri(Uri lookupUri) {
@@ -296,6 +403,29 @@
     }
 
     /**
+     * Returns the contact information stored in an entry of the call log.
+     *
+     * @param c A cursor pointing to an entry in the call log.
+     */
+    public static ContactInfo getContactInfo(Cursor c) {
+        ContactInfo info = new ContactInfo();
+
+        info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
+        info.name = c.getString(CallLogQuery.CACHED_NAME);
+        info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+        info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+        String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
+        info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
+        info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
+        info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
+        info.photoUri = UriUtils.nullForNonContactsUri(
+                UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)));
+        info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
+
+        return info;
+    }
+
+    /**
      * Given a contact's sourceType, return true if the contact is a business
      *
      * @param sourceType sourceType of the contact. This is usually populated by
diff --git a/src/com/android/dialer/contactinfo/ContactInfoRequest.java b/src/com/android/dialer/contactinfo/ContactInfoRequest.java
new file mode 100644
index 0000000..ec5c119
--- /dev/null
+++ b/src/com/android/dialer/contactinfo/ContactInfoRequest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.contactinfo;
+
+import android.text.TextUtils;
+
+import com.android.dialer.calllog.ContactInfo;
+import com.google.common.base.Objects;
+
+/**
+ * A request for contact details for the given number, used by the ContactInfoCache.
+ */
+public final class ContactInfoRequest {
+    /** The number to look-up. */
+    public final String number;
+    /** The country in which a call to or from this number was placed or received. */
+    public final String countryIso;
+    /** The cached contact information stored in the call log. */
+    public final ContactInfo callLogInfo;
+
+    public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
+        this.number = number;
+        this.countryIso = countryIso;
+        this.callLogInfo = callLogInfo;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (!(obj instanceof ContactInfoRequest)) return false;
+
+        ContactInfoRequest other = (ContactInfoRequest) obj;
+
+        if (!TextUtils.equals(number, other.number)) return false;
+        if (!TextUtils.equals(countryIso, other.countryIso)) return false;
+        if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
+        result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
+        result = prime * result + ((number == null) ? 0 : number.hashCode());
+        return result;
+    }
+}
diff --git a/src/com/android/dialer/contactinfo/NumberWithCountryIso.java b/src/com/android/dialer/contactinfo/NumberWithCountryIso.java
new file mode 100644
index 0000000..1383fb7
--- /dev/null
+++ b/src/com/android/dialer/contactinfo/NumberWithCountryIso.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.contactinfo;
+
+import android.text.TextUtils;
+
+/**
+ * Stores a phone number of a call with the country code where it originally occurred. This object
+ * is used as a key in the {@code ContactInfoCache}.
+ *
+ * The country does not necessarily specify the country of the phone number itself, but rather
+ * it is the country in which the user was in when the call was placed or received.
+ */
+public final class NumberWithCountryIso {
+    public final String number;
+    public final String countryIso;
+
+    public NumberWithCountryIso(String number, String countryIso) {
+        this.number = number;
+        this.countryIso = countryIso;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) return false;
+        if (!(o instanceof NumberWithCountryIso)) return false;
+        NumberWithCountryIso other = (NumberWithCountryIso) o;
+        return TextUtils.equals(number, other.number)
+                && TextUtils.equals(countryIso, other.countryIso);
+    }
+
+    @Override
+    public int hashCode() {
+        int numberHashCode = number == null ? 0 : number.hashCode();
+        int countryHashCode = countryIso == null ? 0 : countryIso.hashCode();
+
+        return numberHashCode ^ countryHashCode;
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
index d440f6a..30c6286 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -10,7 +10,11 @@
 # Include all test java files.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_STATIC_JAVA_LIBRARIES += com.android.contacts.common.test
+src_dirs := src \
+    ../../ContactsCommon/TestCommon/src
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
 
 LOCAL_PACKAGE_NAME := DialerTests