Iteration on display groups UI, was neglected for awhile.

Switched to using Accounts metadata provided through
Sources cache.  Also added long-press and menu item to serve
as our "edit sync groups" in the same UI.

Need to iterating to show all accounts regardless of group
existance, and persist DEFAULT_SHOULD_SYNC values.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6a005a0..f77a7bb 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -233,7 +233,7 @@
             />
         </activity>
 
-        <activity android:name=".DisplayGroupsActivity" android:label="@string/displayGroups" />
+        <activity android:name=".ui.DisplayGroupsActivity" android:label="@string/displayGroups" />
 
         <activity
             android:name="ShowOrCreateActivity"
diff --git a/res/layout-finger/display_group.xml b/res/layout-finger/display_group.xml
index 48ff7f4..7d36450 100644
--- a/res/layout-finger/display_group.xml
+++ b/res/layout-finger/display_group.xml
@@ -20,34 +20,36 @@
     android:minHeight="?android:attr/listPreferredItemHeight"
     android:gravity="center_vertical"
     android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
-    android:paddingRight="?android:attr/scrollbarSize"
->
+    android:paddingRight="?android:attr/scrollbarSize">
 
-    <TextView
-        android:id="@android:id/text1"
+    <RelativeLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginRight="6dip"
         android:layout_marginTop="6dip"
         android:layout_marginBottom="6dip"
         android:layout_weight="1"
-        android:singleLine="true"
-        android:ellipsize="marquee"
-        android:textAppearance="?android:attr/textAppearanceLarge"
-        android:duplicateParentState="true"
-    />
+        android:duplicateParentState="true">
 
-    <CheckBox
-        android:id="@android:id/checkbox"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginRight="4dip"
-        android:focusable="false"
-        android:clickable="false"
-        android:gravity="center_vertical"
-        android:orientation="vertical"
-        android:visibility="gone"
-        android:duplicateParentState="true"
-    />
+        <TextView
+            android:id="@android:id/text1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+            android:duplicateParentState="true" />
+
+        <TextView
+            android:id="@android:id/text2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/text1"
+            android:layout_alignLeft="@android:id/text1"
+            android:maxLines="2"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:duplicateParentState="true" />
+
+    </RelativeLayout>
 
 </LinearLayout>
diff --git a/res/layout-finger/display_header.xml b/res/layout-finger/display_header.xml
index a55c5d0..421d421 100644
--- a/res/layout-finger/display_header.xml
+++ b/res/layout-finger/display_header.xml
@@ -53,7 +53,7 @@
 
     </RelativeLayout>
 
-    <CheckBox
+    <RadioButton
         android:id="@android:id/checkbox"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -61,7 +61,6 @@
         android:focusable="false"
         android:clickable="false"
         android:gravity="center_vertical"
-        android:orientation="vertical"
-    />
+        android:orientation="vertical" />
 
 </LinearLayout>
diff --git a/res/menu/display_groups.xml b/res/menu/display_groups.xml
new file mode 100644
index 0000000..3d1a6b0
--- /dev/null
+++ b/res/menu/display_groups.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_add"
+        android:icon="@android:drawable/ic_menu_add"
+        android:title="@string/menu_sync_add" />
+
+</menu>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index dbf9b65..48981e0 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -27,4 +27,6 @@
     <item type="id" name="dialog_label" />
     <item type="id" name="dialog_label_custom" />
 
+    <item type="id" name="dialog_sync_add" />
+
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d845e09..2278761 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -864,6 +864,16 @@
     <string name="dialog_primary_name">Primary name</string>
     <string name="dialog_new_contact_account">Create contact under account</string>
 
+    <string name="menu_sync_remove">Remove sync group</string>
+    <string name="menu_sync_add">Add sync group</string>
+
+    <!-- List title for a special contacts group that covers all contacts that
+         aren't members of any other group.  -->
+    <string name="display_ungrouped">(Ungrouped contacts)</string>
+
+    <!-- Warning message given to users just before they remove a currently syncing
+         group that would also cause all ungrouped contacts to stop syncing. -->
+    <string name="display_warn_remove_ungrouped">Removing '%s' from sync will also remove any ungrouped contacts from sync.</string>
 
 
 <string name="call_home">Call home</string>
diff --git a/src/com/android/contacts/ContactsGroupSyncSelector.java b/src/com/android/contacts/ContactsGroupSyncSelector.java
index 869870f..0384516 100644
--- a/src/com/android/contacts/ContactsGroupSyncSelector.java
+++ b/src/com/android/contacts/ContactsGroupSyncSelector.java
@@ -42,6 +42,7 @@
 
 import com.google.android.googlelogin.GoogleLoginServiceConstants;
 
