Merge "Multi-select to share Part 2"
diff --git a/res/layout-land/people_activity_toolbar.xml b/res/layout-land/people_activity_toolbar.xml
index 1f86879..d33a96c 100644
--- a/res/layout-land/people_activity_toolbar.xml
+++ b/res/layout-land/people_activity_toolbar.xml
@@ -25,11 +25,18 @@
     android:elevation="@dimen/tab_elevation"
     android:layout_height="wrap_content" >
 
-    <Toolbar
+    <FrameLayout
+        android:id="@+id/toolbar_frame"
         android:layout_width="match_parent"
         android:layout_height="?android:attr/actionBarSize"
-        android:background="@color/actionbar_background_color"
-        android:id="@+id/toolbar"
-        style="@style/ContactsToolbarStyle" />
+        android:background="@color/actionbar_background_color">
+
+        <Toolbar
+            android:layout_width="match_parent"
+            android:layout_height="?android:attr/actionBarSize"
+            android:id="@+id/toolbar"
+            style="@style/ContactsToolbarStyle" />
+
+    </FrameLayout>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout/people_activity_toolbar.xml b/res/layout/people_activity_toolbar.xml
index e69728b..cbb4d91 100644
--- a/res/layout/people_activity_toolbar.xml
+++ b/res/layout/people_activity_toolbar.xml
@@ -21,12 +21,19 @@
     android:elevation="@dimen/tab_elevation"
     android:layout_height="wrap_content" >
 
-    <Toolbar
+    <FrameLayout
+        android:id="@+id/toolbar_frame"
         android:layout_width="match_parent"
         android:layout_height="?android:attr/actionBarSize"
-        android:background="@color/actionbar_background_color"
-        android:id="@+id/toolbar"
-        style="@style/ContactsToolbarStyle" />
+        android:background="@color/actionbar_background_color">
+
+        <Toolbar
+            android:layout_width="match_parent"
+            android:layout_height="?android:attr/actionBarSize"
+            android:id="@+id/toolbar"
+            style="@style/ContactsToolbarStyle" />
+
+    </FrameLayout>
 
     <com.android.contacts.common.list.ViewPagerTabs
         android:id="@+id/lists_pager_header"
diff --git a/res/layout/selection_bar.xml b/res/layout/selection_bar.xml
new file mode 100644
index 0000000..e91311f
--- /dev/null
+++ b/res/layout/selection_bar.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/selection_bar"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:background="@color/contextual_selection_bar_color" >
+
+    <ImageButton
+        android:id="@+id/selection_close"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:src="@drawable/ic_close_dk"
+        android:background="?android:attr/selectableItemBackgroundBorderless"
+        android:contentDescription="@string/action_menu_back_from_search"
+        android:tint="@android:color/white" />
+
+    <TextView
+        android:id="@+id/selection_count_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        style="@style/ContactsActionBarTitleText" />
+
+</LinearLayout>
diff --git a/res/menu/people_options.xml b/res/menu/people_options.xml
index a7802f0..987ddc5 100644
--- a/res/menu/people_options.xml
+++ b/res/menu/people_options.xml
@@ -49,4 +49,13 @@
         android:title="@string/menu_export_database"
         android:visible="false"
         android:showAsAction="never" />
+
+    <item
+        android:id="@+id/menu_delete"
+        android:title="@string/menu_deleteContact" />
+
+    <item
+        android:id="@+id/menu_share"
+        android:title="@string/menu_share" />
+
 </menu>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index c3bf79e..cb658b3 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -29,6 +29,10 @@
     <color name="actionbar_background_color">@color/primary_color</color>
     <color name="actionbar_background_color_dark">@color/primary_color_dark</color>
 
+    <color name="contextual_selection_bar_color">#616161</color>
+    <!-- Color of the status bar above the contextual selection bar. -->
+    <color name="contextual_selection_bar_status_bar_color">#424242</color>
+
     <color name="primary_color_dark">#0277bd</color>
     <color name="primary_color">#0288d1</color>
 
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index 4b9e83f..f6cb921 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -18,6 +18,7 @@
 
 import android.animation.ValueAnimator;
 import android.app.ActionBar;
+import android.app.Activity;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.TypedArray;
@@ -26,13 +27,17 @@
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout.LayoutParams;
 import android.widget.SearchView.OnCloseListener;
 import android.view.View.OnClickListener;
 import android.widget.EditText;
+import android.widget.TextView;
 import android.widget.Toolbar;
 
 import com.android.contacts.R;
