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.ArgbEvaluator;
20import android.animation.ValueAnimator;
21import android.app.Activity;
22import android.content.Context;
23import android.content.res.ColorStateList;
24import android.os.Bundle;
25import android.support.v4.content.ContextCompat;
26import android.support.v7.app.ActionBar;
27import android.support.v7.widget.Toolbar;
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.View.OnClickListener;
35import android.view.ViewGroup;
36import android.view.ViewTreeObserver;
37import android.view.Window;
38import android.view.inputmethod.InputMethodManager;
39import android.widget.EditText;
40import android.widget.FrameLayout;
41import android.widget.ImageButton;
42import android.widget.ImageView;
43import android.widget.LinearLayout.LayoutParams;
44import android.widget.SearchView.OnCloseListener;
45import android.widget.TextView;
46
47import com.android.contacts.R;
48import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
49import com.android.contacts.activities.PeopleActivity;
50import com.android.contacts.compat.CompatUtils;
51import com.android.contacts.list.ContactsRequest;
52import com.android.contacts.util.MaterialColorMapUtils;
53
54import java.util.ArrayList;
55
56/**
57 * Adapter for the action bar at the top of the Contacts activity.
58 */
59public class ActionBarAdapter implements OnCloseListener {
60
61    public interface Listener {
62        public abstract class Action {
63            public static final int CHANGE_SEARCH_QUERY = 0;
64            public static final int START_SEARCH_MODE = 1;
65            public static final int START_SELECTION_MODE = 2;
66            public static final int STOP_SEARCH_AND_SELECTION_MODE = 3;
67            public static final int BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE = 4;
68        }
69
70        void onAction(int action);
71
72        void onUpButtonPressed();
73    }
74
75    private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
76    private static final String EXTRA_KEY_QUERY = "navBar.query";
77    private static final String EXTRA_KEY_SELECTED_MODE = "navBar.selectionMode";
78
79    private boolean mSelectionMode;
80    private boolean mSearchMode;
81    private String mQueryString;
82
83    private EditText mSearchView;
84    private View mClearSearchView;
85    private View mSearchContainer;
86    private View mSelectionContainer;
87
88    private int mMaxToolbarContentInsetStart;
89    private int mActionBarAnimationDuration;
90
91    private final Activity mActivity;
92
93    private Listener mListener;
94
95    private final ActionBar mActionBar;
96    private final Toolbar mToolbar;
97    /**
98     *  Frame that contains the toolbar and draws the toolbar's background color. This is useful
99     *  for placing things behind the toolbar.
100     */
101    private final FrameLayout mToolBarFrame;
102
103    private boolean mShowHomeIcon;
104    private boolean mShowHomeAsUp;
105
106    private int mSearchHintResId;
107
108    private ValueAnimator mStatusBarAnimator;
109
110    public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
111            Toolbar toolbar) {
112        this(activity, listener, actionBar, toolbar, R.string.hint_findContacts);
113    }
114
115    public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
116            Toolbar toolbar, int searchHintResId) {
117        mActivity = activity;
118        mListener = listener;
119        mActionBar = actionBar;
120        mToolbar = toolbar;
121        mToolBarFrame = (FrameLayout) mToolbar.getParent();
122        mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
123        mSearchHintResId = searchHintResId;
124        mActionBarAnimationDuration =
125                mActivity.getResources().getInteger(R.integer.action_bar_animation_duration);
126
127        setupSearchAndSelectionViews();
128    }
129
130    public void setShowHomeIcon(boolean showHomeIcon) {
131        mShowHomeIcon = showHomeIcon;
132    }
133
134    public void setShowHomeAsUp(boolean showHomeAsUp) {
135        mShowHomeAsUp = showHomeAsUp;
136    }
137
138    public View getSelectionContainer() {
139        return mSelectionContainer;
140    }
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(mSearchHintResId));
155        mSearchView.addTextChangedListener(new SearchTextWatcher());
156        final ImageButton searchBackButton = (ImageButton) mSearchContainer
157                .findViewById(R.id.search_back_button);
158        searchBackButton.setOnClickListener(
159                new OnClickListener() {
160            @Override
161            public void onClick(View v) {
162                if (mListener != null) {
163                    mListener.onUpButtonPressed();
164                }
165            }
166        });
167        searchBackButton.getDrawable().setAutoMirrored(true);
168
169        mClearSearchView = mSearchContainer.findViewById(R.id.search_close_button);
170        mClearSearchView.setOnClickListener(
171                new OnClickListener() {
172            @Override
173            public void onClick(View v) {
174                setQueryString(null);
175            }
176        });
177
178        // Setup selection bar
179        mSelectionContainer = inflater.inflate(R.layout.selection_bar, mToolbar,
180                /* attachToRoot = */ false);
181        // Insert the selection container into mToolBarFrame behind the Toolbar, so that
182        // the Toolbar's MenuItems can appear on top of the selection container.
183        mToolBarFrame.addView(mSelectionContainer, 0);
184        mSelectionContainer.findViewById(R.id.selection_close).setOnClickListener(
185                new OnClickListener() {
186                    @Override
187                    public void onClick(View v) {
188                        if (mListener != null) {
189                            mListener.onUpButtonPressed();
190                        }
191                    }
192                });
193    }
194
195    public void initialize(Bundle savedState, ContactsRequest request) {
196        if (savedState == null) {
197            mSearchMode = request.isSearchMode();
198            mQueryString = request.getQueryString();
199            mSelectionMode = false;
200        } else {
201            mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
202            mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE);
203            mQueryString = savedState.getString(EXTRA_KEY_QUERY);
204        }
205        // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
206        // search mode.
207        update(true /* skipAnimation */);
208        // Expanding the {@link SearchView} clears the query, so set the query from the
209        // {@link ContactsRequest} after it has been expanded, if applicable.
210        if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
211            setQueryString(mQueryString);
212        }
213    }
214
215    public void setListener(Listener listener) {
216        mListener = listener;
217    }
218
219    private class SearchTextWatcher implements TextWatcher {
220
221        @Override
222        public void onTextChanged(CharSequence queryString, int start, int before, int count) {
223            if (queryString.equals(mQueryString)) {
224                return;
225            }
226            mQueryString = queryString.toString();
227            if (!mSearchMode) {
228                if (!TextUtils.isEmpty(queryString)) {
229                    setSearchMode(true);
230                }
231            } else if (mListener != null) {
232                mListener.onAction(Action.CHANGE_SEARCH_QUERY);
233            }
234            mClearSearchView.setVisibility(
235                    TextUtils.isEmpty(queryString) ? View.GONE : View.VISIBLE);
236        }
237
238        @Override
239        public void afterTextChanged(Editable s) {}
240
241        @Override
242        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
243    }
244
245    /**
246     * @return Whether in search mode, i.e. if the search view is visible/expanded.
247     *
248     * Note even if the action bar is in search mode, if the query is empty, the search fragment
249     * will not be in search mode.
250     */
251    public boolean isSearchMode() {
252        return mSearchMode;
253    }
254
255    /**
256     * @return Whether in selection mode, i.e. if the selection view is visible/expanded.
257     */
258    public boolean isSelectionMode() {
259        return mSelectionMode;
260    }
261
262    public void setSearchMode(boolean flag) {
263        if (mSearchMode != flag) {
264            mSearchMode = flag;
265            update(false /* skipAnimation */);
266            if (mSearchView == null) {
267                return;
268            }
269            if (mSearchMode) {
270                mSearchView.setEnabled(true);
271                setFocusOnSearchView();
272            } else {
273                // Disable search view, so that it doesn't keep the IME visible.
274                mSearchView.setEnabled(false);
275            }
276            setQueryString(null);
277        } else if (flag) {
278            // Everything is already set up. Still make sure the keyboard is up
279            if (mSearchView != null) setFocusOnSearchView();
280        }
281    }
282
283    public void setSelectionMode(boolean flag) {
284        if (mSelectionMode != flag) {
285            mSelectionMode = flag;
286            update(false /* skipAnimation */);
287        }
288    }
289
290    public String getQueryString() {
291        return mSearchMode ? mQueryString : null;
292    }
293
294    public void setQueryString(String query) {
295        mQueryString = query;
296        if (mSearchView != null) {
297            mSearchView.setText(query);
298            // When programmatically entering text into the search view, the most reasonable
299            // place for the cursor is after all the text.
300            mSearchView.setSelection(mSearchView.getText() == null ?
301                    0 : mSearchView.getText().length());
302        }
303    }
304
305    /** @return true if the "UP" icon is showing. */
306    public boolean isUpShowing() {
307        return mSearchMode; // Only shown on the search mode.
308    }
309
310    private void updateDisplayOptionsInner() {
311        // All the flags we may change in this method.
312        final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
313                | ActionBar.DISPLAY_HOME_AS_UP;
314
315        // The current flags set to the action bar.  (only the ones that we may change here)
316        final int current = mActionBar.getDisplayOptions() & MASK;
317
318        final boolean isSearchOrSelectionMode = mSearchMode || mSelectionMode;
319
320        // Build the new flags...
321        int newFlags = 0;
322        if (mShowHomeIcon && !isSearchOrSelectionMode) {
323            newFlags |= ActionBar.DISPLAY_SHOW_HOME;
324            if (mShowHomeAsUp) {
325                newFlags |= ActionBar.DISPLAY_HOME_AS_UP;
326            }
327        }
328        if (mSearchMode && !mSelectionMode) {
329            // The search container is placed inside the toolbar. So we need to disable the
330            // Toolbar's content inset in order to allow the search container to be the width of
331            // the window.
332            mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd());
333        }
334        if (!isSearchOrSelectionMode) {
335            newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
336            mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart,
337                    mToolbar.getContentInsetEnd());
338            mToolbar.setNavigationIcon(R.drawable.quantum_ic_menu_vd_theme_24);
339        } else {
340            mToolbar.setNavigationIcon(null);
341        }
342
343        if (mSelectionMode) {
344            // Minimize the horizontal width of the Toolbar since the selection container is placed
345            // behind the toolbar and its left hand side needs to be clickable.
346            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams();
347            params.width = LayoutParams.WRAP_CONTENT;
348            params.gravity = Gravity.END;
349            mToolbar.setLayoutParams(params);
350        } else {
351            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams();
352            params.width = LayoutParams.MATCH_PARENT;
353            params.gravity = Gravity.END;
354            mToolbar.setLayoutParams(params);
355        }
356
357        if (current != newFlags) {
358            // Pass the mask here to preserve other flags that we're not interested here.
359            mActionBar.setDisplayOptions(newFlags, MASK);
360        }
361    }
362
363    private void update(boolean skipAnimation) {
364        updateOverflowButtonColor();
365
366        final boolean isSelectionModeChanging
367                = (mSelectionContainer.getParent() == null) == mSelectionMode;
368        final boolean isSwitchingFromSearchToSelection =
369                mSearchMode && isSelectionModeChanging || mSearchMode && mSelectionMode;
370        final boolean isSearchModeChanging
371                = (mSearchContainer.getParent() == null) == mSearchMode;
372        final boolean isTabHeightChanging = isSearchModeChanging || isSelectionModeChanging;
373
374        // Update toolbar and status bar color.
375        mToolBarFrame.setBackgroundColor(MaterialColorMapUtils.getToolBarColor(mActivity));
376        updateStatusBarColor(isSelectionModeChanging && !isSearchModeChanging);
377
378        // When skipAnimation=true, it is possible that we will switch from search mode
379        // to selection mode directly. So we need to remove the undesired container in addition
380        // to adding the desired container.
381        if (skipAnimation || isSwitchingFromSearchToSelection) {
382            if (isTabHeightChanging || isSwitchingFromSearchToSelection) {
383                mToolbar.removeView(mSearchContainer);
384                mToolBarFrame.removeView(mSelectionContainer);
385                if (mSelectionMode) {
386                    addSelectionContainer();
387                } else if (mSearchMode) {
388                    addSearchContainer();
389                }
390                updateDisplayOptions(isSearchModeChanging);
391            }
392            return;
393        }
394
395        // Handle a switch to/from selection mode, due to UI interaction.
396        if (isSelectionModeChanging) {
397            if (mSelectionMode) {
398                addSelectionContainer();
399                mSelectionContainer.setAlpha(0);
400                mSelectionContainer.animate().alpha(1).setDuration(mActionBarAnimationDuration);
401                updateDisplayOptions(isSearchModeChanging);
402            } else {
403                if (mListener != null) {
404                    mListener.onAction(Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE);
405                }
406                mSelectionContainer.setAlpha(1);
407                mSelectionContainer.animate().alpha(0).setDuration(mActionBarAnimationDuration)
408                        .withEndAction(new Runnable() {
409                    @Override
410                    public void run() {
411                        updateDisplayOptions(isSearchModeChanging);
412                        mToolBarFrame.removeView(mSelectionContainer);
413                    }
414                });
415            }
416        }
417
418        // Handle a switch to/from search mode, due to UI interaction.
419        if (isSearchModeChanging) {
420            if (mSearchMode) {
421                addSearchContainer();
422                mSearchContainer.setAlpha(0);
423                mSearchContainer.animate().alpha(1).setDuration(mActionBarAnimationDuration);
424                updateDisplayOptions(isSearchModeChanging);
425            } else {
426                mSearchContainer.setAlpha(1);
427                mSearchContainer.animate().alpha(0).setDuration(mActionBarAnimationDuration)
428                        .withEndAction(new Runnable() {
429                    @Override
430                    public void run() {
431                        updateDisplayOptions(isSearchModeChanging);
432                        mToolbar.removeView(mSearchContainer);
433                    }
434                });
435            }
436        }
437    }
438
439    /**
440     * Find overflow menu ImageView by its content description and update its color.
441     */
442    public void updateOverflowButtonColor() {
443        final String overflowDescription = mActivity.getResources().getString(
444                R.string.abc_action_menu_overflow_description);
445        final ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
446        final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
447        viewTreeObserver.addOnGlobalLayoutListener(
448                new ViewTreeObserver.OnGlobalLayoutListener() {
449                    @Override
450                    public void onGlobalLayout() {
451                        // Find the overflow ImageView.
452                        final ArrayList<View> outViews = new ArrayList<>();
453                        decorView.findViewsWithText(outViews, overflowDescription,
454                                View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
455
456                        for (View view : outViews) {
457                            if (!(view instanceof ImageView)) {
458                                continue;
459                            }
460                            final ImageView overflow = (ImageView) view;
461
462                            // Update the overflow image color.
463                            final int iconColor;
464                            if (mSelectionMode) {
465                                iconColor = mActivity.getResources().getColor(
466                                        R.color.actionbar_color_grey_solid);
467                            } else {
468                                iconColor = mActivity.getResources().getColor(
469                                        R.color.actionbar_text_color);
470                            }
471                            overflow.setImageTintList(ColorStateList.valueOf(iconColor));
472                        }
473
474                        // We're done, remove the listener.
475                        decorView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
476                    }
477                });
478    }
479
480    public void setSelectionCount(int selectionCount) {
481        TextView textView = (TextView) mSelectionContainer.findViewById(R.id.selection_count_text);
482        if (selectionCount == 0) {
483            textView.setVisibility(View.GONE);
484        } else {
485            textView.setVisibility(View.VISIBLE);
486        }
487        textView.setText(String.valueOf(selectionCount));
488    }
489
490    public void setActionBarTitle(String title) {
491        final TextView textView =
492                (TextView) mSelectionContainer.findViewById(R.id.selection_count_text);
493        textView.setVisibility(View.VISIBLE);
494        textView.setText(title);
495    }
496
497    private void updateStatusBarColor(boolean shouldAnimate) {
498        if (!CompatUtils.isLollipopCompatible()) {
499            return; // we can't change the status bar color prior to Lollipop
500        }
501
502        if (mSelectionMode) {
503            final int cabStatusBarColor = ContextCompat.getColor(
504                    mActivity, R.color.contextual_selection_bar_status_bar_color);
505            runStatusBarAnimation(/* colorTo */ cabStatusBarColor);
506        } else {
507            if (shouldAnimate) {
508                runStatusBarAnimation(/* colorTo */
509                        MaterialColorMapUtils.getStatusBarColor(mActivity));
510            } else if (mActivity instanceof PeopleActivity) {
511                ((PeopleActivity) mActivity).updateStatusBarBackground();
512            }
513        }
514    }
515
516    private void runStatusBarAnimation(int colorTo) {
517        final Window window = mActivity.getWindow();
518        if (window.getStatusBarColor() != colorTo) {
519            // Cancel running animation.
520            if (mStatusBarAnimator != null && mStatusBarAnimator.isRunning()) {
521                mStatusBarAnimator.cancel();
522            }
523            final int from = window.getStatusBarColor();
524            // Set up mStatusBarAnimator and run animation.
525            mStatusBarAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), from, colorTo);
526            mStatusBarAnimator.addUpdateListener(
527                    new ValueAnimator.AnimatorUpdateListener() {
528                        @Override
529                        public void onAnimationUpdate(ValueAnimator animator) {
530                            window.setStatusBarColor((Integer) animator.getAnimatedValue());
531                        }
532                    });
533            mStatusBarAnimator.setDuration(mActionBarAnimationDuration);
534            mStatusBarAnimator.setStartDelay(0);
535            mStatusBarAnimator.start();
536        }
537    }
538
539    private void addSearchContainer() {
540        mToolbar.removeView(mSearchContainer);
541        mToolbar.addView(mSearchContainer);
542        mSearchContainer.setAlpha(1);
543    }
544
545    private void addSelectionContainer() {
546        mToolBarFrame.removeView(mSelectionContainer);
547        mToolBarFrame.addView(mSelectionContainer, 0);
548        mSelectionContainer.setAlpha(1);
549    }
550
551    private void updateDisplayOptions(boolean isSearchModeChanging) {
552        if (mSearchMode && !mSelectionMode) {
553            setFocusOnSearchView();
554            // Since we have the {@link SearchView} in a custom action bar, we must manually handle
555            // expanding the {@link SearchView} when a search is initiated. Note that a side effect
556            // of this method is that the {@link SearchView} query text is set to empty string.
557            if (isSearchModeChanging) {
558                final CharSequence queryText = mSearchView.getText();
559                if (!TextUtils.isEmpty(queryText)) {
560                    mSearchView.setText(queryText);
561                }
562            }
563        }
564        if (mListener != null) {
565            if (mSearchMode) {
566                mListener.onAction(Action.START_SEARCH_MODE);
567            }
568            if (mSelectionMode) {
569                mListener.onAction(Action.START_SELECTION_MODE);
570            }
571            if (!mSearchMode && !mSelectionMode) {
572                mListener.onAction(Action.STOP_SEARCH_AND_SELECTION_MODE);
573            }
574        }
575        updateDisplayOptionsInner();
576    }
577
578    @Override
579    public boolean onClose() {
580        setSearchMode(false);
581        return false;
582    }
583
584    public void onSaveInstanceState(Bundle outState) {
585        outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
586        outState.putBoolean(EXTRA_KEY_SELECTED_MODE, mSelectionMode);
587        outState.putString(EXTRA_KEY_QUERY, mQueryString);
588    }
589
590    public void setFocusOnSearchView() {
591        mSearchView.requestFocus();
592        showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
593    }
594
595    private void showInputMethod(View view) {
596        final InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(
597                Context.INPUT_METHOD_SERVICE);
598        if (imm != null) {
599            imm.showSoftInput(view, 0);
600        }
601    }
602}
603