Rewrite grouping logic in Dialer.

- Remove expand/collapse and item type logic in GroupingListAdapter.
Losing some potential functionality, but it does not adversely
affect how we currently group, and makes grouping easier to
understanding.
+ Rewrite GroupingListAdapter to provide O(1) lookup for group size
and getItem. This requires maintaining a SparseIntArray of metadata
for each list item. Cut metadata storage from long to int, to help
adjust for the larger memory overhead.
+ Simplify the logic for building and maintaing the metadata in the
GroupingListAdapter, offloading much of it to the SparseIntArray.
+ Explictily add all groups, including groups with single items,
in the CallLogBroupBuilder.
+ Tidied up logic in CallLogGroupBuilder to make it more intuitive
what cases it's handling and what's happening.
+ Updated tests to work and pass with new tests.

Bug: 23422274
Change-Id: Ia7a00c4b580813cade87fdc054ffdd702f59c12c
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index aa307b6..4593e5e 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -131,7 +131,7 @@
 
     private SharedPreferences mPrefs;
 
-    private boolean mShowVoicemailPromoCard = false;
+    protected boolean mShowVoicemailPromoCard = false;
 
     /** Instance of helper class for managing views. */
     private final CallLogListItemHelper mCallLogListItemHelper;
@@ -628,6 +628,11 @@
                         ? 1 : 0));
     }
 
+    @Override
+    public int getGroupSize(int position) {
+        return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0));
+    }
+
     protected boolean isCallLogActivity() {
         return mIsCallLogActivity;
     }
@@ -794,11 +799,6 @@
         mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
     }
 
-    @Override
-    public void addGroup(int cursorPosition, int size, boolean expanded) {
-        super.addGroup(cursorPosition, size, expanded);
-    }
-
     /**
      * Stores the day group associated with a call in the call log.
      *
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
index 0826aeb..4cf2d07 100644
--- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -20,6 +20,7 @@
 import android.provider.CallLog.Calls;
 import android.telephony.PhoneNumberUtils;
 import android.text.format.Time;
+import android.text.TextUtils;
 
 import com.android.contacts.common.util.DateUtils;
 import com.android.contacts.common.util.PhoneNumberHelper;
@@ -46,9 +47,8 @@
          * dialed.
          * @param cursorPosition The starting position of the group in the cursor.
          * @param size The size of the group.
-         * @param expanded Whether the group is expanded; always false for the call log.
          */
