Add group member selection to ContentSelectionActivity (1/2)

* The GroupMemberPickListAdapter queries RawContacts to get
  the list of potential group members
* The raw contact IDs of existing group members are passed
  in to the pick fragment and these are filtered out of
  results by a CursorWrapper.
* Since we can't get the contact photo ID and contact lookup
  URI from the RawContacts query, these are also provided
  by the CursorWrapper.
* Finally, we move the current search to add contact to group
  functionality in place behind the ActionBar search icon,
  moved to the overflow menu.

Bug 28707265
Bug 18641067

Change-Id: Ifde5446e8ce9c0ed27cd2f98fd704ca669c45f59
diff --git a/res/menu/view_group.xml b/res/menu/view_group.xml
index cf945fd..2fc36be 100644
--- a/res/menu/view_group.xml
+++ b/res/menu/view_group.xml
@@ -24,6 +24,11 @@
         contacts:showAsAction="ifRoom" />
 
     <item
+        android:id="@+id/menu_search"
+        android:icon="@drawable/ic_ab_search"
+        android:title="@string/menu_search" />
+
+    <item
         android:id="@+id/menu_rename_group"
         android:title="@string/menu_renameGroup"/>
 
diff --git a/src/com/android/contacts/GroupMemberLoader.java b/src/com/android/contacts/GroupMemberLoader.java
index 43a6427..9f55848 100644
--- a/src/com/android/contacts/GroupMemberLoader.java
+++ b/src/com/android/contacts/GroupMemberLoader.java
@@ -80,14 +80,6 @@
         return new GroupMemberLoader(context, groupId, GroupEditorQuery.PROJECTION);
     }
 
