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.ValueAnimator;
20 import android.app.ActionBar;
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.SharedPreferences;
24 import android.content.res.TypedArray;
25 import android.os.Bundle;
26 import android.preference.PreferenceManager;
27 import android.support.v4.content.ContextCompat;
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.ViewGroup;
35 import android.view.inputmethod.InputMethodManager;
36 import android.widget.FrameLayout;
37 import android.widget.LinearLayout.LayoutParams;
38 import android.widget.SearchView.OnCloseListener;
39 import android.view.View.OnClickListener;
40 import android.widget.EditText;
41 import android.widget.TextView;
42 import android.widget.Toolbar;
43 
44 import com.android.contacts.R;
45 import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
46 import com.android.contacts.common.compat.CompatUtils;
47 import com.android.contacts.list.ContactsRequest;
48 
49 /**
50  * Adapter for the action bar at the top of the Contacts activity.
51  */
52 public 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 
onAction(int action)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          */
onSelectedTabChanged()69         void onSelectedTabChanged();
70 
onUpButtonPressed()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 
ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, View portraitTabs, View landscapeTabs, Toolbar toolbar)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 
setupTabs(Context context)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 
setupSearchAndSelectionViews()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 
initialize(Bundle savedState, ContactsRequest request)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 
setListener(Listener listener)225     public void setListener(Listener listener) {
226         mListener = listener;
227     }
228 
229     private class SearchTextWatcher implements TextWatcher {
230 
231         @Override
onTextChanged(CharSequence queryString, int start, int before, int count)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
afterTextChanged(Editable s)249         public void afterTextChanged(Editable s) {}
250 
251         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)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      */
setCurrentTab(int tab)258     public void setCurrentTab(int tab) {
259         setCurrentTab(tab, true);
260     }
261 
262     /**
263      * Save the current tab selection.
264      */
setCurrentTab(int tab, boolean notifyListener)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 
getCurrentTab()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      */
isSearchMode()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      */
isSelectionMode()292     public boolean isSelectionMode() {
293         return mSelectionMode;
294     }
295 
setSearchMode(boolean flag)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 
setSelectionMode(boolean flag)317     public void setSelectionMode(boolean flag) {
318         if (mSelectionMode != flag) {
319             mSelectionMode = flag;
320             update(false /* skipAnimation */);
321         }
322     }
323 
getQueryString()324     public String getQueryString() {
325         return mSearchMode ? mQueryString : null;
326     }
327 
setQueryString(String query)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. */
isUpShowing()340     public boolean isUpShowing() {
341         return mSearchMode; // Only shown on the search mode.
342     }
343 
updateDisplayOptionsInner()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 
update(boolean skipAnimation)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 
setSelectionCount(int selectionCount)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 
updateStatusBarColor()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 
addLandscapeViewPagerTabs()500     private void addLandscapeViewPagerTabs() {
501         if (mLandscapeTabs != null) {
502             mToolbar.removeView(mLandscapeTabs);
503             mToolbar.addView(mLandscapeTabs);
504         }
505     }
506 
addSearchContainer()507     private void addSearchContainer() {
508         mToolbar.removeView(mSearchContainer);
509         mToolbar.addView(mSearchContainer);
510         mSearchContainer.setAlpha(1);
511     }
512 
addSelectionContainer()513     private void addSelectionContainer() {
514         mToolBarFrame.removeView(mSelectionContainer);
515         mToolBarFrame.addView(mSelectionContainer, 0);
516         mSelectionContainer.setAlpha(1);
517     }
518 
updateDisplayOptions(boolean isSearchModeChanging)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
onClose()548     public boolean onClose() {
549         setSearchMode(false);
550         return false;
551     }
552 
onSaveInstanceState(Bundle outState)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 
setFocusOnSearchView()560     public void setFocusOnSearchView() {
561         mSearchView.requestFocus();
562         showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
563     }
564 
showInputMethod(View view)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 
saveLastTabPreference(int tab)573     private void saveLastTabPreference(int tab) {
574         mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
575     }
576 
loadLastTabPreference()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 
animateTabHeightChange(int start, int end)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 
setPortraitTabHeight(int height)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