Moving ContactListItemView and dependent classes.

Further clean-up of PhoneFavoriteFragment in Dialer app to move all necessary
dependencies into Contacts Common package.

Bug: 6993891
Change-Id: Ie310707da47d5e5c91e281d140f11e1eb47a5118
diff --git a/src/com/android/contacts/common/list/AutoScrollListView.java b/src/com/android/contacts/common/list/AutoScrollListView.java
new file mode 100644
index 0000000..ae7ca17
--- /dev/null
+++ b/src/com/android/contacts/common/list/AutoScrollListView.java
@@ -0,0 +1,117 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ListView;
+
+/**
+ * A ListView that can be asked to scroll (smoothly or otherwise) to a specific
+ * position.  This class takes advantage of similar functionality that exists
+ * in {@link ListView} and enhances it.
+ */
+public class AutoScrollListView extends ListView {
+
+    /**
+     * Position the element at about 1/3 of the list height
+     */
+    private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f;
+
+    private int mRequestedScrollPosition = -1;
+    private boolean mSmoothScrollRequested;
+
+    public AutoScrollListView(Context context) {
+        super(context);
+    }
+
+    public AutoScrollListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    /**
+     * Brings the specified position to view by optionally performing a jump-scroll maneuver:
+     * first it jumps to some position near the one requested and then does a smooth
+     * scroll to the requested position.  This creates an impression of full smooth
+     * scrolling without actually traversing the entire list.  If smooth scrolling is
+     * not requested, instantly positions the requested item at a preferred offset.
+     */
+    public void requestPositionToScreen(int position, boolean smoothScroll) {
+        mRequestedScrollPosition = position;
+        mSmoothScrollRequested = smoothScroll;
+        requestLayout();
+    }
+
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+        if (mRequestedScrollPosition == -1) {
+            return;
+        }
+
+        final int position = mRequestedScrollPosition;
+        mRequestedScrollPosition = -1;
+
+        int firstPosition = getFirstVisiblePosition() + 1;
+        int lastPosition = getLastVisiblePosition();
+        if (position >= firstPosition && position <= lastPosition) {
+            return; // Already on screen
+        }
+
+        final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP);
+        if (!mSmoothScrollRequested) {
+            setSelectionFromTop(position, offset);
+
+            // Since we have changed the scrolling position, we need to redo child layout
+            // Calling "requestLayout" in the middle of a layout pass has no effect,
+            // so we call layoutChildren explicitly
+            super.layoutChildren();
+
+        } else {
+            // We will first position the list a couple of screens before or after
+            // the new selection and then scroll smoothly to it.
+            int twoScreens = (lastPosition - firstPosition) * 2;
+            int preliminaryPosition;
+            if (position < firstPosition) {
+                preliminaryPosition = position + twoScreens;
+                if (preliminaryPosition >= getCount()) {
+                    preliminaryPosition = getCount() - 1;
+                }
+                if (preliminaryPosition < firstPosition) {
+                    setSelection(preliminaryPosition);
+                    super.layoutChildren();
+                }
+            } else {
+                preliminaryPosition = position - twoScreens;
+                if (preliminaryPosition < 0) {
+                    preliminaryPosition = 0;
+                }
+                if (preliminaryPosition > lastPosition) {
+                    setSelection(preliminaryPosition);
+                    super.layoutChildren();
+                }
+            }
+
+
+            smoothScrollToPositionFromTop(position, offset);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactEntryListAdapter.java b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
new file mode 100644
index 0000000..3653796
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
@@ -0,0 +1,672 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.QuickContactBadge;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.R;
+
+import java.util.HashSet;
+
+/**
+ * Common base class for various contact-related lists, e.g. contact list, phone number list
+ * etc.
+ */
+public abstract class ContactEntryListAdapter extends IndexerListAdapter {
+
+    private static final String TAG = "ContactEntryListAdapter";
+
+    /**
+     * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
+     * be included in the search.
+     */
+    public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
+
+    private int mDisplayOrder;
+    private int mSortOrder;
+
+    private boolean mDisplayPhotos;
+    private boolean mQuickContactEnabled;
+
+    /**
+     * indicates if contact queries include profile
+     */
+    private boolean mIncludeProfile;
+
+    /**
+     * indicates if query results includes a profile
+     */
+    private boolean mProfileExists;
+
+    private ContactPhotoManager mPhotoLoader;
+
+    private String mQueryString;
+    private char[] mUpperCaseQueryString;
+    private boolean mSearchMode;
+    private int mDirectorySearchMode;
+    private int mDirectoryResultLimit = Integer.MAX_VALUE;
+
+    private boolean mLoading = true;
+    private boolean mEmptyListEnabled = true;
+
+    private boolean mSelectionVisible;
+
+    private ContactListFilter mFilter;
+    private String mContactsCount = "";
+    private boolean mDarkTheme = false;
+
+    /** Resource used to provide header-text for default filter. */
+    private CharSequence mDefaultFilterHeaderText;
+
+    public ContactEntryListAdapter(Context context) {
+        super(context);
+        addPartitions();
+        setDefaultFilterHeaderText(R.string.local_search_label);
+    }
+
+    protected void setDefaultFilterHeaderText(int resourceId) {
+        mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
+    }
+
+    @Override
+    protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
+        return new ContactListPinnedHeaderView(context, null);
+    }
+
+    @Override
+    protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
+        ((ContactListPinnedHeaderView)pinnedHeaderView).setSectionHeader(title);
+    }
+
+    @Override
+    protected void setPinnedHeaderContactsCount(View header) {
+        // Update the header with the contacts count only if a profile header exists
+        // otherwise, the contacts count are shown in the empty profile header view
+        if (mProfileExists) {
+            ((ContactListPinnedHeaderView)header).setCountView(mContactsCount);
+        } else {
+            clearPinnedHeaderContactsCount(header);
+        }
+    }
+
+    @Override
+    protected void clearPinnedHeaderContactsCount(View header) {
+        ((ContactListPinnedHeaderView)header).setCountView(null);
+    }
+
+    protected void addPartitions() {
+        addPartition(createDefaultDirectoryPartition());
+    }
+
+    protected DirectoryPartition createDefaultDirectoryPartition() {
+        DirectoryPartition partition = new DirectoryPartition(true, true);
+        partition.setDirectoryId(Directory.DEFAULT);
+        partition.setDirectoryType(getContext().getString(R.string.contactsList));
+        partition.setPriorityDirectory(true);
+        partition.setPhotoSupported(true);
+        return partition;
+    }
+
+    /**
+     * Remove all directories after the default directory. This is typically used when contacts
+     * list screens are asked to exit the search mode and thus need to remove all remote directory
+     * results for the search.
+     *
+     * This code assumes that the default directory and directories before that should not be
+     * deleted (e.g. Join screen has "suggested contacts" directory before the default director,
+     * and we should not remove the directory).
+     */
+    public void removeDirectoriesAfterDefault() {
+        final int partitionCount = getPartitionCount();
+        for (int i = partitionCount - 1; i >= 0; i--) {
+            final Partition partition = getPartition(i);
+            if ((partition instanceof DirectoryPartition)
+                    && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
+                break;
+            } else {
+                removePartition(i);
+            }
+        }
+    }
+
+    private int getPartitionByDirectoryId(long id) {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                if (((DirectoryPartition)partition).getDirectoryId() == id) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    public abstract String getContactDisplayName(int position);
+    public abstract void configureLoader(CursorLoader loader, long directoryId);
+
+    /**
+     * Marks all partitions as "loading"
+     */
+    public void onDataReload() {
+        boolean notify = false;
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+                if (!directoryPartition.isLoading()) {
+                    notify = true;
+                }
+                directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+            }
+        }
+        if (notify) {
+            notifyDataSetChanged();
+        }
+    }
+
+    @Override
+    public void clearPartitions() {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+                directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+            }
+        }
+        super.clearPartitions();
+    }
+
+    public boolean isSearchMode() {
+        return mSearchMode;
+    }
+
+    public void setSearchMode(boolean flag) {
+        mSearchMode = flag;
+    }
+
+    public String getQueryString() {
+        return mQueryString;
+    }
+
+    public void setQueryString(String queryString) {
+        mQueryString = queryString;
+        if (TextUtils.isEmpty(queryString)) {
+            mUpperCaseQueryString = null;
+        } else {
+            mUpperCaseQueryString = queryString.toUpperCase().toCharArray();
+        }
+    }
+
+    public char[] getUpperCaseQueryString() {
+        return mUpperCaseQueryString;
+    }
+
+    public int getDirectorySearchMode() {
+        return mDirectorySearchMode;
+    }
+
+    public void setDirectorySearchMode(int mode) {
+        mDirectorySearchMode = mode;
+    }
+
+    public int getDirectoryResultLimit() {
+        return mDirectoryResultLimit;
+    }
+
+    public void setDirectoryResultLimit(int limit) {
+        this.mDirectoryResultLimit = limit;
+    }
+
+    public int getContactNameDisplayOrder() {
+        return mDisplayOrder;
+    }
+
+    public void setContactNameDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+    }
+
+    public int getSortOrder() {
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+    }
+
+    public void setPhotoLoader(ContactPhotoManager photoLoader) {
+        mPhotoLoader = photoLoader;
+    }
+
+    protected ContactPhotoManager getPhotoLoader() {
+        return mPhotoLoader;
+    }
+
+    public boolean getDisplayPhotos() {
+        return mDisplayPhotos;
+    }
+
+    public void setDisplayPhotos(boolean displayPhotos) {
+        mDisplayPhotos = displayPhotos;
+    }
+
+    public boolean isEmptyListEnabled() {
+        return mEmptyListEnabled;
+    }
+
+    public void setEmptyListEnabled(boolean flag) {
+        mEmptyListEnabled = flag;
+    }
+
+    public boolean isSelectionVisible() {
+        return mSelectionVisible;
+    }
+
+    public void setSelectionVisible(boolean flag) {
+        this.mSelectionVisible = flag;
+    }
+
+    public boolean isQuickContactEnabled() {
+        return mQuickContactEnabled;
+    }
+
+    public void setQuickContactEnabled(boolean quickContactEnabled) {
+        mQuickContactEnabled = quickContactEnabled;
+    }
+
+    public boolean shouldIncludeProfile() {
+        return mIncludeProfile;
+    }
+
+    public void setIncludeProfile(boolean includeProfile) {
+        mIncludeProfile = includeProfile;
+    }
+
+    public void setProfileExists(boolean exists) {
+        mProfileExists = exists;
+        // Stick the "ME" header for the profile
+        if (exists) {
+            SectionIndexer indexer = getIndexer();
+            if (indexer != null) {
+                ((ContactsSectionIndexer) indexer).setProfileHeader(
+                        getContext().getString(R.string.user_profile_contacts_list_header));
+            }
+        }
+    }
+
+    public boolean hasProfile() {
+        return mProfileExists;
+    }
+
+    public void setDarkTheme(boolean value) {
+        mDarkTheme = value;
+    }
+
+    /**
+     * Updates partitions according to the directory meta-data contained in the supplied
+     * cursor.
+     */
+    public void changeDirectories(Cursor cursor) {
+        if (cursor.getCount() == 0) {
+            // Directory table must have at least local directory, without which this adapter will
+            // enter very weird state.
+            Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " +
+                    "no directory entries.", new RuntimeException());
+            return;
+        }
+        HashSet<Long> directoryIds = new HashSet<Long>();
+
+        int idColumnIndex = cursor.getColumnIndex(Directory._ID);
+        int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
+        int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
+        int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
+
+        // TODO preserve the order of partition to match those of the cursor
+        // Phase I: add new directories
+        cursor.moveToPosition(-1);
+        while (cursor.moveToNext()) {
+            long id = cursor.getLong(idColumnIndex);
+            directoryIds.add(id);
+            if (getPartitionByDirectoryId(id) == -1) {
+                DirectoryPartition partition = new DirectoryPartition(false, true);
+                partition.setDirectoryId(id);
+                partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
+                partition.setDisplayName(cursor.getString(displayNameColumnIndex));
+                int photoSupport = cursor.getInt(photoSupportColumnIndex);
+                partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
+                        || photoSupport == Directory.PHOTO_SUPPORT_FULL);
+                addPartition(partition);
+            }
+        }
+
+        // Phase II: remove deleted directories
+        int count = getPartitionCount();
+        for (int i = count; --i >= 0; ) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                long id = ((DirectoryPartition)partition).getDirectoryId();
+                if (!directoryIds.contains(id)) {
+                    removePartition(i);
+                }
+            }
+        }
+
+        invalidate();
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public void changeCursor(int partitionIndex, Cursor cursor) {
+        if (partitionIndex >= getPartitionCount()) {
+            // There is no partition for this data
+            return;
+        }
+
+        Partition partition = getPartition(partitionIndex);
+        if (partition instanceof DirectoryPartition) {
+            ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED);
+        }
+
+        if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
+            mPhotoLoader.refreshCache();
+        }
+
+        super.changeCursor(partitionIndex, cursor);
+
+        if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
+            updateIndexer(cursor);
+        }
+    }
+
+    public void changeCursor(Cursor cursor) {
+        changeCursor(0, cursor);
+    }
+
+    /**
+     * Updates the indexer, which is used to produce section headers.
+     */
+    private void updateIndexer(Cursor cursor) {
+        if (cursor == null) {
+            setIndexer(null);
+            return;
+        }
+
+        Bundle bundle = cursor.getExtras();
+        if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
+            String sections[] =
+                    bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+            int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+            setIndexer(new ContactsSectionIndexer(sections, counts));
+        } else {
+            setIndexer(null);
+        }
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        // We need a separate view type for each item type, plus another one for
+        // each type with header, plus one for "other".
+        return getItemViewTypeCount() * 2 + 1;
+    }
+
+    @Override
+    public int getItemViewType(int partitionIndex, int position) {
+        int type = super.getItemViewType(partitionIndex, position);
+        if (!isUserProfile(position)
+                && isSectionHeaderDisplayEnabled()
+                && partitionIndex == getIndexedPartition()) {
+            Placement placement = getItemPlacementInSection(position);
+            return placement.firstInSection ? type : getItemViewTypeCount() + type;
+        } else {
+            return type;
+        }
+    }
+
+    @Override
+    public boolean isEmpty() {
+        // TODO
+//        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+//            return true;
+//        }
+
+        if (!mEmptyListEnabled) {
+            return false;
+        } else if (isSearchMode()) {
+            return TextUtils.isEmpty(getQueryString());
+        } else if (mLoading) {
+            // We don't want the empty state to show when loading.
+            return false;
+        } else {
+            return super.isEmpty();
+        }
+    }
+
+    public boolean isLoading() {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition
+                    && ((DirectoryPartition) partition).isLoading()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean areAllPartitionsEmpty() {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            if (!isPartitionEmpty(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Changes visibility parameters for the default directory partition.
+     */
+    public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
+        int defaultPartitionIndex = -1;
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition &&
+                    ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
+                defaultPartitionIndex = i;
+                break;
+            }
+        }
+        if (defaultPartitionIndex != -1) {
+            setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
+            setHasHeader(defaultPartitionIndex, hasHeader);
+        }
+    }
+
+    @Override
+    protected View newHeaderView(Context context, int partition, Cursor cursor,
+            ViewGroup parent) {
+        LayoutInflater inflater = LayoutInflater.from(context);
+        return inflater.inflate(R.layout.directory_header, parent, false);
+    }
+
+    @Override
+    protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
+        Partition partition = getPartition(partitionIndex);
+        if (!(partition instanceof DirectoryPartition)) {
+            return;
+        }
+
+        DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+        long directoryId = directoryPartition.getDirectoryId();
+        TextView labelTextView = (TextView)view.findViewById(R.id.label);
+        TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
+        if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+            labelTextView.setText(mDefaultFilterHeaderText);
+            displayNameTextView.setText(null);
+        } else {
+            labelTextView.setText(R.string.directory_search_label);
+            String directoryName = directoryPartition.getDisplayName();
+            String displayName = !TextUtils.isEmpty(directoryName)
+                    ? directoryName
+                    : directoryPartition.getDirectoryType();
+            displayNameTextView.setText(displayName);
+        }
+
+        TextView countText = (TextView)view.findViewById(R.id.count);
+        if (directoryPartition.isLoading()) {
+            countText.setText(R.string.search_results_searching);
+        } else {
+            int count = cursor == null ? 0 : cursor.getCount();
+            if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
+                    && count >= getDirectoryResultLimit()) {
+                countText.setText(mContext.getString(
+                        R.string.foundTooManyContacts, getDirectoryResultLimit()));
+            } else {
+                countText.setText(getQuantityText(
+                        count, R.string.listFoundAllContactsZero, R.plurals.searchFoundContacts));
+            }
+        }
+    }
+
+    /**
+     * Checks whether the contact entry at the given position represents the user's profile.
+     */
+    protected boolean isUserProfile(int position) {
+        // The profile only ever appears in the first position if it is present.  So if the position
+        // is anything beyond 0, it can't be the profile.
+        boolean isUserProfile = false;
+        if (position == 0) {
+            int partition = getPartitionForPosition(position);
+            if (partition >= 0) {
+                // Save the old cursor position - the call to getItem() may modify the cursor
+                // position.
+                int offset = getCursor(partition).getPosition();
+                Cursor cursor = (Cursor) getItem(position);
+                if (cursor != null) {
+                    int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
+                    if (profileColumnIndex != -1) {
+                        isUserProfile = cursor.getInt(profileColumnIndex) == 1;
+                    }
+                    // Restore the old cursor position.
+                    cursor.moveToPosition(offset);
+                }
+            }
+        }
+        return isUserProfile;
+    }
+
+    // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
+    public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
+        if (count == 0) {
+            return getContext().getString(zeroResourceId);
+        } else {
+            String format = getContext().getResources()
+                    .getQuantityText(pluralResourceId, count).toString();
+            return String.format(format, count);
+        }
+    }
+
+    public boolean isPhotoSupported(int partitionIndex) {
+        Partition partition = getPartition(partitionIndex);
+        if (partition instanceof DirectoryPartition) {
+            return ((DirectoryPartition) partition).isPhotoSupported();
+        }
+        return true;
+    }
+
+    /**
+     * Returns the currently selected filter.
+     */
+    public ContactListFilter getFilter() {
+        return mFilter;
+    }
+
+    public void setFilter(ContactListFilter filter) {
+        mFilter = filter;
+    }
+
+    // TODO: move sharable logic (bindXX() methods) to here with extra arguments
+
+    /**
+     * Loads the photo for the quick contact view and assigns the contact uri.
+     * @param photoIdColumn Index of the photo id column
+     * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
+     * @param contactIdColumn Index of the contact id column
+     * @param lookUpKeyColumn Index of the lookup key column
+     */
+    protected void bindQuickContact(final ContactListItemView view, int partitionIndex,
+            Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn,
+            int lookUpKeyColumn) {
+        long photoId = 0;
+        if (!cursor.isNull(photoIdColumn)) {
+            photoId = cursor.getLong(photoIdColumn);
+        }
+
+        QuickContactBadge quickContact = view.getQuickContact();
+        quickContact.assignContactUri(
+                getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
+
+        if (photoId != 0 || photoUriColumn == -1) {
+            getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme);
+        } else {
+            final String photoUriString = cursor.getString(photoUriColumn);
+            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+            getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme);
+        }
+
+    }
+
+    protected Uri getContactUri(int partitionIndex, Cursor cursor,
+            int contactIdColumn, int lookUpKeyColumn) {
+        long contactId = cursor.getLong(contactIdColumn);
+        String lookupKey = cursor.getString(lookUpKeyColumn);
+        Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+        if (directoryId != Directory.DEFAULT) {
+            uri = uri.buildUpon().appendQueryParameter(
+                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
+        }
+        return uri;
+    }
+
+    public void setContactsCount(String count) {
+        mContactsCount = count;
+    }
+
+    public String getContactsCount() {
+        return mContactsCount;
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListAdapter.java b/src/com/android/contacts/common/list/ContactListAdapter.java
new file mode 100644
index 0000000..1be48c4
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListAdapter.java
@@ -0,0 +1,362 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.SearchSnippetColumns;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import com.android.contacts.common.R;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ * Also includes support for including the {@link ContactsContract.Profile} record in the
+ * list.
+ */
+public abstract class ContactListAdapter extends ContactEntryListAdapter {
+
+    protected static class ContactQuery {
+        private static final String[] CONTACT_PROJECTION_PRIMARY = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_PRIMARY,          // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.IS_USER_PROFILE,               // 7
+        };
+
+        private static final String[] CONTACT_PROJECTION_ALTERNATIVE = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.IS_USER_PROFILE,               // 7
+        };
+
+        private static final String[] FILTER_PROJECTION_PRIMARY = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_PRIMARY,          // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.IS_USER_PROFILE,               // 7
+            SearchSnippetColumns.SNIPPET,           // 8
+        };
+
+        private static final String[] FILTER_PROJECTION_ALTERNATIVE = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.IS_USER_PROFILE,               // 7
+            SearchSnippetColumns.SNIPPET,           // 8
+        };
+
+        public static final int CONTACT_ID               = 0;
+        public static final int CONTACT_DISPLAY_NAME     = 1;
+        public static final int CONTACT_PRESENCE_STATUS  = 2;
+        public static final int CONTACT_CONTACT_STATUS   = 3;
+        public static final int CONTACT_PHOTO_ID         = 4;
+        public static final int CONTACT_PHOTO_URI        = 5;
+        public static final int CONTACT_LOOKUP_KEY       = 6;
+        public static final int CONTACT_IS_USER_PROFILE  = 7;
+        public static final int CONTACT_SNIPPET          = 8;
+    }
+
+    private CharSequence mUnknownNameText;
+
+    private long mSelectedContactDirectoryId;
+    private String mSelectedContactLookupKey;
+    private long mSelectedContactId;
+
+    public ContactListAdapter(Context context) {
+        super(context);
+
+        mUnknownNameText = context.getText(R.string.missing_name);
+    }
+
+    public CharSequence getUnknownNameText() {
+        return mUnknownNameText;
+    }
+
+    public long getSelectedContactDirectoryId() {
+        return mSelectedContactDirectoryId;
+    }
+
+    public String getSelectedContactLookupKey() {
+        return mSelectedContactLookupKey;
+    }
+
+    public long getSelectedContactId() {
+        return mSelectedContactId;
+    }
+
+    public void setSelectedContact(long selectedDirectoryId, String lookupKey, long contactId) {
+        mSelectedContactDirectoryId = selectedDirectoryId;
+        mSelectedContactLookupKey = lookupKey;
+        mSelectedContactId = contactId;
+    }
+
+    protected static Uri buildSectionIndexerUri(Uri uri) {
+        return uri.buildUpon()
+                .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+    }
+
+    @Override
+    public String getContactDisplayName(int position) {
+        return ((Cursor) getItem(position)).getString(ContactQuery.CONTACT_DISPLAY_NAME);
+    }
+
+    /**
+     * Builds the {@link Contacts#CONTENT_LOOKUP_URI} for the given
+     * {@link ListView} position.
+     */
+    public Uri getContactUri(int position) {
+        int partitionIndex = getPartitionForPosition(position);
+        Cursor item = (Cursor)getItem(position);
+        return item != null ? getContactUri(partitionIndex, item) : null;
+    }
+
+    public Uri getContactUri(int partitionIndex, Cursor cursor) {
+        long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+        String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
+        Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+        if (directoryId != Directory.DEFAULT) {
+            uri = uri.buildUpon().appendQueryParameter(
+                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
+        }
+        return uri;
+    }
+
+    /**
+     * Returns true if the specified contact is selected in the list. For a
+     * contact to be shown as selected, we need both the directory and and the
+     * lookup key to be the same. We are paying no attention to the contactId,
+     * because it is volatile, especially in the case of directories.
+     */
+    public boolean isSelectedContact(int partitionIndex, Cursor cursor) {
+        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+        if (getSelectedContactDirectoryId() != directoryId) {
+            return false;
+        }
+        String lookupKey = getSelectedContactLookupKey();
+        if (lookupKey != null && TextUtils.equals(lookupKey,
+                cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY))) {
+            return true;
+        }
+
+        return directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
+                && getSelectedContactId() == cursor.getLong(ContactQuery.CONTACT_ID);
+    }
+
+    @Override
+    protected View newView(Context context, int partition, Cursor cursor, int position,
+            ViewGroup parent) {
+        ContactListItemView view = new ContactListItemView(context, null);
+        view.setUnknownNameText(mUnknownNameText);
+        view.setQuickContactEnabled(isQuickContactEnabled());
+        view.setActivatedStateSupported(isSelectionVisible());
+        return view;
+    }
+
+    protected void bindSectionHeaderAndDivider(ContactListItemView view, int position,
+            Cursor cursor) {
+        if (isSectionHeaderDisplayEnabled()) {
+            Placement placement = getItemPlacementInSection(position);
+
+            // First position, set the contacts number string
+            if (position == 0 && cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1) {
+                view.setCountView(getContactsCount());
+            } else {
+                view.setCountView(null);
+            }
+            view.setSectionHeader(placement.sectionHeader);
+            view.setDividerVisible(!placement.lastInSection);
+        } else {
+            view.setSectionHeader(null);
+            view.setDividerVisible(true);
+            view.setCountView(null);
+        }
+    }
+
+    protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
+        if (!isPhotoSupported(partitionIndex)) {
+            view.removePhotoView();
+            return;
+        }
+
+        // Set the photo, if available
+        long photoId = 0;
+        if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) {
+            photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID);
+        }
+
+        if (photoId != 0) {
+            getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
+        } else {
+            final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI);
+            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+            getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false);
+        }
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(
+                cursor, ContactQuery.CONTACT_DISPLAY_NAME, getContactNameDisplayOrder());
+        // Note: we don't show phonetic any more (See issue 5265330)
+    }
+
+    protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) {
+        view.showPresenceAndStatusMessage(cursor, ContactQuery.CONTACT_PRESENCE_STATUS,
+                ContactQuery.CONTACT_CONTACT_STATUS);
+    }
+
+    protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+        view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET);
+    }
+
+    public int getSelectedContactPosition() {
+        if (mSelectedContactLookupKey == null && mSelectedContactId == 0) {
+            return -1;
+        }
+
+        Cursor cursor = null;
+        int partitionIndex = -1;
+        int partitionCount = getPartitionCount();
+        for (int i = 0; i < partitionCount; i++) {
+            DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+            if (partition.getDirectoryId() == mSelectedContactDirectoryId) {
+                partitionIndex = i;
+                break;
+            }
+        }
+        if (partitionIndex == -1) {
+            return -1;
+        }
+
+        cursor = getCursor(partitionIndex);
+        if (cursor == null) {
+            return -1;
+        }
+
+        cursor.moveToPosition(-1);      // Reset cursor
+        int offset = -1;
+        while (cursor.moveToNext()) {
+            if (mSelectedContactLookupKey != null) {
+                String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
+                if (mSelectedContactLookupKey.equals(lookupKey)) {
+                    offset = cursor.getPosition();
+                    break;
+                }
+            }
+            if (mSelectedContactId != 0 && (mSelectedContactDirectoryId == Directory.DEFAULT
+                    || mSelectedContactDirectoryId == Directory.LOCAL_INVISIBLE)) {
+                long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+                if (contactId == mSelectedContactId) {
+                    offset = cursor.getPosition();
+                    break;
+                }
+            }
+        }
+        if (offset == -1) {
+            return -1;
+        }
+
+        int position = getPositionForPartition(partitionIndex) + offset;
+        if (hasHeader(partitionIndex)) {
+            position++;
+        }
+        return position;
+    }
+
+    public boolean hasValidSelection() {
+        return getSelectedContactPosition() != -1;
+    }
+
+    public Uri getFirstContactUri() {
+        int partitionCount = getPartitionCount();
+        for (int i = 0; i < partitionCount; i++) {
+            DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+            if (partition.isLoading()) {
+                continue;
+            }
+
+            Cursor cursor = getCursor(i);
+            if (cursor == null) {
+                continue;
+            }
+
+            if (!cursor.moveToFirst()) {
+                continue;
+            }
+
+            return getContactUri(i, cursor);
+        }
+
+        return null;
+    }
+
+    @Override
+    public void changeCursor(int partitionIndex, Cursor cursor) {
+        super.changeCursor(partitionIndex, cursor);
+
+        // Check if a profile exists
+        if (cursor != null && cursor.getCount() > 0) {
+            cursor.moveToFirst();
+            setProfileExists(cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1);
+        }
+    }
+
+    /**
+     * @return Projection useful for children.
+     */
+    protected final String[] getProjection(boolean forSearch) {
+        final int sortOrder = getContactNameDisplayOrder();
+        if (forSearch) {
+            if (sortOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+                return ContactQuery.FILTER_PROJECTION_PRIMARY;
+            } else {
+                return ContactQuery.FILTER_PROJECTION_ALTERNATIVE;
+            }
+        } else {
+            if (sortOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+                return ContactQuery.CONTACT_PROJECTION_PRIMARY;
+            } else {
+                return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE;
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListFilter.java b/src/com/android/contacts/common/list/ContactListFilter.java
new file mode 100644
index 0000000..f81ea74
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListFilter.java
@@ -0,0 +1,306 @@
+/*
+ * 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.common.list;
+
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+/**
+ * Contact list filter parameters.
+ */
+public final class ContactListFilter implements Comparable<ContactListFilter>, Parcelable {
+
+    public static final int FILTER_TYPE_DEFAULT = -1;
+    public static final int FILTER_TYPE_ALL_ACCOUNTS = -2;
+    public static final int FILTER_TYPE_CUSTOM = -3;
+    public static final int FILTER_TYPE_STARRED = -4;
+    public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5;
+    public static final int FILTER_TYPE_SINGLE_CONTACT = -6;
+
+    public static final int FILTER_TYPE_ACCOUNT = 0;
+
+    /**
+     * Obsolete filter which had been used in Honeycomb. This may be stored in
+     * {@link SharedPreferences}, but should be replaced with ALL filter when it is found.
+     *
+     * TODO: "group" filter and relevant variables are all obsolete. Remove them.
+     */
+    private static final int FILTER_TYPE_GROUP = 1;
+
+    private static final String KEY_FILTER_TYPE = "filter.type";
+    private static final String KEY_ACCOUNT_NAME = "filter.accountName";
+    private static final String KEY_ACCOUNT_TYPE = "filter.accountType";
+    private static final String KEY_DATA_SET = "filter.dataSet";
+
+    public final int filterType;
+    public final String accountType;
+    public final String accountName;
+    public final String dataSet;
+    public final Drawable icon;
+    private String mId;
+
+    public ContactListFilter(int filterType, String accountType, String accountName, String dataSet,
+            Drawable icon) {
+        this.filterType = filterType;
+        this.accountType = accountType;
+        this.accountName = accountName;
+        this.dataSet = dataSet;
+        this.icon = icon;
+    }
+
+    public static ContactListFilter createFilterWithType(int filterType) {
+        return new ContactListFilter(filterType, null, null, null, null);
+    }
+
+    public static ContactListFilter createAccountFilter(String accountType, String accountName,
+            String dataSet, Drawable icon) {
+        return new ContactListFilter(ContactListFilter.FILTER_TYPE_ACCOUNT, accountType,
+                accountName, dataSet, icon);
+    }
+
+    /**
+     * Returns true if this filter is based on data and may become invalid over time.
+     */
+    public boolean isValidationRequired() {
+        return filterType == FILTER_TYPE_ACCOUNT;
+    }
+
+    @Override
+    public String toString() {
+        switch (filterType) {
+            case FILTER_TYPE_DEFAULT:
+                return "default";
+            case FILTER_TYPE_ALL_ACCOUNTS:
+                return "all_accounts";
+            case FILTER_TYPE_CUSTOM:
+                return "custom";
+            case FILTER_TYPE_STARRED:
+                return "starred";
+            case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+                return "with_phones";
+            case FILTER_TYPE_SINGLE_CONTACT:
+                return "single";
+            case FILTER_TYPE_ACCOUNT:
+                return "account: " + accountType + (dataSet != null ? "/" + dataSet : "")
+                        + " " + accountName;
+        }
+        return super.toString();
+    }
+
+    @Override
+    public int compareTo(ContactListFilter another) {
+        int res = accountName.compareTo(another.accountName);
+        if (res != 0) {
+            return res;
+        }
+
+        res = accountType.compareTo(another.accountType);
+        if (res != 0) {
+            return res;
+        }
+
+        return filterType - another.filterType;
+    }
+
+    @Override
+    public int hashCode() {
+        int code = filterType;
+        if (accountType != null) {
+            code = code * 31 + accountType.hashCode();
+            code = code * 31 + accountName.hashCode();
+        }
+        if (dataSet != null) {
+            code = code * 31 + dataSet.hashCode();
+        }
+        return code;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (!(other instanceof ContactListFilter)) {
+            return false;
+        }
+
+        ContactListFilter otherFilter = (ContactListFilter) other;
+        if (filterType != otherFilter.filterType
+                || !TextUtils.equals(accountName, otherFilter.accountName)
+                || !TextUtils.equals(accountType, otherFilter.accountType)
+                || !TextUtils.equals(dataSet, otherFilter.dataSet)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Store the given {@link ContactListFilter} to preferences. If the requested filter is
+     * of type {@link #FILTER_TYPE_SINGLE_CONTACT} then do not save it to preferences because
+     * it is a temporary state.
+     */
+    public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) {
+        if (filter != null && filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+            return;
+        }
+        prefs.edit()
+            .putInt(KEY_FILTER_TYPE, filter == null ? FILTER_TYPE_DEFAULT : filter.filterType)
+            .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName)
+            .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType)
+            .putString(KEY_DATA_SET, filter == null ? null : filter.dataSet)
+            .apply();
+    }
+
+    /**
+     * Try to obtain ContactListFilter object saved in SharedPreference.
+     * If there's no info there, return ALL filter instead.
+     */
+    public static ContactListFilter restoreDefaultPreferences(SharedPreferences prefs) {
+        ContactListFilter filter = restoreFromPreferences(prefs);
+        if (filter == null) {
+            filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+        }
+        // "Group" filter is obsolete and thus is not exposed anymore. The "single contact mode"
+        // should also not be stored in preferences anymore since it is a temporary state.
+        if (filter.filterType == FILTER_TYPE_GROUP ||
+                filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+            filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+        }
+        return filter;
+    }
+
+    private static ContactListFilter restoreFromPreferences(SharedPreferences prefs) {
+        int filterType = prefs.getInt(KEY_FILTER_TYPE, FILTER_TYPE_DEFAULT);
+        if (filterType == FILTER_TYPE_DEFAULT) {
+            return null;
+        }
+
+        String accountName = prefs.getString(KEY_ACCOUNT_NAME, null);
+        String accountType = prefs.getString(KEY_ACCOUNT_TYPE, null);
+        String dataSet = prefs.getString(KEY_DATA_SET, null);
+        return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+    }
+
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(filterType);
+        dest.writeString(accountName);
+        dest.writeString(accountType);
+        dest.writeString(dataSet);
+    }
+
+    public static final Parcelable.Creator<ContactListFilter> CREATOR =
+            new Parcelable.Creator<ContactListFilter>() {
+        @Override
+        public ContactListFilter createFromParcel(Parcel source) {
+            int filterType = source.readInt();
+            String accountName = source.readString();
+            String accountType = source.readString();
+            String dataSet = source.readString();
+            return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+        }
+
+        @Override
+        public ContactListFilter[] newArray(int size) {
+            return new ContactListFilter[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns a string that can be used as a stable persistent identifier for this filter.
+     */
+    public String getId() {
+        if (mId == null) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(filterType);
+            if (accountType != null) {
+                sb.append('-').append(accountType);
+            }
+            if (dataSet != null) {
+                sb.append('/').append(dataSet);
+            }
+            if (accountName != null) {
+                sb.append('-').append(accountName.replace('-', '_'));
+            }
+            mId = sb.toString();
+        }
+        return mId;
+    }
+
+    /**
+     * Adds the account query parameters to the given {@code uriBuilder}.
+     *
+     * @throws IllegalStateException if the filter type is not {@link #FILTER_TYPE_ACCOUNT}.
+     */
+    public Uri.Builder addAccountQueryParameterToUrl(Uri.Builder uriBuilder) {
+        if (filterType != FILTER_TYPE_ACCOUNT) {
+            throw new IllegalStateException("filterType must be FILTER_TYPE_ACCOUNT");
+        }
+        uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
+        uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
+        if (!TextUtils.isEmpty(dataSet)) {
+            uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet);
+        }
+        return uriBuilder;
+    }
+
+    public String toDebugString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")");
+        if (filterType == FILTER_TYPE_ACCOUNT) {
+            builder.append(", accountType: " + accountType)
+                    .append(", accountName: " + accountName)
+                    .append(", dataSet: " + dataSet);
+        }
+        builder.append(", icon: " + icon + "]");
+        return builder.toString();
+    }
+
+    public static final String filterTypeToString(int filterType) {
+        switch (filterType) {
+            case FILTER_TYPE_DEFAULT:
+                return "FILTER_TYPE_DEFAULT";
+            case FILTER_TYPE_ALL_ACCOUNTS:
+                return "FILTER_TYPE_ALL_ACCOUNTS";
+            case FILTER_TYPE_CUSTOM:
+                return "FILTER_TYPE_CUSTOM";
+            case FILTER_TYPE_STARRED:
+                return "FILTER_TYPE_STARRED";
+            case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+                return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY";
+            case FILTER_TYPE_SINGLE_CONTACT:
+                return "FILTER_TYPE_SINGLE_CONTACT";
+            case FILTER_TYPE_ACCOUNT:
+                return "FILTER_TYPE_ACCOUNT";
+            default:
+                return "(unknown)";
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
new file mode 100644
index 0000000..d89aea9
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -0,0 +1,1231 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView.SelectionBoundsAdjuster;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPresenceIconUtil;
+import com.android.contacts.common.ContactStatusUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.format.PrefixHighlighter;
+
+/**
+ * A custom view for an item in the contact list.
+ * The view contains the contact's photo, a set of text views (for name, status, etc...) and
+ * icons for presence and call.
+ * The view uses no XML file for layout and all the measurements and layouts are done
+ * in the onMeasure and onLayout methods.
+ *
+ * The layout puts the contact's photo on the right side of the view, the call icon (if present)
+ * to the left of the photo, the text lines are aligned to the left and the presence icon (if
+ * present) is set to the left of the status line.
+ *
+ * The layout also supports a header (used as a header of a group of contacts) that is above the
+ * contact's data and a divider between contact view.
+ */
+
+public class ContactListItemView extends ViewGroup
+        implements SelectionBoundsAdjuster {
+
+    // Style values for layout and appearance
+    // The initialized values are defaults if none is provided through xml.
+    private int mPreferredHeight = 0;
+    private int mGapBetweenImageAndText = 0;
+    private int mGapBetweenLabelAndData = 0;
+    private int mPresenceIconMargin = 4;
+    private int mPresenceIconSize = 16;
+    private int mHeaderTextColor = Color.BLACK;
+    private int mHeaderTextIndent = 0;
+    private int mHeaderTextSize = 12;
+    private int mHeaderUnderlineHeight = 1;
+    private int mHeaderUnderlineColor = 0;
+    private int mCountViewTextSize = 12;
+    private int mContactsCountTextColor = Color.BLACK;
+    private int mTextIndent = 0;
+    private Drawable mActivatedBackgroundDrawable;
+
+    /**
+     * Used with {@link #mLabelView}, specifying the width ratio between label and data.
+     */
+    private int mLabelViewWidthWeight = 3;
+    /**
+     * Used with {@link #mDataView}, specifying the width ratio between label and data.
+     */
+    private int mDataViewWidthWeight = 5;
+
+    // Will be used with adjustListItemSelectionBounds().
+    private int mSelectionBoundsMarginLeft;
+    private int mSelectionBoundsMarginRight;
+
+    // Horizontal divider between contact views.
+    private boolean mHorizontalDividerVisible = true;
+    private Drawable mHorizontalDividerDrawable;
+    private int mHorizontalDividerHeight;
+
+    /**
+     * Where to put contact photo. This affects the other Views' layout or look-and-feel.
+     */
+    public enum PhotoPosition {
+        LEFT,
+        RIGHT
+    }
+    public static final PhotoPosition DEFAULT_PHOTO_POSITION = PhotoPosition.RIGHT;
+    private PhotoPosition mPhotoPosition = DEFAULT_PHOTO_POSITION;
+
+    // Header layout data
+    private boolean mHeaderVisible;
+    private View mHeaderDivider;
+    private int mHeaderBackgroundHeight = 30;
+    private TextView mHeaderTextView;
+
+    // The views inside the contact view
+    private boolean mQuickContactEnabled = true;
+    private QuickContactBadge mQuickContact;
+    private ImageView mPhotoView;
+    private TextView mNameTextView;
+    private TextView mPhoneticNameTextView;
+    private TextView mLabelView;
+    private TextView mDataView;
+    private TextView mSnippetView;
+    private TextView mStatusView;
+    private TextView mCountView;
+    private ImageView mPresenceIcon;
+
+    private ColorStateList mSecondaryTextColor;
+
+    private char[] mHighlightedPrefix;
+
+    private int mDefaultPhotoViewSize = 0;
+    /**
+     * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
+     * to align other data in this View.
+     */
+    private int mPhotoViewWidth;
+    /**
+     * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
+     */
+    private int mPhotoViewHeight;
+
+    /**
+     * Only effective when {@link #mPhotoView} is null.
+     * When true all the Views on the right side of the photo should have horizontal padding on
+     * those left assuming there is a photo.
+     */
+    private boolean mKeepHorizontalPaddingForPhotoView;
+    /**
+     * Only effective when {@link #mPhotoView} is null.
+     */
+    private boolean mKeepVerticalPaddingForPhotoView;
+
+    /**
+     * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
+     * False indicates those values should be updated before being used in position calculation.
+     */
+    private boolean mPhotoViewWidthAndHeightAreReady = false;
+
+    private int mNameTextViewHeight;
+    private int mPhoneticNameTextViewHeight;
+    private int mLabelViewHeight;
+    private int mDataViewHeight;
+    private int mSnippetTextViewHeight;
+    private int mStatusTextViewHeight;
+
+    // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
+    // same row.
+    private int mLabelAndDataViewMaxHeight;
+
+    // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
+    // more efficient for each case or in general, and simplify the whole implementation.
+    // Note: if we're sure MARQUEE will be used every time, there's no reason to use
+    // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
+    // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
+    // TextView without any modification.
+    private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
+    private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
+
+    private boolean mActivatedStateSupported;
+
+    private Rect mBoundsWithoutHeader = new Rect();
+
+    /** A helper used to highlight a prefix in a text field. */
+    private PrefixHighlighter mPrefixHighlighter;
+    private CharSequence mUnknownNameText;
+
+    public ContactListItemView(Context context) {
+        super(context);
+        mContext = context;
+
+        mPrefixHighlighter = new PrefixHighlighter(Color.GREEN);
+    }
+
+    public ContactListItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+
+        // Read all style values
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+        mPreferredHeight = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
+        mActivatedBackgroundDrawable = a.getDrawable(
+                R.styleable.ContactListItemView_activated_background);
+        mHorizontalDividerDrawable = a.getDrawable(
+                R.styleable.ContactListItemView_list_item_divider);
+
+        mGapBetweenImageAndText = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
+                mGapBetweenImageAndText);
+        mGapBetweenLabelAndData = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
+                mGapBetweenLabelAndData);
+        mPresenceIconMargin = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_presence_icon_margin,
+                mPresenceIconMargin);
+        mPresenceIconSize = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize);
+        mDefaultPhotoViewSize = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
+        mHeaderTextIndent = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_header_text_indent, mHeaderTextIndent);
+        mHeaderTextColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_header_text_color, mHeaderTextColor);
+        mHeaderTextSize = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_header_text_size, mHeaderTextSize);
+        mHeaderBackgroundHeight = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_header_height, mHeaderBackgroundHeight);
+        mHeaderUnderlineHeight = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_header_underline_height,
+                mHeaderUnderlineHeight);
+        mHeaderUnderlineColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_header_underline_color,
+                mHeaderUnderlineColor);
+        mTextIndent = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
+        mCountViewTextSize = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_contacts_count_text_size,
+                mCountViewTextSize);
+        mContactsCountTextColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_contacts_count_text_color,
+                mContactsCountTextColor);
+        mDataViewWidthWeight = a.getInteger(
+                R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight);
+        mLabelViewWidthWeight = a.getInteger(
+                R.styleable.ContactListItemView_list_item_label_width_weight,
+                mLabelViewWidthWeight);
+
+        setPadding(
+                a.getDimensionPixelOffset(
+                        R.styleable.ContactListItemView_list_item_padding_left, 0),
+                a.getDimensionPixelOffset(
+                        R.styleable.ContactListItemView_list_item_padding_top, 0),
+                a.getDimensionPixelOffset(
+                        R.styleable.ContactListItemView_list_item_padding_right, 0),
+                a.getDimensionPixelOffset(
+                        R.styleable.ContactListItemView_list_item_padding_bottom, 0));
+
+        final int prefixHighlightColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_prefix_highlight_color, Color.GREEN);
+        mPrefixHighlighter = new PrefixHighlighter(prefixHighlightColor);
+        a.recycle();
+
+        a = getContext().obtainStyledAttributes(android.R.styleable.Theme);
+        mSecondaryTextColor = a.getColorStateList(android.R.styleable.Theme_textColorSecondary);
+        a.recycle();
+
+        mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
+
+        if (mActivatedBackgroundDrawable != null) {
+            mActivatedBackgroundDrawable.setCallback(this);
+        }
+    }
+
+    public void setUnknownNameText(CharSequence unknownNameText) {
+        mUnknownNameText = unknownNameText;
+    }
+
+    public void setQuickContactEnabled(boolean flag) {
+        mQuickContactEnabled = flag;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // We will match parent's width and wrap content vertically, but make sure
+        // height is no less than listPreferredItemHeight.
+        final int specWidth = resolveSize(0, widthMeasureSpec);
+        final int preferredHeight;
+        if (mHorizontalDividerVisible) {
+            preferredHeight = mPreferredHeight + mHorizontalDividerHeight;
+        } else {
+            preferredHeight = mPreferredHeight;
+        }
+
+        mNameTextViewHeight = 0;
+        mPhoneticNameTextViewHeight = 0;
+        mLabelViewHeight = 0;
+        mDataViewHeight = 0;
+        mLabelAndDataViewMaxHeight = 0;
+        mSnippetTextViewHeight = 0;
+        mStatusTextViewHeight = 0;
+
+        ensurePhotoViewSize();
+
+        // Width each TextView is able to use.
+        final int effectiveWidth;
+        // All the other Views will honor the photo, so available width for them may be shrunk.
+        if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
+            effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
+                    - (mPhotoViewWidth + mGapBetweenImageAndText);
+        } else {
+            effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
+        }
+
+        // Go over all visible text views and measure actual width of each of them.
+        // Also calculate their heights to get the total height for this entire view.
+
+        if (isVisible(mNameTextView)) {
+            // Caculate width for name text - this parallels similar measurement in onLayout.
+            int nameTextWidth = effectiveWidth;
+            if (mPhotoPosition != PhotoPosition.LEFT) {
+                nameTextWidth -= mTextIndent;
+            }
+            mNameTextView.measure(
+                    MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mNameTextViewHeight = mNameTextView.getMeasuredHeight();
+        }
+
+        if (isVisible(mPhoneticNameTextView)) {
+            mPhoneticNameTextView.measure(
+                    MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
+        }
+
+        // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
+        // we should ellipsize both using appropriate ratio.
+        final int dataWidth;
+        final int labelWidth;
+        if (isVisible(mDataView)) {
+            if (isVisible(mLabelView)) {
+                final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
+                dataWidth = ((totalWidth * mDataViewWidthWeight)
+                        / (mDataViewWidthWeight + mLabelViewWidthWeight));
+                labelWidth = ((totalWidth * mLabelViewWidthWeight) /
+                        (mDataViewWidthWeight + mLabelViewWidthWeight));
+            } else {
+                dataWidth = effectiveWidth;
+                labelWidth = 0;
+            }
+        } else {
+            dataWidth = 0;
+            if (isVisible(mLabelView)) {
+                labelWidth = effectiveWidth;
+            } else {
+                labelWidth = 0;
+            }
+        }
+
+        if (isVisible(mDataView)) {
+            mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mDataViewHeight = mDataView.getMeasuredHeight();
+        }
+
+        if (isVisible(mLabelView)) {
+            // For performance reason we don't want AT_MOST usually, but when the picture is
+            // on right, we need to use it anyway because mDataView is next to mLabelView.
+            final int mode = (mPhotoPosition == PhotoPosition.LEFT
+                    ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST);
+            mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, mode),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mLabelViewHeight = mLabelView.getMeasuredHeight();
+        }
+        mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
+
+        if (isVisible(mSnippetView)) {
+            mSnippetView.measure(
+                    MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
+        }
+
+        // Status view height is the biggest of the text view and the presence icon
+        if (isVisible(mPresenceIcon)) {
+            mPresenceIcon.measure(
+                    MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
+            mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
+        }
+
+        if (isVisible(mStatusView)) {
+            // Presence and status are in a same row, so status will be affected by icon size.
+            final int statusWidth;
+            if (isVisible(mPresenceIcon)) {
+                statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
+                        - mPresenceIconMargin);
+            } else {
+                statusWidth = effectiveWidth;
+            }
+            mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mStatusTextViewHeight =
+                    Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
+        }
+
+        // Calculate height including padding.
+        int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
+                mLabelAndDataViewMaxHeight +
+                mSnippetTextViewHeight + mStatusTextViewHeight);
+
+        // Make sure the height is at least as high as the photo
+        height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
+
+        // Add horizontal divider height
+        if (mHorizontalDividerVisible) {
+            height += mHorizontalDividerHeight;
+        }
+
+        // Make sure height is at least the preferred height
+        height = Math.max(height, preferredHeight);
+
+        // Add the height of the header if visible
+        if (mHeaderVisible) {
+            mHeaderTextView.measure(
+                    MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+            if (mCountView != null) {
+                mCountView.measure(
+                        MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.AT_MOST),
+                        MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+            }
+            mHeaderBackgroundHeight = Math.max(mHeaderBackgroundHeight,
+                    mHeaderTextView.getMeasuredHeight());
+            height += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+        }
+
+        setMeasuredDimension(specWidth, height);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int height = bottom - top;
+        final int width = right - left;
+
+        // Determine the vertical bounds by laying out the header first.
+        int topBound = 0;
+        int bottomBound = height;
+        int leftBound = getPaddingLeft();
+        int rightBound = width - getPaddingRight();
+
+        // Put the header in the top of the contact view (Text + underline view)
+        if (mHeaderVisible) {
+            mHeaderTextView.layout(leftBound + mHeaderTextIndent,
+                    0,
+                    rightBound,
+                    mHeaderBackgroundHeight);
+            if (mCountView != null) {
+                mCountView.layout(rightBound - mCountView.getMeasuredWidth(),
+                        0,
+                        rightBound,
+                        mHeaderBackgroundHeight);
+            }
+            mHeaderDivider.layout(leftBound,
+                    mHeaderBackgroundHeight,
+                    rightBound,
+                    mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+            topBound += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+        }
+
+        // Put horizontal divider at the bottom
+        if (mHorizontalDividerVisible) {
+            mHorizontalDividerDrawable.setBounds(
+                    leftBound,
+                    height - mHorizontalDividerHeight,
+                    rightBound,
+                    height);
+            bottomBound -= mHorizontalDividerHeight;
+        }
+
+        mBoundsWithoutHeader.set(0, topBound, width, bottomBound);
+
+        if (mActivatedStateSupported && isActivated()) {
+            mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
+        }
+
+        final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
+        if (mPhotoPosition == PhotoPosition.LEFT) {
+            // Photo is the left most view. All the other Views should on the right of the photo.
+            if (photoView != null) {
+                // Center the photo vertically
+                final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+                photoView.layout(
+                        leftBound,
+                        photoTop,
+                        leftBound + mPhotoViewWidth,
+                        photoTop + mPhotoViewHeight);
+                leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+            } else if (mKeepHorizontalPaddingForPhotoView) {
+                // Draw nothing but keep the padding.
+                leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+            }
+        } else {
+            // Photo is the right most view. Right bound should be adjusted that way.
+            if (photoView != null) {
+                // Center the photo vertically
+                final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+                photoView.layout(
+                        rightBound - mPhotoViewWidth,
+                        photoTop,
+                        rightBound,
+                        photoTop + mPhotoViewHeight);
+                rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+            }
+
+            // Add indent between left-most padding and texts.
+            leftBound += mTextIndent;
+        }
+
+        // Center text vertically
+        final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
+                mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
+        int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
+
+        // Layout all text view and presence icon
+        // Put name TextView first
+        if (isVisible(mNameTextView)) {
+            mNameTextView.layout(leftBound,
+                    textTopBound,
+                    rightBound,
+                    textTopBound + mNameTextViewHeight);
+            textTopBound += mNameTextViewHeight;
+        }
+
+        // Presence and status
+        int statusLeftBound = leftBound;
+        if (isVisible(mPresenceIcon)) {
+            int iconWidth = mPresenceIcon.getMeasuredWidth();
+            mPresenceIcon.layout(
+                    leftBound,
+                    textTopBound,
+                    leftBound + iconWidth,
+                    textTopBound + mStatusTextViewHeight);
+            statusLeftBound += (iconWidth + mPresenceIconMargin);
+        }
+
+        if (isVisible(mStatusView)) {
+            mStatusView.layout(statusLeftBound,
+                    textTopBound,
+                    rightBound,
+                    textTopBound + mStatusTextViewHeight);
+        }
+
+        if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
+            textTopBound += mStatusTextViewHeight;
+        }
+
+        // Rest of text views
+        int dataLeftBound = leftBound;
+        if (isVisible(mPhoneticNameTextView)) {
+            mPhoneticNameTextView.layout(leftBound,
+                    textTopBound,
+                    rightBound,
+                    textTopBound + mPhoneticNameTextViewHeight);
+            textTopBound += mPhoneticNameTextViewHeight;
+        }
+
+        // Label and Data align bottom.
+        if (isVisible(mLabelView)) {
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                // When photo is on left, label is placed on the right edge of the list item.
+                mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
+                        textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+                        rightBound,
+                        textTopBound + mLabelAndDataViewMaxHeight);
+                rightBound -= mLabelView.getMeasuredWidth();
+            } else {
+                // When photo is on right, label is placed on the left of data view.
+                dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+                mLabelView.layout(leftBound,
+                        textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+                        dataLeftBound,
+                        textTopBound + mLabelAndDataViewMaxHeight);
+                dataLeftBound += mGapBetweenLabelAndData;
+            }
+        }
+
+        if (isVisible(mDataView)) {
+            mDataView.layout(dataLeftBound,
+                    textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+                    rightBound,
+                    textTopBound + mLabelAndDataViewMaxHeight);
+        }
+        if (isVisible(mLabelView) || isVisible(mDataView)) {
+            textTopBound += mLabelAndDataViewMaxHeight;
+        }
+
+        if (isVisible(mSnippetView)) {
+            mSnippetView.layout(leftBound,
+                    textTopBound,
+                    rightBound,
+                    textTopBound + mSnippetTextViewHeight);
+        }
+    }
+
+    @Override
+    public void adjustListItemSelectionBounds(Rect bounds) {
+        bounds.top += mBoundsWithoutHeader.top;
+        bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
+        bounds.left += mSelectionBoundsMarginLeft;
+        bounds.right -= mSelectionBoundsMarginRight;
+    }
+
+    protected boolean isVisible(View view) {
+        return view != null && view.getVisibility() == View.VISIBLE;
+    }
+
+    /**
+     * Extracts width and height from the style
+     */
+    private void ensurePhotoViewSize() {
+        if (!mPhotoViewWidthAndHeightAreReady) {
+            mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
+            if (!mQuickContactEnabled && mPhotoView == null) {
+                if (!mKeepHorizontalPaddingForPhotoView) {
+                    mPhotoViewWidth = 0;
+                }
+                if (!mKeepVerticalPaddingForPhotoView) {
+                    mPhotoViewHeight = 0;
+                }
+            }
+
+            mPhotoViewWidthAndHeightAreReady = true;
+        }
+    }
+
+    protected void setDefaultPhotoViewSize(int pixels) {
+        mDefaultPhotoViewSize = pixels;
+    }
+
+    protected int getDefaultPhotoViewSize() {
+        return mDefaultPhotoViewSize;
+    }
+
+    /**
+     * Gets a LayoutParam that corresponds to the default photo size.
+     *
+     * @return A new LayoutParam.
+     */
+    private LayoutParams getDefaultPhotoLayoutParams() {
+        LayoutParams params = generateDefaultLayoutParams();
+        params.width = getDefaultPhotoViewSize();
+        params.height = params.width;
+        return params;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        if (mActivatedStateSupported) {
+            mActivatedBackgroundDrawable.setState(getDrawableState());
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mActivatedStateSupported) {
+            mActivatedBackgroundDrawable.jumpToCurrentState();
+        }
+    }
+
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        if (mActivatedStateSupported && isActivated()) {
+            mActivatedBackgroundDrawable.draw(canvas);
+        }
+        if (mHorizontalDividerVisible) {
+            mHorizontalDividerDrawable.draw(canvas);
+        }
+
+        super.dispatchDraw(canvas);
+    }
+
+    /**
+     * Sets the flag that determines whether a divider should drawn at the bottom
+     * of the view.
+     */
+    public void setDividerVisible(boolean visible) {
+        mHorizontalDividerVisible = visible;
+    }
+
+    /**
+     * Sets section header or makes it invisible if the title is null.
+     */
+    public void setSectionHeader(String title) {
+        if (!TextUtils.isEmpty(title)) {
+            if (mHeaderTextView == null) {
+                mHeaderTextView = new TextView(mContext);
+                mHeaderTextView.setTextColor(mHeaderTextColor);
+                mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize);
+                mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
+                mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL);
+                addView(mHeaderTextView);
+            }
+            if (mHeaderDivider == null) {
+                mHeaderDivider = new View(mContext);
+                mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor);
+                addView(mHeaderDivider);
+            }
+            setMarqueeText(mHeaderTextView, title);
+            mHeaderTextView.setVisibility(View.VISIBLE);
+            mHeaderDivider.setVisibility(View.VISIBLE);
+            mHeaderTextView.setAllCaps(true);
+            mHeaderVisible = true;
+        } else {
+            if (mHeaderTextView != null) {
+                mHeaderTextView.setVisibility(View.GONE);
+            }
+            if (mHeaderDivider != null) {
+                mHeaderDivider.setVisibility(View.GONE);
+            }
+            mHeaderVisible = false;
+        }
+    }
+
+    /**
+     * Returns the quick contact badge, creating it if necessary.
+     */
+    public QuickContactBadge getQuickContact() {
+        if (!mQuickContactEnabled) {
+            throw new IllegalStateException("QuickContact is disabled for this view");
+        }
+        if (mQuickContact == null) {
+            mQuickContact = new QuickContactBadge(mContext);
+            mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
+            if (mNameTextView != null) {
+                mQuickContact.setContentDescription(mContext.getString(
+                        R.string.description_quick_contact_for, mNameTextView.getText()));
+            }
+
+            addView(mQuickContact);
+            mPhotoViewWidthAndHeightAreReady = false;
+        }
+        return mQuickContact;
+    }
+
+    /**
+     * Returns the photo view, creating it if necessary.
+     */
+    public ImageView getPhotoView() {
+        if (mPhotoView == null) {
+            mPhotoView = new ImageView(mContext);
+            mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
+            // Quick contact style used above will set a background - remove it
+            mPhotoView.setBackground(null);
+            addView(mPhotoView);
+            mPhotoViewWidthAndHeightAreReady = false;
+        }
+        return mPhotoView;
+    }
+
+    /**
+     * Removes the photo view.
+     */
+    public void removePhotoView() {
+        removePhotoView(false, true);
+    }
+
+    /**
+     * Removes the photo view.
+     *
+     * @param keepHorizontalPadding True means data on the right side will have
+     *            padding on left, pretending there is still a photo view.
+     * @param keepVerticalPadding True means the View will have some height
+     *            enough for accommodating a photo view.
+     */
+    public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
+        mPhotoViewWidthAndHeightAreReady = false;
+        mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
+        mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
+        if (mPhotoView != null) {
+            removeView(mPhotoView);
+            mPhotoView = null;
+        }
+        if (mQuickContact != null) {
+            removeView(mQuickContact);
+            mQuickContact = null;
+        }
+    }
+
+    /**
+     * Sets a word prefix that will be highlighted if encountered in fields like
+     * name and search snippet.
+     * <p>
+     * NOTE: must be all upper-case
+     */
+    public void setHighlightedPrefix(char[] upperCasePrefix) {
+        mHighlightedPrefix = upperCasePrefix;
+    }
+
+    /**
+     * Returns the text view for the contact name, creating it if necessary.
+     */
+    public TextView getNameTextView() {
+        if (mNameTextView == null) {
+            mNameTextView = new TextView(mContext);
+            mNameTextView.setSingleLine(true);
+            mNameTextView.setEllipsize(getTextEllipsis());
+            mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+            // Manually call setActivated() since this view may be added after the first
+            // setActivated() call toward this whole item view.
+            mNameTextView.setActivated(isActivated());
+            mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
+            addView(mNameTextView);
+        }
+        return mNameTextView;
+    }
+
+    /**
+     * Adds or updates a text view for the phonetic name.
+     */
+    public void setPhoneticName(char[] text, int size) {
+        if (text == null || size == 0) {
+            if (mPhoneticNameTextView != null) {
+                mPhoneticNameTextView.setVisibility(View.GONE);
+            }
+        } else {
+            getPhoneticNameTextView();
+            setMarqueeText(mPhoneticNameTextView, text, size);
+            mPhoneticNameTextView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Returns the text view for the phonetic name, creating it if necessary.
+     */
+    public TextView getPhoneticNameTextView() {
+        if (mPhoneticNameTextView == null) {
+            mPhoneticNameTextView = new TextView(mContext);
+            mPhoneticNameTextView.setSingleLine(true);
+            mPhoneticNameTextView.setEllipsize(getTextEllipsis());
+            mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
+            mPhoneticNameTextView.setActivated(isActivated());
+            addView(mPhoneticNameTextView);
+        }
+        return mPhoneticNameTextView;
+    }
+
+    /**
+     * Adds or updates a text view for the data label.
+     */
+    public void setLabel(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mLabelView != null) {
+                mLabelView.setVisibility(View.GONE);
+            }
+        } else {
+            getLabelView();
+            setMarqueeText(mLabelView, text);
+            mLabelView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Returns the text view for the data label, creating it if necessary.
+     */
+    public TextView getLabelView() {
+        if (mLabelView == null) {
+            mLabelView = new TextView(mContext);
+            mLabelView.setSingleLine(true);
+            mLabelView.setEllipsize(getTextEllipsis());
+            mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mCountViewTextSize);
+                mLabelView.setAllCaps(true);
+                mLabelView.setGravity(Gravity.RIGHT);
+            } else {
+                mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
+            }
+            mLabelView.setActivated(isActivated());
+            addView(mLabelView);
+        }
+        return mLabelView;
+    }
+
+    /**
+     * Adds or updates a text view for the data element.
+     */
+    public void setData(char[] text, int size) {
+        if (text == null || size == 0) {
+            if (mDataView != null) {
+                mDataView.setVisibility(View.GONE);
+            }
+        } else {
+            getDataView();
+            setMarqueeText(mDataView, text, size);
+            mDataView.setVisibility(VISIBLE);
+        }
+    }
+
+    private void setMarqueeText(TextView textView, char[] text, int size) {
+        if (getTextEllipsis() == TruncateAt.MARQUEE) {
+            setMarqueeText(textView, new String(text, 0, size));
+        } else {
+            textView.setText(text, 0, size);
+        }
+    }
+
+    private void setMarqueeText(TextView textView, CharSequence text) {
+        if (getTextEllipsis() == TruncateAt.MARQUEE) {
+            // To show MARQUEE correctly (with END effect during non-active state), we need
+            // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
+            final SpannableString spannable = new SpannableString(text);
+            spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            textView.setText(spannable);
+        } else {
+            textView.setText(text);
+        }
+    }
+
+    /**
+     * Returns the text view for the data text, creating it if necessary.
+     */
+    public TextView getDataView() {
+        if (mDataView == null) {
+            mDataView = new TextView(mContext);
+            mDataView.setSingleLine(true);
+            mDataView.setEllipsize(getTextEllipsis());
+            mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            mDataView.setActivated(isActivated());
+            addView(mDataView);
+        }
+        return mDataView;
+    }
+
+    /**
+     * Adds or updates a text view for the search snippet.
+     */
+    public void setSnippet(String text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mSnippetView != null) {
+                mSnippetView.setVisibility(View.GONE);
+            }
+        } else {
+            mPrefixHighlighter.setText(getSnippetView(), text, mHighlightedPrefix);
+            mSnippetView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Returns the text view for the search snippet, creating it if necessary.
+     */
+    public TextView getSnippetView() {
+        if (mSnippetView == null) {
+            mSnippetView = new TextView(mContext);
+            mSnippetView.setSingleLine(true);
+            mSnippetView.setEllipsize(getTextEllipsis());
+            mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
+            mSnippetView.setActivated(isActivated());
+            addView(mSnippetView);
+        }
+        return mSnippetView;
+    }
+
+    /**
+     * Returns the text view for the status, creating it if necessary.
+     */
+    public TextView getStatusView() {
+        if (mStatusView == null) {
+            mStatusView = new TextView(mContext);
+            mStatusView.setSingleLine(true);
+            mStatusView.setEllipsize(getTextEllipsis());
+            mStatusView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
+            mStatusView.setTextColor(mSecondaryTextColor);
+            mStatusView.setActivated(isActivated());
+            addView(mStatusView);
+        }
+        return mStatusView;
+    }
+
+    /**
+     * Returns the text view for the contacts count, creating it if necessary.
+     */
+    public TextView getCountView() {
+        if (mCountView == null) {
+            mCountView = new TextView(mContext);
+            mCountView.setSingleLine(true);
+            mCountView.setEllipsize(getTextEllipsis());
+            mCountView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+            mCountView.setTextColor(R.color.contact_count_text_color);
+            addView(mCountView);
+        }
+        return mCountView;
+    }
+
+    /**
+     * Adds or updates a text view for the contacts count.
+     */
+    public void setCountView(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mCountView != null) {
+                mCountView.setVisibility(View.GONE);
+            }
+        } else {
+            getCountView();
+            setMarqueeText(mCountView, text);
+            mCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize);
+            mCountView.setGravity(Gravity.CENTER_VERTICAL);
+            mCountView.setTextColor(mContactsCountTextColor);
+            mCountView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Adds or updates a text view for the status.
+     */
+    public void setStatus(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mStatusView != null) {
+                mStatusView.setVisibility(View.GONE);
+            }
+        } else {
+            getStatusView();
+            setMarqueeText(mStatusView, text);
+            mStatusView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Adds or updates the presence icon view.
+     */
+    public void setPresence(Drawable icon) {
+        if (icon != null) {
+            if (mPresenceIcon == null) {
+                mPresenceIcon = new ImageView(mContext);
+                addView(mPresenceIcon);
+            }
+            mPresenceIcon.setImageDrawable(icon);
+            mPresenceIcon.setScaleType(ScaleType.CENTER);
+            mPresenceIcon.setVisibility(View.VISIBLE);
+        } else {
+            if (mPresenceIcon != null) {
+                mPresenceIcon.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    private TruncateAt getTextEllipsis() {
+        return TruncateAt.MARQUEE;
+    }
+
+    public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
+        CharSequence name = cursor.getString(nameColumnIndex);
+        if (!TextUtils.isEmpty(name)) {
+            name = mPrefixHighlighter.apply(name, mHighlightedPrefix);
+        } else {
+            name = mUnknownNameText;
+        }
+        setMarqueeText(getNameTextView(), name);
+
+        // Since the quick contact content description is derived from the display name and there is
+        // no guarantee that when the quick contact is initialized the display name is already set,
+        // do it here too.
+        if (mQuickContact != null) {
+            mQuickContact.setContentDescription(mContext.getString(
+                    R.string.description_quick_contact_for, mNameTextView.getText()));
+        }
+    }
+
+    public void hideDisplayName() {
+        if (mNameTextView != null) {
+            removeView(mNameTextView);
+            mNameTextView = null;
+        }
+    }
+
+    public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
+        cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
+        int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
+        if (phoneticNameSize != 0) {
+            setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
+        } else {
+            setPhoneticName(null, 0);
+        }
+    }
+
+    public void hidePhoneticName() {
+        if (mPhoneticNameTextView != null) {
+            removeView(mPhoneticNameTextView);
+            mPhoneticNameTextView = null;
+        }
+    }
+
+    /**
+     * Sets the proper icon (star or presence or nothing) and/or status message.
+     */
+    public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
+            int contactStatusColumnIndex) {
+        Drawable icon = null;
+        int presence = 0;
+        if (!cursor.isNull(presenceColumnIndex)) {
+            presence = cursor.getInt(presenceColumnIndex);
+            icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
+        }
+        setPresence(icon);
+
+        String statusMessage = null;
+        if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
+            statusMessage = cursor.getString(contactStatusColumnIndex);
+        }
+        // If there is no status message from the contact, but there was a presence value, then use
+        // the default status message string
+        if (statusMessage == null && presence != 0) {
+            statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
+        }
+        setStatus(statusMessage);
+    }
+
+    /**
+     * Shows search snippet.
+     */
+    public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
+        if (cursor.getColumnCount() <= summarySnippetColumnIndex) {
+            setSnippet(null);
+            return;
+        }
+        String snippet;
+        String columnContent = cursor.getString(summarySnippetColumnIndex);
+
+        // Do client side snippeting if provider didn't do it
+        Bundle extras = cursor.getExtras();
+        if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
+            int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
+
+            snippet = ContactsContract.snippetize(columnContent,
+                    displayNameIndex < 0 ? null : cursor.getString(displayNameIndex),
+                            extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY),
+                            DefaultContactListAdapter.SNIPPET_START_MATCH,
+                            DefaultContactListAdapter.SNIPPET_END_MATCH,
+                            DefaultContactListAdapter.SNIPPET_ELLIPSIS,
+                            DefaultContactListAdapter.SNIPPET_MAX_TOKENS);
+        } else {
+            snippet = columnContent;
+        }
+
+        if (snippet != null) {
+            int from = 0;
+            int to = snippet.length();
+            int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
+            if (start == -1) {
+                snippet = null;
+            } else {
+                int firstNl = snippet.lastIndexOf('\n', start);
+                if (firstNl != -1) {
+                    from = firstNl + 1;
+                }
+                int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
+                if (end != -1) {
+                    int lastNl = snippet.indexOf('\n', end);
+                    if (lastNl != -1) {
+                        to = lastNl;
+                    }
+                }
+
+                StringBuilder sb = new StringBuilder();
+                for (int i = from; i < to; i++) {
+                    char c = snippet.charAt(i);
+                    if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
+                            c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
+                        sb.append(c);
+                    }
+                }
+                snippet = sb.toString();
+            }
+        }
+        setSnippet(snippet);
+    }
+
+    /**
+     * Shows data element (e.g. phone number).
+     */
+    public void showData(Cursor cursor, int dataColumnIndex) {
+        cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
+        setData(mDataBuffer.data, mDataBuffer.sizeCopied);
+    }
+
+    public void setActivatedStateSupported(boolean flag) {
+        this.mActivatedStateSupported = flag;
+    }
+
+    @Override
+    public void requestLayout() {
+        // We will assume that once measured this will not need to resize
+        // itself, so there is no need to pass the layout request to the parent
+        // view (ListView).
+        forceLayout();
+    }
+
+    public void setPhotoPosition(PhotoPosition photoPosition) {
+        mPhotoPosition = photoPosition;
+    }
+
+    public PhotoPosition getPhotoPosition() {
+        return mPhotoPosition;
+    }
+
+    /**
+     * Specifies left and right margin for selection bounds. See also
+     * {@link #adjustListItemSelectionBounds(Rect)}.
+     */
+    public void setSelectionBoundsHorizontalMargin(int left, int right) {
+        mSelectionBoundsMarginLeft = left;
+        mSelectionBoundsMarginRight = right;
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListPinnedHeaderView.java b/src/com/android/contacts/common/list/ContactListPinnedHeaderView.java
new file mode 100644
index 0000000..9aa9a9b
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListPinnedHeaderView.java
@@ -0,0 +1,178 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+
+/**
+ * A custom view for the pinned section header shown at the top of the contact list.
+ */
+public class ContactListPinnedHeaderView extends ViewGroup {
+
+    protected final Context mContext;
+
+    private final int mHeaderTextColor;
+    private final int mHeaderTextIndent;
+    private final int mHeaderTextSize;
+    private final int mHeaderUnderlineHeight;
+    private final int mHeaderUnderlineColor;
+    private final int mPaddingRight;
+    private final int mPaddingLeft;
+    private final int mContactsCountTextColor;
+    private final int mCountViewTextSize;
+
+    private int mHeaderBackgroundHeight;
+    private TextView mHeaderTextView;
+    private TextView mCountTextView = null;
+    private View mHeaderDivider;
+
+    public ContactListPinnedHeaderView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+
+        mHeaderTextIndent = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_header_text_indent, 0);
+        mHeaderTextColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK);
+        mHeaderTextSize = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_header_text_size, 12);
+        mHeaderUnderlineHeight = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_header_underline_height, 1);
+        mHeaderUnderlineColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_header_underline_color, 0);
+        mHeaderBackgroundHeight = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_header_height, 30);
+        mPaddingLeft = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_padding_left, 0);
+        mPaddingRight = a.getDimensionPixelOffset(
+                R.styleable.ContactListItemView_list_item_padding_right, 0);
+        mContactsCountTextColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_contacts_count_text_color, Color.BLACK);
+        mCountViewTextSize = (int)a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_contacts_count_text_size, 12);
+
+        a.recycle();
+
+        mHeaderTextView = new TextView(mContext);
+        mHeaderTextView.setTextColor(mHeaderTextColor);
+        mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize);
+        mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
+        mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL);
+        mHeaderTextView.setAllCaps(true);
+        addView(mHeaderTextView);
+        mHeaderDivider = new View(mContext);
+        mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor);
+        addView(mHeaderDivider);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+        // We will match parent's width and wrap content vertically.
+        int width = resolveSize(0, widthMeasureSpec);
+
+        mHeaderTextView.measure(
+                MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+        if (isViewMeasurable(mCountTextView)) {
+            mCountTextView.measure(
+                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+                    MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
+        }
+
+        setMeasuredDimension(width, mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        int width = right - left;
+
+        // Take into account left and right padding when laying out the below views.
+        mHeaderTextView.layout(mHeaderTextIndent + mPaddingLeft,
+                0,
+                mHeaderTextView.getMeasuredWidth() + mHeaderTextIndent + mPaddingLeft,
+                mHeaderBackgroundHeight);
+
+        if (isViewMeasurable(mCountTextView)) {
+            mCountTextView.layout(width - mPaddingRight - mCountTextView.getMeasuredWidth(),
+                    0,
+                    width - mPaddingRight,
+                    mHeaderBackgroundHeight);
+        }
+
+        mHeaderDivider.layout(mPaddingLeft,
+                mHeaderBackgroundHeight,
+                width - mPaddingRight,
+                mHeaderBackgroundHeight + mHeaderUnderlineHeight);
+    }
+
+    /**
+     * Sets section header or makes it invisible if the title is null.
+     */
+    public void setSectionHeader(String title) {
+        if (!TextUtils.isEmpty(title)) {
+            mHeaderTextView.setText(title);
+            mHeaderTextView.setVisibility(View.VISIBLE);
+            mHeaderDivider.setVisibility(View.VISIBLE);
+        } else {
+            mHeaderTextView.setVisibility(View.GONE);
+            mHeaderDivider.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void requestLayout() {
+        // We will assume that once measured this will not need to resize
+        // itself, so there is no need to pass the layout request to the parent
+        // view (ListView).
+        forceLayout();
+    }
+
+    public void setCountView(String count) {
+        if (mCountTextView == null) {
+            mCountTextView = new TextView(mContext);
+            mCountTextView.setTextColor(mContactsCountTextColor);
+            mCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize);
+            mCountTextView.setGravity(Gravity.CENTER_VERTICAL);
+            addView(mCountTextView);
+        }
+        mCountTextView.setText(count);
+        if (count == null || count.isEmpty()) {
+            mCountTextView.setVisibility(View.GONE);
+        } else {
+            mCountTextView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private boolean isViewMeasurable(View view) {
+        return (view != null && view.getVisibility() == View.VISIBLE);
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactsSectionIndexer.java b/src/com/android/contacts/common/list/ContactsSectionIndexer.java
new file mode 100644
index 0000000..8d1c9e1
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactsSectionIndexer.java
@@ -0,0 +1,121 @@
+/*
+ * 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.common.list;
+
+import android.text.TextUtils;
+import android.widget.SectionIndexer;
+
+import java.util.Arrays;
+
+/**
+ * A section indexer that is configured with precomputed section titles and
+ * their respective counts.
+ */
+public class ContactsSectionIndexer implements SectionIndexer {
+
+    private String[] mSections;
+    private int[] mPositions;
+    private int mCount;
+    private static final String BLANK_HEADER_STRING = " ";
+
+    /**
+     * Constructor.
+     *
+     * @param sections a non-null array
+     * @param counts a non-null array of the same size as <code>sections</code>
+     */
+    public ContactsSectionIndexer(String[] sections, int[] counts) {
+        if (sections == null || counts == null) {
+            throw new NullPointerException();
+        }
+
+        if (sections.length != counts.length) {
+            throw new IllegalArgumentException(
+                    "The sections and counts arrays must have the same length");
+        }
+
+        // TODO process sections/counts based on current locale and/or specific section titles
+
+        this.mSections = sections;
+        mPositions = new int[counts.length];
+        int position = 0;
+        for (int i = 0; i < counts.length; i++) {
+            if (TextUtils.isEmpty(mSections[i])) {
+                mSections[i] = BLANK_HEADER_STRING;
+            } else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
+                mSections[i] = mSections[i].trim();
+            }
+
+            mPositions[i] = position;
+            position += counts[i];
+        }
+        mCount = position;
+    }
+
+    public Object[] getSections() {
+        return mSections;
+    }
+
+    public int getPositionForSection(int section) {
+        if (section < 0 || section >= mSections.length) {
+            return -1;
+        }
+
+        return mPositions[section];
+    }
+
+    public int getSectionForPosition(int position) {
+        if (position < 0 || position >= mCount) {
+            return -1;
+        }
+
+        int index = Arrays.binarySearch(mPositions, position);
+
+        /*
+         * Consider this example: section positions are 0, 3, 5; the supplied
+         * position is 4. The section corresponding to position 4 starts at
+         * position 3, so the expected return value is 1. Binary search will not
+         * find 4 in the array and thus will return -insertPosition-1, i.e. -3.
+         * To get from that number to the expected value of 1 we need to negate
+         * and subtract 2.
+         */
+        return index >= 0 ? index : -index - 2;
+    }
+
+    public void setProfileHeader(String header) {
+        if (mSections != null) {
+            // Don't do anything if the header is already set properly.
+            if (mSections.length > 0 && header.equals(mSections[0])) {
+                return;
+            }
+
+            // Since the section indexer isn't aware of the profile at the top, we need to add a
+            // special section at the top for it and shift everything else down.
+            String[] tempSections = new String[mSections.length + 1];
+            int[] tempPositions = new int[mPositions.length + 1];
+            tempSections[0] = header;
+            tempPositions[0] = 0;
+            for (int i = 1; i <= mPositions.length; i++) {
+                tempSections[i] = mSections[i - 1];
+                tempPositions[i] = mPositions[i - 1] + 1;
+            }
+            mSections = tempSections;
+            mPositions = tempPositions;
+            mCount++;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/DefaultContactListAdapter.java b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
new file mode 100644
index 0000000..6ad9e8b
--- /dev/null
+++ b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -0,0 +1,222 @@
+/*
+ * 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.common.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.SearchSnippetColumns;
+import android.text.TextUtils;
+import android.view.View;
+
+import com.android.contacts.common.preference.ContactsPreferences;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ */
+public class DefaultContactListAdapter extends ContactListAdapter {
+
+    public static final char SNIPPET_START_MATCH = '\u0001';
+    public static final char SNIPPET_END_MATCH = '\u0001';
+    public static final String SNIPPET_ELLIPSIS = "\u2026";
+    public static final int SNIPPET_MAX_TOKENS = 5;
+
+    public static final String SNIPPET_ARGS = SNIPPET_START_MATCH + "," + SNIPPET_END_MATCH + ","
+            + SNIPPET_ELLIPSIS + "," + SNIPPET_MAX_TOKENS;
+
+    public DefaultContactListAdapter(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader, long directoryId) {
+        if (loader instanceof ProfileAndContactsLoader) {
+            ((ProfileAndContactsLoader) loader).setLoadProfile(shouldIncludeProfile());
+        }
+
+        ContactListFilter filter = getFilter();
+        if (isSearchMode()) {
+            String query = getQueryString();
+            if (query == null) {
+                query = "";
+            }
+            query = query.trim();
+            if (TextUtils.isEmpty(query)) {
+                // Regardless of the directory, we don't want anything returned,
+                // so let's just send a "nothing" query to the local directory.
+                loader.setUri(Contacts.CONTENT_URI);
+                loader.setProjection(getProjection(false));
+                loader.setSelection("0");
+            } else {
+                Builder builder = Contacts.CONTENT_FILTER_URI.buildUpon();
+                builder.appendPath(query);      // Builder will encode the query
+                builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                        String.valueOf(directoryId));
+                if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) {
+                    builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                            String.valueOf(getDirectoryResultLimit()));
+                }
+                builder.appendQueryParameter(SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY,
+                        SNIPPET_ARGS);
+                builder.appendQueryParameter(SearchSnippetColumns.DEFERRED_SNIPPETING_KEY,"1");
+                loader.setUri(builder.build());
+                loader.setProjection(getProjection(true));
+            }
+        } else {
+            configureUri(loader, directoryId, filter);
+            loader.setProjection(getProjection(false));
+            configureSelection(loader, directoryId, filter);
+        }
+
+        String sortOrder;
+        if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
+            sortOrder = Contacts.SORT_KEY_PRIMARY;
+        } else {
+            sortOrder = Contacts.SORT_KEY_ALTERNATIVE;
+        }
+
+        loader.setSortOrder(sortOrder);
+    }
+
+    protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) {
+        Uri uri = Contacts.CONTENT_URI;
+        if (filter != null && filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+            String lookupKey = getSelectedContactLookupKey();
+            if (lookupKey != null) {
+                uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+            } else {
+                uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, getSelectedContactId());
+            }
+        }
+
+        if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) {
+            uri = ContactListAdapter.buildSectionIndexerUri(uri);
+        }
+
+        // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT
+        if (filter != null
+                && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM
+                && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+            final Uri.Builder builder = uri.buildUpon();
+            builder.appendQueryParameter(
+                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+            if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+                filter.addAccountQueryParameterToUrl(builder);
+            }
+            uri = builder.build();
+        }
+
+        loader.setUri(uri);
+    }
+
+    private void configureSelection(
+            CursorLoader loader, long directoryId, ContactListFilter filter) {
+        if (filter == null) {
+            return;
+        }
+
+        if (directoryId != Directory.DEFAULT) {
+            return;
+        }
+
+        StringBuilder selection = new StringBuilder();
+        List<String> selectionArgs = new ArrayList<String>();
+
+        switch (filter.filterType) {
+            case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
+                // We have already added directory=0 to the URI, which takes care of this
+                // filter
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: {
+                // We have already added the lookup key to the URI, which takes care of this
+                // filter
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_STARRED: {
+                selection.append(Contacts.STARRED + "!=0");
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: {
+                selection.append(Contacts.HAS_PHONE_NUMBER + "=1");
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_CUSTOM: {
+                selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+                if (isCustomFilterForPhoneNumbersOnly()) {
+                    selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+                }
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_ACCOUNT: {
+                // We use query parameters for account filter, so no selection to add here.
+                break;
+            }
+        }
+        loader.setSelection(selection.toString());
+        loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
+    }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        final ContactListItemView view = (ContactListItemView)itemView;
+
+        view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
+
+        if (isSelectionVisible()) {
+            view.setActivated(isSelectedContact(partition, cursor));
+        }
+
+        bindSectionHeaderAndDivider(view, position, cursor);
+
+        if (isQuickContactEnabled()) {
+            bindQuickContact(view, partition, cursor, ContactQuery.CONTACT_PHOTO_ID,
+                    ContactQuery.CONTACT_PHOTO_URI, ContactQuery.CONTACT_ID,
+                    ContactQuery.CONTACT_LOOKUP_KEY);
+        } else {
+            if (getDisplayPhotos()) {
+                bindPhoto(view, partition, cursor);
+            }
+        }
+
+        bindName(view, cursor);
+        bindPresenceAndStatusMessage(view, cursor);
+
+        if (isSearchMode()) {
+            bindSearchSnippet(view, cursor);
+        } else {
+            view.setSnippet(null);
+        }
+    }
+
+    private boolean isCustomFilterForPhoneNumbersOnly() {
+        // TODO: this flag should not be stored in shared prefs.  It needs to be in the db.
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+        return prefs.getBoolean(ContactsPreferences.PREF_DISPLAY_ONLY_PHONES,
+                ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT);
+    }
+}
diff --git a/src/com/android/contacts/common/list/DirectoryListLoader.java b/src/com/android/contacts/common/list/DirectoryListLoader.java
new file mode 100644
index 0000000..be9a8e9
--- /dev/null
+++ b/src/com/android/contacts/common/list/DirectoryListLoader.java
@@ -0,0 +1,197 @@
+/*
+ * 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.common.list;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+
+/**
+ * A specialized loader for the list of directories, see {@link Directory}.
+ */
+public class DirectoryListLoader extends AsyncTaskLoader<Cursor> {
+
+    private static final String TAG = "ContactEntryListAdapter";
+
+    public static final int SEARCH_MODE_NONE = 0;
+    public static final int SEARCH_MODE_DEFAULT = 1;
+    public static final int SEARCH_MODE_CONTACT_SHORTCUT = 2;
+    public static final int SEARCH_MODE_DATA_SHORTCUT = 3;
+
+    private static final class DirectoryQuery {
+        public static final Uri URI = Directory.CONTENT_URI;
+        public static final String ORDER_BY = Directory._ID;
+
+        public static final String[] PROJECTION = {
+            Directory._ID,
+            Directory.PACKAGE_NAME,
+            Directory.TYPE_RESOURCE_ID,
+            Directory.DISPLAY_NAME,
+            Directory.PHOTO_SUPPORT,
+        };
+
+        public static final int ID = 0;
+        public static final int PACKAGE_NAME = 1;
+        public static final int TYPE_RESOURCE_ID = 2;
+        public static final int DISPLAY_NAME = 3;
+        public static final int PHOTO_SUPPORT = 4;
+    }
+
+    // This is a virtual column created for a MatrixCursor.
+    public static final String DIRECTORY_TYPE = "directoryType";
+
+    private static final String[] RESULT_PROJECTION = {
+        Directory._ID,
+        DIRECTORY_TYPE,
+        Directory.DISPLAY_NAME,
+        Directory.PHOTO_SUPPORT,
+    };
+
+    private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            forceLoad();
+        }
+    };
+
+    private int mDirectorySearchMode;
+    private boolean mLocalInvisibleDirectoryEnabled;
+
+    private MatrixCursor mDefaultDirectoryList;
+
+    public DirectoryListLoader(Context context) {
+        super(context);
+    }
+
+    public void setDirectorySearchMode(int mode) {
+        mDirectorySearchMode = mode;
+    }
+
+    /**
+     * A flag that indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
+     * be included in the results.
+     */
+    public void setLocalInvisibleDirectoryEnabled(boolean flag) {
+        this.mLocalInvisibleDirectoryEnabled = flag;
+    }
+
+    @Override
+    protected void onStartLoading() {
+        getContext().getContentResolver().
+                registerContentObserver(Directory.CONTENT_URI, false, mObserver);
+        forceLoad();
+    }
+
+    @Override
+    protected void onStopLoading() {
+        getContext().getContentResolver().unregisterContentObserver(mObserver);
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        if (mDirectorySearchMode == SEARCH_MODE_NONE) {
+            return getDefaultDirectories();
+        }
+
+        MatrixCursor result = new MatrixCursor(RESULT_PROJECTION);
+        Context context = getContext();
+        PackageManager pm = context.getPackageManager();
+        String selection;
+        switch (mDirectorySearchMode) {
+            case SEARCH_MODE_DEFAULT:
+                selection = mLocalInvisibleDirectoryEnabled ? null
+                        : (Directory._ID + "!=" + Directory.LOCAL_INVISIBLE);
+                break;
+
+            case SEARCH_MODE_CONTACT_SHORTCUT:
+                selection = Directory.SHORTCUT_SUPPORT + "=" + Directory.SHORTCUT_SUPPORT_FULL
+                        + (mLocalInvisibleDirectoryEnabled ? ""
+                                : (" AND " + Directory._ID + "!=" + Directory.LOCAL_INVISIBLE));
+                break;
+
+            case SEARCH_MODE_DATA_SHORTCUT:
+                selection = Directory.SHORTCUT_SUPPORT + " IN ("
+                        + Directory.SHORTCUT_SUPPORT_FULL + ", "
+                        + Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY + ")"
+                        + (mLocalInvisibleDirectoryEnabled ? ""
+                                : (" AND " + Directory._ID + "!=" + Directory.LOCAL_INVISIBLE));
+                break;
+
+            default:
+                throw new RuntimeException(
+                        "Unsupported directory search mode: " + mDirectorySearchMode);
+        }
+
+        Cursor cursor = context.getContentResolver().query(DirectoryQuery.URI,
+                DirectoryQuery.PROJECTION, selection, null, DirectoryQuery.ORDER_BY);
+        try {
+            while(cursor.moveToNext()) {
+                long directoryId = cursor.getLong(DirectoryQuery.ID);
+                String directoryType = null;
+
+                String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+                int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+                if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) {
+                    try {
+                        directoryType = pm.getResourcesForApplication(packageName)
+                                .getString(typeResourceId);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Cannot obtain directory type from package: " + packageName);
+                    }
+                }
+                String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+                int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT);
+                result.addRow(new Object[]{directoryId, directoryType, displayName, photoSupport});
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return result;
+    }
+
+    private Cursor getDefaultDirectories() {
+        if (mDefaultDirectoryList == null) {
+            mDefaultDirectoryList = new MatrixCursor(RESULT_PROJECTION);
+            mDefaultDirectoryList.addRow(new Object[] {
+                    Directory.DEFAULT,
+                    getContext().getString(R.string.contactsList),
+                    null
+            });
+            mDefaultDirectoryList.addRow(new Object[] {
+                    Directory.LOCAL_INVISIBLE,
+                    getContext().getString(R.string.local_invisible_directory),
+                    null
+            });
+        }
+        return mDefaultDirectoryList;
+    }
+
+    @Override
+    protected void onReset() {
+        stopLoading();
+    }
+}
diff --git a/src/com/android/contacts/common/list/DirectoryPartition.java b/src/com/android/contacts/common/list/DirectoryPartition.java
new file mode 100644
index 0000000..022d1e6
--- /dev/null
+++ b/src/com/android/contacts/common/list/DirectoryPartition.java
@@ -0,0 +1,109 @@
+/*
+ * 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.common.list;
+
+import android.provider.ContactsContract.Directory;
+
+import com.android.common.widget.CompositeCursorAdapter;
+
+/**
+ * Model object for a {@link Directory} row.
+ */
+public final class DirectoryPartition extends CompositeCursorAdapter.Partition {
+
+    public static final int STATUS_NOT_LOADED = 0;
+    public static final int STATUS_LOADING = 1;
+    public static final int STATUS_LOADED = 2;
+
+    private long mDirectoryId;
+    private String mDirectoryType;
+    private String mDisplayName;
+    private int mStatus;
+    private boolean mPriorityDirectory;
+    private boolean mPhotoSupported;
+
+    public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) {
+        super(showIfEmpty, hasHeader);
+    }
+
+    /**
+     * Directory ID, see {@link Directory}.
+     */
+    public long getDirectoryId() {
+        return mDirectoryId;
+    }
+
+    public void setDirectoryId(long directoryId) {
+        this.mDirectoryId = directoryId;
+    }
+
+    /**
+     * Directory type resolved from {@link Directory#PACKAGE_NAME} and
+     * {@link Directory#TYPE_RESOURCE_ID};
+     */
+    public String getDirectoryType() {
+        return mDirectoryType;
+    }
+
+    public void setDirectoryType(String directoryType) {
+        this.mDirectoryType = directoryType;
+    }
+
+    /**
+     * See {@link Directory#DISPLAY_NAME}.
+     */
+    public String getDisplayName() {
+        return mDisplayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.mDisplayName = displayName;
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+    public void setStatus(int status) {
+        mStatus = status;
+    }
+
+    public boolean isLoading() {
+        return mStatus == STATUS_NOT_LOADED || mStatus == STATUS_LOADING;
+    }
+
+    /**
+     * Returns true if this directory should be loaded before non-priority directories.
+     */
+    public boolean isPriorityDirectory() {
+        return mPriorityDirectory;
+    }
+
+    public void setPriorityDirectory(boolean priorityDirectory) {
+        mPriorityDirectory = priorityDirectory;
+    }
+
+    /**
+     * Returns true if this directory supports photos.
+     */
+    public boolean isPhotoSupported() {
+        return mPhotoSupported;
+    }
+
+    public void setPhotoSupported(boolean flag) {
+        this.mPhotoSupported = flag;
+    }
+}
diff --git a/src/com/android/contacts/common/list/IndexerListAdapter.java b/src/com/android/contacts/common/list/IndexerListAdapter.java
new file mode 100644
index 0000000..830ea81
--- /dev/null
+++ b/src/com/android/contacts/common/list/IndexerListAdapter.java
@@ -0,0 +1,235 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+
+/**
+ * A list adapter that supports section indexer and a pinned header.
+ */
+public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer {
+
+    protected Context mContext;
+    private SectionIndexer mIndexer;
+    private int mIndexedPartition = 0;
+    private boolean mSectionHeaderDisplayEnabled;
+    private View mHeader;
+
+    /**
+     * An item view is displayed differently depending on whether it is placed
+     * at the beginning, middle or end of a section. It also needs to know the
+     * section header when it is at the beginning of a section. This object
+     * captures all this configuration.
+     */
+    public static final class Placement {
+        private int position = ListView.INVALID_POSITION;
+        public boolean firstInSection;
+        public boolean lastInSection;
+        public String sectionHeader;
+
+        public void invalidate() {
+            position = ListView.INVALID_POSITION;
+        }
+    }
+
+    private Placement mPlacementCache = new Placement();
+
+    /**
+     * Constructor.
+     */
+    public IndexerListAdapter(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    /**
+     * Creates a section header view that will be pinned at the top of the list
+     * as the user scrolls.
+     */
+    protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent);
+
+    /**
+     * Sets the title in the pinned header as the user scrolls.
+     */
+    protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title);
+
+    /**
+     * Sets the contacts count in the pinned header.
+     */
+    protected abstract void setPinnedHeaderContactsCount(View header);
+
+    /**
+     * clears the contacts count in the pinned header and makes the view invisible.
+     */
+    protected abstract void clearPinnedHeaderContactsCount(View header);
+
+    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 SectionIndexer getIndexer() {
+        return mIndexer;
+    }
+
+    public void setIndexer(SectionIndexer indexer) {
+        mIndexer = indexer;
+        mPlacementCache.invalidate();
+    }
+
+    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 getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) {
+        if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) {
+            if (mHeader == null) {
+                mHeader = createPinnedSectionHeaderView(mContext, parent);
+            }
+            return mHeader;
+        } else {
+            return super.getPinnedHeaderView(viewIndex, convertView, 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, false);
+        } 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, false);
+            } else {
+                setPinnedSectionTitle(mHeader, (String)mIndexer.getSections()[section]);
+                if (section == 0) {
+                    setPinnedHeaderContactsCount(mHeader);
+                } else {
+                    clearPinnedHeaderContactsCount(mHeader);
+                }
+                // 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);
+            }
+        }
+    }
+
+    /**
+     * Computes the item's placement within its section and populates the {@code placement}
+     * object accordingly.  Please note that the returned object is volatile and should be
+     * copied if the result needs to be used later.
+     */
+    public Placement getItemPlacementInSection(int position) {
+        if (mPlacementCache.position == position) {
+            return mPlacementCache;
+        }
+
+        mPlacementCache.position = position;
+        if (isSectionHeaderDisplayEnabled()) {
+            int section = getSectionForPosition(position);
+            if (section != -1 && getPositionForSection(section) == position) {
+                mPlacementCache.firstInSection = true;
+                mPlacementCache.sectionHeader = (String)getSections()[section];
+            } else {
+                mPlacementCache.firstInSection = false;
+                mPlacementCache.sectionHeader = null;
+            }
+
+            mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position);
+        } else {
+            mPlacementCache.firstInSection = false;
+            mPlacementCache.lastInSection = false;
+            mPlacementCache.sectionHeader = null;
+        }
+        return mPlacementCache;
+    }
+}
diff --git a/src/com/android/contacts/common/list/PinnedHeaderListAdapter.java b/src/com/android/contacts/common/list/PinnedHeaderListAdapter.java
new file mode 100644
index 0000000..9591092
--- /dev/null
+++ b/src/com/android/contacts/common/list/PinnedHeaderListAdapter.java
@@ -0,0 +1,171 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.common.widget.CompositeCursorAdapter;
+
+/**
+ * A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers.
+ */
+public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter
+        implements PinnedHeaderListView.PinnedHeaderAdapter {
+
+    public static final int PARTITION_HEADER_TYPE = 0;
+
+    private boolean mPinnedPartitionHeadersEnabled;
+    private boolean mHeaderVisibility[];
+
+    public PinnedHeaderListAdapter(Context context) {
+        super(context);
+    }
+
+    public PinnedHeaderListAdapter(Context context, int initialCapacity) {
+        super(context, initialCapacity);
+    }
+
+    public boolean getPinnedPartitionHeadersEnabled() {
+        return mPinnedPartitionHeadersEnabled;
+    }
+
+    public void setPinnedPartitionHeadersEnabled(boolean flag) {
+        this.mPinnedPartitionHeadersEnabled = flag;
+    }
+
+    @Override
+    public int getPinnedHeaderCount() {
+        if (mPinnedPartitionHeadersEnabled) {
+            return getPartitionCount();
+        } else {
+            return 0;
+        }
+    }
+
+    protected boolean isPinnedPartitionHeaderVisible(int partition) {
+        return mPinnedPartitionHeadersEnabled && hasHeader(partition)
+                && !isPartitionEmpty(partition);
+    }
+
+    /**
+     * The default implementation creates the same type of view as a normal
+     * partition header.
+     */
+    @Override
+    public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) {
+        if (hasHeader(partition)) {
+            View view = null;
+            if (convertView != null) {
+                Integer headerType = (Integer)convertView.getTag();
+                if (headerType != null && headerType == PARTITION_HEADER_TYPE) {
+                    view = convertView;
+                }
+            }
+            if (view == null) {
+                view = newHeaderView(getContext(), partition, null, parent);
+                view.setTag(PARTITION_HEADER_TYPE);
+                view.setFocusable(false);
+                view.setEnabled(false);
+            }
+            bindHeaderView(view, partition, getCursor(partition));
+            return view;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    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, true);
+            }
+        }
+
+        int headerViewsCount = listView.getHeaderViewsCount();
+
+        // 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) - headerViewsCount;
+                int partition = getPartitionForPosition(position);
+                if (i > partition) {
+                    break;
+                }
+
+                listView.setHeaderPinnedAtTop(i, topHeaderHeight, false);
+                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)
+                        - headerViewsCount;
+                if (position < 0) {
+                    break;
+                }
+
+                int partition = getPartitionForPosition(position - 1);
+                if (partition == -1 || i <= partition) {
+                    break;
+                }
+
+                int height = listView.getPinnedHeaderHeight(i);
+                bottomHeaderHeight += height;
+                // Animate the header only if the partition is completely invisible below
+                // the bottom of the view
+                int firstPositionForPartition = getPositionForPartition(i);
+                boolean animate = position < firstPositionForPartition;
+                listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, animate);
+                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, isPartitionEmpty(i));
+            }
+        }
+    }
+
+    @Override
+    public int getScrollPositionForHeader(int viewIndex) {
+        return getPositionForPartition(viewIndex);
+    }
+}
diff --git a/src/com/android/contacts/common/list/PinnedHeaderListView.java b/src/com/android/contacts/common/list/PinnedHeaderListView.java
new file mode 100644
index 0000000..d006f4b
--- /dev/null
+++ b/src/com/android/contacts/common/list/PinnedHeaderListView.java
@@ -0,0 +1,523 @@
+/*
+ * 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.common.list;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ListAdapter;
+
+/**
+ * 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 AutoScrollListView
+        implements OnScrollListener, OnItemSelectedListener {
+
+    /**
+     * Adapter interface.  The list adapter must implement this interface.
+     */
+    public interface PinnedHeaderAdapter {
+
+        /**
+         * Returns the overall number of pinned headers, visible or not.
+         */
+        int getPinnedHeaderCount();
+
+        /**
+         * Creates or updates the pinned header view.
+         */
+        View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent);
+
+        /**
+         * 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.
+         */
+        void configurePinnedHeaders(PinnedHeaderListView listView);
+
+        /**
+         * Returns the list position to scroll to if the pinned header is touched.
+         * Return -1 if the list does not need to be scrolled.
+         */
+        int getScrollPositionForHeader(int viewIndex);
+    }
+
+    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 int DEFAULT_ANIMATION_DURATION = 20;
+
+    private static final class PinnedHeader {
+        View view;
+        boolean visible;
+        int y;
+        int height;
+        int alpha;
+        int state;
+
+        boolean animating;
+        boolean targetVisible;
+        int sourceY;
+        int targetY;
+        long targetTime;
+    }
+
+    private PinnedHeaderAdapter mAdapter;
+    private int mSize;
+    private PinnedHeader[] mHeaders;
+    private RectF mBounds = new RectF();
+    private Rect mClipRect = new Rect();
+    private OnScrollListener mOnScrollListener;
+    private OnItemSelectedListener mOnItemSelectedListener;
+    private int mScrollState;
+
+    private int mAnimationDuration = DEFAULT_ANIMATION_DURATION;
+    private boolean mAnimating;
+    private long mAnimationTargetTime;
+    private int mHeaderPaddingLeft;
+    private int mHeaderWidth;
+
+    public PinnedHeaderListView(Context context) {
+        this(context, null, com.android.internal.R.attr.listViewStyle);
+    }
+
+    public PinnedHeaderListView(Context context, AttributeSet attrs) {
+        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);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mHeaderPaddingLeft = getPaddingLeft();
+        mHeaderWidth = r - l - mHeaderPaddingLeft - getPaddingRight();
+    }
+
+    public void setPinnedHeaderAnimationDuration(int duration) {
+        mAnimationDuration = duration;
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        mAdapter = (PinnedHeaderAdapter)adapter;
+        super.setAdapter(adapter);
+    }
+
+    @Override
+    public void setOnScrollListener(OnScrollListener onScrollListener) {
+        mOnScrollListener = onScrollListener;
+        super.setOnScrollListener(this);
+    }
+
+    @Override
+    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+        mOnItemSelectedListener = listener;
+        super.setOnItemSelectedListener(this);
+    }
+
+    @Override
+    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+            int totalItemCount) {
+        if (mAdapter != null) {
+            int count = mAdapter.getPinnedHeaderCount();
+            if (count != mSize) {
+                mSize = count;
+                if (mHeaders == null) {
+                    mHeaders = new PinnedHeader[mSize];
+                } else if (mHeaders.length < mSize) {
+                    PinnedHeader[] headers = mHeaders;
+                    mHeaders = new PinnedHeader[mSize];
+                    System.arraycopy(headers, 0, mHeaders, 0, headers.length);
+                }
+            }
+
+            for (int i = 0; i < mSize; i++) {
+                if (mHeaders[i] == null) {
+                    mHeaders[i] = new PinnedHeader();
+                }
+                mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this);
+            }
+
+            mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration;
+            mAdapter.configurePinnedHeaders(this);
+            invalidateIfAnimating();
+
+        }
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
+        }
+    }
+
+    @Override
+    protected float getTopFadingEdgeStrength() {
+        // Disable vertical fading at the top when the pinned header is present
+        return mSize > 0 ? 0 : super.getTopFadingEdgeStrength();
+    }
+
+    @Override
+    public void onScrollStateChanged(AbsListView view, int scrollState) {
+        mScrollState = scrollState;
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScrollStateChanged(this, scrollState);
+        }
+    }
+
+    /**
+     * Ensures that the selected item is positioned below the top-pinned headers
+     * and above the bottom-pinned ones.
+     */
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        int height = getHeight();
+
+        int windowTop = 0;
+        int windowBottom = height;
+
+        for (int i = 0; i < mSize; 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;
+                }
+            }
+        }
+
+        View selectedView = getSelectedView();
+        if (selectedView != null) {
+            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);
+        }
+    }
+
+    @Override
+    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.
+     * @param animate true if the transition to the new coordinate should be animated
+     */
+    public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) {
+        ensurePinnedHeaderLayout(viewIndex);
+        PinnedHeader header = mHeaders[viewIndex];
+        header.visible = true;
+        header.y = y;
+        header.state = TOP;
+
+        // TODO perhaps we should animate at the top as well
+        header.animating = false;
+    }
+
+    /**
+     * Set header to be pinned at the bottom.
+     *
+     * @param viewIndex index of the header view
+     * @param y is position of the header in pixels.
+     * @param animate true if the transition to the new coordinate should be animated
+     */
+    public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) {
+        ensurePinnedHeaderLayout(viewIndex);
+        PinnedHeader header = mHeaders[viewIndex];
+        header.state = BOTTOM;
+        if (header.animating) {
+            header.targetTime = mAnimationTargetTime;
+            header.sourceY = header.y;
+            header.targetY = y;
+        } else if (animate && (header.y != y || !header.visible)) {
+            if (header.visible) {
+                header.sourceY = header.y;
+            } else {
+                header.visible = true;
+                header.sourceY = y + header.height;
+            }
+            header.animating = true;
+            header.targetVisible = true;
+            header.targetTime = mAnimationTargetTime;
+            header.targetY = y;
+        } else {
+            header.visible = true;
+            header.y = y;
+        }
+    }
+
+    /**
+     * 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());
+        if (child == null) return;
+
+        PinnedHeader header = mHeaders[viewIndex];
+        header.visible = true;
+        header.state = FADING;
+        header.alpha = MAX_ALPHA;
+        header.animating = false;
+
+        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;
+            }
+        }
+    }
+
+    /**
+     * Makes header invisible.
+     *
+     * @param viewIndex index of the header view
+     * @param animate true if the transition to the new coordinate should be animated
+     */
+    public void setHeaderInvisible(int viewIndex, boolean animate) {
+        PinnedHeader header = mHeaders[viewIndex];
+        if (header.visible && (animate || header.animating) && header.state == BOTTOM) {
+            header.sourceY = header.y;
+            if (!header.animating) {
+                header.visible = true;
+                header.targetY = getBottom() + header.height;
+            }
+            header.animating = true;
+            header.targetTime = mAnimationTargetTime;
+            header.targetVisible = false;
+        } else {
+            header.visible = false;
+        }
+    }
+
+    private void ensurePinnedHeaderLayout(int viewIndex) {
+        View view = mHeaders[viewIndex].view;
+        if (view.isLayoutRequested()) {
+            int widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY);
+            int heightSpec;
+            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+            if (layoutParams != null && layoutParams.height > 0) {
+                heightSpec = View.MeasureSpec
+                        .makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY);
+            } else {
+                heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+            }
+            view.measure(widthSpec, heightSpec);
+            int height = view.getMeasuredHeight();
+            mHeaders[viewIndex].height = height;
+            view.layout(0, 0, mHeaderWidth, height);
+        }
+    }
+
+    /**
+     * Returns the sum of heights of headers pinned to the top.
+     */
+    public int getTotalTopPinnedHeaderHeight() {
+        for (int i = mSize; --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(getPaddingLeft() + 1, 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
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mScrollState == SCROLL_STATE_IDLE) {
+            final int y = (int)ev.getY();
+            for (int i = mSize; --i >= 0;) {
+                PinnedHeader header = mHeaders[i];
+                if (header.visible && header.y <= y && header.y + header.height > y) {
+                    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+                        return smoothScrollToPartition(i);
+                    } else {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return super.onInterceptTouchEvent(ev);
+    }
+
+    private boolean smoothScrollToPartition(int partition) {
+        final int position = mAdapter.getScrollPositionForHeader(partition);
+        if (position == -1) {
+            return false;
+        }
+
+        int offset = 0;
+        for (int i = 0; i < partition; i++) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible) {
+                offset += header.height;
+            }
+        }
+
+        smoothScrollToPositionFromTop(position + getHeaderViewsCount(), offset);
+        return true;
+    }
+
+    private void invalidateIfAnimating() {
+        mAnimating = false;
+        for (int i = 0; i < mSize; i++) {
+            if (mHeaders[i].animating) {
+                mAnimating = true;
+                invalidate();
+                return;
+            }
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        long currentTime = mAnimating ? System.currentTimeMillis() : 0;
+
+        int top = 0;
+        int bottom = getBottom();
+        boolean hasVisibleHeaders = false;
+        for (int i = 0; i < mSize; i++) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible) {
+                hasVisibleHeaders = true;
+                if (header.state == BOTTOM && header.y < bottom) {
+                    bottom = header.y;
+                } else if (header.state == TOP || header.state == FADING) {
+                    int newTop = header.y + header.height;
+                    if (newTop > top) {
+                        top = newTop;
+                    }
+                }
+            }
+        }
+
+        if (hasVisibleHeaders) {
+            canvas.save();
+            mClipRect.set(0, top, getWidth(), bottom);
+            canvas.clipRect(mClipRect);
+        }
+
+        super.dispatchDraw(canvas);
+
+        if (hasVisibleHeaders) {
+            canvas.restore();
+
+            // First draw top headers, then the bottom ones to handle the Z axis correctly
+            for (int i = mSize; --i >= 0;) {
+                PinnedHeader header = mHeaders[i];
+                if (header.visible && (header.state == TOP || header.state == FADING)) {
+                    drawHeader(canvas, header, currentTime);
+                }
+            }
+
+            for (int i = 0; i < mSize; i++) {
+                PinnedHeader header = mHeaders[i];
+                if (header.visible && header.state == BOTTOM) {
+                    drawHeader(canvas, header, currentTime);
+                }
+            }
+        }
+
+        invalidateIfAnimating();
+    }
+
+    private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) {
+        if (header.animating) {
+            int timeLeft = (int)(header.targetTime - currentTime);
+            if (timeLeft <= 0) {
+                header.y = header.targetY;
+                header.visible = header.targetVisible;
+                header.animating = false;
+            } else {
+                header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft
+                        / mAnimationDuration;
+            }
+        }
+        if (header.visible) {
+            View view = header.view;
+            int saveCount = canvas.save();
+            canvas.translate(mHeaderPaddingLeft, header.y);
+            if (header.state == FADING) {
+                mBounds.set(0, 0, mHeaderWidth, view.getHeight());
+                canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
+            }
+            view.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ProfileAndContactsLoader.java b/src/com/android/contacts/common/list/ProfileAndContactsLoader.java
new file mode 100644
index 0000000..9d2bbbb
--- /dev/null
+++ b/src/com/android/contacts/common/list/ProfileAndContactsLoader.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 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.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.os.Bundle;
+import android.provider.ContactsContract.Profile;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * A loader for use in the default contact list, which will also query for the user's profile
+ * if configured to do so.
+ */
+public class ProfileAndContactsLoader extends CursorLoader {
+
+    private boolean mLoadProfile;
+    private String[] mProjection;
+
+    public ProfileAndContactsLoader(Context context) {
+        super(context);
+    }
+
+    public void setLoadProfile(boolean flag) {
+        mLoadProfile = flag;
+    }
+
+    public void setProjection(String[] projection) {
+        super.setProjection(projection);
+        mProjection = projection;
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        // First load the profile, if enabled.
+        List<Cursor> cursors = Lists.newArrayList();
+        if (mLoadProfile) {
+            cursors.add(loadProfile());
+        }
+        final Cursor contactsCursor = super.loadInBackground();
+        cursors.add(contactsCursor);
+        return new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) {
+            @Override
+            public Bundle getExtras() {
+                // Need to get the extras from the contacts cursor.
+                return contactsCursor.getExtras();
+            }
+        };
+    }
+
+    /**
+     * Loads the profile into a MatrixCursor.
+     */
+    private MatrixCursor loadProfile() {
+        Cursor cursor = getContext().getContentResolver().query(Profile.CONTENT_URI, mProjection,
+                null, null, null);
+        try {
+            MatrixCursor matrix = new MatrixCursor(mProjection);
+            Object[] row = new Object[mProjection.length];
+            while (cursor.moveToNext()) {
+                for (int i = 0; i < row.length; i++) {
+                    row[i] = cursor.getString(i);
+                }
+                matrix.addRow(row);
+            }
+            return matrix;
+        } finally {
+            cursor.close();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/preference/ContactsPreferences.java b/src/com/android/contacts/common/preference/ContactsPreferences.java
new file mode 100644
index 0000000..56390fd
--- /dev/null
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -0,0 +1,160 @@
+/*
+ * 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.common.preference;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+
+import com.android.contacts.common.R;
+
+/**
+ * Manages user preferences for contacts.
+ */
+public final class ContactsPreferences extends ContentObserver {
+
+    public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones";
+    public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
+
+    private Context mContext;
+    private int mSortOrder = -1;
+    private int mDisplayOrder = -1;
+    private ChangeListener mListener = null;
+    private Handler mHandler;
+
+    public ContactsPreferences(Context context) {
+        super(null);
+        mContext = context;
+        mHandler = new Handler();
+    }
+
+    public boolean isSortOrderUserChangeable() {
+        return mContext.getResources().getBoolean(R.bool.config_sort_order_user_changeable);
+    }
+
+    public int getDefaultSortOrder() {
+        if (mContext.getResources().getBoolean(R.bool.config_default_sort_order_primary)) {
+            return ContactsContract.Preferences.SORT_ORDER_PRIMARY;
+        } else {
+            return ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE;
+        }
+    }
+
+    public int getSortOrder() {
+        if (!isSortOrderUserChangeable()) {
+            return getDefaultSortOrder();
+        }
+
+        if (mSortOrder == -1) {
+            try {
+                mSortOrder = Settings.System.getInt(mContext.getContentResolver(),
+                        ContactsContract.Preferences.SORT_ORDER);
+            } catch (SettingNotFoundException e) {
+                mSortOrder = getDefaultSortOrder();
+            }
+        }
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+        Settings.System.putInt(mContext.getContentResolver(),
+                ContactsContract.Preferences.SORT_ORDER, sortOrder);
+    }
+
+    public boolean isDisplayOrderUserChangeable() {
+        return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable);
+    }
+
+    public int getDefaultDisplayOrder() {
+        if (mContext.getResources().getBoolean(R.bool.config_default_display_order_primary)) {
+            return ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY;
+        } else {
+            return ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE;
+        }
+    }
+
+    public int getDisplayOrder() {
+        if (!isDisplayOrderUserChangeable()) {
+            return getDefaultDisplayOrder();
+        }
+
+        if (mDisplayOrder == -1) {
+            try {
+                mDisplayOrder = Settings.System.getInt(mContext.getContentResolver(),
+                        ContactsContract.Preferences.DISPLAY_ORDER);
+            } catch (SettingNotFoundException e) {
+                mDisplayOrder = getDefaultDisplayOrder();
+            }
+        }
+        return mDisplayOrder;
+    }
+
+    public void setDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+        Settings.System.putInt(mContext.getContentResolver(),
+                ContactsContract.Preferences.DISPLAY_ORDER, displayOrder);
+    }
+
+    public void registerChangeListener(ChangeListener listener) {
+        if (mListener != null) unregisterChangeListener();
+
+        mListener = listener;
+
+        // Reset preferences to "unknown" because they may have changed while the
+        // observer was unregistered.
+        mDisplayOrder = -1;
+        mSortOrder = -1;
+
+        final ContentResolver contentResolver = mContext.getContentResolver();
+        contentResolver.registerContentObserver(
+                Settings.System.getUriFor(
+                        ContactsContract.Preferences.SORT_ORDER), false, this);
+        contentResolver.registerContentObserver(
+                Settings.System.getUriFor(
+                        ContactsContract.Preferences.DISPLAY_ORDER), false, this);
+    }
+
+    public void unregisterChangeListener() {
+        if (mListener != null) {
+            mContext.getContentResolver().unregisterContentObserver(this);
+            mListener = null;
+        }
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+        // This notification is not sent on the Ui thread. Use the previously created Handler
+        // to switch to the Ui thread
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mSortOrder = -1;
+                mDisplayOrder = -1;
+                if (mListener != null) mListener.onChange();
+            }
+        });
+    }
+
+    public interface ChangeListener {
+        void onChange();
+    }
+}
diff --git a/tests/src/com/android/contacts/common/list/ContactListItemViewTest.java b/tests/src/com/android/contacts/common/list/ContactListItemViewTest.java
new file mode 100644
index 0000000..6eb74db
--- /dev/null
+++ b/tests/src/com/android/contacts/common/list/ContactListItemViewTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2011 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.common.list;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.ContactsContract;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.widget.TextView;
+
+//import com.android.contacts.activities.PeopleActivity;
+import com.android.contacts.common.format.SpannedTestUtils;
+//import com.android.contacts.common.test.IntegrationTestUtils;
+
+/**
+ * Unit tests for {@link com.android.contacts.common.list.ContactListItemView}.
+ *
+ * It uses an {@link ActivityInstrumentationTestCase2} for {@link PeopleActivity} because we need
+ * to have the style properly setup.
+ */
+@LargeTest
+public class ContactListItemViewTest extends AndroidTestCase {
+
+    //private IntegrationTestUtils mUtils;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // This test requires that the screen be turned on.
+        //mUtils = new IntegrationTestUtils(getInstrumentation());
+        //mUtils.acquireScreenWakeLock(getInstrumentation().getTargetContext());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        //mUtils.releaseScreenWakeLock();
+        super.tearDown();
+    }
+
+    public void testShowDisplayName_Simple() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.showDisplayName(cursor, 0, ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
+
+        assertEquals(view.getNameTextView().getText().toString(), "John Doe");
+    }
+
+    public void testShowDisplayName_Unknown() {
+        Cursor cursor = createCursor("", "");
+        ContactListItemView view = createView();
+
+        view.setUnknownNameText("unknown");
+        view.showDisplayName(cursor, 0, ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
+
+        assertEquals(view.getNameTextView().getText().toString(), "unknown");
+    }
+
+    public void testShowDisplayName_WithPrefix() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.setHighlightedPrefix("DOE".toCharArray());
+        view.showDisplayName(cursor, 0, ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
+
+        CharSequence seq = view.getNameTextView().getText();
+        assertEquals("John Doe", seq.toString());
+        SpannedTestUtils.assertPrefixSpan(seq, 5, 7);
+    }
+
+    public void testShowDisplayName_WithPrefixReversed() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.setHighlightedPrefix("DOE".toCharArray());
+        view.showDisplayName(cursor, 0, ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE);
+
+        CharSequence seq = view.getNameTextView().getText();
+        assertEquals("John Doe", seq.toString());
+        SpannedTestUtils.assertPrefixSpan(seq, 5, 7);
+    }
+
+    public void testSetSnippet_Prefix() {
+        ContactListItemView view = createView();
+        view.setHighlightedPrefix("TEST".toCharArray());
+        view.setSnippet("This is a test");
+
+        CharSequence seq = view.getSnippetView().getText();
+
+        assertEquals("This is a test", seq.toString());
+        SpannedTestUtils.assertPrefixSpan(seq, 10, 13);
+    }
+
+    /** Creates the view to be tested. */
+    private ContactListItemView createView() {
+        ContactListItemView view = new ContactListItemView(getContext());
+        // Set the name view to use a Spannable to represent its content.
+        view.getNameTextView().setText("", TextView.BufferType.SPANNABLE);
+        return view;
+    }
+
+    /**
+     * Creates a cursor containing a pair of values.
+     *
+     * @param name the name to insert in the first column of the cursor
+     * @param alternateName the alternate name to insert in the second column of the cursor
+     * @return the newly created cursor
+     */
+    private Cursor createCursor(String name, String alternateName) {
+        MatrixCursor cursor = new MatrixCursor(new String[]{"Name", "AlternateName"});
+        cursor.moveToFirst();
+        cursor.addRow(new Object[]{name, alternateName});
+        return cursor;
+    }
+}