Merge "Replace ListView with RecyclerView in call log."
diff --git a/res/layout/call_log_fragment.xml b/res/layout/call_log_fragment.xml
index 74c6309..c126b77 100644
--- a/res/layout/call_log_fragment.xml
+++ b/res/layout/call_log_fragment.xml
@@ -61,18 +61,11 @@
     <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent">
-        <!-- clipChildren=false is required to ensure shadows drawn
-            within list items aren't clipped by the list item bounds. -->
-        <ListView android:id="@android:id/list"
+
+        <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:fadingEdge="none"
-            android:scrollbarStyle="outsideOverlay"
-            android:background="@color/background_dialer_list_items"
-            android:divider="@null"
-            android:nestedScrollingEnabled="true"
-            android:clipChildren="false"
-        />
+            android:background="@color/background_dialer_list_items" />
 
         <include
             android:id="@+id/empty_list_view"
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 3e8efa0..07cd215 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -23,6 +23,7 @@
 import android.net.Uri;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.PhoneLookup;
+import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.telecom.PhoneAccountHandle;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
@@ -225,13 +226,12 @@
         mLoading = loading;
     }
 
-    @Override
     public boolean isEmpty() {
         if (mLoading) {
             // We don't want the empty state to show when loading.
             return false;
         } else {
-            return super.isEmpty();
+            return getItemCount() == 0;
         }
     }
 
@@ -262,48 +262,19 @@
     }
 
     @Override
-    protected View newStandAloneView(Context context, ViewGroup parent) {
-        return newChildView(context, parent);
-    }
-
-    @Override
-    protected View newGroupView(Context context, ViewGroup parent) {
-        return newChildView(context, parent);
-    }
-
-    @Override
-    protected View newChildView(Context context, ViewGroup parent) {
-        LayoutInflater inflater = LayoutInflater.from(context);
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        LayoutInflater inflater = LayoutInflater.from(mContext);
         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
 
         // Get the views to bind to and cache them.
-        CallLogListItemViews views = CallLogListItemViews.fromView(context, view);
+        CallLogListItemViews views = CallLogListItemViews.fromView(mContext, view);
         view.setTag(views);
 
         // Set text height to false on the TextViews so they don't have extra padding.
         views.phoneCallDetailsViews.nameView.setElegantTextHeight(false);
         views.phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false);
 
-        return view;
-    }
-
-    @Override
-    protected void bindStandAloneView(View view, Context context, Cursor cursor) {
-        bindView(view, cursor, 1);
-    }
-
-    @Override
-    protected void bindChildView(View view, Context context, Cursor cursor) {
-        bindView(view, cursor, 1);
-    }
-
-    @Override
-    protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
-            boolean expanded) {
-        bindView(view, cursor, groupSize);
-    }
-
-    private void findAndCacheViews(View view) {
+        return (CallLogListItemViews) view.getTag();
     }
 
     /**
@@ -312,12 +283,17 @@
      * should not. It invokes cross-process methods and the repeat execution can get costly.
      *
      * @param callLogItemView the view corresponding to this entry
-     * @param c the cursor pointing to the entry in the call log
      * @param count the number of entries in the current item, greater than 1 if it is a group
      */
-    public void bindView(View callLogItemView, Cursor c, int count) {
-        callLogItemView.setAccessibilityDelegate(mAccessibilityDelegate);
-        final CallLogListItemViews views = (CallLogListItemViews) callLogItemView.getTag();
+    public void onBindViewHolder(ViewHolder viewHolder, int position) {
+        Cursor c = (Cursor) getItem(position);
+        if (c == null) {
+            return;
+        }
+        int count = getGroupSize(position);
+
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
+        views.rootView.setAccessibilityDelegate(mAccessibilityDelegate);
 
         // Default case: an item in the call log.
         views.primaryActionView.setVisibility(View.VISIBLE);
@@ -435,7 +411,7 @@
 
         // Listen for the first draw
         if (mViewTreeObserver == null) {
-            mViewTreeObserver = callLogItemView.getViewTreeObserver();
+            mViewTreeObserver = views.rootView.getViewTreeObserver();
             mViewTreeObserver.addOnPreDrawListener(this);
         }
     }
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index d69c2ed..4f4fc1b 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -21,8 +21,8 @@
 import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.app.DialogFragment;
+import android.app.Fragment;
 import android.app.KeyguardManager;
-import android.app.ListFragment;
 import android.content.Context;
 import android.content.Intent;
 import android.database.ContentObserver;
@@ -34,10 +34,11 @@
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract;
 import android.provider.VoicemailContract.Status;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.LinearLayoutManager;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.ListView;
@@ -60,7 +61,7 @@
  * Displays a list of call log entries. To filter for a particular kind of call
  * (all, missed or voicemails), specify it in the constructor.
  */
-public class CallLogFragment extends ListFragment
+public class CallLogFragment extends Fragment
         implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener,
         CallLogAdapter.CallFetcher {
     private static final String TAG = "CallLogFragment";
@@ -76,6 +77,8 @@
     private static final String KEY_LOG_LIMIT = "log_limit";
     private static final String KEY_DATE_LIMIT = "date_limit";
 
+    private RecyclerView mRecyclerView;
+    private LinearLayoutManager mLayoutManager;
     private CallLogAdapter mAdapter;
     private CallLogQueryHandler mCallLogQueryHandler;
     private boolean mScrollToTop;
@@ -172,9 +175,6 @@
         }
 
         String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
-        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
-                new ContactInfoHelper(getActivity(), currentCountryIso), this);
-        setListAdapter(mAdapter);
         mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
                 this, mLogLimit);
         mKeyguardManager =
