Added group/selection email and sms sending.

Added options for user to send emails and sms texts to
all members of a group (or selection) at once. If there
are multiple emails/phones for a single contact, a picker
is displayed to specify which emails/phones to use.

Test: Verified all features are working as intended.
Menu options are only visible when relevant and edge
case of some or all users not having emails/phones
are handled by showing a toast to the user.

Bug: 31648014
Change-Id: I38066cf3be57bf205f7a3721d0064bb716e8a43f
diff --git a/res/menu/view_group.xml b/res/menu/view_group.xml
index 24eb0b5..1bbdd86 100644
--- a/res/menu/view_group.xml
+++ b/res/menu/view_group.xml
@@ -29,6 +29,14 @@
         android:title="@string/menu_editGroup" />
 
     <item
+        android:id="@+id/menu_multi_send_email"
+        android:title="@string/menu_sendEmailOption" />
+
+    <item
+        android:id="@+id/menu_multi_send_message"
+        android:title="@string/menu_sendMessageOption" />
+
+    <item
         android:id="@+id/menu_rename_group"
         android:title="@string/menu_renameGroup"/>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f92d80f..d7800b4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -350,6 +350,24 @@
     <!-- Message displayed when creating a group with the same name as an existing group -->
     <string name="groupExistsErrorMessage">That label already exists</string>
 
+    <!-- Toast displayed when some group contacts do not have any emails (for group send) [CHAR LIMIT=50] -->
+    <string name="groupSomeContactsNoEmailsToast">Some contacts do not have emails.</string>
+
+    <!-- Toast displayed when some group contacts do not have any phone numbers (for group send) [CHAR LIMIT=50] -->
+    <string name="groupSomeContactsNoPhonesToast">Some contacts do not have phone numbers.</string>
+
+    <!-- Option name to send email to all members of a group/selection [CHAR LIMIT=30] -->
+    <string name="menu_sendEmailOption">Send email</string>
+
+    <!-- Option name to send message to all members of a group/selection [CHAR LIMIT=30] -->
+    <string name="menu_sendMessageOption">Send message</string>
+
+    <!-- Activity title when the user is selecting items [CHAR LIMIT=128] -->
+    <string name="pickerSelectContactsActivityTitle">Select Contacts</string>
+
+    <!-- The menu item to send the currently selected contacts to selected items [CHAR LIMIT=10] -->
+    <string name="send_to_selection">Send</string>
+
     <!-- Displayed at the top of the contacts showing the total number of contacts visible when "Only contacts with phones" is selected -->
     <plurals name="listTotalPhoneContacts">
         <item quantity="one">1 contact with phone number</item>
diff --git a/src/com/android/contacts/activities/ContactSelectionActivity.java b/src/com/android/contacts/activities/ContactSelectionActivity.java
index fe95465..cbf6a69 100644
--- a/src/com/android/contacts/activities/ContactSelectionActivity.java
+++ b/src/com/android/contacts/activities/ContactSelectionActivity.java
@@ -52,6 +52,8 @@
 import com.android.contacts.list.GroupMemberPickerFragment;
 import com.android.contacts.list.JoinContactListFragment;
 import com.android.contacts.list.LegacyPhoneNumberPickerFragment;
+import com.android.contacts.list.MultiSelectEmailAddressesListFragment;
+import com.android.contacts.list.MultiSelectPhoneNumbersListFragment;
 import com.android.contacts.list.MultiSelectContactsListFragment;
 import com.android.contacts.list.MultiSelectContactsListFragment.OnCheckBoxListActionListener;
 import com.android.contacts.list.OnContactPickerActionListener;
@@ -154,12 +156,10 @@
 
         // Postal address pickers (and legacy pickers) don't support search, so just show
         // "HomeAsUp" button and title.
-        if (mRequest.getActionCode() == ContactsRequest.ACTION_PICK_POSTAL ||
-                mRequest.isLegacyCompatibilityMode()) {
-            mIsSearchSupported = false;
-        } else {
-            mIsSearchSupported = true;
-        }
+        mIsSearchSupported = mRequest.getActionCode() != ContactsRequest.ACTION_PICK_POSTAL
+                && mRequest.getActionCode() != ContactsRequest.ACTION_PICK_EMAILS
+                && mRequest.getActionCode() != ContactsRequest.ACTION_PICK_PHONES
+                && !mRequest.isLegacyCompatibilityMode();
         configureSearchMode();
     }
 
@@ -226,6 +226,14 @@
                 titleResId = R.string.contactPickerActivityTitle;
                 break;
             }