-        public void addGroup(int cursorPosition, int size, boolean expanded);
+        public void addGroup(int cursorPosition, int size);
 
         /**
          * Defines the interface for tracking the day group each call belongs to.  Calls in a call
@@ -94,7 +94,7 @@
 
     /**
      * Finds all groups of adjacent entries in the call log which should be grouped together and
-     * calls {@link GroupCreator#addGroup(int, int, boolean)} on {@link #mGroupCreator} for each of
+     * calls {@link GroupCreator#addGroup(int, int)} on {@link #mGroupCreator} for each of
      * them.
      * <p>
      * For entries that are not grouped with others, we do not need to create a group of size one.
@@ -114,98 +114,70 @@
 
         // Get current system time, used for calculating which day group calls belong to.
         long currentTime = System.currentTimeMillis();
-
-        int currentGroupSize = 1;
         cursor.moveToFirst();
-        // The number of the first entry in the group.
-        String firstNumber = cursor.getString(CallLogQuery.NUMBER);
-        // This is the type of the first call in the group.
-        int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
-
-        // The account information of the first entry in the group.
-        String firstAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
-        String firstAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
 
         // Determine the day group for the first call in the cursor.
         final long firstDate = cursor.getLong(CallLogQuery.DATE);
         final long firstRowId = cursor.getLong(CallLogQuery.ID);
-        int currentGroupDayGroup = getDayGroup(firstDate, currentTime);
-        mGroupCreator.setDayGroup(firstRowId, currentGroupDayGroup);
+        int groupDayGroup = getDayGroup(firstDate, currentTime);
+        mGroupCreator.setDayGroup(firstRowId, groupDayGroup);
+
+        // Instantiate the group values to those of the first call in the cursor.
+        String groupNumber = cursor.getString(CallLogQuery.NUMBER);
+        int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+        String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+        String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+        int groupSize = 1;
+
+        String number;
+        int callType;
+        String accountComponentName;
+        String accountId;
 
         while (cursor.moveToNext()) {
-            // The number of the current row in the cursor.
-            final String currentNumber = cursor.getString(CallLogQuery.NUMBER);
-            final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
-            final String currentAccountComponentName = cursor.getString(
-                    CallLogQuery.ACCOUNT_COMPONENT_NAME);
-            final String currentAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+            // Obtain the values for the current call to group.
+            number = cursor.getString(CallLogQuery.NUMBER);
+            callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+            accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+            accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
 
-            final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
-            final boolean sameAccountComponentName = Objects.equals(
-                    firstAccountComponentName,
-                    currentAccountComponentName);
-            final boolean sameAccountId = Objects.equals(
-                    firstAccountId,
-                    currentAccountId);
-            final boolean sameAccount = sameAccountComponentName && sameAccountId;
+            final boolean isSameNumber = equalNumbers(groupNumber, number);
+            final boolean isSameAccount = isSameAccount(
+                    groupAccountComponentName, accountComponentName, groupAccountId, accountId);
 
-            final boolean shouldGroup;
-            final long currentCallId = cursor.getLong(CallLogQuery.ID);
-            final long date = cursor.getLong(CallLogQuery.DATE);
-
-            if (!sameNumber || !sameAccount) {
-                // Should only group with calls from the same number.
-                shouldGroup = false;
-            } else if (firstCallType == Calls.VOICEMAIL_TYPE) {
-                // never group voicemail.
-                shouldGroup = false;
-            } else {
-                // Incoming, outgoing, and missed calls group together.
-                shouldGroup = callType != Calls.VOICEMAIL_TYPE;
-            }
-
-            if (shouldGroup) {
+            // Group with the same number and account which are not voicemail.
+            if (isSameNumber && isSameAccount
+                    && (callType != Calls.VOICEMAIL_TYPE)
+                    && (groupCallType != Calls.VOICEMAIL_TYPE)) {
                 // Increment the size of the group to include the current call, but do not create
-                // the group until we find a call that does not match.
-                currentGroupSize++;
+                // the group until finding a call that does not match.
+                groupSize++;
             } else {
-                // The call group has changed, so determine the day group for the new call group.
-                // This ensures all calls grouped together in the call log are assigned the same
-                // day group.
-                currentGroupDayGroup = getDayGroup(date, currentTime);
+                // The call group has changed. Determine the day group for the new call group.
+                final long date = cursor.getLong(CallLogQuery.DATE);
+                groupDayGroup = getDayGroup(date, currentTime);
 
-                // Create a group for the previous set of calls, excluding the current one, but do
-                // not create a group for a single call.
-                if (currentGroupSize > 1) {
-                    addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
-                }
+                // Create a group for the previous group of calls, which does not include the
+                // current call.
+                mGroupCreator.addGroup(cursor.getPosition() - groupSize, groupSize);
+
                 // Start a new group; it will include at least the current call.
-                currentGroupSize = 1;
-                // The current entry is now the first in the group.
-                firstNumber = currentNumber;
-                firstCallType = callType;
-                firstAccountComponentName = currentAccountComponentName;
-                firstAccountId = currentAccountId;
+                groupSize = 1;
+
+                // Update the group values to those of the current call.
+                groupNumber = number;
+                groupCallType = callType;
+                groupAccountComponentName = accountComponentName;
+                groupAccountId = accountId;
             }
 
             // Save the day group associated with the current call.
-            mGroupCreator.setDayGroup(currentCallId, currentGroupDayGroup);
+            final long currentCallId = cursor.getLong(CallLogQuery.ID);
+            mGroupCreator.setDayGroup(currentCallId, groupDayGroup);
         }
-        // If the last set of calls at the end of the call log was itself a group, create it now.
-        if (currentGroupSize > 1) {
-            addGroup(count - currentGroupSize, currentGroupSize);
-        }
-    }
 
-    /**
-     * Creates a group of items in the cursor.
-     * <p>
-     * The group is always unexpanded.
-     *
-     * @see CallLogAdapter#addGroup(int, int, boolean)
-     */
-    private void addGroup(int cursorPosition, int size) {
-        mGroupCreator.addGroup(cursorPosition, size, false);
+        // Create a group for the last set of calls.
+        mGroupCreator.addGroup(count - groupSize, groupSize);
     }
 
     @VisibleForTesting