@@ -201,9 +201,8 @@
         // This will update the state of the "Clear call log" menu item.
         getActivity().invalidateOptionsMenu();
 
-        final ListView listView = getListView();
         boolean showListView = cursor.getCount() > 0;
-        listView.setVisibility(showListView ? View.VISIBLE : View.GONE);
+        mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
         mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
 
         if (mScrollToTop) {
@@ -213,8 +212,9 @@
             // will not experience the illusion of downward motion.  Instead,
             // if we're not already near the top of the list, we instantly jump
             // near the top, and animate from there.
-            if (listView.getFirstVisiblePosition() > 5) {
-                listView.setSelection(5);
+            if (mLayoutManager.findFirstVisibleItemPosition() > 5) {
+                // TODO: Jump to near the top, then begin smooth scroll.
+                mRecyclerView.smoothScrollToPosition(0);
             }
             // Workaround for framework issue: the smooth-scroll doesn't
             // occur if setSelection() is called immediately before.
@@ -224,7 +224,7 @@
                    if (getActivity() == null || getActivity().isFinishing()) {
                        return;
                    }
-                   listView.smoothScrollToPosition(0);
+                   mRecyclerView.smoothScrollToPosition(0);
                }
             });
 
@@ -269,6 +269,17 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
         View view = inflater.inflate(R.layout.call_log_fragment, container, false);
+
+        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
+        mRecyclerView.setHasFixedSize(true);
+        mLayoutManager = new LinearLayoutManager(getActivity());
+        mRecyclerView.setLayoutManager(mLayoutManager);
+
+        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
+        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
+                new ContactInfoHelper(getActivity(), currentCountryIso), this);
+        mRecyclerView.setAdapter(mAdapter);
+
         mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
         mStatusMessageView = view.findViewById(R.id.voicemail_status);
         mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
@@ -280,7 +291,6 @@
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
         mEmptyListView = view.findViewById(R.id.empty_list_view);
-        getListView().setItemsCanFocus(true);
 
         updateEmptyMessage(mCallTypeFilter);
     }
diff --git a/src/com/android/dialer/calllog/CallLogListItemViews.java b/src/com/android/dialer/calllog/CallLogListItemViews.java
index 9d11a3a..f2bed53 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViews.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViews.java
@@ -20,6 +20,7 @@
 import android.content.res.Resources;
 import android.net.Uri;
 import android.provider.CallLog.Calls;
+import android.support.v7.widget.RecyclerView;
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
 import android.view.View;
@@ -44,7 +45,7 @@
  * is a way of isolating view logic from the CallLogAdapter. We should consider moving that logic
  * if the call log list item is eventually represented as a UI component.
  */
