Merge "Pass data to ContactsPreferenceActivity"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c2c4eab..bbc1713 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,8 +16,8 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.contacts"
-    android:versionCode="10500"
-    android:versionName="1.5.0">
+    android:versionCode="10501"
+    android:versionName="1.5.1">
 
     <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="23" />
     <original-package android:name="com.android.contacts" />
@@ -245,7 +245,7 @@
 
         <!-- Displays the members of a group in a list -->
         <activity android:name=".activities.GroupMembersActivity"
-            android:theme="@style/ContactPickerTheme"/>
+            android:theme="@style/PeopleActivityTheme"/>
 
         <!-- Views the details of a single group -->
         <activity android:name=".activities.GroupDetailActivity"
diff --git a/res/layout-v23/edit_spinner.xml b/res/layout-v23/edit_spinner.xml
index 0c20ab9..37015f7 100644
--- a/res/layout-v23/edit_spinner.xml
+++ b/res/layout-v23/edit_spinner.xml
@@ -21,8 +21,7 @@
     android:id="@+id/spinner"
     android:layout_gravity="bottom|start"
     style="@android:style/Widget.Material.Spinner.Underlined"
-    android:dropDownWidth="@dimen/editor_type_label_dropdown_width"
-    android:layout_width="@dimen/editor_type_label_width"
+    android:layout_width="wrap_content"
     android:layout_height="@dimen/editor_min_line_item_height"
     android:paddingBottom="0dp"
     android:paddingTop="0dp"
diff --git a/res/layout/edit_spinner.xml b/res/layout/edit_spinner.xml
index 9e6b465..b1c879c 100644
--- a/res/layout/edit_spinner.xml
+++ b/res/layout/edit_spinner.xml
@@ -21,8 +21,7 @@
     android:id="@+id/spinner"
     android:layout_gravity="bottom|start"
     style="@android:style/Widget.Material.Spinner.Underlined"
-    android:dropDownWidth="@dimen/editor_type_label_dropdown_width"
-    android:layout_width="@dimen/editor_type_label_width"
+    android:layout_width="wrap_content"
     android:layout_height="@dimen/editor_min_line_item_height"
     android:paddingBottom="0dp"
     android:paddingTop="0dp"
diff --git a/res/layout/group_editor_autocomplete_view.xml b/res/layout/group_editor_autocomplete_view.xml
deleted file mode 100644
index c8e716a..0000000
--- a/res/layout/group_editor_autocomplete_view.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!-- Autocomplete text input field for adding new members to a group in the group editor -->
-
-<AutoCompleteTextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:textAppearance="?android:attr/textAppearanceMedium"
-    android:textColor="?android:attr/textColorPrimary"
-    android:imeOptions="flagNoExtractUi|flagNoFullscreen"
-    android:hint="@string/enter_contact_name"
-    android:minHeight="48dip"
-    android:paddingLeft="@dimen/group_editor_autocomplete_left_padding"
-    android:paddingStart="@dimen/group_editor_autocomplete_left_padding"/>
\ No newline at end of file
diff --git a/res/layout/group_editor_view.xml b/res/layout/group_editor_view.xml
index d94853d..72ceaf3 100644
--- a/res/layout/group_editor_view.xml
+++ b/res/layout/group_editor_view.xml
@@ -45,10 +45,6 @@
             android:paddingStart="8dip"/>
 
         <include
-            layout="@layout/group_editor_autocomplete_view"
-            android:id="@+id/add_member_field"/>
-
-        <include
             layout="@layout/group_editor_existing_member_list"
             android:id="@android:id/list"/>
     </LinearLayout>
diff --git a/res/layout/group_members_activity.xml b/res/layout/group_members_activity.xml
index e8cc594..eecec50 100644
--- a/res/layout/group_members_activity.xml
+++ b/res/layout/group_members_activity.xml
@@ -19,4 +19,16 @@
     android:id="@+id/fragment_container"
     android:orientation="vertical"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"/>
\ No newline at end of file
+    android:layout_height="match_parent">
+
+    <include
+        layout="@layout/people_activity_toolbar"
+        android:id="@+id/toolbar_parent" />
+
+    <com.android.contacts.widget.NoSwipeViewPager
+        android:id="@+id/view_pager"
+        android:layout_width="match_parent"
+        android:layout_height="0px"
+        android:layout_weight="1"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/menu/edit_group.xml b/res/menu/edit_group.xml
index b94563f..c2d7f47 100644
--- a/res/menu/edit_group.xml
+++ b/res/menu/edit_group.xml
@@ -14,15 +14,22 @@
      limitations under the License.
 -->
 
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:contacts="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/menu_add"
+        android:icon="@drawable/ic_person_add_tinted_24dp"
+        android:title="@string/menu_addToGroup"
+        contacts:showAsAction="ifRoom" />
+
     <item
         android:id="@+id/menu_save"
-        android:showAsAction="always"
         android:icon="@drawable/ic_done_wht_24dp"
-        android:title="@string/menu_save" />
+        android:title="@string/menu_save"
+        contacts:showAsAction="ifRoom" />
+
     <item
         android:id="@+id/menu_discard"
-        android:alphabeticShortcut="q"
-        android:title="@string/menu_discard"
-        android:showAsAction="withText" />
+        android:title="@string/menu_discard" />
 </menu>
diff --git a/res/menu/view_group.xml b/res/menu/view_group.xml
index 669f401..6c5979e 100644
--- a/res/menu/view_group.xml
+++ b/res/menu/view_group.xml
@@ -14,13 +14,20 @@
      limitations under the License.
 -->
 
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:contacts="http://schemas.android.com/apk/res-auto">
+
     <item
         android:id="@+id/menu_edit_group"
+        android:icon="@drawable/ic_create_24dp"
         android:title="@string/menu_editGroup"
-        android:alphabeticShortcut="e" />
+        contacts:showAsAction="ifRoom" />
 
     <item
         android:id="@+id/menu_delete_group"
         android:title="@string/menu_deleteGroup" />
+
+    <item
+        android:id="@+id/menu_remove_from_group"
+        android:title="@string/menu_removeFromGroup" />
 </menu>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index 78bec34..b519318 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -52,8 +52,6 @@
     <dimen name="group_editor_member_list_right_margin">4dip</dimen>
     <!-- Account title left padding -->
     <dimen name="account_container_left_padding">16dip</dimen>
-    <!-- Left padding of the auto complete field to line hint text up with member list -->
-    <dimen name="group_editor_autocomplete_left_padding">16dip</dimen>
     <dimen name="contact_detail_list_top_padding">8dip</dimen>
     <dimen name="frequently_contacted_title_text_size">24sp</dimen>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f60670c..6d516cf 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -102,9 +102,6 @@
     <!-- Width of the Type-Label in the Editor -->
     <dimen name="editor_type_label_width">150dip</dimen>
 
-    <!-- Width of the drop down that appears when you click on the Type-Label spinner in the editor -->
-    <dimen name="editor_type_label_dropdown_width">150dp</dimen>
-
     <!-- Left padding of the label in the add field button for the contact editor -->
     <dimen name="editor_add_field_label_left_padding">16dip</dimen>
 
@@ -157,9 +154,6 @@
     <!-- Right margin for the group member list to match the built in margin in the autocomplete asset -->
     <dimen name="group_editor_member_list_right_margin">4dip</dimen>
 
-    <!-- Left padding of the auto complete field to line hint text up with member list -->
-    <dimen name="group_editor_autocomplete_left_padding">8dip</dimen>
-
     <!-- Border padding for the group detail fragment -->
     <dimen name="group_detail_border_padding">0dip</dimen>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 37e276f..22169e4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -135,10 +135,16 @@
     <string name="menu_editGroup">Edit</string>
 
     <!-- Menu item that deletes the currently selected label [CHAR LIMIT=30] -->
-    <string name="menu_deleteGroup">Delete</string>
+    <string name="menu_deleteGroup">Delete label</string>
+
+    <!-- Menu item to search for contacts to add to the currently selected label. CHAR LIMIT=30] -->
+    <string name="menu_addToGroup">Add contact</string>
+
+    <!-- Menu item to remove the currently selected contacts from the currently selected label. [CHAR LIMIT=60] -->
+    <string name="menu_removeFromGroup">Remove from label</string>
 
     <!-- Menu item (in the action bar) that creates a new contact [CHAR LIMIT=30] -->
-    <string name="menu_new_contact_action_bar">Add Contact</string>
+    <string name="menu_new_contact_action_bar">Add contact</string>
 
     <!-- Menu item (in the action bar) that creates a new label [CHAR LIMIT=30] -->
     <string name="menu_new_group_action_bar">Create new&#8230;</string>
@@ -284,6 +290,12 @@
     <!-- The text displayed when the labels list is empty while displaying all labels [CHAR LIMIT=30] -->
     <string name="noGroups">No labels.</string>
 
+    <!-- The text displayed when the groups list is empty and no accounts are set on the device while displaying all groups [CHAR LIMIT=NONE] -->
+    <string name="noAccounts">To create groups you need an account.</string>
+
+    <!-- The text displayed to instruct users to add members to a group (when viewing a group detail page for a group with no members) [CHAR LIMIT=50] -->
+    <string name="addPeopleToGroup">To add some, edit the group.</string>
+
     <!-- The text displayed when there are no members that have this label while displaying the label detail page [CHAR LIMIT=70] -->
     <string name="emptyGroup">No people with this label.</string>
 
@@ -433,6 +445,9 @@
     <!-- String describing which account a contact came from when editing it -->
     <string name="from_account_format"><xliff:g id="source" example="user@gmail.com">%1$s</xliff:g></string>
 
+    <!-- Text used to explain that a group cannot be edited since the data is read only [CHAR LIMIT=40] -->
+    <string name="group_read_only">Not editable on this device.</string>
+
     <!-- An option in the 'Contact photo' dialog, if there is no photo yet [CHAR LIMIT=50] -->
     <string name="take_photo">Take photo</string>
 
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 5c9c899..08eb1c7 100755
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -45,6 +45,7 @@
 import android.provider.ContactsContract.Profile;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.RawContactsEntity;
+import android.support.v4.os.ResultReceiver;
 import android.util.Log;
 import android.widget.Toast;
 
@@ -59,7 +60,6 @@
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.contacts.compat.PinnedPositionsCompat;
-import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor.SaveMode;
 import com.android.contacts.util.ContactPhotoUtils;
 
 import com.google.common.collect.Lists;
@@ -86,6 +86,8 @@
     public static final String EXTRA_DATA_SET = "dataSet";
     public static final String EXTRA_CONTENT_VALUES = "contentValues";
     public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
+    public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
+    public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
 
     public static final String ACTION_SAVE_CONTACT = "saveContact";
     public static final String EXTRA_CONTACT_STATE = "state";
@@ -114,6 +116,8 @@
     public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
     public static final String EXTRA_DATA_ID = "dataId";
 
+    public static final String ACTION_SPLIT_CONTACT = "splitContact";
+
     public static final String ACTION_JOIN_CONTACTS = "joinContacts";
     public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
     public static final String EXTRA_CONTACT_ID1 = "contactId1";
@@ -125,6 +129,10 @@
     public static final String ACTION_SET_RINGTONE = "setRingtone";
     public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
 
+    public static final int CP2_ERROR = 0;
+    public static final int CONTACTS_LINKED = 1;
+    public static final int CONTACTS_SPLIT = 2;
+
     private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
         Data.MIMETYPE,
         Data.IS_PRIMARY,
@@ -264,6 +272,8 @@
             deleteMultipleContacts(intent);
         } else if (ACTION_DELETE_CONTACT.equals(action)) {
             deleteContact(intent);
+        } else if (ACTION_SPLIT_CONTACT.equals(action)) {
+            splitContact(intent);
         } else if (ACTION_JOIN_CONTACTS.equals(action)) {
             joinContacts(intent);
         } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