@@ -217,6 +189,10 @@
         }
     }
 
+    private boolean isSameAccount(String name1, String name2, String id1, String id2) {
+        return TextUtils.equals(name1, name2) && TextUtils.equals(id1, id2);
+    }
+
     @VisibleForTesting
     boolean compareSipAddresses(String number1, String number2) {
         if (number1 == null || number2 == null) return number1 == number2;
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
index 8d3ab45..54dd5f6 100644
--- a/src/com/android/dialer/calllog/GroupingListAdapter.java
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -22,78 +22,28 @@
 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;
-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.
+ * Maintains a list that groups items into groups of consecutive elements which are disjoint,
+ * that is, an item can only belong to one group. This is leveraged for grouping calls in the
+ * call log received from or made to the same phone number.
  *
- * 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.
+ * There are two integers stored as metadata for every list item in the adapter.
  */
 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;
-    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.
+     * SparseIntArray, which maps the cursor position of the first element of a group to the size
+     * of the group. The index of a key in this map corresponds to the list position of that group.
      */
-    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();
+    private SparseIntArray mGroupMetadata;
+    private int mItemCount;
 
     protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
-
         @Override
         public boolean deliverSelfNotifications() {
             return true;
@@ -106,7 +56,6 @@
     };
 
     protected DataSetObserver mDataSetObserver = new DataSetObserver() {
-
         @Override
         public void onChanged() {
             notifyDataSetChanged();
@@ -115,7 +64,7 @@
 
     public GroupingListAdapter(Context context) {
         mContext = context;
-        resetCache();
+        reset();
     }
 
     /**
@@ -126,18 +75,6 @@
 
     protected abstract void onContentChanged();
 
-    /**
-     * 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();
-    }
-
     public void changeCursor(Cursor cursor) {
         if (cursor == mCursor) {
             return;
@@ -148,288 +85,73 @@
             mCursor.unregisterDataSetObserver(mDataSetObserver);
             mCursor.close();
         }
+
+        // Reset whenever the cursor is changed.
+        reset();
         mCursor = cursor;
-        resetCache();
-        findGroups();
 
         if (cursor != null) {
+            addGroups(mCursor);
+
+            // Calculate the item count by subtracting group child counts from the cursor count.
+            mItemCount = mGroupMetadata.size();
+
             cursor.registerContentObserver(mChangeObserver);
             cursor.registerDataSetObserver(mDataSetObserver);
-            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
             notifyDataSetChanged();
         }
     }
 
-    @NeededForTesting
-    public Cursor getCursor() {
-        return mCursor;
-    }
-
     /**
-     * Scans over the entire cursor looking for duplicate phone numbers that need
-     * to be collapsed.
+     * Records information about grouping in the list.
+     * Should be called by the overridden {@link #addGroups} method.
      */
-    private void findGroups() {
-        mGroupCount = 0;
-        mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
-
-        if (mCursor == null) {
-            return;
+    public void addGroup(int cursorPosition, int groupSize) {
+        int lastIndex = mGroupMetadata.size() - 1;
+        if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) {
+            mGroupMetadata.put(cursorPosition, groupSize);
+        } else {
+            // Optimization to avoid binary search if adding groups in ascending cursor position.
+            mGroupMetadata.append(cursorPosition, groupSize);
         }
-
-        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;
     }
 
     @Override
     public int getItemCount() {
-        if (mCursor == null) {
+        return mItemCount;
+    }
+
+    /**
+     * Given the position of a list item, returns the size of the group of items corresponding to
+     * that position.
+     */
+    public int getGroupSize(int listPosition) {
+        if (listPosition >= mGroupMetadata.size()) {
             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;
+        return mGroupMetadata.valueAt(listPosition);
     }
 
     /**
-     * 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.
+     * Given the position of a list item, returns the the first item in the group of items
+     * corresponding to that 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);
-                metadata.childCount = 1;
-                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);
-        metadata.childCount = 1;
-    }
-
-    /**
-     * 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();
-    }
-
-    public int getItemViewType(int position) {
-        obtainPositionMetadata(mPositionMetadata, position);
-        return mPositionMetadata.itemType;
-    }
-
-    public Object getItem(int position) {
-        if (mCursor == null) {
+    public Object getItem(int listPosition) {
+        if (mCursor == null || listPosition >= mGroupMetadata.size()) {
             return null;
         }
 
-        obtainPositionMetadata(mPositionMetadata, position);
-        if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
+        int cursorPosition = mGroupMetadata.keyAt(listPosition);
+        if (mCursor.moveToPosition(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;
-        }
+    private void reset() {
+        mItemCount = 0;
+        mGroupMetadata = new SparseIntArray();
     }
 }
diff --git a/src/com/android/dialer/calllog/PromoCardViewHolder.java b/src/com/android/dialer/calllog/PromoCardViewHolder.java
index 4c96027..656b669 100644
--- a/src/com/android/dialer/calllog/PromoCardViewHolder.java
+++ b/src/com/android/dialer/calllog/PromoCardViewHolder.java
@@ -15,12 +15,14 @@
  */
 package com.android.dialer.calllog;
 
-import com.android.dialer.R;
-
+import android.content.Context;
 import android.support.v7.widget.CardView;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
+import com.android.contacts.common.testing.NeededForTesting;
+import com.android.dialer.R;
+
 /**
  * View holder class for a promo card which will appear in the voicemail tab.
  */
@@ -68,4 +70,12 @@
     public View getOkTextView() {
         return mOkTextView;
     }
+
+    @NeededForTesting
+    public static PromoCardViewHolder createForTest(Context context) {
+        PromoCardViewHolder viewHolder = new PromoCardViewHolder(new View(context));
+        viewHolder.mSettingsTextView = new View(context);
+        viewHolder.mOkTextView = new View(context);
+        return viewHolder;
+    }
 }
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index b4162e1..2bdc197 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -35,7 +35,9 @@
  */
 @SmallTest
 public class CallLogAdapterTest extends AndroidTestCase {
-    private static final String TEST_NUMBER = "12345678";
+    private static final String TEST_NUMBER_1 = "12345678";
+    private static final String TEST_NUMBER_2 = "87654321";
+    private static final String TEST_NUMBER_3 = "18273645";
     private static final String TEST_NAME = "name";
     private static final String TEST_NUMBER_LABEL = "label";
     private static final int TEST_NUMBER_TYPE = 1;
@@ -46,7 +48,7 @@
 
     private MatrixCursor mCursor;
     private View mView;
-    private ViewHolder mViewHolder;
+    private CallLogListItemViewHolder mViewHolder;
 
     @Override
     protected void setUp() throws Exception {
@@ -98,7 +100,7 @@
 
         TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
         // It is for the number we need to show.
-        assertEquals(TEST_NUMBER, request.number);
+        assertEquals(TEST_NUMBER_1, request.number);
         // It has the right country.
         assertEquals(TEST_COUNTRY_ISO, request.countryIso);
         // Since there is nothing in the cache, it is an immediate request.
@@ -125,7 +127,7 @@
 
     public void testBindView_NoCallLogButMemoryCache_EnqueueRequest() {
         mCursor.addRow(createCallLogEntry());
-        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, createContactInfo());
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -141,7 +143,7 @@
 
     public void testBindView_BothCallLogAndMemoryCache_NoEnqueueRequest() {
         mCursor.addRow(createCallLogEntryWithCachedValues());
-        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, createContactInfo());
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -157,7 +159,7 @@
         // Contact info contains a different name.
         ContactInfo info = createContactInfo();
         info.name = "new name";
-        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, info);
+        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, info);
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -171,10 +173,37 @@
         assertFalse("should not be immediate", request.immediate);
     }
 
+    public void testBindVoicemailPromoCard() {
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_1));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_1));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_2));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_2));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_2));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_3));
+
+        // Bind the voicemail promo card.
+        mAdapter.showVoicemailPromoCard(true);
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(PromoCardViewHolder.createForTest(getContext()), 0);
+
+        // Check that displaying the promo card does not affect the grouping or list display.
+        mAdapter.onBindViewHolder(mViewHolder, 1);
+        assertEquals(2, mAdapter.getGroupSize(1));
+        assertEquals(TEST_NUMBER_1, mViewHolder.number);
+
+        mAdapter.onBindViewHolder(mViewHolder, 2);
+        assertEquals(3, mAdapter.getGroupSize(2));
+        assertEquals(TEST_NUMBER_2, mViewHolder.number);
+
+        mAdapter.onBindViewHolder(mViewHolder, 3);
+        assertEquals(1, mAdapter.getGroupSize(3));
+        assertEquals(TEST_NUMBER_3, mViewHolder.number);
+    }
+
     /** Returns a contact info with default values. */
     private ContactInfo createContactInfo() {
         ContactInfo info = new ContactInfo();
-        info.number = TEST_NUMBER;
+        info.number = TEST_NUMBER_1;
         info.name = TEST_NAME;
         info.type = TEST_NUMBER_TYPE;
         info.label = TEST_NUMBER_LABEL;
@@ -183,8 +212,12 @@
 
     /** Returns a call log entry without cached values. */
     private Object[] createCallLogEntry() {
+        return createCallLogEntry(TEST_NUMBER_1);
+    }
+
+    private Object[] createCallLogEntry(String testNumber) {
         Object[] values = CallLogQueryTestUtils.createTestValues();
-        values[CallLogQuery.NUMBER] = TEST_NUMBER;
+        values[CallLogQuery.NUMBER] = testNumber;
         values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
         return values;
     }
@@ -212,6 +245,10 @@
         public TestContactInfoCache getContactInfoCache() {
             return (TestContactInfoCache) mContactInfoCache;
         }
+
+        public void showVoicemailPromoCard(boolean show) {
+            mShowVoicemailPromoCard = true;
+        }
     }
 
     private static final class TestContactInfoCache extends ContactInfoCache {
diff --git a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
index 891f068..95558bc 100644
--- a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
@@ -82,7 +82,7 @@
         addCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
         mBuilder.addGroups(mCursor);
         assertEquals(1, mFakeGroupCreator.groups.size());
-        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(0, 3, mFakeGroupCreator.groups.get(0));
     }
 
     public void testAddGroups_MatchingIncomingAndOutgoing() {
@@ -91,13 +91,12 @@
         addCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
         mBuilder.addGroups(mCursor);
         assertEquals(1, mFakeGroupCreator.groups.size());
-        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(0, 3, mFakeGroupCreator.groups.get(0));
     }
 
     public void testAddGroups_Voicemail() {
         // Does not group with other types of calls, include voicemail themselves.
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE);
-        //assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.VOICEMAIL_TYPE);
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.INCOMING_TYPE);
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.OUTGOING_TYPE);
@@ -150,8 +149,8 @@
                 Calls.OUTGOING_TYPE);
         mBuilder.addGroups(mCursor);
         assertEquals(2, mFakeGroupCreator.groups.size());