-    /**
-     * @return GroupMemberLoader object used in group detail page.
-     */
-    public static GroupMemberLoader constructLoaderForGroupDetailQuery(
-            Context context, long groupId) {
-        return new GroupMemberLoader(context, groupId, GroupDetailQuery.PROJECTION);
-    }
-
     private GroupMemberLoader(Context context, long groupId, String[] projection) {
         super(context);
         mGroupId = groupId;
diff --git a/src/com/android/contacts/activities/ContactSelectionActivity.java b/src/com/android/contacts/activities/ContactSelectionActivity.java
index 50f50dd..80a4acb 100644
--- a/src/com/android/contacts/activities/ContactSelectionActivity.java
+++ b/src/com/android/contacts/activities/ContactSelectionActivity.java
@@ -18,7 +18,6 @@
 
 import android.app.ActionBar;
 import android.app.ActionBar.LayoutParams;
-import android.app.Activity;
 import android.app.Fragment;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
@@ -26,7 +25,6 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Intents.Insert;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -46,6 +44,8 @@
 import com.android.contacts.R;
 import com.android.contacts.common.activity.RequestPermissionsActivity;
 import com.android.contacts.common.list.ContactEntryListFragment;
+import com.android.contacts.list.GroupMemberPickerFragment;
+import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.editor.EditorIntents;
 import com.android.contacts.list.ContactPickerFragment;
 import com.android.contacts.list.ContactsIntentResolver;
@@ -62,7 +62,7 @@
 import com.android.contacts.common.list.PhoneNumberPickerFragment;
 import com.android.contacts.list.PostalAddressPickerFragment;
 
-import java.util.Set;
+import java.util.ArrayList;
 
 /**
  * Displays a list of contacts (or phone numbers or postal addresses) for the
@@ -139,9 +139,10 @@
                 .inflate(R.layout.custom_action_bar, null);
         mSearchView = (SearchView) mSearchViewContainer.findViewById(R.id.search_view);
 
-        // Postal address pickers (and legacy pickers) don't support search, so just show
+        // Postal address  group member,and legacy pickers don't support search, so just show
         // "HomeAsUp" button and title.
         if (mRequest.getActionCode() == ContactsRequest.ACTION_PICK_POSTAL ||
+                mRequest.getActionCode() == ContactsRequest.ACTION_PICK_GROUP_MEMBERS ||
                 mRequest.isLegacyCompatibilityMode()) {
             mSearchView.setVisibility(View.GONE);
             if (actionBar != null) {
@@ -270,6 +271,11 @@
                 setTitle(R.string.titleJoinContactDataWith);
                 break;
             }
+
+            case ContactsRequest.ACTION_PICK_GROUP_MEMBERS: {
+                setTitle(R.string.contactPickerActivityTitle);
+                break;
+            }
         }
     }
 
@@ -350,6 +356,15 @@
                 break;
             }
 
+            case ContactsRequest.ACTION_PICK_GROUP_MEMBERS: {
+                final AccountWithDataSet account = getIntent().getParcelableExtra(
+                        UiIntentActions.GROUP_ACCOUNT_WITH_DATA_SET);
+                final ArrayList<String> rawContactIds = getIntent().getStringArrayListExtra(
+                        UiIntentActions.GROUP_RAW_CONTACT_IDS);
+                mListFragment = GroupMemberPickerFragment.newInstance(account, rawContactIds);
+                break;
+            }
+
             default:
                 throw new IllegalStateException("Invalid action code: " + mActionCode);
         }
@@ -389,6 +404,9 @@
         } else if (mListFragment instanceof JoinContactListFragment) {
             ((JoinContactListFragment) mListFragment).setOnContactPickerActionListener(
                     new JoinContactActionListener());
+        } else if (mListFragment instanceof GroupMemberPickerFragment) {
+            ((GroupMemberPickerFragment) mListFragment).setListener(
+                    new GroupMemberPickerListener());
         } else {
             throw new IllegalStateException("Unsupported list fragment type: " + mListFragment);
         }
@@ -462,6 +480,14 @@
         }
     }
 
+    private final class GroupMemberPickerListener implements GroupMemberPickerFragment.Listener {
+
+        @Override
+        public void onGroupMemberClicked(Uri uri) {
+            returnPickerResult(uri);
+        }
+    }
+
     private final class PostalAddressPickerActionListener implements
             OnPostalAddressPickerActionListener {
         @Override
diff --git a/src/com/android/contacts/activities/GroupMembersActivity.java b/src/com/android/contacts/activities/GroupMembersActivity.java
index c8d2c19..f41972e 100644
--- a/src/com/android/contacts/activities/GroupMembersActivity.java
+++ b/src/com/android/contacts/activities/GroupMembersActivity.java
@@ -24,6 +24,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Intents;
 import android.support.v7.widget.Toolbar;
 import android.util.Log;
@@ -41,13 +42,14 @@
 import com.android.contacts.GroupMemberLoader.GroupEditorQuery;
 import com.android.contacts.R;
 import com.android.contacts.common.editor.SelectAccountDialogFragment;
-import com.android.contacts.common.logging.Logger;
 import com.android.contacts.common.logging.ListEvent;
+import com.android.contacts.common.logging.Logger;
 import com.android.contacts.common.logging.ScreenEvent.ScreenType;
 import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contacts.group.GroupMembersListAdapter.GroupMembersQuery;
 import com.android.contacts.group.GroupMembersListFragment;
 import com.android.contacts.group.GroupMetadata;
 import com.android.contacts.group.GroupNameEditDialogFragment;
@@ -57,6 +59,7 @@
 import com.android.contacts.interactions.GroupDeletionDialogFragment;
 import com.android.contacts.list.ContactsRequest;
 import com.android.contacts.list.MultiSelectContactsListFragment;
+import com.android.contacts.list.UiIntentActions;
 import com.android.contacts.quickcontact.QuickContactActivity;
 
 import java.util.ArrayList;
@@ -90,6 +93,8 @@
     private static final String ACTION_ADD_TO_GROUP = "addToGroup";
     private static final String ACTION_REMOVE_FROM_GROUP = "removeFromGroup";
 
+    private static final int RESULT_GROUP_ADD_MEMBER = 100;
+
     /** Loader callbacks for existing group members for the autocomplete text view. */
     private final LoaderCallbacks<Cursor> mGroupMemberCallbacks = new LoaderCallbacks<Cursor>() {
 
@@ -312,7 +317,10 @@
         final boolean isGroupReadOnly = mGroupMetadata != null && mGroupMetadata.readOnly;
 
         setVisible(menu, R.id.menu_add,
-                isGroupEditable &&!isSelectionMode && !isSearchMode);
+                isGroupEditable && !isSelectionMode && !isSearchMode);
+
+        setVisible(menu, R.id.menu_search,
+                isGroupEditable && !isSelectionMode && !isSearchMode);
 
         setVisible(menu, R.id.menu_rename_group,
                 isGroupEditable && !isSelectionMode && !isSearchMode);
@@ -341,6 +349,16 @@
                 return true;
             }
             case R.id.menu_add: {
+                final Intent intent = new Intent(Intent.ACTION_PICK);
+                intent.setType(ContactsContract.Groups.CONTENT_ITEM_TYPE);
+                intent.putExtra(UiIntentActions.GROUP_ACCOUNT_WITH_DATA_SET,
+                        mGroupMetadata.createAccountWithDataSet());
+                intent.putExtra(UiIntentActions.GROUP_RAW_CONTACT_IDS,
+                        getExistingGroupMemberRawContactIds());
+                startActivityForResult(intent, RESULT_GROUP_ADD_MEMBER);
+                return true;
+            }
+            case R.id.menu_search: {
                 if (mActionBarAdapter != null) {
                     mActionBarAdapter.setSearchMode(true);
                 }
@@ -367,6 +385,17 @@
         return super.onOptionsItemSelected(item);
     }
 
