Reworking contacts list adapters for the new CompositeCursorAdapter API

Also redesigning PinnedHeaderListAdapter

Change-Id: Ia4a2e7bb449fc82e1c3ac9b7a3f0c54a8e4d9d38
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 762d66d..4caf5c7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -509,5 +509,13 @@
 
         <activity android:name=".ExportVCardActivity"
             android:theme="@style/BackgroundOnly" />
+
+        <!-- Pinned header list demo -->
+        <activity android:name=".widget.PinnedHeaderListDemoActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/res/layout-finger/pinned_header_list_demo.xml b/res/layout-finger/pinned_header_list_demo.xml
new file mode 100644
index 0000000..9a26e98
--- /dev/null
+++ b/res/layout-finger/pinned_header_list_demo.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/pinned_header_list_layout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        >
+
+    <view
+        class="com.android.contacts.widget.PinnedHeaderListView"
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:fastScrollEnabled="true"
+        android:layout_weight="1"
+    />
+</LinearLayout>
diff --git a/src/com/android/contacts/ContactEntryListView.java b/src/com/android/contacts/ContactEntryListView.java
index 795be43..21ff25e 100644
--- a/src/com/android/contacts/ContactEntryListView.java
+++ b/src/com/android/contacts/ContactEntryListView.java
@@ -39,15 +39,17 @@
     private boolean mHighlightNamesWhenScrolling;
 
     public ContactEntryListView(Context context) {
-        super(context);
+        this(context, null);
     }
 
     public ContactEntryListView(Context context, AttributeSet attrs) {
-        super(context, attrs);
+        this(context, attrs, com.android.internal.R.attr.listViewStyle);
     }
 
     public ContactEntryListView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        setPinnedHeaderBackgroundColor(
+                context.getResources().getColor(R.color.pinned_header_background));
     }
 
     public TextHighlightingAnimation getTextHighlightingAnimation() {
diff --git a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
index 617c855..1529c1d 100644
--- a/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
+++ b/src/com/android/contacts/list/ContactBrowseListContextMenuAdapter.java
@@ -57,15 +57,15 @@
 
         ContactListAdapter adapter = mContactListFragment.getAdapter();
         int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
-        adapter.moveToPosition(info.position - headerViewsCount);
+        int position = info.position - headerViewsCount;
 
         // Setup the menu header
-        menu.setHeaderTitle(adapter.getContactDisplayName());
+        menu.setHeaderTitle(adapter.getContactDisplayName(position));
 
         // View contact details
         menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact);
 
