Merge "Switch to new SelectPhoneAccountDialogFragment API"
diff --git a/Android.mk b/Android.mk
index 696caf1..b029189 100644
--- a/Android.mk
+++ b/Android.mk
@@ -22,19 +22,21 @@
 
 LOCAL_AAPT_FLAGS := \
     --auto-add-overlay \
+    --extra-packages android.support.v7.recyclerview \
     --extra-packages com.android.incallui \
     --extra-packages com.android.contacts.common \
     --extra-packages com.android.phone.common
 
 LOCAL_JAVA_LIBRARIES := telephony-common
 LOCAL_STATIC_JAVA_LIBRARIES := \
-    com.android.services.telephony.common \
-    com.android.vcard \
     android-common \
-    guava \
+    android-ex-variablespeed \
     android-support-v13 \
     android-support-v4 \
-    android-ex-variablespeed \
+    android-support-v7-recyclerview \
+    com.android.services.telephony.common \
+    com.android.vcard \
+    guava \
     libphonenumber
 
 LOCAL_REQUIRED_MODULES := libvariablespeed
diff --git a/res/drawable/recent_lists_footer_background.xml b/res/drawable/recent_lists_footer_background.xml
deleted file mode 100644
index b5029af..0000000
--- a/res/drawable/recent_lists_footer_background.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-  ~ Copyright (C) 2014 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
-  -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="?android:attr/colorControlHighlight">
-    <!-- Mask to constrain the ripple to the bounds of the view. -->
-    <item android:id="@android:id/mask">
-        <color android:color="@android:color/white" />
-    </item>
-</ripple>
diff --git a/res/layout/recents_list_footer.xml b/res/layout/recents_list_footer.xml
deleted file mode 100644
index 3a56cbe..0000000
--- a/res/layout/recents_list_footer.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2014 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.
--->
-
-<!-- Text field and possibly soft menu button above the keypad where
-     the digits are displayed. -->
-
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/recents_list_footer"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:paddingTop="20dp"
-    android:paddingBottom="20dp"
-    android:gravity="center"
-    android:fontFamily="@string/view_full_call_history_font_family"
-    android:textStyle="bold"
-    android:textColor="@color/dialtacts_secondary_text_color"
-    android:textSize="14sp"
-    android:text="@string/recents_footer_text"
-    android:background="@drawable/recent_lists_footer_background" />
diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml
index b8b2a59..4e4bc36 100644
--- a/res/values/animation_constants.xml
+++ b/res/values/animation_constants.xml
@@ -27,16 +27,4 @@
     <dimen name="min_swipe">0dip</dimen>
     <dimen name="min_vert">10dip</dimen>
     <dimen name="min_lock">20dip</dimen>
-
-    <!-- Expand/collapse of call log entry duration. -->
-    <integer name="call_log_expand_collapse_duration">200</integer>
-
-    <!-- Start delay for the fade in of the call log actions. -->
-    <integer name="call_log_actions_fade_start">150</integer>
-
-    <!-- Duration of the fade in of the call log actions. -->
-    <integer name="call_log_actions_fade_in_duration">50</integer>
-
-    <!-- Duration of the fade out of the call log actions. -->
-    <integer name="call_log_actions_fade_out_duration">20</integer>
 </resources>
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index f5a3f62..fd6b37b 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -36,7 +36,6 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
-import com.android.common.widget.GroupingListAdapter;
 import com.android.contacts.common.util.UriUtils;
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.PhoneCallDetailsHelper;
@@ -96,12 +95,6 @@
     protected ContactInfoCache mContactInfoCache;
 
     /**
-     * Tracks the call log row which was previously expanded.  Used so that the closure of a
-     * previously expanded call log entry can be animated on rebind.
-     */
-    private long mPreviouslyExpanded = NONE_EXPANDED;
-
-    /**
      * Tracks the currently expanded call log row.
      */
     private long mCurrentlyExpanded = NONE_EXPANDED;
@@ -160,7 +153,7 @@
         @Override
         public void onClick(View v) {
             final View callLogItem = (View) v.getParent().getParent();
-            handleRowExpanded(callLogItem, true /* animate */, false /* forceExpand */);
+            handleRowExpanded(callLogItem, false /* forceExpand */);
         }
     };
 
@@ -177,8 +170,7 @@
         public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
                 AccessibilityEvent event) {
             if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
-                handleRowExpanded(host, false /* animate */,
-                        true /* forceExpand */);
+                handleRowExpanded(host, true /* forceExpand */);
             }
             return super.onRequestSendAccessibilityEvent(host, child, event);
         }
@@ -193,15 +185,16 @@
         return true;
     }
 
