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 
17 package com.android.contacts.activities;
18 
19 import android.animation.ArgbEvaluator;
20 import android.animation.ValueAnimator;
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.res.ColorStateList;
24 import android.os.Bundle;
25 import androidx.core.content.ContextCompat;
26 import androidx.appcompat.app.ActionBar;
27 import androidx.appcompat.widget.Toolbar;
28 import android.text.Editable;
29 import android.text.TextUtils;
30 import android.text.TextWatcher;
31 import android.view.Gravity;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.View.OnClickListener;
35 import android.view.ViewGroup;
36 import android.view.ViewTreeObserver;
37 import android.view.Window;
38 import android.view.inputmethod.InputMethodManager;
39 import android.widget.EditText;
40 import android.widget.FrameLayout;
41 import android.widget.ImageButton;
42 import android.widget.ImageView;
43 import android.widget.LinearLayout.LayoutParams;
44 import android.widget.SearchView.OnCloseListener;
45 import android.widget.TextView;
46 
47 import com.android.contacts.R;
48 import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
49 import com.android.contacts.activities.PeopleActivity;
50 import com.android.contacts.compat.CompatUtils;
51 import com.android.contacts.list.ContactsRequest;
52 import com.android.contacts.util.MaterialColorMapUtils;
53 
54 import java.util.ArrayList;
55 
56 /**
57  * Adapter for the action bar at the top of the Contacts activity.
58  */
59 public 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 
onAction(int action)70         void onAction(int action);
71 
onUpButtonPressed()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 
ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, Toolbar toolbar)110     public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar,
111             Toolbar toolbar) {
112         this(activity, listener, actionBar, toolbar, R.string.hint_findContacts);
113     }
114 
ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, Toolbar toolbar, int searchHintResId)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 
setShowHomeIcon(boolean showHomeIcon)130     public void setShowHomeIcon(boolean showHomeIcon) {
131         mShowHomeIcon = showHomeIcon;
132     }
133 
setShowHomeAsUp(boolean showHomeAsUp)134     public void setShowHomeAsUp(boolean showHomeAsUp) {
135         mShowHomeAsUp = showHomeAsUp;
136     }
137 
getSelectionContainer()138     public View getSelectionContainer() {
139         return mSelectionContainer;
140     }
141 
setupSearchAndSelectionViews()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 
initialize(Bundle savedState, ContactsRequest request)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 
setListener(Listener listener)215     public void setListener(Listener listener) {
216         mListener = listener;
217     }
218 
219     private class SearchTextWatcher implements TextWatcher {
220 
221         @Override
onTextChanged(CharSequence queryString, int start, int before, int count)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
afterTextChanged(Editable s)239         public void afterTextChanged(Editable s) {}
240 
241         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)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      */
isSearchMode()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      */
isSelectionMode()258     public boolean isSelectionMode() {
259         return mSelectionMode;
260     }
261 
setSearchMode(boolean flag)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 
setSelectionMode(boolean flag)283     public void setSelectionMode(boolean flag) {
284         if (mSelectionMode != flag) {
285             mSelectionMode = flag;
286             update(false /* skipAnimation */);
287         }
288     }
289 
getQueryString()290     public String getQueryString() {
291         return mSearchMode ? mQueryString : null;
292     }
293 
setQueryString(String query)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. */
isUpShowing()306     public boolean isUpShowing() {
307         return mSearchMode; // Only shown on the search mode.
308     }
309 
updateDisplayOptionsInner()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 
update(boolean skipAnimation)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      */
updateOverflowButtonColor()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 
setSelectionCount(int selectionCount)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 
setActionBarTitle(String title)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 
updateStatusBarColor(boolean shouldAnimate)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 
runStatusBarAnimation(int colorTo)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 
addSearchContainer()539     private void addSearchContainer() {
540         mToolbar.removeView(mSearchContainer);
541         mToolbar.addView(mSearchContainer);
542         mSearchContainer.setAlpha(1);
543     }
544 
addSelectionContainer()545     private void addSelectionContainer() {
546         mToolBarFrame.removeView(mSelectionContainer);
547         mToolBarFrame.addView(mSelectionContainer, 0);
548         mSelectionContainer.setAlpha(1);
549     }
550 
updateDisplayOptions(boolean isSearchModeChanging)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
onClose()579     public boolean onClose() {
580         setSearchMode(false);
581         return false;
582     }
583 
onSaveInstanceState(Bundle outState)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 
setFocusOnSearchView()590     public void setFocusOnSearchView() {
591         mSearchView.requestFocus();
592         showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
593     }
594 
showInputMethod(View view)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