Added new display groups UI from spec.

Instead of only displaying a single group, we allow the
user to toggle the visibility of all available groups, and
the backend provider takes care of the visibility logic to
keep our list-of-contacts cursor spiffy.

This UI has some limitations and is just a first revision,
for example you can't display contacts from a provider that
aren't part of a group, other than picking the gloal "all
contacts" option.  Also, filtering to "only with phones" can
be confusing to users, need to iterate.

The group list is a custom ExpandableListAdapter that walks
the summary cursor, splitting each source package into its
own expandable group.  There is some fancy work done so this
only requires one cursor, so it scales nicely.
diff --git a/src/com/android/contacts/DisplayGroupsActivity.java b/src/com/android/contacts/DisplayGroupsActivity.java
new file mode 100644
index 0000000..c468655
--- /dev/null
+++ b/src/com/android/contacts/DisplayGroupsActivity.java
@@ -0,0 +1,635 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+
+import android.app.ExpandableListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.CharArrayBuffer;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.GroupsColumns;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CheckBox;
+import android.widget.ExpandableListView;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Shows a list of all available {@link Groups} available, letting the user
+ * select which ones they want to be visible.
+ */
+public final class DisplayGroupsActivity extends ExpandableListActivity implements
+        QueryCompleteListener, OnItemClickListener {
+    private static final String TAG = "DisplayGroupsActivity";
+
+    public interface Prefs {
+        public static final String DISPLAY_ALL = "display_all";
+        public static final boolean DISPLAY_ALL_DEFAULT = true;
+
+        public static final String DISPLAY_ONLY_PHONES = "only_phones";
+        public static final boolean DISPLAY_ONLY_PHONES_DEFAULT = true;
+
+    }
+
+    private ExpandableListView mList;
+    private DisplayGroupsAdapter mAdapter;
+
+    private SharedPreferences mPrefs;
+    private NotifyingAsyncQueryHandler mHandler;
+
+    private static final int QUERY_TOKEN = 42;
+
+    private View mHeaderAll;
+    private View mHeaderPhones;
+    private View mHeaderSeparator;
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(android.R.layout.expandable_list_content);
+
+        mList = getExpandableListView();
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+        boolean displayAll = mPrefs.getBoolean(Prefs.DISPLAY_ALL, Prefs.DISPLAY_ALL_DEFAULT);
+        boolean displayOnlyPhones = mPrefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
+                Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
+
+        final LayoutInflater inflater = getLayoutInflater();
+
+        // Add the "All contacts" header modifier.
+        mHeaderAll = inflater.inflate(R.layout.display_header, mList, false);
+        mHeaderAll.setId(R.id.header_all);
+        {
+            CheckBox checkbox = (CheckBox)mHeaderAll.findViewById(android.R.id.checkbox);
+            TextView text1 = (TextView)mHeaderAll.findViewById(android.R.id.text1);
+            checkbox.setChecked(displayAll);
+            text1.setText(R.string.showAllGroups);
+        }
+        mList.addHeaderView(mHeaderAll, null, true);
+
+
+        // Add the "Only contacts with phones" header modifier.
+        mHeaderPhones = inflater.inflate(R.layout.display_header, mList, false);
+        mHeaderPhones.setId(R.id.header_phones);
+        {
+            CheckBox checkbox = (CheckBox)mHeaderPhones.findViewById(android.R.id.checkbox);
+            TextView text1 = (TextView)mHeaderPhones.findViewById(android.R.id.text1);
+            TextView text2 = (TextView)mHeaderPhones.findViewById(android.R.id.text2);
+            checkbox.setChecked(displayOnlyPhones);
+            text1.setText(R.string.showFilterPhones);
+            text2.setText(R.string.showFilterPhonesDescrip);
+        }
+        mList.addHeaderView(mHeaderPhones, null, true);
+
+
+        // Add the separator before showing the detailed group list.
+        mHeaderSeparator = inflater.inflate(R.layout.list_separator, mList, false);
+        {
+            TextView text1 = (TextView)mHeaderSeparator;
+            text1.setText(R.string.headerContactGroups);
+        }
+        mList.addHeaderView(mHeaderSeparator, null, false);
+
+
+        final TextView allContactsView = (TextView)mHeaderAll.findViewById(android.R.id.text2);
+
+        mAdapter = new DisplayGroupsAdapter(this);
+        mAdapter.setAllContactsView(allContactsView);
+
+        mAdapter.setEnabled(!displayAll);
+        mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+
+        setListAdapter(mAdapter);
+
+        // Catch clicks on the header views
+        mList.setOnItemClickListener(this);
+
+        mHandler = new NotifyingAsyncQueryHandler(this, this);
+        startQuery();
+
+    }
+
+    @Override
+    protected void onRestart() {
+        super.onRestart();
+        startQuery();
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        mHandler.cancelOperation(QUERY_TOKEN);
+    }
+
+
+    private void startQuery() {
+        mHandler.cancelOperation(QUERY_TOKEN);
+        mHandler.startQuery(QUERY_TOKEN, null, Groups.CONTENT_SUMMARY_URI,
+                Projections.PROJ_SUMMARY, null, null, Projections.SORT_ORDER);
+    }
+
+    public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        mAdapter.changeCursor(cursor);
+
+        // Expand all data sources
+        final int groupCount = mAdapter.getGroupCount();
+        for (int i = 0; i < groupCount; i++) {
+            mList.expandGroup(i);
+        }
+    }
+
+    /**
+     * Handle any clicks on header views added to our {@link #mAdapter}, which
+     * are usually the global modifier checkboxes.
+     */
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
+        switch (view.getId()) {
+            case R.id.header_all: {
+                checkbox.toggle();
+                final boolean displayAll = checkbox.isChecked();
+
+                Editor editor = mPrefs.edit();
+                editor.putBoolean(Prefs.DISPLAY_ALL, displayAll);
+                editor.commit();
+
+                mAdapter.setEnabled(!displayAll);
+                mAdapter.notifyDataSetChanged();
+
+                break;
+            }
+            case R.id.header_phones: {
+                checkbox.toggle();
+                final boolean displayOnlyPhones = checkbox.isChecked();
+
+                Editor editor = mPrefs.edit();
+                editor.putBoolean(Prefs.DISPLAY_ONLY_PHONES, displayOnlyPhones);
+                editor.commit();
+
+                mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+                mAdapter.notifyDataSetChanged();
+
+                break;
+            }
+        }
+    }
+
+    /**
+     * Handle any clicks on {@link ExpandableListAdapter} children, which
+     * usually mean toggling its visible state.
+     */
+    @Override
+    public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+            int childPosition, long id) {
+        if (!mAdapter.isEnabled()) {
+            return false;
+        }
+
+        final CheckBox checkbox = (CheckBox)v.findViewById(android.R.id.checkbox);
+        checkbox.toggle();
+
+        // Build visibility update and send down to database
+        final ContentResolver resolver = getContentResolver();
+        final ContentValues values = new ContentValues();
+
+        values.put(Groups.GROUP_VISIBLE, checkbox.isChecked() ? 1 : 0);
+
+        final long groupId = mAdapter.getChildId(groupPosition, childPosition);
+        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
+
+        resolver.update(groupUri, values, null, null);
+
+        return true;
+    }
+
+    /**
+     * Helper for obtaining {@link Resources} instances that are based in an
+     * external package. Maintains internal cache to remain fast.
+     */
+    private static class ExternalResources {
+        private Context mContext;
+        private HashMap<String, Context> mCache = new HashMap<String, Context>();
+
+        public ExternalResources(Context context) {
+            mContext = context;
+        }
+
+        private Context getPackageContext(String packageName) throws NameNotFoundException {
+            Context theirContext = mCache.get(packageName);
+            if (theirContext == null) {
+                theirContext = mContext.createPackageContext(packageName, 0);
+                mCache.put(packageName, theirContext);
+            }
+            return theirContext;
+        }
+
+        public Resources getResources(String packageName) throws NameNotFoundException {
+            return getPackageContext(packageName).getResources();
+        }
+
+        public CharSequence getText(String packageName, int stringRes)
+                throws NameNotFoundException {
+            return getResources(packageName).getText(stringRes);
+        }
+    }
+
+    /**
+     * Adapter that shows all display groups as returned by a {@link Cursor}
+     * over {@link Groups#CONTENT_SUMMARY_URI}, along with their current visible
+     * status. Splits groups into sections based on {@link Groups#PACKAGE}.
+     */
+    private static class DisplayGroupsAdapter extends BaseExpandableListAdapter {
+        private boolean mDataValid;
+        private Cursor mCursor;
+        private Context mContext;
+        private Resources mResources;
+        private ExternalResources mExternalRes;
+        private LayoutInflater mInflater;
+        private int mRowIDColumn;
+
+        private TextView mAllContactsView;
+
+        private boolean mEnabled = true;
+        private boolean mChildWithPhones = false;
+
+        private ContentObserver mContentObserver = new MyChangeObserver();
+        private DataSetObserver mDataSetObserver = new MyDataSetObserver();
+
+        /**
+         * A single group in our expandable list.
+         */
+        private static class Group {
+            public long packageId = -1;
+            public String packageName = null;
+            public int firstPos;
+            public int lastPos;
+            public CharSequence label;
+        }
+
+        /**
+         * Maintain a list of all groups that need to be displayed by this
+         * adapter, usually built by walking across a single {@link Cursor} and
+         * finding the {@link Groups#PACKAGE} boundaries.
+         */
+        private static final ArrayList<Group> mGroups = new ArrayList<Group>();
+
+        public DisplayGroupsAdapter(Context context) {
+            mContext = context;
+            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            mResources = context.getResources();
+            mExternalRes = new ExternalResources(mContext);
+        }
+
+        /**
+         * In group descriptions, show the number of contacts with phone
+         * numbers, in addition to the total contacts.
+         */
+        public void setChildDescripWithPhones(boolean withPhones) {
+            mChildWithPhones = withPhones;
+        }
+
+        /**
+         * Set a {@link TextView} to be filled with the total number of contacts
+         * across all available groups.
+         */
+        public void setAllContactsView(TextView allContactsView) {
+            mAllContactsView = allContactsView;
+        }
+
+        /**
+         * Set the {@link View#setEnabled(boolean)} state of any views
+         * constructed by this adapter.
+         */
+        public void setEnabled(boolean enabled) {
+            mEnabled = enabled;
+        }
+
+        /**
+         * Returns the {@link View#setEnabled(boolean)} value being set for any
+         * children views of this adapter.
+         */
+        public boolean isEnabled() {
+            return mEnabled;
+        }
+
+        /**
+         * Used internally to build the {@link #mGroups} mapping. Call when you
+         * have a valid cursor and are ready to rebuild the mapping.
+         */
+        private void buildInternalMapping() {
+            final PackageManager pm = mContext.getPackageManager();
+            int totalContacts = 0;
+            Group group = null;
+
+            mGroups.clear();
+            mCursor.moveToPosition(-1);
+            while (mCursor.moveToNext()) {
+                final int position = mCursor.getPosition();
+                final long packageId = mCursor.getLong(Projections.COL_PACKAGE_ID);
+                totalContacts += mCursor.getInt(Projections.COL_SUMMARY_COUNT);
+                if (group == null || packageId != group.packageId) {
+                    group = new Group();
+                    group.packageId = packageId;
+                    group.packageName = mCursor.getString(Projections.COL_PACKAGE);
+                    group.firstPos = position;
+                    group.label = group.packageName;
+
+                    try {
+                        group.label = pm.getApplicationInfo(group.packageName, 0).loadLabel(pm);
+                    } catch (NameNotFoundException e) {
+                        Log.w(TAG, "couldn't find label for package " + group.packageName);
+                    }
+
+                    mGroups.add(group);
+                }
+                group.lastPos = position;
+            }
+
+            if (mAllContactsView != null) {
+                mAllContactsView.setText(mResources.getQuantityString(R.plurals.groupDescrip,
+                        totalContacts, totalContacts));
+            }
+
+        }
+
+        /**
+         * Map the given group and child position into a flattened position on
+         * our single {@link Cursor}.
+         */
+        public int getCursorPosition(int groupPosition, int childPosition) {
+            // The actual cursor position for a child is simply stepping from
+            // the first position for that group.
+            final Group group = mGroups.get(groupPosition);
+            final int position = group.firstPos + childPosition;
+            return position;
+        }
+
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        public boolean isChildSelectable(int groupPosition, int childPosition) {
+            return true;
+        }
+
+        public Object getChild(int groupPosition, int childPosition) {
+            if (mDataValid && mCursor != null) {
+                final int position = getCursorPosition(groupPosition, childPosition);
+                mCursor.moveToPosition(position);
+                return mCursor;
+            } else {
+                return null;
+            }
+        }
+
+        public long getChildId(int groupPosition, int childPosition) {
+            if (mDataValid && mCursor != null) {
+                final int position = getCursorPosition(groupPosition, childPosition);
+                if (mCursor.moveToPosition(position)) {
+                    return mCursor.getLong(mRowIDColumn);
+                } else {
+                    return 0;
+                }
+            } else {
+                return 0;
+            }
+        }
+
+        public int getChildrenCount(int groupPosition) {
+            if (mDataValid && mCursor != null) {
+                final Group group = mGroups.get(groupPosition);
+                final int size = group.lastPos - group.firstPos + 1;
+                return size;
+            } else {
+                return 0;
+            }
+        }
+
+        public Object getGroup(int groupPosition) {
+            if (mDataValid && mCursor != null) {
+                return mGroups.get(groupPosition);
+            } else {
+                return null;
+            }
+        }
+
+        public int getGroupCount() {
+            if (mDataValid && mCursor != null) {
+                return mGroups.size();
+            } else {
+                return 0;
+            }
+        }
+
+        public long getGroupId(int groupPosition) {
+            if (mDataValid && mCursor != null) {
+                final Group group = mGroups.get(groupPosition);
+                return group.packageId;
+            } else {
+                return 0;
+            }
+        }
+
+        public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+                ViewGroup parent) {
+            if (!mDataValid) {
+                throw new IllegalStateException("called with invalid cursor");
+            }
+
+            final Group group = mGroups.get(groupPosition);
+
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.display_group, parent, false);
+            }
+
+            final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+
+            text1.setText(group.label);
+
+            convertView.setEnabled(mEnabled);
+
+            return convertView;
+        }
+
+        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+                View convertView, ViewGroup parent) {
+            if (!mDataValid) {
+                throw new IllegalStateException("called with invalid cursor");
+            }
+
+            final int position = getCursorPosition(groupPosition, childPosition);
+            if (!mCursor.moveToPosition(position)) {
+                throw new IllegalStateException("couldn't move cursor to position " + position);
+            }
+
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.display_child, parent, false);
+            }
+
+            final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+            final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
+
+            final int count = mCursor.getInt(Projections.COL_SUMMARY_COUNT);
+            final int withPhones = mCursor.getInt(Projections.COL_SUMMARY_WITH_PHONES);
+            final int membersVisible = mCursor.getInt(Projections.COL_GROUP_VISIBLE);
+
+            // Read title, but override with string resource when present
+            CharSequence title = mCursor.getString(Projections.COL_TITLE);
+            if (!mCursor.isNull(Projections.COL_TITLE_RESOURCE)) {
+                final String packageName = mCursor.getString(Projections.COL_PACKAGE);
+                final int titleRes = mCursor.getInt(Projections.COL_TITLE_RESOURCE);
+                try {
+                    title = mExternalRes.getText(packageName, titleRes);
+                } catch (NameNotFoundException e) {
+                    Log.w(TAG, "couldn't load group title resource for " + packageName);
+                }
+            }
+
+            final int descripString = mChildWithPhones ? R.plurals.groupDescripPhones
+                    : R.plurals.groupDescrip;
+
+            text1.setText(title);
+            text2.setText(mResources.getQuantityString(descripString, count, count, withPhones));
+            checkbox.setChecked((membersVisible == 1));
+
+            convertView.setEnabled(mEnabled);
+
+            return convertView;
+        }
+
+        public void changeCursor(Cursor cursor) {
+            if (cursor == mCursor) {
+                return;
+            }
+            if (mCursor != null) {
+                mCursor.unregisterContentObserver(mContentObserver);
+                mCursor.unregisterDataSetObserver(mDataSetObserver);
+                mCursor.close();
+            }
+            mCursor = cursor;
+            if (cursor != null) {
+                cursor.registerContentObserver(mContentObserver);
+                cursor.registerDataSetObserver(mDataSetObserver);
+                mRowIDColumn = cursor.getColumnIndexOrThrow("_id");
+                mDataValid = true;
+                buildInternalMapping();
+                // notify the observers about the new cursor
+                notifyDataSetChanged();
+            } else {
+                mRowIDColumn = -1;
+                mDataValid = false;
+                // notify the observers about the lack of a data set
+                notifyDataSetInvalidated();
+            }
+        }
+
+        protected void onContentChanged() {
+            if (mCursor != null && !mCursor.isClosed()) {
+                mDataValid = mCursor.requery();
+            }
+        }
+
+        private class MyChangeObserver extends ContentObserver {
+            public MyChangeObserver() {
+                super(new Handler());
+            }
+
+            @Override
+            public boolean deliverSelfNotifications() {
+                return true;
+            }
+
+            @Override
+            public void onChange(boolean selfChange) {
+                onContentChanged();
+            }
+        }
+
+        private class MyDataSetObserver extends DataSetObserver {
+            @Override
+            public void onChanged() {
+                mDataValid = true;
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public void onInvalidated() {
+                mDataValid = false;
+                notifyDataSetInvalidated();
+            }
+        }
+
+    }
+
+    /**
+     * Database projections used locally.
+     */
+    private interface Projections {
+
+        public static final String[] PROJ_SUMMARY = new String[] {
+            Groups._ID,
+            Groups.PACKAGE_ID,
+            Groups.PACKAGE,
+            Groups.TITLE,
+            Groups.TITLE_RESOURCE,
+            Groups.GROUP_VISIBLE,
+            Groups.SUMMARY_COUNT,
+            Groups.SUMMARY_WITH_PHONES,
+        };
+
+        public static final String SORT_ORDER = Groups.PACKAGE + " ASC";
+
+        public static final int COL_ID = 0;
+        public static final int COL_PACKAGE_ID = 1;
+        public static final int COL_PACKAGE = 2;
+        public static final int COL_TITLE = 3;
+        public static final int COL_TITLE_RESOURCE = 4;
+        public static final int COL_GROUP_VISIBLE = 5;
+        public static final int COL_SUMMARY_COUNT = 6;
+        public static final int COL_SUMMARY_WITH_PHONES = 7;
+
+    }
+
+}