-public final class CallLogListItemViews {
+public final class CallLogListItemViews extends RecyclerView.ViewHolder {
     /** The root view of the call log list item */
     public final View rootView;
     /** The quick contact badge for the contact. */
@@ -147,6 +148,7 @@
             PhoneCallDetailsViews phoneCallDetailsViews,
             View callLogEntryView,
             TextView dayGroupHeader) {
+        super(rootView);
         mContext = context;
 
         this.rootView = rootView;
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
index 7895549..501e88d 100644
--- a/src/com/android/dialer/calllog/GroupingListAdapter.java
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -21,6 +21,8 @@
 import android.database.Cursor;
 import android.database.DataSetObserver;
 import android.os.Handler;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
 import android.util.SparseIntArray;
 import android.view.View;
 import android.view.ViewGroup;
@@ -34,7 +36,7 @@
  * The list has three types of elements: stand-alone, group header and group child. Groups are
  * collapsible and collapsed by default. This is used by the call log to group related entries.
  */
-abstract class GroupingListAdapter extends BaseAdapter {
+abstract class GroupingListAdapter extends RecyclerView.Adapter {
 
     private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
     private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
@@ -109,11 +111,6 @@
         public void onChanged() {
             notifyDataSetChanged();
         }
-
-        @Override
-        public void onInvalidated() {
-            notifyDataSetInvalidated();
-        }
     };
 
     public GroupingListAdapter(Context context) {
@@ -127,15 +124,7 @@
      */
     protected abstract void addGroups(Cursor cursor);
 
-    protected abstract View newStandAloneView(Context context, ViewGroup parent);
-    protected abstract void bindStandAloneView(View view, Context context, Cursor cursor);
-
-    protected abstract View newGroupView(Context context, ViewGroup parent);
-    protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
-            boolean expanded);
-
-    protected abstract View newChildView(Context context, ViewGroup parent);
-    protected abstract void bindChildView(View view, Context context, Cursor cursor);
+    protected abstract void onContentChanged();
 
     /**
      * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
@@ -149,9 +138,6 @@
         mPositionCache.clear();
     }
 
-    protected void onContentChanged() {
-    }
-
     public void changeCursor(Cursor cursor) {
         if (cursor == mCursor) {
             return;
@@ -171,13 +157,10 @@
             cursor.registerDataSetObserver(mDataSetObserver);
             mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
             notifyDataSetChanged();
-        } else {
-            // notify the observers about the lack of a data set
-            notifyDataSetInvalidated();
         }
-
     }
 
+    @NeededForTesting
     public Cursor getCursor() {
         return mCursor;
     }
@@ -231,7 +214,8 @@
         return need;
     }
 
-    public int getCount() {
+    @Override
+    public int getItemCount() {
         if (mCursor == null) {
             return 0;
         }
@@ -343,6 +327,7 @@
             if (position < listPosition) {
                 metadata.itemType = ITEM_TYPE_STANDALONE;
                 metadata.cursorPosition = cursorPosition - (listPosition - position);
+                metadata.childCount = 1;
                 return;
             }
 
@@ -382,6 +367,7 @@
         // The required item is past the last group
         metadata.itemType = ITEM_TYPE_STANDALONE;
         metadata.cursorPosition = cursorPosition + (position - listPosition);
+        metadata.childCount = 1;
     }
 
     /**
@@ -421,12 +407,6 @@
         notifyDataSetChanged();
     }
 
-    @Override
-    public int getViewTypeCount() {
-        return 3;
-    }
-
-    @Override
     public int getItemViewType(int position) {
         obtainPositionMetadata(mPositionMetadata, position);
         return mPositionMetadata.itemType;
@@ -454,37 +434,16 @@
         }
     }
 
-    public View getView(int position, View convertView, ViewGroup parent) {
-        obtainPositionMetadata(mPositionMetadata, position);
-        View view = convertView;
-        if (view == null) {
-            switch (mPositionMetadata.itemType) {
-                case ITEM_TYPE_STANDALONE:
-                    view = newStandAloneView(mContext, parent);
-                    break;
-                case ITEM_TYPE_GROUP_HEADER:
-                    view = newGroupView(mContext, parent);
-                    break;
-                case ITEM_TYPE_IN_GROUP:
-                    view = newChildView(mContext, parent);
-                    break;
-            }
+    /**
+     * Used for setting the cursor without triggering a UI thread update.
+     */
+    @NeededForTesting
+    public void setCursorForTesting(Cursor cursor) {
+        if (cursor != null) {
+            mCursor = cursor;
+            cursor.registerContentObserver(mChangeObserver);
+            cursor.registerDataSetObserver(mDataSetObserver);
+            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
         }
-
-        mCursor.moveToPosition(mPositionMetadata.cursorPosition);
-        switch (mPositionMetadata.itemType) {
-            case ITEM_TYPE_STANDALONE:
-                bindStandAloneView(view, mContext, mCursor);
-                break;
-            case ITEM_TYPE_GROUP_HEADER:
-                bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount,
-                        mPositionMetadata.isExpanded);
-                break;
-            case ITEM_TYPE_IN_GROUP:
-                bindChildView(view, mContext, mCursor);
-                break;
-
-        }
-        return view;
     }
 }
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index 845e279..bffbe5c 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -87,7 +87,8 @@
         mCursor.addRow(createCallLogEntry());
 
         // Bind the views of a single row.
-        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0);
 
         // There is one request for contact details.
         assertEquals(1, mAdapter.getContactInfoCache().requests.size());