+            case ContactsRequest.ACTION_PICK_PHONES: {
+                titleResId = R.string.pickerSelectContactsActivityTitle;
+                break;
+            }
+            case ContactsRequest.ACTION_PICK_EMAILS: {
+                titleResId = R.string.pickerSelectContactsActivityTitle;
+                break;
+            }
             case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: {
                 titleResId = R.string.callShortcutActivityTitle;
                 break;
@@ -305,6 +313,17 @@
                 break;
             }
 
+            case ContactsRequest.ACTION_PICK_PHONES: {
+                mListFragment = new MultiSelectPhoneNumbersListFragment();
+                mListFragment.setArguments(getIntent().getExtras());
+                break;
+            }
+
+            case ContactsRequest.ACTION_PICK_EMAILS: {
+                mListFragment = new MultiSelectEmailAddressesListFragment();
+                mListFragment.setArguments(getIntent().getExtras());
+                break;
+            }
             case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: {
                 PhoneNumberPickerFragment fragment = getPhoneNumberPickerFragment(mRequest);
                 fragment.setShortcutAction(Intent.ACTION_CALL);
@@ -387,6 +406,10 @@
         } else if (mListFragment instanceof EmailAddressPickerFragment) {
             ((EmailAddressPickerFragment) mListFragment).setOnEmailAddressPickerActionListener(
                     new EmailAddressPickerActionListener());
+        } else if (mListFragment instanceof MultiSelectEmailAddressesListFragment) {
+            ((MultiSelectEmailAddressesListFragment) mListFragment).setCheckBoxListListener(this);
+        } else if (mListFragment instanceof MultiSelectPhoneNumbersListFragment) {
+            ((MultiSelectPhoneNumbersListFragment) mListFragment).setCheckBoxListListener(this);
         } else if (mListFragment instanceof JoinContactListFragment) {
             ((JoinContactListFragment) mListFragment).setOnContactPickerActionListener(
                     new JoinContactActionListener());
diff --git a/src/com/android/contacts/common/Experiments.java b/src/com/android/contacts/common/Experiments.java
index 5de4d5d..e872694 100644
--- a/src/com/android/contacts/common/Experiments.java
+++ b/src/com/android/contacts/common/Experiments.java
@@ -73,6 +73,11 @@
      */
     public static final String SEARCH_YENTA_TIMEOUT_MILLIS = "Search__yenta_timeout";
 
+    /**
+     * The options for sending email/messages to groups and selections
+     */
+    public static final String SEND_TO_GROUP = "Groups__send_to_group";
+
     private Experiments() {
     }
 }
diff --git a/src/com/android/contacts/common/list/MultiSelectEntryContactListAdapter.java b/src/com/android/contacts/common/list/MultiSelectEntryContactListAdapter.java
index dbfd70e..991ad52 100644
--- a/src/com/android/contacts/common/list/MultiSelectEntryContactListAdapter.java
+++ b/src/com/android/contacts/common/list/MultiSelectEntryContactListAdapter.java
@@ -23,6 +23,8 @@
 import android.view.View.OnClickListener;
 import android.widget.CheckBox;
 
