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