@@ -48,7 +53,8 @@
         public abstract class Action {
             public static final int CHANGE_SEARCH_QUERY = 0;
             public static final int START_SEARCH_MODE = 1;
-            public static final int STOP_SEARCH_MODE = 2;
+            public static final int START_SELECTION_MODE = 2;
+            public static final int STOP_SEARCH_AND_SELECTION_MODE = 3;
         }
 
         void onAction(int action);
@@ -65,9 +71,11 @@
     private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
     private static final String EXTRA_KEY_QUERY = "navBar.query";
     private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
+    private static final String EXTRA_KEY_SELECTED_MODE = "navBar.selectionMode";
 
     private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
 
+    private boolean mSelectionMode;
     private boolean mSearchMode;
     private String mQueryString;
 
@@ -77,17 +85,23 @@
     /** The view that represents tabs when we are in landscape mode **/
     private View mLandscapeTabs;
     private View mSearchContainer;
+    private View mSelectionContainer;
 
     private int mMaxPortraitTabHeight;
     private int mMaxToolbarContentInsetStart;
 
-    private final Context mContext;
+    private final Activity mActivity;
     private final SharedPreferences mPrefs;
 
     private Listener mListener;
 
     private final ActionBar mActionBar;
     private final Toolbar mToolbar;
+    /**
+     *  Frame that contains the toolbar and draws the toolbar's background color. This is useful
+     *  for placing things behind the toolbar.
+     */
+    private final FrameLayout mToolBarFrame;
 
     private boolean mShowHomeIcon;
 
@@ -101,20 +115,21 @@
 
     private int mCurrentTab = TabState.DEFAULT;
 
-    public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
+    public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
             View portraitTabs, View landscapeTabs, Toolbar toolbar) {
-        mContext = context;
+        mActivity = activity;
         mListener = listener;
         mActionBar = actionBar;
-        mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
         mPortraitTabs = portraitTabs;
         mLandscapeTabs = landscapeTabs;
         mToolbar = toolbar;
+        mToolBarFrame = (FrameLayout) mToolbar.getParent();
         mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
-        mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon);
+        mShowHomeIcon = mActivity.getResources().getBoolean(R.bool.show_home_icon);
 
-        setupSearchView();
-        setupTabs(context);
+        setupSearchAndSelectionViews();
+        setupTabs(mActivity);
     }
 
     private void setupTabs(Context context) {
@@ -124,19 +139,19 @@
         // Hide tabs initially
         setPortraitTabHeight(0);
     }
-
-    private void setupSearchView() {
+    private void setupSearchAndSelectionViews() {
         final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService(
                 Context.LAYOUT_INFLATER_SERVICE);
+
+        // Setup search bar
         mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar,
                 /* attachToRoot = */ false);
         mSearchContainer.setVisibility(View.VISIBLE);
         mToolbar.addView(mSearchContainer);
-
-        mSearchContainer.setBackgroundColor(mContext.getResources().getColor(
+        mSearchContainer.setBackgroundColor(mActivity.getResources().getColor(
                 R.color.searchbox_background_color));
         mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view);
-        mSearchView.setHint(mContext.getString(R.string.hint_findContacts));
+        mSearchView.setHint(mActivity.getString(R.string.hint_findContacts));
         mSearchView.addTextChangedListener(new SearchTextWatcher());
         mSearchContainer.findViewById(R.id.search_close_button).setOnClickListener(
                 new OnClickListener() {
@@ -154,6 +169,22 @@
                 }
             }
         });
+
+        // Setup selection bar
+        mSelectionContainer = inflater.inflate(R.layout.selection_bar, mToolbar,
+                /* attachToRoot = */ false);
+        // Insert the selection container into mToolBarFrame behind the Toolbar, so that
+        // the Toolbar's MenuItems can appear on top of the selection container.
+        mToolBarFrame.addView(mSelectionContainer, 0);
+        mSelectionContainer.findViewById(R.id.selection_close).setOnClickListener(
+                new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        if (mListener != null) {
+                            mListener.onUpButtonPressed();
+                        }
+                    }
+                });
     }
 
     public void initialize(Bundle savedState, ContactsRequest request) {
@@ -163,6 +194,7 @@
             mCurrentTab = loadLastTabPreference();
         } else {
             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
+            mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE);
             mQueryString = savedState.getString(EXTRA_KEY_QUERY);
 
             // Just set to the field here.  The listener will be notified by update().
@@ -244,6 +276,13 @@
         return mSearchMode;
     }
 
+    /**
+     * @return Whether in selection mode, i.e. if the selection view is visible/expanded.
+     */
+    public boolean isSelectionMode() {
+        return mSelectionMode;
+    }
+
     public void setSearchMode(boolean flag) {
         if (mSearchMode != flag) {
             mSearchMode = flag;
@@ -265,6 +304,13 @@
         }
     }
 
