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