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