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