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