-    public CallLogAdapter(Context context, CallFetcher callFetcher,
-            ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener,
+    public CallLogAdapter(
+            Context context,
+            CallFetcher callFetcher,
+            ContactInfoHelper contactInfoHelper,
             OnReportButtonClickListener onReportButtonClickListener) {
         super(context);
 
         mContext = context;
         mCallFetcher = callFetcher;
         mContactInfoHelper = contactInfoHelper;
-        mCallItemExpandedListener = callItemExpandedListener;
 
         mOnReportButtonClickListener = onReportButtonClickListener;
 
@@ -322,7 +315,7 @@
      * @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
      */
-    private void bindView(View callLogItemView, Cursor c, int count) {
+    public void bindView(View callLogItemView, Cursor c, int count) {
         callLogItemView.setAccessibilityDelegate(mAccessibilityDelegate);
         final CallLogListItemViews views = (CallLogListItemViews) callLogItemView.getTag();
 
@@ -500,14 +493,10 @@
     private boolean toggleExpansion(long rowId) {
         if (rowId == mCurrentlyExpanded) {
             // Collapsing currently expanded row.
-            mPreviouslyExpanded = NONE_EXPANDED;
             mCurrentlyExpanded = NONE_EXPANDED;
-
             return false;
         } else {
             // Expanding a row (collapsing current expanded one).
-
-            mPreviouslyExpanded = mCurrentlyExpanded;
             mCurrentlyExpanded = rowId;
             return true;
         }
@@ -551,22 +540,6 @@
     }
 
     /**
-     * Bind a call log entry view for testing purposes.  Also inflates the action view stub so
-     * unit tests can access the buttons contained within.
-     *
-     * @param view The current call log row.
-     * @param context The current context.
-     * @param cursor The cursor to bind from.
-     */
-    @VisibleForTesting
-    void bindViewForTest(View view, Context context, Cursor cursor) {
-        bindStandAloneView(view, context, cursor);
-        CallLogListItemViews views = CallLogListItemViews.fromView(context, view);
-        views.inflateActionViewStub(mOnReportButtonClickListener, mActionListener,
-                mPhoneNumberUtilsWrapper, mCallLogViewsHelper);
-    }
-
-    /**
      * Sets whether processing of requests for contact details should be enabled.
      *
      * This method should be called in tests to disable such processing of requests when not
@@ -650,11 +623,10 @@
      * Manages the state changes for the UI interaction where a call log row is expanded.
      *
      * @param view The view that was tapped
-     * @param animate Whether or not to animate the expansion/collapse
      * @param forceExpand Whether or not to force the call log row into an expanded state regardless
      *        of its previous state
      */
-    private void handleRowExpanded(View view, boolean animate, boolean forceExpand) {
+    private void handleRowExpanded(View view, boolean forceExpand) {
         final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
 
         if (forceExpand && isExpanded(views.rowId)) {
@@ -663,38 +635,20 @@
 
         // Hide or show the actions view.
         boolean expanded = toggleExpansion(views.rowId);
+        expandItem(views, expanded);
+    }
 
+    /**
+     * @param views The view holder for the item to expand or collapse.
+     * @param expand {@code true} to expand the item, {@code false} otherwise.
+     */
+    public void expandItem(CallLogListItemViews views, boolean expand) {
         // Trigger loading of the viewstub and visual expand or collapse.
         views.expandOrCollapseActions(
-                expanded,
+                expand,
                 mOnReportButtonClickListener,
                 mActionListener,
                 mPhoneNumberUtilsWrapper,
                 mCallLogViewsHelper);
-
-        // Animate the expansion or collapse.
-        if (mCallItemExpandedListener != null) {
-            if (animate) {
-                mCallItemExpandedListener.onItemExpanded(view);
-            }
-
-            // Animate the collapse of the previous item if it is still visible on screen.
-            if (mPreviouslyExpanded != NONE_EXPANDED) {
-                View previousItem = mCallItemExpandedListener.getViewForCallId(mPreviouslyExpanded);
-
-                if (previousItem != null) {
-                    ((CallLogListItemViews) previousItem.getTag()).expandOrCollapseActions(
-                            false /* isExpanded */,
-                            mOnReportButtonClickListener,
-                            mActionListener,
-                            mPhoneNumberUtilsWrapper,
-                            mCallLogViewsHelper);
-                    if (animate) {
-                        mCallItemExpandedListener.onItemExpanded(previousItem);
-                    }
-                }
-                mPreviouslyExpanded = NONE_EXPANDED;
-            }
-        }
     }
 }
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 7756576..d69c2ed 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -62,13 +62,10 @@
  */
 public class CallLogFragment extends ListFragment
         implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener,
-        CallLogAdapter.CallFetcher,
-        CallLogAdapter.CallItemExpandedListener {
+        CallLogAdapter.CallFetcher {
     private static final String TAG = "CallLogFragment";
 
     private static final String REPORT_DIALOG_TAG = "report_dialog";
-    private String mReportDialogNumber;
-    private boolean mIsReportDialogShowing;
 
     /**
      * ID of the empty loader to defer other fragments.
@@ -78,9 +75,6 @@
     private static final String KEY_FILTER_TYPE = "filter_type";
     private static final String KEY_LOG_LIMIT = "log_limit";
     private static final String KEY_DATE_LIMIT = "date_limit";
-    private static final String KEY_SHOW_FOOTER = "show_footer";
-    private static final String KEY_IS_REPORT_DIALOG_SHOWING = "is_report_dialog_showing";
-    private static final String KEY_REPORT_DIALOG_NUMBER = "report_dialog_number";
 
     private CallLogAdapter mAdapter;
     private CallLogQueryHandler mCallLogQueryHandler;
@@ -91,21 +85,15 @@
 
     private VoicemailStatusHelper mVoicemailStatusHelper;
     private View mStatusMessageView;
+    private View mEmptyListView;
     private TextView mStatusMessageText;
     private TextView mStatusMessageAction;
     private KeyguardManager mKeyguardManager;
-    private View mFooterView;
 
     private boolean mEmptyLoaderRunning;
     private boolean mCallLogFetched;
     private boolean mVoicemailStatusFetched;
 
-    private float mExpandedItemTranslationZ;
-    private int mFadeInDuration;
-    private int mFadeInStartDelay;
-    private int mFadeOutDuration;
-    private int mExpandCollapseDuration;
-
     private final Handler mHandler = new Handler();
 
     private class CustomContentObserver extends ContentObserver {
@@ -138,9 +126,6 @@
     // the date filter are included.  If zero, no date-based filtering occurs.
     private long mDateLimit = 0;
 
-    // Whether or not to show the Show call history footer view
-    private boolean mHasFooterView = false;
-
     public CallLogFragment() {
         this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
     }
@@ -184,15 +169,11 @@
             mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
             mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
             mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
-            mHasFooterView = state.getBoolean(KEY_SHOW_FOOTER, mHasFooterView);
-            mIsReportDialogShowing = state.getBoolean(KEY_IS_REPORT_DIALOG_SHOWING,
-                    mIsReportDialogShowing);
-            mReportDialogNumber = state.getString(KEY_REPORT_DIALOG_NUMBER, mReportDialogNumber);
         }
 
         String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
         mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
-                new ContactInfoHelper(getActivity(), currentCountryIso), this, this);
+                new ContactInfoHelper(getActivity(), currentCountryIso), this);
         setListAdapter(mAdapter);
         mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
                 this, mLogLimit);
@@ -206,22 +187,6 @@
                 Status.CONTENT_URI, true, mVoicemailStatusObserver);
         setHasOptionsMenu(true);
         fetchCalls();
-
-        mExpandedItemTranslationZ =
-                getResources().getDimension(R.dimen.call_log_expanded_translation_z);
-        mFadeInDuration = getResources().getInteger(R.integer.call_log_actions_fade_in_duration);
-        mFadeInStartDelay = getResources().getInteger(R.integer.call_log_actions_fade_start);
-        mFadeOutDuration = getResources().getInteger(R.integer.call_log_actions_fade_out_duration);
-        mExpandCollapseDuration = getResources().getInteger(
-                R.integer.call_log_expand_collapse_duration);
-
-        if (mIsReportDialogShowing) {
-            DialogFragment df = ObjectFactory.getReportDialogFragment(mReportDialogNumber);
-            if (df != null) {
-                df.setTargetFragment(this, 0);
-                df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG);
-            }
-        }
     }
 
     /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
@@ -235,8 +200,13 @@
         mAdapter.changeCursor(cursor);
         // 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);
+        mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
+
         if (mScrollToTop) {
-            final ListView listView = getListView();
             // The smooth-scroll animation happens over a fixed time period.
             // As a result, if it scrolls through a large portion of the list,
             // each frame will jump so far from the previous one that the user
@@ -309,9 +279,8 @@
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        getListView().setEmptyView(view.findViewById(R.id.empty_list_view));
+        mEmptyListView = view.findViewById(R.id.empty_list_view);
         getListView().setItemsCanFocus(true);
-        maybeAddFooterView();
 
         updateEmptyMessage(mCallTypeFilter);
     }
@@ -404,9 +373,6 @@
         outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
         outState.putInt(KEY_LOG_LIMIT, mLogLimit);
         outState.putLong(KEY_DATE_LIMIT, mDateLimit);
-        outState.putBoolean(KEY_SHOW_FOOTER, mHasFooterView);
-        outState.putBoolean(KEY_IS_REPORT_DIALOG_SHOWING, mIsReportDialogShowing);
-        outState.putString(KEY_REPORT_DIALOG_NUMBER, mReportDialogNumber);
     }
 
     @Override
@@ -431,7 +397,7 @@
                         + filterType);
         }
         DialerUtils.configureEmptyListView(
-                getListView().getEmptyView(), R.drawable.empty_call_log, messageId, getResources());
+                mEmptyListView, R.drawable.empty_call_log, messageId, getResources());
     }
 
     CallLogAdapter getAdapter() {
@@ -494,180 +460,7 @@
         }
     }
 
-    /**
-     * Enables/disables the showing of the view full call history footer
-     *
-     * @param hasFooterView Whether or not to show the footer
-     */
-    public void setHasFooterView(boolean hasFooterView) {
-        mHasFooterView = hasFooterView;
-        maybeAddFooterView();
-    }
-
-    /**
-     * Determine whether or not the footer view should be added to the listview. If getView()
-     * is null, which means onCreateView hasn't been called yet, defer the addition of the footer
-     * until onViewCreated has been called.
-     */
-    private void maybeAddFooterView() {
-        if (!mHasFooterView || getView() == null) {
-            return;
-        }
-
-        if (mFooterView == null) {
-            mFooterView = getActivity().getLayoutInflater().inflate(
-                    R.layout.recents_list_footer, getListView(), false);
-            mFooterView.setOnClickListener(new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    ((HostInterface) getActivity()).showCallHistory();
-                }
-            });
-        }
-
-        final ListView listView = getListView();
-        listView.removeFooterView(mFooterView);
-        listView.addFooterView(mFooterView);
-
-        ViewUtil.addBottomPaddingToListViewForFab(listView, getResources());
-    }
-
-    @Override
-    public void onItemExpanded(final View view) {
-        final int startingHeight = view.getHeight();
-        final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag();
-        final ViewTreeObserver observer = getListView().getViewTreeObserver();
-        observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-            @Override
-            public boolean onPreDraw() {
-                // We don't want to continue getting called for every draw.
-                if (observer.isAlive()) {
-                    observer.removeOnPreDrawListener(this);
-                }
-                // Calculate some values to help with the animation.
-                final int endingHeight = view.getHeight();
-                final int distance = Math.abs(endingHeight - startingHeight);
-                final int baseHeight = Math.min(endingHeight, startingHeight);
-                final boolean isExpand = endingHeight > startingHeight;
-
-                // Set the views back to the start state of the animation
-                view.getLayoutParams().height = startingHeight;
-                if (!isExpand) {
-                    viewHolder.actionsView.setVisibility(View.VISIBLE);
-                }
-                viewHolder.expandVoicemailTranscriptionView(!isExpand);
-
-                // Set up the fade effect for the action buttons.
-                if (isExpand) {
-                    // Start the fade in after the expansion has partly completed, otherwise it
-                    // will be mostly over before the expansion completes.
-                    viewHolder.actionsView.setAlpha(0f);
-                    viewHolder.actionsView.animate()
-                            .alpha(1f)
-                            .setStartDelay(mFadeInStartDelay)
-                            .setDuration(mFadeInDuration)
-                            .start();
-                } else {
-                    viewHolder.actionsView.setAlpha(1f);
-                    viewHolder.actionsView.animate()
-                            .alpha(0f)
-                            .setDuration(mFadeOutDuration)
-                            .start();
-                }
-                view.requestLayout();
-
-                // Set up the animator to animate the expansion and shadow depth.
-                ValueAnimator animator = isExpand ? ValueAnimator.ofFloat(0f, 1f)
-                        : ValueAnimator.ofFloat(1f, 0f);
-
-                // Figure out how much scrolling is needed to make the view fully visible.
-                final Rect localVisibleRect = new Rect();
-                view.getLocalVisibleRect(localVisibleRect);
-                final int scrollingNeeded = localVisibleRect.top > 0 ? -localVisibleRect.top
-                        : view.getMeasuredHeight() - localVisibleRect.height();
-                final ListView listView = getListView();
-                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-
-                    private int mCurrentScroll = 0;
-
-                    @Override
-                    public void onAnimationUpdate(ValueAnimator animator) {
-                        Float value = (Float) animator.getAnimatedValue();
-
-                        // For each value from 0 to 1, animate the various parts of the layout.
-                        view.getLayoutParams().height = (int) (value * distance + baseHeight);
-                        float z = mExpandedItemTranslationZ * value;
-                        viewHolder.callLogEntryView.setTranslationZ(z);
-                        view.setTranslationZ(z); // WAR
-                        view.requestLayout();
-
-                        if (isExpand) {
-                            if (listView != null) {
-                                int scrollBy = (int) (value * scrollingNeeded) - mCurrentScroll;
-                                listView.smoothScrollBy(scrollBy, /* duration = */ 0);
-                                mCurrentScroll += scrollBy;
-                            }
-                        }
-                    }
-                });
-                // Set everything to their final values when the animation's done.
-                animator.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        view.getLayoutParams().height = LayoutParams.WRAP_CONTENT;
-
-                        if (!isExpand) {
-                            viewHolder.actionsView.setVisibility(View.GONE);
-                        } else {
-                            // This seems like it should be unnecessary, but without this, after
-                            // navigating out of the activity and then back, the action view alpha
-                            // is defaulting to the value (0) at the start of the expand animation.
-                            viewHolder.actionsView.setAlpha(1);
-                        }
-                        viewHolder.expandVoicemailTranscriptionView(isExpand);
-                    }
-                });
-
-                animator.setDuration(mExpandCollapseDuration);
-                animator.start();
-
-                // Return false so this draw does not occur to prevent the final frame from
-                // being drawn for the single frame before the animations start.
-                return false;
-            }
-        });
-    }
-
-    /**
-     * Retrieves the call log view for the specified call Id.  If the view is not currently
-     * visible, returns null.
-     *
-     * @param callId The call Id.
-     * @return The call log view.
-     */
-    @Override
-    public View getViewForCallId(long callId) {
-        ListView listView = getListView();
-
-        int firstPosition = listView.getFirstVisiblePosition();
-        int lastPosition = listView.getLastVisiblePosition();
-
-        for (int position = 0; position <= lastPosition - firstPosition; position++) {
-            View view = listView.getChildAt(position);
-
-            if (view != null) {
-                final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag();
-                if (viewHolder != null && viewHolder.rowId == callId) {
-                    return view;
-                }
-            }
-        }
-
-        return null;
-    }
-
     public void onBadDataReported(String number) {
-        mIsReportDialogShowing = false;
         if (number == null) {
             return;
         }
@@ -680,8 +473,6 @@
         if (df != null) {
             df.setTargetFragment(this, 0);
             df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG);
-            mReportDialogNumber = number;
-            mIsReportDialogShowing = true;
         }
     }
 }
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
index 1f11e1e..0826aeb 100644
--- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -21,7 +21,6 @@
 import android.telephony.PhoneNumberUtils;
 import android.text.format.Time;
 
