Introducing CompositeCursorAdapter
Change-Id: Id3b33c0c4fe0cd21ff33c7439e23c95e1d970a34
diff --git a/src/com/android/contacts/widget/CompositeCursorAdapter.java b/src/com/android/contacts/widget/CompositeCursorAdapter.java
new file mode 100644
index 0000000..d917155
--- /dev/null
+++ b/src/com/android/contacts/widget/CompositeCursorAdapter.java
@@ -0,0 +1,421 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * A general purpose adapter that is composed of multiple cursors. It just
+ * appends them in the order they are added.
+ */
+public abstract class CompositeCursorAdapter extends BaseAdapter {
+
+ private static final int INITIAL_CAPACITY = 2;
+
+ private static class Partition {
+ final boolean showIfEmpty;
+ final boolean hasHeader;
+
+ int count;
+ Cursor cursor;
+ int idColumnIndex;
+
+ public Partition(boolean showIfEmpty, boolean hasHeader) {
+ this.showIfEmpty = showIfEmpty;
+ this.hasHeader = hasHeader;
+ }
+ }
+
+ private final Context mContext;
+ private Partition[] mPartitions;
+ private int mSize = 0;
+ private int mCount = 0;
+ private boolean mCacheValid = true;
+
+ public CompositeCursorAdapter(Context context) {
+ this(context, INITIAL_CAPACITY);
+ }
+
+ public CompositeCursorAdapter(Context context, int initialCapacity) {
+ mContext = context;
+ mPartitions = new Partition[INITIAL_CAPACITY];
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Registers a partition. The cursor for that partition can be set later.
+ * Partitions should be added in the order they are supposed to appear in the
+ * list.
+ */
+ public void addPartition(boolean showIfEmpty, boolean hasHeader) {
+ if (mSize >= mPartitions.length) {
+ int newCapacity = mSize + 2;
+ Partition[] newAdapters = new Partition[newCapacity];
+ System.arraycopy(mPartitions, 0, newAdapters, 0, mSize);
+ mPartitions = newAdapters;
+ }
+ mPartitions[mSize++] = new Partition(showIfEmpty, hasHeader);
+ invalidate();
+ }
+
+ protected void invalidate() {
+ mCacheValid = false;
+ }
+
+ public int getPartitionCount() {
+ return mSize;
+ }
+
+ protected void ensureCacheValid() {
+ if (mCacheValid) {
+ return;
+ }
+
+ if (mSize == 0) {
+ throw new IllegalStateException("A CompositeCursorAdapter should have "
+ + "at least one partition");
+ }
+
+ mCount = 0;
+ for (int i = 0; i < mSize; i++) {
+ Cursor cursor = mPartitions[i].cursor;
+ int count = cursor != null ? cursor.getCount() : 0;
+ if (mPartitions[i].hasHeader) {
+ if (count != 0 || mPartitions[i].showIfEmpty) {
+ count++;
+ }
+ }
+ mPartitions[i].count = count;
+ mCount += count;
+ }
+
+ mCacheValid = true;
+ }
+
+ /**
+ * Returns true if the specified partition was configured to have a header.
+ */
+ public boolean hasHeader(int partition) {
+ return mPartitions[partition].hasHeader;
+ }
+
+ /**
+ * Returns the total number of list items in all partitions.
+ */
+ public int getCount() {
+ ensureCacheValid();
+ return mCount;
+ }
+
+ /**
+ * Changes the cursor for an individual partition.
+ */
+ public void changeCursor(int partition, Cursor cursor) {
+ mPartitions[partition].cursor = cursor;
+ if (cursor != null) {
+ mPartitions[partition].idColumnIndex = cursor.getColumnIndex("_id");
+ }
+ invalidate();
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Returns true if the specified partition has no cursor or an empty cursor.
+ */
+ public boolean isPartitionEmpty(int partition) {
+ Cursor cursor = mPartitions[partition].cursor;
+ return cursor == null || cursor.getCount() == 0;
+ }
+
+ /**
+ * Given a list position, returns the index of the corresponding partition.
+ */
+ public int getPartitionForPosition(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ return i;
+ }
+ start = end;
+ }
+ return -1;
+ }
+
+ /**
+ * Given a list position, return the offset of the corresponding item in its
+ * partition. The header, if any, will have offset -1.
+ */
+ public int getOffsetInPartition(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ return offset;
+ }
+ start = end;
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the first list position for the specified partition.
+ */
+ public int getPositionForPartition(int partition) {
+ ensureCacheValid();
+ int position = 0;
+ for (int i = 0; i < partition; i++) {
+ position += mPartitions[i].count;
+ }
+ return position;
+ }
+
+ /**
+ * Returns the overall number of view types across all partitions. An implementation
+ * of this method needs to ensure that the returned count is consistent with the
+ * values returned by {@link #getItemViewType(int,int)}.
+ */
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ /**
+ * Returns the view type for the list item at the specified position in the
+ * specified partition.
+ */
+ protected int getItemViewType(int partition, int position) {
+ return 0;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader && offset == 0) {
+ return IGNORE_ITEM_VIEW_TYPE;
+ }
+ return getItemViewType(i, position);
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ View view;
+ if (offset == -1) {
+ view = getHeaderView(i, mPartitions[i].cursor, convertView, parent);
+ } else {
+ if (!mPartitions[i].cursor.moveToPosition(offset)) {
+ throw new IllegalStateException("Couldn't move cursor to position "
+ + offset);
+ }
+ view = getView(i, mPartitions[i].cursor, offset, convertView, parent);
+ }
+ if (view == null) {
+ throw new NullPointerException("View should not be null, partition: " + i
+ + " position: " + offset);
+ }
+ return view;
+ }
+ start = end;
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ /**
+ * Returns the header view for the specified partition, creating one if needed.
+ */
+ protected View getHeaderView(int partition, Cursor cursor, View convertView,
+ ViewGroup parent) {
+ View view = convertView != null
+ ? convertView
+ : newHeaderView(mContext, partition, cursor, parent);
+ bindHeaderView(view, partition, cursor);
+ return view;
+ }
+
+ /**
+ * Creates the header view for the specified partition.
+ */
+ protected View newHeaderView(Context context, int partition, Cursor cursor,
+ ViewGroup parent) {
+ return null;
+ }
+
+ /**
+ * Binds the header view for the specified partition.
+ */
+ protected void bindHeaderView(View view, int partition, Cursor cursor) {
+ }
+
+ /**
+ * Returns an item view for the specified partition, creating one if needed.
+ */
+ protected View getView(int partition, Cursor cursor, int position, View convertView,
+ ViewGroup parent) {
+ View view;
+ if (convertView != null) {
+ view = convertView;
+ } else {
+ view = newView(mContext, partition, cursor, position, parent);
+ }
+ bindView(view, partition, cursor, position);
+ return view;
+ }
+
+ /**
+ * Creates an item view for the specified partition and position. Position
+ * corresponds directly to the current cursor position.
+ */
+ protected abstract View newView(Context context, int partition, Cursor cursor, int position,
+ ViewGroup parent);
+
+ /**
+ * Binds an item view for the specified partition and position. Position
+ * corresponds directly to the current cursor position.
+ */
+ protected abstract void bindView(View v, int partition, Cursor cursor, int position);
+
+ /**
+ * Returns a pre-positioned cursor for the specified list position.
+ */
+ public Object getItem(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ if (offset == -1) {
+ return null;
+ }
+ Cursor cursor = mPartitions[i].cursor;
+ cursor.moveToPosition(offset);
+ return cursor;
+ }
+ start = end;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the item ID for the specified list position.
+ */
+ public long getItemId(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader) {
+ offset--;
+ }
+ if (offset == -1) {
+ return -1;
+ }
+ if (mPartitions[i].idColumnIndex == -1) {
+ return -1;
+ }
+
+ Cursor cursor = mPartitions[i].cursor;
+ cursor.moveToPosition(offset);
+ return cursor.getLong(mPartitions[i].idColumnIndex);
+ }
+ start = end;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns false if any partition has a header.
+ */
+ @Override
+ public boolean areAllItemsEnabled() {
+ for (int i = 0; i < mSize; i++) {
+ if (mPartitions[i].hasHeader) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true for all items except headers.
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ ensureCacheValid();
+ int start = 0;
+ for (int i = 0; i < mSize; i++) {
+ int end = start + mPartitions[i].count;
+ if (position >= start && position < end) {
+ int offset = position - start;
+ if (mPartitions[i].hasHeader && offset == 0) {
+ return false;
+ } else {
+ return isEnabled(i, offset);
+ }
+ }
+ start = end;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the item at the specified offset of the specified
+ * partition is selectable and clickable.
+ */
+ protected boolean isEnabled(int partition, int position) {
+ return true;
+ }
+}
diff --git a/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java b/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java
new file mode 100644
index 0000000..813d2be
--- /dev/null
+++ b/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Tests for {@link CompositeCursorAdapter}.
+ */
+@SmallTest
+public class CompositeCursorAdapterTest extends AndroidTestCase {
+
+ public class TestCompositeCursorAdapter extends CompositeCursorAdapter {
+
+ public TestCompositeCursorAdapter() {
+ super(CompositeCursorAdapterTest.this.getContext());
+ }
+
+ private StringBuilder mRequests = new StringBuilder();
+
+ @Override
+ protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) {
+ return new View(context);
+ }
+
+ @Override
+ protected void bindHeaderView(View view, int partition, Cursor cursor) {
+ mRequests.append(partition + (cursor == null ? "" : cursor.getColumnNames()[0])
+ + "[H] ");
+ }
+
+ @Override
+ protected View newView(Context context, int sectionIndex, Cursor cursor, int position,
+ ViewGroup parent) {
+ return new View(context);
+ }
+
+ @Override
+ protected void bindView(View v, int partition, Cursor cursor, int position) {
+ if (!cursor.moveToPosition(position)) {
+ fail("Invalid position:" + partition + " " + cursor.getColumnNames()[0] + " "
+ + position);
+ }
+
+ mRequests.append(partition + cursor.getColumnNames()[0] + "["
+ + cursor.getInt(0) + "] ");
+ }
+
+ @Override
+ public String toString() {
+ return mRequests.toString().trim();
+ }
+ }
+
+ public void testGetCountNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, false);
+ adapter.addPartition(false, false);
+
+ adapter.changeCursor(0, makeCursor("a", 2));
+ adapter.changeCursor(1, makeCursor("b", 3));
+
+ assertEquals(5, adapter.getCount());
+ }
+
+ public void testGetViewNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, false);
+ adapter.addPartition(false, false);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("0a[0] 1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testGetCountWithHeadersAndNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, true);
+ adapter.addPartition(false, true);
+
+ adapter.changeCursor(0, makeCursor("a", 2));
+ adapter.changeCursor(1, makeCursor("b", 3));
+
+ assertEquals(7, adapter.getCount());
+ }
+
+ public void testGetViewWithHeadersNoEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, true);
+ adapter.addPartition(false, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("0a[H] 0a[0] 1b[H] 1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testGetCountWithHiddenEmptySection() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, true);
+ adapter.addPartition(false, true);
+
+ adapter.changeCursor(1, makeCursor("a", 2));
+
+ assertEquals(3, adapter.getCount());
+ }
+
+ public void testGetPartitionForPosition() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertEquals(0, adapter.getPartitionForPosition(0));
+ assertEquals(1, adapter.getPartitionForPosition(1));
+ assertEquals(1, adapter.getPartitionForPosition(2));
+ assertEquals(1, adapter.getPartitionForPosition(3));
+ }
+
+ public void testGetOffsetForPosition() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertEquals(0, adapter.getOffsetInPartition(0));
+ assertEquals(-1, adapter.getOffsetInPartition(1));
+ assertEquals(0, adapter.getOffsetInPartition(2));
+ assertEquals(1, adapter.getOffsetInPartition(3));
+ }
+
+ public void testGetPositionForPartition() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, true);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertEquals(0, adapter.getPositionForPartition(0));
+ assertEquals(2, adapter.getPositionForPartition(1));
+ }
+
+ public void testGetViewWithHiddenEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(false, false);
+ adapter.addPartition(false, false);
+
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testGetCountWithShownEmptySection() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, true);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(1, makeCursor("a", 2));
+
+ assertEquals(4, adapter.getCount());
+ }
+
+ public void testGetViewWithShownEmptySections() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, true);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ for (int i = 0; i < adapter.getCount(); i++) {
+ adapter.getView(i, null, null);
+ }
+
+ assertEquals("0[H] 1b[H] 1b[0] 1b[1]", adapter.toString());
+ }
+
+ public void testAreAllItemsEnabledFalse() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ assertFalse(adapter.areAllItemsEnabled());
+ }
+
+ public void testAreAllItemsEnabledTrue() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, false);
+
+ assertTrue(adapter.areAllItemsEnabled());
+ }
+
+ public void testIsEnabled() {
+ TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
+ adapter.addPartition(true, false);
+ adapter.addPartition(true, true);
+
+ adapter.changeCursor(0, makeCursor("a", 1));
+ adapter.changeCursor(1, makeCursor("b", 2));
+
+ assertTrue(adapter.isEnabled(0));
+ assertFalse(adapter.isEnabled(1));
+ assertTrue(adapter.isEnabled(2));
+ assertTrue(adapter.isEnabled(3));
+ }
+
+ private Cursor makeCursor(String name, int count) {
+ MatrixCursor cursor = new MatrixCursor(new String[]{name});
+ for (int i = 0; i < count; i++) {
+ cursor.addRow(new Object[]{i});
+ }
+ return cursor;
+ }
+}