Updates grouping behavior for the call log.
We are now using the following policy:
Voicemail and missed calls are grouped with following missed calls (not
voicemail) from the same number.
Incoming and outgoing calls are grouped with following incoming or
outgoing calls from the same number.
Bug: 4968671
Change-Id: I304baca0c02e10e9cbee1c9b01a573e28738fa2a
diff --git a/src/com/android/contacts/calllog/CallLogGroupBuilder.java b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
index 95d85da..f5aef3f 100644
--- a/src/com/android/contacts/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
@@ -16,6 +16,7 @@
package com.android.contacts.calllog;
+import com.android.common.widget.GroupingListAdapter;
import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
import android.database.CharArrayBuffer;
@@ -25,6 +26,8 @@
/**
* Groups together calls in the call log.
+ * <p>
+ * This class is meant to be used in conjunction with {@link GroupingListAdapter}.
*/
public class CallLogGroupBuilder {
/** Reusable char array buffer. */
@@ -32,66 +35,95 @@
/** Reusable char array buffer. */
private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
- private final CallLogFragment.GroupCreator mAdapter;
+ /** The object on which the groups are created. */
+ private final CallLogFragment.GroupCreator mGroupCreator;
- public CallLogGroupBuilder(CallLogFragment.GroupCreator adapter) {
- mAdapter = adapter;
+ public CallLogGroupBuilder(CallLogFragment.GroupCreator groupCreator) {
+ mGroupCreator = groupCreator;
}
+ /**
+ * Finds all groups of adjacent entries in the call log which should be grouped together and
+ * calls {@link CallLogFragment.GroupCreator#addGroup(int, int, boolean)} 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.
+ * <p>
+ * It assumes that the cursor will not change during its execution.
+ *
+ * @see GroupingListAdapter#addGroups(Cursor)
+ */
public void addGroups(Cursor cursor) {
- int count = cursor.getCount();
+ final int count = cursor.getCount();
if (count == 0) {
return;
}
- int groupItemCount = 1;
-
- CharArrayBuffer currentValue = mBuffer1;
- CharArrayBuffer value = mBuffer2;
+ int currentGroupSize = 1;
+ // The number of the first entry in the group.
+ CharArrayBuffer firstNumber = mBuffer1;
+ // The number of the current row in the cursor.
+ CharArrayBuffer currentNumber = mBuffer2;
cursor.moveToFirst();
- cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentValue);
- int currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
- for (int i = 1; i < count; i++) {
- cursor.moveToNext();
- cursor.copyStringToBuffer(CallLogQuery.NUMBER, value);
- boolean sameNumber = equalPhoneNumbers(value, currentValue);
+ cursor.copyStringToBuffer(CallLogQuery.NUMBER, firstNumber);
+ // This is the type of the first call in the group.
+ int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ while (cursor.moveToNext()) {
+ cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentNumber);
+ final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+ final boolean sameNumber = equalPhoneNumbers(firstNumber, currentNumber);
+ final boolean shouldGroup;
- // Group adjacent calls with the same number. Make an exception
- // for the latest item if it was a missed call. We don't want
- // a missed call to be hidden inside a group.
- if (sameNumber && currentCallType != Calls.MISSED_TYPE
- && !CallLogFragment.CallLogQuery.isSectionHeader(cursor)) {
- groupItemCount++;
+ if (CallLogFragment.CallLogQuery.isSectionHeader(cursor)) {
+ // Cannot group headers.
+ shouldGroup = false;
+ } else if (!sameNumber) {
+ // Should only group with calls from the same number.
+ shouldGroup = false;
+ } else if (firstCallType == Calls.VOICEMAIL_TYPE
+ || firstCallType == Calls.MISSED_TYPE) {
+ // Voicemail and missed calls should only be grouped with subsequent missed calls.
+ shouldGroup = callType == Calls.MISSED_TYPE;
} else {
- if (groupItemCount > 1) {
- addGroup(i - groupItemCount, groupItemCount, false);
+ // Incoming and outgoing calls group together.
+ shouldGroup = callType == Calls.INCOMING_TYPE || callType == Calls.OUTGOING_TYPE;
+ }
+
+ if (shouldGroup) {
+ // 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++;
+ } else {
+ // 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);
}
-
- groupItemCount = 1;
-
- // Swap buffers
- CharArrayBuffer temp = currentValue;
- currentValue = value;
- value = temp;
-
- // If we have just examined a row following a missed call, make
- // sure that it is grouped with subsequent calls from the same number
- // even if it was also missed.
- if (sameNumber && currentCallType == Calls.MISSED_TYPE) {
- currentCallType = 0; // "not a missed call"
- } else {
- currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
- }
+ // Start a new group; it will include at least the current call.
+ currentGroupSize = 1;
+ // The current entry is now the first in the group. For the CharArrayBuffers, we
+ // need to swap them.
+ firstCallType = callType;
+ CharArrayBuffer temp = firstNumber; // Used to swap.
+ firstNumber = currentNumber;
+ currentNumber = temp;
}
}
- if (groupItemCount > 1) {
- addGroup(count - groupItemCount, groupItemCount, false);
+ // 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);
}
}
- /** @see CallLogFragment.CallLogAdapter#addGroup(int, int, boolean) */
- private void addGroup(int cursorPosition, int size, boolean expanded) {
- mAdapter.addGroup(cursorPosition, size, expanded);
+ /**
+ * Creates a group of items in the cursor.
+ * <p>
+ * The group is always unexpanded.
+ *
+ * @see CallLogFragment.CallLogAdapter#addGroup(int, int, boolean)
+ */
+ private void addGroup(int cursorPosition, int size) {
+ mGroupCreator.addGroup(cursorPosition, size, false);
}
private boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
diff --git a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
index f8da9c8..9aa5d7b 100644
--- a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
@@ -47,7 +47,7 @@
super.setUp();
mFakeGroupCreator = new FakeGroupCreator();
mBuilder = new CallLogGroupBuilder(mFakeGroupCreator);
- mCursor = new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+ createCursor();
}
@Override
@@ -107,10 +107,111 @@
assertGroupIs(4, 2, false, mFakeGroupCreator.groups.get(1));
}
+ public void testAddGroups_Voicemail() {
+ // Groups with one or more missed calls.
+ assertCallsAreGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE);
+ assertCallsAreGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+ // Does not group with other types of calls, include voicemail themselves.
+ assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.OUTGOING_TYPE);
+ }
+
+ public void testAddGroups_Missed() {
+ // Groups with one or more missed calls.
+ assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+ assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+ // Does not group with other types of calls.
+ assertCallsAreNotGrouped(Calls.MISSED_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.MISSED_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreNotGrouped(Calls.MISSED_TYPE, Calls.OUTGOING_TYPE);
+ }
+
+ public void testAddGroups_Incoming() {
+ // Groups with one or more incoming or outgoing.
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+ // Does not group with voicemail and missed calls.
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.MISSED_TYPE);
+ }
+
+ public void testAddGroups_Outgoing() {
+ // Groups with one or more incoming or outgoing.
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+ assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+ // Does not group with voicemail and missed calls.
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.VOICEMAIL_TYPE);
+ assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.MISSED_TYPE);
+ }
+
+ public void testAddGroups_Mixed() {
+ addMultipleOldCallLogEntries(TEST_NUMBER1,
+ Calls.VOICEMAIL_TYPE, // Stand-alone
+ Calls.INCOMING_TYPE, // Group 1: 1-2
+ Calls.OUTGOING_TYPE,
+ Calls.MISSED_TYPE, // Group 2: 3-4
+ Calls.MISSED_TYPE,
+ Calls.VOICEMAIL_TYPE, // Stand-alone
+ Calls.INCOMING_TYPE, // Stand-alone
+ Calls.VOICEMAIL_TYPE, // Group 3: 7-9
+ Calls.MISSED_TYPE,
+ Calls.MISSED_TYPE,
+ Calls.OUTGOING_TYPE); // Stand-alone
+ mBuilder.addGroups(mCursor);
+ assertEquals(3, mFakeGroupCreator.groups.size());
+ assertGroupIs(1, 2, false, mFakeGroupCreator.groups.get(0));
+ assertGroupIs(3, 2, false, mFakeGroupCreator.groups.get(1));
+ assertGroupIs(7, 3, false, mFakeGroupCreator.groups.get(2));
+ }
+
+ /** Creates (or recreates) the cursor used to store the call log content for the tests. */
+ private void createCursor() {
+ mCursor = new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+ }
+
+ /** Clears the content of the {@link FakeGroupCreator} used in the tests. */
+ private void clearFakeGroupCreator() {
+ mFakeGroupCreator.groups.clear();
+ }
+
+ /** Asserts that calls of the given types are grouped together into a single group. */
+ private void assertCallsAreGrouped(int... types) {
+ createCursor();
+ clearFakeGroupCreator();
+ addMultipleOldCallLogEntries(TEST_NUMBER1, types);
+ mBuilder.addGroups(mCursor);
+ assertEquals(1, mFakeGroupCreator.groups.size());
+ assertGroupIs(0, types.length, false, mFakeGroupCreator.groups.get(0));
+
+ }
+
+ /** Asserts that calls of the given types are not grouped together at all. */
+ private void assertCallsAreNotGrouped(int... types) {
+ createCursor();
+ clearFakeGroupCreator();
+ addMultipleOldCallLogEntries(TEST_NUMBER1, types);
+ mBuilder.addGroups(mCursor);
+ assertEquals(0, mFakeGroupCreator.groups.size());
+ }
+
+ /** Adds a set of calls with the given types, all from the same number, in the old section. */
+ private void addMultipleOldCallLogEntries(String number, int... types) {
+ for (int type : types) {
+ addOldCallLogEntry(number, type);
+ }
+ }
+
+ /** Adds a call with the given number and type to the old section of the call log. */
private void addOldCallLogEntry(String number, int type) {
addCallLogEntry(number, type, CallLogQuery.SECTION_OLD_ITEM);
}
+ /** Adds a call with the given number and type to the new section of the call log. */
private void addNewCallLogEntry(String number, int type) {
addCallLogEntry(number, type, CallLogQuery.SECTION_NEW_ITEM);
}
@@ -127,10 +228,12 @@
});
}
+ /** Adds the old section header to the call log. */
private void addOldCallLogHeader() {
addCallLogHeader(CallLogQuery.SECTION_OLD_HEADER);
}
+ /** Adds the new section header to the call log. */
private void addNewCallLogHeader() {
addCallLogHeader(CallLogQuery.SECTION_NEW_HEADER);
}
@@ -152,9 +255,13 @@
assertEquals(expanded, group.expanded);
}
+ /** Defines an added group. Used by the {@link FakeGroupCreator}. */
private static class GroupSpec {
+ /** The starting position of the group. */
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) {
@@ -164,8 +271,11 @@
}
}
+ /** Fake implementation of a GroupCreator which stores the created groups in a member field. */
private static class FakeGroupCreator implements CallLogFragment.GroupCreator {
+ /** The list of created groups. */
public final List<GroupSpec> groups = newArrayList();
+
@Override
public void addGroup(int cursorPosition, int size, boolean expanded) {
groups.add(new GroupSpec(cursorPosition, size, expanded));