-import com.android.common.widget.GroupingListAdapter;
 import com.android.contacts.common.util.DateUtils;
 import com.android.contacts.common.util.PhoneNumberHelper;
 
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
new file mode 100644
index 0000000..7895549
--- /dev/null
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -0,0 +1,490 @@
+/*
+ * 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.calllog;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.android.contacts.common.testing.NeededForTesting;
+
+/**
+ * Maintains a list that groups adjacent items sharing the same value of a "group-by" field.
+ *
+ * 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 {
+
+    private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
+    private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
+    private static final long GROUP_OFFSET_MASK    = 0x00000000FFFFFFFFL;
+    private static final long GROUP_SIZE_MASK     = 0x7FFFFFFF00000000L;
+    private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;
+
+    public static final int ITEM_TYPE_STANDALONE = 0;
+    public static final int ITEM_TYPE_GROUP_HEADER = 1;
+    public static final int ITEM_TYPE_IN_GROUP = 2;
+
+    /**
+     * Information about a specific list item: is it a group, if so is it expanded.
+     * Otherwise, is it a stand-alone item or a group member.
+     */
+    protected static class PositionMetadata {
+        int itemType;
+        boolean isExpanded;
+        int cursorPosition;
+        int childCount;
+        private int groupPosition;
+        private int listPosition = -1;
+    }
+
+    private Context mContext;
+    private Cursor mCursor;
+
+    /**
+     * Count of list items.
+     */
+    private int mCount;
+
+    private int mRowIdColumnIndex;
+
+    /**
+     * Count of groups in the list.
+     */
+    private int mGroupCount;
+
+    /**
+     * Information about where these groups are located in the list, how large they are
+     * and whether they are expanded.
+     */
+    private long[] mGroupMetadata;
+
+    private SparseIntArray mPositionCache = new SparseIntArray();
+    private int mLastCachedListPosition;
+    private int mLastCachedCursorPosition;
+    private int mLastCachedGroup;
+
+    /**
+     * A reusable temporary instance of PositionMetadata
+     */
+    private PositionMetadata mPositionMetadata = new PositionMetadata();
+
+    protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            onContentChanged();
+        }
+    };
+
+    protected DataSetObserver mDataSetObserver = new DataSetObserver() {
+
+        @Override
+        public void onChanged() {
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            notifyDataSetInvalidated();
+        }
+    };
+
+    public GroupingListAdapter(Context context) {
+        mContext = context;
+        resetCache();
+    }
+
+    /**
+     * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for
+     * each of them.
+     */
+    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);
+
+    /**
+     * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
+     */
+    private void resetCache() {
+        mCount = -1;
+        mLastCachedListPosition = -1;
+        mLastCachedCursorPosition = -1;
+        mLastCachedGroup = -1;
+        mPositionMetadata.listPosition = -1;
+        mPositionCache.clear();
+    }
+
+    protected void onContentChanged() {
+    }
+
+    public void changeCursor(Cursor cursor) {
+        if (cursor == mCursor) {
+            return;
+        }
+
+        if (mCursor != null) {
+            mCursor.unregisterContentObserver(mChangeObserver);
+            mCursor.unregisterDataSetObserver(mDataSetObserver);
+            mCursor.close();
+        }
+        mCursor = cursor;
+        resetCache();
+        findGroups();
+
+        if (cursor != null) {
+            cursor.registerContentObserver(mChangeObserver);
+            cursor.registerDataSetObserver(mDataSetObserver);
+            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
+            notifyDataSetChanged();
+        } else {
+            // notify the observers about the lack of a data set
+            notifyDataSetInvalidated();
+        }
+
+    }
+
+    public Cursor getCursor() {
+        return mCursor;
+    }
+
+    /**
+     * Scans over the entire cursor looking for duplicate phone numbers that need
+     * to be collapsed.
+     */
+    private void findGroups() {
+        mGroupCount = 0;
+        mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
+
+        if (mCursor == null) {
+            return;
+        }
+
+        addGroups(mCursor);
+    }
+
+    /**
+     * Records information about grouping in the list.  Should be called by the overridden
+     * {@link #addGroups} method.
+     */
+    protected void addGroup(int cursorPosition, int size, boolean expanded) {
+        if (mGroupCount >= mGroupMetadata.length) {
+            int newSize = idealLongArraySize(
+                    mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
+            long[] array = new long[newSize];
+            System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
+            mGroupMetadata = array;
+        }
+
+        long metadata = ((long)size << 32) | cursorPosition;
+        if (expanded) {
+            metadata |= EXPANDED_GROUP_MASK;
+        }
+        mGroupMetadata[mGroupCount++] = metadata;
+    }
+
+    // Copy/paste from ArrayUtils
+    private int idealLongArraySize(int need) {
+        return idealByteArraySize(need * 8) / 8;
+    }
+
+    // Copy/paste from ArrayUtils
+    private int idealByteArraySize(int need) {
+        for (int i = 4; i < 32; i++)
+            if (need <= (1 << i) - 12)
+                return (1 << i) - 12;
+
+        return need;
+    }
+
+    public int getCount() {
+        if (mCursor == null) {
+            return 0;
+        }
+
+        if (mCount != -1) {
+            return mCount;
+        }
+
+        int cursorPosition = 0;
+        int count = 0;
+        for (int i = 0; i < mGroupCount; i++) {
+            long metadata = mGroupMetadata[i];
+            int offset = (int)(metadata & GROUP_OFFSET_MASK);
+            boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
+            int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);
+
+            count += (offset - cursorPosition);
+
+            if (expanded) {
+                count += size + 1;
+            } else {
+                count++;
+            }
+
+            cursorPosition = offset + size;
+        }
+
+        mCount = count + mCursor.getCount() - cursorPosition;
+        return mCount;
+    }
+
+    /**
+     * Figures out whether the item at the specified position represents a
+     * stand-alone element, a group or a group child. Also computes the
+     * corresponding cursor position.
+     */
+    public void obtainPositionMetadata(PositionMetadata metadata, int position) {
+
+        // If the description object already contains requested information, just return
+        if (metadata.listPosition == position) {
+            return;
+        }
+
+        int listPosition = 0;
+        int cursorPosition = 0;
+        int firstGroupToCheck = 0;
+
+        // Check cache for the supplied position.  What we are looking for is
+        // the group descriptor immediately preceding the supplied position.
+        // Once we have that, we will be able to tell whether the position
+        // is the header of the group, a member of the group or a standalone item.
+        if (mLastCachedListPosition != -1) {
+            if (position <= mLastCachedListPosition) {
+
+                // Have SparceIntArray do a binary search for us.
+                int index = mPositionCache.indexOfKey(position);
+
+                // If we get back a positive number, the position corresponds to
+                // a group header.
+                if (index < 0) {
+
+                    // We had a cache miss, but we did obtain valuable information anyway.
+                    // The negative number will allow us to compute the location of
+                    // the group header immediately preceding the supplied position.
+                    index = ~index - 1;
+
+                    if (index >= mPositionCache.size()) {
+                        index--;
+                    }
+                }
+
+                // A non-negative index gives us the position of the group header
+                // corresponding or preceding the position, so we can
+                // search for the group information at the supplied position
+                // starting with the cached group we just found
+                if (index >= 0) {
+                    listPosition = mPositionCache.keyAt(index);
+                    firstGroupToCheck = mPositionCache.valueAt(index);
+                    long descriptor = mGroupMetadata[firstGroupToCheck];
+                    cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
+                }
+            } else {
+
+                // If we haven't examined groups beyond the supplied position,
+                // we will start where we left off previously
+                firstGroupToCheck = mLastCachedGroup;
+                listPosition = mLastCachedListPosition;
+                cursorPosition = mLastCachedCursorPosition;
+            }
+        }
+
+        for (int i = firstGroupToCheck; i < mGroupCount; i++) {
+            long group = mGroupMetadata[i];
+            int offset = (int)(group & GROUP_OFFSET_MASK);
+
+            // Move pointers to the beginning of the group
+            listPosition += (offset - cursorPosition);
+            cursorPosition = offset;
+
+            if (i > mLastCachedGroup) {
+                mPositionCache.append(listPosition, i);
+                mLastCachedListPosition = listPosition;
+                mLastCachedCursorPosition = cursorPosition;
+                mLastCachedGroup = i;
+            }
+
+            // Now we have several possibilities:
+            // A) The requested position precedes the group
+            if (position < listPosition) {
+                metadata.itemType = ITEM_TYPE_STANDALONE;
+                metadata.cursorPosition = cursorPosition - (listPosition - position);
+                return;
+            }
+
+            boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
+            int size = (int) ((group & GROUP_SIZE_MASK) >> 32);
+
+            // B) The requested position is a group header
+            if (position == listPosition) {
+                metadata.itemType = ITEM_TYPE_GROUP_HEADER;
+                metadata.groupPosition = i;
+                metadata.isExpanded = expanded;
+                metadata.childCount = size;
+                metadata.cursorPosition = offset;
+                return;
+            }
+
+            if (expanded) {
+                // C) The requested position is an element in the expanded group
+                if (position < listPosition + size + 1) {
+                    metadata.itemType = ITEM_TYPE_IN_GROUP;
+                    metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
+                    return;
+                }
+
+                // D) The element is past the expanded group
+                listPosition += size + 1;
+            } else {
+
+                // E) The element is past the collapsed group
+                listPosition++;
+            }
+
+            // Move cursor past the group
+            cursorPosition += size;
+        }
+
+        // The required item is past the last group
+        metadata.itemType = ITEM_TYPE_STANDALONE;
+        metadata.cursorPosition = cursorPosition + (position - listPosition);
+    }
+
+    /**
+     * Returns true if the specified position in the list corresponds to a
+     * group header.
+     */
+    public boolean isGroupHeader(int position) {
+        obtainPositionMetadata(mPositionMetadata, position);
+        return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
+    }
+
+    /**
+     * Given a position of a groups header in the list, returns the size of
+     * the corresponding group.
+     */
+    public int getGroupSize(int position) {
+        obtainPositionMetadata(mPositionMetadata, position);
+        return mPositionMetadata.childCount;
+    }
+
+    /**
+     * Mark group as expanded if it is collapsed and vice versa.
+     */
+    @NeededForTesting
+    public void toggleGroup(int position) {
+        obtainPositionMetadata(mPositionMetadata, position);
+        if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
+            throw new IllegalArgumentException("Not a group at position " + position);
+        }
+
+        if (mPositionMetadata.isExpanded) {
+            mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
+        } else {
+            mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
+        }
+        resetCache();
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 3;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        obtainPositionMetadata(mPositionMetadata, position);
+        return mPositionMetadata.itemType;
+    }
+
+    public Object getItem(int position) {
+        if (mCursor == null) {
+            return null;
+        }
+
+        obtainPositionMetadata(mPositionMetadata, position);
+        if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
+            return mCursor;
+        } else {
+            return null;
+        }
+    }
+
+    public long getItemId(int position) {
+        Object item = getItem(position);
+        if (item != null) {
+            return mCursor.getLong(mRowIdColumnIndex);
+        } else {
+            return -1;
+        }
+    }
+
+    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;
+            }
+        }
+
+        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/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index 0e558bf..f22a5d1 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -109,7 +109,6 @@
                 case TAB_INDEX_RECENTS:
                     mRecentsFragment = new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL,
                             MAX_RECENTS_ENTRIES, System.currentTimeMillis() - OLDEST_RECENTS_DATE);
