Adding support for "collapsibility" to the call log
Please note the optimizations for handling long call logs.
There is a change from the previous patch set: we now
break out from a group a bounced call if it is the latest
call in the group.
Bug: 2325659
Change-Id: I92e7dc25c2240f15f174391bf7b955c2596dbe30
diff --git a/res/drawable/call_background_secondary.xml b/res/drawable/call_background_secondary.xml
new file mode 100644
index 0000000..b784862
--- /dev/null
+++ b/res/drawable/call_background_secondary.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_window_focused="false"
+ android:drawable="@android:color/transparent" />
+ <item android:state_focused="false" android:state_pressed="true"
+ android:drawable="@*android:drawable/list_selector_background_transition" />
+ <item android:state_focused="false" android:state_pressed="false"
+ android:drawable="@color/background_secondary"/>
+
+</selector>
diff --git a/res/drawable/list_item_background_secondary.xml b/res/drawable/list_item_background_secondary.xml
new file mode 100644
index 0000000..0a27206
--- /dev/null
+++ b/res/drawable/list_item_background_secondary.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:drawable="@android:color/transparent"/>
+ <item android:state_selected="true" android:drawable="@android:color/transparent"/>
+ <item android:drawable="@color/background_secondary"/> <!-- not selected -->
+</selector>
diff --git a/res/layout-finger/recent_calls_list_child_item.xml b/res/layout-finger/recent_calls_list_child_item.xml
new file mode 100644
index 0000000..14eb24d
--- /dev/null
+++ b/res/layout-finger/recent_calls_list_child_item.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:paddingLeft="7dip"
+ android:background="@drawable/list_item_background_secondary"
+>
+
+ <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="14dip"
+ android:paddingRight="14dip"
+ android:layout_alignParentRight="true"
+
+ android:gravity="center_vertical"
+ android:src="@android:drawable/sym_action_call"
+ android:background="@drawable/call_background_secondary"
+ />
+
+ <include layout="@layout/recent_calls_list_item_layout"/>
+
+</RelativeLayout>
diff --git a/res/layout-finger/recent_calls_list_group_item.xml b/res/layout-finger/recent_calls_list_group_item.xml
new file mode 100644
index 0000000..acaecaa
--- /dev/null
+++ b/res/layout-finger/recent_calls_list_group_item.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:paddingLeft="7dip"
+>
+
+ <com.android.contacts.ui.widget.DontPressWithParentImageView android:id="@+id/call_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="14dip"
+ android:paddingRight="14dip"
+ android:layout_alignParentRight="true"
+
+ android:gravity="center_vertical"
+ android:src="@android:drawable/sym_action_call"
+ android:background="@drawable/call_background"
+ />
+
+ <View android:id="@+id/divider"
+ android:layout_width="1px"
+ android:layout_height="match_parent"
+ android:layout_marginTop="5dip"
+ android:layout_marginBottom="5dip"
+ android:layout_toLeftOf="@id/call_icon"
+ android:layout_marginLeft="11dip"
+ android:background="@drawable/divider_vertical_dark"
+ />
+
+ <TextView android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@id/divider"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="8dip"
+
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ />
+
+ <TextView android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:layout_marginLeft="36dip"
+ android:layout_alignBaseline="@id/date"
+
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textStyle="bold"
+ />
+
+ <TextView android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="5dip"
+ android:layout_toRightOf="@id/label"
+ android:layout_toLeftOf="@id/date"
+ android:layout_alignBaseline="@id/label"
+ android:layout_alignWithParentIfMissing="true"
+
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ />
+
+ <TextView android:id="@+id/groupSize"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/divider"
+ android:layout_above="@id/label"
+ android:layout_alignWithParentIfMissing="true"
+
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:singleLine="true"
+ android:gravity="center_vertical"
+ />
+
+ <TextView android:id="@+id/line1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@+id/groupSize"
+ android:layout_above="@id/date"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_marginLeft="36dip"
+ android:layout_marginBottom="-10dip"
+
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:gravity="center_vertical"
+ />
+
+ <ImageView
+ android:id="@+id/groupIndicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:src="@*android:drawable/expander_ic_minimized"
+ android:gravity="center_vertical"
+ />
+</RelativeLayout>
diff --git a/res/layout-finger/recent_calls_list_item.xml b/res/layout-finger/recent_calls_list_item.xml
index 00691c9..8efa23c 100644
--- a/res/layout-finger/recent_calls_list_item.xml
+++ b/res/layout-finger/recent_calls_list_item.xml
@@ -32,75 +32,6 @@
android:background="@drawable/call_background"
/>
- <View android:id="@+id/divider"
- android:layout_width="1px"
- android:layout_height="match_parent"
- android:layout_marginTop="5dip"
- android:layout_marginBottom="5dip"
- android:layout_toLeftOf="@id/call_icon"
- android:layout_marginLeft="11dip"
- android:background="@drawable/divider_vertical_dark"
- />
-
- <ImageView android:id="@+id/call_type_icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_toLeftOf="@id/divider"
- />
-
- <TextView android:id="@+id/date"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_toLeftOf="@id/divider"
- android:layout_alignParentBottom="true"
- android:layout_marginBottom="8dip"
-
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:singleLine="true"
- />
-
- <TextView android:id="@+id/label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignParentBottom="true"
- android:layout_marginBottom="8dip"
- android:layout_marginTop="-10dip"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textStyle="bold"
- />
-
- <TextView android:id="@+id/number"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="5dip"
- android:layout_toRightOf="@id/label"
- android:layout_toLeftOf="@id/date"
- android:layout_alignBaseline="@id/label"
- android:layout_alignWithParentIfMissing="true"
-
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceSmall"
- />
-
- <TextView android:id="@+id/line1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignParentTop="true"
- android:layout_toLeftOf="@+id/call_type_icon"
- android:layout_above="@id/label"
- android:layout_alignWithParentIfMissing="true"
-
- android:textAppearance="?android:attr/textAppearanceLarge"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:gravity="center_vertical"
- />
+ <include layout="@layout/recent_calls_list_item_layout"/>
</RelativeLayout>
diff --git a/res/layout-finger/recent_calls_list_item_layout.xml b/res/layout-finger/recent_calls_list_item_layout.xml
new file mode 100644
index 0000000..29ec59e
--- /dev/null
+++ b/res/layout-finger/recent_calls_list_item_layout.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <View android:id="@+id/divider"
+ android:layout_width="1px"
+ android:layout_height="match_parent"
+ android:layout_marginTop="5dip"
+ android:layout_marginBottom="5dip"
+ android:layout_toLeftOf="@id/call_icon"
+ android:layout_marginLeft="11dip"
+ android:background="@drawable/divider_vertical_dark"
+ />
+
+ <ImageView android:id="@+id/call_type_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="4dip"
+ />
+
+ <TextView android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@id/divider"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="8dip"
+
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ />
+
+ <TextView android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:layout_marginLeft="36dip"
+ android:layout_alignBaseline="@id/date"
+
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textStyle="bold"
+ />
+
+ <TextView android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="5dip"
+ android:layout_toRightOf="@id/label"
+ android:layout_toLeftOf="@id/date"
+ android:layout_alignBaseline="@id/label"
+ android:layout_alignWithParentIfMissing="true"
+
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ />
+
+ <TextView android:id="@+id/line1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/divider"
+ android:layout_above="@id/date"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_marginLeft="36dip"
+ android:layout_marginBottom="-10dip"
+
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:gravity="center_vertical"
+ />
+</merge>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 69ab865..3ba7106 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -22,4 +22,5 @@
<color name="quickcontact_disambig_divider">#afafaf</color>
<color name="edit_divider">#ff666666</color>
+ <color name="background_secondary">#ff202020</color>
</resources>
diff --git a/src/com/android/contacts/GroupingListAdapter.java b/src/com/android/contacts/GroupingListAdapter.java
new file mode 100644
index 0000000..885007f
--- /dev/null
+++ b/src/com/android/contacts/GroupingListAdapter.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.internal.util.ArrayUtils;
+
+import android.content.Context;
+import android.database.CharArrayBuffer;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * Maintains a list that groups adjacent items sharing the same value of
+ * a "group-by" field. The list has three types of elements: stand-alone, group header and group
+ * child. Groups are collapsible and collapsed by default.
+ */
+public abstract class GroupingListAdapter extends BaseAdapter {
+
+ private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
+ private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
+ private static final long GROUP_OFFSET_MASK = 0x00000000FFFFFFFFL;
+ private static final long GROUP_SIZE_MASK = 0x7FFFFFFF00000000L;
+ private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;
+
+ public static final int ITEM_TYPE_STANDALONE = 0;
+ public static final int ITEM_TYPE_GROUP_HEADER = 1;
+ public static final int ITEM_TYPE_IN_GROUP = 2;
+
+ /**
+ * Information about a specific list item: is it a group, if so is it expanded.
+ * Otherwise, is it a stand-alone item or a group member.
+ */
+ protected static class PositionMetadata {
+ int itemType;
+ boolean isExpanded;
+ int cursorPosition;
+ int childCount;
+ private int groupPosition;
+ private int listPosition = -1;
+ }
+
+ private Context mContext;
+ private Cursor mCursor;
+
+ /**
+ * Count of list items.
+ */
+ private int mCount;
+
+ private int mRowIdColumnIndex;
+
+ /**
+ * Count of groups in the list.
+ */
+ private int mGroupCount;
+
+ /**
+ * Information about where these groups are located in the list, how large they are
+ * and whether they are expanded.
+ */
+ private long[] mGroupMetadata;
+
+ private SparseIntArray mPositionCache = new SparseIntArray();
+ private int mLastCachedListPosition;
+ private int mLastCachedCursorPosition;
+ private int mLastCachedGroup;
+
+ /**
+ * A reusable temporary instance of PositionMetadata
+ */
+ private PositionMetadata mPositionMetadata = new PositionMetadata();
+
+ protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ onContentChanged();
+ }
+ };
+
+ protected DataSetObserver mDataSetObserver = new DataSetObserver() {
+
+ @Override
+ public void onChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ notifyDataSetInvalidated();
+ }
+ };
+
+ public GroupingListAdapter(Context context) {
+ mContext = context;
+ resetCache();
+ }
+
+ /**
+ * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for
+ * each of them.
+ */
+ protected abstract void addGroups(Cursor cursor);
+
+ protected abstract View newStandAloneView(Context context, ViewGroup parent);
+ protected abstract void bindStandAloneView(View view, Context context, Cursor cursor);
+
+ protected abstract View newGroupView(Context context, ViewGroup parent);
+ protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded);
+
+ protected abstract View newChildView(Context context, ViewGroup parent);
+ protected abstract void bindChildView(View view, Context context, Cursor cursor);
+
+ /**
+ * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
+ */
+ private void resetCache() {
+ mCount = -1;
+ mLastCachedListPosition = -1;
+ mLastCachedCursorPosition = -1;
+ mLastCachedGroup = -1;
+ mPositionMetadata.listPosition = -1;
+ mPositionCache.clear();
+ }
+
+ protected void onContentChanged() {
+ }
+
+ public void changeCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return;
+ }
+
+ if (mCursor != null) {
+ mCursor.unregisterContentObserver(mChangeObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mCursor.close();
+ }
+ mCursor = cursor;
+ if (cursor != null) {
+ cursor.registerContentObserver(mChangeObserver);
+ cursor.registerDataSetObserver(mDataSetObserver);
+ mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
+ notifyDataSetChanged();
+ } else {
+ // notify the observers about the lack of a data set
+ notifyDataSetInvalidated();
+ }
+
+ resetCache();
+ findGroups();
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ /**
+ * Scans over the entire cursor looking for duplicate phone numbers that need
+ * to be collapsed.
+ */
+ private void findGroups() {
+ mGroupCount = 0;
+ mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
+
+ if (mCursor == null) {
+ return;
+ }
+
+ addGroups(mCursor);
+ }
+
+ /**
+ * Records information about grouping in the list. Should be called by the overridden
+ * {@link #addGroups} method.
+ */
+ protected void addGroup(int cursorPosition, int size, boolean expanded) {
+ if (mGroupCount >= mGroupMetadata.length) {
+ int newSize = ArrayUtils.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;
+ }
+
+ public int getCount() {
+ if (mCursor == null) {
+ return 0;
+ }
+
+ if (mCount != -1) {
+ return mCount;
+ }
+
+ int cursorPosition = 0;
+ int count = 0;
+ for (int i = 0; i < mGroupCount; i++) {
+ long metadata = mGroupMetadata[i];
+ int offset = (int)(metadata & GROUP_OFFSET_MASK);
+ boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
+ int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);
+
+ count += (offset - cursorPosition);
+
+ if (expanded) {
+ count += size + 1;
+ } else {
+ count++;
+ }
+
+ cursorPosition = offset + size;
+ }
+
+ mCount = count + mCursor.getCount() - cursorPosition;
+ return mCount;
+ }
+
+ /**
+ * Figures out whether the item at the specified position represents a
+ * stand-alone element, a group or a group child. Also computes the
+ * corresponding cursor position.
+ */
+ public void obtainPositionMetadata(PositionMetadata metadata, int position) {
+
+ // If the description object already contains requested information, just return
+ if (metadata.listPosition == position) {
+ return;
+ }
+
+ int listPosition = 0;
+ int cursorPosition = 0;
+ int firstGroupToCheck = 0;
+
+ // Check cache for the supplied position. What we are looking for is
+ // the group descriptor immediately preceding the supplied position.
+ // Once we have that, we will be able to tell whether the position
+ // is the header of the group, a member of the group or a standalone item.
+ if (mLastCachedListPosition != -1) {
+ if (position <= mLastCachedListPosition) {
+
+ // Have SparceIntArray do a binary search for us.
+ int index = mPositionCache.indexOfKey(position);
+
+ // If we get back a positive number, the position corresponds to
+ // a group header.
+ if (index < 0) {
+
+ // We had a cache miss, but we did obtain valuable information anyway.
+ // The negative number will allow us to compute the location of
+ // the group header immediately preceding the supplied position.
+ index = ~index - 1;
+
+ if (index >= mPositionCache.size()) {
+ index--;
+ }
+ }
+
+ // A non-negative index gives us the position of the group header
+ // corresponding or preceding the position, so we can
+ // search for the group information at the supplied position
+ // starting with the cached group we just found
+ if (index >= 0) {
+ listPosition = mPositionCache.keyAt(index);
+ firstGroupToCheck = mPositionCache.valueAt(index);
+ long descriptor = mGroupMetadata[firstGroupToCheck];
+ cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
+ }
+ } else {
+
+ // If we haven't examined groups beyond the supplied position,
+ // we will start where we left off previously
+ firstGroupToCheck = mLastCachedGroup;
+ listPosition = mLastCachedListPosition;
+ cursorPosition = mLastCachedCursorPosition;
+ }
+ }
+
+ for (int i = firstGroupToCheck; i < mGroupCount; i++) {
+ long group = mGroupMetadata[i];
+ int offset = (int)(group & GROUP_OFFSET_MASK);
+
+ // Move pointers to the beginning of the group
+ listPosition += (offset - cursorPosition);
+ cursorPosition = offset;
+
+ if (i > mLastCachedGroup) {
+ mPositionCache.append(listPosition, i);
+ mLastCachedListPosition = listPosition;
+ mLastCachedCursorPosition = cursorPosition;
+ mLastCachedGroup = i;
+ }
+
+ // Now we have several possibilities:
+ // A) The requested position precedes the group
+ if (position < listPosition) {
+ metadata.itemType = ITEM_TYPE_STANDALONE;
+ metadata.cursorPosition = cursorPosition - (listPosition - position);
+ return;
+ }
+
+ boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
+ int size = (int) ((group & GROUP_SIZE_MASK) >> 32);
+
+ // B) The requested position is a group header
+ if (position == listPosition) {
+ metadata.itemType = ITEM_TYPE_GROUP_HEADER;
+ metadata.groupPosition = i;
+ metadata.isExpanded = expanded;
+ metadata.childCount = size;
+ metadata.cursorPosition = offset;
+ return;
+ }
+
+ if (expanded) {
+ // C) The requested position is an element in the expanded group
+ if (position < listPosition + size + 1) {
+ metadata.itemType = ITEM_TYPE_IN_GROUP;
+ metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
+ return;
+ }
+
+ // D) The element is past the expanded group
+ listPosition += size + 1;
+ } else {
+
+ // E) The element is past the collapsed group
+ listPosition++;
+ }
+
+ // Move cursor past the group
+ cursorPosition += size;
+ }
+
+ // The required item is past the last group
+ metadata.itemType = ITEM_TYPE_STANDALONE;
+ metadata.cursorPosition = cursorPosition + (position - listPosition);
+ }
+
+ /**
+ * Returns true if the specified position in the list corresponds to a
+ * group header.
+ */
+ public boolean isGroupHeader(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
+ }
+
+ /**
+ * Given a position of a groups header in the list, returns the size of
+ * the corresponding group.
+ */
+ public int getGroupSize(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ return mPositionMetadata.childCount;
+ }
+
+ /**
+ * Mark group as expanded if it is collapsed and vice versa.
+ */
+ public void toggleGroup(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
+ throw new IllegalArgumentException("Not a group at position " + position);
+ }
+
+
+ if (mPositionMetadata.isExpanded) {
+ mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
+ } else {
+ mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
+ }
+ resetCache();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 3;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ return mPositionMetadata.itemType;
+ }
+
+ public Object getItem(int position) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ mCursor.moveToPosition(mPositionMetadata.cursorPosition);
+ return mCursor;
+ }
+
+ public long getItemId(int position) {
+ getItem(position);
+ return mCursor.getLong(mRowIdColumnIndex);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ obtainPositionMetadata(mPositionMetadata, position);
+ View view = convertView;
+ if (view == null) {
+ switch (mPositionMetadata.itemType) {
+ case ITEM_TYPE_STANDALONE:
+ view = newStandAloneView(mContext, parent);
+ break;
+ case ITEM_TYPE_GROUP_HEADER:
+ view = newGroupView(mContext, parent);
+ break;
+ case ITEM_TYPE_IN_GROUP:
+ view = newChildView(mContext, parent);
+ break;
+ }
+ }
+
+ mCursor.moveToPosition(mPositionMetadata.cursorPosition);
+ switch (mPositionMetadata.itemType) {
+ case ITEM_TYPE_STANDALONE:
+ bindStandAloneView(view, mContext, mCursor);
+ break;
+ case ITEM_TYPE_GROUP_HEADER:
+ bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount,
+ mPositionMetadata.isExpanded);
+ break;
+ case ITEM_TYPE_IN_GROUP:
+ bindChildView(view, mContext, mCursor);
+ break;
+
+ }
+ return view;
+ }
+}
diff --git a/src/com/android/contacts/RecentCallsListActivity.java b/src/com/android/contacts/RecentCallsListActivity.java
index 547cd45..8e553d1 100644
--- a/src/com/android/contacts/RecentCallsListActivity.java
+++ b/src/com/android/contacts/RecentCallsListActivity.java
@@ -16,6 +16,9 @@
package com.android.contacts;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.ITelephony;
+
import android.app.ListActivity;
import android.content.ActivityNotFoundException;
import android.content.AsyncQueryHandler;
@@ -23,9 +26,11 @@
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.database.CharArrayBuffer;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteFullException;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -38,10 +43,10 @@
import android.os.SystemClock;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.Intents.Insert;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.Contacts.Intents.Insert;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.SpannableStringBuilder;
@@ -50,6 +55,7 @@
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -60,16 +66,12 @@
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
-import android.widget.ResourceCursorAdapter;
import android.widget.TextView;
-import com.android.internal.telephony.CallerInfo;
-import com.android.internal.telephony.ITelephony;
-
+import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Locale;
-import java.lang.ref.WeakReference;
/**
* Displays a list of call log entries.
@@ -143,6 +145,8 @@
TextView dateView;
ImageView iconView;
View callView;
+ ImageView groupIndicator;
+ TextView groupSize;
}
static final class CallerInfoQuery {
@@ -171,7 +175,7 @@
private static int sFormattingType = FORMATTING_TYPE_INVALID;
/** Adapter class to fill in data for the Call Log */
- final class RecentCallsAdapter extends ResourceCursorAdapter
+ final class RecentCallsAdapter extends GroupingListAdapter
implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener {
HashMap<String,ContactInfo> mContactInfo;
private final LinkedList<CallerInfoQuery> mRequests;
@@ -189,6 +193,12 @@
private Drawable mDrawableOutgoing;
private Drawable mDrawableMissed;
+ /**
+ * Reusable char array buffers.
+ */
+ private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
+ private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
+
public void onClick(View view) {
String number = (String) view.getTag();
if (!TextUtils.isEmpty(number)) {
@@ -220,7 +230,7 @@
};
public RecentCallsAdapter() {
- super(RecentCallsListActivity.this, R.layout.recent_calls_list_item, null);
+ super(RecentCallsListActivity.this);
mContactInfo = new HashMap<String,ContactInfo>();
mRequests = new LinkedList<CallerInfoQuery>();
@@ -388,8 +398,116 @@
}
@Override
- public View newView(Context context, Cursor cursor, ViewGroup parent) {
- View view = super.newView(context, cursor, parent);
+ protected void addGroups(Cursor cursor) {
+
+ int count = cursor.getCount();
+ if (count == 0) {
+ return;
+ }
+
+ int groupItemCount = 1;
+
+ CharArrayBuffer currentValue = mBuffer1;
+ CharArrayBuffer value = mBuffer2;
+ cursor.moveToFirst();
+ cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, currentValue);
+ int currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX);
+ for (int i = 1; i < count; i++) {
+ cursor.moveToNext();
+ cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, value);
+ boolean sameNumber = equalPhoneNumbers(value, currentValue);
+
+ // 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) {
+ groupItemCount++;
+ } else {
+ if (groupItemCount > 1) {
+ addGroup(i - groupItemCount, groupItemCount, false);
+ }
+
+ 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(CALL_TYPE_COLUMN_INDEX);
+ }
+ }
+ }
+ if (groupItemCount > 1) {
+ addGroup(count - groupItemCount, groupItemCount, false);
+ }
+ }
+
+ protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
+
+ // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid
+ // string allocation
+ return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied),
+ new String(buffer2.data, 0, buffer2.sizeCopied));
+ }
+
+
+ @Override
+ protected View newStandAloneView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.recent_calls_list_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindStandAloneView(View view, Context context, Cursor cursor) {
+ bindView(context, view, cursor);
+ }
+
+ @Override
+ protected View newChildView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.recent_calls_list_child_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor) {
+ bindView(context, view, cursor);
+ }
+
+ @Override
+ protected View newGroupView(Context context, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.recent_calls_list_group_item, parent, false);
+ findAndCacheViews(view);
+ return view;
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded) {
+ final RecentCallsListItemViews views = (RecentCallsListItemViews) view.getTag();
+ int groupIndicator = expanded
+ ? com.android.internal.R.drawable.expander_ic_maximized
+ : com.android.internal.R.drawable.expander_ic_minimized;
+ views.groupIndicator.setImageResource(groupIndicator);
+ views.groupSize.setText("(" + groupSize + ")");
+ bindView(context, view, cursor);
+ }
+
+ private void findAndCacheViews(View view) {
// Get the views to bind to
RecentCallsListItemViews views = new RecentCallsListItemViews();
@@ -400,15 +518,12 @@
views.iconView = (ImageView) view.findViewById(R.id.call_type_icon);
views.callView = view.findViewById(R.id.call_icon);
views.callView.setOnClickListener(this);
-
+ views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator);
+ views.groupSize = (TextView) view.findViewById(R.id.groupSize);
view.setTag(views);
-
- return view;
}
-
- @Override
- public void bindView(View view, Context context, Cursor c) {
+ public void bindView(Context context, View view, Cursor c) {
final RecentCallsListItemViews views = (RecentCallsListItemViews) view.getTag();
String number = c.getString(NUMBER_COLUMN_INDEX);
@@ -501,7 +616,6 @@
views.labelView.setVisibility(View.GONE);
}
- int type = c.getInt(CALL_TYPE_COLUMN_INDEX);
long date = c.getLong(DATE_COLUMN_INDEX);
// Set the date/time field by mixing relative and absolute times.
@@ -510,19 +624,22 @@
views.dateView.setText(DateUtils.getRelativeTimeSpanString(date,
System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags));
- // Set the icon
- switch (type) {
- case Calls.INCOMING_TYPE:
- views.iconView.setImageDrawable(mDrawableIncoming);
- break;
+ if (views.iconView != null) {
+ int type = c.getInt(CALL_TYPE_COLUMN_INDEX);
+ // Set the icon
+ switch (type) {
+ case Calls.INCOMING_TYPE:
+ views.iconView.setImageDrawable(mDrawableIncoming);
+ break;
- case Calls.OUTGOING_TYPE:
- views.iconView.setImageDrawable(mDrawableOutgoing);
- break;
+ case Calls.OUTGOING_TYPE:
+ views.iconView.setImageDrawable(mDrawableOutgoing);
+ break;
- case Calls.MISSED_TYPE:
- views.iconView.setImageDrawable(mDrawableMissed);
- break;
+ case Calls.MISSED_TYPE:
+ views.iconView.setImageDrawable(mDrawableMissed);
+ break;
+ }
}
// Listen for the first draw
@@ -819,12 +936,24 @@
switch (item.getItemId()) {
case MENU_ITEM_DELETE: {
- Cursor cursor = mAdapter.getCursor();
- if (cursor != null) {
- cursor.moveToPosition(menuInfo.position);
- cursor.deleteRow();
+ Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position);
+ int groupSize = 1;
+ if (mAdapter.isGroupHeader(menuInfo.position)) {
+ groupSize = mAdapter.getGroupSize(menuInfo.position);
}
- return true;
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < groupSize; i++) {
+ if (i != 0) {
+ sb.append(",");
+ cursor.moveToNext();
+ }
+ long id = cursor.getLong(ID_COLUMN_INDEX);
+ sb.append(id);
+ }
+
+ getContentResolver().delete(Calls.CONTENT_URI, Calls._ID + " IN (" + sb + ")",
+ null);
}
}
return super.onContextItemSelected(item);
@@ -946,8 +1075,12 @@
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
- Intent intent = new Intent(this, CallDetailActivity.class);
- intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id));
- startActivity(intent);
+ if (mAdapter.isGroupHeader(position)) {
+ mAdapter.toggleGroup(position);
+ } else {
+ Intent intent = new Intent(this, CallDetailActivity.class);
+ intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id));
+ startActivity(intent);
+ }
}
}
diff --git a/tests/src/com/android/contacts/GroupingListAdapterTests.java b/tests/src/com/android/contacts/GroupingListAdapterTests.java
new file mode 100644
index 0000000..1877fac
--- /dev/null
+++ b/tests/src/com/android/contacts/GroupingListAdapterTests.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import android.content.Context;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.CallLog.Calls;
+import android.test.AndroidTestCase;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+import static com.android.contacts.GroupingListAdapter.ITEM_TYPE_STANDALONE;
+import static com.android.contacts.GroupingListAdapter.ITEM_TYPE_IN_GROUP;
+import static com.android.contacts.GroupingListAdapter.ITEM_TYPE_GROUP_HEADER;
+
+/**
+ * Tests for the contact call list adapter.
+ *
+ * Running all tests:
+ *
+ * runtest contacts
+ * or
+ * adb shell am instrument \
+ * -w com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+public class GroupingListAdapterTests extends AndroidTestCase {
+
+ static private final String[] CALL_LOG_PROJECTION = new String[] {
+ Calls._ID,
+ Calls.NUMBER,
+ Calls.DATE,
+ };
+
+ private static final int CALLS_NUMBER_COLUMN_INDEX = 1;
+
+ private MatrixCursor mCursor;
+ private long mNextCall;
+
+ private GroupingListAdapter mAdapter = new GroupingListAdapter(null) {
+
+ @Override
+ protected void addGroups(Cursor cursor) {
+ int count = cursor.getCount();
+ int groupItemCount = 1;
+ cursor.moveToFirst();
+ String currentValue = cursor.getString(CALLS_NUMBER_COLUMN_INDEX);
+ for (int i = 1; i < count; i++) {
+ cursor.moveToNext();
+ String value = cursor.getString(CALLS_NUMBER_COLUMN_INDEX);
+ if (TextUtils.equals(value, currentValue)) {
+ groupItemCount++;
+ } else {
+ if (groupItemCount > 1) {
+ addGroup(i - groupItemCount, groupItemCount, false);
+ }
+
+ groupItemCount = 1;
+ currentValue = value;
+ }
+ }
+ if (groupItemCount > 1) {
+ addGroup(count - groupItemCount, groupItemCount, false);
+ }
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor) {
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded) {
+ }
+
+ @Override
+ protected void bindStandAloneView(View view, Context context, Cursor cursor) {
+ }
+
+ @Override
+ protected View newChildView(Context context, ViewGroup parent) {
+ return null;
+ }
+
+ @Override
+ protected View newGroupView(Context context, ViewGroup parent) {
+ return null;
+ }
+
+ @Override
+ protected View newStandAloneView(Context context, ViewGroup parent) {
+ return null;
+ }
+ };
+
+ private void buildCursor(String... numbers) {
+ mCursor = new MatrixCursor(CALL_LOG_PROJECTION);
+ mNextCall = 1;
+ for (String number : numbers) {
+ mCursor.addRow(new Object[]{mNextCall, number, 1000 - mNextCall});
+ mNextCall++;
+ }
+ }
+
+ public void testGroupingWithoutGroups() {
+ buildCursor("1", "2", "3");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(3, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithCollapsedGroupAtTheBeginning() {
+ buildCursor("1", "1", "2");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(2, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithExpandedGroupAtTheBeginning() {
+ buildCursor("1", "1", "2");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(0);
+
+ assertEquals(4, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, true, 0);
+ assertPositionMetadata(1, ITEM_TYPE_IN_GROUP, false, 0);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithExpandCollapseCycleAtTheBeginning() {
+ buildCursor("1", "1", "2");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(0);
+ mAdapter.toggleGroup(0);
+
+ assertEquals(2, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+ }
+
+ public void testGroupingWithCollapsedGroupInTheMiddle() {
+ buildCursor("1", "2", "2", "2", "3");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(3, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 4);
+ }
+
+ public void testGroupingWithExpandedGroupInTheMiddle() {
+ buildCursor("1", "2", "2", "2", "3");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(1);
+
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 4);
+ }
+
+ public void testGroupingWithCollapsedGroupAtTheEnd() {
+ buildCursor("1", "2", "3", "3", "3");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(3, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, false, 2);
+ }
+
+ public void testGroupingWithExpandedGroupAtTheEnd() {
+ buildCursor("1", "2", "3", "3", "3");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(2);
+
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, true, 2);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_IN_GROUP, false, 4);
+ }
+
+ public void testGroupingWithMultipleCollapsedGroups() {
+ buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+ }
+
+ public void testGroupingWithMultipleExpandedGroups() {
+ buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+ mAdapter.changeCursor(mCursor);
+ mAdapter.toggleGroup(1);
+
+ // Note that expanding the group of 2's shifted the group of 5's down from the
+ // 4th to the 6th position
+ mAdapter.toggleGroup(6);
+
+ assertEquals(10, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, true, 6);
+ assertPositionMetadata(7, ITEM_TYPE_IN_GROUP, false, 6);
+ assertPositionMetadata(8, ITEM_TYPE_IN_GROUP, false, 7);
+ assertPositionMetadata(9, ITEM_TYPE_STANDALONE, false, 8);
+ }
+
+ public void testPositionCache() {
+ buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
+ mAdapter.changeCursor(mCursor);
+
+ // First pass - building up cache
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+
+ // Second pass - using cache
+ assertEquals(6, mAdapter.getCount());
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
+ assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
+
+ // Invalidate cache by expanding a group
+ mAdapter.toggleGroup(1);
+
+ // First pass - building up cache
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
+
+ // Second pass - using cache
+ assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
+ assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
+ assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
+ assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
+ assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
+ assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
+ assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
+ assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
+ }
+
+ public void testGroupDescriptorArrayGrowth() {
+ String[] numbers = new String[500];
+ for (int i = 0; i < numbers.length; i++) {
+
+ // Make groups of 2
+ numbers[i] = String.valueOf((i / 2) * 2);
+ }
+
+ buildCursor(numbers);
+ mAdapter.changeCursor(mCursor);
+
+ assertEquals(250, mAdapter.getCount());
+ }
+
+ private void assertPositionMetadata(int position, int itemType, boolean isExpanded,
+ int cursorPosition) {
+ GroupingListAdapter.PositionMetadata metadata = new GroupingListAdapter.PositionMetadata();
+ mAdapter.obtainPositionMetadata(metadata, position);
+ assertEquals(itemType, metadata.itemType);
+ if (metadata.itemType == ITEM_TYPE_GROUP_HEADER) {
+ assertEquals(isExpanded, metadata.isExpanded);
+ }
+ assertEquals(cursorPosition, metadata.cursorPosition);
+ }
+}
diff --git a/tests/src/com/android/contacts/RecentCallsListActivityTests.java b/tests/src/com/android/contacts/RecentCallsListActivityTests.java
index afa1a7a..d00854f 100644
--- a/tests/src/com/android/contacts/RecentCallsListActivityTests.java
+++ b/tests/src/com/android/contacts/RecentCallsListActivityTests.java
@@ -97,7 +97,7 @@
@Override
public void setUp() {
- mActivity = (RecentCallsListActivity) getActivity();
+ mActivity = getActivity();
mVoicemail = mActivity.mVoiceMailNumber;
mAdapter = mActivity.mAdapter;
mParentView = new FrameLayout(mActivity);
@@ -224,9 +224,9 @@
mCursor.moveToLast();
while(!mCursor.isBeforeFirst()) {
if (null == mList[i]) {
- mList[i] = mAdapter.newView(mActivity, mCursor, mParentView);
+ mList[i] = mAdapter.newStandAloneView(mActivity, mParentView);
}
- mAdapter.bindView(mList[i], mActivity, mCursor);
+ mAdapter.bindStandAloneView(mList[i], mActivity, mCursor);
mCursor.moveToPrevious();
i++;
}