From 0599e880630892becf1a1991e6712e0e6d9df3c8 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 24 Aug 2015 17:56:17 -0700 Subject: 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 --- src/com/android/dialer/calllog/CallLogAdapter.java | 12 +- .../dialer/calllog/CallLogGroupBuilder.java | 136 ++++---- .../dialer/calllog/GroupingListAdapter.java | 362 +++------------------ .../dialer/calllog/PromoCardViewHolder.java | 14 +- .../android/dialer/calllog/CallLogAdapterTest.java | 53 ++- .../dialer/calllog/CallLogGroupBuilderTest.java | 23 +- .../dialer/calllog/GroupingListAdapterTests.java | 188 ++--------- 7 files changed, 197 insertions(+), 591 deletions(-) diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index aa307b6c6..4593e5e4d 100644 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -131,7 +131,7 @@ public class CallLogAdapter extends GroupingListAdapter 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 @@ public class CallLogAdapter extends GroupingListAdapter ? 1 : 0)); } + @Override + public int getGroupSize(int position) { + return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0)); + } + protected boolean isCallLogActivity() { return mIsCallLogActivity; } @@ -794,11 +799,6 @@ public class CallLogAdapter extends GroupingListAdapter 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 0826aeb4a..4cf2d07cd 100644 --- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java +++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java @@ -20,6 +20,7 @@ import android.database.Cursor; 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 @@ public class CallLogGroupBuilder { * 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 @@ public class CallLogGroupBuilder { /** * 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. *

* For entries that are not grouped with others, we do not need to create a group of size one. @@ -114,98 +114,70 @@ public class CallLogGroupBuilder { // 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); - 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); - - 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 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; - } + // 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; - if (shouldGroup) { + while (cursor.moveToNext()) { + // 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 isSameNumber = equalNumbers(groupNumber, number); + final boolean isSameAccount = isSameAccount( + groupAccountComponentName, accountComponentName, groupAccountId, accountId); + + // 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); - - // 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); - } + // 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 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); - } - // 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); + final long currentCallId = cursor.getLong(CallLogQuery.ID); + mGroupCreator.setDayGroup(currentCallId, groupDayGroup); } - } - /** - * Creates a group of items in the cursor. - *

- * 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 @@ public class CallLogGroupBuilder { } } + 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 8d3ab4545..54dd5f679 100644 --- a/src/com/android/dialer/calllog/GroupingListAdapter.java +++ b/src/com/android/dialer/calllog/GroupingListAdapter.java @@ -22,78 +22,28 @@ import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.util.SparseIntArray; -import android.view.View; -import android.view.ViewGroup; -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. - */ - 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 + * 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 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 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter { }; protected DataSetObserver mDataSetObserver = new DataSetObserver() { - @Override public void onChanged() { notifyDataSetChanged(); @@ -115,7 +64,7 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter { public GroupingListAdapter(Context context) { mContext = context; - resetCache(); + reset(); } /** @@ -126,18 +75,6 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter { 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 @@ abstract class GroupingListAdapter extends RecyclerView.Adapter { 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. - */ - 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. + * 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; + 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); } - 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 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 mItemCount; } /** - * 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 size of 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; + public int getGroupSize(int listPosition) { + if (listPosition >= mGroupMetadata.size()) { + return 0; } - // 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; + return mGroupMetadata.valueAt(listPosition); } /** - * Given a position of a groups header in the list, returns the size of - * the corresponding group. + * Given the position of a list item, returns the the first item in the group of items + * corresponding to that position. */ - 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 4c9602759..656b66938 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 class PromoCardViewHolder extends RecyclerView.ViewHolder { 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 b4162e166..2bdc1972b 100644 --- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java +++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java @@ -35,7 +35,9 @@ import java.util.List; */ @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 @@ public class CallLogAdapterTest extends AndroidTestCase { private MatrixCursor mCursor; private View mView; - private ViewHolder mViewHolder; + private CallLogListItemViewHolder mViewHolder; @Override protected void setUp() throws Exception { @@ -98,7 +100,7 @@ public class CallLogAdapterTest extends AndroidTestCase { 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 class CallLogAdapterTest extends AndroidTestCase { 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 class CallLogAdapterTest extends AndroidTestCase { 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 @@ public class CallLogAdapterTest extends AndroidTestCase { // 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 @@ public class CallLogAdapterTest extends AndroidTestCase { 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 @@ public class CallLogAdapterTest extends AndroidTestCase { /** 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 class CallLogAdapterTest extends AndroidTestCase { 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 891f0686f..95558bcf6 100644 --- a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java +++ b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java @@ -82,7 +82,7 @@ public class CallLogGroupBuilderTest extends AndroidTestCase { 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 @@ public class CallLogGroupBuilderTest extends AndroidTestCase { 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 @@ public class CallLogGroupBuilderTest extends AndroidTestCase { 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 @@ public class CallLogGroupBuilderTest extends AndroidTestCase { 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 @@ public class CallLogGroupBuilderTest extends AndroidTestCase { } /** 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 class CallLogGroupBuilderTest extends AndroidTestCase { 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 class CallLogGroupBuilderTest extends AndroidTestCase { public final List 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 53583e0a7..45bc59812 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 @@ public class GroupingListAdapterTests extends AndroidTestCase { 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 @@ public class GroupingListAdapterTests extends AndroidTestCase { } }; - private void buildCursor(String... numbers) { mCursor = new MatrixCursor(PROJECTION); mNextId = 1; @@ -107,170 +97,51 @@ public class GroupingListAdapterTests extends AndroidTestCase { 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); - } - - 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); + assertMetadata(0, 2, "1"); + assertMetadata(1, 1, "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() { - 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() { + public void testGroupingWithMultipleGroups() { 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 @@ public class GroupingListAdapterTests extends AndroidTestCase { 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)); } } -- cgit v1.2.3