+    public void setSelectionMode(boolean flag) {
+        if (mSelectionMode != flag) {
+            mSelectionMode = flag;
+            update(false /* skipAnimation */);
+        }
+    }
+
     public String getQueryString() {
         return mSearchMode ? mQueryString : null;
     }
@@ -288,25 +334,43 @@
     private void updateDisplayOptionsInner() {
         // All the flags we may change in this method.
         final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
-                | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
+                | ActionBar.DISPLAY_HOME_AS_UP;
 
         // The current flags set to the action bar.  (only the ones that we may change here)
         final int current = mActionBar.getDisplayOptions() & MASK;
 
+        final boolean isSearchOrSelectionMode = mSearchMode || mSelectionMode;
+
         // Build the new flags...
         int newFlags = 0;
-        if (mShowHomeIcon && !mSearchMode) {
+        if (mShowHomeIcon && !isSearchOrSelectionMode) {
             newFlags |= ActionBar.DISPLAY_SHOW_HOME;
         }
         if (mSearchMode) {
-            newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
+            // The search container is placed inside the toolbar. So we need to disable the
+            // Toolbar's content inset in order to allow the search container to be the width of
+            // the window.
             mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd());
-        } else {
+        }
+        if (!isSearchOrSelectionMode) {
             newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
             mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart,
                     mToolbar.getContentInsetEnd());
         }
 