-        assertGroupIs(1, 4, false, mFakeGroupCreator.groups.get(0));
-        assertGroupIs(8, 3, false, mFakeGroupCreator.groups.get(1));
+        assertGroupIs(1, 4, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(8, 3, mFakeGroupCreator.groups.get(1));
     }
 
     public void testEqualPhoneNumbers() {
@@ -228,7 +227,7 @@
         addMultipleCallLogEntries(TEST_NUMBER1, types);
         mBuilder.addGroups(mCursor);
         assertEquals(1, mFakeGroupCreator.groups.size());
-        assertGroupIs(0, types.length, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(0, types.length, mFakeGroupCreator.groups.get(0));
 
     }
 
@@ -266,10 +265,9 @@
     }
 
     /** Asserts that the group matches the given values. */
-    private void assertGroupIs(int cursorPosition, int size, boolean expanded, GroupSpec group) {
+    private void assertGroupIs(int cursorPosition, int size, GroupSpec group) {
         assertEquals(cursorPosition, group.cursorPosition);
         assertEquals(size, group.size);
-        assertEquals(expanded, group.expanded);
     }
 
     /** Defines an added group. Used by the {@link FakeGroupCreator}. */
@@ -278,13 +276,10 @@
         public final int cursorPosition;
         /** The number of elements in the group. */
         public final int size;
-        /** Whether the group should be initially expanded. */
-        public final boolean expanded;
 
-        public GroupSpec(int cursorPosition, int size, boolean expanded) {
+        public GroupSpec(int cursorPosition, int size) {
             this.cursorPosition = cursorPosition;
             this.size = size;
-            this.expanded = expanded;
         }
     }
 
@@ -294,8 +289,8 @@
         public final List<GroupSpec> groups = newArrayList();
 
         @Override
-        public void addGroup(int cursorPosition, int size, boolean expanded) {
-            groups.add(new GroupSpec(cursorPosition, size, expanded));
+        public void addGroup(int cursorPosition, int size) {
+            groups.add(new GroupSpec(cursorPosition, size));
         }
 
         @Override
diff --git a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
index 53583e0..45bc598 100644
--- a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
+++ b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
@@ -16,10 +16,6 @@
 
 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;
@@ -63,17 +59,12 @@
                 if (TextUtils.equals(value, currentValue)) {
                     groupItemCount++;
                 } else {
-                    if (groupItemCount > 1) {
-                        addGroup(i - groupItemCount, groupItemCount, false);
-                    }
-
+                    addGroup(i - groupItemCount, groupItemCount);
                     groupItemCount = 1;
                     currentValue = value;
                 }
             }
-            if (groupItemCount > 1) {
-                addGroup(count - groupItemCount, groupItemCount, false);
-            }
+            addGroup(count - groupItemCount, groupItemCount);
         }
 
         @Override