+import com.android.contacts.group.GroupUtil;
+
 import java.util.TreeSet;
 
 /**
@@ -88,13 +90,7 @@
      * Returns the selected contacts as an array.
      */
     public long[] getSelectedContactIdsArray() {
-        final Long[] contactIds = mSelectedContactIds.toArray(
-                new Long[mSelectedContactIds.size()]);
-        final long[] result = new long[contactIds.length];
-        for (int i = 0; i < contactIds.length; i++) {
-            result[i] = contactIds[i];
-        }
-        return result;
+        return GroupUtil.convertLongSetToLongArray(mSelectedContactIds);
     }
 
     /**
diff --git a/src/com/android/contacts/group/GroupMembersFragment.java b/src/com/android/contacts/group/GroupMembersFragment.java
index 30f339c..77f193c 100644
--- a/src/com/android/contacts/group/GroupMembersFragment.java
+++ b/src/com/android/contacts/group/GroupMembersFragment.java
@@ -17,6 +17,7 @@
 
 import android.app.Activity;
 import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ContentResolver;
 import android.content.CursorLoader;
 import android.content.Intent;
 import android.content.Loader;
@@ -26,7 +27,9 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
@@ -46,6 +49,8 @@
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
 import com.android.contacts.activities.ActionBarAdapter;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.Experiments;
 import com.android.contacts.common.list.ContactsSectionIndexer;
 import com.android.contacts.common.list.MultiSelectEntryContactListAdapter.DeleteContactListener;
 import com.android.contacts.common.logging.ListEvent;
@@ -59,6 +64,7 @@
 import com.android.contacts.list.ContactsRequest;
 import com.android.contacts.list.MultiSelectContactsListFragment;
 import com.android.contacts.list.UiIntentActions;
+import com.android.contactsbind.experiments.Flags;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -257,7 +263,13 @@
         final boolean isSelectionMode = mActionBarAdapter.isSelectionMode();
         final boolean isGroupEditable = mGroupMetaData != null && mGroupMetaData.editable;
         final boolean isGroupReadOnly = mGroupMetaData != null && mGroupMetaData.readOnly;
+        final boolean experimentFlagSet =
+                Flags.getInstance(getContext()).getBoolean(Experiments.SEND_TO_GROUP);
 
+        setVisible(menu, R.id.menu_multi_send_email, !mIsEditMode && !isGroupEmpty()
+                && experimentFlagSet);
+        setVisible(menu, R.id.menu_multi_send_message, !mIsEditMode && !isGroupEmpty()
+                && experimentFlagSet);
         setVisible(menu, R.id.menu_add, isGroupEditable && !isSelectionMode);
         setVisible(menu, R.id.menu_rename_group, !isGroupReadOnly && !isSelectionMode);
         setVisible(menu, R.id.menu_delete_group, !isGroupReadOnly && !isSelectionMode);
@@ -278,6 +290,91 @@
         }
     }
 
+    /**
+     * Helper class for cp2 query used to look up all contact's emails and phone numbers.
+     */
+    private static abstract class ContactQuery {
+        public static final String EMAIL_SELECTION =
+                ContactsContract.Data.MIMETYPE + "='"
+                        + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'";
+
+        public static final String PHONE_SELECTION =
+                ContactsContract.Data.MIMETYPE + "='"
+                        + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'";
+
+        public static final String[] PROJECTION = {
+                ContactsContract.Data.CONTACT_ID,
+                ContactsContract.Data.IS_PRIMARY,
+                ContactsContract.Data.DATA1
+        };
+
+        public static final int CONTACT_ID = 0;
+        public static final int IS_PRIMARY = 1;
+        public static final int DATA1 = 2;
+    }
+
+    private void sendToGroup(long[] ids, String sendScheme, String title) {
+        if(ids == null || ids.length == 0) return;
+
+        // Get emails or phone numbers
+        final List<String> itemsData = new ArrayList<>();
+        final Set<String> usedContactIds = new HashSet<>();
+        final String sIds = GroupUtil.convertArrayToString(ids);
+        final String select = (ContactsUtils.SCHEME_MAILTO.equals(sendScheme)
+                ? ContactQuery.EMAIL_SELECTION
+                : ContactQuery.PHONE_SELECTION)
+                + " AND " + ContactsContract.Data.CONTACT_ID + " IN (" + sIds + ")";
+        final ContentResolver contentResolver = getContext().getContentResolver();
+        final Cursor cursor = contentResolver.query(ContactsContract.Data.CONTENT_URI,
+                ContactQuery.PROJECTION, select, null, null);
+
+        if (cursor == null) {
+            return;
+        }
+
+        try {
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                final String contactId = cursor.getString(ContactQuery.CONTACT_ID);
+                final String data = cursor.getString(ContactQuery.DATA1);
+
+                if (!usedContactIds.contains(contactId)) {
+                    usedContactIds.add(contactId);
+                } else {
+                    // If we found a contact with multiple items (email, phone), start the picker
+                    startSendToSelectionPickerActivity(ids, sendScheme);
+                    return;
+                } if (!TextUtils.isEmpty(data)) {
+                    itemsData.add(data);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+
+        if (itemsData.size() == 0 || usedContactIds.size() < ids.length) {
+            Toast.makeText(getContext(), ContactsUtils.SCHEME_MAILTO.equals(sendScheme)
+                            ? getString(R.string.groupSomeContactsNoEmailsToast)
+                            : getString(R.string.groupSomeContactsNoPhonesToast),
+                    Toast.LENGTH_LONG).show();
+        }
+
+        if (itemsData.size() == 0) {
+            return;
+        }
+
+        final String itemsString = GroupUtil.convertListToString(itemsData);
+        startSendToSelectionActivity(itemsString, sendScheme, title);
+    }
+
+    private void startSendToSelectionActivity(String listItems, String sendScheme, String title) {
+        startActivity(GroupUtil.createSendToSelectionIntent(listItems, sendScheme, title));
+    }
+
+    private void startSendToSelectionPickerActivity(long[] ids, String sendScheme) {
+        startActivity(GroupUtil.createSendToSelectionPickerIntent(getContext(), ids, sendScheme));
+    }
+
     private void startGroupAddMemberActivity() {
         startActivityForResult(GroupUtil.createPickMemberIntent(getContext(), mGroupMetaData,
                 getMemberContactIds()), RESULT_GROUP_ADD_MEMBER);
@@ -294,6 +391,22 @@
                 startGroupAddMemberActivity();
                 return true;
             }
+            case R.id.menu_multi_send_email: {
+                final long[] ids = mActionBarAdapter.isSelectionMode()
+                        ? getAdapter().getSelectedContactIdsArray()
+                        : GroupUtil.convertStringSetToLongArray(mGroupMemberContactIds);
+                sendToGroup(ids, ContactsUtils.SCHEME_MAILTO,
+                        getString(R.string.menu_sendEmailOption));
+                return true;
+            }
+            case R.id.menu_multi_send_message: {
+                final long[] ids = mActionBarAdapter.isSelectionMode()
+                        ? getAdapter().getSelectedContactIdsArray()
+                        : GroupUtil.convertStringSetToLongArray(mGroupMemberContactIds);
+                sendToGroup(ids, ContactsUtils.SCHEME_SMSTO,
+                        getString(R.string.menu_sendMessageOption));
+                return true;
+            }
             case R.id.menu_rename_group: {
                 GroupNameEditDialogFragment.newInstanceForUpdate(
                         new AccountWithDataSet(mGroupMetaData.accountName,
diff --git a/src/com/android/contacts/group/GroupUtil.java b/src/com/android/contacts/group/GroupUtil.java
index 1a99a65..1faf2c9 100644
--- a/src/com/android/contacts/group/GroupUtil.java
+++ b/src/com/android/contacts/group/GroupUtil.java
@@ -21,12 +21,14 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Groups;
 import android.text.TextUtils;
 
 import com.android.contacts.GroupListLoader;
 import com.android.contacts.activities.ContactSelectionActivity;
+import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.list.ContactsSectionIndexer;
 import com.android.contacts.common.model.account.GoogleAccountType;
 import com.android.contacts.list.UiIntentActions;
@@ -98,6 +100,26 @@
                 isFirstGroupInAccount, memberCount, isReadOnly, systemId);
     }
 
+    /** Returns an Intent to send emails/phones to some activity/app */
+    public static Intent createSendToSelectionIntent(
+            String itemsList, String sendScheme, String title) {
+        final Intent intent = new Intent(Intent.ACTION_SENDTO,
+                Uri.fromParts(sendScheme, itemsList, null));
+        return Intent.createChooser(intent, title);
+    }
+
+    /** Returns an Intent to pick emails/phones to send to selection (or group) */
+    public static Intent createSendToSelectionPickerIntent(
+            Context context, long[] ids, String sendScheme) {
+        final Intent intent = new Intent(context, ContactSelectionActivity.class);
+        intent.setAction(UiIntentActions.ACTION_SELECT_ITEMS);
+        intent.setType(ContactsUtils.SCHEME_MAILTO.equals(sendScheme)
+                ? ContactsContract.CommonDataKinds.Email.CONTENT_TYPE
+                : ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);
+        intent.putExtra(UiIntentActions.LIST_CONTACTS, ids);
+        return intent;
+    }
+
     /** Returns an Intent to pick contacts to add to a group. */
     public static Intent createPickMemberIntent(Context context,
             GroupMetaData groupMetaData, ArrayList<String> memberContactIds) {
@@ -111,6 +133,38 @@
         return intent;
     }
 
+    public static String convertArrayToString(long[] list) {
+        if (list == null || list.length == 0) return "";
+        return Arrays.toString(list).replace("[", "").replace("]", "");
+    }
+
+    public static String convertListToString(List<String> list) {
+        if (list == null || list.size() == 0) return "";
+        return list.toString().replace("[", "").replace("]", "");
+    }
+
+    public static long[] convertLongSetToLongArray(Set<Long> set) {
+        final Long[] contactIds = set.toArray(new Long[set.size()]);
+        final long[] result = new long[contactIds.length];
+        for (int i = 0; i < contactIds.length; i++) {
+            result[i] = contactIds[i];
+        }
+        return result;
+    }
+
+    public static long[] convertStringSetToLongArray(Set<String> set) {
+        final String[] contactIds = set.toArray(new String[set.size()]);
+        final long[] result = new long[contactIds.length];
+        for (int i = 0; i < contactIds.length; i++) {
+            try {
+                result[i] = Long.parseLong(contactIds[i]);
+            } catch (NumberFormatException e) {
+                result[i] = -1;
+            }
+        }
+        return result;
+    }
+
     /**
      * Returns true if it's an empty and read-only group and the system ID of
      * the group is one of "Friends", "Family" and "Coworkers".
diff --git a/src/com/android/contacts/list/ContactsIntentResolver.java b/src/com/android/contacts/list/ContactsIntentResolver.java
index 8e93baf..b110605 100644
--- a/src/com/android/contacts/list/ContactsIntentResolver.java
+++ b/src/com/android/contacts/list/ContactsIntentResolver.java
@@ -74,6 +74,13 @@
         } else if (UiIntentActions.LIST_GROUP_ACTION.equals(action)) {
             request.setActionCode(ContactsRequest.ACTION_GROUP);
             // We no longer support UiIntentActions.GROUP_NAME_EXTRA_KEY
+        } else if (UiIntentActions.ACTION_SELECT_ITEMS.equals(action)) {
+            final String resolvedType = intent.resolveType(mContext);
+            if (Phone.CONTENT_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_PHONES);
+            } else if (Email.CONTENT_TYPE.equals(resolvedType)) {
+                request.setActionCode(ContactsRequest.ACTION_PICK_EMAILS);
+            }
         } else if (Intent.ACTION_PICK.equals(action)) {
             final String resolvedType = intent.resolveType(mContext);
             if (Contacts.CONTENT_TYPE.equals(resolvedType)) {
diff --git a/src/com/android/contacts/list/ContactsRequest.java b/src/com/android/contacts/list/ContactsRequest.java
index de6a4ba..70ce80d 100644
--- a/src/com/android/contacts/list/ContactsRequest.java
+++ b/src/com/android/contacts/list/ContactsRequest.java
@@ -78,6 +78,12 @@
     /** Show all postal addresses and pick them when clicking */
     public static final int ACTION_PICK_EMAIL = 105;
 
+    /** Show a list of emails for selected contacts and select them when clicking */
+    public static final int ACTION_PICK_EMAILS = 106;
+
+    /** Show a list of phones for selected contacts and select them when clicking */
+    public static final int ACTION_PICK_PHONES = 107;
+
     /** Show all contacts and create a shortcut for the picked contact */
     public static final int ACTION_CREATE_SHORTCUT_CONTACT = 110;
 
diff --git a/src/com/android/contacts/list/MultiSelectEmailAddressesListAdapter.java b/src/com/android/contacts/list/MultiSelectEmailAddressesListAdapter.java
new file mode 100644
index 0000000..d77a98e
--- /dev/null
+++ b/src/com/android/contacts/list/MultiSelectEmailAddressesListAdapter.java
@@ -0,0 +1,206 @@
+/*
+ * 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.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.MultiSelectEntryContactListAdapter;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.group.GroupUtil;
+
+/** Email addresses multi-select cursor adapter. */
+public class MultiSelectEmailAddressesListAdapter extends MultiSelectEntryContactListAdapter {
+
+    protected static class EmailQuery {
+        public static final String[] PROJECTION_PRIMARY = new String[] {
+                Email._ID,                          // 0
+                Email.TYPE,                         // 1
+                Email.LABEL,                        // 2
+                Email.ADDRESS,                      // 3
+                Email.CONTACT_ID,                   // 4
+                Email.LOOKUP_KEY,                   // 5
+                Email.PHOTO_ID,                     // 6
+                Email.DISPLAY_NAME_PRIMARY,         // 7
+                Email.PHOTO_THUMBNAIL_URI,          // 8
+        };
+
+        public static final String[] PROJECTION_ALTERNATIVE = new String[] {
+                Email._ID,                          // 0
+                Email.TYPE,                         // 1
+                Email.LABEL,                        // 2
+                Email.ADDRESS,                      // 3
+                Email.CONTACT_ID,                   // 4
+                Email.LOOKUP_KEY,                   // 5
+                Email.PHOTO_ID,                     // 6
+                Email.DISPLAY_NAME_ALTERNATIVE,     // 7
+                Email.PHOTO_THUMBNAIL_URI,          // 8
+        };
+
+        public static final int EMAIL_ID                = 0;
+        public static final int EMAIL_TYPE              = 1;
+        public static final int EMAIL_LABEL             = 2;
+        public static final int EMAIL_ADDRESS           = 3;
+        public static final int CONTACT_ID              = 4;
+        public static final int LOOKUP_KEY              = 5;
+        public static final int PHOTO_ID                = 6;
+        public static final int DISPLAY_NAME            = 7;
+        public static final int PHOTO_URI               = 8;
+    }
+
+    private final CharSequence mUnknownNameText;
+    private long[] mContactIdsFilter = null;
+
+    public MultiSelectEmailAddressesListAdapter(Context context) {
+        super(context, EmailQuery.EMAIL_ID);
+
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    public void setArguments(Bundle bundle) {
+        mContactIdsFilter = bundle.getLongArray(UiIntentActions.LIST_CONTACTS);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader, long directoryId) {
+        final Builder builder;
+        if (isSearchMode()) {
+            builder = Email.CONTENT_FILTER_URI.buildUpon();
+            final String query = getQueryString();
+            builder.appendPath(TextUtils.isEmpty(query) ? "" : query);
+        } else {
+            builder = Email.CONTENT_URI.buildUpon();
+            if (isSectionHeaderDisplayEnabled()) {
+                builder.appendQueryParameter(Email.EXTRA_ADDRESS_BOOK_INDEX, "true");
+            }
+        }
+        builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                String.valueOf(directoryId));
+        loader.setUri(builder.build());
+
+        if (mContactIdsFilter != null) {
+            loader.setSelection(ContactsContract.Data.CONTACT_ID
+                    + " IN (" + GroupUtil.convertArrayToString(mContactIdsFilter) + ")");
+        }
+
+        if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+            loader.setProjection(EmailQuery.PROJECTION_PRIMARY);
+        } else {
+            loader.setProjection(EmailQuery.PROJECTION_ALTERNATIVE);
+        }
+
+        if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+            loader.setSortOrder(Email.SORT_KEY_PRIMARY);
+        } else {
+            loader.setSortOrder(Email.SORT_KEY_ALTERNATIVE);
+        }
+    }
+
+    @Override
+    public String getContactDisplayName(int position) {
+        return ((Cursor) getItem(position)).getString(EmailQuery.DISPLAY_NAME);
+    }
+
+    /**
+     * Builds a {@link Data#CONTENT_URI} for the current cursor position.
+     */
+    public Uri getDataUri(int position) {
+        final long id = ((Cursor) getItem(position)).getLong(EmailQuery.EMAIL_ID);
+        return ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, id);
+    }
+
+    @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);
+        view.setQuickContactEnabled(isQuickContactEnabled());
+        return view;
+    }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        super.bindView(itemView, partition, cursor, position);
+        final ContactListItemView view = (ContactListItemView)itemView;
+
+        cursor.moveToPosition(position);
+        boolean isFirstEntry = true;
+        final long currentContactId = cursor.getLong(EmailQuery.CONTACT_ID);
+        if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
+            final long previousContactId = cursor.getLong(EmailQuery.CONTACT_ID);
+            if (currentContactId == previousContactId) {
+                isFirstEntry = false;
+            }
+        }
+        cursor.moveToPosition(position);
+
+        bindViewId(view, cursor, EmailQuery.EMAIL_ID);
+        bindSectionHeaderAndDivider(view, position);
+        if (isFirstEntry) {
+            bindName(view, cursor);
+            bindQuickContact(view, partition, cursor, EmailQuery.PHOTO_ID,
+                    EmailQuery.PHOTO_URI, EmailQuery.CONTACT_ID,
+                    EmailQuery.LOOKUP_KEY, EmailQuery.DISPLAY_NAME);
+        } else {
+            unbindName(view);
+            view.removePhotoView(true, false);
+        }
+        bindEmailAddress(view, cursor);
+    }
+
+    protected void unbindName(final ContactListItemView view) {
+        view.hideDisplayName();
+    }
+
+    protected void bindEmailAddress(ContactListItemView view, Cursor cursor) {
+        CharSequence label = null;
+        if (!cursor.isNull(EmailQuery.EMAIL_TYPE)) {
+            final int type = cursor.getInt(EmailQuery.EMAIL_TYPE);
+            final String customLabel = cursor.getString(EmailQuery.EMAIL_LABEL);
+
+            // TODO cache
+            label = Email.getTypeLabel(getContext().getResources(), type, customLabel);
+        }
+        view.setLabel(label);
+        view.showData(cursor, EmailQuery.EMAIL_ADDRESS);
+    }
+
+    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
+        final int section = getSectionForPosition(position);
+        if (getPositionForSection(section) == position) {
+            final String title = (String)getSections()[section];
+            view.setSectionHeader(title);
+        } else {
+            view.setSectionHeader(null);
+        }
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, EmailQuery.DISPLAY_NAME, getContactNameDisplayOrder());
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/list/MultiSelectEmailAddressesListFragment.java b/src/com/android/contacts/list/MultiSelectEmailAddressesListFragment.java
new file mode 100644
index 0000000..06e44e9
--- /dev/null
+++ b/src/com/android/contacts/list/MultiSelectEmailAddressesListFragment.java
@@ -0,0 +1,59 @@
+/*
+ * 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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.logging.ListEvent;
+
+/** Displays a list of emails with check boxes. */
+public class MultiSelectEmailAddressesListFragment
+        extends MultiSelectContactsListFragment<MultiSelectEmailAddressesListAdapter>{
+
+    public MultiSelectEmailAddressesListFragment() {
+        setPhotoLoaderEnabled(true);
+        setSectionHeaderDisplayEnabled(true);
+        setSearchMode(false);
+        setHasOptionsMenu(false);
+        setListType(ListEvent.ListType.PICK_EMAIL);
+    }
+
+    @Override
+    public MultiSelectEmailAddressesListAdapter createListAdapter() {
+        final MultiSelectEmailAddressesListAdapter adapter =
+                new MultiSelectEmailAddressesListAdapter(getActivity());
+        adapter.setArguments(getArguments());
+        return adapter;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        displayCheckBoxes(true);
+    }
+
+    @Override
+    protected boolean onItemLongClick(int position, long id) {
+        return true;
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        return inflater.inflate(com.android.contacts.common.R.layout.contact_list_content, null);
+    }
+}
diff --git a/src/com/android/contacts/list/MultiSelectPhoneNumbersListAdapter.java b/src/com/android/contacts/list/MultiSelectPhoneNumbersListAdapter.java
new file mode 100644
index 0000000..a545b31
--- /dev/null
+++ b/src/com/android/contacts/list/MultiSelectPhoneNumbersListAdapter.java
@@ -0,0 +1,205 @@
+/*
+ * 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.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.MultiSelectEntryContactListAdapter;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.group.GroupUtil;
+
+/** Phone Numbers multi-select cursor adapter. */
+public class MultiSelectPhoneNumbersListAdapter extends MultiSelectEntryContactListAdapter {
+
+    public static class PhoneQuery {
+        public static final String[] PROJECTION_PRIMARY = new String[] {
+                Phone._ID,                          // 0
+                Phone.TYPE,                         // 1
+                Phone.LABEL,                        // 2
+                Phone.NUMBER,                       // 3
+                Phone.CONTACT_ID,                   // 4
+                Phone.LOOKUP_KEY,                   // 5
+                Phone.PHOTO_ID,                     // 6
+                Phone.DISPLAY_NAME_PRIMARY,         // 7
+                Phone.PHOTO_THUMBNAIL_URI,          // 8
+        };
+
+        public static final String[] PROJECTION_ALTERNATIVE = new String[] {
+                Phone._ID,                          // 0
+                Phone.TYPE,                         // 1
+                Phone.LABEL,                        // 2
+                Phone.NUMBER,                       // 3
+                Phone.CONTACT_ID,                   // 4
+                Phone.LOOKUP_KEY,                   // 5
+                Phone.PHOTO_ID,                     // 6
+                Phone.DISPLAY_NAME_ALTERNATIVE,     // 7
+                Phone.PHOTO_THUMBNAIL_URI,          // 8
+        };
+
+        public static final int PHONE_ID                = 0;
+        public static final int PHONE_TYPE              = 1;
+        public static final int PHONE_LABEL             = 2;
+        public static final int PHONE_NUMBER            = 3;
+        public static final int CONTACT_ID              = 4;
+        public static final int LOOKUP_KEY              = 5;
+        public static final int PHOTO_ID                = 6;
+        public static final int DISPLAY_NAME            = 7;
+        public static final int PHOTO_URI               = 8;
+    }
+
+    private final CharSequence mUnknownNameText;
+    private long[] mContactIdsFilter = null;
+
+    public MultiSelectPhoneNumbersListAdapter(Context context) {
+        super(context, PhoneQuery.PHONE_ID);
+
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+    }
+
+    public void setArguments(Bundle bundle) {
+        mContactIdsFilter = bundle.getLongArray(UiIntentActions.LIST_CONTACTS);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader, long directoryId) {
+        final Builder builder;
+        if (isSearchMode()) {
+            builder = Phone.CONTENT_FILTER_URI.buildUpon();
+            final String query = getQueryString();
+            builder.appendPath(TextUtils.isEmpty(query) ? "" : query);
+        } else {
+            builder = Phone.CONTENT_URI.buildUpon();
+            if (isSectionHeaderDisplayEnabled()) {
+                builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true");
+            }
+        }
+        builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                String.valueOf(directoryId));
+        loader.setUri(builder.build());
+
+        if (mContactIdsFilter != null) {
+            loader.setSelection(ContactsContract.Data.CONTACT_ID
+                    + " IN (" + GroupUtil.convertArrayToString(mContactIdsFilter) + ")");
+        }
+
+        if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+            loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+        } else {
+            loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
+        }
+
+        if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+            loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
+        } else {
+            loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+        }
+    }
+
+    @Override
+    public String getContactDisplayName(int position) {
+        return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
+    }
+
+    /**
+     * Builds a {@link Data#CONTENT_URI} for the current cursor position.
+     */
+    public Uri getDataUri(int position) {
+        final long id = ((Cursor) getItem(position)).getLong(PhoneQuery.PHONE_ID);
+        return ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, id);
+    }
+
+    @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);
+        view.setQuickContactEnabled(isQuickContactEnabled());
+        return view;
+    }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        super.bindView(itemView, partition, cursor, position);
+        final ContactListItemView view = (ContactListItemView)itemView;
+
+        cursor.moveToPosition(position);
+        boolean isFirstEntry = true;
+        final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+        if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
+            final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+            if (currentContactId == previousContactId) {
+                isFirstEntry = false;
+            }
+        }
+        cursor.moveToPosition(position);
+
+        bindViewId(view, cursor, PhoneQuery.PHONE_ID);
+        bindSectionHeaderAndDivider(view, position);
+        if (isFirstEntry) {
+            bindName(view, cursor);
+            bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
+                        PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID,
+                        PhoneQuery.LOOKUP_KEY, PhoneQuery.DISPLAY_NAME);
+        } else {
+            unbindName(view);
+            view.removePhotoView(true, false);
+        }
+        bindPhoneNumber(view, cursor);
+    }
+
+    protected void unbindName(final ContactListItemView view) {
+        view.hideDisplayName();
+    }
+
+    protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) {
+        CharSequence label = null;
+        if (!cursor.isNull(PhoneQuery.PHONE_TYPE)) {
+            final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
+            final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
+
+            // TODO cache
+            label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+        }
+        view.setLabel(label);
+        view.showData(cursor, PhoneQuery.PHONE_NUMBER);
+    }
+
+    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
+        if (isSectionHeaderDisplayEnabled()) {
+            Placement placement = getItemPlacementInSection(position);
+            view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
+        } else {
+            view.setSectionHeader(null);
+        }
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
+    }
+}
diff --git a/src/com/android/contacts/list/MultiSelectPhoneNumbersListFragment.java b/src/com/android/contacts/list/MultiSelectPhoneNumbersListFragment.java
new file mode 100644
index 0000000..b057911
--- /dev/null
+++ b/src/com/android/contacts/list/MultiSelectPhoneNumbersListFragment.java
@@ -0,0 +1,59 @@
+/*
+ * 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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.logging.ListEvent;
+
+/** Displays a list of phone numbers with check boxes. */
+public class MultiSelectPhoneNumbersListFragment
+        extends MultiSelectContactsListFragment<MultiSelectPhoneNumbersListAdapter> {
+
+    public MultiSelectPhoneNumbersListFragment() {
+        setPhotoLoaderEnabled(true);
+        setSectionHeaderDisplayEnabled(true);
+        setSearchMode(false);
+        setHasOptionsMenu(false);
+        setListType(ListEvent.ListType.PICK_PHONE);
+    }
+
+    @Override
+    public MultiSelectPhoneNumbersListAdapter createListAdapter() {
+        final MultiSelectPhoneNumbersListAdapter adapter =
+                new MultiSelectPhoneNumbersListAdapter(getActivity());
+        adapter.setArguments(getArguments());
+        return adapter;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        displayCheckBoxes(true);
+    }
+
+    @Override
+    protected boolean onItemLongClick(int position, long id) {
+        return true;
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        return inflater.inflate(com.android.contacts.common.R.layout.contact_list_content, null);
+    }
+}
diff --git a/src/com/android/contacts/list/UiIntentActions.java b/src/com/android/contacts/list/UiIntentActions.java
index 6ea984f..e765768 100644
--- a/src/com/android/contacts/list/UiIntentActions.java
+++ b/src/com/android/contacts/list/UiIntentActions.java
@@ -35,6 +35,12 @@
             "com.android.contacts.action.LIST_CONTACTS";
 
     /**
+     * The action for selecting multiple items (email, phone) from a list.
+     */
+    public static final String ACTION_SELECT_ITEMS =
+            "com.android.contacts.action.ACTION_SELECT_ITEMS";
+
+    /**
      * The action for the contacts list tab.
      */
     public static final String LIST_GROUP_ACTION =