• 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.app.ActionBar;
20 import android.app.ActionBar.LayoutParams;
21 import android.app.ActionBar.Tab;
22 import android.app.FragmentTransaction;
23 import android.content.Context;
24 import android.content.SharedPreferences;
25 import android.os.Bundle;
26 import android.preference.PreferenceManager;
27 import android.text.TextUtils;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.inputmethod.InputMethodManager;
32 import android.widget.ArrayAdapter;
33 import android.widget.SearchView;
34 import android.widget.SearchView.OnCloseListener;
35 import android.widget.SearchView.OnQueryTextListener;
36 import android.widget.TextView;
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 OnQueryTextListener, 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     }
62 
63     private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
64     private static final String EXTRA_KEY_QUERY = "navBar.query";
65     private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
66 
67     private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
68 
69     private boolean mSearchMode;
70     private String mQueryString;
71 
72     private SearchView mSearchView;
73 
74     private final Context mContext;
75     private final SharedPreferences mPrefs;
76 
77     private Listener mListener;
78 
79     private final ActionBar mActionBar;
80     private final int mActionBarNavigationMode;
81     private final MyTabListener mTabListener;
82     private final MyNavigationListener mNavigationListener;
83 
84     private boolean mShowHomeIcon;
85     private boolean mShowTabsAsText;
86 
87     public interface TabState {
88         public static int GROUPS = 0;
89         public static int ALL = 1;
90         public static int FAVORITES = 2;
91 
92         public static int COUNT = 3;
93         public static int DEFAULT = ALL;
94     }
95 
96     private int mCurrentTab = TabState.DEFAULT;
97 
98     /**
99      * Extension of ArrayAdapter to be used for the action bar navigation drop list.  It is not
100      * possible to change the text appearance of a text item that is in the spinner header or
101      * in the drop down list using a selector xml file.  The only way to differentiate the two
102      * is if the view is gotten via {@link #getView(int, View, ViewGroup)} or
103      * {@link #getDropDownView(int, View, ViewGroup)}.
104      */
105     private class CustomArrayAdapter extends ArrayAdapter<String> {
106 
CustomArrayAdapter(Context context, int textResId)107         public CustomArrayAdapter(Context context, int textResId) {
108             super(context, textResId);
109         }
110 
getView(int position, View convertView, ViewGroup parent)111         public View getView (int position, View convertView, ViewGroup parent) {
112             TextView textView = (TextView) super.getView(position, convertView, parent);
113             textView.setTextAppearance(mContext,
114                     R.style.PeopleNavigationDropDownHeaderTextAppearance);
115             return textView;
116         }
117 
getDropDownView(int position, View convertView, ViewGroup parent)118         public View getDropDownView (int position, View convertView, ViewGroup parent) {
119             TextView textView = (TextView) super.getDropDownView(position, convertView, parent);
120             textView.setTextAppearance(mContext,
121                     R.style.PeopleNavigationDropDownTextAppearance);
122             return textView;
123         }
124     }
125 
ActionBarAdapter(Context context, Listener listener, ActionBar actionBar, boolean isUsingTwoPanes)126     public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
127             boolean isUsingTwoPanes) {
128         mContext = context;
129         mListener = listener;
130         mActionBar = actionBar;
131         mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
132 
133         mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon);
134 
135         // On wide screens, show the tabs as text (instead of icons)
136         mShowTabsAsText = isUsingTwoPanes;
137         if (isUsingTwoPanes) {
138             mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_LIST;
139             mTabListener = null;
140             mNavigationListener = new MyNavigationListener();
141         } else {
142             mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_TABS;
143             mTabListener = new MyTabListener();
144             mNavigationListener = null;
145         }
146 
147         // Set up search view.
148         View customSearchView = LayoutInflater.from(mActionBar.getThemedContext()).inflate(
149                 R.layout.custom_action_bar, null);
150         int searchViewWidth = mContext.getResources().getDimensionPixelSize(
151                 R.dimen.search_view_width);
152         if (searchViewWidth == 0) {
153             searchViewWidth = LayoutParams.MATCH_PARENT;
154         }
155         LayoutParams layoutParams = new LayoutParams(searchViewWidth, LayoutParams.WRAP_CONTENT);
156         mSearchView = (SearchView) customSearchView.findViewById(R.id.search_view);
157         // Since the {@link SearchView} in this app is "click-to-expand", set the below mode on the
158         // {@link SearchView} so that the magnifying glass icon appears inside the editable text
159         // field. (In the "click-to-expand" search pattern, the user must explicitly expand the
160         // search field and already knows a search is being conducted, so the icon is redundant
161         // and can go away once the user starts typing.)
162         mSearchView.setIconifiedByDefault(true);
163         mSearchView.setQueryHint(mContext.getString(R.string.hint_findContacts));
164         mSearchView.setOnQueryTextListener(this);
165         mSearchView.setOnCloseListener(this);
166         mSearchView.setQuery(mQueryString, false);
167         mActionBar.setCustomView(customSearchView, layoutParams);
168 
169         // Set up tabs or navigation list
170         switch(mActionBarNavigationMode) {
171             case ActionBar.NAVIGATION_MODE_TABS:
172                 setupTabs();
173                 break;
174             case ActionBar.NAVIGATION_MODE_LIST:
175                 setupNavigationList();
176                 break;
177         }
178     }
179 
setupTabs()180     private void setupTabs() {
181         addTab(TabState.GROUPS, R.drawable.ic_tab_groups, R.string.contactsGroupsLabel);
182         addTab(TabState.ALL, R.drawable.ic_tab_all, R.string.contactsAllLabel);
183         addTab(TabState.FAVORITES, R.drawable.ic_tab_starred, R.string.contactsFavoritesLabel);
184     }
185 
setupNavigationList()186     private void setupNavigationList() {
187         ArrayAdapter<String> navAdapter = new CustomArrayAdapter(mContext,
188                 R.layout.people_navigation_item);
189         navAdapter.add(mContext.getString(R.string.contactsAllLabel));
190         navAdapter.add(mContext.getString(R.string.contactsFavoritesLabel));
191         navAdapter.add(mContext.getString(R.string.contactsGroupsLabel));
192         mActionBar.setListNavigationCallbacks(navAdapter, mNavigationListener);
193     }
194 
195     /**
196      * Because the navigation list items are in a different order than tab items, this returns
197      * the appropriate tab from the navigation item position.
198      */
getTabPositionFromNavigationItemPosition(int navItemPos)199     private int getTabPositionFromNavigationItemPosition(int navItemPos) {
200         switch(navItemPos) {
201             case 0:
202                 return TabState.ALL;
203             case 1:
204                 return TabState.FAVORITES;
205             case 2:
206                 return TabState.GROUPS;
207         }
208         throw new IllegalArgumentException(
209                 "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1)
210                 + " inclusive.");
211     }
212 
213     /**
214      * This is the inverse of {@link getTabPositionFromNavigationItemPosition}.
215      */
getNavigationItemPositionFromTabPosition(int tabPos)216     private int getNavigationItemPositionFromTabPosition(int tabPos) {
217         switch(tabPos) {
218             case TabState.ALL:
219                 return 0;
220             case TabState.FAVORITES:
221                 return 1;
222             case TabState.GROUPS:
223                 return 2;
224         }
225         throw new IllegalArgumentException(
226                 "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1)
227                 + " inclusive.");
228     }
229 
initialize(Bundle savedState, ContactsRequest request)230     public void initialize(Bundle savedState, ContactsRequest request) {
231         if (savedState == null) {
232             mSearchMode = request.isSearchMode();
233             mQueryString = request.getQueryString();
234             mCurrentTab = loadLastTabPreference();
235         } else {
236             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
237             mQueryString = savedState.getString(EXTRA_KEY_QUERY);
238 
239             // Just set to the field here.  The listener will be notified by update().
240             mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
241         }
242         // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
243         // search mode.
244         update();
245         // Expanding the {@link SearchView} clears the query, so set the query from the
246         // {@link ContactsRequest} after it has been expanded, if applicable.
247         if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
248             setQueryString(mQueryString);
249         }
250     }
251 
setListener(Listener listener)252     public void setListener(Listener listener) {
253         mListener = listener;
254     }
255 
addTab(int expectedTabIndex, int icon, int description)256     private void addTab(int expectedTabIndex, int icon, int description) {
257         final Tab tab = mActionBar.newTab();
258         tab.setTabListener(mTabListener);
259         if (mShowTabsAsText) {
260             tab.setText(description);
261         } else {
262             tab.setIcon(icon);
263             tab.setContentDescription(description);
264         }
265         mActionBar.addTab(tab);
266         if (expectedTabIndex != tab.getPosition()) {
267             throw new IllegalStateException("Tabs must be created in the right order");
268         }
269     }
270 
271     private class MyTabListener implements ActionBar.TabListener {
272         /**
273          * If true, it won't call {@link #setCurrentTab} in {@link #onTabSelected}.
274          * This flag is used when we want to programmatically update the current tab without
275          * {@link #onTabSelected} getting called.
276          */
277         public boolean mIgnoreTabSelected;
278 
onTabReselected(Tab tab, FragmentTransaction ft)279         @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { }
onTabUnselected(Tab tab, FragmentTransaction ft)280         @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { }
281 
onTabSelected(Tab tab, FragmentTransaction ft)282         @Override public void onTabSelected(Tab tab, FragmentTransaction ft) {
283             if (!mIgnoreTabSelected) {
284                 setCurrentTab(tab.getPosition());
285             }
286         }
287     }
288 
289     private class MyNavigationListener implements ActionBar.OnNavigationListener {
290         public boolean mIgnoreNavigationItemSelected;
291 
onNavigationItemSelected(int itemPosition, long itemId)292         public boolean onNavigationItemSelected(int itemPosition, long itemId) {
293             if (!mIgnoreNavigationItemSelected) {
294                 setCurrentTab(getTabPositionFromNavigationItemPosition(itemPosition));
295             }
296             return true;
297         }
298     }
299 
300     /**
301      * Change the current tab, and notify the listener.
302      */
setCurrentTab(int tab)303     public void setCurrentTab(int tab) {
304         setCurrentTab(tab, true);
305     }
306 
307     /**
308      * Change the current tab
309      */
setCurrentTab(int tab, boolean notifyListener)310     public void setCurrentTab(int tab, boolean notifyListener) {
311         if (tab == mCurrentTab) {
312             return;
313         }
314         mCurrentTab = tab;
315 
316         final int actionBarSelectedNavIndex = mActionBar.getSelectedNavigationIndex();
317         switch(mActionBar.getNavigationMode()) {
318             case ActionBar.NAVIGATION_MODE_TABS:
319                 if (mCurrentTab != actionBarSelectedNavIndex) {
320                     mActionBar.setSelectedNavigationItem(mCurrentTab);
321                 }
322                 break;
323             case ActionBar.NAVIGATION_MODE_LIST:
324                 if (mCurrentTab != getTabPositionFromNavigationItemPosition(
325                         actionBarSelectedNavIndex)) {
326                     mActionBar.setSelectedNavigationItem(
327                             getNavigationItemPositionFromTabPosition(mCurrentTab));
328                 }
329                 break;
330         }
331 
332         if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
333         saveLastTabPreference(mCurrentTab);
334     }
335 
getCurrentTab()336     public int getCurrentTab() {
337         return mCurrentTab;
338     }
339 
340     /**
341      * @return Whether in search mode, i.e. if the search view is visible/expanded.
342      *
343      * Note even if the action bar is in search mode, if the query is empty, the search fragment
344      * will not be in search mode.
345      */
isSearchMode()346     public boolean isSearchMode() {
347         return mSearchMode;
348     }
349 
setSearchMode(boolean flag)350     public void setSearchMode(boolean flag) {
351         if (mSearchMode != flag) {
352             mSearchMode = flag;
353             update();
354             if (mSearchView == null) {
355                 return;
356             }
357             if (mSearchMode) {
358                 setFocusOnSearchView();
359             } else {
360                 mSearchView.setQuery(null, false);
361             }
362         } else if (flag) {
363             // Everything is already set up. Still make sure the keyboard is up
364             if (mSearchView != null) setFocusOnSearchView();
365         }
366     }
367 
getQueryString()368     public String getQueryString() {
369         return mSearchMode ? mQueryString : null;
370     }
371 
setQueryString(String query)372     public void setQueryString(String query) {
373         mQueryString = query;
374         if (mSearchView != null) {
375             mSearchView.setQuery(query, false);
376         }
377     }
378 
379     /** @return true if the "UP" icon is showing. */
isUpShowing()380     public boolean isUpShowing() {
381         return mSearchMode; // Only shown on the search mode.
382     }
383 
updateDisplayOptions()384     private void updateDisplayOptions() {
385         // All the flags we may change in this method.
386         final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
387                 | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
388 
389         // The current flags set to the action bar.  (only the ones that we may change here)
390         final int current = mActionBar.getDisplayOptions() & MASK;
391 
392         // Build the new flags...
393         int newFlags = 0;
394         newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
395         if (mShowHomeIcon) {
396             newFlags |= ActionBar.DISPLAY_SHOW_HOME;
397         }
398         if (mSearchMode) {
399             newFlags |= ActionBar.DISPLAY_SHOW_HOME;
400             newFlags |= ActionBar.DISPLAY_HOME_AS_UP;
401             newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
402         }
403         mActionBar.setHomeButtonEnabled(mSearchMode);
404 
405         if (current != newFlags) {
406             // Pass the mask here to preserve other flags that we're not interested here.
407             mActionBar.setDisplayOptions(newFlags, MASK);
408         }
409     }
410 
update()411     private void update() {
412         boolean isIconifiedChanging = mSearchView.isIconified() == mSearchMode;
413         if (mSearchMode) {
414             setFocusOnSearchView();
415             // Since we have the {@link SearchView} in a custom action bar, we must manually handle
416             // expanding the {@link SearchView} when a search is initiated. Note that a side effect
417             // of this method is that the {@link SearchView} query text is set to empty string.
418             if (isIconifiedChanging) {
419                 final CharSequence queryText = mSearchView.getQuery();
420                 mSearchView.onActionViewExpanded();
421                 if (!TextUtils.isEmpty(queryText)) {
422                     mSearchView.setQuery(queryText, false);
423                 }
424             }
425             if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_STANDARD) {
426                 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
427             }
428             if (mListener != null) {
429                 mListener.onAction(Action.START_SEARCH_MODE);
430             }
431         } else {
432             final int currentNavigationMode = mActionBar.getNavigationMode();
433             if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_TABS
434                     && currentNavigationMode != ActionBar.NAVIGATION_MODE_TABS) {
435                 // setNavigationMode will trigger onTabSelected() with the tab which was previously
436                 // selected.
437                 // The issue is that when we're first switching to the tab navigation mode after
438                 // screen orientation changes, onTabSelected() will get called with the first tab
439                 // (i.e. favorite), which would results in mCurrentTab getting set to FAVORITES and
440                 // we'd lose restored tab.
441                 // So let's just disable the callback here temporarily.  We'll notify the listener
442                 // after this anyway.
443                 mTabListener.mIgnoreTabSelected = true;
444                 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
445                 mActionBar.setSelectedNavigationItem(mCurrentTab);
446                 mTabListener.mIgnoreTabSelected = false;
447             } else if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_LIST
448                     && currentNavigationMode != ActionBar.NAVIGATION_MODE_LIST) {
449                 mNavigationListener.mIgnoreNavigationItemSelected = true;
450                 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
451                 mActionBar.setSelectedNavigationItem(
452                         getNavigationItemPositionFromTabPosition(mCurrentTab));
453                 mNavigationListener.mIgnoreNavigationItemSelected = false;
454             }
455             mActionBar.setTitle(null);
456             // Since we have the {@link SearchView} in a custom action bar, we must manually handle
457             // collapsing the {@link SearchView} when search mode is exited.
458             if (isIconifiedChanging) {
459                 mSearchView.onActionViewCollapsed();
460             }
461             if (mListener != null) {
462                 mListener.onAction(Action.STOP_SEARCH_MODE);
463                 mListener.onSelectedTabChanged();
464             }
465         }
466         updateDisplayOptions();
467     }
468 
469     @Override
onQueryTextChange(String queryString)470     public boolean onQueryTextChange(String queryString) {
471         // TODO: Clean up SearchView code because it keeps setting the SearchView query,
472         // invoking onQueryChanged, setting up the fragment again, invalidating the options menu,
473         // storing the SearchView again, and etc... unless we add in the early return statements.
474         if (queryString.equals(mQueryString)) {
475             return false;
476         }
477         mQueryString = queryString;
478         if (!mSearchMode) {
479             if (!TextUtils.isEmpty(queryString)) {
480                 setSearchMode(true);
481             }
482         } else if (mListener != null) {
483             mListener.onAction(Action.CHANGE_SEARCH_QUERY);
484         }
485 
486         return true;
487     }
488 
489     @Override
onQueryTextSubmit(String query)490     public boolean onQueryTextSubmit(String query) {
491         // When the search is "committed" by the user, then hide the keyboard so the user can
492         // more easily browse the list of results.
493         if (mSearchView != null) {
494             InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
495                     Context.INPUT_METHOD_SERVICE);
496             if (imm != null) {
497                 imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
498             }
499             mSearchView.clearFocus();
500         }
501         return true;
502     }
503 
504     @Override
onClose()505     public boolean onClose() {
506         setSearchMode(false);
507         return false;
508     }
509 
onSaveInstanceState(Bundle outState)510     public void onSaveInstanceState(Bundle outState) {
511         outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
512         outState.putString(EXTRA_KEY_QUERY, mQueryString);
513         outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
514     }
515 
516     /**
517      * Clears the focus from the {@link SearchView} if we are in search mode.
518      * This will suppress the IME if it is visible.
519      */
clearFocusOnSearchView()520     public void clearFocusOnSearchView() {
521         if (isSearchMode()) {
522             if (mSearchView != null) {
523                 mSearchView.clearFocus();
524             }
525         }
526     }
527 
setFocusOnSearchView()528     public void setFocusOnSearchView() {
529         mSearchView.requestFocus();
530         mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue.
531     }
532 
saveLastTabPreference(int tab)533     private void saveLastTabPreference(int tab) {
534         mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
535     }
536 
loadLastTabPreference()537     private int loadLastTabPreference() {
538         try {
539             return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT);
540         } catch (IllegalArgumentException e) {
541             // Preference is corrupt?
542             return TabState.DEFAULT;
543         }
544     }
545 }
546