Bringing display options to the browser screen

Change-Id: Ic316e51146ced2aef73a85d1ee5135c21d03a06c
diff --git a/res/layout-xlarge/contacts_list_content.xml b/res/layout-xlarge/contacts_list_content.xml
index 2b279be..0ef0544 100644
--- a/res/layout-xlarge/contacts_list_content.xml
+++ b/res/layout-xlarge/contacts_list_content.xml
@@ -35,6 +35,15 @@
             android:layout_weight="1"
             >
 
+        <Spinner
+            android:id="@+id/filter_spinner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:prompt="@string/list_filter_prompt"
+            android:spinnerMode="dropdown"
+            android:visibility="gone"
+        />
+
         <view
             class="com.android.contacts.ContactEntryListView"
             android:id="@android:id/list"
diff --git a/res/layout/contacts_list_content.xml b/res/layout/contacts_list_content.xml
index 8d1c50a..0737bbc 100644
--- a/res/layout/contacts_list_content.xml
+++ b/res/layout/contacts_list_content.xml
@@ -29,6 +29,14 @@
             android:layout_weight="1"
             >
 
+        <Spinner
+            android:id="@+id/filter_spinner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:prompt="@string/list_filter_prompt"
+            android:visibility="gone"
+        />
+
         <view
             class="com.android.contacts.ContactEntryListView"
             android:id="@android:id/list"
diff --git a/res/layout/filter_spinner_item.xml b/res/layout/filter_spinner_item.xml
new file mode 100644
index 0000000..79c871c
--- /dev/null
+++ b/res/layout/filter_spinner_item.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="52dip"
+    android:layout_width="fill_parent"
+    android:paddingLeft="7dip"
+    android:paddingRight="7dip"
+    android:paddingTop="2dip"
+    android:paddingBottom="2dip"
+    android:gravity="left">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:scaleType="fitCenter"
+        android:layout_width="48dip"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="7dip" />
+
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:gravity="center_vertical"
+        android:ellipsize="end" />
+
+    <TextView
+        android:id="@+id/indented_label"
+        android:layout_width="wrap_content"
+        android:layout_height="fill_parent"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:gravity="center_vertical"
+        android:ellipsize="end"
+        android:paddingLeft="80dip" />
+</LinearLayout>
+
diff --git a/res/values/ids.xml b/res/values/ids.xml
index 13844a3..1b381ab 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -57,4 +57,7 @@
 
     <!-- ContactDetailFragment ContextMenu Ids -->
     <item type="id" name="menu_detail_makeDefault" />
+
+    <!-- Loader ID for contact filters -->
+    <item type="id" name="contact_list_filter_loader" />
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6d68404..5478665 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1312,4 +1312,16 @@
 
     <!-- Toast shown when creating a personal copy of a contact [CHAR LIMIT=100] -->
     <string name="toast_making_personal_copy">Creating a personal copy</string>
+
+    <!-- Prompt for selection of a contact list filter [CHAR LIMIT=64] -->
+    <string name="list_filter_prompt">Choose contact list</string>
+
+    <!-- Contact list filter label indicating that the list is showing all available accounts [CHAR LIMIT=64] -->
+    <string name="list_filter_all_accounts">All accounts</string>
+
+    <!-- Contact list filter indicating that the list shows groups chosen by the user [CHAR LIMIT=64] -->
+    <string name="list_filter_custom">Custom list</string>
+
+    <!-- Contact list filter selection indicating that the list shows groups chosen by the user [CHAR LIMIT=64] -->
+    <string name="list_filter_customize">Customize...</string>
 </resources>