@@ -105,7 +106,8 @@
         mCursor.addRow(createCallLogEntryWithCachedValues());
 
         // Bind the views of a single row.
-        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0);
 
         // There is one request for contact details.
         assertEquals(1, mAdapter.getContactInfoCache().requests.size());
@@ -123,7 +125,9 @@
         mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
 
         // Bind the views of a single row.
-        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(
+                CallLogListItemViews.fromView(getContext(), mView), 0);
 
         // There is one request for contact details.
         assertEquals(1, mAdapter.getContactInfoCache().requests.size());
@@ -138,7 +142,8 @@
         mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
 
         // Bind the views of a single row.
-        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0);
 
         // Cache and call log are up-to-date: no need to request update.
         assertEquals(0, mAdapter.getContactInfoCache().requests.size());
@@ -153,7 +158,8 @@
         mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, info);
 
         // Bind the views of a single row.
-        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(CallLogListItemViews.fromView(getContext(), mView), 0);
 
         // There is one request for contact details.
         assertEquals(1, mAdapter.getContactInfoCache().requests.size());
diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
index b57489d..fe14f87 100644
--- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
@@ -30,6 +30,7 @@
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.VoicemailContract;
+import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.test.ActivityInstrumentationTestCase2;
@@ -94,9 +95,9 @@
 
     // An item in the call list. All the methods performing checks use it.
     private CallLogListItemViews mItem;
-    // The list of views representing the data in the DB. View are in
-    // reverse order compare to the DB.
-    private View[] mList;
+
+    // The list of view holderss representing the data in the DB, in reverse order from the DB.
+    private ViewHolder[] mList;
 
     public CallLogFragmentTest() {
         super(FragmentTestActivity.class);
@@ -129,6 +130,7 @@
         mAdapter.pauseCache();
         mParentView = new FrameLayout(mActivity);
         mCursor = new MatrixCursor(CallLogQuery._PROJECTION);
+        mAdapter.setCursorForTesting(mCursor);
     }
 
     /**
@@ -140,7 +142,7 @@
     @MediumTest
     public void testCallViewIsNotVisibleForPrivateAndUnknownNumbers() {
         final int SIZE = 100;
-        mList = new View[SIZE];
+        mList = new ViewHolder[SIZE];
 
         // Insert the first batch of entries.
         mCursor.moveToFirst();
@@ -168,34 +170,34 @@
         insertPrivate(NOW, 0);
         insertPrivate(NOW, 0);
         insertPrivate(NOW, 0);
-        View view = mAdapter.newGroupView(getActivity(), mParentView);
-        mAdapter.bindGroupView(view, getActivity(), mCursor, 3, false);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        mAdapter.onBindViewHolder(viewHolder, /* position */ 0);
     }
 
     @MediumTest
     public void testCallAndGroupViews_StandAloneView() {
         mCursor.moveToFirst();
         insertPrivate(NOW, 0);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, 0);
+        bindViewForTest(viewHolder);
     }
 
     @MediumTest
     public void testCallAndGroupViews_ChildView() {
         mCursor.moveToFirst();
         insertPrivate(NOW, 0);
-        View view = mAdapter.newChildView(getActivity(), mParentView);
-        mAdapter.bindChildView(view, getActivity(), mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        mAdapter.onBindViewHolder(viewHolder, /* position */ 0);
     }
 
     @MediumTest
     public void testBindView_NumberOnlyNoCache() {
         mCursor.moveToFirst();
         insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertNameIs(views, TEST_NUMBER);
     }
 
@@ -206,10 +208,10 @@
                 Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
         values[CallLogQuery.CACHED_FORMATTED_NUMBER] = TEST_FORMATTED_NUMBER;
         insertValues(values);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertNameIs(views, TEST_FORMATTED_NUMBER);
     }
 
@@ -220,10 +222,10 @@
         // {@value com.android.dialer.calllog.ContactInfo#GEOCODE_AS_LABEL}
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertNameIs(views, "John Doe");
         assertLabel(views, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
     }