+@Deprecated
 public final class ContactsGroupSyncSelector extends ListActivity implements View.OnClickListener {
 
     private static final String[] PROJECTION = new String[] {
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 22cae96..2eb1ddd 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -16,8 +16,9 @@
 
 package com.android.contacts;
 
-import com.android.contacts.DisplayGroupsActivity.Prefs;
+import com.android.contacts.ui.DisplayGroupsActivity;
 import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
 
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -258,7 +259,7 @@
     private boolean mJustCreated;
     private boolean mSyncEnabled;
 
-    private boolean mDisplayAll;
+//    private boolean mDisplayAll;
     private boolean mDisplayOnlyPhones;
 
     /**
@@ -373,7 +374,6 @@
             buildUserGroupUri(groupName);
         } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
             mMode = MODE_CUSTOM;
-            mDisplayAll = true;
             mDisplayOnlyPhones = false;
         } else if (UI.LIST_STARRED_ACTION.equals(action)) {
             mMode = MODE_STARRED;
@@ -383,12 +383,10 @@
             mMode = MODE_STREQUENT;
         } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
             mMode = MODE_CUSTOM;
-            mDisplayAll = true;
             mDisplayOnlyPhones = true;
         } else if (Intent.ACTION_PICK.equals(action)) {
             // XXX These should be showing the data from the URI given in
             // the Intent.
-            mDisplayAll = true;
             final String type = intent.resolveType(this);
             if (Contacts.CONTENT_TYPE.equals(type)) {
                 mMode = MODE_PICK_AGGREGATE;
@@ -585,8 +583,6 @@
 
         if (mDisplayOnlyPhones) {
             empty.setText(getText(R.string.noContactsWithPhoneNumbers));
-        } else if (mDisplayAll) {
-            empty.setText(getText(R.string.noContacts));
         } else {
             if (mSyncEnabled) {
                 empty.setText(getText(R.string.noContactsHelpTextWithSync));
@@ -609,7 +605,6 @@
         // Load the preferences
         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
 
-        mDisplayAll = prefs.getBoolean(Prefs.DISPLAY_ALL, Prefs.DISPLAY_ALL_DEFAULT);
         mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
                 Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
 
@@ -1248,14 +1243,11 @@
      * {@link #mDisplayAll} and {@link #mDisplayOnlyPhones} flags.
      */
     private String getAggregateSelection() {
-        if (!mDisplayAll && mDisplayOnlyPhones) {
+        if (mDisplayOnlyPhones) {
             return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
-        } else if (!mDisplayAll) {
+        } else {
             return CLAUSE_ONLY_VISIBLE;
-        } else if (mDisplayOnlyPhones) {
-            return CLAUSE_ONLY_PHONES;
         }
-        return null;
     }
 
     private Uri getAggregateFilterUri(String filter) {
diff --git a/src/com/android/contacts/DisplayGroupsActivity.java b/src/com/android/contacts/DisplayGroupsActivity.java
deleted file mode 100644
index 7215966..0000000
--- a/src/com/android/contacts/DisplayGroupsActivity.java
+++ /dev/null
@@ -1,640 +0,0 @@
-/*
- * 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.util.NotifyingAsyncQueryHandler;
-
-import android.app.ExpandableListActivity;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.EntityIterator;
-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
-        NotifyingAsyncQueryHandler.AsyncQueryListener, 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);
-    }
-
-    /** {@inheritDoc} */
-    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);
-        }
-    }
-
-    /** {@inheritDoc} */
-    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
-        // No actions
-    }
-
-    /**
-     * 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_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_RES_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_RES_TITLE)) {
-                final String packageName = mCursor.getString(Projections.COL_RES_PACKAGE);
-                final int titleRes = mCursor.getInt(Projections.COL_RES_TITLE);
-                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.TITLE,
-            Groups.RES_PACKAGE,
-            Groups.TITLE_RES,
-            Groups.GROUP_VISIBLE,
-            Groups.SUMMARY_COUNT,
-            Groups.SUMMARY_WITH_PHONES,
-        };
-
-        public static final String SORT_ORDER = Groups.ACCOUNT_TYPE + " ASC, "
-                + Groups.ACCOUNT_NAME + " ASC";
-
-        public static final int COL_ID = 0;
-        public static final int COL_TITLE = 1;
-        public static final int COL_RES_PACKAGE = 2;
-        public static final int COL_RES_TITLE = 3;
-        public static final int COL_GROUP_VISIBLE = 4;
-        public static final int COL_SUMMARY_COUNT = 5;
-        public static final int COL_SUMMARY_WITH_PHONES = 6;
-
-    }
-}
diff --git a/src/com/android/contacts/EdgeTriggerView.java b/src/com/android/contacts/EdgeTriggerView.java
deleted file mode 100644
index d40dbad..0000000
--- a/src/com/android/contacts/EdgeTriggerView.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * 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 android.content.Context;
-import android.content.res.TypedArray;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-import android.widget.FrameLayout;
-
-/**
- * Lightweight view that wraps around an existing view, watching and capturing
- * sliding gestures from the left or right edges within a given tolerance.
- */
-public class EdgeTriggerView extends FrameLayout {
-    public static final int FLAG_NONE = 0x00;
-    public static final int FLAG_LEFT = 0x01;
-    public static final int FLAG_RIGHT = 0x02;
-
-    private int mTouchSlop;
-
-    private int mEdgeWidth;
-    private int mListenEdges;
-
-    private boolean mListenLeft = false;
-    private boolean mListenRight = false;
-
-    private MotionEvent mDownStart;
-    private int mEdge = FLAG_NONE;
-
-    public static interface EdgeTriggerListener {
-        public void onTrigger(float downX, float downY, int edge);
-    }
-
-    private EdgeTriggerListener mListener;
-
-    /**
-     * Add a {@link EdgeTriggerListener} to watch for edge events.
-     */
-    public void setOnEdgeTriggerListener(EdgeTriggerListener listener) {
-        mListener = listener;
-    }
-
-    public EdgeTriggerView(Context context) {
-        this(context, null);
-    }
-
-    public EdgeTriggerView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public EdgeTriggerView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        setClickable(true);
-
-        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
-
-        // TODO: enable reading these values again once we move away from symlinks
-//        TypedArray a = context.obtainStyledAttributes(attrs,
-//                R.styleable.EdgeTriggerView, defStyle, -1);
-//        mEdgeWidth = a.getDimensionPixelSize(R.styleable.EdgeTriggerView_edgeWidth, mTouchSlop);
-//        mListenEdges = a.getInt(R.styleable.EdgeTriggerView_listenEdges, FLAG_LEFT);
-
-        mEdgeWidth = 80;
-        mListenEdges = FLAG_LEFT;
-
-        mListenLeft = (mListenEdges & FLAG_LEFT) == FLAG_LEFT;
-        mListenRight = (mListenEdges & FLAG_RIGHT) == FLAG_RIGHT;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent event) {
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN: {
-                // Consider watching this touch event based on listening flags
-                final float x = event.getX();
-                if (mListenLeft && x < mEdgeWidth) {
-                    mEdge = FLAG_LEFT;
-                } else if (mListenRight && x > getWidth() - mEdgeWidth) {
-                    mEdge = FLAG_RIGHT;
-                } else {
-                    mEdge = FLAG_NONE;
-                }
-
-                if (mEdge != FLAG_NONE) {
-                    mDownStart = MotionEvent.obtain(event);
-                } else {
-                    mDownStart = null;
-                }
-                break;
-            }
-            case MotionEvent.ACTION_MOVE: {
-                if (mEdge != FLAG_NONE) {
-                    // If moved far enough, capture touch event for ourselves
-                    float delta = event.getX() - mDownStart.getX();
-                    if (mEdge == FLAG_LEFT && delta > mTouchSlop) {
-                        return true;
-                    } else if (mEdge == FLAG_RIGHT && delta < -mTouchSlop) {
-                        return true;
-                    }
-                }
-                break;
-            }
-        }
-
-        // Otherwise let the event slip through to children
-        return false;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        // Pass trigger event to listener and return true to consume
-        if (mEdge != FLAG_NONE && mListener != null) {
-            mListener.onTrigger(mDownStart.getX(), mDownStart.getY(), mEdge);
-
-            // Reset values so we don't sent twice
-            mEdge = FLAG_NONE;
-            mDownStart = null;
-        }
-        return true;
-    }
-}
diff --git a/src/com/android/contacts/SocialStreamActivity.java b/src/com/android/contacts/SocialStreamActivity.java
index a3adf76..11f9367 100644
--- a/src/com/android/contacts/SocialStreamActivity.java
+++ b/src/com/android/contacts/SocialStreamActivity.java
@@ -16,7 +16,6 @@
 
 package com.android.contacts;
 