+        if (mSelectionMode) {
+            // Minimize the horizontal width of the Toolbar since the selection container is placed
+            // behind the toolbar and its left hand side needs to be clickable.
+            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams();
+            params.width = LayoutParams.WRAP_CONTENT;
+            params.gravity = Gravity.END;
+            mToolbar.setLayoutParams(params);
+        } else {
+            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams();
+            params.width = LayoutParams.MATCH_PARENT;
+            params.gravity = Gravity.END;
+            mToolbar.setLayoutParams(params);
+        }
 
         if (current != newFlags) {
             // Pass the mask here to preserve other flags that we're not interested here.
@@ -315,43 +379,105 @@
     }
 
     private void update(boolean skipAnimation) {
-        final boolean isIconifiedChanging
+        updateStatusBarColor();
+
+        final boolean isSelectionModeChanging
+                = (mSelectionContainer.getParent() == null) == mSelectionMode;
+        final boolean isSearchModeChanging
                 = (mSearchContainer.getParent() == null) == mSearchMode;
-        if (isIconifiedChanging && !skipAnimation) {
+        final boolean isTabHeightChanging = isSearchModeChanging || isSelectionModeChanging;
+
+        // When skipAnimation=true, it is possible that we will switch from search mode
+        // to selection mode directly. So we need to remove the undesired container in addition
+        // to adding the desired container.
+        if (skipAnimation) {
+            if (isTabHeightChanging) {
+                mToolbar.removeView(mLandscapeTabs);
+                if (mSearchMode) {
+                    setPortraitTabHeight(0);
+                    mToolBarFrame.removeView(mSelectionContainer);
+                    addSearchContainer();
+                } else if (mSelectionMode) {
+                    setPortraitTabHeight(0);
+                    mToolbar.removeView(mSearchContainer);
+                    addSelectionContainer();
+                } else {
+                    setPortraitTabHeight(mMaxPortraitTabHeight);
+                    mToolbar.removeView(mSearchContainer);
+                    mToolBarFrame.removeView(mSelectionContainer);
+                    addLandscapeViewPagerTabs();
+                }
+                updateDisplayOptions(isSearchModeChanging);
+            }
+            return;
+        }
+
+        // Handle a switch to/from selection mode, due to UI interaction.
+        if (isSelectionModeChanging) {
+            mToolbar.removeView(mLandscapeTabs);
+            if (mSelectionMode) {
+                addSelectionContainer();
+                mSelectionContainer.setAlpha(0);
+                mSelectionContainer.animate().alpha(1);
+                animateTabHeightChange(mMaxPortraitTabHeight, 0);
+                updateDisplayOptions(isSearchModeChanging);
+            } else {
+                mSelectionContainer.setAlpha(1);
+                animateTabHeightChange(0, mMaxPortraitTabHeight);
+                mSelectionContainer.animate().alpha(0).withEndAction(new Runnable() {
+                    @Override
+                    public void run() {
+                        updateDisplayOptions(isSearchModeChanging);
+                        addLandscapeViewPagerTabs();
+                        mToolBarFrame.removeView(mSelectionContainer);
+                    }
+                });
+            }
+        }
+
+        // Handle a switch to/from search mode, due to UI interaction.
+        if (isSearchModeChanging) {
             mToolbar.removeView(mLandscapeTabs);
             if (mSearchMode) {
                 addSearchContainer();
                 mSearchContainer.setAlpha(0);
                 mSearchContainer.animate().alpha(1);
                 animateTabHeightChange(mMaxPortraitTabHeight, 0);
-                updateDisplayOptions(isIconifiedChanging);
+                updateDisplayOptions(isSearchModeChanging);
             } else {
                 mSearchContainer.setAlpha(1);
                 animateTabHeightChange(0, mMaxPortraitTabHeight);
                 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() {
                     @Override
                     public void run() {
-                        updateDisplayOptionsInner();
-                        updateDisplayOptions(isIconifiedChanging);
+                        updateDisplayOptions(isSearchModeChanging);
                         addLandscapeViewPagerTabs();
                         mToolbar.removeView(mSearchContainer);
                     }
                 });
             }
-            return;
         }
-        if (isIconifiedChanging && skipAnimation) {
-            mToolbar.removeView(mLandscapeTabs);
-            if (mSearchMode) {
-                setPortraitTabHeight(0);
-                addSearchContainer();
-            } else {
-                setPortraitTabHeight(mMaxPortraitTabHeight);
-                mToolbar.removeView(mSearchContainer);
-                addLandscapeViewPagerTabs();
-            }
+    }
+
+    public void setSelectionCount(int selectionCount) {
+        TextView textView = (TextView) mSelectionContainer.findViewById(R.id.selection_count_text);
+        if (selectionCount == 0) {
+            textView.setVisibility(View.GONE);
+        } else {
+            textView.setVisibility(View.VISIBLE);
         }
-        updateDisplayOptions(isIconifiedChanging);
+        textView.setText(String.valueOf(selectionCount));
+    }
+
+    private void updateStatusBarColor() {
+        if (mSelectionMode) {
+            int cabStatusBarColor = mActivity.getResources().getColor(
+                    R.color.contextual_selection_bar_status_bar_color);
+            mActivity.getWindow().setStatusBarColor(cabStatusBarColor);
+        } else {
+            int normalStatusBarColor = mActivity.getColor(R.color.primary_color_dark);
+            mActivity.getWindow().setStatusBarColor(normalStatusBarColor);
+        }
     }
 
     private void addLandscapeViewPagerTabs() {
@@ -366,24 +492,33 @@
         mToolbar.addView(mSearchContainer);
     }
 
-    private void updateDisplayOptions(boolean isIconifiedChanging) {
+    private void addSelectionContainer() {
+        mToolBarFrame.removeView(mSelectionContainer);
+        mToolBarFrame.addView(mSelectionContainer, 0);
+    }
+
+    private void updateDisplayOptions(boolean isSearchModeChanging) {
         if (mSearchMode) {
             setFocusOnSearchView();
             // Since we have the {@link SearchView} in a custom action bar, we must manually handle
             // expanding the {@link SearchView} when a search is initiated. Note that a side effect
             // of this method is that the {@link SearchView} query text is set to empty string.
-            if (isIconifiedChanging) {
+            if (isSearchModeChanging) {
                 final CharSequence queryText = mSearchView.getText();
                 if (!TextUtils.isEmpty(queryText)) {
                     mSearchView.setText(queryText);
                 }
             }
-            if (mListener != null) {
+        }
+        if (mListener != null) {
+            if (mSearchMode) {
                 mListener.onAction(Action.START_SEARCH_MODE);
             }
-        } else {
-            if (mListener != null) {
-                mListener.onAction(Action.STOP_SEARCH_MODE);
+            if (mSelectionMode) {
+                mListener.onAction(Action.START_SELECTION_MODE);
+            }
+            if (!mSearchMode && !mSelectionMode) {
+                mListener.onAction(Action.STOP_SEARCH_AND_SELECTION_MODE);
                 mListener.onSelectedTabChanged();
             }
         }
@@ -398,6 +533,7 @@
 
     public void onSaveInstanceState(Bundle outState) {
         outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
+        outState.putBoolean(EXTRA_KEY_SELECTED_MODE, mSelectionMode);
         outState.putString(EXTRA_KEY_QUERY, mQueryString);
         outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
     }
@@ -408,7 +544,7 @@
     }
 
     private void showInputMethod(View view) {
-        final InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
+        final InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(
                 Context.INPUT_METHOD_SERVICE);
         if (imm != null) {
             imm.showSoftInput(view, 0);
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 33e92e9..a3af101 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -20,6 +20,7 @@
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
@@ -61,11 +62,12 @@
 import com.android.contacts.common.list.ContactListFilter;
 import com.android.contacts.common.list.ContactListFilterController;
 import com.android.contacts.common.list.ContactTileAdapter.DisplayType;
+import com.android.contacts.list.MultiSelectContactsListFragment;
+import com.android.contacts.list.MultiSelectContactsListFragment.OnCheckBoxListActionListener;
 import com.android.contacts.list.ContactTileListFragment;
 import com.android.contacts.list.ContactsIntentResolver;
 import com.android.contacts.list.ContactsRequest;
 import com.android.contacts.list.ContactsUnavailableFragment;
-import com.android.contacts.list.DefaultContactBrowseListFragment;
 import com.android.contacts.common.list.DirectoryListLoader;
 import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment;
 import com.android.contacts.list.OnContactBrowserActionListener;
@@ -82,6 +84,7 @@
 import com.android.contacts.util.DialogManager;
 import com.android.contactsbind.HelpUtils;
 
+import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -124,7 +127,7 @@
     /**
      * Showing a list of Contacts. Also used for showing search results in search mode.
      */
-    private DefaultContactBrowseListFragment mAllFragment;
+    private MultiSelectContactsListFragment mAllFragment;
     private ContactTileListFragment mFavoritesFragment;
 
     /** ViewPager for swipe */
@@ -322,12 +325,12 @@
         // existing.
         mFavoritesFragment = (ContactTileListFragment)
                 fragmentManager.findFragmentByTag(FAVORITE_TAG);
-        mAllFragment = (DefaultContactBrowseListFragment)
+        mAllFragment = (MultiSelectContactsListFragment)
                 fragmentManager.findFragmentByTag(ALL_TAG);
 
         if (mFavoritesFragment == null) {
             mFavoritesFragment = new ContactTileListFragment();
-            mAllFragment = new DefaultContactBrowseListFragment();
+            mAllFragment = new MultiSelectContactsListFragment();
 
             transaction.add(R.id.tab_pager, mFavoritesFragment, FAVORITE_TAG);
             transaction.add(R.id.tab_pager, mAllFragment, ALL_TAG);
@@ -336,6 +339,7 @@
         mFavoritesFragment.setListener(mFavoritesFragmentListener);
 
         mAllFragment.setOnContactListActionListener(new ContactBrowserActionListener());
+        mAllFragment.setCheckBoxListListener(new CheckBoxListListener());
 
         // Hide all fragments for now.  We adjust visibility when we get onSelectedTabChanged()
         // from ActionBarAdapter.
@@ -505,13 +509,16 @@
     @Override
     public void onAction(int action) {
         switch (action) {
+            case ActionBarAdapter.Listener.Action.START_SELECTION_MODE:
+                mAllFragment.displayCheckBoxes(true);
+                // Fall through:
             case ActionBarAdapter.Listener.Action.START_SEARCH_MODE:
-                // Tell the fragments that we're in the search mode
+                // Tell the fragments that we're in the search mode or selection mode
                 configureFragments(false /* from request */);
                 updateFragmentsVisibility();
                 invalidateOptionsMenu();
                 break;
-            case ActionBarAdapter.Listener.Action.STOP_SEARCH_MODE:
+            case ActionBarAdapter.Listener.Action.STOP_SEARCH_AND_SELECTION_MODE:
                 setQueryTextToFragment("");
                 updateFragmentsVisibility();
                 invalidateOptionsMenu();
@@ -551,14 +558,15 @@
     private void updateFragmentsVisibility() {
         int tab = mActionBarAdapter.getCurrentTab();
 
-        if (mActionBarAdapter.isSearchMode()) {
-            mTabPagerAdapter.setSearchMode(true);
+        if (mActionBarAdapter.isSearchMode() || mActionBarAdapter.isSelectionMode()) {
+            mTabPagerAdapter.setTabsHidden(true);
         } else {
-            // No smooth scrolling if quitting from the search mode.
-            final boolean wasSearchMode = mTabPagerAdapter.isSearchMode();
-            mTabPagerAdapter.setSearchMode(false);
+            // No smooth scrolling if quitting from the search/selection mode.
+            final boolean wereTabsHidden = mTabPagerAdapter.areTabsHidden()
+                    || mActionBarAdapter.isSelectionMode();
+            mTabPagerAdapter.setTabsHidden(false);
             if (mTabPager.getCurrentItem() != tab) {
-                mTabPager.setCurrentItem(tab, !wasSearchMode);
+                mTabPager.setCurrentItem(tab, !wereTabsHidden);
             }
         }
         invalidateOptionsMenu();
@@ -601,14 +609,14 @@
 
         @Override
         public void onPageScrollStateChanged(int state) {
-            if (!mTabPagerAdapter.isSearchMode()) {
+            if (!mTabPagerAdapter.areTabsHidden()) {
                 mViewPagerTabs.onPageScrollStateChanged(state);
             }
         }
 
         @Override
         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-            if (!mTabPagerAdapter.isSearchMode()) {
+            if (!mTabPagerAdapter.areTabsHidden()) {
                 mViewPagerTabs.onPageScrolled(position, positionOffset, positionOffsetPixels);
             }
         }
@@ -616,7 +624,7 @@
         @Override
         public void onPageSelected(int position) {
             // Make sure not in the search mode, in which case position != TabState.ordinal().
-            if (!mTabPagerAdapter.isSearchMode()) {
+            if (!mTabPagerAdapter.areTabsHidden()) {
                 mActionBarAdapter.setCurrentTab(position, false);
                 mViewPagerTabs.onPageSelected(position);
                 showEmptyStateForTab(position);
@@ -639,7 +647,7 @@
         private final FragmentManager mFragmentManager;
         private FragmentTransaction mCurTransaction = null;
 
-        private boolean mTabPagerAdapterSearchMode;
+        private boolean mAreTabsHiddenInTabPager;
 
         private Fragment mCurrentPrimaryItem;
 
@@ -647,27 +655,27 @@
             mFragmentManager = getFragmentManager();
         }
 
-        public boolean isSearchMode() {
-            return mTabPagerAdapterSearchMode;
+        public boolean areTabsHidden() {
+            return mAreTabsHiddenInTabPager;
         }
 
-        public void setSearchMode(boolean searchMode) {
-            if (searchMode == mTabPagerAdapterSearchMode) {
+        public void setTabsHidden(boolean hideTabs) {
+            if (hideTabs == mAreTabsHiddenInTabPager) {
                 return;
             }
-            mTabPagerAdapterSearchMode = searchMode;
+            mAreTabsHiddenInTabPager = hideTabs;
             notifyDataSetChanged();
         }
 
         @Override
         public int getCount() {
-            return mTabPagerAdapterSearchMode ? 1 : TabState.COUNT;
+            return mAreTabsHiddenInTabPager ? 1 : TabState.COUNT;
         }
 
         /** Gets called when the number of items changes. */
         @Override
         public int getItemPosition(Object object) {
-            if (mTabPagerAdapterSearchMode) {
+            if (mAreTabsHiddenInTabPager) {
                 if (object == mAllFragment) {
                     return 0; // Only 1 page in search mode
                 }
@@ -688,7 +696,7 @@
 
         private Fragment getFragment(int position) {
             position = getTabPositionForTextDirection(position);
-            if (mTabPagerAdapterSearchMode) {
+            if (mAreTabsHiddenInTabPager) {
                 if (position != 0) {
                     // This has only been observed in monkey tests.
                     // Let's log this issue, but not crash
@@ -919,6 +927,20 @@
         }
     }
 
+    private final class CheckBoxListListener implements OnCheckBoxListActionListener {
+        @Override
+        public void onStartDisplayingCheckBoxes() {
+            mActionBarAdapter.setSelectionMode(true);
+            invalidateOptionsMenu();
+        }
+
+        @Override
+        public void onSelectedContactIdsChanged() {
+            mActionBarAdapter.setSelectionCount(mAllFragment.getSelectedContactIds().size());
+            invalidateOptionsMenu();
+        }
+    }
+
     private class ContactsUnavailableFragmentListener
             implements OnContactsUnavailableActionListener {
         ContactsUnavailableFragmentListener() {}
@@ -1007,8 +1029,9 @@
         final MenuItem clearFrequentsMenu = menu.findItem(R.id.menu_clear_frequents);
         final MenuItem helpMenu = menu.findItem(R.id.menu_help);
 
-        final boolean isSearchMode = mActionBarAdapter.isSearchMode();
-        if (isSearchMode) {
+        final boolean isSearchOrSelectionMode = mActionBarAdapter.isSearchMode()
+                || mActionBarAdapter.isSelectionMode();
+        if (isSearchOrSelectionMode) {
             contactsFilterMenu.setVisible(false);
             clearFrequentsMenu.setVisible(false);
             helpMenu.setVisible(false);
@@ -1025,13 +1048,18 @@
             }
             helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
         }
-        final boolean showMiscOptions = !isSearchMode;
+        final boolean showMiscOptions = !isSearchOrSelectionMode;
         makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions);
         makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions);
         makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions);
         makeMenuItemVisible(menu, R.id.menu_settings,
                 showMiscOptions && !ContactsPreferenceActivity.isEmpty(this));
 
+        final boolean showSelectedContactOptions = mActionBarAdapter.isSelectionMode()
+                && mAllFragment.getSelectedContactIds().size() != 0;
+        makeMenuItemVisible(menu, R.id.menu_share, showSelectedContactOptions);
+        makeMenuItemVisible(menu, R.id.menu_delete, showSelectedContactOptions);
+
         // Debug options need to be visible even in search mode.
         makeMenuItemVisible(menu, R.id.export_database, mEnableDebugMenuOptions);
 
@@ -1093,6 +1121,9 @@
                 onSearchRequested();
                 return true;
             }
+            case R.id.menu_share:
+                shareSelectedContacts();
+                return true;
             case R.id.menu_import_export: {
                 ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(),
                         PeopleActivity.class);
@@ -1126,10 +1157,37 @@
 
     @Override
     public boolean onSearchRequested() { // Search key pressed.
-        mActionBarAdapter.setSearchMode(true);
+        if (!mActionBarAdapter.isSelectionMode()) {
+            mActionBarAdapter.setSearchMode(true);
+        }
         return true;
     }
 
+    /**
+     * Share all contacts that are currently selected in mAllFragment. This method is pretty
+     * inefficient for handling large numbers of contacts. I don't expect this to be a problem.
+     */
+    private void shareSelectedContacts() {
+        final StringBuilder uriListBuilder = new StringBuilder();
+        boolean firstIteration = true;
+        for (Long contactId : mAllFragment.getSelectedContactIds()) {
+            if (!firstIteration)
+                uriListBuilder.append(':');
+            final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+            final Uri lookupUri = Contacts.getLookupUri(getContentResolver(), contactUri);
+            List<String> pathSegments = lookupUri.getPathSegments();
+            uriListBuilder.append(pathSegments.get(pathSegments.size() - 2));
+            firstIteration = false;
+        }
+        final Uri uri = Uri.withAppendedPath(
+                Contacts.CONTENT_MULTI_VCARD_URI,
+                Uri.encode(uriListBuilder.toString()));
+        final Intent intent = new Intent(Intent.ACTION_SEND);
+        intent.setType(Contacts.CONTENT_VCARD_TYPE);
+        intent.putExtra(Intent.EXTRA_STREAM, uri);
+        ImplicitIntentsUtil.startActivityOutsideApp(this, intent);
+    }
+
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
@@ -1159,34 +1217,22 @@
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         // TODO move to the fragment
-        switch (keyCode) {
-//            case KeyEvent.KEYCODE_CALL: {
-//                if (callSelection()) {
-//                    return true;
-//                }
-//                break;
-//            }
 
-            case KeyEvent.KEYCODE_DEL: {
-                if (deleteSelection()) {
-                    return true;
-                }
-                break;
+        // Bring up the search UI if the user starts typing
+        final int unicodeChar = event.getUnicodeChar();
+        if ((unicodeChar != 0)
+                // If COMBINING_ACCENT is set, it's not a unicode character.
+                && ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) == 0)
+                && !Character.isWhitespace(unicodeChar)) {
+            if (mActionBarAdapter.isSelectionMode()) {
+                // Ignore keyboard input when in selection mode.
+                return true;
             }
-            default: {
-                // Bring up the search UI if the user starts typing
-                final int unicodeChar = event.getUnicodeChar();
-                if ((unicodeChar != 0)
-                        // If COMBINING_ACCENT is set, it's not a unicode character.
-                        && ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) == 0)
-                        && !Character.isWhitespace(unicodeChar)) {
-                    String query = new String(new int[]{ unicodeChar }, 0, 1);
-                    if (!mActionBarAdapter.isSearchMode()) {
-                        mActionBarAdapter.setSearchMode(true);
-                        mActionBarAdapter.setQueryString(query);
-                        return true;
-                    }
-                }
+            String query = new String(new int[]{unicodeChar}, 0, 1);
+            if (!mActionBarAdapter.isSearchMode()) {
+                mActionBarAdapter.setSearchMode(true);
+                mActionBarAdapter.setQueryString(query);
+                return true;
             }
         }
 
@@ -1195,28 +1241,16 @@
 
     @Override
     public void onBackPressed() {
-        if (mActionBarAdapter.isSearchMode()) {
+        if (mActionBarAdapter.isSelectionMode()) {
+            mActionBarAdapter.setSelectionMode(false);
+            mAllFragment.displayCheckBoxes(false);
+        } else if (mActionBarAdapter.isSearchMode()) {
             mActionBarAdapter.setSearchMode(false);
         } else {
             super.onBackPressed();
         }
     }
 
-    private boolean deleteSelection() {
-        // TODO move to the fragment
-//        if (mActionCode == ContactsRequest.ACTION_DEFAULT) {
-//            final int position = mListView.getSelectedItemPosition();
-//            if (position != ListView.INVALID_POSITION) {
-//                Uri contactUri = getContactUri(position);
-//                if (contactUri != null) {
-//                    doContactDelete(contactUri);
-//                    return true;
-//                }
-//            }
-//        }
-        return false;
-    }
-
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
diff --git a/src/com/android/contacts/list/MultiSelectContactsListFragment.java b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
new file mode 100644
index 0000000..bdd0a6f
--- /dev/null
+++ b/src/com/android/contacts/list/MultiSelectContactsListFragment.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ContactListAdapter;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.list.DefaultContactListAdapter;
+import com.android.contacts.list.MultiSelectEntryContactListAdapter.SelectedContactsListener;
+
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.TreeSet;
+
+/**
+ * Fragment containing a contact list used for browsing contacts and optionally selecting
+ * multiple contacts via checkboxes.
+ */
+public class MultiSelectContactsListFragment extends DefaultContactBrowseListFragment
+        implements SelectedContactsListener {
+
+    public interface OnCheckBoxListActionListener {
+        void onStartDisplayingCheckBoxes();
+        void onSelectedContactIdsChanged();
+    }
+
+    private static final String EXTRA_KEY_SELECTED_CONTACTS = "selected_contacts";
+
+    private OnCheckBoxListActionListener mCheckBoxListListener;
+
+    public void setCheckBoxListListener(OnCheckBoxListActionListener checkBoxListListener) {
+        mCheckBoxListListener = checkBoxListListener;
+    }
+
+    @Override
+    public void onSelectedContactsChanged() {
+        if (mCheckBoxListListener != null) {
+            mCheckBoxListListener.onSelectedContactIdsChanged();
+        }
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        if (savedInstanceState != null) {
+            final TreeSet<Long> selectedContactIds = (TreeSet<Long>)
+                    savedInstanceState.getSerializable(EXTRA_KEY_SELECTED_CONTACTS);
+            getAdapter().setSelectedContactIds(selectedContactIds);
+            if (mCheckBoxListListener != null) {
+                mCheckBoxListListener.onSelectedContactIdsChanged();
+            }
+        }
+    }
+
+    public TreeSet<Long> getSelectedContactIds() {
+        final MultiSelectEntryContactListAdapter adapter = getAdapter();
+        return adapter.getSelectedContactIds();
+    }
+
+    @Override
+    public MultiSelectEntryContactListAdapter getAdapter() {
+        return (MultiSelectEntryContactListAdapter) super.getAdapter();
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+        getAdapter().setSelectedContactsListener(this);
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putSerializable(EXTRA_KEY_SELECTED_CONTACTS, getSelectedContactIds());
+    }
+
+    public void displayCheckBoxes(boolean displayCheckBoxes) {
+        getAdapter().setDisplayCheckBoxes(displayCheckBoxes);
+        if (!displayCheckBoxes) {
+            getAdapter().setSelectedContactIds(new TreeSet<Long>());
+        }
+    }
+    @Override
+    protected boolean onItemLongClick(int position, long id) {
+        final MultiSelectEntryContactListAdapter adapter = getAdapter();
+        adapter.setDisplayCheckBoxes(true);
+        if (mCheckBoxListListener != null) {
+            mCheckBoxListListener.onStartDisplayingCheckBoxes();
+        }
+        final Uri uri = getAdapter().getContactUri(position);
+        if (position > 0 && uri != null) {
+            final String contactId = uri.getLastPathSegment();
+            getAdapter().toggleSelectionOfContactId(Long.valueOf(contactId));
+        }
+        return true;
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        final Uri uri = getAdapter().getContactUri(position);
+        if (uri == null) {
+            return;
+        }
+        if (getAdapter().isDisplayingCheckBoxes()) {
+            final String contactId = uri.getLastPathSegment();
+            getAdapter().toggleSelectionOfContactId(Long.valueOf(contactId));
+        } else {
+            super.onItemClick(position, id);
+        }
+    }
+
+    @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
new file mode 100644
index 0000000..5acd570
--- /dev/null
+++ b/src/com/android/contacts/list/MultiSelectEntryContactListAdapter.java
@@ -0,0 +1,143 @@
+/*
+ * 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.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();
+    }
+
+    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) {
+        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);
+    }
+
+    private void bindCheckBox(ContactListItemView view, Cursor cursor, int position) {
+        // Disable clicking on the first entry when showing check boxes. We do this by
+        // telling the view to handle clicking itself.
+        view.setClickable(position == 0 && mDisplayCheckBoxes);
+        // Only show checkboxes is mDisplayCheckBoxes is enabled. Also, never show the
+        // checkbox for the first entry in the list (the Me profile).
+        if (position == 0 || !mDisplayCheckBoxes) {
+            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);
+            }
+            if (mSelectedContactsListener != null) {
+                mSelectedContactsListener.onSelectedContactsChanged();
+            }
+        }
+    };
+}