+    private ArrayList<String> getExistingGroupMemberRawContactIds() {
+        final ArrayList<String> rawContactIds = new ArrayList<>();
+        final Cursor cursor = mMembersListFragment.getAdapter().getCursor(/* partition */ 0);
+        if (cursor != null && cursor.moveToFirst()) {
+            do {
+                rawContactIds.add(cursor.getString(GroupMembersQuery.RAW_CONTACT_ID));
+            } while (cursor.moveToNext());
+        }
+        return rawContactIds;
+    }
+
     private void deleteGroup() {
         if (mGroupMetadata.memberCount == 0) {
             final Intent intent = ContactSaveService.createGroupDeletionIntent(
@@ -417,6 +446,33 @@
         }
     }
 
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == RESULT_GROUP_ADD_MEMBER && resultCode == RESULT_OK && data != null) {
+            final Uri rawContactUri = data.getData();
+            if (rawContactUri != null) {
+                long rawContactId = -1;
+                try {
+                    rawContactId = Long.parseLong(rawContactUri.getLastPathSegment());
+                } catch (NumberFormatException ignored) {}
+                if (rawContactId < 0) {
+                    Toast.makeText(this, R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
+                    Log.w(TAG, "Failed to parse ID from pick group member result uri " +
+                            rawContactUri);
+                    return;
+                }
+                final long[] rawContactIdsToAdd = new long[1];
+                rawContactIdsToAdd[0] = rawContactId;
+                final Intent intent = ContactSaveService.createGroupUpdateIntent(
+                        GroupMembersActivity.this, mGroupMetadata.groupId, /* newLabel */ null,
+                        rawContactIdsToAdd, /* rawContactIdsToRemove */ null,
+                        GroupMembersActivity.class, GroupMembersActivity.ACTION_ADD_TO_GROUP);
+                startService(intent);
+            }
+        }
+    }
+
     private boolean isSelectAccountDialogFound() {
         return getFragmentManager().findFragmentByTag(TAG_SELECT_ACCOUNT_DIALOG) != null;
     }