-import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
 import com.android.contacts.ui.FastTrackWindow;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -71,7 +70,7 @@
 import java.util.HashMap;
 import java.util.List;
 
-public class SocialStreamActivity extends ListActivity implements OnClickListener, EdgeTriggerListener {
+public class SocialStreamActivity extends ListActivity implements OnClickListener {
     private static final String TAG = "SocialStreamActivity";
 
     private static final String[] PROJ_ACTIVITIES = new String[] {
@@ -108,12 +107,9 @@
     private SocialAdapter mAdapter;
 
     private ListView mListView;
-    private EdgeTriggerView mEdgeTrigger;
     private FastTrackWindow mFastTrack;
     private MappingCache mMappingCache;
 
-    private static final boolean USE_GESTURE = false;
-
     private ContactsCache mContactsCache;
 
     @Override
@@ -133,12 +129,6 @@
 
         mListView = getListView();
         mFastTrack = new FastTrackWindow(this);
-
-        if (USE_GESTURE) {
-            // Find and listen for edge triggers
-            mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
-            mEdgeTrigger.setOnEdgeTriggerListener(this);
-        }
     }
 
     /** {@inheritDoc} */
@@ -147,23 +137,6 @@
         showFastTrack(v, (Long)v.getTag());
     }
 
-    /** {@inheritDoc} */
-    public void onTrigger(float downX, float downY, int edge) {
-        // Find list item user triggered over
-        final int position = mListView.pointToPosition((int)downX, (int)downY);
-        if (position == ListView.INVALID_POSITION) return;
-
-        // Reverse to find the exact top of the triggered entry
-        final int index = position - mListView.getFirstVisiblePosition();
-        final View anchor = mListView.getChildAt(index);
-
-        Cursor cursor = (Cursor)mAdapter.getItem(position);
-        long aggId = cursor.getLong(COL_AGGREGATE_ID);
-
-        showFastTrack(anchor, aggId);
-
-    }
-
     private int[] mLocation = new int[2];
     private Rect mRect = new Rect();
 
@@ -335,9 +308,7 @@
             holder.published = (TextView) view.findViewById(R.id.published);
             view.setTag(holder);
 
-            if (!USE_GESTURE) {
-                holder.photo.setOnClickListener(mPhotoListener);
-            }
+            holder.photo.setOnClickListener(mPhotoListener);
 
             return view;
         }
