Home | History | Annotate | Download | only in activities
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.activities;
     18 
     19 import android.animation.ValueAnimator;
     20 import android.app.ActionBar;
     21 import android.app.Activity;
     22 import android.content.Context;
     23 import android.content.SharedPreferences;
     24 import android.content.res.TypedArray;
     25 import android.os.Bundle;
     26 import android.preference.PreferenceManager;
     27 import android.support.v4.content.ContextCompat;
     28 import android.text.Editable;
     29 import android.text.TextUtils;
     30 import android.text.TextWatcher;
     31 import android.view.Gravity;
     32 import android.view.LayoutInflater;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 import android.view.inputmethod.InputMethodManager;
     36 import android.widget.FrameLayout;
     37 import android.widget.LinearLayout.LayoutParams;
     38 import android.widget.SearchView.OnCloseListener;
     39 import android.view.View.OnClickListener;
     40 import android.widget.EditText;
     41 import android.widget.TextView;
     42 import android.widget.Toolbar;
     43 
     44 import com.android.contacts.R;
     45 import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
     46 import com.android.contacts.common.compat.CompatUtils;
     47 import com.android.contacts.list.ContactsRequest;
     48 
     49 /**
     50  * Adapter for the action bar at the top of the Contacts activity.
     51  */
     52 public class ActionBarAdapter implements OnCloseListener {
     53 
     54     public interface Listener {
     55         public abstract class Action {
     56             public static final int CHANGE_SEARCH_QUERY = 0;
     57             public static final int START_SEARCH_MODE = 1;
     58             public static final int START_SELECTION_MODE = 2;
     59             public static final int STOP_SEARCH_AND_SELECTION_MODE = 3;
     60             public static final int BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE = 4;
     61         }
     62 
     63         void onAction(int action);
     64 
     65         /**
     66          * Called when the user selects a tab.  The new tab can be obtained using
     67          * {@link #getCurrentTab}.
     68          */
     69         void onSelectedTabChanged();
     70 
     71         void onUpButtonPressed();
     72     }
     73 
     74     private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
     75     private static final String EXTRA_KEY_QUERY = "navBar.query";
     76     private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
     77     private static final String EXTRA_KEY_SELECTED_MODE = "navBar.selectionMode";
     78 
     79     private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
     80 
     81     private boolean mSelectionMode;
     82     private boolean mSearchMode;
     83     private String mQueryString;
     84 
     85     private EditText mSearchView;
     86     private View mClearSearchView;
     87     /** The view that represents tabs when we are in portrait mode **/
     88     private View mPortraitTabs;
     89     /** The view that represents tabs when we are in landscape mode **/
     90     private View mLandscapeTabs;
     91     private View mSearchContainer;
     92     private View mSelectionContainer;
     93 
     94     private int mMaxPortraitTabHeight;
     95     private int mMaxToolbarContentInsetStart;
     96 
     97     private final Activity mActivity;
     98     private final SharedPreferences mPrefs;
     99 
    100     private Listener mListener;
    101 
    102     private final ActionBar mActionBar;
    103     private final Toolbar mToolbar;
    104     /**
    105      *  Frame that contains the toolbar and draws the toolbar's background color. This is useful
    106      *  for placing things behind the toolbar.
    107      */
    108     private final FrameLayout mToolBarFrame;
    109 
    110     private boolean mShowHomeIcon;
    111 
    112     public interface TabState {
    113         public static int FAVORITES = 0;
    114         public static int ALL = 1;
    115 
    116         public static int COUNT = 2;
    117         public static int DEFAULT = ALL;
    118     }
    119 
    120     private int mCurrentTab = TabState.DEFAULT;
    121 
    122     public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
    123             View portraitTabs, View landscapeTabs, Toolbar toolbar) {
    124         mActivity = activity;
    125         mListener = listener;
    126         mActionBar = actionBar;
    127         mPrefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
    128         mPortraitTabs = portraitTabs;
    129         mLandscapeTabs = landscapeTabs;
    130         mToolbar = toolbar;
    131         mToolBarFrame = (FrameLayout) mToolbar.getParent();
    132         mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
    133         mShowHomeIcon = mActivity.getResources().getBoolean(R.bool.show_home_icon);
    134 
    135         setupSearchAndSelectionViews();
    136         setupTabs(mActivity);
    137     }
    138 
    139     private void setupTabs(Context context) {
    140         final TypedArray attributeArray = context.obtainStyledAttributes(
    141                 new int[]{android.R.attr.actionBarSize});
    142         mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0);
    143         // Hide tabs initially
    144         setPortraitTabHeight(0);
    145     }
    146 
    147     private void setupSearchAndSelectionViews() {
    148         final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService(
    149                 Context.LAYOUT_INFLATER_SERVICE);
    150 
    151         // Setup search bar
    152         mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar,
    153                 /* attachToRoot = */ false);
    154         mSearchContainer.setVisibility(View.VISIBLE);
    155         mToolbar.addView(mSearchContainer);
    156         mSearchContainer.setBackgroundColor(mActivity.getResources().getColor(
    157                 R.color.searchbox_background_color));
    158         mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view);
    159         mSearchView.setHint(mActivity.getString(R.string.hint_findContacts));
    160         mSearchView.addTextChangedListener(new SearchTextWatcher());
    161         mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener(
    162                 new OnClickListener() {
    163             @Override
    164             public void onClick(View v) {
    165                 if (mListener != null) {
    166                     mListener.onUpButtonPressed();
    167                 }
    168             }
    169         });
    170 
    171         mClearSearchView = mSearchContainer.findViewById(R.id.search_close_button);
    172         mClearSearchView.setOnClickListener(
    173                 new OnClickListener() {
    174             @Override
    175             public void onClick(View v) {
    176                 setQueryString(null);
    177             }
    178         });
    179 
    180         // Setup selection bar
    181         mSelectionContainer = inflater.inflate(R.layout.selection_bar, mToolbar,
    182                 /* attachToRoot = */ false);
    183         // Insert the selection container into mToolBarFrame behind the Toolbar, so that
    184         // the Toolbar's MenuItems can appear on top of the selection container.
    185         mToolBarFrame.addView(mSelectionContainer, 0);
    186         mSelectionContainer.findViewById(R.id.selection_close).setOnClickListener(
    187                 new OnClickListener() {
    188                     @Override
    189                     public void onClick(View v) {
    190                         if (mListener != null) {
    191                             mListener.onUpButtonPressed();
    192                         }
    193                     }
    194                 });
    195     }
    196 
    197     public void initialize(Bundle savedState, ContactsRequest request) {
    198         if (savedState == null) {
    199             mSearchMode = request.isSearchMode();
    200             mQueryString = request.getQueryString();
    201             mCurrentTab = loadLastTabPreference();
    202             mSelectionMode = false;
    203         } else {
    204             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
    205             mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE);
    206             mQueryString = savedState.getString(EXTRA_KEY_QUERY);
    207 
    208             // Just set to the field here.  The listener will be notified by update().
    209             mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
    210         }
    211         if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) {
    212             // Invalid tab index was saved (b/12938207). Restore the default.
    213             mCurrentTab = TabState.DEFAULT;
    214         }
    215         // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
    216         // search mode.
    217         update(true /* skipAnimation */);
    218         // Expanding the {@link SearchView} clears the query, so set the query from the
    219         // {@link ContactsRequest} after it has been expanded, if applicable.
    220         if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
    221             setQueryString(mQueryString);
    222         }
    223     }
    224 
    225     public void setListener(Listener listener) {
    226         mListener = listener;
    227     }
    228 
    229     private class SearchTextWatcher implements TextWatcher {
    230 
    231         @Override
    232         public void onTextChanged(CharSequence queryString, int start, int before, int count) {
    233             if (queryString.equals(mQueryString)) {
    234                 return;
    235             }
    236             mQueryString = queryString.toString();
    237             if (!mSearchMode) {
    238                 if (!TextUtils.isEmpty(queryString)) {
    239                     setSearchMode(true);
    240                 }
    241             } else if (mListener != null) {
    242                 mListener.onAction(Action.CHANGE_SEARCH_QUERY);
    243             }
    244             mClearSearchView.setVisibility(
    245                     TextUtils.isEmpty(queryString) ? View.GONE : View.VISIBLE);
    246         }
    247 
    248         @Override
    249         public void afterTextChanged(Editable s) {}
    250 
    251         @Override
    252         public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
    253     }
    254 
    255     /**
    256      * Save the current tab selection, and notify the listener.
    257      */
    258     public void setCurrentTab(int tab) {
    259         setCurrentTab(tab, true);
    260     }
    261 
    262     /**
    263      * Save the current tab selection.
    264      */
    265     public void setCurrentTab(int tab, boolean notifyListener) {
    266         if (tab == mCurrentTab) {
    267             return;
    268         }
    269         mCurrentTab = tab;
    270 
    271         if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
    272         saveLastTabPreference(mCurrentTab);
    273     }
    274 
    275     public int getCurrentTab() {
    276         return mCurrentTab;
    277     }
    278 
    279     /**
    280      * @return Whether in search mode, i.e. if the search view is visible/expanded.
    281      *
    282      * Note even if the action bar is in search mode, if the query is empty, the search fragment
    283      * will not be in search mode.
    284      */
    285     public boolean isSearchMode() {
    286         return mSearchMode;
    287     }
    288 
    289     /**
    290      * @return Whether in selection mode, i.e. if the selection view is visible/expanded.
    291      */
    292     public boolean isSelectionMode() {
    293         return mSelectionMode;
    294     }
    295 
    296     public void setSearchMode(boolean flag) {
    297         if (mSearchMode != flag) {
    298             mSearchMode = flag;
    299             update(false /* skipAnimation */);
    300             if (mSearchView == null) {
    301                 return;
    302             }
    303             if (mSearchMode) {
    304                 mSearchView.setEnabled(true);
    305                 setFocusOnSearchView();
    306             } else {
    307                 // Disable search view, so that it doesn't keep the IME visible.
    308                 mSearchView.setEnabled(false);
    309             }
    310             setQueryString(null);
    311         } else if (flag) {
    312             // Everything is already set up. Still make sure the keyboard is up
    313             if (mSearchView != null) setFocusOnSearchView();
    314         }
    315     }
    316 
    317     public void setSelectionMode(boolean flag) {
    318         if (mSelectionMode != flag) {
    319             mSelectionMode = flag;
    320             update(false /* skipAnimation */);
    321         }
    322     }
    323 
    324     public String getQueryString() {
    325         return mSearchMode ? mQueryString : null;
    326     }
    327 
    328     public void setQueryString(String query) {
    329         mQueryString = query;
    330         if (mSearchView != null) {
    331             mSearchView.setText(query);
    332             // When programmatically entering text into the search view, the most reasonable
    333             // place for the cursor is after all the text.
    334             mSearchView.setSelection(mSearchView.getText() == null ?
    335                     0 : mSearchView.getText().length());
    336         }
    337     }
    338 
    339     /** @return true if the "UP" icon is showing. */
    340     public boolean isUpShowing() {
    341         return mSearchMode; // Only shown on the search mode.
    342     }
    343 
    344     private void updateDisplayOptionsInner() {
    345         // All the flags we may change in this method.
    346         final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
    347                 | ActionBar.DISPLAY_HOME_AS_UP;
    348 
    349         // The current flags set to the action bar.  (only the ones that we may change here)
    350         final int current = mActionBar.getDisplayOptions() & MASK;
    351 
    352         final boolean isSearchOrSelectionMode = mSearchMode || mSelectionMode;
    353 
    354         // Build the new flags...
    355         int newFlags = 0;
    356         if (mShowHomeIcon && !isSearchOrSelectionMode) {
    357             newFlags |= ActionBar.DISPLAY_SHOW_HOME;
    358         }
    359         if (mSearchMode && !mSelectionMode) {
    360             // The search container is placed inside the toolbar. So we need to disable the
    361             // Toolbar's content inset in order to allow the search container to be the width of
    362             // the window.
    363             mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd());
    364         }
    365         if (!isSearchOrSelectionMode) {
    366             newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
    367             mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart,
    368                     mToolbar.getContentInsetEnd());
    369         }
    370 
    371         if (mSelectionMode) {
    372             // Minimize the horizontal width of the Toolbar since the selection container is placed
    373             // behind the toolbar and its left hand side needs to be clickable.
    374             FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams();
    375             params.width = LayoutParams.WRAP_CONTENT;
    376             params.gravity = Gravity.END;
    377             mToolbar.setLayoutParams(params);
    378         } else {
    379             FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams();
    380             params.width = LayoutParams.MATCH_PARENT;
    381             params.gravity = Gravity.END;
    382             mToolbar.setLayoutParams(params);
    383         }
    384 
    385         if (current != newFlags) {
    386             // Pass the mask here to preserve other flags that we're not interested here.
    387             mActionBar.setDisplayOptions(newFlags, MASK);
    388         }
    389     }
    390 
    391     private void update(boolean skipAnimation) {
    392         updateStatusBarColor();
    393 
    394         final boolean isSelectionModeChanging
    395                 = (mSelectionContainer.getParent() == null) == mSelectionMode;
    396         final boolean isSwitchingFromSearchToSelection =
    397                 mSearchMode && isSelectionModeChanging || mSearchMode && mSelectionMode;
    398         final boolean isSearchModeChanging
    399                 = (mSearchContainer.getParent() == null) == mSearchMode;
    400         final boolean isTabHeightChanging = isSearchModeChanging || isSelectionModeChanging;
    401 
    402         // When skipAnimation=true, it is possible that we will switch from search mode
    403         // to selection mode directly. So we need to remove the undesired container in addition
    404         // to adding the desired container.
    405         if (skipAnimation || isSwitchingFromSearchToSelection) {
    406             if (isTabHeightChanging || isSwitchingFromSearchToSelection) {
    407                 mToolbar.removeView(mLandscapeTabs);
    408                 mToolbar.removeView(mSearchContainer);
    409                 mToolBarFrame.removeView(mSelectionContainer);
    410                 if (mSelectionMode) {
    411                     setPortraitTabHeight(0);
    412                     addSelectionContainer();
    413                 } else if (mSearchMode) {
    414                     setPortraitTabHeight(0);
    415                     addSearchContainer();
    416                 } else {
    417                     setPortraitTabHeight(mMaxPortraitTabHeight);
    418                     addLandscapeViewPagerTabs();
    419                 }
    420                 updateDisplayOptions(isSearchModeChanging);
    421             }
    422             return;
    423         }
    424 
    425         // Handle a switch to/from selection mode, due to UI interaction.
    426         if (isSelectionModeChanging) {
    427             mToolbar.removeView(mLandscapeTabs);
    428             if (mSelectionMode) {
    429                 addSelectionContainer();
    430                 mSelectionContainer.setAlpha(0);
    431                 mSelectionContainer.animate().alpha(1);
    432                 animateTabHeightChange(mMaxPortraitTabHeight, 0);
    433                 updateDisplayOptions(isSearchModeChanging);
    434             } else {
    435                 if (mListener != null) {
    436                     mListener.onAction(Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE);
    437                 }
    438                 mSelectionContainer.setAlpha(1);
    439                 animateTabHeightChange(0, mMaxPortraitTabHeight);
    440                 mSelectionContainer.animate().alpha(0).withEndAction(new Runnable() {
    441                     @Override
    442                     public void run() {
    443                         updateDisplayOptions(isSearchModeChanging);
    444                         addLandscapeViewPagerTabs();
    445                         mToolBarFrame.removeView(mSelectionContainer);
    446                     }
    447                 });
    448             }
    449         }
    450 
    451         // Handle a switch to/from search mode, due to UI interaction.
    452         if (isSearchModeChanging) {
    453             mToolbar.removeView(mLandscapeTabs);
    454             if (mSearchMode) {
    455                 addSearchContainer();
    456                 mSearchContainer.setAlpha(0);
    457                 mSearchContainer.animate().alpha(1);
    458                 animateTabHeightChange(mMaxPortraitTabHeight, 0);
    459                 updateDisplayOptions(isSearchModeChanging);
    460             } else {
    461                 mSearchContainer.setAlpha(1);
    462                 animateTabHeightChange(0, mMaxPortraitTabHeight);
    463                 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() {
    464                     @Override
    465                     public void run() {
    466                         updateDisplayOptions(isSearchModeChanging);
    467                         addLandscapeViewPagerTabs();
    468                         mToolbar.removeView(mSearchContainer);
    469                     }
    470                 });
    471             }
    472         }
    473     }
    474 
    475     public void setSelectionCount(int selectionCount) {
    476         TextView textView = (TextView) mSelectionContainer.findViewById(R.id.selection_count_text);
    477         if (selectionCount == 0) {
    478             textView.setVisibility(View.GONE);
    479         } else {
    480             textView.setVisibility(View.VISIBLE);
    481         }
    482         textView.setText(String.valueOf(selectionCount));
    483     }
    484 
    485     private void updateStatusBarColor() {
    486         if (!CompatUtils.isLollipopCompatible()) {
    487             return; // we can't change the status bar color prior to Lollipop
    488         }
    489         if (mSelectionMode) {
    490             final int cabStatusBarColor = mActivity.getResources().getColor(
    491                     R.color.contextual_selection_bar_status_bar_color);
    492             mActivity.getWindow().setStatusBarColor(cabStatusBarColor);
    493         } else {
    494             final int normalStatusBarColor = ContextCompat.getColor(
    495                     mActivity, R.color.primary_color_dark);
    496             mActivity.getWindow().setStatusBarColor(normalStatusBarColor);
    497         }
    498     }
    499 
    500     private void addLandscapeViewPagerTabs() {
    501         if (mLandscapeTabs != null) {
    502             mToolbar.removeView(mLandscapeTabs);
    503             mToolbar.addView(mLandscapeTabs);
    504         }
    505     }
    506 
    507     private void addSearchContainer() {
    508         mToolbar.removeView(mSearchContainer);
    509         mToolbar.addView(mSearchContainer);
    510         mSearchContainer.setAlpha(1);
    511     }
    512 
    513     private void addSelectionContainer() {
    514         mToolBarFrame.removeView(mSelectionContainer);
    515         mToolBarFrame.addView(mSelectionContainer, 0);
    516         mSelectionContainer.setAlpha(1);
    517     }
    518 
    519     private void updateDisplayOptions(boolean isSearchModeChanging) {
    520         if (mSearchMode && !mSelectionMode) {
    521             setFocusOnSearchView();
    522             // Since we have the {@link SearchView} in a custom action bar, we must manually handle
    523             // expanding the {@link SearchView} when a search is initiated. Note that a side effect
    524             // of this method is that the {@link SearchView} query text is set to empty string.
    525             if (isSearchModeChanging) {
    526                 final CharSequence queryText = mSearchView.getText();
    527                 if (!TextUtils.isEmpty(queryText)) {
    528                     mSearchView.setText(queryText);
    529                 }
    530             }
    531         }
    532         if (mListener != null) {
    533             if (mSearchMode) {
    534                 mListener.onAction(Action.START_SEARCH_MODE);
    535             }
    536             if (mSelectionMode) {
    537                 mListener.onAction(Action.START_SELECTION_MODE);
    538             }
    539             if (!mSearchMode && !mSelectionMode) {
    540                 mListener.onAction(Action.STOP_SEARCH_AND_SELECTION_MODE);
    541                 mListener.onSelectedTabChanged();
    542             }
    543         }
    544         updateDisplayOptionsInner();
    545     }
    546 
    547     @Override
    548     public boolean onClose() {
    549         setSearchMode(false);
    550         return false;
    551     }
    552 
    553     public void onSaveInstanceState(Bundle outState) {
    554         outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
    555         outState.putBoolean(EXTRA_KEY_SELECTED_MODE, mSelectionMode);
    556         outState.putString(EXTRA_KEY_QUERY, mQueryString);
    557         outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
    558     }
    559 
    560     public void setFocusOnSearchView() {
    561         mSearchView.requestFocus();
    562         showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
    563     }
    564 
    565     private void showInputMethod(View view) {
    566         final InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(
    567                 Context.INPUT_METHOD_SERVICE);
    568         if (imm != null) {
    569             imm.showSoftInput(view, 0);
    570         }
    571     }
    572 
    573     private void saveLastTabPreference(int tab) {
    574         mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
    575     }
    576 
    577     private int loadLastTabPreference() {
    578         try {
    579             return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT);
    580         } catch (IllegalArgumentException e) {
    581             // Preference is corrupt?
    582             return TabState.DEFAULT;
    583         }
    584     }
    585 
    586     private void animateTabHeightChange(int start, int end) {
    587         if (mPortraitTabs == null) {
    588             return;
    589         }
    590         final ValueAnimator animator = ValueAnimator.ofInt(start, end);
    591         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    592             @Override
    593             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    594                 int value = (Integer) valueAnimator.getAnimatedValue();
    595                 setPortraitTabHeight(value);
    596             }
    597         });
    598         animator.setDuration(100).start();
    599     }
    600 
    601     private void setPortraitTabHeight(int height) {
    602         if (mPortraitTabs == null) {
    603             return;
    604         }
    605         ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams();
    606         layoutParams.height = height;
    607         mPortraitTabs.setLayoutParams(layoutParams);
    608     }
    609 }
    610