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