-                    mRecentsFragment.setHasFooterView(true);
                     return mRecentsFragment;
                 case TAB_INDEX_ALL_CONTACTS:
                     mAllContactsFragment = new AllContactsFragment();
diff --git a/src/com/android/dialerbind/ObjectFactory.java b/src/com/android/dialerbind/ObjectFactory.java
index e5c39d07..dfacd3f 100644
--- a/src/com/android/dialerbind/ObjectFactory.java
+++ b/src/com/android/dialerbind/ObjectFactory.java
@@ -42,15 +42,15 @@
      * @param context The context to use.
      * @param callFetcher Instance of call fetcher to use.
      * @param contactInfoHelper Instance of contact info helper class to use.
-     * @param isCallLog Is this call log adapter being used on the call log?
      * @return Instance of CallLogAdapter.
      */
-    public static CallLogAdapter newCallLogAdapter(Context context,
-            CallFetcher callFetcher, ContactInfoHelper contactInfoHelper,
-            CallItemExpandedListener callItemExpandedListener,
+    public static CallLogAdapter newCallLogAdapter(
+            Context context,
+            CallFetcher callFetcher,
+            ContactInfoHelper contactInfoHelper,
             OnReportButtonClickListener onReportButtonClickListener) {
-        return new CallLogAdapter(context, callFetcher, contactInfoHelper,
-                callItemExpandedListener, onReportButtonClickListener);
+        return new CallLogAdapter(
+                context, callFetcher, contactInfoHelper, onReportButtonClickListener);
     }
 
     public static DialogFragment getReportDialogFragment(String number) {
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index dbdde68..845e279 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -196,7 +196,7 @@
     private static final class TestCallLogAdapter extends CallLogAdapter {
         public TestCallLogAdapter(Context context, CallFetcher callFetcher,
                 ContactInfoHelper contactInfoHelper) {
-            super(context, callFetcher, contactInfoHelper, null, null);
+            super(context, callFetcher, contactInfoHelper, null);
             mContactInfoCache = new TestContactInfoCache(
                     contactInfoHelper, mOnContactInfoChangedListener);
         }
diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
index 0553422..b57489d 100644
--- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
@@ -57,7 +57,7 @@
  *   runtest contacts
  * or
  *   adb shell am instrument \
- *     -w com.android.contacts.tests/android.test.InstrumentationTestRunner
+ *     -w com.android.dialer.tests/android.test.InstrumentationTestRunner
  */
 @LargeTest
 public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<FragmentTestActivity> {
@@ -177,7 +177,7 @@
         mCursor.moveToFirst();
         insertPrivate(NOW, 0);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
     }
 
     @MediumTest
@@ -193,7 +193,7 @@
         mCursor.moveToFirst();
         insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertNameIs(views, TEST_NUMBER);
@@ -207,7 +207,7 @@
         values[CallLogQuery.CACHED_FORMATTED_NUMBER] = TEST_FORMATTED_NUMBER;
         insertValues(values);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertNameIs(views, TEST_FORMATTED_NUMBER);
@@ -221,7 +221,7 @@
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertNameIs(views, "John Doe");
@@ -234,7 +234,7 @@
         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);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertNameIs(views, "John Doe");
@@ -249,7 +249,7 @@
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertNameIs(views, "John Doe");
@@ -264,7 +264,7 @@
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_WORK, TEST_DEFAULT_CUSTOM_LABEL);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertNameIs(views, "John Doe");
@@ -278,7 +278,7 @@
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_CUSTOM, numberLabel);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertNameIs(views, "John Doe");
@@ -291,7 +291,7 @@
         insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
                 "John Doe", Phone.TYPE_HOME, "");
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertTrue(views.quickContactView.isEnabled());
@@ -302,7 +302,7 @@
         mCursor.moveToFirst();
         insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         assertFalse(views.quickContactView.isEnabled());