@@ -233,10 +235,10 @@
         mCursor.moveToFirst();
         insertWithCachedValues("sip:johndoe@gmail.com", NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertNameIs(views, "John Doe");
         assertLabel(views, "sip:johndoe@gmail.com", "sip:johndoe@gmail.com");
     }
@@ -248,10 +250,10 @@
         // {@value com.android.dialer.calllog.ContactInfo#GEOCODE_AS_LABEL}
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertNameIs(views, "John Doe");
         assertLabel(views, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
     }
@@ -263,10 +265,10 @@
         // {@link com.android.dialer.calllog.ContactInfo#GEOCODE_AS_LABEL}
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_WORK, TEST_DEFAULT_CUSTOM_LABEL);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertNameIs(views, "John Doe");
         assertLabel(views, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_WORK));
     }
@@ -277,10 +279,10 @@
         String numberLabel = "My label";
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_CUSTOM, numberLabel);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertNameIs(views, "John Doe");
         assertLabel(views, TEST_FORMATTED_NUMBER, numberLabel);
     }
@@ -290,10 +292,10 @@
         mCursor.moveToFirst();
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_HOME, "");
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertTrue(views.quickContactView.isEnabled());
     }
 
@@ -301,10 +303,10 @@
     public void testBindView_WithoutQuickContactBadge() {
         mCursor.moveToFirst();
         insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         assertFalse(views.quickContactView.isEnabled());
     }
 