@@ -409,7 +380,10 @@
      * Store a mapping from a package name and mime-type pair to a set of
      * {@link RemoteViews}, default icon, and column to use from the
      * {@link Data} table to use as a summary.
+     * 
+     * @deprecated use {@link ContactsSource} instead
      */
+    @Deprecated
     public static class Mapping {
         String packageName;
         String mimeType;
@@ -431,7 +405,10 @@
      * Store a parsed <code>Mapping</code> object, which maps package and
      * mime-type combinations to {@link RemoteViews} XML resources, default
      * icons, and summary columns in the {@link Data} table.
+     * 
+     * @deprecated use {@link Sources} instead
      */
+    @Deprecated
     public static class MappingCache extends HashMap<String, Mapping> {
         private static final String TAG = "MappingCache";
 
@@ -603,12 +580,14 @@
          * The size of the thumbnail is defined by the dimension
          * android.R.dimen.launcher_application_icon_size. This method is not
          * thread-safe and should be invoked on the UI thread only.
-         *
+         * 
          * @param bitmap The bitmap to get a thumbnail of.
          * @param context The application's context.
          * @return A thumbnail for the specified bitmap or the bitmap itself if
          *         the thumbnail could not be created.
+         * @deprecated use {@link Bitmap#createScaledBitmap} instead.
          */
+        @Deprecated
         static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
             int width = size;
             int height = size;
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index 8611bb3..b9bd4ae 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -158,6 +158,15 @@
         throw new UnsupportedOperationException("Custom constraint parser not implemented");
     }
 
+    public CharSequence getDisplayLabel(Context context) {
+        if (this.titleRes > 0) {
+            final PackageManager pm = context.getPackageManager();
+            return pm.getText(this.resPackageName, this.titleRes, null);
+        } else {
+            return this.accountType;
+        }
+    }
+
     /**
      * {@link Comparator} to sort by {@link DataKind#weight}.
      */
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index 196a306..e19d1b6 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -122,25 +122,27 @@
         }
         throw new IllegalStateException("Couldn't find authenticator for specific account type");
     }
-
+    
     /**
      * Return list of all known, writable {@link ContactsSource}. Sources
      * returned may require inflation before they can be used.
      */