@@ -313,7 +313,7 @@
         mCursor.moveToFirst();
         insert(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0, Calls.INCOMING_TYPE);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
 
@@ -334,7 +334,7 @@
         mCursor.moveToFirst();
         insertVoicemail(TEST_NUMBER, Calls.PRESENTATION_ALLOWED, NOW, 0);
         View view = mAdapter.newStandAloneView(getActivity(), mParentView);
-        mAdapter.bindViewForTest(view, getActivity(), mCursor);
+        bindViewForTest(view, mCursor);
 
         CallLogListItemViews views = (CallLogListItemViews) view.getTag();
         IntentProvider intentProvider = (IntentProvider) views.voicemailButtonView.getTag();
@@ -424,7 +424,7 @@
             if (null == mList[i]) {
                 mList[i] = mAdapter.newStandAloneView(mActivity, mParentView);
             }
-            mAdapter.bindViewForTest(mList[i], mActivity, mCursor);
+            bindViewForTest(mList[i], mCursor);
             mCursor.moveToPrevious();
             i++;
         }
@@ -442,6 +442,19 @@
     //
 
     /**
+     * Bind a call log entry view for testing purposes.  Also inflates the action view stub so
+     * unit tests can access the buttons contained within.
+     *
+     * @param view The current call log row.
+     * @param cursor The cursor to bind from.
+     */
+    private void bindViewForTest(View view, MatrixCursor cursor) {
+        mAdapter.bindView(view, cursor, /* count */ 1);
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        mAdapter.expandItem(views, /* expand */ true);
+    }
+
+    /**
      * Insert a certain number of random numbers in the DB. Makes sure
      * there is at least one private and one unknown number in the DB.
      * @param num Of entries to be inserted.
diff --git a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
new file mode 100644
index 0000000..3eb5f06
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
@@ -0,0 +1,311 @@
+/*
+ * 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.calllog;
+
+import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_GROUP_HEADER;
+import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_IN_GROUP;
+import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_STANDALONE;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.test.AndroidTestCase;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Tests for {@link GroupingListAdapter}.
+ *
+ * Running all tests:
+ *
+ *   adb shell am instrument -e class com.android.dialer.calllog.GroupingListAdapterTests \
+ *     -w com.google.android.dialer.tests/android.test.InstrumentationTestRunner
+ */
+public class GroupingListAdapterTests extends AndroidTestCase {
+
+    static private final String[] PROJECTION = new String[] {
+        "_id",
+        "group",
+    };
+
+    private static final int GROUPING_COLUMN_INDEX = 1;
+
+    private MatrixCursor mCursor;
+    private long mNextId;
+
+    private GroupingListAdapter mAdapter = new GroupingListAdapter(null) {
+
+        @Override
+        protected void addGroups(Cursor cursor) {
+            int count = cursor.getCount();
+            int groupItemCount = 1;
+            cursor.moveToFirst();
+            String currentValue = cursor.getString(GROUPING_COLUMN_INDEX);
+            for (int i = 1; i < count; i++) {
+                cursor.moveToNext();
+                String value = cursor.getString(GROUPING_COLUMN_INDEX);
+                if (TextUtils.equals(value, currentValue)) {
+                    groupItemCount++;
+                } else {
+                    if (groupItemCount > 1) {
+                        addGroup(i - groupItemCount, groupItemCount, false);
+                    }
+
+                    groupItemCount = 1;
+                    currentValue = value;
+                }
+            }
+            if (groupItemCount > 1) {
+                addGroup(count - groupItemCount, groupItemCount, false);
+            }
+        }
+
+        @Override
+        protected void bindChildView(View view, Context context, Cursor cursor) {
+        }
+
+        @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) {
+            return null;
+        }
+
+        @Override
+        protected View newGroupView(Context context, ViewGroup parent) {
+            return null;
+        }
+
+        @Override
+        protected View newStandAloneView(Context context, ViewGroup parent) {
+            return null;
+        }
+    };
+
+    private void buildCursor(String... numbers) {
+        mCursor = new MatrixCursor(PROJECTION);
+        mNextId = 1;
+        for (String number : numbers) {
+            mCursor.addRow(new Object[]{mNextId, number});
+            mNextId++;
+        }
+    }
+
+    public void testGroupingWithoutGroups() {
+        buildCursor("1", "2", "3");
+        mAdapter.changeCursor(mCursor);
+
+        assertEquals(3, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 2);
+    }
+
+    public void testGroupingWithCollapsedGroupAtTheBeginning() {
+        buildCursor("1", "1", "2");
+        mAdapter.changeCursor(mCursor);
+
+        assertEquals(2, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+    }
+
+    public void testGroupingWithExpandedGroupAtTheBeginning() {
+        buildCursor("1", "1", "2");
+        mAdapter.changeCursor(mCursor);
+        mAdapter.toggleGroup(0);
+
+        assertEquals(4, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, true, 0);
+        assertPositionMetadata(1, ITEM_TYPE_IN_GROUP, false, 0);
+        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+        assertPositionMetadata(3, ITEM_TYPE_STANDALONE, false, 2);
+    }
+
+    public void testGroupingWithExpandCollapseCycleAtTheBeginning() {
+        buildCursor("1", "1", "2");
+        mAdapter.changeCursor(mCursor);
+        mAdapter.toggleGroup(0);
+        mAdapter.toggleGroup(0);
+
+        assertEquals(2, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+    }
+
+    public void testGroupingWithCollapsedGroupInTheMiddle() {
+        buildCursor("1", "2", "2", "2", "3");
+        mAdapter.changeCursor(mCursor);
+
+        assertEquals(3, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 4);
+    }
+
+    public void testGroupingWithExpandedGroupInTheMiddle() {
+        buildCursor("1", "2", "2", "2", "3");
+        mAdapter.changeCursor(mCursor);
+        mAdapter.toggleGroup(1);
+
+        assertEquals(6, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+        assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
+        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 4);
+    }
+
+    public void testGroupingWithCollapsedGroupAtTheEnd() {
+        buildCursor("1", "2", "3", "3", "3");
+        mAdapter.changeCursor(mCursor);
+
+        assertEquals(3, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+        assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, false, 2);
+    }
+
+    public void testGroupingWithExpandedGroupAtTheEnd() {
+        buildCursor("1", "2", "3", "3", "3");
+        mAdapter.changeCursor(mCursor);
+        mAdapter.toggleGroup(2);
+
+        assertEquals(6, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+        assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, true, 2);
+        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+        assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
+        assertPositionMetadata(5, ITEM_TYPE_IN_GROUP, false, 4);
+    }
+
+    public void testGroupingWithMultipleCollapsedGroups() {
+        buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+        mAdapter.changeCursor(mCursor);
+
+        assertEquals(6, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+        assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+        assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+    }
+
+    public void testGroupingWithMultipleExpandedGroups() {
+        buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+        mAdapter.changeCursor(mCursor);
+        mAdapter.toggleGroup(1);
+
+        // Note that expanding the group of 2's shifted the group of 5's down from the
+        // 4th to the 6th position
+        mAdapter.toggleGroup(6);
+
+        assertEquals(10, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+        assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+        assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+        assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, true, 6);
+        assertPositionMetadata(7, ITEM_TYPE_IN_GROUP, false, 6);
+        assertPositionMetadata(8, ITEM_TYPE_IN_GROUP, false, 7);
+        assertPositionMetadata(9, ITEM_TYPE_STANDALONE, false, 8);
+    }
+
+    public void testPositionCache() {
+        buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+        mAdapter.changeCursor(mCursor);
+
+        // First pass - building up cache
+        assertEquals(6, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+        assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+        assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+
+        // Second pass - using cache
+        assertEquals(6, mAdapter.getCount());
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+        assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+        assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+
+        // Invalidate cache by expanding a group
+        mAdapter.toggleGroup(1);
+
+        // First pass - building up cache
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+        assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+        assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+        assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
+        assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
+
+        // Second pass - using cache
+        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+        assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+        assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+        assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
+        assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
+    }
+
+    public void testGroupDescriptorArrayGrowth() {
+        String[] numbers = new String[500];
+        for (int i = 0; i < numbers.length; i++) {
+
+            // Make groups of 2
+            numbers[i] = String.valueOf((i / 2) * 2);
+        }
+
+        buildCursor(numbers);
+        mAdapter.changeCursor(mCursor);
+
+        assertEquals(250, mAdapter.getCount());
+    }
+
+    private void assertPositionMetadata(int position, int itemType, boolean isExpanded,
+            int cursorPosition) {
+        GroupingListAdapter.PositionMetadata metadata = new GroupingListAdapter.PositionMetadata();
+        mAdapter.obtainPositionMetadata(metadata, position);
+        assertEquals(itemType, metadata.itemType);
+        if (metadata.itemType == ITEM_TYPE_GROUP_HEADER) {
+            assertEquals(isExpanded, metadata.isExpanded);
+        }
+        assertEquals(cursorPosition, metadata.cursorPosition);
+    }
+}