diff --git a/src/com/android/contacts/group/GroupMembersListAdapter.java b/src/com/android/contacts/group/GroupMembersListAdapter.java
index fe692e7..86e75b1 100644
--- a/src/com/android/contacts/group/GroupMembersListAdapter.java
+++ b/src/com/android/contacts/group/GroupMembersListAdapter.java
@@ -35,7 +35,7 @@
 /** Group members cursor adapter. */
 public class GroupMembersListAdapter extends MultiSelectEntryContactListAdapter {
 
-    private static class GroupMembersQuery {
+    public static class GroupMembersQuery {
 
         private static final String[] PROJECTION_PRIMARY = new String[] {
                 Data.CONTACT_ID,
diff --git a/src/com/android/contacts/group/GroupMembersListFragment.java b/src/com/android/contacts/group/GroupMembersListFragment.java
index 5c86d04..47eed0c 100644
--- a/src/com/android/contacts/group/GroupMembersListFragment.java
+++ b/src/com/android/contacts/group/GroupMembersListFragment.java
@@ -31,9 +31,9 @@
 import com.android.contacts.GroupListLoader;
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
+import com.android.contacts.common.logging.ListEvent.ListType;
 import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.account.AccountType;
-import com.android.contacts.common.logging.ListEvent.ListType;
 import com.android.contacts.list.MultiSelectContactsListFragment;
 
 /** Displays the members of a group. */
diff --git a/src/com/android/contacts/list/ContactsIntentResolver.java b/src/com/android/contacts/list/ContactsIntentResolver.java
index 259e0c7..ded72c2 100644
--- a/src/com/android/contacts/list/ContactsIntentResolver.java
+++ b/src/com/android/contacts/list/ContactsIntentResolver.java
@@ -28,11 +28,14 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.Intents.Insert;
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
 /**
  * Parses a Contacts intent, extracting all relevant parts and packaging them
  * as a {@link ContactsRequest} object.
@@ -89,6 +92,12 @@
                 request.setLegacyCompatibilityMode(true);
             } else if (Email.CONTENT_TYPE.equals(resolvedType)) {
                 request.setActionCode(ContactsRequest.ACTION_PICK_EMAIL);
+            } else if (Groups.CONTENT_ITEM_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_GROUP_MEMBERS);
+                request.setAccountWithDataSet(intent.<AccountWithDataSet> getParcelableExtra(
+                        UiIntentActions.GROUP_ACCOUNT_WITH_DATA_SET));
+                request.setRawContactIds(intent.getStringArrayListExtra(
+                        UiIntentActions.GROUP_RAW_CONTACT_IDS));
             }
         } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
             String component = intent.getComponent().getClassName();
diff --git a/src/com/android/contacts/list/ContactsRequest.java b/src/com/android/contacts/list/ContactsRequest.java
index a1428be..a686752 100644
--- a/src/com/android/contacts/list/ContactsRequest.java
+++ b/src/com/android/contacts/list/ContactsRequest.java
@@ -21,6 +21,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.ArrayList;
+
 /**
  * Parsed form of the intent sent to the Contacts application.
  */
@@ -38,6 +42,9 @@
     /** Show contents of a specific group */
     public static final int ACTION_GROUP = 20;
 
+    /** Show potential new members of a specific group */
+    public static final int ACTION_PICK_GROUP_MEMBERS = 21;
+
     /** Show all starred contacts */
     public static final int ACTION_STARRED = 30;
 
@@ -89,6 +96,8 @@
     private boolean mLegacyCompatibilityMode;
     private boolean mDirectorySearchEnabled = true;
     private Uri mContactUri;
+    private AccountWithDataSet mAccountWithDataSet;
+    private ArrayList<String> mRawContactIds;
 
     @Override
     public String toString() {
@@ -101,6 +110,8 @@
                 + " mLegacyCompatibilityMode=" + mLegacyCompatibilityMode
                 + " mDirectorySearchEnabled=" + mDirectorySearchEnabled
                 + " mContactUri=" + mContactUri
+                + " mAccountWithDataSet=" + mAccountWithDataSet
+                + " mRawContactIds=" + mRawContactIds
                 + "}";
     }
 
@@ -179,4 +190,20 @@
     public void setContactUri(Uri contactUri) {
         this.mContactUri = contactUri;
     }
+
+    public AccountWithDataSet getAccountWithDataSet() {
+        return mAccountWithDataSet;
+    }
+
+    public void setAccountWithDataSet(AccountWithDataSet accountWithDataSet) {
+        mAccountWithDataSet = accountWithDataSet;
+    }
+
+    public ArrayList<String> getRawContactIds() {
+        return mRawContactIds;
+    }
+
+    public void setRawContactIds(ArrayList<String> rawContactIds) {
+        mRawContactIds = rawContactIds;
+    }
 }
diff --git a/src/com/android/contacts/list/GroupMemberPickListAdapter.java b/src/com/android/contacts/list/GroupMemberPickListAdapter.java
new file mode 100644
index 0000000..7a1d355
--- /dev/null
+++ b/src/com/android/contacts/list/GroupMemberPickListAdapter.java
@@ -0,0 +1,178 @@
+/*
+ * 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.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.view.View;
+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.model.account.AccountWithDataSet;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Adapter for raw contacts owned by an account that are not already members of a given group.
+ */
+public class GroupMemberPickListAdapter extends ContactEntryListAdapter {
+
+    static class GroupMembersQuery {
+
+        private static final String[] PROJECTION_PRIMARY = new String[] {
+                RawContacts._ID,                        // 0
+                RawContacts.CONTACT_ID,                 // 1
+                RawContacts.DISPLAY_NAME_PRIMARY,       // 2
+                // Dummy columns overwritten by the cursor wrapper
+                RawContacts.SYNC1,                      // 3
+                RawContacts.SYNC2                       // 4
+        };
+
+        private static final String[] PROJECTION_ALTERNATIVE = new String[] {
+                RawContacts._ID,                        // 0
+                RawContacts.CONTACT_ID,                 // 1
+                RawContacts.DISPLAY_NAME_ALTERNATIVE,   // 2
+                // Dummy columns overwritten by the cursor wrapper
+                RawContacts.SYNC1,                      // 3
+                RawContacts.SYNC2                       // 4
+        };
+
+        static final int RAW_CONTACT_ID = 0;
+        static final int CONTACT_ID = 1;
+        static final int CONTACT_DISPLAY_NAME = 2;
+        // Provided by the cursor wrapper.
+        static final int CONTACT_PHOTO_ID = 3;
+        // Provided by the cursor wrapper.
+        static final int CONTACT_LOOKUP_KEY = 4;
+
+        private GroupMembersQuery() {
+        }
+    }
+
+    private AccountWithDataSet mAccount;
+    private final Set<String> mRawContactIds = new HashSet<>();
+
+    private final CharSequence mUnknownNameText;
+
+    public GroupMemberPickListAdapter(Context context) {
+        super(context);
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    public GroupMemberPickListAdapter setAccount(AccountWithDataSet account) {
+        mAccount = account;
+        return this;
+    }
+
+    public GroupMemberPickListAdapter setRawContactIds(ArrayList<String> rawContactIds) {
+        mRawContactIds.clear();
+        mRawContactIds.addAll(rawContactIds);
+        return this;
+    }
+
+    @Override
+    public String getContactDisplayName(int position) {
+        final Cursor cursor = (Cursor) getItem(position);
+        return cursor.getString(GroupMembersQuery.CONTACT_DISPLAY_NAME);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader, long directoryId) {
+        loader.setUri(RawContacts.CONTENT_URI);
+
+        loader.setProjection(
+                getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+                        ? GroupMembersQuery.PROJECTION_PRIMARY
+                        : GroupMembersQuery.PROJECTION_ALTERNATIVE);
+        loader.setSelection(getSelection());
+        loader.setSelectionArgs(getSelectionArgs());
+
+        loader.setSortOrder(getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY
+                ? Data.SORT_KEY_PRIMARY : Data.SORT_KEY_ALTERNATIVE
+                + " COLLATE LOCALIZED ASC");
+    }
+
+    private String getSelection() {
+        // Select raw contacts by account
+        String result = RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=? AND ";
+        if (TextUtils.isEmpty(mAccount.dataSet)) {
+            result += Data.DATA_SET + " IS NULL";
+        } else {
+            result += Data.DATA_SET + "=?";
+        }
+        return result;
+    }
+
+    private String[] getSelectionArgs() {
+        final ArrayList<String> result = new ArrayList<>();
+        result.add(mAccount.name);
+        result.add(mAccount.type);
+        if (!TextUtils.isEmpty(mAccount.dataSet)) result.add(mAccount.dataSet);
+        return result.toArray(new String[0]);
+    }
+
+    public Uri getRawContactUri(int position) {
+        final Cursor cursor = (Cursor) getItem(position);
+        final long rawContactId = cursor.getLong(GroupMembersQuery.RAW_CONTACT_ID);
+        return RawContacts.CONTENT_URI.buildUpon()
+                .appendPath(Long.toString(rawContactId))
+                .build();
+    }
+
+    @Override
+    protected ContactListItemView newView(Context context, int partition, Cursor cursor,
+            int position, ViewGroup parent) {
+        final ContactListItemView view =
+                super.newView(context, partition, cursor, position, parent);
+        view.setUnknownNameText(mUnknownNameText);
+        return view;
+    }
+
+    @Override
+    protected void bindView(View v, int partition, Cursor cursor, int position) {
+        super.bindView(v, partition, cursor, position);
+        final ContactListItemView view = (ContactListItemView) v;
+        bindName(view, cursor);
+        bindViewId(view, cursor, GroupMembersQuery.RAW_CONTACT_ID);
+        bindPhoto(view, cursor);
+    }
+
+    private void bindName(ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, GroupMembersQuery.CONTACT_DISPLAY_NAME,
+                getContactNameDisplayOrder());
+    }
+
+    private void bindPhoto(final ContactListItemView view, Cursor cursor) {
+        final long photoId = cursor.isNull(GroupMembersQuery.CONTACT_PHOTO_ID)
+                ? 0 : cursor.getLong(GroupMembersQuery.CONTACT_PHOTO_ID);
+        final DefaultImageRequest imageRequest = photoId == 0
+                ? getDefaultImageRequestFromCursor(cursor, GroupMembersQuery.CONTACT_DISPLAY_NAME,
+                GroupMembersQuery.CONTACT_LOOKUP_KEY)
+                : null;
+        getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(),
+                imageRequest);
+    }
+}
diff --git a/src/com/android/contacts/list/GroupMemberPickerFragment.java b/src/com/android/contacts/list/GroupMemberPickerFragment.java
new file mode 100644
index 0000000..ae82b21
--- /dev/null
+++ b/src/com/android/contacts/list/GroupMemberPickerFragment.java
@@ -0,0 +1,299 @@
+/*
+ * 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.list;
+
+import android.content.Loader;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.ContactEntryListFragment;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.list.GroupMemberPickListAdapter.GroupMembersQuery;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Fragment containing raw contacts for a specified account that are not already in a group.
+ */
+public class GroupMemberPickerFragment extends
+        ContactEntryListFragment<GroupMemberPickListAdapter> {
+
+    private static final String KEY_ACCOUNT = "account";
+    private static final String KEY_RAW_CONTACT_IDS = "rawContactIds";
+
+    private static final String ARG_ACCOUNT = "account";
+    private static final String ARG_RAW_CONTACT_IDS = "rawContactIds";
+
+    /** Callbacks for host of {@link GroupMemberPickerFragment}. */
+    public interface Listener {
+
+        /** Invoked when a potential group member is selected. */
+        void onGroupMemberClicked(Uri uri);
+    }
+
+    /**
+     * Filters out raw contacts that are already in the group and also handles queries for contact
+     * photo IDs and lookup keys which cannot be retrieved from the raw contact table directly.
+     */
+    private class FilterCursorWrapper extends CursorWrapper {
+
+        private int[] mIndex;
+        private int mCount = 0;
+        private int mPos = 0;
+
+        public FilterCursorWrapper(Cursor cursor) {
+            super(cursor);
+
+            mCount = super.getCount();
+            mIndex = new int[mCount];
+            for (int i = 0; i < mCount; i++) {
+                super.moveToPosition(i);
+                final String rawContactId = getString(GroupMembersQuery.RAW_CONTACT_ID);
+                if (!mRawContactIds.contains(rawContactId)) {
+                    mIndex[mPos++] = i;
+                }
+            }
+            mCount = mPos;
+            mPos = 0;
+            super.moveToFirst();
+        }
+
+        @Override
+        public int getColumnIndex(String columnName) {
+            final int index = getColumnIndexForContactColumn(columnName);
+            return index < 0 ? super.getColumnIndex(columnName) : index;
+        }
+
+        @Override
+        public int getColumnIndexOrThrow(String columnName) {
+            final int index = getColumnIndexForContactColumn(columnName);
+            return index < 0 ? super.getColumnIndexOrThrow(columnName) : index;
+        }
+
+        private int getColumnIndexForContactColumn(String columnName) {
+            if (Contacts.PHOTO_ID.equals(columnName)) {
+                return GroupMembersQuery.CONTACT_PHOTO_ID;
+            }
+            if (Contacts.LOOKUP_KEY.equals(columnName)) {
+                return GroupMembersQuery.CONTACT_LOOKUP_KEY;
+            }
+            return -1;
+        }
+
+        @Override
+        public String[] getColumnNames() {
+            final String displayNameColumnName =
+                    getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+                            ? RawContacts.DISPLAY_NAME_PRIMARY
+                            : RawContacts.DISPLAY_NAME_ALTERNATIVE;
+            return new String[] {
+                    RawContacts._ID,
+                    RawContacts.CONTACT_ID,
+                    displayNameColumnName,
+                    Contacts.PHOTO_ID,
+                    Contacts.LOOKUP_KEY,
+            };
+        }
+
+        @Override
+        public String getString(int columnIndex) {
+            if (columnIndex == GroupMembersQuery.CONTACT_LOOKUP_KEY) {
+                if (columnIndex == GroupMembersQuery.CONTACT_PHOTO_ID) {
+                    final long contactId = getLong(GroupMembersQuery.CONTACT_ID);
+                    final Pair<Long,String> pair = getContactPhotoPair(contactId);
+                    if (pair != null) {
+                        return pair.second;
+                    }
+                }
+                return null;
+            }
+            return super.getString(columnIndex);
+        }
+
+        @Override
+        public long getLong(int columnIndex) {
+            if (columnIndex == GroupMembersQuery.CONTACT_PHOTO_ID) {
+                final long contactId = getLong(GroupMembersQuery.CONTACT_ID);
+                final Pair<Long,String> pair = getContactPhotoPair(contactId);
+                if (pair != null) {
+                    return pair.first;
+                }
+                return 0;
+            }
+            return super.getLong(columnIndex);
+        }
+
+        @Override
+        public boolean move(int offset) {
+            return moveToPosition(mPos + offset);
+        }
+
+        @Override
+        public boolean moveToNext() {
+            return moveToPosition(mPos + 1);
+        }
+
+        @Override
+        public boolean moveToPrevious() {
+            return moveToPosition(mPos - 1);
+        }
+
+        @Override
+        public boolean moveToFirst() {
+            return moveToPosition(0);
+        }
+
+        @Override
+        public boolean moveToLast() {
+            return moveToPosition(mCount - 1);
+        }
+
+        @Override
+        public boolean moveToPosition(int position) {
+            if (position >= mCount || position < 0) return false;
+            return super.moveToPosition(mIndex[position]);
+        }
+
+        @Override
+        public int getCount() {
+            return mCount;
+        }
+
+        @Override
+        public int getPosition() {
+            return mPos;
+        }
+    }
+
+    private AccountWithDataSet mAccount;
+    private ArrayList<String> mRawContactIds;
+    private Map<Long, Pair<Long,String>> mContactPhotoMap = new HashMap();
+
+    private Listener mListener;
+
+    public static GroupMemberPickerFragment newInstance(AccountWithDataSet account,
+            ArrayList<String> rawContactids) {
+        final Bundle args = new Bundle();
+        args.putParcelable(ARG_ACCOUNT, account);
+        args.putStringArrayList(ARG_RAW_CONTACT_IDS, rawContactids);
+
+        final GroupMemberPickerFragment fragment = new GroupMemberPickerFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    public GroupMemberPickerFragment() {
+        setQuickContactEnabled(false);
+        setPhotoLoaderEnabled(true);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        if (savedState == null) {
+            mAccount = getArguments().getParcelable(ARG_ACCOUNT);
+            mRawContactIds = getArguments().getStringArrayList(ARG_RAW_CONTACT_IDS);
+        } else {
+            mAccount = savedState.getParcelable(KEY_ACCOUNT);
+            mRawContactIds = savedState.getStringArrayList(KEY_RAW_CONTACT_IDS);
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(KEY_ACCOUNT, mAccount);
+        outState.putStringArrayList(KEY_RAW_CONTACT_IDS, mRawContactIds);
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        return inflater.inflate(R.layout.contact_list_content, null);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        super.onLoadFinished(loader, new FilterCursorWrapper(data));
+    }
+
+    @Override
+    protected GroupMemberPickListAdapter createListAdapter() {
+        final GroupMemberPickListAdapter adapter = new GroupMemberPickListAdapter(getActivity());
+        adapter.setDisplayPhotos(true);
+        return adapter;
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+        getAdapter().setAccount(mAccount);
+        getAdapter().setRawContactIds(mRawContactIds);
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        if (mListener != null) {
+            mListener.onGroupMemberClicked(getAdapter().getRawContactUri(position));
+        }
+    }
+
+    // TODO(wjang): unacceptable scrolling performance for big groups
+    private Pair<Long,String> getContactPhotoPair(long contactId) {
+        if (mContactPhotoMap.containsKey(contactId)) {
+            return mContactPhotoMap.get(contactId);
+        }
+        final Uri uri  = Data.CONTENT_URI.buildUpon()
+                .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                        String.valueOf(Directory.DEFAULT))
+                .build();
+        final String[] projection = new String[] { Data.PHOTO_ID, Data.LOOKUP_KEY };
+        final String selection = Data.CONTACT_ID + "=?";
+        final String[] selectionArgs = new String[] { Long.toString(contactId) };
+        Cursor cursor = null;
+        try {
+            cursor = getActivity().getContentResolver().query(
+                    uri, projection, selection, selectionArgs, /* sortOrder */ null);
+            if (cursor != null && cursor.moveToFirst()) {
+                final Pair<Long, String> pair = new Pair(cursor.getLong(0), cursor.getString(1));
+                mContactPhotoMap.put(contactId, pair);
+                return pair;
+            }
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/list/UiIntentActions.java b/src/com/android/contacts/list/UiIntentActions.java
index 5539635..31e5ade 100644
--- a/src/com/android/contacts/list/UiIntentActions.java
+++ b/src/com/android/contacts/list/UiIntentActions.java
@@ -40,6 +40,18 @@
     public static final String GROUP_NAME_EXTRA_KEY = "com.android.contacts.extra.GROUP";
 
     /**
+     * The account used to filter potential raw contact groups members.
+     */
+    public static final String GROUP_ACCOUNT_WITH_DATA_SET =
+            "com.android.contacts.extra.GROUP_ACCOUNT_WITH_DATA_SET";
+
+    /**
+     * The raw contact IDs for existing group members.
+     */
+    public static final String GROUP_RAW_CONTACT_IDS =
+            "com.android.contacts.extra.GROUP_RAW_CONTACT_IDS";
+
+    /**
      * The action for the all contacts list tab.
      */
     public static final String LIST_ALL_CONTACTS_ACTION =