-    public ArrayList<Account> getWritableAccounts() {
+    public ArrayList<Account> getAccounts(boolean writableOnly) {
         final AccountManager am = AccountManager.get(mContext);
         final Account[] accounts = am.getAccounts();
-        final ArrayList<Account> writable = new ArrayList<Account>();
+        final ArrayList<Account> matching = new ArrayList<Account>();
 
         for (Account account : accounts) {
             // Ensure we have details loaded for each account
             final ContactsSource source = getInflatedSource(account.type,
                     ContactsSource.LEVEL_SUMMARY);
-            if (!source.readOnly) {
-                writable.add(account);
+            final boolean hasContacts = source != null;
+            final boolean matchesWritable = (!writableOnly || (writableOnly && !source.readOnly));
+            if (hasContacts && matchesWritable) {
+                matching.add(account);
             }
         }
-        return writable;
+        return matching;
     }
 
     protected ContactsSource getSourceForType(String accountType) {
@@ -157,7 +159,7 @@
      */
     public ContactsSource getInflatedSource(String accountType, int inflateLevel) {
         final ContactsSource source = getSourceForType(accountType);
-        if (source.isInflated(inflateLevel)) {
+        if (source == null || source.isInflated(inflateLevel)) {
             // Found inflated, so return directly
             return source;
         } else {
diff --git a/src/com/android/contacts/ui/DisplayGroupsActivity.java b/src/com/android/contacts/ui/DisplayGroupsActivity.java
new file mode 100644
index 0000000..b202127
--- /dev/null
+++ b/src/com/android/contacts/ui/DisplayGroupsActivity.java
@@ -0,0 +1,702 @@
+/*
+ * 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.ui;
+
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.util.WeakAsyncTask;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ExpandableListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Settings;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.CursorTreeAdapter;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.RadioButton;
+import android.widget.TextView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/**
+ * 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
+        AdapterView.OnItemClickListener {
+    private static final String TAG = "DisplayGroupsActivity";
+
+    private static final int UNGROUPED_ID = -2;
+
+    public interface Prefs {
+        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 RadioButton mDisplayAll;
+    private RadioButton mDisplayPhones;
+
+    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);
+
+        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);
+        mDisplayAll = (RadioButton)mHeaderAll.findViewById(android.R.id.checkbox);
+        {
+            final TextView text1 = (TextView)mHeaderAll.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)mHeaderAll.findViewById(android.R.id.text2);
+            text1.setText(R.string.showAllGroups);
+            text2.setVisibility(View.GONE);
+        }
+        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);
+        mDisplayPhones = (RadioButton)mHeaderPhones.findViewById(android.R.id.checkbox);
+        {
+            final TextView text1 = (TextView)mHeaderPhones.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)mHeaderPhones.findViewById(android.R.id.text2);
+            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);
+        {
+            final TextView text1 = (TextView)mHeaderSeparator;
+            text1.setText(R.string.headerContactGroups);
+        }
+        mList.addHeaderView(mHeaderSeparator, null, false);
+
+        mAdapter = new DisplayGroupsAdapter(null, this, this);
+
+        boolean displayOnlyPhones = mPrefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
+                Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
+
+        mDisplayAll.setChecked(!displayOnlyPhones);
+        mDisplayPhones.setChecked(displayOnlyPhones);
+
+        mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+
+        setListAdapter(mAdapter);
+
+        // Catch clicks on the header views
+        mList.setOnItemClickListener(this);
+        mList.setOnCreateContextMenuListener(this);
+
+        // Start background query to find account details
+        new QuerySettingsTask(this).execute();
+    }
+
+    private static class QuerySettingsTask extends
+            WeakAsyncTask<Void, Void, Cursor, DisplayGroupsActivity> {
+        public QuerySettingsTask(DisplayGroupsActivity target) {
+            super(target);
+        }
+
+        @Override
+        protected Cursor doInBackground(DisplayGroupsActivity target, Void... params) {
+            final Context context = target;
+            final Sources sources = Sources.getInstance(context);
+
+            // Query to find Settings for all data sources
+            final ContentResolver resolver = context.getContentResolver();
+            final Cursor cursor = resolver.query(ContactsContract.Settings.CONTENT_URI,
+                    SettingsQuery.PROJECTION, null, null, null);
+            target.startManagingCursor(cursor);
+
+            // Make records for each account known by Settings
+            final HashSet<Account> knownAccounts = new HashSet<Account>();
+            while (cursor.moveToNext()) {
+                final String accountName = cursor.getString(SettingsQuery.ACCOUNT_NAME);
+                final String accountType = cursor.getString(SettingsQuery.ACCOUNT_TYPE);
+                final Account account = new Account(accountName, accountType);
+                knownAccounts.add(account);
+            }
+
+            // Assert that Settings exist for each data source
+            boolean changedSettings = false;
+            final ArrayList<Account> expectedAccounts = sources.getAccounts(false);
+            for (Account account : expectedAccounts) {
+                if (!knownAccounts.contains(account)) {
+                    // Expected account that doesn't exist yet in Settings
+                    final ContentValues values = new ContentValues();
+                    values.put(Settings.ACCOUNT_NAME, account.name);
+                    values.put(Settings.ACCOUNT_TYPE, account.type);
+                    resolver.insert(Settings.CONTENT_URI, values);
+
+                    // Make sure we requery to catch this insert
+                    changedSettings = true;
+                }
+            }
+
+            if (changedSettings) {
+                // Catch any new sources discovered above
+                cursor.requery();
+            }
+
+            // Wrap cursor to provide _id column
+            final Cursor settingsCursor = new CursorWrapper(cursor) {
+                @Override
+                public long getLong(int columnIndex) {
+                    if (columnIndex == -1) {
+                        return this.getPosition();
+                    } else {
+                        return super.getLong(columnIndex);
+                    }
+                }
+            };
+
+            return settingsCursor;
+        }
+
+        @Override
+        protected void onPostExecute(DisplayGroupsActivity target, Cursor result) {
+            // Update cursor for data sources
+            target.mAdapter.setGroupCursor(result);
+        }
+    }
+
+    /**
+     * 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) {
+        switch (view.getId()) {
+            case R.id.header_all: {
+                mDisplayAll.toggle();
+                setDisplayOnlyPhones(!mDisplayAll.isChecked());
+                break;
+            }
+            case R.id.header_phones: {
+                mDisplayPhones.toggle();
+                setDisplayOnlyPhones(mDisplayPhones.isChecked());
+                break;
+            }
+        }
+    }
+
+    /**
+     * Assign a specific value to {@link Prefs#DISPLAY_ONLY_PHONES}, refreshing
+     * the visible list as needed.
+     */
+    protected void setDisplayOnlyPhones(boolean displayOnlyPhones) {
+        mDisplayAll.setChecked(!displayOnlyPhones);
+        mDisplayPhones.setChecked(displayOnlyPhones);
+
+        Editor editor = mPrefs.edit();
+        editor.putBoolean(Prefs.DISPLAY_ONLY_PHONES, displayOnlyPhones);
+        editor.commit();
+
+        mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * 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) {
+        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();
+
+        // TODO: heavy update, perhaps push to background query
+        if (id != UNGROUPED_ID) {
+            // Handle persisting for normal group
+            values.put(Groups.GROUP_VISIBLE, checkbox.isChecked() ? 1 : 0);
+
+            final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, id);
+            final int count = resolver.update(groupUri, values, null, null);
+        } else {
+            // Handle persisting for ungrouped through Settings
+            values.put(Settings.UNGROUPED_VISIBLE, checkbox.isChecked() ? 1 : 0);
+
+            final Cursor settings = mAdapter.getGroup(groupPosition);
+            final int count = resolver.update(Settings.CONTENT_URI, values, Groups.ACCOUNT_NAME
+                    + "=? AND " + Groups.ACCOUNT_TYPE + "=?", new String[] {
+                    settings.getString(SettingsQuery.ACCOUNT_NAME),
+                    settings.getString(SettingsQuery.ACCOUNT_TYPE)
+            });
+        }
+
+        return true;
+    }
+
+    // TODO: move these definitions to framework constants when we begin
+    // defining this mode through <sync-adapter> tags
+    private static final int SYNC_MODE_UNSUPPORTED = 0;
+    private static final int SYNC_MODE_UNGROUPED = 1;
+    private static final int SYNC_MODE_EVERYTHING = 2;
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+        super.onCreateContextMenu(menu, v, menuInfo);
+
+        // Bail if not working with expandable long-press, or if not child
+        if (!(menuInfo instanceof ExpandableListContextMenuInfo)) return;
+
+        final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
+        final int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+        final int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
+
+        final Cursor groupCursor = mAdapter.getGroup(groupPosition);
+        // TODO: read sync mode through <sync-adapter> definition
+        final int syncMode = SYNC_MODE_EVERYTHING;
+
+        // Ignore when selective syncing unsupported
+        if (syncMode == SYNC_MODE_UNSUPPORTED) return;
+
+        final String accountName = groupCursor.getString(SettingsQuery.ACCOUNT_NAME);
+        final String accountType = groupCursor.getString(SettingsQuery.ACCOUNT_TYPE);
+        final Account account = new Account(accountName, accountType);
+
+        if (childPosition == -1) {
+            // Show add dialog for this overall source
+            showAddSync(menu, groupCursor, account, syncMode);
+
+        } else {
+            // Show remove dialog for this specific group
+            final Cursor childCursor = mAdapter.getChild(groupPosition, childPosition);
+            showRemoveSync(menu, account, childCursor, syncMode);
+        }
+    }
+
+    protected void showRemoveSync(ContextMenu menu, final Account account, Cursor childCursor,
+            final int syncMode) {
+        final long groupId = childCursor.getLong(GroupsQuery._ID);
+        final CharSequence title = getGroupTitle(this, childCursor);
+
+        menu.setHeaderTitle(title);
+        menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener(
+                new OnMenuItemClickListener() {
+                    public boolean onMenuItemClick(MenuItem item) {
+                        handleRemoveSync(groupId, account, syncMode, title);
+                        return true;
+                    }
+                });
+    }
+
+    protected void handleRemoveSync(final long groupId, final Account account, final int syncMode,
+            CharSequence title) {
+        if (syncMode == SYNC_MODE_EVERYTHING && groupId != UNGROUPED_ID) {
+            // Warn before removing this group when it would cause ungrouped to stop syncing
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+            final CharSequence removeMessage = this.getString(
+                    R.string.display_warn_remove_ungrouped, title);
+            builder.setMessage(removeMessage);
+            builder.setNegativeButton(android.R.string.cancel, null);
+            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    // Mark this group to not sync
+                    setGroupShouldSync(groupId, account, syncMode, false);
+                }
+            });
+            builder.show();
+        } else {
+            // Mark this group to not sync
+            setGroupShouldSync(groupId, account, syncMode, false);
+        }
+    }
+
+    protected void showAddSync(ContextMenu menu, Cursor groupCursor, final Account account, final int syncMode) {
+        menu.setHeaderTitle(R.string.menu_sync_add);
+
+        // Create single "Ungrouped" item when not synced
+        final boolean ungroupedAvailable = groupCursor.getInt(SettingsQuery.SHOULD_SYNC) == 0;
+        if (ungroupedAvailable) {
+            menu.add(R.string.display_ungrouped).setOnMenuItemClickListener(
+                    new OnMenuItemClickListener() {
+                        public boolean onMenuItemClick(MenuItem item) {
+                            // Adding specific group for syncing
+                            setGroupShouldSync(UNGROUPED_ID, account, syncMode, true);
+                            return true;
+                        }
+                    });
+        }
+
+        // Create item for each available, unsynced group
+        final Cursor availableGroups = this.managedQuery(Groups.CONTENT_SUMMARY_URI,
+                GroupsQuery.PROJECTION, Groups.SHOULD_SYNC + "=0", null);
+        while (availableGroups.moveToNext()) {
+            // Create item this unsynced group
+            final long groupId = availableGroups.getLong(GroupsQuery._ID);
+            final CharSequence title = getGroupTitle(this, availableGroups);
+            menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+                public boolean onMenuItemClick(MenuItem item) {
+                    // Adding specific group for syncing
+                    setGroupShouldSync(groupId, account, syncMode, true);
+                    return true;
+                }
+            });
+        }
+    }
+
+    /**
+     * Mark the {@link Groups#SHOULD_SYNC} state of the given group.
+     */
+    protected void setGroupShouldSync(long groupId, Account account, int syncMode, boolean shouldSync) {
+        final ContentResolver resolver = getContentResolver();
+        final ContentValues values = new ContentValues();
+
+        if (syncMode == SYNC_MODE_UNSUPPORTED) {
+            // Ignore changes when source doesn't support syncing
+            return;
+        }
+
+        if (groupId == UNGROUPED_ID) {
+            // Updating the overall syncing flag for this account
+            values.put(Settings.SHOULD_SYNC, shouldSync ? 1 : 0);
+            resolver.update(Settings.CONTENT_URI, values, Settings.ACCOUNT_NAME + "=? AND "
+                    + Settings.ACCOUNT_TYPE + "=?", new String[] {
+                    account.name, account.type
+            });
+
+            if (syncMode == SYNC_MODE_EVERYTHING && shouldSync) {
+                // If syncing mode is everything, force-enable all children groups
+                values.clear();
+                values.put(Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
+                resolver.update(Groups.CONTENT_URI, values, Groups.ACCOUNT_NAME + "=? AND "
+                        + Groups.ACCOUNT_TYPE + "=?", new String[] {
+                        account.name, account.type
+                });
+            }
+        } else {
+            // Treat as normal group
+            values.put(Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
+            resolver.update(Groups.CONTENT_URI, values, Groups._ID + "=" + groupId, null);
+
+            if (syncMode == SYNC_MODE_EVERYTHING && !shouldSync) {
+                // Remove "everything" from sync, user has already been warned
+                values.clear();
+                values.put(Settings.SHOULD_SYNC, shouldSync ? 1 : 0);
+                resolver.update(Settings.CONTENT_URI, values, Settings.ACCOUNT_NAME + "=? AND "
+                        + Settings.ACCOUNT_TYPE + "=?", new String[] {
+                        account.name, account.type
+                });
+            }
+        }
+    }
+
+    /**
+     * Return the best title for the {@link Groups} entry at the current
+     * {@link Cursor} position.
+     */
+    protected static CharSequence getGroupTitle(Context context, Cursor cursor) {
+        final PackageManager pm = context.getPackageManager();
+        if (!cursor.isNull(GroupsQuery.TITLE_RES)) {
+            final String packageName = cursor.getString(GroupsQuery.RES_PACKAGE);
+            final int titleRes = cursor.getInt(GroupsQuery.TITLE_RES);
+            return pm.getText(packageName, titleRes, null);
+        } else {
+            return cursor.getString(GroupsQuery.TITLE);
+        }
+    }
+
+    /**
+     * Special {@link Cursor} that shows zero or one items based on
+     * {@link Settings#SHOULD_SYNC} value. This header only supports
+     * {@link #SYNC_MODE_UNGROUPED} and {@link #SYNC_MODE_UNSUPPORTED}.
+     */
+    private static class HeaderCursor extends AbstractCursor {
+        private Context mContext;
+        private Cursor mCursor;
+        private int mPosition;
+
+        public HeaderCursor(Context context, Cursor cursor, int position) {
+            mContext = context;
+            mCursor = cursor;
+            mPosition = position;
+        }
+
+        @Override
+        public int getCount() {
+            assertParent();
+
+            final boolean shouldSync = mCursor.getInt(SettingsQuery.SHOULD_SYNC) != 0;
+            return shouldSync ? 1 : 0;
+        }
+
+        @Override
+        public String[] getColumnNames() {
+            return GroupsQuery.PROJECTION;
+        }
+
+        protected void assertParent() {
+            mCursor.moveToPosition(mPosition);
+        }
+
+        @Override
+        public String getString(int column) {
+            assertParent();
+            switch(column) {
+                case GroupsQuery.ACCOUNT_NAME:
+                    return mCursor.getString(SettingsQuery.ACCOUNT_NAME);
+                case GroupsQuery.ACCOUNT_TYPE:
+                    return mCursor.getString(SettingsQuery.ACCOUNT_TYPE);
+                case GroupsQuery.TITLE:
+                    return null;
+                case GroupsQuery.RES_PACKAGE:
+                    return mContext.getPackageName();
+                case GroupsQuery.TITLE_RES:
+                    return Integer.toString(UNGROUPED_ID);
+            }
+            throw new IllegalArgumentException("Requested column not available as string");
+        }
+
+        @Override
+        public short getShort(int column) {
+            throw new IllegalArgumentException("Requested column not available as short");
+        }
+
+        @Override
+        public int getInt(int column) {
+            assertParent();
+            switch(column) {
+                case GroupsQuery._ID:
+                    return UNGROUPED_ID;
+                case GroupsQuery.TITLE_RES:
+                    return R.string.display_ungrouped;
+                case GroupsQuery.GROUP_VISIBLE:
+                    return mCursor.getInt(SettingsQuery.UNGROUPED_VISIBLE);
+                case GroupsQuery.SUMMARY_COUNT:
+                    return mCursor.getInt(SettingsQuery.UNGROUPED_COUNT);
+                case GroupsQuery.SUMMARY_WITH_PHONES:
+                    return mCursor.getInt(SettingsQuery.UNGROUPED_WITH_PHONES);
+            }
+            throw new IllegalArgumentException("Requested column not available as int");
+        }
+
+        @Override
+        public long getLong(int column) {
+            return getInt(column);
+        }
+
+        @Override
+        public float getFloat(int column) {
+            throw new IllegalArgumentException("Requested column not available as float");
+        }
+
+        @Override
+        public double getDouble(int column) {
+            throw new IllegalArgumentException("Requested column not available as double");
+        }
+
+        @Override
+        public boolean isNull(int column) {
+            return getString(column) == null;
+        }
+    }
+
+    /**
+     * 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 Account}.
+     */
+    private static class DisplayGroupsAdapter extends CursorTreeAdapter {
+        private Context mContext;
+        private Activity mActivity;
+        private LayoutInflater mInflater;
+        private Sources mSources;
+
+        private boolean mChildWithPhones = false;
+
+        public DisplayGroupsAdapter(Cursor cursor, Context context, Activity activity) {
+            super(cursor, context, true);
+
+            mContext = context;
+            mActivity = activity;
+            mSources = Sources.getInstance(mContext);
+            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        }
+
+        /**
+         * In group descriptions, show the number of contacts with phone
+         * numbers, in addition to the total contacts.
+         */
+        public void setChildDescripWithPhones(boolean withPhones) {
+            mChildWithPhones = withPhones;
+        }
+
+        @Override
+        protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
+                ViewGroup parent) {
+            return mInflater.inflate(R.layout.display_group, parent, false);
+        }
+
+        @Override
+        protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
+            final TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+
+            final String accountName = cursor.getString(SettingsQuery.ACCOUNT_NAME);
+            final String accountType = cursor.getString(SettingsQuery.ACCOUNT_TYPE);
+
+            final ContactsSource source = mSources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
+
+            text1.setText(source.getDisplayLabel(mContext));
+            text2.setText(accountName);
+            text2.setVisibility(accountName == null ? View.GONE : View.VISIBLE);
+        }
+
+        @Override
+        protected Cursor getChildrenCursor(Cursor groupCursor) {
+            final String selection = Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE
+                    + "=? AND " + Groups.SHOULD_SYNC + "=1";
+            final String[] selectionArgs = new String[] {
+                    groupCursor.getString(SettingsQuery.ACCOUNT_NAME),
+                    groupCursor.getString(SettingsQuery.ACCOUNT_TYPE)
+            };
+
+            final int position = groupCursor.getPosition();
+            final Cursor ungroupedCursor = new HeaderCursor(mContext, groupCursor, position);
+
+            final ContentResolver resolver = mContext.getContentResolver();
+            final Cursor groupsCursor = resolver.query(Groups.CONTENT_SUMMARY_URI,
+                    GroupsQuery.PROJECTION, selection, selectionArgs, null);
+            mActivity.startManagingCursor(groupsCursor);
+
+            return new MergeCursor(new Cursor[] { ungroupedCursor, groupsCursor });
+        }
+
+        @Override
+        protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
+                ViewGroup parent) {
+            return mInflater.inflate(R.layout.display_child, parent, false);
+        }
+
+        @Override
+        protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
+            final TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+            final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
+
+            final int count = cursor.getInt(GroupsQuery.SUMMARY_COUNT);
+            final int withPhones = cursor.getInt(GroupsQuery.SUMMARY_WITH_PHONES);
+            final int membersVisible = cursor.getInt(GroupsQuery.GROUP_VISIBLE);
+
+            // Read title, but override with string resource when present
+            final CharSequence title = getGroupTitle(mContext, cursor);
+            final CharSequence descrip = mContext.getResources().getQuantityString(
+                    mChildWithPhones ? R.plurals.groupDescripPhones : R.plurals.groupDescrip,
+                    count, count, withPhones);
+
+            text1.setText(title);
+            text2.setText(descrip);
+            checkbox.setChecked((membersVisible == 1));
+        }
+    }
+
+    private interface SettingsQuery {
+        final String[] PROJECTION = new String[] {
+                Settings.ACCOUNT_NAME,
+                Settings.ACCOUNT_TYPE,
+                Settings.SHOULD_SYNC,
+                Settings.UNGROUPED_VISIBLE,
+                Settings.UNGROUPED_COUNT,
+                Settings.UNGROUPED_WITH_PHONES,
+        };
+
+        final int ACCOUNT_NAME = 0;
+        final int ACCOUNT_TYPE = 1;
+        final int SHOULD_SYNC = 2;
+        final int UNGROUPED_VISIBLE = 3;
+        final int UNGROUPED_COUNT = 4;
+        final int UNGROUPED_WITH_PHONES = 5;
+    }
+
+    private interface GroupsQuery {
+        final String[] PROJECTION = new String[] {
+            Groups._ID,
+            Groups.TITLE,
+            Groups.RES_PACKAGE,
+            Groups.TITLE_RES,
+            Groups.GROUP_VISIBLE,
+            Groups.SUMMARY_COUNT,
+            Groups.SUMMARY_WITH_PHONES,
+            Groups.ACCOUNT_NAME,
+            Groups.ACCOUNT_TYPE,
+        };
+
+        final int _ID = 0;
+        final int TITLE = 1;
+        final int RES_PACKAGE = 2;
+        final int TITLE_RES = 3;
+        final int GROUP_VISIBLE = 4;
+        final int SUMMARY_COUNT = 5;
+        final int SUMMARY_WITH_PHONES = 6;
+        final int ACCOUNT_NAME = 7;
+        final int ACCOUNT_TYPE = 8;
+    }
+}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index a5b1a47..4fa6f01 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -48,6 +48,7 @@
 import android.content.EntityIterator;
 import android.content.Intent;
 import android.content.OperationApplicationException;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -659,7 +660,7 @@
             final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 
-            final ArrayList<Account> writable = sources.getWritableAccounts();
+            final ArrayList<Account> writable = sources.getAccounts(true);
             final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(target,
                     android.R.layout.simple_list_item_2, writable) {
                 @Override
@@ -676,9 +677,8 @@
                     final Account account = this.getItem(position);
                     final ContactsSource source = sources.getInflatedSource(account.type,
                             ContactsSource.LEVEL_SUMMARY);
-                    if (source.titleRes > 0) {
-                        text1.setText(source.titleRes);
-                    }
+
+                    text1.setText(source.getDisplayLabel(target));
                     text2.setText(account.name);
 
                     return convertView;