Home | History | Annotate | Download | only in activities
      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 
     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          */
     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 
    107         public CustomArrayAdapter(Context context, int textResId) {
    108             super(context, textResId);
    109         }
    110 
    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 
    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 
    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 
    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 
    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      */
    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      */
    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 
    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 
    252     public void setListener(Listener listener) {
    253         mListener = listener;
    254     }
    255 
    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 
    279         @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { }
    280         @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { }
    281 
    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 
    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      */
    303     public void setCurrentTab(int tab) {
    304         setCurrentTab(tab, true);
    305     }
    306 
    307     /**
    308      * Change the current tab
    309      */
    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 
    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      */
    346     public boolean isSearchMode() {
    347         return mSearchMode;
    348     }
    349 
    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 
    368     public String getQueryString() {
    369         return mSearchMode ? mQueryString : null;
    370     }
    371 
    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. */
    380     public boolean isUpShowing() {
    381         return mSearchMode; // Only shown on the search mode.
    382     }
    383 
    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 
    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
    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
    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
    505     public boolean onClose() {
    506         setSearchMode(false);
    507         return false;
    508     }
    509 
    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      */
    520     public void clearFocusOnSearchView() {
    521         if (isSearchMode()) {
    522             if (mSearchView != null) {
    523                 mSearchView.clearFocus();
    524             }
    525         }
    526     }
    527 
    528     public void setFocusOnSearchView() {
    529         mSearchView.requestFocus();
    530         mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue.
    531     }
    532 
    533     private void saveLastTabPreference(int tab) {
    534         mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
    535     }
    536 
    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