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.content.Context;
     22 import android.content.SharedPreferences;
     23 import android.content.res.TypedArray;
     24 import android.os.Bundle;
     25 import android.preference.PreferenceManager;
     26 import android.text.Editable;
     27 import android.text.TextUtils;
     28 import android.text.TextWatcher;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.view.inputmethod.InputMethodManager;
     33 import android.widget.SearchView.OnCloseListener;
     34 import android.view.View.OnClickListener;
     35 import android.widget.EditText;
     36 import android.widget.Toolbar;
     37 
     38 import com.android.contacts.R;
     39 import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
     40 import com.android.contacts.list.ContactsRequest;
     41 
     42 /**
     43  * Adapter for the action bar at the top of the Contacts activity.
     44  */
     45 public class ActionBarAdapter implements OnCloseListener {
     46 
     47     public interface Listener {
     48         public abstract class Action {
     49             public static final int CHANGE_SEARCH_QUERY = 0;
     50             public static final int START_SEARCH_MODE = 1;
     51             public static final int STOP_SEARCH_MODE = 2;
     52         }
     53 
     54         void onAction(int action);
     55 
     56         /**
     57          * Called when the user selects a tab.  The new tab can be obtained using
     58          * {@link #getCurrentTab}.
     59          */
     60         void onSelectedTabChanged();
     61 
     62         void onUpButtonPressed();
     63     }
     64 
     65     private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
     66     private static final String EXTRA_KEY_QUERY = "navBar.query";
     67     private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
     68 
     69     private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
     70 
     71     private boolean mSearchMode;
     72     private String mQueryString;
     73 
     74     private EditText mSearchView;
     75     /** The view that represents tabs when we are in portrait mode **/
     76     private View mPortraitTabs;
     77     /** The view that represents tabs when we are in landscape mode **/
     78     private View mLandscapeTabs;
     79     private View mSearchContainer;
     80 
     81     private int mMaxPortraitTabHeight;
     82     private int mMaxToolbarContentInsetStart;
     83 
     84     private final Context mContext;
     85     private final SharedPreferences mPrefs;
     86 
     87     private Listener mListener;
     88 
     89     private final ActionBar mActionBar;
     90     private final Toolbar mToolbar;
     91 
     92     private boolean mShowHomeIcon;
     93 
     94     public interface TabState {
     95         public static int FAVORITES = 0;
     96         public static int ALL = 1;
     97 
     98         public static int COUNT = 2;
     99         public static int DEFAULT = ALL;
    100     }
    101 
    102     private int mCurrentTab = TabState.DEFAULT;
    103 
    104     public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
    105             View portraitTabs, View landscapeTabs, Toolbar toolbar) {
    106         mContext = context;
    107         mListener = listener;
    108         mActionBar = actionBar;
    109         mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    110         mPortraitTabs = portraitTabs;
    111         mLandscapeTabs = landscapeTabs;
    112         mToolbar = toolbar;
    113         mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
    114         mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon);
    115 
    116         setupSearchView();
    117         setupTabs(context);
    118     }
    119 
    120     private void setupTabs(Context context) {
    121         final TypedArray attributeArray = context.obtainStyledAttributes(
    122                 new int[]{android.R.attr.actionBarSize});
    123         mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0);
    124         // Hide tabs initially
    125         setPortraitTabHeight(0);
    126     }
    127 
    128     private void setupSearchView() {
    129         final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService(
    130                 Context.LAYOUT_INFLATER_SERVICE);
    131         mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar,
    132                 /* attachToRoot = */ false);
    133         mSearchContainer.setVisibility(View.VISIBLE);
    134         mToolbar.addView(mSearchContainer);
    135 
    136         mSearchContainer.setBackgroundColor(mContext.getResources().getColor(
    137                 R.color.searchbox_background_color));
    138         mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view);
    139         mSearchView.setHint(mContext.getString(R.string.hint_findContacts));
    140         mSearchView.addTextChangedListener(new SearchTextWatcher());
    141         mSearchContainer.findViewById(R.id.search_close_button).setOnClickListener(
    142                 new OnClickListener() {
    143             @Override
    144             public void onClick(View v) {
    145                 setQueryString(null);
    146             }
    147         });
    148         mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener(
    149                 new OnClickListener() {
    150             @Override
    151             public void onClick(View v) {
    152                 if (mListener != null) {
    153                     mListener.onUpButtonPressed();
    154                 }
    155             }
    156         });
    157     }
    158 
    159     public void initialize(Bundle savedState, ContactsRequest request) {
    160         if (savedState == null) {
    161             mSearchMode = request.isSearchMode();
    162             mQueryString = request.getQueryString();
    163             mCurrentTab = loadLastTabPreference();
    164         } else {
    165             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
    166             mQueryString = savedState.getString(EXTRA_KEY_QUERY);
    167 
    168             // Just set to the field here.  The listener will be notified by update().
    169             mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
    170         }
    171         if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) {
    172             // Invalid tab index was saved (b/12938207). Restore the default.
    173             mCurrentTab = TabState.DEFAULT;
    174         }
    175         // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
    176         // search mode.
    177         update(true /* skipAnimation */);
    178         // Expanding the {@link SearchView} clears the query, so set the query from the
    179         // {@link ContactsRequest} after it has been expanded, if applicable.
    180         if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
    181             setQueryString(mQueryString);
    182         }
    183     }
    184 
    185     public void setListener(Listener listener) {
    186         mListener = listener;
    187     }
    188 
    189     private class SearchTextWatcher implements TextWatcher {
    190 
    191         @Override
    192         public void onTextChanged(CharSequence queryString, int start, int before, int count) {
    193             if (queryString.equals(mQueryString)) {
    194                 return;
    195             }
    196             mQueryString = queryString.toString();
    197             if (!mSearchMode) {
    198                 if (!TextUtils.isEmpty(queryString)) {
    199                     setSearchMode(true);
    200                 }
    201             } else if (mListener != null) {
    202                 mListener.onAction(Action.CHANGE_SEARCH_QUERY);
    203             }
    204         }
    205 
    206         @Override
    207         public void afterTextChanged(Editable s) {}
    208 
    209         @Override
    210         public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
    211     }
    212 
    213     /**
    214      * Save the current tab selection, and notify the listener.
    215      */
    216     public void setCurrentTab(int tab) {
    217         setCurrentTab(tab, true);
    218     }
    219 
    220     /**
    221      * Save the current tab selection.
    222      */
    223     public void setCurrentTab(int tab, boolean notifyListener) {
    224         if (tab == mCurrentTab) {
    225             return;
    226         }
    227         mCurrentTab = tab;
    228 
    229         if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
    230         saveLastTabPreference(mCurrentTab);
    231     }
    232 
    233     public int getCurrentTab() {
    234         return mCurrentTab;
    235     }
    236 
    237     /**
    238      * @return Whether in search mode, i.e. if the search view is visible/expanded.
    239      *
    240      * Note even if the action bar is in search mode, if the query is empty, the search fragment
    241      * will not be in search mode.
    242      */
    243     public boolean isSearchMode() {
    244         return mSearchMode;
    245     }
    246 
    247     public void setSearchMode(boolean flag) {
    248         if (mSearchMode != flag) {
    249             mSearchMode = flag;
    250             update(false /* skipAnimation */);
    251             if (mSearchView == null) {
    252                 return;
    253             }
    254             if (mSearchMode) {
    255                 mSearchView.setEnabled(true);
    256                 setFocusOnSearchView();
    257             } else {
    258                 // Disable search view, so that it doesn't keep the IME visible.
    259                 mSearchView.setEnabled(false);
    260             }
    261             setQueryString(null);
    262         } else if (flag) {
    263             // Everything is already set up. Still make sure the keyboard is up
    264             if (mSearchView != null) setFocusOnSearchView();
    265         }
    266     }
    267 
    268     public String getQueryString() {
    269         return mSearchMode ? mQueryString : null;
    270     }
    271 
    272     public void setQueryString(String query) {
    273         mQueryString = query;
    274         if (mSearchView != null) {
    275             mSearchView.setText(query);
    276             // When programmatically entering text into the search view, the most reasonable
    277             // place for the cursor is after all the text.
    278             mSearchView.setSelection(mSearchView.getText() == null ?
    279                     0 : mSearchView.getText().length());
    280         }
    281     }
    282 
    283     /** @return true if the "UP" icon is showing. */
    284     public boolean isUpShowing() {
    285         return mSearchMode; // Only shown on the search mode.
    286     }
    287 
    288     private void updateDisplayOptionsInner() {
    289         // All the flags we may change in this method.
    290         final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
    291                 | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
    292 
    293         // The current flags set to the action bar.  (only the ones that we may change here)
    294         final int current = mActionBar.getDisplayOptions() & MASK;
    295 
    296         // Build the new flags...
    297         int newFlags = 0;
    298         if (mShowHomeIcon && !mSearchMode) {
    299             newFlags |= ActionBar.DISPLAY_SHOW_HOME;
    300         }
    301         if (mSearchMode) {
    302             newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
    303             mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd());
    304         } else {
    305             newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
    306             mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart,
    307                     mToolbar.getContentInsetEnd());
    308         }
    309 
    310 
    311         if (current != newFlags) {
    312             // Pass the mask here to preserve other flags that we're not interested here.
    313             mActionBar.setDisplayOptions(newFlags, MASK);
    314         }
    315     }
    316 
    317     private void update(boolean skipAnimation) {
    318         final boolean isIconifiedChanging
    319                 = (mSearchContainer.getParent() == null) == mSearchMode;
    320         if (isIconifiedChanging && !skipAnimation) {
    321             mToolbar.removeView(mLandscapeTabs);
    322             if (mSearchMode) {
    323                 addSearchContainer();
    324                 mSearchContainer.setAlpha(0);
    325                 mSearchContainer.animate().alpha(1);
    326                 animateTabHeightChange(mMaxPortraitTabHeight, 0);
    327                 updateDisplayOptions(isIconifiedChanging);
    328             } else {
    329                 mSearchContainer.setAlpha(1);
    330                 animateTabHeightChange(0, mMaxPortraitTabHeight);
    331                 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() {
    332                     @Override
    333                     public void run() {
    334                         updateDisplayOptionsInner();
    335                         updateDisplayOptions(isIconifiedChanging);
    336                         addLandscapeViewPagerTabs();
    337                         mToolbar.removeView(mSearchContainer);
    338                     }
    339                 });
    340             }
    341             return;
    342         }
    343         if (isIconifiedChanging && skipAnimation) {
    344             mToolbar.removeView(mLandscapeTabs);
    345             if (mSearchMode) {
    346                 setPortraitTabHeight(0);
    347                 addSearchContainer();
    348             } else {
    349                 setPortraitTabHeight(mMaxPortraitTabHeight);
    350                 mToolbar.removeView(mSearchContainer);
    351                 addLandscapeViewPagerTabs();
    352             }
    353         }
    354         updateDisplayOptions(isIconifiedChanging);
    355     }
    356 
    357     private void addLandscapeViewPagerTabs() {
    358         if (mLandscapeTabs != null) {
    359             mToolbar.removeView(mLandscapeTabs);
    360             mToolbar.addView(mLandscapeTabs);
    361         }
    362     }
    363 
    364     private void addSearchContainer() {
    365         mToolbar.removeView(mSearchContainer);
    366         mToolbar.addView(mSearchContainer);
    367     }
    368 
    369     private void updateDisplayOptions(boolean isIconifiedChanging) {
    370         if (mSearchMode) {
    371             setFocusOnSearchView();
    372             // Since we have the {@link SearchView} in a custom action bar, we must manually handle
    373             // expanding the {@link SearchView} when a search is initiated. Note that a side effect
    374             // of this method is that the {@link SearchView} query text is set to empty string.
    375             if (isIconifiedChanging) {
    376                 final CharSequence queryText = mSearchView.getText();
    377                 if (!TextUtils.isEmpty(queryText)) {
    378                     mSearchView.setText(queryText);
    379                 }
    380             }
    381             if (mListener != null) {
    382                 mListener.onAction(Action.START_SEARCH_MODE);
    383             }
    384         } else {
    385             if (mListener != null) {
    386                 mListener.onAction(Action.STOP_SEARCH_MODE);
    387                 mListener.onSelectedTabChanged();
    388             }
    389         }
    390         updateDisplayOptionsInner();
    391     }
    392 
    393     @Override
    394     public boolean onClose() {
    395         setSearchMode(false);
    396         return false;
    397     }
    398 
    399     public void onSaveInstanceState(Bundle outState) {
    400         outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
    401         outState.putString(EXTRA_KEY_QUERY, mQueryString);
    402         outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
    403     }
    404 
    405     public void setFocusOnSearchView() {
    406         mSearchView.requestFocus();
    407         showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
    408     }
    409 
    410     private void showInputMethod(View view) {
    411         final InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
    412                 Context.INPUT_METHOD_SERVICE);
    413         if (imm != null) {
    414             imm.showSoftInput(view, 0);
    415         }
    416     }
    417 
    418     private void saveLastTabPreference(int tab) {
    419         mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
    420     }
    421 
    422     private int loadLastTabPreference() {
    423         try {
    424             return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT);
    425         } catch (IllegalArgumentException e) {
    426             // Preference is corrupt?
    427             return TabState.DEFAULT;
    428         }
    429     }
    430 
    431     private void animateTabHeightChange(int start, int end) {
    432         if (mPortraitTabs == null) {
    433             return;
    434         }
    435         final ValueAnimator animator = ValueAnimator.ofInt(start, end);
    436         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    437             @Override
    438             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    439                 int value = (Integer) valueAnimator.getAnimatedValue();
    440                 setPortraitTabHeight(value);
    441             }
    442         });
    443         animator.setDuration(100).start();
    444     }
    445 
    446     private void setPortraitTabHeight(int height) {
    447         if (mPortraitTabs == null) {
    448             return;
    449         }
    450         ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams();
    451         layoutParams.height = height;
    452         mPortraitTabs.setLayoutParams(layoutParams);
    453     }
    454 }
    455