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.content.Context;
22 import android.content.SharedPreferences;
23 import android.content.res.TypedArray;
24 import android.os.Bundle;
25 import android.preference.PreferenceManager;
26 import android.text.Editable;
27 import android.text.TextUtils;
28 import android.text.TextWatcher;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.inputmethod.InputMethodManager;
33 import android.widget.SearchView.OnCloseListener;
34 import android.view.View.OnClickListener;
35 import android.widget.EditText;
36 import android.widget.Toolbar;
37 
38 import com.android.contacts.R;
39 import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
40 import com.android.contacts.list.ContactsRequest;
41 
42 /**
43  * Adapter for the action bar at the top of the Contacts activity.
44  */
45 public class ActionBarAdapter implements OnCloseListener {
46 
47     public interface Listener {
48         public abstract class Action {
49             public static final int CHANGE_SEARCH_QUERY = 0;
50             public static final int START_SEARCH_MODE = 1;
51             public static final int STOP_SEARCH_MODE = 2;
52         }
53 
onAction(int action)54         void onAction(int action);
55 
56         /**
57          * Called when the user selects a tab.  The new tab can be obtained using
58          * {@link #getCurrentTab}.
59          */
onSelectedTabChanged()60         void onSelectedTabChanged();
61 
onUpButtonPressed()62         void onUpButtonPressed();
63     }
64 
65     private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
66     private static final String EXTRA_KEY_QUERY = "navBar.query";
67     private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
68 
69     private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
70 
71     private boolean mSearchMode;
72     private String mQueryString;
73 
74     private EditText mSearchView;
75     /** The view that represents tabs when we are in portrait mode **/
76     private View mPortraitTabs;
77     /** The view that represents tabs when we are in landscape mode **/
78     private View mLandscapeTabs;
79     private View mSearchContainer;
80 
81     private int mMaxPortraitTabHeight;
82     private int mMaxToolbarContentInsetStart;
83 
84     private final Context mContext;
85     private final SharedPreferences mPrefs;
86 
87     private Listener mListener;
88 
89     private final ActionBar mActionBar;
90     private final Toolbar mToolbar;
91 
92     private boolean mShowHomeIcon;
93 
94     public interface TabState {
95         public static int FAVORITES = 0;
96         public static int ALL = 1;
97 
98         public static int COUNT = 2;
99         public static int DEFAULT = ALL;
100     }
101 
102     private int mCurrentTab = TabState.DEFAULT;
103 
ActionBarAdapter(Context context, Listener listener, ActionBar actionBar, View portraitTabs, View landscapeTabs, Toolbar toolbar)104     public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
105             View portraitTabs, View landscapeTabs, Toolbar toolbar) {
106         mContext = context;
107         mListener = listener;
108         mActionBar = actionBar;
109         mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
110         mPortraitTabs = portraitTabs;
111         mLandscapeTabs = landscapeTabs;
112         mToolbar = toolbar;
113         mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart();
114         mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon);
115 
116         setupSearchView();
117         setupTabs(context);
118     }
119 
setupTabs(Context context)120     private void setupTabs(Context context) {
121         final TypedArray attributeArray = context.obtainStyledAttributes(
122                 new int[]{android.R.attr.actionBarSize});
123         mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0);
124         // Hide tabs initially
125         setPortraitTabHeight(0);
126     }
127 
setupSearchView()128     private void setupSearchView() {
129         final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService(
130                 Context.LAYOUT_INFLATER_SERVICE);
131         mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar,
132                 /* attachToRoot = */ false);
133         mSearchContainer.setVisibility(View.VISIBLE);
134         mToolbar.addView(mSearchContainer);
135 
136         mSearchContainer.setBackgroundColor(mContext.getResources().getColor(
137                 R.color.searchbox_background_color));
138         mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view);
139         mSearchView.setHint(mContext.getString(R.string.hint_findContacts));
140         mSearchView.addTextChangedListener(new SearchTextWatcher());
141         mSearchContainer.findViewById(R.id.search_close_button).setOnClickListener(
142                 new OnClickListener() {
143             @Override
144             public void onClick(View v) {
145                 setQueryString(null);
146             }
147         });
148         mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener(
149                 new OnClickListener() {
150             @Override
151             public void onClick(View v) {
152                 if (mListener != null) {
153                     mListener.onUpButtonPressed();
154                 }
155             }
156         });
157     }
158 
initialize(Bundle savedState, ContactsRequest request)159     public void initialize(Bundle savedState, ContactsRequest request) {
160         if (savedState == null) {
161             mSearchMode = request.isSearchMode();
162             mQueryString = request.getQueryString();
163             mCurrentTab = loadLastTabPreference();
164         } else {
165             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
166             mQueryString = savedState.getString(EXTRA_KEY_QUERY);
167 
168             // Just set to the field here.  The listener will be notified by update().
169             mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
170         }
171         if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) {
172             // Invalid tab index was saved (b/12938207). Restore the default.
173             mCurrentTab = TabState.DEFAULT;
174         }
175         // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
176         // search mode.
177         update(true /* skipAnimation */);
178         // Expanding the {@link SearchView} clears the query, so set the query from the
179         // {@link ContactsRequest} after it has been expanded, if applicable.
180         if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
181             setQueryString(mQueryString);
182         }
183     }
184 
setListener(Listener listener)185     public void setListener(Listener listener) {
186         mListener = listener;
187     }
188 
189     private class SearchTextWatcher implements TextWatcher {
190 
191         @Override
onTextChanged(CharSequence queryString, int start, int before, int count)192         public void onTextChanged(CharSequence queryString, int start, int before, int count) {
193             if (queryString.equals(mQueryString)) {
194                 return;
195             }
196             mQueryString = queryString.toString();
197             if (!mSearchMode) {
198                 if (!TextUtils.isEmpty(queryString)) {
199                     setSearchMode(true);
200                 }
201             } else if (mListener != null) {
202                 mListener.onAction(Action.CHANGE_SEARCH_QUERY);
203             }
204         }
205 
206         @Override
afterTextChanged(Editable s)207         public void afterTextChanged(Editable s) {}
208 
209         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)210         public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
211     }
212 
213     /**
214      * Save the current tab selection, and notify the listener.
215      */
setCurrentTab(int tab)216     public void setCurrentTab(int tab) {
217         setCurrentTab(tab, true);
218     }
219 
220     /**
221      * Save the current tab selection.
222      */
setCurrentTab(int tab, boolean notifyListener)223     public void setCurrentTab(int tab, boolean notifyListener) {
224         if (tab == mCurrentTab) {
225             return;
226         }
227         mCurrentTab = tab;
228 
229         if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
230         saveLastTabPreference(mCurrentTab);
231     }
232 
getCurrentTab()233     public int getCurrentTab() {
234         return mCurrentTab;
235     }
236 
237     /**
238      * @return Whether in search mode, i.e. if the search view is visible/expanded.
239      *
240      * Note even if the action bar is in search mode, if the query is empty, the search fragment
241      * will not be in search mode.
242      */
isSearchMode()243     public boolean isSearchMode() {
244         return mSearchMode;
245     }
246 
setSearchMode(boolean flag)247     public void setSearchMode(boolean flag) {
248         if (mSearchMode != flag) {
249             mSearchMode = flag;
250             update(false /* skipAnimation */);
251             if (mSearchView == null) {
252                 return;
253             }
254             if (mSearchMode) {
255                 mSearchView.setEnabled(true);
256                 setFocusOnSearchView();
257             } else {
258                 // Disable search view, so that it doesn't keep the IME visible.
259                 mSearchView.setEnabled(false);
260             }
261             setQueryString(null);
262         } else if (flag) {
263             // Everything is already set up. Still make sure the keyboard is up
264             if (mSearchView != null) setFocusOnSearchView();
265         }
266     }
267 
getQueryString()268     public String getQueryString() {
269         return mSearchMode ? mQueryString : null;
270     }
271 
setQueryString(String query)272     public void setQueryString(String query) {
273         mQueryString = query;
274         if (mSearchView != null) {
275             mSearchView.setText(query);
276             // When programmatically entering text into the search view, the most reasonable
277             // place for the cursor is after all the text.
278             mSearchView.setSelection(mSearchView.getText() == null ?
279                     0 : mSearchView.getText().length());
280         }
281     }
282 
283     /** @return true if the "UP" icon is showing. */
isUpShowing()284     public boolean isUpShowing() {
285         return mSearchMode; // Only shown on the search mode.
286     }
287 
updateDisplayOptionsInner()288     private void updateDisplayOptionsInner() {
289         // All the flags we may change in this method.
290         final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
291                 | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
292 
293         // The current flags set to the action bar.  (only the ones that we may change here)
294         final int current = mActionBar.getDisplayOptions() & MASK;
295 
296         // Build the new flags...
297         int newFlags = 0;
298         if (mShowHomeIcon && !mSearchMode) {
299             newFlags |= ActionBar.DISPLAY_SHOW_HOME;
300         }
301         if (mSearchMode) {
302             newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
303             mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd());
304         } else {
305             newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
306             mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart,
307                     mToolbar.getContentInsetEnd());
308         }
309 
310 
311         if (current != newFlags) {
312             // Pass the mask here to preserve other flags that we're not interested here.
313             mActionBar.setDisplayOptions(newFlags, MASK);
314         }
315     }
316 
update(boolean skipAnimation)317     private void update(boolean skipAnimation) {
318         final boolean isIconifiedChanging
319                 = (mSearchContainer.getParent() == null) == mSearchMode;
320         if (isIconifiedChanging && !skipAnimation) {
321             mToolbar.removeView(mLandscapeTabs);
322             if (mSearchMode) {
323                 addSearchContainer();
324                 mSearchContainer.setAlpha(0);
325                 mSearchContainer.animate().alpha(1);
326                 animateTabHeightChange(mMaxPortraitTabHeight, 0);
327                 updateDisplayOptions(isIconifiedChanging);
328             } else {
329                 mSearchContainer.setAlpha(1);
330                 animateTabHeightChange(0, mMaxPortraitTabHeight);
331                 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() {
332                     @Override
333                     public void run() {
334                         updateDisplayOptionsInner();
335                         updateDisplayOptions(isIconifiedChanging);
336                         addLandscapeViewPagerTabs();
337                         mToolbar.removeView(mSearchContainer);
338                     }
339                 });
340             }
341             return;
342         }
343         if (isIconifiedChanging && skipAnimation) {
344             mToolbar.removeView(mLandscapeTabs);
345             if (mSearchMode) {
346                 setPortraitTabHeight(0);
347                 addSearchContainer();
348             } else {
349                 setPortraitTabHeight(mMaxPortraitTabHeight);
350                 mToolbar.removeView(mSearchContainer);
351                 addLandscapeViewPagerTabs();
352             }
353         }
354         updateDisplayOptions(isIconifiedChanging);
355     }
356 
addLandscapeViewPagerTabs()357     private void addLandscapeViewPagerTabs() {
358         if (mLandscapeTabs != null) {
359             mToolbar.removeView(mLandscapeTabs);
360             mToolbar.addView(mLandscapeTabs);
361         }
362     }
363 
addSearchContainer()364     private void addSearchContainer() {
365         mToolbar.removeView(mSearchContainer);
366         mToolbar.addView(mSearchContainer);
367     }
368 
updateDisplayOptions(boolean isIconifiedChanging)369     private void updateDisplayOptions(boolean isIconifiedChanging) {
370         if (mSearchMode) {
371             setFocusOnSearchView();
372             // Since we have the {@link SearchView} in a custom action bar, we must manually handle
373             // expanding the {@link SearchView} when a search is initiated. Note that a side effect
374             // of this method is that the {@link SearchView} query text is set to empty string.
375             if (isIconifiedChanging) {
376                 final CharSequence queryText = mSearchView.getText();
377                 if (!TextUtils.isEmpty(queryText)) {
378                     mSearchView.setText(queryText);
379                 }
380             }
381             if (mListener != null) {
382                 mListener.onAction(Action.START_SEARCH_MODE);
383             }
384         } else {
385             if (mListener != null) {
386                 mListener.onAction(Action.STOP_SEARCH_MODE);
387                 mListener.onSelectedTabChanged();
388             }
389         }
390         updateDisplayOptionsInner();
391     }
392 
393     @Override
onClose()394     public boolean onClose() {
395         setSearchMode(false);
396         return false;
397     }
398 
onSaveInstanceState(Bundle outState)399     public void onSaveInstanceState(Bundle outState) {
400         outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
401         outState.putString(EXTRA_KEY_QUERY, mQueryString);
402         outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
403     }
404 
setFocusOnSearchView()405     public void setFocusOnSearchView() {
406         mSearchView.requestFocus();
407         showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue.
408     }
409 
showInputMethod(View view)410     private void showInputMethod(View view) {
411         final InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
412                 Context.INPUT_METHOD_SERVICE);
413         if (imm != null) {
414             imm.showSoftInput(view, 0);
415         }
416     }
417 
saveLastTabPreference(int tab)418     private void saveLastTabPreference(int tab) {
419         mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
420     }
421 
loadLastTabPreference()422     private int loadLastTabPreference() {
423         try {
424             return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT);
425         } catch (IllegalArgumentException e) {
426             // Preference is corrupt?
427             return TabState.DEFAULT;
428         }
429     }
430 
animateTabHeightChange(int start, int end)431     private void animateTabHeightChange(int start, int end) {
432         if (mPortraitTabs == null) {
433             return;
434         }
435         final ValueAnimator animator = ValueAnimator.ofInt(start, end);
436         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
437             @Override
438             public void onAnimationUpdate(ValueAnimator valueAnimator) {
439                 int value = (Integer) valueAnimator.getAnimatedValue();
440                 setPortraitTabHeight(value);
441             }
442         });
443         animator.setDuration(100).start();
444     }
445 
setPortraitTabHeight(int height)446     private void setPortraitTabHeight(int height) {
447         if (mPortraitTabs == null) {
448             return;
449         }
450         ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams();
451         layoutParams.height = height;
452         mPortraitTabs.setLayoutParams(layoutParams);
453     }
454 }
455