@@ -312,10 +314,11 @@
     public void testBindView_CallButton() {
         mCursor.moveToFirst();
         insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        mAdapter.changeCursor(mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
 
         // The primaryActionView tag is set in the
         // {@link com.android.dialer.calllog.CallLogAdapter#bindView} method.  If it is possible
@@ -333,10 +336,10 @@
     public void testBindView_PlayButton() {
         mCursor.moveToFirst();
         insertVoicemail(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0);
-        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        bindViewForTest(view, mCursor);
+        ViewHolder viewHolder = mAdapter.onCreateViewHolder(mParentView, /* viewType */ 0);
+        bindViewForTest(viewHolder);
 
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        CallLogListItemViews views = (CallLogListItemViews) viewHolder;
         IntentProvider intentProvider = (IntentProvider) views.voicemailButtonView.getTag();
         Intent intent = intentProvider.getIntent(mActivity);
         // Starts the call detail activity.
@@ -372,7 +375,7 @@
             if (null == mList[i]) {
                 break;
             }
-            mItem = (CallLogListItemViews) mList[i].getTag();
+            mItem = (CallLogListItemViews) mList[i];
             int presentation = getPhoneNumberPresentationForListEntry(i);
             if (presentation == Calls.PRESENTATION_RESTRICTED ||
                     presentation == Calls.PRESENTATION_UNKNOWN) {
@@ -420,11 +423,12 @@
     private void buildViewListFromDb() {
         int i = 0;
         mCursor.moveToLast();
-        while(!mCursor.isBeforeFirst()) {
+        while (!mCursor.isBeforeFirst()) {
             if (null == mList[i]) {
-                mList[i] = mAdapter.newStandAloneView(mActivity, mParentView);
+                mList[i] = mAdapter.onCreateViewHolder(mParentView, /* itemType */ 0);
             }
-            bindViewForTest(mList[i], mCursor);
+            // Bind to the proper position, despite iterating in reverse.
+            bindViewForTest(mList[i], mCursor.getCount() - i - 1);
             mCursor.moveToPrevious();
             i++;
         }
@@ -446,12 +450,15 @@
      * unit tests can access the buttons contained within.
      *
      * @param view The current call log row.
-     * @param cursor The cursor to bind from.
+     * @param position The position of hte item.
      */
-    private void bindViewForTest(View view, MatrixCursor cursor) {
-        mAdapter.bindView(view, cursor, /* count */ 1);
-        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
-        mAdapter.expandItem(views, /* expand */ true);
+    private void bindViewForTest(ViewHolder viewHolder, int position) {
+        mAdapter.onBindViewHolder(viewHolder, position);
+        mAdapter.expandItem((CallLogListItemViews) viewHolder, /* expand */ true);
+    }
+
+    private void bindViewForTest(ViewHolder viewHolder) {
+        bindViewForTest(viewHolder, /* position */ 0);
     }
 
     /**
diff --git a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
index 3eb5f06..53583e0 100644
--- a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
+++ b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.support.v7.widget.RecyclerView;
 import android.test.AndroidTestCase;
 import android.text.TextUtils;
 import android.view.View;
@@ -34,7 +35,7 @@
  * Running all tests:
  *
  *   adb shell am instrument -e class com.android.dialer.calllog.GroupingListAdapterTests \
- *     -w com.google.android.dialer.tests/android.test.InstrumentationTestRunner
+ *     -w com.android.dialer.tests/android.test.InstrumentationTestRunner
  */
 public class GroupingListAdapterTests extends AndroidTestCase {
 
@@ -76,34 +77,22 @@
         }
 
         @Override
-        protected void bindChildView(View view, Context context, Cursor cursor) {
+        public void onContentChanged() {
+            // Do nothing.
         }
 
         @Override
-        protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
-                boolean expanded) {
-        }
-
-        @Override
-        protected void bindStandAloneView(View view, Context context, Cursor cursor) {
-        }
-
-        @Override
-        protected View newChildView(Context context, ViewGroup parent) {
+        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
             return null;
         }
 
         @Override
-        protected View newGroupView(Context context, ViewGroup parent) {
-            return null;
-        }
-
-        @Override
-        protected View newStandAloneView(Context context, ViewGroup parent) {
-            return null;
+        public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+            // Do nothing.
         }
     };
 
+
     private void buildCursor(String... numbers) {
         mCursor = new MatrixCursor(PROJECTION);
         mNextId = 1;
@@ -117,7 +106,7 @@
         buildCursor("1", "2", "3");
         mAdapter.changeCursor(mCursor);
 
-        assertEquals(3, mAdapter.getCount());
+        assertEquals(3, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
         assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 2);
@@ -127,7 +116,7 @@
         buildCursor("1", "1", "2");
         mAdapter.changeCursor(mCursor);
 
-        assertEquals(2, mAdapter.getCount());
+        assertEquals(2, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
     }
@@ -137,7 +126,7 @@
         mAdapter.changeCursor(mCursor);
         mAdapter.toggleGroup(0);
 
-        assertEquals(4, mAdapter.getCount());
+        assertEquals(4, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, true, 0);
         assertPositionMetadata(1, ITEM_TYPE_IN_GROUP, false, 0);
         assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
@@ -150,7 +139,7 @@
         mAdapter.toggleGroup(0);
         mAdapter.toggleGroup(0);
 
-        assertEquals(2, mAdapter.getCount());
+        assertEquals(2, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
     }
@@ -159,7 +148,7 @@
         buildCursor("1", "2", "2", "2", "3");
         mAdapter.changeCursor(mCursor);
 
-        assertEquals(3, mAdapter.getCount());
+        assertEquals(3, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
         assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 4);
@@ -170,7 +159,7 @@
         mAdapter.changeCursor(mCursor);
         mAdapter.toggleGroup(1);
 
-        assertEquals(6, mAdapter.getCount());
+        assertEquals(6, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
         assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
@@ -183,7 +172,7 @@
         buildCursor("1", "2", "3", "3", "3");
         mAdapter.changeCursor(mCursor);
 
-        assertEquals(3, mAdapter.getCount());
+        assertEquals(3, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
         assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, false, 2);
@@ -194,7 +183,7 @@
         mAdapter.changeCursor(mCursor);
         mAdapter.toggleGroup(2);
 
-        assertEquals(6, mAdapter.getCount());
+        assertEquals(6, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
         assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, true, 2);
@@ -207,7 +196,7 @@
         buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
         mAdapter.changeCursor(mCursor);
 
-        assertEquals(6, mAdapter.getCount());
+        assertEquals(6, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
         assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
@@ -225,7 +214,7 @@
         // 4th to the 6th position
         mAdapter.toggleGroup(6);
 
-        assertEquals(10, mAdapter.getCount());
+        assertEquals(10, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
         assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
@@ -243,7 +232,7 @@
         mAdapter.changeCursor(mCursor);
 
         // First pass - building up cache
-        assertEquals(6, mAdapter.getCount());
+        assertEquals(6, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
         assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
@@ -252,7 +241,7 @@
         assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
 
         // Second pass - using cache
-        assertEquals(6, mAdapter.getCount());
+        assertEquals(6, mAdapter.getItemCount());
         assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
         assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
         assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
@@ -295,7 +284,7 @@
         buildCursor(numbers);
         mAdapter.changeCursor(mCursor);
 
-        assertEquals(250, mAdapter.getCount());
+        assertEquals(250, mAdapter.getItemCount());
     }
 
     private void assertPositionMetadata(int position, int itemType, boolean isExpanded,