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();
+ }
+ }
+ };
+}