diff --git a/src/com/android/contacts/list/ContactGroupFilterLoader.java b/src/com/android/contacts/list/ContactGroupFilterLoader.java
new file mode 100644
index 0000000..757994e
--- /dev/null
+++ b/src/com/android/contacts/list/ContactGroupFilterLoader.java
@@ -0,0 +1,129 @@
+/*
+ * 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.list;
+
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+
+import android.accounts.Account;
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.Groups;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A loader for the data needed for the group selector.
+ */
+public class ContactGroupFilterLoader extends AsyncTaskLoader<List<ContactListFilter>> {
+
+    private static final class GroupQuery {
+        public static final String[] COLUMNS = {
+            Groups._ID,
+            Groups.ACCOUNT_TYPE,
+            Groups.ACCOUNT_NAME,
+            Groups.TITLE,
+            Groups.AUTO_ADD,
+        };
+
+        public static final int ID = 0;
+        public static final int ACCOUNT_TYPE = 1;
+        public static final int ACCOUNT_NAME = 2;
+        public static final int TITLE = 3;
+        public static final int IS_DEFAULT_GROUP = 4;       // Using the AUTO_ADD group as default
+
+        private static final String SELECTION =
+                Groups.DELETED + "=0 AND " + Groups.FAVORITES + "=0";
+    }
+
+    public ContactGroupFilterLoader(Context context) {
+        super(context);
+    }
+
+    @Override
+    public List<ContactListFilter> loadInBackground() {
+
+        ArrayList<ContactListFilter> results = new ArrayList<ContactListFilter>();
+        Context context = getContext();
+        final Sources sources = Sources.getInstance(context);
+        ArrayList<Account> accounts = sources.getAccounts(false);
+        for (Account account : accounts) {
+            ContactsSource source = sources.getInflatedSource(
+                    account.type, ContactsSource.LEVEL_SUMMARY);
+            Drawable icon = source != null ? source.getDisplayIcon(getContext()) : null;
+            results.add(new ContactListFilter(account.type, account.name, icon, account.name));
+        }
+
+        HashSet<Account> accountsWithGroups = new HashSet<Account>();
+        ContentResolver resolver = context.getContentResolver();
+
+        Cursor cursor = resolver.query(
+                Groups.CONTENT_URI, GroupQuery.COLUMNS, GroupQuery.SELECTION, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                long groupId = cursor.getLong(GroupQuery.ID);
+                String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+                String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+                boolean defaultGroup = false;
+                if (!cursor.isNull(GroupQuery.IS_DEFAULT_GROUP)) {
+                    defaultGroup = cursor.getInt(GroupQuery.IS_DEFAULT_GROUP) != 0;
+                }
+                if (defaultGroup) {
+                    // Find the filter for this account and set the default group ID
+                    for (ContactListFilter filter : results) {
+                        if (filter.accountName.equals(accountName)
+                                && filter.accountType.equals(accountType)) {
+                            filter.groupId = groupId;
+                            break;
+                        }
+                    }
+                } else {
+                    String title = cursor.getString(GroupQuery.TITLE);
+                    results.add(new ContactListFilter(accountType, accountName, groupId, title));
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+
+        Collections.sort(results);
+
+        return results;
+    }
+
+    @Override
+    public void destroy() {
+        stopLoading();
+    }
+
+    @Override
+    public void startLoading() {
+        cancelLoad();
+        forceLoad();
+    }
+
+    @Override
+    public void stopLoading() {
+        cancelLoad();
+    }
+}
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
index 38f1017..2b1970e 100644
--- a/src/com/android/contacts/list/ContactListAdapter.java
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -21,6 +21,7 @@
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.ContactCounts;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.SearchSnippetColumns;
 import android.text.TextUtils;
@@ -29,12 +30,14 @@
 import android.widget.ListView;
 import android.widget.QuickContactBadge;
 
+import java.util.List;
+
 /**
  * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
  */
 public abstract class ContactListAdapter extends ContactEntryListAdapter {
 
-    protected static final String[] PROJECTION = new String[] {
+    protected static final String[] PROJECTION_CONTACT = new String[] {
         Contacts._ID,                           // 0
         Contacts.DISPLAY_NAME_PRIMARY,          // 1
         Contacts.DISPLAY_NAME_ALTERNATIVE,      // 2
@@ -47,6 +50,19 @@
         Contacts.HAS_PHONE_NUMBER,              // 9
     };
 
+    protected static final String[] PROJECTION_DATA = new String[] {
+        Data.CONTACT_ID,                        // 0
+        Data.DISPLAY_NAME_PRIMARY,              // 1
+        Data.DISPLAY_NAME_ALTERNATIVE,          // 2
+        Data.SORT_KEY_PRIMARY,                  // 3
+        Data.STARRED,                           // 4
+        Data.CONTACT_PRESENCE,                  // 5
+        Data.PHOTO_ID,                          // 6
+        Data.LOOKUP_KEY,                        // 7
+        Data.PHONETIC_NAME,                     // 8
+        Data.HAS_PHONE_NUMBER,                  // 9
+    };
+
     protected static final String[] FILTER_PROJECTION = new String[] {
         Contacts._ID,                           // 0
         Contacts.DISPLAY_NAME_PRIMARY,          // 1
@@ -84,6 +100,9 @@
     private long mSelectedContactDirectoryId;
     private String mSelectedContactLookupKey;
 
+    private ContactListFilter mFilter;
+    private List<ContactListFilter> mAllFilters;
+
     public ContactListAdapter(Context context) {
         super(context);
 
@@ -94,6 +113,25 @@
         return mUnknownNameText;
     }
 
+    /**
+     * Returns a full set of all available list filters.
+     */
+    public List<ContactListFilter> getAllFilters() {
+        return mAllFilters;
+    }
+
+    /**
+     * Returns the currently selected filter.
+     */
+    public ContactListFilter getFilter() {
+        return mFilter;
+    }
+
+    public void setFilter(ContactListFilter filter, List<ContactListFilter> allFilters) {
+        mFilter = filter;
+        mAllFilters = allFilters;
+    }
+
     public long getSelectedContactDirectoryId() {
         return mSelectedContactDirectoryId;
     }
diff --git a/src/com/android/contacts/list/ContactListFilter.java b/src/com/android/contacts/list/ContactListFilter.java
new file mode 100644
index 0000000..3d32dd0
--- /dev/null
+++ b/src/com/android/contacts/list/ContactListFilter.java
@@ -0,0 +1,123 @@
+/*
+ * 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.list;
+
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+/**
+ * Contact list filter parameters.
+ */
+public final class ContactListFilter implements Comparable<ContactListFilter> {
+
+    public static final int FILTER_TYPE_ALL_ACCOUNTS = -1;
+    public static final int FILTER_TYPE_CUSTOM = -2;
+    public static final int FILTER_TYPE_ACCOUNT = 0;
+    public static final int FILTER_TYPE_GROUP = 1;
+
+    public int filterType;
+    public String accountType;
+    public String accountName;
+    public Drawable icon;
+    public long groupId;
+    public String title;
+
+    public ContactListFilter(int filterType) {
+        this.filterType = filterType;
+    }
+
+    public ContactListFilter(
+            String accountType, String accountName, Drawable icon, String title) {
+        this.filterType = ContactListFilter.FILTER_TYPE_ACCOUNT;
+        this.accountType = accountType;
+        this.accountName = accountName;
+        this.icon = icon;
+        this.title = title;
+    }
+
+    public ContactListFilter(
+            String accountType, String accountName, long groupId, String title) {
+        this.filterType = ContactListFilter.FILTER_TYPE_GROUP;
+        this.accountType = accountType;
+        this.accountName = accountName;
+        this.groupId = groupId;
+        this.title = title;
+    }
+
+    @Override
+    public String toString() {
+        switch (filterType) {
+            case ContactListFilter.FILTER_TYPE_ACCOUNT:
+                return "account: " + accountType + " " + accountName;
+            case ContactListFilter.FILTER_TYPE_GROUP:
+                return "group: " + accountType + " " + accountName + " " + title + "(" + groupId
+                        + ")";
+        }
+        return super.toString();
+    }
+
+    @Override
+    public int compareTo(ContactListFilter another) {
+        int res = accountType.compareTo(another.accountType);
+        if (res != 0) {
+            return res;
+        }
+
+        res = accountName.compareTo(another.accountName);
+        if (res != 0) {
+            return res;
+        }
+
+        if (filterType != another.filterType) {
+            return filterType - another.filterType;
+        }
+
+        String title1 = title != null ? title : "";
+        String title2 = another.title != null ? another.title : "";
+        return title1.compareTo(title2);
+    }
+
+    @Override
+    public int hashCode() {
+        int code = filterType;
+        if (accountType != null) {
+            code = code * 31 + accountType.hashCode();
+            code = code * 31 + accountName.hashCode();
+        }
+        if (groupId != 0) {
+            code = code * 31 + (int) groupId;
+        }
+        return code;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (!(other instanceof ContactListFilter)) {
+            return false;
+        }
+
+        ContactListFilter otherFilter = (ContactListFilter) other;
+        return filterType == otherFilter.filterType
+                && TextUtils.equals(accountName, otherFilter.accountName)
+                && TextUtils.equals(accountType, otherFilter.accountType)
+                && groupId == otherFilter.groupId;
+    }
+}
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 76ba490..3e6dbe5 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -18,21 +18,33 @@
 import com.android.contacts.R;
 import com.android.contacts.ui.ContactsPreferencesActivity.Prefs;
 
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Loader;
 import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
+import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.BaseAdapter;
 import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.Spinner;
 import android.widget.TextView;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Fragment containing a contact list used for browsing (as compared to
  * picking a contact with one of the PICK intents).
  */
-public class DefaultContactBrowseListFragment extends ContactBrowseListFragment {
+public class DefaultContactBrowseListFragment extends ContactBrowseListFragment
+        implements OnItemSelectedListener {
 
     private static final String KEY_EDIT_MODE = "editMode";
     private static final String KEY_CREATE_CONTACT_ENABLED = "createContactEnabled";
@@ -45,6 +57,29 @@
     private boolean mVisibleContactsRestrictionEnabled = true;
     private View mHeaderView;
 
+    private boolean mFilterEnabled = true;
+    private SparseArray<ContactListFilter> mFilters;
+    private ArrayList<ContactListFilter> mFilterList;
+    private int mNextFilterId = 1;
+    private Spinner mFilterSpinner;
+    private ContactListFilter mFilter;
+    private boolean mFiltersLoaded;
+
+    private LoaderCallbacks<List<ContactListFilter>> mGroupFilterLoaderCallbacks =
+            new LoaderCallbacks<List<ContactListFilter>>() {
+
+        @Override
+        public ContactGroupFilterLoader onCreateLoader(int id, Bundle args) {
+            return new ContactGroupFilterLoader(getContext());
+        }
+
+        @Override
+        public void onLoadFinished(
+                Loader<List<ContactListFilter>> loader, List<ContactListFilter> data) {
+            onGroupFilterLoadFinished(data);
+        }
+    };
+
     public DefaultContactBrowseListFragment() {
         setPhotoLoaderEnabled(true);
         setSectionHeaderDisplayEnabled(true);
@@ -139,6 +174,7 @@
         if (adapter != null) {
             adapter.setContactsWithPhoneNumbersOnly(isShowingContactsWithPhonesOnly());
             adapter.setVisibleContactsOnly(mVisibleContactsRestrictionEnabled);
+            adapter.setFilter(mFilter, mFilterList);
         }
     }
 
@@ -158,6 +194,20 @@
         headerContainer.addView(mHeaderView);
         getListView().addHeaderView(headerContainer);
         checkHeaderViewVisibility();
+        configureFilterSpinner();
+    }
+
+    protected void configureFilterSpinner() {
+        mFilterSpinner = (Spinner)getView().findViewById(R.id.filter_spinner);
+        if (mFilterSpinner == null) {
+            return;
+        }
+
+        if (!mFilterEnabled) {
+            mFilterSpinner.setVisibility(View.GONE);
+            return;
+        }
+        mFilterSpinner.setOnItemSelectedListener(this);
     }
 
     @Override
@@ -203,4 +253,201 @@
     public void setCreateContactEnabled(boolean flag) {
         this.mCreateContactEnabled = flag;
     }
+
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        setContactListFilter((int) id);
+    }
+
+    public void onNothingSelected(AdapterView<?> parent) {
+        setContactListFilter(0);
+    }
+
+    @Override
+    public void onStart() {
+        if (mFilterEnabled) {
+            mFiltersLoaded = false;
+        }
+        super.onStart();
+    }
+
+    @Override
+    protected void startLoading() {
+        // We need to load filters before we can load the list contents
+        if (mFilterEnabled && !mFiltersLoaded) {
+            getLoaderManager().restartLoader(
+                    R.id.contact_list_filter_loader, null, mGroupFilterLoaderCallbacks);
+        } else {
+            super.startLoading();
+        }
+    }
+
+    protected void onGroupFilterLoadFinished(List<ContactListFilter> filters) {
+        if (mFilters == null) {
+            mFilters = new SparseArray<ContactListFilter>(filters.size());
+            mFilterList = new ArrayList<ContactListFilter>();
+        } else {
+            mFilters.clear();
+            mFilterList.clear();
+        }
+
+        boolean filterValid = mFilter != null
+                && (mFilter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
+                        || mFilter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM);
+
+        int accountCount = 0;
+        int count = filters.size();
+        for (int index = 0; index < count; index++) {
+            if (filters.get(index).filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+                accountCount++;
+            }
+        }
+
+        if (accountCount > 1) {
+            mFilters.append(mNextFilterId++,
+                    new ContactListFilter(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS));
+            mFilters.append(mNextFilterId++,
+                    new ContactListFilter(ContactListFilter.FILTER_TYPE_CUSTOM));
+        }
+
+        boolean firstAccount = true;
+        for (int index = 0; index < count; index++) {
+            ContactListFilter filter = filters.get(index);
+            mFilters.append(mNextFilterId++, filter);
+            mFilterList.add(filter);
+            filterValid |= filter.equals(mFilter);
+
+            if (firstAccount && filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT
+                    && accountCount == 1) {
+                firstAccount = false;
+                mFilters.append(mNextFilterId++,
+                        new ContactListFilter(ContactListFilter.FILTER_TYPE_CUSTOM));
+            }
+        }
+
+        mFiltersLoaded = true;
+        if (mFilter == null  || !filterValid) {
+            mFilter = getDefaultFilter();
+        }
+
+        mFilterSpinner.setAdapter(new FilterSpinnerAdapter());
+        updateFilterView();
+
+        startLoading();
+    }
+
+    protected void setContactListFilter(int filterId) {
+        ContactListFilter filter;
+        if (filterId == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+            filter = new ContactListFilter(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
+        } else if (filterId == ContactListFilter.FILTER_TYPE_CUSTOM) {
+            filter = new ContactListFilter(ContactListFilter.FILTER_TYPE_CUSTOM);
+        } else {
+            filter = mFilters.get(filterId);
+            if (filter == null) {
+                filter = getDefaultFilter();
+            }
+        }
+
+        if (!filter.equals(mFilter)) {
+            mFilter = filter;
+            updateFilterView();
+            reloadData();
+        }
+    }
+
+    private ContactListFilter getDefaultFilter() {
+        return mFilters.valueAt(0);
+    }
+
+    protected void updateFilterView() {
+        if (mFiltersLoaded) {
+            mFilterSpinner.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private class FilterSpinnerAdapter extends BaseAdapter {
+        private LayoutInflater mLayoutInflater;
+
+        public FilterSpinnerAdapter() {
+            mLayoutInflater = LayoutInflater.from(getContext());
+        }
+
+        @Override
+        public int getCount() {
+            return mFilters.size();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return mFilters.keyAt(position);
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mFilters.valueAt(position);
+        }
+
+        @Override
+        public View getDropDownView(int position, View convertView, ViewGroup parent) {
+            return getView(position, convertView, parent, true);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            return getView(position, convertView, parent, false);
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent, boolean dropdown) {
+            View view = convertView != null ? convertView
+                    : mLayoutInflater.inflate(R.layout.filter_spinner_item, parent, false);
+            ImageView icon = (ImageView) view.findViewById(R.id.icon);
+            TextView label = (TextView) view.findViewById(R.id.label);
+            TextView indentedLabel = (TextView) view.findViewById(R.id.indented_label);
+            ContactListFilter filter = mFilters.valueAt(position);
+            switch (filter.filterType) {
+                case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
+                    icon.setVisibility(View.GONE);
+                    label.setText(R.string.list_filter_all_accounts);
+                    label.setVisibility(View.VISIBLE);
+                    indentedLabel.setVisibility(View.GONE);
+                    break;
+                }
+                case ContactListFilter.FILTER_TYPE_CUSTOM: {
+                    icon.setVisibility(View.GONE);
+                    label.setText(dropdown
+                            ? R.string.list_filter_customize
+                            : R.string.list_filter_custom);
+                    label.setVisibility(View.VISIBLE);
+                    indentedLabel.setVisibility(View.GONE);
+                    break;
+                }
+                case ContactListFilter.FILTER_TYPE_ACCOUNT: {
+                    icon.setVisibility(View.VISIBLE);
+                    if (filter.icon != null) {
+                        icon.setImageDrawable(filter.icon);
+                    } else {
+                        icon.setImageResource(R.drawable.unknown_source);
+                    }
+                    label.setText(filter.accountName);
+                    label.setVisibility(View.VISIBLE);
+                    indentedLabel.setVisibility(View.GONE);
+                    break;
+                }
+                case ContactListFilter.FILTER_TYPE_GROUP: {
+                    icon.setVisibility(View.GONE);
+                    if (dropdown) {
+                        label.setVisibility(View.GONE);
+                        indentedLabel.setText(filter.title);
+                        indentedLabel.setVisibility(View.VISIBLE);
+                    } else {
+                        label.setText(filter.title);
+                        label.setVisibility(View.VISIBLE);
+                        indentedLabel.setVisibility(View.GONE);
+                    }
+                    break;
+                }
+            }
+            return view;
+        }
+    }
 }
diff --git a/src/com/android/contacts/list/DefaultContactListAdapter.java b/src/com/android/contacts/list/DefaultContactListAdapter.java
index 593b9ba..5549820 100644
--- a/src/com/android/contacts/list/DefaultContactListAdapter.java
+++ b/src/com/android/contacts/list/DefaultContactListAdapter.java
@@ -21,11 +21,17 @@
 import android.net.Uri;
 import android.net.Uri.Builder;
 import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.view.View;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
  */
@@ -48,8 +54,8 @@
 
     @Override
     public void configureLoader(CursorLoader loader, long directoryId) {
-        Uri uri;
 
+        ContactListFilter filter = getFilter();
         if (isSearchMode()) {
             String query = getQueryString();
             Builder builder = Contacts.CONTENT_FILTER_URI.buildUpon();
@@ -61,29 +67,14 @@
 
             builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
                     String.valueOf(directoryId));
-            uri = builder.build();
+            loader.setUri(builder.build());
             loader.setProjection(FILTER_PROJECTION);
         } else {
-            uri = Contacts.CONTENT_URI;
-            loader.setProjection(PROJECTION);
+            configureUri(loader, directoryId, filter);
+            configureProjection(loader, directoryId, filter);
+            configureSelection(loader, directoryId, filter);
         }
 
-        if (directoryId == Directory.DEFAULT) {
-            if (mVisibleContactsOnly && mContactsWithPhoneNumbersOnly) {
-                loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1"
-                        + " AND " + Contacts.HAS_PHONE_NUMBER + "=1");
-            } else if (mVisibleContactsOnly) {
-                loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1");
-            } else if (mContactsWithPhoneNumbersOnly) {
-                loader.setSelection(Contacts.HAS_PHONE_NUMBER + "=1");
-            }
-            if (isSectionHeaderDisplayEnabled()) {
-                uri = buildSectionIndexerUri(uri);
-            }
-        }
-
-        loader.setUri(uri);
-
         String sortOrder;
         if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
             sortOrder = Contacts.SORT_KEY_PRIMARY;
@@ -94,6 +85,115 @@
         loader.setSortOrder(sortOrder);
     }
 
+    protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) {
+        Uri uri;
+        if (filter != null && filter.groupId != 0) {
+            uri = Data.CONTENT_URI;
+        } else {
+            uri = Contacts.CONTENT_URI;
+        }
+
+        if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) {
+            uri = buildSectionIndexerUri(uri);
+        }
+        loader.setUri(uri);
+    }
+
+    protected void configureProjection(
+            CursorLoader loader, long directoryId, ContactListFilter filter) {
+        if (filter != null && filter.groupId != 0) {
+            loader.setProjection(PROJECTION_DATA);
+        } else {
+            loader.setProjection(PROJECTION_CONTACT);
+        }
+    }
+
+    private void configureSelection(
+            CursorLoader loader, long directoryId, ContactListFilter filter) {
+        if (filter == null) {
+            return;
+        }
+
+        if (directoryId != Directory.DEFAULT || filter == null) {
+            loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1");
+            return;
+        }
+
+        StringBuilder selection = new StringBuilder();
+        List<String> selectionArgs = new ArrayList<String>();
+
+        switch (filter.filterType) {
+            case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
+                List<ContactListFilter> filters = getAllFilters();
+                for (ContactListFilter aFilter : filters) {
+                    if (aFilter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+                        if (selection.length() > 0) {
+                            selection.append(" OR ");
+                        }
+                        selection.append("(");
+                        if (aFilter.groupId != 0) {
+                            // TODO: avoid the use of private API
+                            // TODO: optimize the query
+                            selection.append(
+                                    Contacts._ID + " IN ("
+                                            + "SELECT " + RawContacts.CONTACT_ID
+                                            + " FROM view_data"
+                                            + " WHERE " + Data.MIMETYPE + "=?"
+                                            + "   AND " + GroupMembership.GROUP_ROW_ID + "=?)");
+                            selectionArgs.add(GroupMembership.CONTENT_ITEM_TYPE);
+                            selectionArgs.add(String.valueOf(aFilter.groupId));
+                        } else {
+                            // TODO: avoid the use of private API
+                            selection.append(
+                                    Contacts._ID + " IN ("
+                                            + "SELECT " + RawContacts.CONTACT_ID
+                                            + " FROM raw_contacts"
+                                            + " WHERE " + RawContacts.ACCOUNT_TYPE + "=?"
+                                            + "   AND " + RawContacts.ACCOUNT_NAME + "=?)");
+                            selectionArgs.add(aFilter.accountType);
+                            selectionArgs.add(aFilter.accountName);
+                        }
+                        selection.append(")");
+                    }
+                }
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_CUSTOM: {
+                if (mVisibleContactsOnly && mContactsWithPhoneNumbersOnly) {
+                    selection.append(Contacts.IN_VISIBLE_GROUP + "=1"
+                            + " AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+                } else if (mVisibleContactsOnly) {
+                    selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+                } else if (mContactsWithPhoneNumbersOnly) {
+                    selection.append(Contacts.HAS_PHONE_NUMBER + "=1");
+                }
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_ACCOUNT:
+            case ContactListFilter.FILTER_TYPE_GROUP: {
+                if (filter.groupId != 0) {
+                    selection.append(Data.MIMETYPE + "=?"
+                            + " AND " + GroupMembership.GROUP_ROW_ID + "=?");
+                    selectionArgs.add(GroupMembership.CONTENT_ITEM_TYPE);
+                    selectionArgs.add(String.valueOf(filter.groupId));
+                } else {
+                    // TODO: avoid the use of private API
+                    selection.append(
+                            Contacts._ID + " IN ("
+                                    + "SELECT " + RawContacts.CONTACT_ID
+                                    + " FROM raw_contacts"
+                                    + " WHERE " + RawContacts.ACCOUNT_TYPE + "=?"
+                                    + "   AND " + RawContacts.ACCOUNT_NAME + "=?)");
+                    selectionArgs.add(filter.accountType);
+                    selectionArgs.add(filter.accountName);
+                }
+                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;
diff --git a/src/com/android/contacts/list/JoinContactListAdapter.java b/src/com/android/contacts/list/JoinContactListAdapter.java
index 14cfa8f..b1bb586 100644
--- a/src/com/android/contacts/list/JoinContactListAdapter.java
+++ b/src/com/android/contacts/list/JoinContactListAdapter.java
@@ -97,7 +97,7 @@
         loader.setSuggestionUri(builder.build());
 
         // TODO simplify projection
-        loader.setProjection(PROJECTION);
+        loader.setProjection(PROJECTION_CONTACT);
         loader.setUri(buildSectionIndexerUri(Contacts.CONTENT_URI));
         loader.setSelection(Contacts.IN_VISIBLE_GROUP + "=1 AND " + Contacts._ID + "!=?");
         loader.setSelectionArgs(new String[]{String.valueOf(mTargetContactId)});
@@ -218,8 +218,8 @@
     }
 
     public Cursor getShowAllContactsLabelCursor() {
-        MatrixCursor matrixCursor = new MatrixCursor(PROJECTION);
-        Object[] row = new Object[PROJECTION.length];
+        MatrixCursor matrixCursor = new MatrixCursor(PROJECTION_CONTACT);
+        Object[] row = new Object[PROJECTION_CONTACT.length];
         matrixCursor.addRow(row);
         return matrixCursor;
     }
diff --git a/src/com/android/contacts/list/StrequentContactListAdapter.java b/src/com/android/contacts/list/StrequentContactListAdapter.java
index 712876e..b8fedb1 100644
--- a/src/com/android/contacts/list/StrequentContactListAdapter.java
+++ b/src/com/android/contacts/list/StrequentContactListAdapter.java
@@ -80,7 +80,7 @@
                     + "FrequentlyContactedContactsIncluded is set");
         }
 
-        loader.setProjection(PROJECTION);
+        loader.setProjection(PROJECTION_CONTACT);
         loader.setSortOrder(sortOrder);
     }