@@ -1113,6 +1123,85 @@
     }
 
     /**
+     * Creates an intent that can be sent to this service to split a contact into it's constituent
+     * pieces.
+     */
+    public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
+            ResultReceiver receiver) {
+        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
+        serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
+        return serviceIntent;
+    }
+
+    private void splitContact(Intent intent) {
+        final long rawContactIds[][] = (long[][]) intent
+                .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
+        if (rawContactIds == null) {
+            Log.e(TAG, "Invalid argument for splitContact request");
+            return;
+        }
+        final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
+        final ContentResolver resolver = getContentResolver();
+        final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
+        final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
+        for (int i = 0; i < rawContactIds.length; i++) {
+            for (int j = 0; j < rawContactIds.length; j++) {
+                if (i != j) {
+                    if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j])) {
+                        if (receiver != null) {
+                            receiver.send(CP2_ERROR, new Bundle());
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+        if (operations.size() > 0 && !applyOperations(resolver, operations)) {
+            if (receiver != null) {
+                receiver.send(CP2_ERROR, new Bundle());
+            }
+            return;
+        }
+        if (receiver != null) {
+            receiver.send(CONTACTS_SPLIT, new Bundle());
+        } else {
+            showToast(R.string.contactUnlinkedToast);
+        }
+    }
+
+    /**
+     * Adds insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
+     * and {@param rawContactIds2} to {@param operations}.
+     * @return false if an error occurred, true otherwise.
+     */
+    private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
+            long[] rawContactIds1, long[] rawContactIds2) {
+        if (rawContactIds1 == null || rawContactIds2 == null) {
+            Log.e(TAG, "Invalid arguments for splitContact request");
+            return false;
+        }
+        // For each pair of raw contacts, insert an aggregation exception
+        final ContentResolver resolver = getContentResolver();
+        // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
+        final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
+        for (int i = 0; i < rawContactIds1.length; i++) {
+            for (int j = 0; j < rawContactIds2.length; j++) {
+                buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j]);
+                // Before we get to 500 we need to flush the operations list
+                if (operations.size() > 0 && operations.size() % batchSize == 0) {
+                    if (!applyOperations(resolver, operations)) {
+                        return false;
+                    }
+                    operations.clear();
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
      * Creates an intent that can be sent to this service to join two contacts.
      * The resulting contact uses the name from {@param contactId1} if possible.
      */
@@ -1135,13 +1224,22 @@
      * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
      * No special attention is paid to where the resulting contact's name is taken from.
      */
-    public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
-        Intent serviceIntent = new Intent(context, ContactSaveService.class);
+    public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
+            ResultReceiver receiver) {
+        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
         return serviceIntent;
     }
 
+    /**
+     * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
+     * No special attention is paid to where the resulting contact's name is taken from.
+     */
+    public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
+        return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
+    }
 
     private interface JoinContactQuery {
         String[] PROJECTION = {
@@ -1173,9 +1271,11 @@
 
     private void joinSeveralContacts(Intent intent) {
         final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
+        final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
 
         // Load raw contact IDs for all contacts involved.
-        long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
+        final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
+        final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
         if (rawContactIds == null) {
             Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
             return;
@@ -1193,21 +1293,33 @@
                 }
                 // Before we get to 500 we need to flush the operations list
                 if (operations.size() > 0 && operations.size() % batchSize == 0) {
-                    if (!applyJoinOperations(resolver, operations)) {
+                    if (!applyOperations(resolver, operations)) {
+                        if (receiver != null) {
+                            receiver.send(CP2_ERROR, new Bundle());
+                        }
                         return;
                     }
                     operations.clear();
                 }
             }
         }
-        if (operations.size() > 0 && !applyJoinOperations(resolver, operations)) {
+        if (operations.size() > 0 && !applyOperations(resolver, operations)) {
+            if (receiver != null) {
+                receiver.send(CP2_ERROR, new Bundle());
+            }
             return;
         }
-        showToast(R.string.contactsJoinedMessage);
+        if (receiver != null) {
+            final Bundle result = new Bundle();
+            result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
+            receiver.send(CONTACTS_LINKED, result);
+        } else {
+            showToast(R.string.contactsJoinedMessage);
+        }
     }
 
     /** Returns true if the batch was successfully applied and false otherwise. */
-    private boolean applyJoinOperations(ContentResolver resolver,
+    private boolean applyOperations(ContentResolver resolver,
             ArrayList<ContentProviderOperation> operations) {
         try {
             resolver.applyBatch(ContactsContract.AUTHORITY, operations);
@@ -1219,7 +1331,6 @@
         }
     }
 
-
     private void joinContacts(Intent intent) {
         long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
         long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
@@ -1296,13 +1407,61 @@
         deliverCallback(callbackIntent);
     }
 
+    /**
+     * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
+     * array of the return value holds an array of raw contact ids for one contactId.
+     * @param contactIds
+     * @return
+     */
+    private long[][] getSeparatedRawContactIds(long[] contactIds) {
+        final long[][] rawContactIds = new long[contactIds.length][];
+        for (int i = 0; i < contactIds.length; i++) {
+            rawContactIds[i] = getRawContactIds(contactIds[i]);
+        }
+        return rawContactIds;
+    }
+
+    /**
+     * Gets the raw contact ids associated with {@param contactId}.
+     * @param contactId
+     * @return Array of raw contact ids.
+     */
+    private long[] getRawContactIds(long contactId) {
+        final ContentResolver resolver = getContentResolver();
+        long rawContactIds[];
+
+        final StringBuilder queryBuilder = new StringBuilder();
+            queryBuilder.append(RawContacts.CONTACT_ID)
+                    .append("=")
+                    .append(String.valueOf(contactId));
+
+        final Cursor c = resolver.query(RawContacts.CONTENT_URI,
+                JoinContactQuery.PROJECTION,
+                queryBuilder.toString(),
+                null, null);
+        if (c == null) {
+            Log.e(TAG, "Unable to open Contacts DB cursor");
+            return null;
+        }
+        try {
+            rawContactIds = new long[c.getCount()];
+            for (int i = 0; i < rawContactIds.length; i++) {
+                c.moveToPosition(i);
+                final long rawContactId = c.getLong(JoinContactQuery._ID);
+                rawContactIds[i] = rawContactId;
+            }
+        } finally {
+            c.close();
+        }
+        return rawContactIds;
+    }
+
     private long[] getRawContactIdsForAggregation(long[] contactIds) {
         if (contactIds == null) {
             return null;
         }
 
         final ContentResolver resolver = getContentResolver();
-        long rawContactIds[];
 
         final StringBuilder queryBuilder = new StringBuilder();
         final String stringContactIds[] = new String[contactIds.length];
@@ -1327,6 +1486,7 @@
             showToast(R.string.contactSavedErrorToast);
             return null;
         }
+        long rawContactIds[];
         try {
             if (c.getCount() < 2) {
                 Log.e(TAG, "Not enough raw contacts to aggregate together.");
@@ -1362,6 +1522,19 @@
     }
 
     /**
+     * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation.
+     */
+    private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
+            long rawContactId1, long rawContactId2) {
+        final Builder builder =
+                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+        operations.add(builder.build());
+    }
+
+    /**
      * Shows a toast on the UI thread.
      */
     private void showToast(final int message) {
diff --git a/src/com/android/contacts/GroupMetaDataLoader.java b/src/com/android/contacts/GroupMetaDataLoader.java
index 47648b3..9bfcd42 100644
--- a/src/com/android/contacts/GroupMetaDataLoader.java
+++ b/src/com/android/contacts/GroupMetaDataLoader.java
@@ -50,7 +50,7 @@
 
     public GroupMetaDataLoader(Context context, Uri groupUri) {
         super(context, ensureIsGroupUri(groupUri), COLUMNS, Groups.ACCOUNT_TYPE + " NOT NULL AND "
-                + Groups.ACCOUNT_NAME + " NOT NULL", null, Groups.TITLE);
+                + Groups.ACCOUNT_NAME + " NOT NULL", null, Groups.TITLE + " COLLATE NOCASE");
     }
 
     /**
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index 2d6740d..7042121 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -110,6 +110,8 @@
     private boolean mShowHomeIcon;
     private boolean mShowHomeAsUp;
 
+    private int mSearchHintResId;
+
     public interface TabState {
         public static int ALL = 0;
 
@@ -121,6 +123,12 @@
 
     public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
             View portraitTabs, View landscapeTabs, Toolbar toolbar) {
+        this(activity, listener, actionBar, portraitTabs, landscapeTabs, toolbar,
+                R.string.hint_findContacts);
+    }
+
+    public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
+            View portraitTabs, View landscapeTabs, Toolbar toolbar, int searchHintResId) {
         mActivity = activity;
         mListener = listener;
         mActionBar = actionBar;
@@ -130,6 +138,7 @@
         mToolbar = toolbar;
         mToolBarFrame = (FrameLayout) mToolbar.getParent();
         mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
+        mSearchHintResId = searchHintResId;
 
         setupSearchAndSelectionViews();
         setupTabs(mActivity);
@@ -143,6 +152,10 @@
         mShowHomeAsUp = showHomeAsUp;
     }
 
+    public EditText getSearchView() {
+        return mSearchView;
+    }
+
     private void setupTabs(Context context) {
         final TypedArray attributeArray = context.obtainStyledAttributes(
                 new int[]{android.R.attr.actionBarSize});
@@ -163,7 +176,7 @@
         mSearchContainer.setBackgroundColor(mActivity.getResources().getColor(
                 R.color.searchbox_background_color));
         mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view);
-        mSearchView.setHint(mActivity.getString(R.string.hint_findContacts));
+        mSearchView.setHint(mActivity.getString(mSearchHintResId));
         mSearchView.addTextChangedListener(new SearchTextWatcher());
         mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener(
                 new OnClickListener() {
diff --git a/src/com/android/contacts/activities/GroupEditorActivity.java b/src/com/android/contacts/activities/GroupEditorActivity.java
index 8a285a7..e316902 100644
--- a/src/com/android/contacts/activities/GroupEditorActivity.java
+++ b/src/com/android/contacts/activities/GroupEditorActivity.java
@@ -22,6 +22,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
+import android.widget.AutoCompleteTextView;
 
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
@@ -30,6 +31,7 @@
 import com.android.contacts.quickcontact.QuickContactActivity;
 import com.android.contacts.util.DialogManager;
 
+// TODO(wjang): it longer works. will be deleted shortly
 public class GroupEditorActivity extends ContactsActivity
         implements DialogManager.DialogShowingViewActivity {
 
@@ -63,13 +65,13 @@
         mFragment = (GroupEditorFragment) getFragmentManager().findFragmentById(
                 R.id.group_editor_fragment);
         mFragment.setListener(mFragmentListener);
-        mFragment.setContentResolver(getContentResolver());
+        // mFragment.setContentResolver(getContentResolver());
 
         // NOTE The fragment will restore its state by itself after orientation changes, so
         // we need to do this only for a new instance.
         if (savedState == null) {
             Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null;
-            mFragment.load(action, uri, getIntent().getExtras());
+            // mFragment.load(action, uri, getIntent().getExtras());
         }
     }
 
@@ -139,6 +141,20 @@
             startActivity(ImplicitIntentsUtil.composeQuickContactIntent(
                     contactLookupUri, QuickContactActivity.MODE_FULLY_EXPANDED));
         }
+
+        @Override
+        public AutoCompleteTextView getSearchView() {
+            return null;
+        }
+
+        @Override
+        public boolean isSearchMode() {
+            return false;
+        }
+
+        @Override
+        public void setSearchMode(boolean searchMode) {
+        }
     };
 
     @Override
diff --git a/src/com/android/contacts/activities/GroupMembersActivity.java b/src/com/android/contacts/activities/GroupMembersActivity.java
index e13bcd6..7ebb3e0 100644
--- a/src/com/android/contacts/activities/GroupMembersActivity.java
+++ b/src/com/android/contacts/activities/GroupMembersActivity.java
@@ -15,59 +15,511 @@
  */
 package com.android.contacts.activities;
 
-import android.app.ActionBar;
+import android.app.Fragment;
 import android.app.FragmentManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.CursorLoader;
 import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.AutoCompleteTextView;
+import android.widget.Toast;
 
-import com.android.contacts.ContactsActivity;
+import com.android.contacts.AppCompatContactsActivity;
+import com.android.contacts.ContactSaveService;
+import com.android.contacts.GroupListLoader;
+import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
 import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contacts.group.GroupEditorFragment;
 import com.android.contacts.group.GroupMembersListFragment;
-import com.android.contacts.group.GroupMembersListFragment.GroupMembersListCallbacks;
+import com.android.contacts.group.GroupMetadata;
+import com.android.contacts.group.GroupUtil;
+import com.android.contacts.interactions.GroupDeletionDialogFragment;
+import com.android.contacts.list.ContactsRequest;
+import com.android.contacts.list.MultiSelectContactsListFragment;
 import com.android.contacts.quickcontact.QuickContactActivity;
 
-/** Displays the members of a group. */
-public class GroupMembersActivity extends ContactsActivity implements GroupMembersListCallbacks {
+/**
+ * Displays the members of a group and allows the user to edit it.
+ */
+// TODO(wjang): rename it to GroupActivity since it does both display and edit now.
+public class GroupMembersActivity extends AppCompatContactsActivity implements
+        ActionBarAdapter.Listener,
+        MultiSelectContactsListFragment.OnCheckBoxListActionListener,
+        GroupMembersListFragment.GroupMembersListListener,
+        GroupEditorFragment.Listener {
 
-    private static final String TAG_GROUP_MEMBERS = "group_members";
+    private static final String TAG = "GroupMembersActivity";
 
-    private GroupMembersListFragment mFragment;
+    private static final boolean DEBUG = false;
+
+    private static final String KEY_GROUP_METADATA = "groupMetadata";
+
+    private static final int LOADER_GROUP_METADATA = 0;
+    private static final int LOADER_GROUP_LIST_DETAILS = 1;
+
+    private static final int FRAGMENT_MEMBERS_LIST = -1;
+    private static final int FRAGMENT_EDITOR = -2;
+
+    public static final String ACTION_SAVE_COMPLETED = "saveCompleted";
+
+    private class GroupPagerAdapter extends FragmentPagerAdapter {
+
+        public GroupPagerAdapter(FragmentManager fragmentManager) {
+            super(fragmentManager);
+        }
+
+        @Override
+        public int getCount() {
+            return mIsInsertAction ? 1 : 2;
+        }
+
+        public Fragment getItem(int position) {
+            if (mIsInsertAction) {
+                switch (position) {
+                    case 0:
+                        mEditorFragment = GroupEditorFragment.newInstance(
+                                Intent.ACTION_INSERT, mGroupMetadata, getIntent().getExtras());
+                        return mEditorFragment;
+                }
+                throw new IllegalStateException("Unhandled position " + position);
+            } else {
+                switch (position) {
+                    case 0:
+                        mMembersListFragment = GroupMembersListFragment.newInstance(mGroupMetadata);
+                        return mMembersListFragment;
+                    case 1:
+                        // TODO: double check what intent extras need to be supported
+                        mEditorFragment = GroupEditorFragment.newInstance(
+                                Intent.ACTION_EDIT, mGroupMetadata, getIntent().getExtras());
+                        return mEditorFragment;
+                }
+                throw new IllegalStateException("Unhandled position " + position);
+            }
+        }
+
+        private boolean isCurrentItem(int fragment) {
+            if (mIsInsertAction) {
+                return FRAGMENT_EDITOR == fragment;
+            }
+            int currentItem = mViewPager.getCurrentItem();
+            switch (fragment) {
+                case FRAGMENT_MEMBERS_LIST:
+                    return currentItem == 0;
+                case FRAGMENT_EDITOR:
+                    return currentItem == 1;
+            }
+            return false;
+        }
+
+        private void setCurrentItem(int fragment) {
+            if (mIsInsertAction) {
+                switch (fragment) {
+                    case FRAGMENT_EDITOR:
+                        mViewPager.setCurrentItem(0);
+                        break;
+                    default:
+                        throw new IllegalStateException("Unsupported fragment " + fragment);
+                }
+            } else {
+                switch (fragment) {
+                    case FRAGMENT_MEMBERS_LIST:
+                        mViewPager.setCurrentItem(0);
+                        break;
+                    case FRAGMENT_EDITOR:
+                        mViewPager.setCurrentItem(1);
+                        break;
+                    default:
+                        throw new IllegalStateException("Unsupported fragment " + fragment);
+                }
+            }
+        }
+    }
+
+    /** Step 1 of loading group metadata. */
+    private final LoaderCallbacks<Cursor> mGroupMetadataCallbacks = new LoaderCallbacks<Cursor>() {
+
+        @Override
+        public CursorLoader onCreateLoader(int id, Bundle args) {
+            return new GroupMetaDataLoader(GroupMembersActivity.this, mGroupUri);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+            if (cursor == null || cursor.isClosed()) {
+                Log.e(TAG, "Failed to load group metadata");
+                return;
+            }
+            if (cursor.moveToNext()) {
+                final boolean deleted = cursor.getInt(GroupMetaDataLoader.DELETED) == 1;
+                if (!deleted) {
+                    mGroupMetadata = new GroupMetadata();
+                    mGroupMetadata.uri = mGroupUri;
+                    mGroupMetadata.accountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME);
+                    mGroupMetadata.accountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
+                    mGroupMetadata.dataSet = cursor.getString(GroupMetaDataLoader.DATA_SET);
+                    mGroupMetadata.groupId = cursor.getLong(GroupMetaDataLoader.GROUP_ID);
+                    mGroupMetadata.groupName = cursor.getString(GroupMetaDataLoader.TITLE);
+                    mGroupMetadata.readOnly = cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1;
+
+                    final AccountTypeManager accountTypeManager =
+                            AccountTypeManager.getInstance(GroupMembersActivity.this);
+                    final AccountType accountType = accountTypeManager.getAccountType(
+                            mGroupMetadata.accountType, mGroupMetadata.dataSet);
+                    mGroupMetadata.editable = accountType.isGroupMembershipEditable();
+
+                    getLoaderManager().restartLoader(LOADER_GROUP_LIST_DETAILS, null,
+                            mGroupListCallbacks);
+                }
+            }
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {}
+    };
+
+    /** Step 2 of loading group metadata. */
+    private final LoaderCallbacks<Cursor> mGroupListCallbacks = new LoaderCallbacks<Cursor>() {
+
+        @Override
+        public CursorLoader onCreateLoader(int id, Bundle args) {
+            final GroupListLoader groupListLoader = new GroupListLoader(GroupMembersActivity.this);
+
+            // TODO(wjang): modify GroupListLoader to accept this selection criteria more naturally
+            groupListLoader.setSelection(groupListLoader.getSelection()
+                    + " AND " + ContactsContract.Groups._ID + "=?");
+
+            final String[] selectionArgs = new String[1];
+            selectionArgs[0] = Long.toString(mGroupMetadata.groupId);
+            groupListLoader.setSelectionArgs(selectionArgs);
+
+            return groupListLoader;
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+            if (cursor == null || cursor.isClosed()) {
+                Log.e(TAG, "Failed to load group list details");
+                return;
+            }
+            if (cursor.moveToNext()) {
+                mGroupMetadata.memberCount = cursor.getInt(GroupListLoader.MEMBER_COUNT);
+            }
+            onGroupMetadataLoaded();
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {}
+    };
+
+    private ActionBarAdapter mActionBarAdapter;
+    private ViewPager mViewPager;
+
+    private GroupPagerAdapter mPagerAdapter;
+
+    private Uri mGroupUri;
+    private GroupMetadata mGroupMetadata;
+
+    private GroupMembersListFragment mMembersListFragment;
+    private GroupEditorFragment mEditorFragment;
+
+    private boolean mIsInsertAction;
 
     @Override
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
 
+        mIsInsertAction = Intent.ACTION_INSERT.equals(getIntent().getAction());
+
+        mGroupUri = getIntent().getData();
+        if (savedState != null) {
+            mGroupMetadata = savedState.getParcelable(KEY_GROUP_METADATA);
+        }
+
+        // Setup the view
         setContentView(R.layout.group_members_activity);
+        mViewPager = (ViewPager) findViewById(R.id.view_pager);
 
-        final ActionBar actionBar = getActionBar();
-        if (actionBar != null) {
-            actionBar.setDisplayShowHomeEnabled(true);
-            actionBar.setDisplayHomeAsUpEnabled(true);
-            actionBar.setDisplayShowTitleEnabled(true);
-        }
-
-        final FragmentManager fragmentManager = getFragmentManager();
-        mFragment = (GroupMembersListFragment) fragmentManager.findFragmentByTag(TAG_GROUP_MEMBERS);
-        if (mFragment == null) {
-            mFragment = new GroupMembersListFragment();
-            fragmentManager.beginTransaction()
-                    .add(R.id.fragment_container, mFragment, TAG_GROUP_MEMBERS)
-                    .commit();
-        }
-        mFragment.setGroupUri(getIntent().getData());
-        mFragment.setCallbacks(this);
+        // Set up the action bar
+        final Toolbar toolbar = getView(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        final ContactsRequest contactsRequest = new ContactsRequest();
+        contactsRequest.setActionCode(ContactsRequest.ACTION_GROUP);
+        mActionBarAdapter = new ActionBarAdapter(this, this, getSupportActionBar(),
+                /* portraitTabs */ null, /* landscapeTabs */ null, toolbar,
+                R.string.enter_contact_name);
+        mActionBarAdapter.setShowHomeIcon(true);
+        mActionBarAdapter.setShowHomeAsUp(true);
+        mActionBarAdapter.initialize(savedState, contactsRequest);
     }
 
     @Override
-    public void onHomePressed() {
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(KEY_GROUP_METADATA, mGroupMetadata);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        if (mIsInsertAction) {
+            mGroupMetadata = new GroupMetadata();
+            onGroupMetadataLoaded();
+        } else {
+            if (mGroupMetadata == null) {
+                getLoaderManager().restartLoader(
+                        LOADER_GROUP_METADATA, null, mGroupMetadataCallbacks);
+            } else {
+                onGroupMetadataLoaded();
+            }
+        }
+    }
+
+    @Override
+    protected void onNewIntent(Intent newIntent) {
+        super.onNewIntent(newIntent);
+
+        if (ACTION_SAVE_COMPLETED.equals(newIntent.getAction())) {
+            final Uri groupUri = newIntent.getData();
+            if (groupUri == null) {
+                Toast.makeText(this, R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
+                setResult(RESULT_CANCELED);
+                finish();
+            } else {
+                Toast.makeText(this, R.string.groupSavedToast,Toast.LENGTH_SHORT).show();
+
+                final Intent intent = GroupUtil.createViewGroupIntent(this, groupUri);
+                finish();
+                startActivity(intent);
+            }
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        if (mGroupMetadata == null || mGroupMetadata.memberCount < 0) {
+            // Hide menu options until metatdata is fully loaded
+            return false;
+        }
+        super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.view_group, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        final boolean isSelectionMode = mActionBarAdapter.isSelectionMode();
+        final boolean isSearchMode = false;
+
+        final boolean isListFragment = mPagerAdapter.isCurrentItem(FRAGMENT_MEMBERS_LIST);
+        final boolean isEditorFragment = mPagerAdapter.isCurrentItem(FRAGMENT_EDITOR);
+
+        final boolean isGroupEditable = mGroupMetadata.editable;
+        final boolean isGroupReadOnly = mGroupMetadata.readOnly;
+
+        setVisible(menu, R.id.menu_edit_group, isGroupEditable && !isEditorFragment &&
+                !isSelectionMode && !isSearchMode);
+
+        setVisible(menu, R.id.menu_delete_group, !isGroupReadOnly && !isEditorFragment &&
+                !isSelectionMode && !isSearchMode);
+
+        setVisible(menu, R.id.menu_remove_from_group,
+                isGroupEditable && isSelectionMode && isListFragment);
+
+        return true;
+    }
+
+    private static void setVisible(Menu menu, int id, boolean visible) {
+        final MenuItem menuItem = menu.findItem(id);
+        if (menuItem != null) {
+            menuItem.setVisible(visible);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home: {
+                onBackPressed();
+                return true;
+            }
+            case R.id.menu_edit_group: {
+                mPagerAdapter.setCurrentItem(FRAGMENT_EDITOR);
+                return true;
+            }
+            case R.id.menu_delete_group: {
+                GroupDeletionDialogFragment.show(getFragmentManager(), mGroupMetadata.groupId,
+                        mGroupMetadata.groupName, /* endActivity */ true);
+                return true;
+            }
+            case R.id.menu_remove_from_group: {
+                removeSelectedContacts();
+                return true;
+            }
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void removeSelectedContacts() {
+        final long[] rawContactsToRemove =
+                mMembersListFragment.getAdapter().getSelectedContactIdsArray();
+        final Intent intent = ContactSaveService.createGroupUpdateIntent(
+                this, mGroupMetadata.groupId, /* groupName */ null,
+                /* rawContactsToAdd */ null, rawContactsToRemove, getClass(),
+                GroupMembersActivity.ACTION_SAVE_COMPLETED);
+        startService(intent);
+    }
+
+    private void onGroupMetadataLoaded() {
+        if (DEBUG) Log.d(TAG, "Loaded " + mGroupMetadata);
+
+        if (mPagerAdapter == null) {
+            mPagerAdapter = new GroupPagerAdapter(getFragmentManager());
+            mViewPager.setAdapter(mPagerAdapter);
+        }
+
+        if (mIsInsertAction) {
+            mPagerAdapter.setCurrentItem(FRAGMENT_EDITOR);
+            getSupportActionBar().setTitle(getString(R.string.editGroupDescription));
+        } else {
+            getSupportActionBar().setTitle(mGroupMetadata.groupName);
+            mPagerAdapter.setCurrentItem(FRAGMENT_MEMBERS_LIST);
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mIsInsertAction) {
+            finish();
+        } else if (mActionBarAdapter.isSelectionMode()) {
+            mActionBarAdapter.setSelectionMode(false);
+            if (mMembersListFragment != null) {
+                mMembersListFragment.displayCheckBoxes(false);
+            }
+        } else if (mActionBarAdapter.isSearchMode()) {
+            mActionBarAdapter.setSearchMode(false);
+        } else if (mPagerAdapter.isCurrentItem(FRAGMENT_EDITOR)) {
+            mPagerAdapter.setCurrentItem(FRAGMENT_MEMBERS_LIST);
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    // GroupsMembersListFragment callbacks
+
+    @Override
+    public void onGroupMemberListItemClicked(Uri contactLookupUri) {
+        startActivity(ImplicitIntentsUtil.composeQuickContactIntent(
+                contactLookupUri, QuickContactActivity.MODE_FULLY_EXPANDED));
+    }
+
+    // ActionBarAdapter callbacks
+
+    @Override
+    public void onAction(int action) {
+        switch (action) {
+            case ActionBarAdapter.Listener.Action.START_SEARCH_MODE:
+                mActionBarAdapter.setSearchMode(true);
+                invalidateOptionsMenu();
+                showFabWithAnimation(/* showFabWithAnimation = */ false);
+                break;
+            case ActionBarAdapter.Listener.Action.START_SELECTION_MODE:
+                if (mMembersListFragment != null) {
+                    mMembersListFragment.displayCheckBoxes(true);
+                }
+                invalidateOptionsMenu();
+                break;
+            case ActionBarAdapter.Listener.Action.STOP_SEARCH_AND_SELECTION_MODE:
+                mActionBarAdapter.setSearchMode(false);
+                if (mMembersListFragment != null) {
+                    mMembersListFragment.displayCheckBoxes(false);
+                }
+                invalidateOptionsMenu();
+                showFabWithAnimation(/* showFabWithAnimation */ true);
+                break;
+            case ActionBarAdapter.Listener.Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE:
+                showFabWithAnimation(/* showFabWithAnimation */ true);
+                break;
+        }
+    }
+
+    private void showFabWithAnimation(boolean showFab) {
+        // TODO(wjang): b/28497108
+    }
+
+    @Override
+    public void onSelectedTabChanged() {
+    }
+
+    @Override
+    public void onUpButtonPressed() {
         onBackPressed();
     }
 
+    // MultiSelect checkbox callbacks
+
     @Override
-    public void onGroupNameLoaded(String groupName) {
-        setTitle(groupName);
+    public void onStartDisplayingCheckBoxes() {
+        mActionBarAdapter.setSelectionMode(true);
+        invalidateOptionsMenu();
+    }
+
+    @Override
+    public void onSelectedContactIdsChanged() {
+        if (mActionBarAdapter.isSelectionMode() && mMembersListFragment != null) {
+            mActionBarAdapter.setSelectionCount(
+                    mMembersListFragment.getSelectedContactIds().size());
+        }
+        invalidateOptionsMenu();
+    }
+
+    @Override
+    public void onStopDisplayingCheckBoxes() {
+        mActionBarAdapter.setSelectionMode(false);
+        invalidateOptionsMenu();
+    }
+
+    // GroupEditorFragment.Listener callbacks
+
+    @Override
+    public void onGroupNotFound() {
+        finish();
+    }
+
+    @Override
+    public void onReverted() {
+        if (mIsInsertAction) {
+            finish();
+        } else {
+            mPagerAdapter.setCurrentItem(FRAGMENT_MEMBERS_LIST);
+        }
+    }
+
+    @Override
+    public void onSaveFinished(int resultCode, Intent resultIntent) {
+        if (mIsInsertAction) {
+            final Intent intent = GroupUtil.createViewGroupIntent(this, resultIntent.getData());
+            finish();
+            startActivity(intent);
+        }
+    }
+
+    @Override
+    public void onAccountsNotFound() {
+        finish();
     }
 
     @Override
@@ -77,10 +529,20 @@
     }
 
     @Override
-    public void onEditGroup(Uri groupUri) {
-        final Intent intent = new Intent(this, GroupEditorActivity.class);
-        intent.setData(groupUri);
-        intent.setAction(Intent.ACTION_EDIT);
-        startActivity(intent);
+    public AutoCompleteTextView getSearchView() {
+        return mActionBarAdapter == null
+                ? null : (AutoCompleteTextView) mActionBarAdapter.getSearchView();
+    }
+
+    @Override
+    public boolean isSearchMode() {
+        return mActionBarAdapter == null ? false : mActionBarAdapter.isSearchMode();
+    }
+
+    @Override
+    public void setSearchMode(boolean searchMode) {
+        if (mActionBarAdapter != null) {
+            mActionBarAdapter.setSearchMode(searchMode);
+        }
     }
 }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 002d74b..93d3f84 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -94,7 +94,7 @@
 import com.android.contacts.list.ContactsIntentResolver;
 import com.android.contacts.list.ContactsRequest;
 import com.android.contacts.list.ContactsUnavailableFragment;
-import com.android.contacts.list.MultiSelectContactsListFragment;
+import com.android.contacts.list.DefaultContactBrowseListFragment;
 import com.android.contacts.list.MultiSelectContactsListFragment.OnCheckBoxListActionListener;
 import com.android.contacts.list.OnContactBrowserActionListener;
 import com.android.contacts.list.OnContactsUnavailableActionListener;
@@ -150,7 +150,7 @@
     /**
      * Showing a list of Contacts. Also used for showing search results in search mode.
      */
-    private MultiSelectContactsListFragment mAllFragment;
+    private DefaultContactBrowseListFragment mAllFragment;
     private GroupsFragment mGroupsFragment;
     private AccountFiltersFragment mAccountFiltersFragment;
 
@@ -376,7 +376,7 @@
         // However, if it's after screen rotation, the fragments have been re-created by
         // the fragment manager, so first see if there're already the target fragments
         // existing.
-        mAllFragment = (MultiSelectContactsListFragment)
+        mAllFragment = (DefaultContactBrowseListFragment)
                 fragmentManager.findFragmentByTag(ALL_TAG);
         mGroupsFragment = (GroupsFragment)
                 fragmentManager.findFragmentByTag(GROUPS_TAG);
@@ -384,7 +384,7 @@
                 fragmentManager.findFragmentByTag(FILTERS_TAG);
 
         if (mAllFragment == null) {
-            mAllFragment = new MultiSelectContactsListFragment();
+            mAllFragment = new DefaultContactBrowseListFragment();
             transaction.add(R.id.tab_pager, mAllFragment, ALL_TAG);
 
             if (areGroupWritableAccountsAvailable()) {
@@ -1316,14 +1316,8 @@
     }
 
     private void joinSelectedContacts() {
-        final Long[] contactIdsArray = mAllFragment.getSelectedContactIds().toArray(
-                new Long[mAllFragment.getSelectedContactIds().size()]);
-        final long[] contactIdsArray2 = new long[contactIdsArray.length];
-        for (int i = 0; i < contactIdsArray.length; i++) {
-            contactIdsArray2[i] = contactIdsArray[i];
-        }
-        final Intent intent = ContactSaveService.createJoinSeveralContactsIntent(this,
-                contactIdsArray2);
+        final Intent intent = ContactSaveService.createJoinSeveralContactsIntent(
+                this, mAllFragment.getSelectedContactIdsArray());
         this.startService(intent);
 
         mActionBarAdapter.setSelectionMode(false);
diff --git a/src/com/android/contacts/group/GroupEditorFragment.java b/src/com/android/contacts/group/GroupEditorFragment.java
index 25a07f1..a7d30a4 100644
--- a/src/com/android/contacts/group/GroupEditorFragment.java
+++ b/src/com/android/contacts/group/GroupEditorFragment.java
@@ -25,7 +25,6 @@
 import android.app.LoaderManager;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ContentResolver;
-import android.content.ContentUris;
 import android.content.Context;
 import android.content.CursorLoader;
 import android.content.DialogInterface;
@@ -59,9 +58,8 @@
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.GroupMemberLoader;
 import com.android.contacts.GroupMemberLoader.GroupEditorQuery;
-import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
-import com.android.contacts.activities.GroupEditorActivity;
+import com.android.contacts.activities.GroupMembersActivity;
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
 import com.android.contacts.common.model.account.AccountType;
@@ -79,11 +77,13 @@
 public class GroupEditorFragment extends Fragment implements SelectAccountDialogFragment.Listener {
     private static final String TAG = "GroupEditorFragment";
 
-    private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
+    private static final String ARG_ACTION = "action";
+    private static final String ARG_GROUP_METADATA = "groupMetadata";
+    private static final String ARG_INTENT_EXTRAS = "intentExtras";
 
     private static final String KEY_ACTION = "action";
-    private static final String KEY_GROUP_URI = "groupUri";
-    private static final String KEY_GROUP_ID = "groupId";
+    private static final String KEY_GROUP_METADATA = "groupMetadata";
+    private static final String KEY_INTENT_EXTRAS = "intentExtras";
     private static final String KEY_STATUS = "status";
     private static final String KEY_ACCOUNT_NAME = "accountName";
     private static final String KEY_ACCOUNT_TYPE = "accountType";
@@ -96,11 +96,11 @@
 
     private static final String CURRENT_EDITOR_TAG = "currentEditorForAccount";
 
-    public static interface Listener {
+    public interface Listener {
         /**
          * Group metadata was not found, close the fragment now.
          */
-        public void onGroupNotFound();
+        void onGroupNotFound();
 
         /**
          * User has tapped Revert, close the fragment now.
@@ -121,9 +121,26 @@
          * Group member name or photo was clicked in order to view contact details.
          */
         void onGroupMemberClicked(Uri contactLookupUri);
+
+        // TODO(wjang): consider calling these directly on the host Activity or moving these
+        // options menu items to the host Activity.
+
+        /**
+         * Returns the autocomplete view from the action bar.
+         */
+        AutoCompleteTextView getSearchView();
+
+        /**
+         * Whether the action bar is currently in search mode.
+         */
+        boolean isSearchMode();
+
+        /**
+         * Change whether the action bar is in search mode.
+         */
+        void setSearchMode(boolean searchMode);
     }
 
-    private static final int LOADER_GROUP_METADATA = 1;
     private static final int LOADER_EXISTING_MEMBERS = 2;
     private static final int LOADER_NEW_GROUP_MEMBER = 3;
 
@@ -165,7 +182,6 @@
      */
     public enum Status {
         SELECTING_ACCOUNT, // Account select dialog is showing
-        LOADING,    // Loader is fetching the group metadata
         EDITING,    // Not currently busy. We are waiting forthe user to enter data.
         SAVING,     // Data is currently being saved
         CLOSING     // Prevents any more saves
@@ -174,8 +190,8 @@
     private Context mContext;
     private String mAction;
     private Bundle mIntentExtras;
-    private Uri mGroupUri;
-    private long mGroupId;
+    private GroupMetadata mGroupMetadata;
+
     private Listener mListener;
 
     private Status mStatus;
@@ -205,41 +221,72 @@
     private ArrayList<Member> mListMembersToRemove = new ArrayList<Member>();
     private ArrayList<Member> mListToDisplay = new ArrayList<Member>();
 
-    public GroupEditorFragment() {
+    public static GroupEditorFragment newInstance(String action, GroupMetadata groupMetadata,
+            Bundle intentExtras) {
+        final Bundle args = new Bundle();
+        args.putString(ARG_ACTION, action);
+        args.putParcelable(ARG_GROUP_METADATA, groupMetadata);
+        args.putParcelable(ARG_INTENT_EXTRAS, intentExtras);
+        final GroupEditorFragment fragment = new GroupEditorFragment();
+        fragment.setArguments(args);
+        return fragment;
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
         setHasOptionsMenu(true);
+
         mLayoutInflater = inflater;
         mRootView = (ViewGroup) inflater.inflate(R.layout.group_editor_fragment, container, false);
         return mRootView;
     }
 
     @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        mContext = activity;
-        mPhotoManager = ContactPhotoManager.getInstance(mContext);
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState == null) {
+            final Bundle args = getArguments();
+            if (args == null) {
+                throw new IllegalStateException("Group editor fragment created without arguments");
+            }
+            mAction = args.getString(ARG_ACTION);
+            mGroupMetadata = args.getParcelable(ARG_GROUP_METADATA);
+            mIntentExtras = args.getParcelable(ARG_INTENT_EXTRAS);
+        } else {
+            onRestoreInstanceState(savedInstanceState);
+        }
+
         mMemberListAdapter = new MemberListAdapter();
     }
 
     @Override
+    public void onAttach(Activity context) {
+        super.onAttach(context);
+        mContext = context;
+        mPhotoManager = ContactPhotoManager.getInstance(mContext);
+        mContentResolver = mContext.getContentResolver();
+
+        try {
+            mListener = (Listener) getActivity();
+        } catch (ClassCastException e) {
+            throw new ClassCastException(getActivity() + " must implement " +
+                    Listener.class.getSimpleName());
+        }
+    }
+
+    @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
         if (savedInstanceState != null) {
-            // Just restore from the saved state.  No loading.
-            onRestoreInstanceState(savedInstanceState);
-            if (mStatus == Status.SELECTING_ACCOUNT) {
-                // Account select dialog is showing.  Don't setup the editor yet.
-            } else if (mStatus == Status.LOADING) {
-                startGroupMetaDataLoader();
-            } else {
+            if (mStatus != Status.SELECTING_ACCOUNT) {
                 setupEditorForAccount();
-            }
+            } // else Account select dialog is showing.  Don't setup the editor yet.
         } else if (Intent.ACTION_EDIT.equals(mAction)) {
-            startGroupMetaDataLoader();
+            bindGroupMetaData();
+            // Load existing members
+            getLoaderManager().initLoader(
+                    LOADER_EXISTING_MEMBERS, null, mGroupMemberListLoaderListener);
         } else if (Intent.ACTION_INSERT.equals(mAction)) {
             final Account account = mIntentExtras == null ? null :
                     (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
@@ -256,24 +303,15 @@
                 // No Account specified. Let the user choose from a disambiguation dialog.
                 selectAccountAndCreateGroup();
             }
-        } else {
-            throw new IllegalArgumentException("Unknown Action String " + mAction +
-                    ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
         }
     }
 
-    private void startGroupMetaDataLoader() {
-        mStatus = Status.LOADING;
-        getLoaderManager().initLoader(LOADER_GROUP_METADATA, null,
-                mGroupMetaDataLoaderListener);
-    }
-
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         outState.putString(KEY_ACTION, mAction);
-        outState.putParcelable(KEY_GROUP_URI, mGroupUri);
-        outState.putLong(KEY_GROUP_ID, mGroupId);
+        outState.putParcelable(KEY_GROUP_METADATA, mGroupMetadata);
+        outState.putParcelable(KEY_INTENT_EXTRAS, mIntentExtras);
 
         outState.putSerializable(KEY_STATUS, mStatus);
         outState.putString(KEY_ACCOUNT_NAME, mAccountName);
@@ -290,8 +328,8 @@
 
     private void onRestoreInstanceState(Bundle state) {
         mAction = state.getString(KEY_ACTION);
-        mGroupUri = state.getParcelable(KEY_GROUP_URI);
-        mGroupId = state.getLong(KEY_GROUP_ID);
+        mGroupMetadata = state.getParcelable(KEY_GROUP_METADATA);
+        mIntentExtras = state.getParcelable(KEY_INTENT_EXTRAS);
 
         mStatus = (Status) state.getSerializable(KEY_STATUS);
         mAccountName = state.getString(KEY_ACCOUNT_NAME);
@@ -306,13 +344,6 @@
         mListToDisplay = state.getParcelableArrayList(KEY_MEMBERS_TO_DISPLAY);
     }
 
-    public void setContentResolver(ContentResolver resolver) {
-        mContentResolver = resolver;
-        if (mAutoCompleteAdapter != null) {
-            mAutoCompleteAdapter.setContentResolver(mContentResolver);
-        }
-    }
-
     private void selectAccountAndCreateGroup() {
         final List<AccountWithDataSet> accounts =
                 AccountTypeManager.getInstance(mContext).getAccounts(true /* writeable */);
@@ -400,13 +431,17 @@
         } else {
             editorView = mRootView.findViewWithTag(CURRENT_EDITOR_TAG);
             if (editorView == null) {
-                throw new IllegalStateException("Group editor view not found");
+                // TODO(wjang): should not happen once this is fully integrated into group members
+                // activity so just let it go for now
+                // throw new IllegalStateException("Group editor view not found");
+                return;
             }
         }
 
         mGroupNameView = (TextView) editorView.findViewById(R.id.group_name);
-        mAutoCompleteTextView = (AutoCompleteTextView) editorView.findViewById(
-                R.id.add_member_field);
+        if (mListener != null) {
+            mAutoCompleteTextView = mListener.getSearchView();
+        }
 
         mListView = (ListView) editorView.findViewById(android.R.id.list);
         mListView.setAdapter(mMemberListAdapter);
@@ -466,26 +501,12 @@
         mStatus = Status.EDITING;
     }
 
-    public void load(String action, Uri groupUri, Bundle intentExtras) {
-        mAction = action;
-        mGroupUri = groupUri;
-        mGroupId = (groupUri != null) ? ContentUris.parseId(mGroupUri) : 0;
-        mIntentExtras = intentExtras;
-    }
-
-    private void bindGroupMetaData(Cursor cursor) {
-        if (!cursor.moveToFirst()) {
-            Log.i(TAG, "Group not found with URI: " + mGroupUri + " Closing activity now.");
-            if (mListener != null) {
-                mListener.onGroupNotFound();
-            }
-            return;
-        }
-        mOriginalGroupName = cursor.getString(GroupMetaDataLoader.TITLE);
-        mAccountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME);
-        mAccountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
-        mDataSet = cursor.getString(GroupMetaDataLoader.DATA_SET);
-        mGroupNameIsReadOnly = (cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1);
+    private void bindGroupMetaData() {
+        mOriginalGroupName = mGroupMetadata.groupName;
+        mAccountName = mGroupMetadata.accountName;
+        mAccountType = mGroupMetadata.accountType;
+        mDataSet = mGroupMetadata.dataSet;
+        mGroupNameIsReadOnly = mGroupMetadata.readOnly;
         setupEditorForAccount();
 
         // Setup the group metadata display
@@ -518,12 +539,36 @@
     }
 
     @Override
+    public void onPrepareOptionsMenu(Menu menu) {
+        final boolean isSearchMode = mListener == null ? false : mListener.isSearchMode();
+        setVisible(menu, R.id.menu_add, !isSearchMode);
+        setVisible(menu, R.id.menu_save, !isSearchMode);
+        setVisible(menu, R.id.menu_discard, !isSearchMode);
+    }
+
+    private static void setVisible(Menu menu, int id, boolean visible) {
+        final MenuItem menuItem = menu.findItem(id);
+        if (menuItem != null) {
+            menuItem.setVisible(visible);
+        }
+    }
+
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case android.R.id.home: {
-                getActivity().onBackPressed();
+                if (!hasNameChange() && !hasMembershipChange()) {
+                    getActivity().onBackPressed();
+                } else {
+                    CancelEditDialogFragment.show(this);
+                }
                 return true;
             }
+            case R.id.menu_add:
+                if (mListener != null) {
+                    mListener.setSearchMode(true);
+                }
+                return true;
             case R.id.menu_save:
                 onDoneClicked();
                 return true;
@@ -586,7 +631,7 @@
 
         // If there are no changes, then go straight to onSaveCompleted()
         if (!hasNameChange() && !hasMembershipChange()) {
-            onSaveCompleted(false, mGroupUri);
+            onSaveCompleted(false, mGroupMetadata.uri);
             return true;
         }
 
@@ -607,7 +652,7 @@
                     new AccountWithDataSet(mAccountName, mAccountType, mDataSet),
                     mGroupNameView.getText().toString(),
                     membersToAddArray, activity.getClass(),
-                    GroupEditorActivity.ACTION_SAVE_COMPLETED);
+                    GroupMembersActivity.ACTION_SAVE_COMPLETED);
         } else if (Intent.ACTION_EDIT.equals(mAction)) {
             // Create array of raw contact IDs for contacts to add to the group
             long[] membersToAddArray = convertToArray(mListMembersToAdd);
@@ -616,9 +661,10 @@
             long[] membersToRemoveArray = convertToArray(mListMembersToRemove);
 
             // Create the update intent (which includes the updated group name if necessary)
-            saveIntent = ContactSaveService.createGroupUpdateIntent(activity, mGroupId,
+            saveIntent = ContactSaveService.createGroupUpdateIntent(activity,
+                    mGroupMetadata.groupId,
                     getUpdatedName(), membersToAddArray, membersToRemoveArray,
-                    activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED);
+                    activity.getClass(), GroupMembersActivity.ACTION_SAVE_COMPLETED);
         } else {
             throw new IllegalStateException("Invalid intent action type " + mAction);
         }
@@ -636,21 +682,8 @@
         final Intent resultIntent;
         final int resultCode;
         if (success && groupUri != null) {
-            final String requestAuthority = groupUri.getAuthority();
-
             resultIntent = new Intent();
-            if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) {
-                // Build legacy Uri when requested by caller
-                final long groupId = ContentUris.parseId(groupUri);
-                final Uri legacyContentUri = Uri.parse("content://contacts/groups");
-                final Uri legacyUri = ContentUris.withAppendedId(
-                        legacyContentUri, groupId);
-                resultIntent.setData(legacyUri);
-            } else {
-                // Otherwise pass back the given Uri
-                resultIntent.setData(groupUri);
-            }
-
+            resultIntent.setData(GroupUtil.maybeConvertToLegacyUri(groupUri));
             resultCode = Activity.RESULT_OK;
         } else {
             resultCode = Activity.RESULT_CANCELED;
@@ -744,30 +777,6 @@
     }
 
     /**
-     * The listener for the group metadata (i.e. group name, account type, and account name) loader.
-     */
-    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener =
-            new LoaderCallbacks<Cursor>() {
-
-        @Override
-        public CursorLoader onCreateLoader(int id, Bundle args) {
-            return new GroupMetaDataLoader(mContext, mGroupUri);
-        }
-
-        @Override
-        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-            bindGroupMetaData(data);
-
-            // Load existing members
-            getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null,
-                    mGroupMemberListLoaderListener);
-        }
-
-        @Override
-        public void onLoaderReset(Loader<Cursor> loader) {}
-    };
-
-    /**
      * The loader listener for the list of existing group members.
      */
     private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener =
@@ -775,7 +784,8 @@
 
         @Override
         public CursorLoader onCreateLoader(int id, Bundle args) {
-            return GroupMemberLoader.constructLoaderForGroupEditorQuery(mContext, mGroupId);
+            return GroupMemberLoader.constructLoaderForGroupEditorQuery(
+                    mContext, mGroupMetadata.groupId);
         }
 
         @Override
diff --git a/src/com/android/contacts/group/GroupMembersListAdapter.java b/src/com/android/contacts/group/GroupMembersListAdapter.java
index 71e8f8d..167c014 100644
--- a/src/com/android/contacts/group/GroupMembersListAdapter.java
+++ b/src/com/android/contacts/group/GroupMembersListAdapter.java
@@ -28,17 +28,18 @@
 import android.view.ViewGroup;
 
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
-import com.android.contacts.common.list.ContactEntryListAdapter;
 import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.MultiSelectEntryContactListAdapter;
 import com.android.contacts.common.preference.ContactsPreferences;
 
 /** Group members cursor adapter. */
-public class GroupMembersListAdapter extends ContactEntryListAdapter {
+public class GroupMembersListAdapter extends MultiSelectEntryContactListAdapter {
 
     private static class GroupMembersQuery {
 
         private static final String[] PROJECTION_PRIMARY = new String[] {
                 Data.CONTACT_ID,
+                Data.RAW_CONTACT_ID,
                 Data.PHOTO_ID,
                 Data.LOOKUP_KEY,
                 Data.CONTACT_PRESENCE,
@@ -48,6 +49,7 @@
 
         private static final String[] PROJECTION_ALTERNATIVE = new String[] {
                 Data.CONTACT_ID,
+                Data.RAW_CONTACT_ID,
                 Data.PHOTO_ID,
                 Data.LOOKUP_KEY,
                 Data.CONTACT_PRESENCE,
@@ -56,18 +58,19 @@
         };
 
         public static final int CONTACT_ID                   = 0;
-        public static final int CONTACT_PHOTO_ID             = 1;
-        public static final int CONTACT_LOOKUP_KEY           = 2;
-        public static final int CONTACT_PRESENCE             = 3;
-        public static final int CONTACT_STATUS               = 4;
-        public static final int CONTACT_DISPLAY_NAME         = 5;
+        public static final int RAW_CONTACT_ID               = 1;
+        public static final int CONTACT_PHOTO_ID             = 2;
+        public static final int CONTACT_LOOKUP_KEY           = 3;
+        public static final int CONTACT_PRESENCE             = 4;
+        public static final int CONTACT_STATUS               = 5;
+        public static final int CONTACT_DISPLAY_NAME         = 6;
     }
 
     private final CharSequence mUnknownNameText;
     private long mGroupId;
 
     public GroupMembersListAdapter(Context context) {
-        super(context);
+        super(context, GroupMembersQuery.RAW_CONTACT_ID);
         mUnknownNameText = context.getText(android.R.string.unknownName);
         setIndexedPartition(0);
     }
@@ -116,6 +119,13 @@
         return ((Cursor) getItem(position)).getString(GroupMembersQuery.CONTACT_DISPLAY_NAME);
     }
 
+    public Uri getContactUri(int position) {
+        final Cursor cursor = (Cursor) getItem(position);
+        final long contactId = cursor.getLong(GroupMembersQuery.CONTACT_ID);
+        final String lookupKey = cursor.getString(GroupMembersQuery.CONTACT_LOOKUP_KEY);
+        return Contacts.getLookupUri(contactId, lookupKey);
+    }
+
     @Override
     protected ContactListItemView newView(Context context, int partition, Cursor cursor,
             int position, ViewGroup parent) {
diff --git a/src/com/android/contacts/group/GroupMembersListFragment.java b/src/com/android/contacts/group/GroupMembersListFragment.java
index b8dad5e..b896a4e 100644
--- a/src/com/android/contacts/group/GroupMembersListFragment.java
+++ b/src/com/android/contacts/group/GroupMembersListFragment.java
@@ -15,207 +15,44 @@
  */
 package com.android.contacts.group;
 
-import android.app.Activity;
-import android.app.LoaderManager.LoaderCallbacks;
-import android.content.CursorLoader;
-import android.content.Loader;
-import android.database.Cursor;
+import android.content.Context;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.provider.ContactsContract.Groups;
-import android.util.Log;
 import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 
-import com.android.contacts.GroupListLoader;
-import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
-import com.android.contacts.common.list.ContactEntryListFragment;
-import com.android.contacts.common.model.AccountTypeManager;
-import com.android.contacts.common.model.account.AccountType;
-import com.android.contacts.interactions.GroupDeletionDialogFragment;
+import com.android.contacts.list.MultiSelectContactsListFragment;
 
 /** Displays the members of a group. */
-public class GroupMembersListFragment extends ContactEntryListFragment<GroupMembersListAdapter> {
+public class GroupMembersListFragment extends MultiSelectContactsListFragment {
 
-    private static final String TAG = "GroupMembersList";
-
-    private static final String KEY_GROUP_URI = "groupUri";
     private static final String KEY_GROUP_METADATA = "groupMetadata";
 
-    private static final int LOADER_GROUP_METADATA = 0;
-    private static final int LOADER_GROUP_LIST_DETAILS = 1;
-
-    private final LoaderCallbacks<Cursor> mGroupMetadataCallbacks = new LoaderCallbacks<Cursor>() {
-
-        @Override
-        public CursorLoader onCreateLoader(int id, Bundle args) {
-            return new GroupMetaDataLoader(getContext(), mGroupUri);
-        }
-
-        @Override
-        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-            if (cursor == null || cursor.isClosed()) {
-                Log.e(TAG, "Failed to load group metadata");
-                return;
-            }
-            if (cursor.moveToNext()) {
-                final boolean deleted = cursor.getInt(GroupMetaDataLoader.DELETED) == 1;
-                if (!deleted) {
-                    mGroupMetadata = new GroupMetadata();
-                    mGroupMetadata.accountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
-                    mGroupMetadata.dataSet = cursor.getString(GroupMetaDataLoader.DATA_SET);
-                    mGroupMetadata.groupId = cursor.getLong(GroupMetaDataLoader.GROUP_ID);
-                    mGroupMetadata.groupName = cursor.getString(GroupMetaDataLoader.TITLE);
-                    mGroupMetadata.readOnly = cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1;
-
-                    final AccountTypeManager accountTypeManager =
-                            AccountTypeManager.getInstance(getContext());
-                    final AccountType accountType = accountTypeManager.getAccountType(
-                            mGroupMetadata.accountType, mGroupMetadata.dataSet);
-                    mGroupMetadata.editable = accountType.isGroupMembershipEditable();
-
-                    getLoaderManager().restartLoader(LOADER_GROUP_LIST_DETAILS, null,
-                            mGroupListDetailsCallbacks);
-                }
-            }
-        }
-
-        @Override
-        public void onLoaderReset(Loader<Cursor> loader) {}
-    };
-
-    private final LoaderCallbacks<Cursor> mGroupListDetailsCallbacks =
-            new LoaderCallbacks<Cursor>() {
-
-        @Override
-        public CursorLoader onCreateLoader(int id, Bundle args) {
-            final GroupListLoader groupListLoader = new GroupListLoader(getContext());
-
-            // TODO(wjang): modify GroupListLoader to accept this selection criteria more naturally
-            groupListLoader.setSelection(groupListLoader.getSelection()
-                    + " AND " + Groups._ID + "=?");
-
-            final String[] selectionArgs = new String[1];
-            selectionArgs[0] = Long.toString(mGroupMetadata.groupId);
-            groupListLoader.setSelectionArgs(selectionArgs);
-
-            return groupListLoader;
-        }
-
-        @Override
-        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-            if (cursor == null || cursor.isClosed()) {
-                Log.e(TAG, "Failed to load group list details");
-                return;
-            }
-            if (cursor.moveToNext()) {
-                mGroupMetadata.memberCount = cursor.getInt(GroupListLoader.MEMBER_COUNT);
-            }
-
-            onGroupMetadataLoaded();
-        }
-
-        @Override
-        public void onLoaderReset(Loader<Cursor> loader) {}
-    };
-
-    private static final class GroupMetadata implements Parcelable {
-
-        public static final Creator<GroupMetadata> CREATOR = new Creator<GroupMetadata>() {
-
-            public GroupMetadata createFromParcel(Parcel in) {
-                return new GroupMetadata(in);
-            }
-
-            public GroupMetadata[] newArray(int size) {
-                return new GroupMetadata[size];
-            }
-        };
-
-        String accountType;
-        String dataSet;
-        long groupId;
-        String groupName;
-        boolean readOnly;
-        boolean editable;
-        int memberCount = -1;
-
-        GroupMetadata() {
-        }
-
-        GroupMetadata(Parcel source) {
-            readFromParcel(source);
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel dest, int flags) {
-            dest.writeString(accountType);
-            dest.writeString(dataSet);
-            dest.writeLong(groupId);
-            dest.writeString(groupName);
-            dest.writeInt(readOnly ? 1 : 0);
-            dest.writeInt(editable ? 1 : 0);
-            dest.writeInt(memberCount);
-        }
-
-        private void readFromParcel(Parcel source) {
-            accountType = source.readString();
-            dataSet = source.readString();
-            groupId = source.readLong();
-            groupName = source.readString();
-            readOnly = source.readInt() == 1;
-            editable = source.readInt() == 1;
-            memberCount = source.readInt();
-        }
-
-        @Override
-        public String toString() {
-            return "GroupMetadata[accountType=" + accountType +
-                    " dataSet=" + dataSet +
-                    " groupId=" + groupId +
-                    " groupName=" + groupName +
-                    " readOnly=" + readOnly +
-                    " editable=" + editable +
-                    " memberCount=" + memberCount +
-                    "]";
-        }
-    }
+    private static final String ARG_GROUP_METADATA = "groupMetadata";
 
     /** Callbacks for hosts of {@link GroupMembersListFragment}. */
-    public interface GroupMembersListCallbacks {
-
-        /** Invoked when the user hits back in the action bar. */
-        void onHomePressed();
-
-        /** Invoked after group metadata has been loaded. */
-        void onGroupNameLoaded(String groupName);
+    public interface GroupMembersListListener {
 
         /** Invoked when a group member in the list is clicked. */
-        void onGroupMemberClicked(Uri contactLookupUri);
-
-        /** Invoked when a user chooses ot edit the group whose members are being displayed. */
-        void onEditGroup(Uri groupUri);
+        void onGroupMemberListItemClicked(Uri contactLookupUri);
     }
 
-    private Uri mGroupUri;
-
-    private GroupMembersListCallbacks mCallbacks;
+    private GroupMembersListListener mListener;
 
     private GroupMetadata mGroupMetadata;
 
+    public static GroupMembersListFragment newInstance(GroupMetadata groupMetadata) {
+        final Bundle args = new Bundle();
+        args.putParcelable(ARG_GROUP_METADATA, groupMetadata);
+
+        final GroupMembersListFragment fragment = new GroupMembersListFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
     public GroupMembersListFragment() {
         setHasOptionsMenu(true);
 
@@ -226,105 +63,53 @@
         setQuickContactEnabled(false);
     }
 
-    /** Sets the Uri of the group whose members will be displayed. */
-    public void setGroupUri(Uri groupUri) {
-        mGroupUri = groupUri;
-    }
-
-    /** Sets a listener for group member click events. */
-    public void setCallbacks(GroupMembersListCallbacks callbacks) {
-        mCallbacks = callbacks;
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        try {
+            mListener = (GroupMembersListListener) getActivity();
+        } catch (ClassCastException e) {
+            throw new ClassCastException(getActivity() + " must implement " +
+                    GroupMembersListListener.class.getSimpleName());
+        }
     }
 
     @Override
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
-        if (savedState != null) {
-            mGroupUri = savedState.getParcelable(KEY_GROUP_URI);
+        if (savedState == null) {
+            mGroupMetadata = getArguments().getParcelable(ARG_GROUP_METADATA);
+        } else {
             mGroupMetadata = savedState.getParcelable(KEY_GROUP_METADATA);
         }
+
+        // Don't attach the multi select check box listener if we can't edit the group
+        if (mGroupMetadata.editable) {
+            try {
+                setCheckBoxListListener((OnCheckBoxListActionListener) getActivity());
+            } catch (ClassCastException e) {
+                throw new ClassCastException(getActivity() + " must implement " +
+                        OnCheckBoxListActionListener.class.getSimpleName());
+            }
+        }
+    }
+
+    @Override
+    public View onCreateView (LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        final View view = super.onCreateView(inflater, container, savedInstanceState);
+        bindMembersCount(view);
+        return view;
     }
 
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-        outState.putParcelable(KEY_GROUP_URI, mGroupUri);
         outState.putParcelable(KEY_GROUP_METADATA, mGroupMetadata);
     }
 
-    @Override
-    protected void startLoading() {
-        if (mGroupMetadata == null) {
-            getLoaderManager().restartLoader(LOADER_GROUP_METADATA, null, mGroupMetadataCallbacks);
-        } else {
-            onGroupMetadataLoaded();
-        }
-    }
-
-    @Override
-    public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
-        inflater.inflate(R.menu.view_group, menu);
-    }
-
-    @Override
-    public void onPrepareOptionsMenu(Menu menu) {
-        final MenuItem editMenu = menu.findItem(R.id.menu_edit_group);
-        editMenu.setVisible(isGroupEditable());
-
-        final MenuItem deleteMenu = menu.findItem(R.id.menu_delete_group);
-        deleteMenu.setVisible(isGroupDeletable());
-    }
-
-    private boolean isGroupEditable() {
-        return mGroupUri != null && mGroupMetadata != null && mGroupMetadata.editable;
-    }
-
-    private boolean isGroupDeletable() {
-        return mGroupUri != null && mGroupMetadata != null && !mGroupMetadata.readOnly;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home: {
-                if (mCallbacks != null) {
-                    mCallbacks.onHomePressed();
-                }
-                return true;
-            }
-            case R.id.menu_edit_group: {
-                if (mCallbacks != null) {
-                    mCallbacks.onEditGroup(mGroupUri);
-                }
-                break;
-            }
-            case R.id.menu_delete_group: {
-                GroupDeletionDialogFragment.show(getFragmentManager(), mGroupMetadata.groupId,
-                        mGroupMetadata.groupName, /* endActivity */ true);
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void onGroupMetadataLoaded() {
-        final Activity activity = getActivity();
-        if (activity != null) activity.invalidateOptionsMenu();
-
-        // Set the title
-        if (mCallbacks != null) {
-            mCallbacks.onGroupNameLoaded(mGroupMetadata.groupName);
-        }
-
-        // Set the header
-        bindMembersCount();
-
-        // Start loading the group members
-        super.startLoading();
-    }
-
-    private void bindMembersCount() {
-        final View accountFilterContainer = getView().findViewById(
+    private void bindMembersCount(View view) {
+        final View accountFilterContainer = view.findViewById(
                 R.id.account_filter_header_container);
         if (mGroupMetadata.memberCount >= 0) {
             accountFilterContainer.setVisibility(View.VISIBLE);
@@ -348,11 +133,14 @@
     }
 
     @Override
+    public GroupMembersListAdapter getAdapter() {
+        return (GroupMembersListAdapter) super.getAdapter();
+    }
+
+    @Override
     protected void configureAdapter() {
         super.configureAdapter();
-        if (mGroupMetadata != null) {
-            getAdapter().setGroupId(mGroupMetadata.groupId);
-        }
+        getAdapter().setGroupId(mGroupMetadata.groupId);
     }
 
     @Override
@@ -362,9 +150,17 @@
 
     @Override
     protected void onItemClick(int position, long id) {
-        if (mCallbacks != null) {
+        final Uri uri = getAdapter().getContactUri(position);
+        if (uri == null) {
+            return;
+        }
+        if (getAdapter().isDisplayingCheckBoxes()) {
+            super.onItemClick(position, id);
+            return;
+        }
+        if (mListener != null) {
             final Uri contactLookupUri = getAdapter().getContactLookupUri(position);
-            mCallbacks.onGroupMemberClicked(contactLookupUri);
+            mListener.onGroupMemberListItemClicked(contactLookupUri);
         }
     }
 }
diff --git a/src/com/android/contacts/group/GroupMetadata.java b/src/com/android/contacts/group/GroupMetadata.java
new file mode 100644
index 0000000..788c1d4
--- /dev/null
+++ b/src/com/android/contacts/group/GroupMetadata.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 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.group;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/** Meta data for a contact group. */
+// TODO(wjang): consolidate with com.android.contacts.common.GroupMetaData;
+public final class GroupMetadata implements Parcelable {
+
+    public static final Creator<GroupMetadata> CREATOR = new Creator<GroupMetadata>() {
+
+        public GroupMetadata createFromParcel(Parcel in) {
+            return new GroupMetadata(in);
+        }
+
+        public GroupMetadata[] newArray(int size) {
+            return new GroupMetadata[size];
+        }
+    };
+
+    // TODO(wjang): make them all final and add getters
+    public Uri uri;
+    public String accountName;
+    public String accountType;
+    public String dataSet;
+    public long groupId;
+    public String groupName;
+    public boolean readOnly;
+    public boolean editable;
+    public int memberCount = -1;
+
+    public GroupMetadata() {
+    }
+
+    public GroupMetadata(Parcel source) {
+        readFromParcel(source);
+    }
+
+    private void readFromParcel(Parcel source) {
+        uri = source.readParcelable(Uri.class.getClassLoader());
+        accountName = source.readString();
+        accountType = source.readString();
+        dataSet = source.readString();
+        groupId = source.readLong();
+        groupName = source.readString();
+        readOnly = source.readInt() == 1;
+        editable = source.readInt() == 1;
+        memberCount = source.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(uri, 0);
+        dest.writeString(accountName);
+        dest.writeString(accountType);
+        dest.writeString(dataSet);
+        dest.writeLong(groupId);
+        dest.writeString(groupName);
+        dest.writeInt(readOnly ? 1 : 0);
+        dest.writeInt(editable ? 1 : 0);
+        dest.writeInt(memberCount);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return "GroupMetadata[uri=" + uri +
+                " accountName=" + accountName +
+                " accountType=" + accountType +
+                " dataSet=" + dataSet +
+                " groupId=" + groupId +
+                " groupName=" + groupName +
+                " readOnly=" + readOnly +
+                " editable=" + editable +
+                " memberCount=" + memberCount +
+                "]";
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/group/GroupUtil.java b/src/com/android/contacts/group/GroupUtil.java
index 6d6fec1..f9f5007 100644
--- a/src/com/android/contacts/group/GroupUtil.java
+++ b/src/com/android/contacts/group/GroupUtil.java
@@ -24,7 +24,6 @@
 import android.provider.ContactsContract.Groups;
 
 import com.android.contacts.GroupListLoader;
-import com.android.contacts.activities.GroupEditorActivity;
 import com.android.contacts.activities.GroupMembersActivity;
 import com.google.common.base.Objects;
 
@@ -33,6 +32,9 @@
  */
 public final class GroupUtil {
 
+    private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
+    private static final String LEGACY_CONTACTS_URI = "content://contacts/groups";
+
     private GroupUtil() {
     }
 
@@ -71,18 +73,40 @@
 
     /** Returns an Intent to create a new group. */
     public static Intent createAddGroupIntent(Context context) {
-        final Intent intent = new Intent(context, GroupEditorActivity.class);
+        final Intent intent = new Intent(context, GroupMembersActivity.class);
         intent.setAction(Intent.ACTION_INSERT);
         return intent;
     }
 
-    /** Returns an Intent to view the details of the group identified by the given Uri. */
+    /** Returns an Intent to view the details of the group identified by the given ID. */
     public static Intent createViewGroupIntent(Context context, long groupId) {
+        return createViewGroupIntent(context, getGroupUriFromId(groupId));
+    }
+
+    /** Returns an Intent to view the details of the group identified by the given Uri. */
+    public static Intent createViewGroupIntent(Context context, Uri uri) {
         final Intent intent = new Intent(context, GroupMembersActivity.class);
-        intent.setData(getGroupUriFromId(groupId));
+        intent.setAction(Intent.ACTION_VIEW);
+        // TODO(wjang): do we still need it?
+        // intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.setData(uri);
         return intent;
     }
 
+    /**
+     * Converts the given group Uri to the legacy format if the legacy authority was specified
+     * in the given Uri.
+     */
+    public static Uri maybeConvertToLegacyUri(Uri groupUri) {
+        final String requestAuthority = groupUri.getAuthority();
+        if (!LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) {
+            return groupUri;
+        }
+        final long groupId = ContentUris.parseId(groupUri);
+        final Uri legacyContentUri = Uri.parse(LEGACY_CONTACTS_URI);
+        return ContentUris.withAppendedId(legacyContentUri, groupId);
+    }
+
     /** TODO: Make it private after {@link GroupBrowseListAdapter} is removed. */
     static Uri getGroupUriFromId(long groupId) {
         return ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
index 4b4b326..ea55333 100644
--- a/src/com/android/contacts/list/ContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -36,7 +36,6 @@
 
 import com.android.common.widget.CompositeCursorAdapter.Partition;
 import com.android.contacts.common.list.AutoScrollListView;
-import com.android.contacts.common.list.ContactEntryListFragment;
 import com.android.contacts.common.list.ContactListAdapter;
 import com.android.contacts.common.list.ContactListFilter;
 import com.android.contacts.common.list.DirectoryPartition;
@@ -49,7 +48,7 @@
  * picking a contact with one of the PICK intents).
  */
 public abstract class ContactBrowseListFragment extends
-        ContactEntryListFragment<ContactListAdapter> {
+        MultiSelectContactsListFragment<ContactListAdapter> {
 
     private static final String TAG = "ContactList";
 
@@ -383,6 +382,11 @@
     }
 
     @Override
+    public ContactListAdapter getAdapter() {
+        return (ContactListAdapter) super.getAdapter();
+    }
+
+    @Override
     protected void configureAdapter() {
         super.configureAdapter();
 
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 672e63e..97bb86a 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -39,8 +39,6 @@
  * picking a contact with one of the PICK intents).
  */
 public class DefaultContactBrowseListFragment extends ContactBrowseListFragment {
-    private static final String TAG = DefaultContactBrowseListFragment.class.getSimpleName();
-
     private View mSearchHeaderView;
     private View mSearchProgress;
     private TextView mSearchProgressText;
@@ -65,6 +63,10 @@
         if (uri == null) {
             return;
         }
+        if (getAdapter().isDisplayingCheckBoxes()) {
+            super.onItemClick(position, id);
+            return;
+        }
         viewContact(uri, getAdapter().isEnterpriseContact(position));
     }
 
@@ -147,4 +149,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/list/MultiSelectContactsListFragment.java b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
index 8dd18c1..53f5a74 100644
--- a/src/com/android/contacts/list/MultiSelectContactsListFragment.java
+++ b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
@@ -16,18 +16,16 @@
 
 package com.android.contacts.list;
 
-import com.android.contacts.common.list.ContactListAdapter;
-import com.android.contacts.common.list.ContactListItemView;
-import com.android.contacts.common.list.DefaultContactListAdapter;
+import com.android.contacts.common.list.ContactEntryListFragment;
+import com.android.contacts.common.list.MultiSelectEntryContactListAdapter;
+import com.android.contacts.common.list.MultiSelectEntryContactListAdapter.SelectedContactsListener;
 import com.android.contacts.common.logging.SearchState;
-import com.android.contacts.list.MultiSelectEntryContactListAdapter.SelectedContactsListener;
 import com.android.contacts.common.logging.Logger;
 
 import android.database.Cursor;
-import android.net.Uri;
 import android.os.Bundle;
 import android.provider.ContactsContract;
-import android.text.TextUtils;
+import android.util.Log;
 import android.view.accessibility.AccessibilityEvent;
 
 import java.util.ArrayList;
@@ -38,9 +36,12 @@
  * Fragment containing a contact list used for browsing contacts and optionally selecting
  * multiple contacts via checkboxes.
  */
-public class MultiSelectContactsListFragment extends DefaultContactBrowseListFragment
+public abstract class MultiSelectContactsListFragment<T extends MultiSelectEntryContactListAdapter>
+        extends ContactEntryListFragment<T>
         implements SelectedContactsListener {
 
+    private static final String TAG = "MultiContactsList";
+
     public interface OnCheckBoxListActionListener {
         void onStartDisplayingCheckBoxes();
         void onSelectedContactIdsChanged();
@@ -60,7 +61,7 @@
 
     /**
      * Whether a search result was clicked by the user. Tracked so that we can distinguish
-     * between exiting the search mode after a result was clicked from existing w/o clicking
+     * between exiting the search mode after a result was clicked from exiting w/o clicking
      * any search result.
      */
     public boolean wasSearchResultClicked() {
@@ -76,9 +77,7 @@
 
     @Override
     public void onSelectedContactsChanged() {
-        if (mCheckBoxListListener != null) {
-            mCheckBoxListListener.onSelectedContactIdsChanged();
-        }
+        if (mCheckBoxListListener != null) mCheckBoxListListener.onSelectedContactIdsChanged();
     }
 
     @Override
@@ -106,13 +105,11 @@
     }
 
     public TreeSet<Long> getSelectedContactIds() {
-        final MultiSelectEntryContactListAdapter adapter = getAdapter();
-        return adapter.getSelectedContactIds();
+        return getAdapter().getSelectedContactIds();
     }
 
-    @Override
-    public MultiSelectEntryContactListAdapter getAdapter() {
-        return (MultiSelectEntryContactListAdapter) super.getAdapter();
+    public long[] getSelectedContactIdsArray() {
+        return getAdapter().getSelectedContactIdsArray();
     }
 
     @Override
@@ -129,9 +126,11 @@
     }
 
     public void displayCheckBoxes(boolean displayCheckBoxes) {
-        getAdapter().setDisplayCheckBoxes(displayCheckBoxes);
-        if (!displayCheckBoxes) {
-            clearCheckBoxes();
+        if (getAdapter() != null) {
+            getAdapter().setDisplayCheckBoxes(displayCheckBoxes);
+            if (!displayCheckBoxes) {
+                clearCheckBoxes();
+            }
         }
     }
 
@@ -142,23 +141,20 @@
     @Override
     protected boolean onItemLongClick(int position, long id) {
         final int previouslySelectedCount = getAdapter().getSelectedContactIds().size();
-        final Uri uri = getAdapter().getContactUri(position);
+        final long contactId = getContactId(position);
         final int partition = getAdapter().getPartitionForPosition(position);
-        if (uri != null && partition == ContactsContract.Directory.DEFAULT) {
-            final String contactId = uri.getLastPathSegment();
-            if (!TextUtils.isEmpty(contactId)) {
-                if (mCheckBoxListListener != null) {
-                    mCheckBoxListListener.onStartDisplayingCheckBoxes();
-                }
-                getAdapter().toggleSelectionOfContactId(Long.valueOf(contactId));
-                // Manually send clicked event if there is a checkbox.
-                // See b/24098561.  TalkBack will not read it otherwise.
-                final int index = position + getListView().getHeaderViewsCount() - getListView()
-                        .getFirstVisiblePosition();
-                if (index >= 0 && index < getListView().getChildCount()) {
-                    getListView().getChildAt(index).sendAccessibilityEvent(AccessibilityEvent
-                            .TYPE_VIEW_CLICKED);
-                }
+        if (contactId >= 0 && partition == ContactsContract.Directory.DEFAULT) {
+            if (mCheckBoxListListener != null) {
+                mCheckBoxListListener.onStartDisplayingCheckBoxes();
+            }
+            getAdapter().toggleSelectionOfContactId(contactId);
+            // Manually send clicked event if there is a checkbox.
+            // See b/24098561.  TalkBack will not read it otherwise.
+            final int index = position + getListView().getHeaderViewsCount() - getListView()
+                    .getFirstVisiblePosition();
+            if (index >= 0 && index < getListView().getChildCount()) {
+                getListView().getChildAt(index).sendAccessibilityEvent(AccessibilityEvent
+                        .TYPE_VIEW_CLICKED);
             }
         }
         final int nowSelectedCount = getAdapter().getSelectedContactIds().size();
@@ -172,27 +168,37 @@
 
     @Override
     protected void onItemClick(int position, long id) {
-        final Uri uri = getAdapter().getContactUri(position);
-        if (uri == null) {
+        final long contactId = getContactId(position);
+        if (contactId < 0) {
             return;
         }
         if (getAdapter().isDisplayingCheckBoxes()) {
-            final String contactId = uri.getLastPathSegment();
-            if (!TextUtils.isEmpty(contactId)) {
-                getAdapter().toggleSelectionOfContactId(Long.valueOf(contactId));
-            }
+            getAdapter().toggleSelectionOfContactId(contactId);
         } else {
             if (isSearchMode()) {
                 mSearchResultClicked = true;
                 Logger.logSearchEvent(createSearchStateForSearchResultClick(position));
             }
-            super.onItemClick(position, id);
         }
         if (mCheckBoxListListener != null && getAdapter().getSelectedContactIds().size() == 0) {
             mCheckBoxListListener.onStopDisplayingCheckBoxes();
         }
     }
 
+    private long getContactId(int position) {
+        final int contactIdColumnIndex = getAdapter().getContactColumnIdIndex();
+
+        final Cursor cursor = (Cursor) getAdapter().getItem(position);
+        if (cursor != null) {
+            if (cursor.getColumnCount() > contactIdColumnIndex) {
+                return cursor.getLong(contactIdColumnIndex);
+            }
+        }
+
+        Log.w(TAG, "Failed to get contact ID from cursor column " + contactIdColumnIndex);
+        return -1;
+    }
+
     /**
      * Returns the state of the search results currently presented to the user.
      */
@@ -259,14 +265,4 @@
         }
         return searchState;
     }
-
-    @Override
-    protected ContactListAdapter createListAdapter() {
-        DefaultContactListAdapter adapter = new MultiSelectEntryContactListAdapter(getContext());
-        adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled());
-        adapter.setDisplayPhotos(true);
-        adapter.setPhotoPosition(
-                ContactListItemView.getDefaultPhotoPosition(/* opposite = */ false));
-        return adapter;
-    }
 }
diff --git a/src/com/android/contacts/list/MultiSelectEntryContactListAdapter.java b/src/com/android/contacts/list/MultiSelectEntryContactListAdapter.java
deleted file mode 100644
index 5a54c51..0000000
--- a/src/com/android/contacts/list/MultiSelectEntryContactListAdapter.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.contacts.list;
-
-import com.android.contacts.common.list.ContactListItemView;
-import com.android.contacts.common.list.DefaultContactListAdapter;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.provider.ContactsContract;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.CheckBox;
-
-import java.util.TreeSet;
-
-/**
- * An extension of the default contact adapter that adds checkboxes and the ability
- * to select multiple contacts.
- */
-public class MultiSelectEntryContactListAdapter extends DefaultContactListAdapter {
-
-    private SelectedContactsListener mSelectedContactsListener;
-    private TreeSet<Long> mSelectedContactIds = new TreeSet<Long>();
-    private boolean mDisplayCheckBoxes;
-
-    public interface SelectedContactsListener {
-        void onSelectedContactsChanged();
-        void onSelectedContactsChangedViaCheckBox();
-    }
-
-    public MultiSelectEntryContactListAdapter(Context context) {
-        super(context);
-    }
-
-    public void setSelectedContactsListener(SelectedContactsListener listener) {
-        mSelectedContactsListener = listener;
-    }
-
-    /**
-     * Returns set of selected contacts.
-     */
-    public TreeSet<Long> getSelectedContactIds() {
-        return mSelectedContactIds;
-    }
-
-    /**
-     * Update set of selected contacts. This changes which checkboxes are set.
-     */
-    public void setSelectedContactIds(TreeSet<Long> selectedContactIds) {
-        this.mSelectedContactIds = selectedContactIds;
-        notifyDataSetChanged();
-        if (mSelectedContactsListener != null) {
-            mSelectedContactsListener.onSelectedContactsChanged();
-        }
-    }
-
-    /**
-     * Shows checkboxes beside contacts if {@param displayCheckBoxes} is {@code TRUE}.
-     * Not guaranteed to work with all configurations of this adapter.
-     */
-    public void setDisplayCheckBoxes(boolean showCheckBoxes) {
-        if (!mDisplayCheckBoxes && showCheckBoxes) {
-            setSelectedContactIds(new TreeSet<Long>());
-        }
-        mDisplayCheckBoxes = showCheckBoxes;
-        notifyDataSetChanged();
-        if (mSelectedContactsListener != null) {
-            mSelectedContactsListener.onSelectedContactsChanged();
-        }
-    }
-
-    /**
-     * Checkboxes are being displayed beside contacts.
-     */
-    public boolean isDisplayingCheckBoxes() {
-        return mDisplayCheckBoxes;
-    }
-
-    /**
-     * Toggle the checkbox beside the contact for {@param contactId}.
-     */
-    public void toggleSelectionOfContactId(long contactId) {
-        if (mSelectedContactIds.contains(contactId)) {
-            mSelectedContactIds.remove(contactId);
-        } else {
-            mSelectedContactIds.add(contactId);
-        }
-        notifyDataSetChanged();
-        if (mSelectedContactsListener != null) {
-            mSelectedContactsListener.onSelectedContactsChanged();
-        }
-    }
-
-    @Override
-    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
-        super.bindView(itemView, partition, cursor, position);
-        final ContactListItemView view = (ContactListItemView) itemView;
-        bindCheckBox(view, cursor, position, partition == ContactsContract.Directory.DEFAULT);
-    }
-
-    private void bindCheckBox(ContactListItemView view, Cursor cursor, int position,
-            boolean isLocalDirectory) {
-        // Disable clicking on all contacts from remote directories when showing check boxes. We do
-        // this by telling the view to handle clicking itself.
-        view.setClickable(!isLocalDirectory && mDisplayCheckBoxes);
-        // Only show checkboxes if mDisplayCheckBoxes is enabled. Also, never show the
-        // checkbox for other directory contacts except local directory.
-        if (!mDisplayCheckBoxes || !isLocalDirectory) {
-            view.hideCheckBox();
-            return;
-        }
-        final CheckBox checkBox = view.getCheckBox();
-        final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
-        checkBox.setChecked(mSelectedContactIds.contains(contactId));
-        checkBox.setTag(contactId);
-        checkBox.setOnClickListener(mCheckBoxClickListener);
-    }
-
-    private final OnClickListener mCheckBoxClickListener = new OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            final CheckBox checkBox = (CheckBox) v;
-            final Long contactId = (Long) checkBox.getTag();
-            if (checkBox.isChecked()) {
-                mSelectedContactIds.add(contactId);
-            } else {
-                mSelectedContactIds.remove(contactId);
-            }
-            notifyDataSetChanged();
-            if (mSelectedContactsListener != null) {
-                mSelectedContactsListener.onSelectedContactsChangedViaCheckBox();
-            }
-        }
-    };
-}
diff --git a/src/com/android/contacts/widget/NoSwipeViewPager.java b/src/com/android/contacts/widget/NoSwipeViewPager.java
new file mode 100644
index 0000000..b24df39
--- /dev/null
+++ b/src/com/android/contacts/widget/NoSwipeViewPager.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.widget;
+
+import android.content.Context;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * ViewPager with swipe disabled.
+ */
+public class NoSwipeViewPager extends ViewPager {
+
+    public NoSwipeViewPager(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        return false;
+    }
+}
\ No newline at end of file