@@ -92,7 +83,6 @@
         }
     };
 
-
     private void buildCursor(String... numbers) {
         mCursor = new MatrixCursor(PROJECTION);
         mNextId = 1;
@@ -107,170 +97,51 @@
         mAdapter.changeCursor(mCursor);
 
         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);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 1, "2");
+        assertMetadata(2, 1, "3");
     }
 
-    public void testGroupingWithCollapsedGroupAtTheBeginning() {
+    public void testGroupingWithGroupAtTheBeginning() {
         buildCursor("1", "1", "2");
         mAdapter.changeCursor(mCursor);
 
         assertEquals(2, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+        assertMetadata(0, 2, "1");
+        assertMetadata(1, 1, "2");
     }
 
-    public void testGroupingWithExpandedGroupAtTheBeginning() {
-        buildCursor("1", "1", "2");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(0);
-
-        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);
-        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.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
-    }
-
-    public void testGroupingWithCollapsedGroupInTheMiddle() {
+    public void testGroupingWithGroupInTheMiddle() {
         buildCursor("1", "2", "2", "2", "3");
         mAdapter.changeCursor(mCursor);
 
         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);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 3, "2");
+        assertMetadata(2, 1, "3");
     }
 
-    public void testGroupingWithExpandedGroupInTheMiddle() {
-        buildCursor("1", "2", "2", "2", "3");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(1);
-
-        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);
-        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() {
+    public void testGroupingWithGroupAtTheEnd() {
         buildCursor("1", "2", "3", "3", "3");
         mAdapter.changeCursor(mCursor);
 
         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);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 1, "2");
+        assertMetadata(2, 3, "3");
     }
 
-    public void testGroupingWithExpandedGroupAtTheEnd() {
-        buildCursor("1", "2", "3", "3", "3");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(2);
-
-        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);
-        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() {
+    public void testGroupingWithMultipleGroups() {
         buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
         mAdapter.changeCursor(mCursor);
 
         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);
-        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.getItemCount());
-        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.getItemCount());
-        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.getItemCount());
-        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);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 2, "2");
+        assertMetadata(2, 1, "3");
+        assertMetadata(3, 2, "4");
+        assertMetadata(4, 2, "5");
+        assertMetadata(5, 1, "6");
     }
 
     public void testGroupDescriptorArrayGrowth() {
@@ -287,14 +158,9 @@
         assertEquals(250, mAdapter.getItemCount());
     }
 
-    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);
+    private void assertMetadata(int listPosition, int groupSize, String objectValue) {
+        assertEquals(groupSize, mAdapter.getGroupSize(listPosition));
+        MatrixCursor cursor = (MatrixCursor) mAdapter.getItem(listPosition);
+        assertEquals(objectValue, (String) cursor.getString(GROUPING_COLUMN_INDEX));
     }
 }