-        if (adapter.getHasPhoneNumber()) {
+        if (adapter.getHasPhoneNumber(position)) {
             // Calling contact
             menu.add(0, MENU_ITEM_CALL, 0, R.string.menu_call);
             // Send SMS item
@@ -73,7 +73,7 @@
         }
 
         // Star toggling
-        if (!adapter.isContactStarred()) {
+        if (!adapter.isContactStarred(position)) {
             menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
         } else {
             menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
@@ -95,9 +95,9 @@
 
         ContactListAdapter adapter = mContactListFragment.getAdapter();
         int headerViewsCount = mContactListFragment.getListView().getHeaderViewsCount();
-        adapter.moveToPosition(info.position - headerViewsCount);
+        int position = info.position - headerViewsCount;
 
-        final Uri contactUri = adapter.getContactUri();
+        final Uri contactUri = adapter.getContactUri(position);
         switch (item.getItemId()) {
             case MENU_ITEM_VIEW_CONTACT: {
                 mContactListFragment.viewContact(contactUri);
@@ -105,7 +105,7 @@
             }
 
             case MENU_ITEM_TOGGLE_STAR: {
-                if (adapter.isContactStarred()) {
+                if (adapter.isContactStarred(position)) {
                     mContactListFragment.removeFromFavorites(contactUri);
                 } else {
                     mContactListFragment.addToFavorites(contactUri);
diff --git a/src/com/android/contacts/list/ContactEntryListAdapter.java b/src/com/android/contacts/list/ContactEntryListAdapter.java
index 7c2a16a..f3d406d 100644
--- a/src/com/android/contacts/list/ContactEntryListAdapter.java
+++ b/src/com/android/contacts/list/ContactEntryListAdapter.java
@@ -18,7 +18,7 @@
 import com.android.contacts.ContactPhotoLoader;
 import com.android.contacts.ContactsSectionIndexer;
 import com.android.contacts.R;
-import com.android.contacts.widget.PinnedHeaderListAdapter;
+import com.android.contacts.widget.IndexerListAdapter;
 import com.android.contacts.widget.TextWithHighlightingFactory;
 
 import android.content.Context;
@@ -35,17 +35,15 @@
  * Common base class for various contact-related lists, e.g. contact list, phone number list
  * etc.
  */
-public abstract class ContactEntryListAdapter extends PinnedHeaderListAdapter {
+public abstract class ContactEntryListAdapter extends IndexerListAdapter {
 
     /**
      * The animation is used here to allocate animated name text views.
      */
     private TextWithHighlightingFactory mTextWithHighlightingFactory;
-
     private int mDisplayOrder;
     private int mSortOrder;
     private boolean mNameHighlightingEnabled;
-    private boolean mSectionHeaderDisplayEnabled;
 
     private boolean mDisplayPhotos;
     private ContactPhotoLoader mPhotoLoader;
@@ -58,15 +56,19 @@
     private boolean mEmptyListEnabled = true;
 
     public ContactEntryListAdapter(Context context) {
-        super(context, R.layout.list_section, R.id.header_text,
-                context.getResources().getColor(R.color.pinned_header_background));
+        super(context, R.layout.list_section, R.id.header_text);
+        addPartitions();
     }
 
-    public Context getContext() {
-        return mContext;
+    /**
+     * Adds all partitions this adapter will handle. The default implementation
+     * creates one partition with no header.
+     */
+    protected void addPartitions() {
+        addPartition(false, false);
     }
 
-    public abstract String getContactDisplayName();
+    public abstract String getContactDisplayName(int position);
     public abstract void configureLoader(CursorLoader loader);
 
     public boolean isSearchMode() {
@@ -93,14 +95,6 @@
         mQueryString = queryString;
     }
 
-    public boolean isSectionHeaderDisplayEnabled() {
-        return mSectionHeaderDisplayEnabled;
-    }
-
-    public void setSectionHeaderDisplayEnabled(boolean flag) {
-        mSectionHeaderDisplayEnabled = flag;
-    }
-
     public int getContactNameDisplayOrder() {
         return mDisplayOrder;
     }
@@ -158,25 +152,20 @@
         mEmptyListEnabled = flag;
     }
 
-    /*
-     * TODO change this method when loaders are introduced.
-     */
     @Override
-    @Deprecated
-    public void onContentChanged() {
-        super.onContentChanged();
-    }
-
-    @Override
-    public void changeCursor(Cursor cursor) {
+    public void changeCursor(int partition, Cursor cursor) {
         mLoading = false;
-        super.changeCursor(cursor);
+        super.changeCursor(partition, cursor);
 
-        if (isSectionHeaderDisplayEnabled()) {
+        if (isSectionHeaderDisplayEnabled() && partition == getIndexedPartition()) {
             updateIndexer(cursor);
         }
     }
 
+    public void changeCursor(Cursor cursor) {
+        changeCursor(0, cursor);
+    }
+
     /**
      * Updates the indexer, which is used to produce section headers.
      */
@@ -223,10 +212,6 @@
 
     @Override
     public int getCount() {
-        if (!mDataValid) {
-            return 0;
-        }
-
         int count = super.getCount();
 
         if (mSearchMode) {
@@ -247,15 +232,16 @@
             return IGNORE_ITEM_VIEW_TYPE;
         }
 
-        return 0;
+        return super.getItemViewType(position);
     }
 
     @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
+    protected View getView(int partition, Cursor cursor, int position, View convertView,
+            ViewGroup parent) {
         if (isSearchAllContactsItemPosition(position)) {
             return LayoutInflater.from(getContext()).inflate(
                     R.layout.contacts_list_search_all_item, parent, false);
         }
-        return super.getView(position, convertView, parent);
+        return super.getView(partition, cursor, position, convertView, parent);
     }
 }
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
index 3dfde7d..b2c9dbc 100644
--- a/src/com/android/contacts/list/ContactListAdapter.java
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -95,17 +95,17 @@
                 .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
     }
 
-    public boolean getHasPhoneNumber() {
-        return getCursor().getInt(CONTACT_HAS_PHONE_COLUMN_INDEX) != 0;
+    public boolean getHasPhoneNumber(int position) {
+        return ((Cursor)getItem(position)).getInt(CONTACT_HAS_PHONE_COLUMN_INDEX) != 0;
     }
 
-    public boolean isContactStarred() {
-        return getCursor().getInt(CONTACT_STARRED_COLUMN_INDEX) != 0;
+    public boolean isContactStarred(int position) {
+        return ((Cursor)getItem(position)).getInt(CONTACT_STARRED_COLUMN_INDEX) != 0;
     }
 
     @Override
-    public String getContactDisplayName() {
-        return getCursor().getString(mDisplayNameColumnIndex);
+    public String getContactDisplayName(int position) {
+        return ((Cursor)getItem(position)).getString(mDisplayNameColumnIndex);
     }
 
     public boolean isQuickContactEnabled() {
@@ -132,25 +132,26 @@
      * Builds the {@link Contacts#CONTENT_LOOKUP_URI} for the given
      * {@link ListView} position.
      */
-    public Uri getContactUri() {
-        Cursor cursor = getCursor();
+    public Uri getContactUri(int position) {
+        Cursor item = (Cursor)getItem(position);
+        return item != null ? getContactUri(item) : null;
+    }
+
+    public Uri getContactUri(Cursor cursor) {
         long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
         String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
         return Contacts.getLookupUri(contactId, lookupKey);
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
         final ContactListItemView view = new ContactListItemView(context, null);
         view.setUnknownNameText(mUnknownNameText);
         view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
         return view;
     }
 
-    protected void bindSectionHeaderAndDivider(ContactListItemView view, Cursor cursor) {
-        bindSectionHeaderAndDivider(view, cursor.getPosition());
-    }
-
     protected void bindSectionHeaderAndDivider(ContactListItemView view, int position) {
         final int section = getSectionForPosition(position);
         if (section != -1 && getPositionForSection(section) == position) {
@@ -186,7 +187,7 @@
         }
 
         QuickContactBadge quickContact = view.getQuickContact();
-        quickContact.assignContactUri(getContactUri());
+        quickContact.assignContactUri(getContactUri(cursor));
         getPhotoLoader().loadPhoto(quickContact, photoId);
     }
 
diff --git a/src/com/android/contacts/list/ContactPickerFragment.java b/src/com/android/contacts/list/ContactPickerFragment.java
index d2b550a..27cadfa 100644
--- a/src/com/android/contacts/list/ContactPickerFragment.java
+++ b/src/com/android/contacts/list/ContactPickerFragment.java
@@ -84,12 +84,10 @@
             mListener.onSearchAllContactsAction((String)null);
         } else {
             Uri uri;
-            ContactEntryListAdapter adapter = getAdapter();
-            adapter.moveToPosition(position);
             if (isLegacyCompatibilityMode()) {
-                uri = ((LegacyContactListAdapter)adapter).getPersonUri();
+                uri = ((LegacyContactListAdapter)getAdapter()).getPersonUri(position);
             } else {
-                uri = ((ContactListAdapter)adapter).getContactUri();
+                uri = ((ContactListAdapter)getAdapter()).getContactUri(position);
             }
             if (mShortcutRequested) {
                 ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 179d25a..e923466 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -87,11 +87,11 @@
                     createNewContact();
                 } else {
                     adapter.moveToPosition(position);
-                    editContact(adapter.getContactUri());
+                    editContact(adapter.getContactUri(position));
                 }
             } else {
                 adapter.moveToPosition(position);
-                viewContact(adapter.getContactUri());
+                viewContact(adapter.getContactUri(position));
             }
         }
     }
diff --git a/src/com/android/contacts/list/DefaultContactListAdapter.java b/src/com/android/contacts/list/DefaultContactListAdapter.java
index 6e0daf5..24c493a 100644
--- a/src/com/android/contacts/list/DefaultContactListAdapter.java
+++ b/src/com/android/contacts/list/DefaultContactListAdapter.java
@@ -81,10 +81,10 @@
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         final ContactListItemView view = (ContactListItemView)itemView;
 
-        bindSectionHeaderAndDivider(view, cursor);
+        bindSectionHeaderAndDivider(view, position);
 
         if (isQuickContactEnabled()) {
             bindQuickContact(view, cursor);
diff --git a/src/com/android/contacts/list/JoinContactListAdapter.java b/src/com/android/contacts/list/JoinContactListAdapter.java
index f1c9a83..4228b0c 100644
--- a/src/com/android/contacts/list/JoinContactListAdapter.java
+++ b/src/com/android/contacts/list/JoinContactListAdapter.java
@@ -37,19 +37,40 @@
     /** Maximum number of suggestions shown for joining aggregates */
     private static final int MAX_SUGGESTIONS = 4;
 
-    private Cursor mSuggestionsCursor;
-    private int mSuggestionsCursorCount;
+    public static final int PARTITION_SUGGESTIONS = 0;
+    public static final int PARTITION_SHOW_ALL_CONTACTS = 1;
+    public static final int PARTITION_ALL_CONTACTS = 2;
+
     private long mTargetContactId;
 
+    private int mShowAllContactsViewType;
+
     /**
      * Determines whether we display a list item with the label
      * "Show all contacts" or actually show all contacts
      */
     private boolean mAllContactsListShown;
 
+
     public JoinContactListAdapter(Context context) {
         super(context);
+        setPinnedPartitionHeadersEnabled(true);
         setSectionHeaderDisplayEnabled(true);
+        setIndexedPartition(PARTITION_ALL_CONTACTS);
+        mShowAllContactsViewType = super.getViewTypeCount();
+    }
+
+    @Override
+    protected void addPartitions() {
+
+        // Partition 0: suggestions
+        addPartition(false, true);
+
+        // Partition 1: "Show all contacts"
+        addPartition(false, false);
+
+        // Partition 2: All contacts
+        addPartition(false, true);
     }
 
     public void setTargetContactId(long targetContactId) {
@@ -76,16 +97,13 @@
 
         // TODO simplify projection
         loader.setProjection(PROJECTION);
-
-        if (mAllContactsListShown) {
-            loader.setUri(buildSectionIndexerUri(Contacts.CONTENT_URI));
-            loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1 AND " + Contacts._ID + "!=?");
-            loader.setSelectionArgs(new String[]{String.valueOf(mTargetContactId)});
-            if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
-                loader.setSortOrder(Contacts.SORT_KEY_PRIMARY);
-            } else {
-                loader.setSortOrder(Contacts.SORT_KEY_ALTERNATIVE);
-            }
+        loader.setUri(buildSectionIndexerUri(Contacts.CONTENT_URI));
+        loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1 AND " + Contacts._ID + "!=?");
+        loader.setSelectionArgs(new String[]{String.valueOf(mTargetContactId)});
+        if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+            loader.setSortOrder(Contacts.SORT_KEY_PRIMARY);
+        } else {
+            loader.setSortOrder(Contacts.SORT_KEY_ALTERNATIVE);
         }
     }
 
@@ -94,10 +112,6 @@
         return false;
     }
 
-    private boolean hasSuggestions() {
-        return mSuggestionsCursorCount != 0;
-    }
-
     public boolean isAllContactsListShown() {
         return mAllContactsListShown;
     }
@@ -107,58 +121,62 @@
     }
 
     public void setSuggestionsCursor(Cursor cursor) {
-        mSuggestionsCursor = cursor;
-        mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
-    }
-
-    public boolean isShowAllContactsItemPosition(int position) {
-        return !mAllContactsListShown
-                && hasSuggestions() && position == mSuggestionsCursorCount + 2;
+        changeCursor(PARTITION_SUGGESTIONS, cursor);
+        if (cursor != null && cursor.getCount() != 0 && !mAllContactsListShown) {
+            changeCursor(PARTITION_SHOW_ALL_CONTACTS, getShowAllContactsLabelCursor());
+        } else {
+            changeCursor(PARTITION_SHOW_ALL_CONTACTS, null);
+        }
     }
 
     @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        if (!mDataValid) {
-            throw new IllegalStateException(
-                    "this should only be called when the cursor is valid");
-        }
+    public void changeCursor(Cursor cursor) {
+        changeCursor(PARTITION_ALL_CONTACTS, cursor);
+    }
 
-        Cursor cursor;
-        boolean showingSuggestion = false;
-        if (hasSuggestions()) {
-            if (position == 0) {
-                // First section: "suggestions"
-                TextView view = (TextView) inflate(R.layout.list_separator, parent);
-                view.setText(R.string.separatorJoinAggregateSuggestions);
-                return view;
-            } else if (position < mSuggestionsCursorCount + 1) {
-                showingSuggestion = true;
-                cursor = mSuggestionsCursor;
-                cursor.moveToPosition(position - 1);
-            } else if (position == mSuggestionsCursorCount + 1) {
-                // Second section: "all contacts"
-                TextView view = (TextView) inflate(R.layout.list_separator, parent);
-                view.setText(R.string.separatorJoinAggregateAll);
-                return view;
-            } else if (!mAllContactsListShown && position == mSuggestionsCursorCount + 2) {
-                return inflate(R.layout.contacts_list_show_all_item, parent);
-            } else {
-                cursor = mCursor;
-                cursor.moveToPosition(position - mSuggestionsCursorCount - 2);
+    @Override
+    public int getViewTypeCount() {
+        return super.getViewTypeCount() + 1;
+    }
+
+    @Override
+    protected int getItemViewType(int partition, int position) {
+        if (partition == PARTITION_SHOW_ALL_CONTACTS) {
+            return mShowAllContactsViewType;
+        }
+        return super.getItemViewType(partition, position);
+    }
+
+    @Override
+    protected View newHeaderView(Context context, int partition, Cursor cursor,
+            ViewGroup parent) {
+        switch (partition) {
+            case PARTITION_SUGGESTIONS: {
+              TextView view = (TextView) inflate(R.layout.list_separator, parent);
+              view.setText(R.string.separatorJoinAggregateSuggestions);
+              return view;
             }
-        } else {
-            cursor = mCursor;
-            cursor.moveToPosition(position);
+            case PARTITION_ALL_CONTACTS: {
+              TextView view = (TextView) inflate(R.layout.list_separator, parent);
+              view.setText(R.string.separatorJoinAggregateAll);
+              return view;
+            }
         }
 
-        View v;
-        if (convertView == null || convertView.getTag() == null) {
-            v = newView(getContext(), cursor, parent);
-        } else {
-            v = convertView;
+        return null;
+    }
+
+    @Override
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
+        switch (partition) {
+            case PARTITION_SUGGESTIONS:
+            case PARTITION_ALL_CONTACTS:
+                return super.newView(context, partition, cursor, position, parent);
+            case PARTITION_SHOW_ALL_CONTACTS:
+                return inflate(R.layout.contacts_list_show_all_item, parent);
         }
-        bindView(position, v, cursor, showingSuggestion);
-        return v;
+        return null;
     }
 
     private View inflate(int layoutId, ViewGroup parent) {
@@ -166,17 +184,25 @@
     }
 
     @Override
-    public void bindView(View view, Context context, Cursor cursor) {
-        // not used
-    }
-
-    public void bindView(int position, View itemView, Cursor cursor, boolean showingSuggestion) {
-        final ContactListItemView view = (ContactListItemView)itemView;
-        if (!showingSuggestion) {
-            bindSectionHeaderAndDivider(view, position);
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        switch (partition) {
+            case PARTITION_SUGGESTIONS: {
+                final ContactListItemView view = (ContactListItemView)itemView;
+                bindPhoto(view, cursor);
+                bindName(view, cursor);
+                break;
+            }
+            case PARTITION_SHOW_ALL_CONTACTS: {
+                break;
+            }
+            case PARTITION_ALL_CONTACTS: {
+                final ContactListItemView view = (ContactListItemView)itemView;
+                bindSectionHeaderAndDivider(view, position);
+                bindPhoto(view, cursor);
+                bindName(view, cursor);
+                break;
+            }
         }
-        bindPhoto(view, cursor);
-        bindName(view, cursor);
     }
 
     public Cursor getShowAllContactsLabelCursor() {
@@ -187,128 +213,9 @@
     }
 
     @Override
-    public void changeCursor(Cursor cursor) {
-        if (cursor == null) {
-            setSuggestionsCursor(null);
-        }
-
-        super.changeCursor(cursor);
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        if (isShowAllContactsItemPosition(position)) {
-            return IGNORE_ITEM_VIEW_TYPE;
-        }
-
-        return super.getItemViewType(position);
-    }
-
-    @Override
-    public int getPositionForSection(int sectionIndex) {
-        if (mSuggestionsCursorCount == 0) {
-            return super.getPositionForSection(sectionIndex);
-        }
-
-        // Get section position in the full list
-        int position = super.getPositionForSection(sectionIndex);
-        return position + mSuggestionsCursorCount + 2;
-    }
-
-    @Override
-    public int getSectionForPosition(int position) {
-        if (mSuggestionsCursorCount == 0) {
-            return super.getSectionForPosition(position);
-        }
-
-        if (position < mSuggestionsCursorCount + 2) {
-            return -1;
-        }
-
-        return super.getSectionForPosition(position - mSuggestionsCursorCount - 2);
-    }
-
-    @Override
-    public boolean areAllItemsEnabled() {
-        return super.areAllItemsEnabled() && mSuggestionsCursorCount == 0;
-    }
-
-    @Override
-    public boolean isEnabled(int position) {
-        if (position == 0) {
-            return false;
-        }
-
-        if (mSuggestionsCursorCount > 0) {
-            return position != 0 && position != mSuggestionsCursorCount + 1;
-        }
-        return true;
-    }
-
-    @Override
-    public int getCount() {
-        if (!mDataValid) {
-            return 0;
-        }
-        int superCount = super.getCount();
-        if (hasSuggestions()) {
-            // When showing suggestions, we have 2 additional list items: the "Suggestions"
-            // and "All contacts" headers.
-            return mSuggestionsCursorCount + superCount + 2;
-        }
-        return superCount;
-    }
-
-    public int getSuggestionsCursorCount() {
-        return mSuggestionsCursorCount;
-    }
-
-    @Override
-    public Object getItem(int pos) {
-        if (hasSuggestions()) {
-            // When showing suggestions, we have 2 additional list items: the "Suggestions"
-            // and "All contacts" separators.
-            if (pos == 0) {
-                return null;
-            }
-            else if (pos < mSuggestionsCursorCount + 1) {
-                // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
-                // separator.
-                mSuggestionsCursor.moveToPosition(pos - 1);
-                return mSuggestionsCursor;
-            } else if (pos == mSuggestionsCursorCount + 1) {
-                // This is the "All contacts" separator
-                return null;
-            } else {
-                if (!isAllContactsListShown()) {
-                    // This is the "Show all contacts" item
-                    return null;
-                } else {
-                    // We are in the lower partition (All contacts). Adjusting for the size
-                    // of the upper partition plus the two separators.
-                    mCursor.moveToPosition(pos - mSuggestionsCursorCount - 2);
-                    return mCursor;
-                }
-            }
-        } else if (mCursor != null) {
-            // No separators
-            mCursor.moveToPosition(pos);
-            return mCursor;
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public long getItemId(int pos) {
-        Cursor cursor = (Cursor)getItem(pos);
-        return cursor == null ? 0 : cursor.getLong(mRowIDColumn);
-    }
-
-    public Uri getContactUri(int position) {
-        Cursor cursor = (Cursor)getItem(position);
+    public Uri getContactUri(Cursor cursor) {
         long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
         String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
         return Contacts.getLookupUri(contactId, lookupKey);
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/contacts/list/JoinContactListFragment.java b/src/com/android/contacts/list/JoinContactListFragment.java
index ebad6f2..12b4ae4 100644
--- a/src/com/android/contacts/list/JoinContactListFragment.java
+++ b/src/com/android/contacts/list/JoinContactListFragment.java
@@ -75,14 +75,9 @@
             }
         } else {
             JoinContactListAdapter adapter = getAdapter();
-            if (adapter.isAllContactsListShown()) {
-                Cursor suggestionsCursor = ((JoinContactLoader)loader).getSuggestionsCursor();
-                adapter.setSuggestionsCursor(suggestionsCursor);
-                super.onLoadFinished(loader, data);
-            } else {
-                adapter.setSuggestionsCursor(data);
-                super.onLoadFinished(loader, adapter.getShowAllContactsLabelCursor());
-            }
+            Cursor suggestionsCursor = ((JoinContactLoader)loader).getSuggestionsCursor();
+            adapter.setSuggestionsCursor(suggestionsCursor);
+            super.onLoadFinished(loader, data);
         }
     }
 
@@ -118,7 +113,8 @@
     @Override
     protected void onItemClick(int position, long id) {
         JoinContactListAdapter adapter = getAdapter();
-        if (adapter.isShowAllContactsItemPosition(position)) {
+        int partition = adapter.getPartitionForPosition(position);
+        if (partition == JoinContactListAdapter.PARTITION_SHOW_ALL_CONTACTS) {
             mAllContactsListShown = true;
             reloadData();
         } else {
diff --git a/src/com/android/contacts/list/JoinContactLoader.java b/src/com/android/contacts/list/JoinContactLoader.java
index dc04165..25c9ab4 100644
--- a/src/com/android/contacts/list/JoinContactLoader.java
+++ b/src/com/android/contacts/list/JoinContactLoader.java
@@ -22,7 +22,7 @@
 import android.net.Uri;
 
 /**
- * A specialized loader for the Join Contacts UI.  It executes up to two queries:
+ * A specialized loader for the Join Contacts UI.  It executes two queries:
  * join suggestions and (optionally) the full contact list.
  */
 public class JoinContactLoader extends CursorLoader {
@@ -30,12 +30,16 @@
     private boolean mLoadSuggestionsAndAllContact;
     private String[] mProjection;
     private Uri mSuggestionUri;
-    private MatrixCursor mCachedCursor;
+    private MatrixCursor mSuggestionsCursor;
 
     public JoinContactLoader(Context context) {
         super(context, null, null, null, null, null);
     }
 
+    public void setLoadSuggestionsAndAllContacts(boolean flag) {
+        mLoadSuggestionsAndAllContact = flag;
+    }
+
     public void setSuggestionUri(Uri uri) {
         this.mSuggestionUri = uri;
     }
@@ -47,25 +51,22 @@
     }
 
     public Cursor getSuggestionsCursor() {
-        return mCachedCursor;
+        return mSuggestionsCursor;
     }
 
     @Override
     public Cursor loadInBackground() {
-        if (mLoadSuggestionsAndAllContact) {
-            // First execute the suggestions query, then call super.loadInBackground
-            // to load the entire list
-            mCachedCursor = loadSuggestions();
-            return super.loadInBackground();
-        } else {
-            // Use the default behavior of the super.loadInBackground to load join
-            // suggestions only
-            setUri(mSuggestionUri);
-            setSelection(null);
+        // First execute the suggestions query, then call super.loadInBackground
+        // to load the entire list
+        mSuggestionsCursor = loadSuggestions();
+        if (!mLoadSuggestionsAndAllContact && mSuggestionsCursor.getCount() != 0) {
+            // In case we only need suggestions, send "0" as the search query, which
+            // will always return an empty cursor (but we can still register to
+            // listen for changes on it).
+            setSelection("0");
             setSelectionArgs(null);
-            setSortOrder(null);
-            return super.loadInBackground();
         }
+        return super.loadInBackground();
     }
 
     /**
@@ -88,8 +89,4 @@
             cursor.close();
         }
     }
-
-    public void setLoadSuggestionsAndAllContacts(boolean flag) {
-        mLoadSuggestionsAndAllContact = flag;
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/contacts/list/LegacyContactListAdapter.java b/src/com/android/contacts/list/LegacyContactListAdapter.java
index fb23e4a..34ae708 100644
--- a/src/com/android/contacts/list/LegacyContactListAdapter.java
+++ b/src/com/android/contacts/list/LegacyContactListAdapter.java
@@ -21,10 +21,8 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.provider.Contacts.People;
-import android.provider.ContactsContract.Contacts;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ListView;
 
 /**
  * A cursor adapter for the People.CONTENT_TYPE content type.
@@ -60,30 +58,27 @@
         loader.setSortOrder(People.DISPLAY_NAME);
     }
 
-    public boolean isContactStarred() {
-        return getCursor().getInt(PERSON_STARRED_COLUMN_INDEX) != 0;
-    }
-
     @Override
-    public String getContactDisplayName() {
-        return getCursor().getString(PERSON_DISPLAY_NAME_COLUMN_INDEX);
+    public String getContactDisplayName(int position) {
+        return ((Cursor)getItem(position)).getString(PERSON_DISPLAY_NAME_COLUMN_INDEX);
     }
 
-    public Uri getPersonUri() {
-        Cursor cursor = getCursor();
+    public Uri getPersonUri(int position) {
+        Cursor cursor = ((Cursor)getItem(position));
         long personId = cursor.getLong(PERSON_ID_COLUMN_INDEX);
         return ContentUris.withAppendedId(People.CONTENT_URI, personId);
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
         final ContactListItemView view = new ContactListItemView(context, null);
         view.setUnknownNameText(mUnknownNameText);
         return view;
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         ContactListItemView view = (ContactListItemView)itemView;
         bindName(view, cursor);
         bindPresence(view, cursor);
diff --git a/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
index a2b9aa5..ed53ef8 100644
--- a/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
+++ b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
@@ -63,25 +63,26 @@
     }
 
     @Override
-    public String getContactDisplayName() {
-        return getCursor().getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
+    public String getContactDisplayName(int position) {
+        return ((Cursor)getItem(position)).getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
     }
 
-    public Uri getPhoneUri() {
-        Cursor cursor = getCursor();
+    public Uri getPhoneUri(int position) {
+        Cursor cursor = ((Cursor)getItem(position));
         long id = cursor.getLong(PHONE_ID_COLUMN_INDEX);
         return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
         final ContactListItemView view = new ContactListItemView(context, null);
         view.setUnknownNameText(mUnknownNameText);
         return view;
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         ContactListItemView view = (ContactListItemView)itemView;
         bindName(view, cursor);
         bindPhoneNumber(view, cursor);
diff --git a/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
index f90c564..36df18b 100644
--- a/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
+++ b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
@@ -64,25 +64,26 @@
     }
 
     @Override
-    public String getContactDisplayName() {
-        return getCursor().getString(POSTAL_DISPLAY_NAME_COLUMN_INDEX);
+    public String getContactDisplayName(int position) {
+        return ((Cursor)getItem(position)).getString(POSTAL_DISPLAY_NAME_COLUMN_INDEX);
     }
 
-    public Uri getContactMethodUri() {
-        Cursor cursor = getCursor();
+    public Uri getContactMethodUri(int position) {
+        Cursor cursor = ((Cursor)getItem(position));
         long id = cursor.getLong(POSTAL_ID_COLUMN_INDEX);
         return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
         final ContactListItemView view = new ContactListItemView(context, null);
         view.setUnknownNameText(mUnknownNameText);
         return view;
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         ContactListItemView view = (ContactListItemView)itemView;
         bindName(view, cursor);
         bindPostalAddress(view, cursor);
diff --git a/src/com/android/contacts/list/MultiplePhonePickerAdapter.java b/src/com/android/contacts/list/MultiplePhonePickerAdapter.java
index 16ec183..2b1196b 100644
--- a/src/com/android/contacts/list/MultiplePhonePickerAdapter.java
+++ b/src/com/android/contacts/list/MultiplePhonePickerAdapter.java
@@ -148,7 +148,7 @@
             String phoneNumber = mPhoneNumbers.get(position);
             setPhoneSelected(phoneNumber, !isSelected(phoneNumber));
         } else {
-            Cursor cursor = getCursor();
+            Cursor cursor = ((Cursor)getItem(position));
             cursor.moveToPosition(position - mFilteredPhoneNumbers.size());
             long phoneId = cursor.getLong(PHONE_ID_COLUMN_INDEX);
             setPhoneSelected(phoneId, !isSelected(phoneId));
@@ -238,29 +238,31 @@
         return position < mPhoneNumbers.size() ? 0 : 1;
     }
 
+    // TODO redo as two separate partitions
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
-        View view;
-        if (convertView == null || convertView.getTag() == null) {
-            view = newView(getContext(), null, parent);
-        } else {
-            view = convertView;
-        }
-
-        boolean showingSuggestion = false;
-
-        if (position < mFilteredPhoneNumbers.size()) {
-            bindExtraPhoneView(view, position);
-        } else {
-            Cursor cursor = getCursor();
-            cursor.moveToPosition(position - mFilteredPhoneNumbers.size());
-            bindView(view, getContext(), cursor);
-        }
+        View view = null;
+//        if (convertView == null || convertView.getTag() == null) {
+//            view = newView(getContext(), null, parent);
+//        } else {
+//            view = convertView;
+//        }
+//
+//        boolean showingSuggestion = false;
+//
+//        if (position < mFilteredPhoneNumbers.size()) {
+//            bindExtraPhoneView(view, position);
+//        } else {
+//            Cursor cursor = ((Cursor)getItem(position));
+//            cursor.moveToPosition(position - mFilteredPhoneNumbers.size());
+//            bindView(view, getContext(), cursor);
+//        }
         return view;
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
         final MultiplePhonePickerItemView view = new MultiplePhonePickerItemView(context, null);
         view.setUnknownNameText(getUnknownNameText());
         view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
@@ -278,8 +280,8 @@
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
-        super.bindView(itemView, context, cursor);
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        super.bindView(itemView, partition, cursor, position);
 
         final MultiplePhonePickerItemView view = (MultiplePhonePickerItemView)itemView;
         view.phoneId = Long.valueOf(cursor.getLong(PHONE_ID_COLUMN_INDEX));
@@ -321,10 +323,6 @@
 
     @Override
     public int getCount() {
-        if (!mDataValid) {
-            return 0;
-        }
-
         return super.getCount() + mFilteredPhoneNumbers.size();
     }
 
diff --git a/src/com/android/contacts/list/PhoneNumberListAdapter.java b/src/com/android/contacts/list/PhoneNumberListAdapter.java
index 0d8975e..2894ed6 100644
--- a/src/com/android/contacts/list/PhoneNumberListAdapter.java
+++ b/src/com/android/contacts/list/PhoneNumberListAdapter.java
@@ -86,8 +86,8 @@
     }
 
     @Override
-    public String getContactDisplayName() {
-        return getCursor().getString(mDisplayNameColumnIndex);
+    public String getContactDisplayName(int position) {
+        return ((Cursor)getItem(position)).getString(mDisplayNameColumnIndex);
     }
 
     @Override
@@ -103,17 +103,17 @@
     }
 
     /**
-     * Builds a {@link Data#CONTENT_URI} for the current cursor
-     * position.
+     * Builds a {@link Data#CONTENT_URI} for the given cursor position.
      */
-    public Uri getDataUri() {
-        Cursor cursor = getCursor();
+    public Uri getDataUri(int position) {
+        Cursor cursor = ((Cursor)getItem(position));
         long id = cursor.getLong(PHONE_ID_COLUMN_INDEX);
         return ContentUris.withAppendedId(Data.CONTENT_URI, id);
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
         final ContactListItemView view = new ContactListItemView(context, null);
         view.setUnknownNameText(mUnknownNameText);
         view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
@@ -121,9 +121,9 @@
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         ContactListItemView view = (ContactListItemView)itemView;
-        bindSectionHeaderAndDivider(view, cursor);
+        bindSectionHeaderAndDivider(view, position);
         bindName(view, cursor);
         bindPhoto(view, cursor);
         bindPhoneNumber(view, cursor);
@@ -142,8 +142,7 @@
         view.showData(cursor, PHONE_NUMBER_COLUMN_INDEX);
     }
 
-    protected void bindSectionHeaderAndDivider(final ContactListItemView view, Cursor cursor) {
-        final int position = cursor.getPosition();
+    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
         final int section = getSectionForPosition(position);
         if (getPositionForSection(section) == position) {
             String title = (String)getSections()[section];
diff --git a/src/com/android/contacts/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
index 6e2bb31..4e8ebe9 100644
--- a/src/com/android/contacts/list/PhoneNumberPickerFragment.java
+++ b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
@@ -56,13 +56,11 @@
 //          if (adapter.isSearchAllContactsItemPosition(position)) {
 //              searchAllContacts();
 //          } else {
-            adapter.moveToPosition(position);
-            pickPhoneNumber(adapter.getDataUri());
+            pickPhoneNumber(adapter.getDataUri(position));
 //          }
         } else {
             LegacyPhoneNumberListAdapter adapter = (LegacyPhoneNumberListAdapter)getAdapter();
-            adapter.moveToPosition(position);
-            pickPhoneNumber(adapter.getPhoneUri());
+            pickPhoneNumber(adapter.getPhoneUri(position));
         }
     }
 
diff --git a/src/com/android/contacts/list/PostalAddressListAdapter.java b/src/com/android/contacts/list/PostalAddressListAdapter.java
index 7c86c75..b6f8fc4 100644
--- a/src/com/android/contacts/list/PostalAddressListAdapter.java
+++ b/src/com/android/contacts/list/PostalAddressListAdapter.java
@@ -23,7 +23,6 @@
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.ContactCounts;
 import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.view.View;
 import android.view.ViewGroup;
@@ -79,8 +78,8 @@
     }
 
     @Override
-    public String getContactDisplayName() {
-        return getCursor().getString(mDisplayNameColumnIndex);
+    public String getContactDisplayName(int position) {
+        return ((Cursor)getItem(position)).getString(mDisplayNameColumnIndex);
     }
 
     @Override
@@ -99,14 +98,14 @@
      * Builds a {@link Data#CONTENT_URI} for the current cursor
      * position.
      */
-    public Uri getDataUri() {
-        Cursor cursor = getCursor();
-        long id = cursor.getLong(POSTAL_ID_COLUMN_INDEX);
+    public Uri getDataUri(int position) {
+        long id = ((Cursor)getItem(position)).getLong(POSTAL_ID_COLUMN_INDEX);
         return ContentUris.withAppendedId(Data.CONTENT_URI, id);
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
         final ContactListItemView view = new ContactListItemView(context, null);
         view.setUnknownNameText(mUnknownNameText);
         view.setTextWithHighlightingFactory(getTextWithHighlightingFactory());
@@ -114,9 +113,9 @@
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         ContactListItemView view = (ContactListItemView)itemView;
-        bindSectionHeaderAndDivider(view, cursor);
+        bindSectionHeaderAndDivider(view, position);
         bindName(view, cursor);
         bindPhoto(view, cursor);
         bindPostalAddress(view, cursor);
@@ -135,8 +134,7 @@
         view.showData(cursor, POSTAL_ADDRESS_COLUMN_INDEX);
     }
 
-    protected void bindSectionHeaderAndDivider(final ContactListItemView view, Cursor cursor) {
-        final int position = cursor.getPosition();
+    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
         final int section = getSectionForPosition(position);
         if (getPositionForSection(section) == position) {
             String title = (String)getSections()[section];
diff --git a/src/com/android/contacts/list/PostalAddressPickerFragment.java b/src/com/android/contacts/list/PostalAddressPickerFragment.java
index 734873b..95ed7f8 100644
--- a/src/com/android/contacts/list/PostalAddressPickerFragment.java
+++ b/src/com/android/contacts/list/PostalAddressPickerFragment.java
@@ -47,12 +47,12 @@
 //              searchAllContacts();
 //          } else {
             adapter.moveToPosition(position);
-            pickPostalAddress(adapter.getDataUri());
+            pickPostalAddress(adapter.getDataUri(position));
 //          }
         } else {
             LegacyPostalAddressListAdapter adapter = (LegacyPostalAddressListAdapter)getAdapter();
             adapter.moveToPosition(position);
-            pickPostalAddress(adapter.getContactMethodUri());
+            pickPostalAddress(adapter.getContactMethodUri(position));
         }
     }
 
diff --git a/src/com/android/contacts/list/StrequentContactListAdapter.java b/src/com/android/contacts/list/StrequentContactListAdapter.java
index 78bd51a..c6ad061 100644
--- a/src/com/android/contacts/list/StrequentContactListAdapter.java
+++ b/src/com/android/contacts/list/StrequentContactListAdapter.java
@@ -174,14 +174,16 @@
     }
 
     @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
-        ContactListItemView view = (ContactListItemView)super.newView(context, cursor, parent);
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
+        ContactListItemView view = (ContactListItemView)super.newView(context, partition, cursor,
+                position, parent);
         view.setOnCallButtonClickListener(mCallButtonListener);
         return view;
     }
 
     @Override
-    public void bindView(View itemView, Context context, Cursor cursor) {
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         final ContactListItemView view = (ContactListItemView)itemView;
 
         bindName(view, cursor);
@@ -189,8 +191,7 @@
         bindPresence(view, cursor);
 
         // Make the call button visible if requested.
-        if (getHasPhoneNumber()) {
-            int position = cursor.getPosition();
+        if (getHasPhoneNumber(position)) {
             view.showCallButton(mCallButtonId, position);
         } else {
             view.hideCallButton();
diff --git a/src/com/android/contacts/list/StrequentContactListFragment.java b/src/com/android/contacts/list/StrequentContactListFragment.java
index 9779d10..4d2fd7f 100644
--- a/src/com/android/contacts/list/StrequentContactListFragment.java
+++ b/src/com/android/contacts/list/StrequentContactListFragment.java
@@ -52,7 +52,7 @@
     protected void onItemClick(int position, long id) {
         ContactListAdapter adapter = getAdapter();
         adapter.moveToPosition(position);
-        viewContact(adapter.getContactUri());
+        viewContact(adapter.getContactUri(position));
     }
 
     @Override
@@ -95,7 +95,7 @@
                 final int position = (Integer)v.getTag();
                 ContactListAdapter adapter = getAdapter();
                 adapter.moveToPosition(position);
-                callContact(adapter.getContactUri());
+                callContact(adapter.getContactUri(position));
                 break;
             }
         }
diff --git a/src/com/android/contacts/widget/IndexerListAdapter.java b/src/com/android/contacts/widget/IndexerListAdapter.java
new file mode 100644
index 0000000..783ef8d
--- /dev/null
+++ b/src/com/android/contacts/widget/IndexerListAdapter.java
@@ -0,0 +1,172 @@
+/*
+ * 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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+/**
+ * A list adapter that supports section indexer and a pinned header.
+ */
+public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer {
+
+    private final int mSectionHeaderTextViewId;
+    private final int mSectionHeaderLayoutResId;
+
+    protected Context mContext;
+    private SectionIndexer mIndexer;
+    private int mIndexedPartition = 0;
+    private boolean mSectionHeaderDisplayEnabled;
+    private View mHeader;
+
+    /**
+     * Constructor.
+     *
+     * @param context
+     * @param sectionHeaderLayoutResourceId section header layout resource ID
+     * @param sectionHeaderTextViewId section header text view ID
+     */
+    public IndexerListAdapter(Context context, int sectionHeaderLayoutResourceId,
+            int sectionHeaderTextViewId) {
+        super(context);
+        mContext = context;
+        mSectionHeaderLayoutResId = sectionHeaderLayoutResourceId;
+        mSectionHeaderTextViewId = sectionHeaderTextViewId;
+    }
+
+    public boolean isSectionHeaderDisplayEnabled() {
+        return mSectionHeaderDisplayEnabled;
+    }
+
+    public void setSectionHeaderDisplayEnabled(boolean flag) {
+        this.mSectionHeaderDisplayEnabled = flag;
+    }
+
+    public int getIndexedPartition() {
+        return mIndexedPartition;
+    }
+
+    public void setIndexedPartition(int partition) {
+        this.mIndexedPartition = partition;
+    }
+
+    public void setIndexer(SectionIndexer indexer) {
+        mIndexer = indexer;
+    }
+
+    public Object[] getSections() {
+        if (mIndexer == null) {
+            return new String[] { " " };
+        } else {
+            return mIndexer.getSections();
+        }
+    }
+
+    /**
+     * @return relative position of the section in the indexed partition
+     */
+    public int getPositionForSection(int sectionIndex) {
+        if (mIndexer == null) {
+            return -1;
+        }
+
+        return mIndexer.getPositionForSection(sectionIndex);
+    }
+
+    /**
+     * @param position relative position in the indexed partition
+     */
+    public int getSectionForPosition(int position) {
+        if (mIndexer == null) {
+            return -1;
+        }
+
+        return mIndexer.getSectionForPosition(position);
+    }
+
+    @Override
+    public int getPinnedHeaderCount() {
+        if (isSectionHeaderDisplayEnabled()) {
+            return super.getPinnedHeaderCount() + 1;
+        } else {
+            return super.getPinnedHeaderCount();
+        }
+    }
+
+    @Override
+    public View createPinnedHeaderView(int viewIndex, ViewGroup parent) {
+        if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) {
+            mHeader = LayoutInflater.from(mContext).
+                    inflate(mSectionHeaderLayoutResId, parent, false);
+            return mHeader;
+        } else {
+            return super.createPinnedHeaderView(viewIndex, parent);
+        }
+    }
+
+    @Override
+    public void configurePinnedHeaders(PinnedHeaderListView listView) {
+        super.configurePinnedHeaders(listView);
+
+        if (!isSectionHeaderDisplayEnabled()) {
+            return;
+        }
+
+        int index = getPinnedHeaderCount() - 1;
+        if (mIndexer == null || getCount() == 0) {
+            listView.setHeaderInvisible(index);
+        } else {
+            int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight());
+            int position = listPosition - listView.getHeaderViewsCount();
+
+            int section = -1;
+            int partition = getPartitionForPosition(position);
+            if (partition == mIndexedPartition) {
+                int offset = getOffsetInPartition(position);
+                if (offset != -1) {
+                    section = getSectionForPosition(offset);
+                }
+            }
+
+            if (section == -1) {
+                listView.setHeaderInvisible(index);
+            } else {
+                String title = (String)mIndexer.getSections()[section];
+                TextView titleView = (TextView)mHeader.getTag();
+                if (titleView == null) {
+                    titleView = (TextView)mHeader.findViewById(mSectionHeaderTextViewId);
+                    mHeader.setTag(titleView);
+                }
+                titleView.setText(title);
+
+                // Compute the item position where the current partition begins
+                int partitionStart = getPositionForPartition(mIndexedPartition);
+                if (hasHeader(mIndexedPartition)) {
+                    partitionStart++;
+                }
+
+                // Compute the item position where the next section begins
+                int nextSectionPosition = partitionStart + getPositionForSection(section + 1);
+                boolean isLastInSection = position == nextSectionPosition - 1;
+                listView.setFadingHeader(index, listPosition, isLastInSection);
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/widget/PinnedHeaderListAdapter.java b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
index 7492de1..88ab3db 100644
--- a/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
+++ b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
@@ -16,147 +16,127 @@
 package com.android.contacts.widget;
 
 import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.CursorAdapter;
-import android.widget.SectionIndexer;
-import android.widget.TextView;
 
 /**
- * A list adapter that supports section indexer and a pinned header.
+ * A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers.
  */
-public abstract class PinnedHeaderListAdapter extends CursorAdapter
-        implements SectionIndexer, PinnedHeaderListView.PinnedHeaderAdapter {
+public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter
+        implements PinnedHeaderListView.PinnedHeaderAdapter {
 
-    private final int mPinnedHeaderBackgroundColor;
-    private final int mSectionHeaderTextViewId;
-    private final int mSectionHeaderLayoutResId;
+    private boolean mPinnedPartitionHeadersEnabled;
+    private boolean mHeaderVisibility[];
 
-    private SectionIndexer mIndexer;
-
-    /**
-     * Constructor.
-     *
-     * @param context
-     * @param sectionHeaderLayoutResourceId section header layout resource ID
-     * @param sectionHeaderTextViewId section header text view ID
-     * @param backgroundColor An approximation of the background color of the
-     *            pinned header. This color is used when the pinned header is
-     *            being pushed up. At that point the header "fades away". Rather
-     *            than computing a faded bitmap based on the 9-patch normally
-     *            used for the background, we will use a solid color, which will
-     *            provide better performance and reduced complexity.
-     */
-    public PinnedHeaderListAdapter(Context context, int sectionHeaderLayoutResourceId,
-            int sectionHeaderTextViewId, int backgroundColor) {
-        super(context, null, false);
-        this.mContext = context;
-        mPinnedHeaderBackgroundColor = backgroundColor;
-        mSectionHeaderLayoutResId = sectionHeaderLayoutResourceId;
-        mSectionHeaderTextViewId = sectionHeaderTextViewId;
+    public PinnedHeaderListAdapter(Context context) {
+        super(context);
     }
 
-    public void setIndexer(SectionIndexer indexer) {
-        mIndexer = indexer;
+    public PinnedHeaderListAdapter(Context context, int initialCapacity) {
+        super(context, initialCapacity);
     }
 
-    public Object [] getSections() {
-        if (mIndexer == null) {
-            return new String[] { " " };
+    public boolean getPinnedPartitionHeadersEnabled() {
+        return mPinnedPartitionHeadersEnabled;
+    }
+
+    public void setPinnedPartitionHeadersEnabled(boolean flag) {
+        this.mPinnedPartitionHeadersEnabled = flag;
+    }
+
+    public int getPinnedHeaderCount() {
+        if (mPinnedPartitionHeadersEnabled) {
+            return getPartitionCount();
         } else {
-            return mIndexer.getSections();
+            return 0;
         }
     }
 
-    public int getPositionForSection(int sectionIndex) {
-        if (mIndexer == null) {
-            return -1;
-        }
-
-        return mIndexer.getPositionForSection(sectionIndex);
-    }
-
-    public int getSectionForPosition(int position) {
-        if (mIndexer == null) {
-            return -1;
-        }
-
-        return mIndexer.getSectionForPosition(position);
+    protected boolean isPinnedPartitionHeaderVisible(int partition) {
+        return mPinnedPartitionHeadersEnabled && hasHeader(partition)
+                && !isPartitionEmpty(partition);
     }
 
     /**
-     * Computes the state of the pinned header.  It can be invisible, fully
-     * visible or partially pushed up out of the view.
+     * The default implementation creates the same type of view as a normal
+     * partition header.
      */
-    public int getPinnedHeaderState(int position) {
-        if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) {
-            return PINNED_HEADER_GONE;
-        }
-
-        // The header should get pushed up if the top item shown
-        // is the last item in a section for a particular letter.
-        int section = getSectionForPosition(position);
-        if (section == -1) {
-            return PINNED_HEADER_GONE;
-        }
-
-        int nextSectionPosition = getPositionForSection(section + 1);
-        if (nextSectionPosition != -1 && position == nextSectionPosition - 1) {
-            return PINNED_HEADER_PUSHED_UP;
-        }
-
-        return PINNED_HEADER_VISIBLE;
-    }
-
-    final static class PinnedHeaderCache {
-        public TextView titleView;
-        public ColorStateList textColor;
-        public Drawable background;
-    }
-
-    /**
-     * Configures the pinned header by setting the appropriate text label
-     * and also adjusting color if necessary.  The color needs to be
-     * adjusted when the pinned header is being pushed up from the view.
-     */
-    public void configurePinnedHeader(View header, int position, int alpha) {
-        PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag();
-        if (cache == null) {
-            cache = new PinnedHeaderCache();
-            cache.titleView = (TextView)header.findViewById(mSectionHeaderTextViewId);
-            cache.textColor = cache.titleView.getTextColors();
-            cache.background = header.getBackground();
-            header.setTag(cache);
-        }
-
-        int section = getSectionForPosition(position);
-
-        String title = (String)mIndexer.getSections()[section];
-        cache.titleView.setText(title);
-
-        if (alpha == 255) {
-            // Opaque: use the default background, and the original text color
-            header.setBackgroundDrawable(cache.background);
-            cache.titleView.setTextColor(cache.textColor);
+    public View createPinnedHeaderView(int partition, ViewGroup parent) {
+        if (hasHeader(partition)) {
+            View view = newHeaderView(getContext(), partition, null, parent);
+            view.setFocusable(false);
+            view.setEnabled(false);
+            bindHeaderView(view, partition, null);
+            return view;
         } else {
-            // Faded: use a solid color approximation of the background, and
-            // a translucent text color
-            header.setBackgroundColor(Color.rgb(
-                    Color.red(mPinnedHeaderBackgroundColor) * alpha / 255,
-                    Color.green(mPinnedHeaderBackgroundColor) * alpha / 255,
-                    Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255));
-
-            int textColor = cache.textColor.getDefaultColor();
-            cache.titleView.setTextColor(Color.argb(alpha,
-                    Color.red(textColor), Color.green(textColor), Color.blue(textColor)));
+            return null;
         }
     }
 
-    public View createPinnedHeaderView(ViewGroup parent) {
-        return LayoutInflater.from(mContext).inflate(mSectionHeaderLayoutResId, parent, false);
+    public void configurePinnedHeaders(PinnedHeaderListView listView) {
+        if (!mPinnedPartitionHeadersEnabled) {
+            return;
+        }
+
+        int size = getPartitionCount();
+
+        // Cache visibility bits, because we will need them several times later on
+        if (mHeaderVisibility == null || mHeaderVisibility.length != size) {
+            mHeaderVisibility = new boolean[size];
+        }
+        for (int i = 0; i < size; i++) {
+            boolean visible = isPinnedPartitionHeaderVisible(i);
+            mHeaderVisibility[i] = visible;
+            if (!visible) {
+                listView.setHeaderInvisible(i);
+            }
+        }
+
+        // Starting at the top, find and pin headers for partitions preceding the visible one(s)
+        int maxTopHeader = -1;
+        int topHeaderHeight = 0;
+        for (int i = 0; i < size; i++) {
+            if (mHeaderVisibility[i]) {
+                int position = listView.getPositionAt(topHeaderHeight);
+                int partition = getPartitionForPosition(position);
+                if (i > partition) {
+                    break;
+                }
+
+                listView.setHeaderPinnedAtTop(i, topHeaderHeight);
+                topHeaderHeight += listView.getPinnedHeaderHeight(i);
+                maxTopHeader = i;
+            }
+        }
+
+        // Starting at the bottom, find and pin headers for partitions following the visible one(s)
+        int maxBottomHeader = size;
+        int bottomHeaderHeight = 0;
+        int listHeight = listView.getHeight();
+        for (int i = size; --i > maxTopHeader;) {
+            if (mHeaderVisibility[i]) {
+                int position = listView.getPositionAt(listHeight - bottomHeaderHeight);
+                if (position < 0) {
+                    break;
+                }
+
+                int partition = getPartitionForPosition(position - 1);
+                if (partition == -1 || i <= partition) {
+                    break;
+                }
+
+                int height = listView.getPinnedHeaderHeight(i);
+                bottomHeaderHeight += height;
+                listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight);
+                maxBottomHeader = i;
+            }
+        }
+
+        // Headers in between the top-pinned and bottom-pinned should be hidden
+        for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) {
+            if (mHeaderVisibility[i]) {
+                listView.setHeaderInvisible(i);
+            }
+        }
     }
 }
diff --git a/src/com/android/contacts/widget/PinnedHeaderListDemoActivity.java b/src/com/android/contacts/widget/PinnedHeaderListDemoActivity.java
new file mode 100644
index 0000000..66b70f3
--- /dev/null
+++ b/src/com/android/contacts/widget/PinnedHeaderListDemoActivity.java
@@ -0,0 +1,142 @@
+/*
+ * 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 com.android.contacts.R;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * An activity that demonstrates various use cases for the {@link PinnedHeaderListView}.
+ * If we decide to move PinnedHeaderListView to the framework, this class could go
+ * to API demos.
+ */
+public class PinnedHeaderListDemoActivity extends ListActivity {
+
+    public final static class TestPinnedHeaderListAdapter extends PinnedHeaderListAdapter {
+
+        public TestPinnedHeaderListAdapter(Context context) {
+            super(context);
+            setPinnedPartitionHeadersEnabled(true);
+        }
+
+        private String[] mHeaders;
+        private int mPinnedHeaderCount;
+
+        public void setHeaders(String[] headers) {
+            this.mHeaders = headers;
+        }
+
+        @Override
+        protected View newHeaderView(Context context, int partition, Cursor cursor,
+                ViewGroup parent) {
+            LayoutInflater inflater = LayoutInflater.from(context);
+            return inflater.inflate(R.layout.list_section, null);
+        }
+
+        @Override
+        protected void bindHeaderView(View view, int parition, Cursor cursor) {
+            TextView headerText = (TextView)view.findViewById(R.id.header_text);
+            headerText.setText(mHeaders[parition]);
+        }
+
+        @Override
+        protected View newView(Context context, int partition, Cursor cursor, int position,
+                ViewGroup parent) {
+            LayoutInflater inflater = LayoutInflater.from(context);
+            return inflater.inflate(android.R.layout.simple_list_item_1, null);
+        }
+
+        @Override
+        protected void bindView(View v, int partition, Cursor cursor, int position) {
+            TextView text = (TextView)v.findViewById(android.R.id.text1);
+            text.setText(cursor.getString(1));
+        }
+
+        @Override
+        public View createPinnedHeaderView(int viewIndex, ViewGroup parent) {
+            LayoutInflater inflater = LayoutInflater.from(getContext());
+            View view = inflater.inflate(R.layout.list_section, parent, false);
+            view.setFocusable(false);
+            view.setEnabled(false);
+            bindHeaderView(view, viewIndex, null);
+            return view;
+        }
+
+        @Override
+        public int getPinnedHeaderCount() {
+            return mPinnedHeaderCount;
+        }
+    }
+
+    private Handler mHandler = new Handler();
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        setContentView(R.layout.pinned_header_list_demo);
+
+        final TestPinnedHeaderListAdapter adapter = new TestPinnedHeaderListAdapter(this);
+
+        Bundle extras = getIntent().getExtras();
+        int[] counts = extras.getIntArray("counts");
+        String[] names = extras.getStringArray("names");
+        boolean[] showIfEmpty = extras.getBooleanArray("showIfEmpty");
+        boolean[] hasHeader = extras.getBooleanArray("headers");
+        int[] delays = extras.getIntArray("delays");
+
+        if (counts == null || names == null || showIfEmpty == null || delays == null) {
+            throw new IllegalArgumentException("Missing required extras");
+        }
+
+        adapter.setHeaders(names);
+        for (int i = 0; i < counts.length; i++) {
+            adapter.addPartition(showIfEmpty[i], names[i] != null);
+            adapter.mPinnedHeaderCount = names.length;
+        }
+        setListAdapter(adapter);
+        for (int i = 0; i < counts.length; i++) {
+            final int sectionId = i;
+            final Cursor cursor = makeCursor(names[i], counts[i]);
+            mHandler.postDelayed(new Runnable() {
+
+                public void run() {
+                    adapter.changeCursor(sectionId, cursor);
+
+                }
+            }, delays[i]);
+        }
+    }
+
+    private Cursor makeCursor(String name, int count) {
+        MatrixCursor cursor = new MatrixCursor(new String[]{"_id", name});
+        for (int i = 0; i < count; i++) {
+            cursor.addRow(new Object[]{i, name + "[" + i + "]"});
+        }
+        return cursor;
+    }
+}
diff --git a/src/com/android/contacts/widget/PinnedHeaderListView.java b/src/com/android/contacts/widget/PinnedHeaderListView.java
index 5893ac6..9ed51d8 100644
--- a/src/com/android/contacts/widget/PinnedHeaderListView.java
+++ b/src/com/android/contacts/widget/PinnedHeaderListView.java
@@ -18,19 +18,24 @@
 
 import android.content.Context;
 import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AbsListView;
+import android.widget.AdapterView;
 import android.widget.ListAdapter;
 import android.widget.ListView;
 import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView.OnItemSelectedListener;
 
 /**
  * A ListView that maintains a header pinned at the top of the list. The
  * pinned header can be pushed up and dissolved as needed.
  */
-public class PinnedHeaderListView extends ListView implements OnScrollListener {
+public class PinnedHeaderListView extends ListView
+        implements OnScrollListener, OnItemSelectedListener {
 
     /**
      * Adapter interface.  The list adapter must implement this interface.
@@ -38,90 +43,94 @@
     public interface PinnedHeaderAdapter {
 
         /**
-         * Pinned header state: don't show the header.
+         * Returns the overall number of pinned headers, visible or not.
          */
-        public static final int PINNED_HEADER_GONE = 0;
-
-        /**
-         * Pinned header state: show the header at the top of the list.
-         */
-        public static final int PINNED_HEADER_VISIBLE = 1;
-
-        /**
-         * Pinned header state: show the header. If the header extends beyond
-         * the bottom of the first shown element, push it up and clip.
-         */
-        public static final int PINNED_HEADER_PUSHED_UP = 2;
+        int getPinnedHeaderCount();
 
         /**
          * Creates the pinned header view.
          */
-        View createPinnedHeaderView(ViewGroup parent);
+        View createPinnedHeaderView(int viewIndex, ViewGroup parent);
 
         /**
-         * Computes the desired state of the pinned header for the given
-         * position of the first visible list item. Allowed return values are
-         * {@link #PINNED_HEADER_GONE}, {@link #PINNED_HEADER_VISIBLE} or
-         * {@link #PINNED_HEADER_PUSHED_UP}.
+         * Configures the pinned headers to match the visible list items. The
+         * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop},
+         * {@link PinnedHeaderListView#setHeaderPinnedAtBottom},
+         * {@link PinnedHeaderListView#setFadingHeader} or
+         * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that
+         * needs to change its position or visibility.
          */
-        int getPinnedHeaderState(int position);
-
-        /**
-         * Configures the pinned header view to match the first visible list item.
-         *
-         * @param header pinned header view.
-         * @param position position of the first visible list item.
-         * @param alpha fading of the header view, between 0 and 255.
-         */
-        void configurePinnedHeader(View header, int position, int alpha);
+        void configurePinnedHeaders(PinnedHeaderListView listView);
     }
 
     private static final int MAX_ALPHA = 255;
+    private static final int TOP = 0;
+    private static final int BOTTOM = 1;
+    private static final int FADING = 2;
+
+    private static final class PinnedHeader {
+        View view;
+        boolean visible;
+        int y;
+        int height;
+        int alpha;
+        int state;
+    }
 
     private PinnedHeaderAdapter mAdapter;
-    private View mHeaderView;
-    private boolean mHeaderViewVisible;
-    private float mHeaderOffset;
-    private int mHeaderViewWidth;
-    private int mHeaderViewHeight;
-
+    private PinnedHeader[] mHeaders;
+    private int mPinnedHeaderBackgroundColor;
+    private RectF mBounds = new RectF();
+    private Paint mPaint = new Paint();
     private OnScrollListener mOnScrollListener;
-
-    private boolean mHeaderViewConfigured;
+    private OnItemSelectedListener mOnItemSelectedListener;
 
     public PinnedHeaderListView(Context context) {
-        super(context);
+        this(context, null, com.android.internal.R.attr.listViewStyle);
     }
 
     public PinnedHeaderListView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        super.setOnScrollListener(this);
+        this(context, attrs, com.android.internal.R.attr.listViewStyle);
     }
 
     public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         super.setOnScrollListener(this);
+        super.setOnItemSelectedListener(this);
     }
 
-    public void setPinnedHeaderView(View view) {
-        mHeaderView = view;
-
-        // Disable vertical fading when the pinned header is present
-        // TODO change ListView to allow separate measures for top and bottom fading edge;
-        // in this particular case we would like to disable the top, but not the bottom edge.
-        if (mHeaderView != null) {
-            setFadingEdgeLength(0);
-        }
-        mHeaderViewConfigured = false;
-        requestLayout();
+    /**
+     * An approximation of the background color of the pinned header. This color
+     * is used when the pinned header is being pushed up. At that point the
+     * header "fades away". Rather than computing a faded bitmap based on the
+     * 9-patch normally used for the background, we will use a solid color,
+     * which will provide better performance and reduced complexity.
+     */
+    public void setPinnedHeaderBackgroundColor(int color) {
+        mPinnedHeaderBackgroundColor = color;
+        mPaint.setColor(mPinnedHeaderBackgroundColor);
     }
 
     @Override
     public void setAdapter(ListAdapter adapter) {
         super.setAdapter(adapter);
         mAdapter = (PinnedHeaderAdapter)adapter;
-        View headerView = mAdapter.createPinnedHeaderView(this);
-        setPinnedHeaderView(headerView);
+        int count = mAdapter.getPinnedHeaderCount();
+        mHeaders = new PinnedHeader[count];
+        for (int i = 0; i < count; i++) {
+            PinnedHeader header = new PinnedHeader();
+            header.view = mAdapter.createPinnedHeaderView(i, this);
+            mHeaders[i] = header;
+        }
+
+        // Disable vertical fading when the pinned header is present
+        // TODO change ListView to allow separate measures for top and bottom fading edge;
+        // in this particular case we would like to disable the top, but not the bottom edge.
+        if (count > 0) {
+            setFadingEdgeLength(0);
+        }
+
+        requestLayout();
     }
 
     @Override
@@ -131,32 +140,16 @@
     }
 
     @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        if (mHeaderView != null) {
-            if (!mHeaderViewConfigured) {
-                configureHeaderView(getFirstVisiblePosition() - getHeaderViewsCount());
-            }
-            measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
-            mHeaderViewWidth = mHeaderView.getMeasuredWidth();
-            mHeaderViewHeight = mHeaderView.getMeasuredHeight();
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        if (mHeaderView != null) {
-            if (!mHeaderViewConfigured) {
-                configureHeaderView(getFirstVisiblePosition() - getHeaderViewsCount());
-            }
-            mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
-        }
+    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+        mOnItemSelectedListener = listener;
+        super.setOnItemSelectedListener(this);
     }
 
     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
             int totalItemCount) {
-        configureHeaderView(firstVisibleItem - getHeaderViewsCount());
+        if (mAdapter != null) {
+            mAdapter.configurePinnedHeaders(this);
+        }
         if (mOnScrollListener != null) {
             mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
         }
@@ -168,54 +161,175 @@
         }
     }
 
-    public void configureHeaderView(int position) {
-        if (mHeaderView == null) {
-            return;
-        }
+    /**
+     * Ensures that the selected item is positioned below the top-pinned headers
+     * and above the bottom-pinned ones.
+     */
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        int height = getHeight();
 
-        int state = mAdapter.getPinnedHeaderState(position);
-        switch (state) {
-            case PinnedHeaderAdapter.PINNED_HEADER_GONE: {
-                mHeaderViewVisible = false;
-                break;
-            }
+        int windowTop = 0;
+        int windowBottom = height;
 
-            case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {
-                mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);
-                mHeaderOffset = 0;
-                mHeaderViewVisible = true;
-                break;
-            }
-
-            case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {
-                int y = 0;
-                int alpha = MAX_ALPHA;
-                if (getChildCount() != 0) {
-                    View firstView = getChildAt(0);
-                    int bottom = firstView.getBottom();
-                    int headerHeight = mHeaderViewHeight;
-                    if (bottom < headerHeight) {
-                        y = (bottom - headerHeight);
-                        alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;
-                    }
+        int prevHeaderBottom = 0;
+        for (int i = 0; i < mHeaders.length; i++) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible) {
+                if (header.state == TOP) {
+                    windowTop = header.y + header.height;
+                } else if (header.state == BOTTOM) {
+                    windowBottom = header.y;
+                    break;
                 }
-                mAdapter.configurePinnedHeader(mHeaderView, position, alpha);
-                mHeaderOffset = y;
-                mHeaderViewVisible = true;
-                break;
             }
         }
-        mHeaderViewConfigured = true;
+
+        View selectedView = getSelectedView();
+        if (selectedView.getTop() < windowTop) {
+            setSelectionFromTop(position, windowTop);
+        } else if (selectedView.getBottom() > windowBottom) {
+            setSelectionFromTop(position, windowBottom - selectedView.getHeight());
+        }
+
+        if (mOnItemSelectedListener != null) {
+            mOnItemSelectedListener.onItemSelected(parent, view, position, id);
+        }
+    }
+
+    public void onNothingSelected(AdapterView<?> parent) {
+        if (mOnItemSelectedListener != null) {
+            mOnItemSelectedListener.onNothingSelected(parent);
+        }
+    }
+
+    public int getPinnedHeaderHeight(int viewIndex) {
+        ensurePinnedHeaderLayout(viewIndex);
+        return mHeaders[viewIndex].view.getHeight();
+    }
+
+    /**
+     * Set header to be pinned at the top.
+     *
+     * @param viewIndex index of the header view
+     * @param y is position of the header in pixels.
+     */
+    public void setHeaderPinnedAtTop(int viewIndex, int y) {
+        ensurePinnedHeaderLayout(viewIndex);
+        PinnedHeader header = mHeaders[viewIndex];
+        header.visible = true;
+        header.y = y;
+        header.state = TOP;
+    }
+
+    /**
+     * Set header to be pinned at the bottom.
+     *
+     * @param viewIndex index of the header view
+     * @param y is position of the header in pixels.
+     */
+    public void setHeaderPinnedAtBottom(int viewIndex, int y) {
+        ensurePinnedHeaderLayout(viewIndex);
+        PinnedHeader header = mHeaders[viewIndex];
+        header.visible = true;
+        header.y = y;
+        header.state = BOTTOM;
+    }
+
+    /**
+     * Set header to be pinned at the top of the first visible item.
+     *
+     * @param viewIndex index of the header view
+     * @param position is position of the header in pixels.
+     */
+    public void setFadingHeader(int viewIndex, int position, boolean fade) {
+        ensurePinnedHeaderLayout(viewIndex);
+
+        View child = getChildAt(position - getFirstVisiblePosition());
+
+        PinnedHeader header = mHeaders[viewIndex];
+        header.visible = true;
+        header.state = FADING;
+        header.alpha = MAX_ALPHA;
+
+        int top = getTotalTopPinnedHeaderHeight();
+        header.y = top;
+        if (fade) {
+            int bottom = child.getBottom() - top;
+            int headerHeight = header.height;
+            if (bottom < headerHeight) {
+                int portion = bottom - headerHeight;
+                header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight;
+                header.y = top + portion;
+            }
+        }
+    }
+
+    public void setHeaderInvisible(int viewIndex) {
+        mHeaders[viewIndex].visible = false;
+    }
+
+    private void ensurePinnedHeaderLayout(int viewIndex) {
+        View view = mHeaders[viewIndex].view;
+        if (view.isLayoutRequested()) {
+            view.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
+                    MeasureSpec.UNSPECIFIED);
+            int height = view.getMeasuredHeight();
+            mHeaders[viewIndex].height = height;
+            view.layout(0, 0, view.getMeasuredWidth(), height);
+        }
+    }
+
+    /**
+     * Returns the sum of heights of headers pinned to the top.
+     */
+    public int getTotalTopPinnedHeaderHeight() {
+        for (int i = mHeaders.length; --i >= 0;) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible && header.state == TOP) {
+                return header.y + header.height;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Returns the list item position at the specified y coordinate.
+     */
+    public int getPositionAt(int y) {
+        do {
+            int position = pointToPosition(0, y);
+            if (position != -1) {
+                return position;
+            }
+            // If position == -1, we must have hit a separator. Let's examine
+            // a nearby pixel
+            y--;
+        } while (y > 0);
+        return 0;
     }
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
-        if (mHeaderViewVisible) {
-            canvas.save();
-            canvas.translate(0, mHeaderOffset);
-            drawChild(canvas, mHeaderView, getDrawingTime());
-            canvas.restore();
+        for (int i = mHeaders.length; --i >= 0;) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible) {
+                View view = header.view;
+                if (header.state == FADING) {
+                    int saveCount = canvas.save();
+                    canvas.translate(0, header.y);
+                    mBounds.set(0, 0, view.getWidth(), view.getHeight());
+                    canvas.drawRect(mBounds, mPaint);
+                    canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
+                    view.draw(canvas);
+                    canvas.restoreToCount(saveCount);
+                } else {
+                    canvas.save();
+                    canvas.translate(0, header.y);
+                    view.draw(canvas);
+                    canvas.restore();
+                }
+            }
         }
     }
 }
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 17ad3f7..0c5ee70 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -43,6 +43,16 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".widget.PinnedHeaderUseCaseActivity"
+            android:label="@string/pinnedHeaderList"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
@@ -55,7 +65,6 @@
         android:label="Contacts launch performance">
     </instrumentation>
 
-
     <instrumentation android:name="com.android.contacts.DialerLaunchPerformance"
         android:targetPackage="com.android.contacts"
         android:label="Dialer launch performance">
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
index 745b351..93095e3 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -73,4 +73,12 @@
         <item>EDIT (create new raw contact)</item>
         <item>EDIT (create new legacy)</item>
     </string-array>
+
+    <string name="pinnedHeaderList">Pinned Headers</string>
+
+    <string-array name="pinnedHeaderUseCases">
+        <item>One short section - no headers</item>
+        <item>Two short sections with headers</item>
+        <item>Five short sections with headers</item>
+    </string-array>
 </resources>
diff --git a/tests/src/com/android/contacts/tests/widget/PinnedHeaderUseCaseActivity.java b/tests/src/com/android/contacts/tests/widget/PinnedHeaderUseCaseActivity.java
new file mode 100644
index 0000000..b01963f
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/widget/PinnedHeaderUseCaseActivity.java
@@ -0,0 +1,89 @@
+/*
+ * 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.tests.widget;
+
+import com.android.contacts.tests.R;
+import com.android.contacts.widget.PinnedHeaderListView;
+
+import android.app.ListActivity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+/**
+ * An activity that demonstrates various use cases for the {@link PinnedHeaderListView}.
+ */
+public class PinnedHeaderUseCaseActivity extends ListActivity {
+
+    private static final int SINGLE_SHORT_SECTION_NO_HEADERS = 0;
+    private static final int TWO_SHORT_SECTIONS_WITH_HEADERS = 1;
+    private static final int FIVE_SHORT_SECTIONS_WITH_HEADERS = 2;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setListAdapter(new ArrayAdapter<String>(this, R.layout.intent_list_item,
+                getResources().getStringArray(R.array.pinnedHeaderUseCases)));
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        switch (position) {
+            case SINGLE_SHORT_SECTION_NO_HEADERS:
+                startActivity(
+                        new int[]{5},
+                        new String[]{"Line"},
+                        new boolean[]{false},
+                        new boolean[]{false},
+                        new int[]{0});
+                break;
+            case TWO_SHORT_SECTIONS_WITH_HEADERS:
+                startActivity(
+                        new int[]{2, 30},
+                        new String[]{"First", "Second"},
+                        new boolean[]{true, true},
+                        new boolean[]{false, false},
+                        new int[]{0, 2000});
+                break;
+            case FIVE_SHORT_SECTIONS_WITH_HEADERS:
+                startActivity(
+                        new int[]{1, 5, 5, 5, 5},
+                        new String[]{"First", "Second", "Third", "Fourth", "Fifth"},
+                        new boolean[]{true, true, true, true, true},
+                        new boolean[]{false, false, false, false, false},
+                        new int[]{0, 2000, 3000, 4000, 5000});
+                break;
+        }
+    }
+
+    private void startActivity(int[] counts, String[] names, boolean[] headers,
+            boolean[] showIfEmpty, int[] delays) {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName("com.android.contacts",
+                "com.android.contacts.widget.PinnedHeaderListDemoActivity"));
+        intent.putExtra("counts", counts);
+        intent.putExtra("names", names);
+        intent.putExtra("headers", headers);
+        intent.putExtra("showIfEmpty", showIfEmpty);
+        intent.putExtra("delays", delays);
+
+        startActivity(intent);
+    }
+}