ActionBarAdapter.java revision 26dc5592145eeddcd435daf2dae29d8e483f1b7b
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
17package com.android.contacts.activities;
18
19import android.app.ActionBar;
20import android.app.ActionBar.LayoutParams;
21import android.app.ActionBar.Tab;
22import android.app.FragmentTransaction;
23import android.content.Context;
24import android.content.SharedPreferences;
25import android.os.Bundle;
26import android.preference.PreferenceManager;
27import android.text.TextUtils;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.inputmethod.InputMethodManager;
32import android.widget.ArrayAdapter;
33import android.widget.SearchView;
34import android.widget.SearchView.OnCloseListener;
35import android.widget.SearchView.OnQueryTextListener;
36import android.widget.TextView;
37
38import com.android.contacts.R;
39import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
40import com.android.contacts.list.ContactsRequest;
41
42/**
43 * Adapter for the action bar at the top of the Contacts activity.
44 */
45public 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 FAVORITES = 0;
89        public static int ALL = 1;
90        public static int GROUPS = 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.FAVORITES, R.drawable.ic_tab_starred, R.string.contactsFavoritesLabel);
182        addTab(TabState.ALL, R.drawable.ic_tab_all, R.string.contactsAllLabel);
183        addTab(TabState.GROUPS, R.drawable.ic_tab_groups, R.string.contactsGroupsLabel);
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.contactsFavoritesLabel));
190        navAdapter.add(mContext.getString(R.string.contactsAllLabel));
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.FAVORITES;
203            case 1:
204                return TabState.ALL;
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.FAVORITES:
219                return 0;
220            case TabState.ALL:
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        if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) {
243            // Invalid tab index was saved (b/12938207). Restore the default.
244            mCurrentTab = TabState.DEFAULT;
245        }
246        // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
247        // search mode.
248        update();
249        // Expanding the {@link SearchView} clears the query, so set the query from the
250        // {@link ContactsRequest} after it has been expanded, if applicable.
251        if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
252            setQueryString(mQueryString);
253        }
254    }
255
256    public void setListener(Listener listener) {
257        mListener = listener;
258    }
259
260    private void addTab(int expectedTabIndex, int icon, int description) {
261        final Tab tab = mActionBar.newTab();
262        tab.setTabListener(mTabListener);
263        if (mShowTabsAsText) {
264            tab.setText(description);
265        } else {
266            tab.setIcon(icon);
267            tab.setContentDescription(description);
268        }
269        mActionBar.addTab(tab);
270        if (expectedTabIndex != tab.getPosition()) {
271            throw new IllegalStateException("Tabs must be created in the right order");
272        }
273    }
274
275    private class MyTabListener implements ActionBar.TabListener {
276        /**
277         * If true, it won't call {@link #setCurrentTab} in {@link #onTabSelected}.
278         * This flag is used when we want to programmatically update the current tab without
279         * {@link #onTabSelected} getting called.
280         */
281        public boolean mIgnoreTabSelected;
282
283        @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { }
284        @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { }
285
286        @Override public void onTabSelected(Tab tab, FragmentTransaction ft) {
287            if (!mIgnoreTabSelected) {
288                setCurrentTab(tab.getPosition());
289            }
290        }
291    }
292
293    private class MyNavigationListener implements ActionBar.OnNavigationListener {
294        public boolean mIgnoreNavigationItemSelected;
295
296        public boolean onNavigationItemSelected(int itemPosition, long itemId) {
297            if (!mIgnoreNavigationItemSelected) {
298                setCurrentTab(getTabPositionFromNavigationItemPosition(itemPosition));
299            }
300            return true;
301        }
302    }
303
304    /**
305     * Change the current tab, and notify the listener.
306     */
307    public void setCurrentTab(int tab) {
308        setCurrentTab(tab, true);
309    }
310
311    /**
312     * Change the current tab
313     */
314    public void setCurrentTab(int tab, boolean notifyListener) {
315        if (tab == mCurrentTab) {
316            return;
317        }
318        mCurrentTab = tab;
319
320        final int actionBarSelectedNavIndex = mActionBar.getSelectedNavigationIndex();
321        switch(mActionBar.getNavigationMode()) {
322            case ActionBar.NAVIGATION_MODE_TABS:
323                if (mCurrentTab != actionBarSelectedNavIndex) {
324                    mActionBar.setSelectedNavigationItem(mCurrentTab);
325                }
326                break;
327            case ActionBar.NAVIGATION_MODE_LIST:
328                if (mCurrentTab != getTabPositionFromNavigationItemPosition(
329                        actionBarSelectedNavIndex)) {
330                    mActionBar.setSelectedNavigationItem(
331                            getNavigationItemPositionFromTabPosition(mCurrentTab));
332                }
333                break;
334        }
335
336        if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
337        saveLastTabPreference(mCurrentTab);
338    }
339
340    public int getCurrentTab() {
341        return mCurrentTab;
342    }
343
344    /**
345     * @return Whether in search mode, i.e. if the search view is visible/expanded.
346     *
347     * Note even if the action bar is in search mode, if the query is empty, the search fragment
348     * will not be in search mode.
349     */
350    public boolean isSearchMode() {
351        return mSearchMode;
352    }
353
354    public void setSearchMode(boolean flag) {
355        if (mSearchMode != flag) {
356            mSearchMode = flag;
357            update();
358            if (mSearchView == null) {
359                return;
360            }
361            if (mSearchMode) {
362                setFocusOnSearchView();
363            } else {
364                mSearchView.setQuery(null, false);
365            }
366        } else if (flag) {
367            // Everything is already set up. Still make sure the keyboard is up
368            if (mSearchView != null) setFocusOnSearchView();
369        }
370    }
371
372    public String getQueryString() {
373        return mSearchMode ? mQueryString : null;
374    }
375
376    public void setQueryString(String query) {
377        mQueryString = query;
378        if (mSearchView != null) {
379            mSearchView.setQuery(query, false);
380        }
381    }
382
383    /** @return true if the "UP" icon is showing. */
384    public boolean isUpShowing() {
385        return mSearchMode; // Only shown on the search mode.
386    }
387
388    private void updateDisplayOptions() {
389        // All the flags we may change in this method.
390        final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
391                | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
392
393        // The current flags set to the action bar.  (only the ones that we may change here)
394        final int current = mActionBar.getDisplayOptions() & MASK;
395
396        // Build the new flags...
397        int newFlags = 0;
398        newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
399        if (mShowHomeIcon) {
400            newFlags |= ActionBar.DISPLAY_SHOW_HOME;
401        }
402        if (mSearchMode) {
403            newFlags |= ActionBar.DISPLAY_SHOW_HOME;
404            newFlags |= ActionBar.DISPLAY_HOME_AS_UP;
405            newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
406        }
407        mActionBar.setHomeButtonEnabled(mSearchMode);
408
409        if (current != newFlags) {
410            // Pass the mask here to preserve other flags that we're not interested here.
411            mActionBar.setDisplayOptions(newFlags, MASK);
412        }
413    }
414
415    private void update() {
416        boolean isIconifiedChanging = mSearchView.isIconified() == mSearchMode;
417        if (mSearchMode) {
418            setFocusOnSearchView();
419            // Since we have the {@link SearchView} in a custom action bar, we must manually handle
420            // expanding the {@link SearchView} when a search is initiated. Note that a side effect
421            // of this method is that the {@link SearchView} query text is set to empty string.
422            if (isIconifiedChanging) {
423                final CharSequence queryText = mSearchView.getQuery();
424                mSearchView.onActionViewExpanded();
425                if (!TextUtils.isEmpty(queryText)) {
426                    mSearchView.setQuery(queryText, false);
427                }
428            }
429            if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_STANDARD) {
430                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
431            }
432            if (mListener != null) {
433                mListener.onAction(Action.START_SEARCH_MODE);
434            }
435        } else {
436            final int currentNavigationMode = mActionBar.getNavigationMode();
437            if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_TABS
438                    && currentNavigationMode != ActionBar.NAVIGATION_MODE_TABS) {
439                // setNavigationMode will trigger onTabSelected() with the tab which was previously
440                // selected.
441                // The issue is that when we're first switching to the tab navigation mode after
442                // screen orientation changes, onTabSelected() will get called with the first tab
443                // (i.e. favorite), which would results in mCurrentTab getting set to FAVORITES and
444                // we'd lose restored tab.
445                // So let's just disable the callback here temporarily.  We'll notify the listener
446                // after this anyway.
447                mTabListener.mIgnoreTabSelected = true;
448                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
449                mActionBar.setSelectedNavigationItem(mCurrentTab);
450                mTabListener.mIgnoreTabSelected = false;
451            } else if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_LIST
452                    && currentNavigationMode != ActionBar.NAVIGATION_MODE_LIST) {
453                mNavigationListener.mIgnoreNavigationItemSelected = true;
454                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
455                mActionBar.setSelectedNavigationItem(
456                        getNavigationItemPositionFromTabPosition(mCurrentTab));
457                mNavigationListener.mIgnoreNavigationItemSelected = false;
458            }
459            mActionBar.setTitle(null);
460            // Since we have the {@link SearchView} in a custom action bar, we must manually handle
461            // collapsing the {@link SearchView} when search mode is exited.
462            if (isIconifiedChanging) {
463                mSearchView.onActionViewCollapsed();
464            }
465            if (mListener != null) {
466                mListener.onAction(Action.STOP_SEARCH_MODE);
467                mListener.onSelectedTabChanged();
468            }
469        }
470        updateDisplayOptions();
471    }
472
473    @Override
474    public boolean onQueryTextChange(String queryString) {
475        // TODO: Clean up SearchView code because it keeps setting the SearchView query,
476        // invoking onQueryChanged, setting up the fragment again, invalidating the options menu,
477        // storing the SearchView again, and etc... unless we add in the early return statements.
478        if (queryString.equals(mQueryString)) {
479            return false;
480        }
481        mQueryString = queryString;
482        if (!mSearchMode) {
483            if (!TextUtils.isEmpty(queryString)) {
484                setSearchMode(true);
485            }
486        } else if (mListener != null) {
487            mListener.onAction(Action.CHANGE_SEARCH_QUERY);
488        }
489
490        return true;
491    }
492
493    @Override
494    public boolean onQueryTextSubmit(String query) {
495        // When the search is "committed" by the user, then hide the keyboard so the user can
496        // more easily browse the list of results.
497        if (mSearchView != null) {
498            InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
499                    Context.INPUT_METHOD_SERVICE);
500            if (imm != null) {
501                imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
502            }
503            mSearchView.clearFocus();
504        }
505        return true;
506    }
507
508    @Override
509    public boolean onClose() {
510        setSearchMode(false);
511        return false;
512    }
513
514    public void onSaveInstanceState(Bundle outState) {
515        outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
516        outState.putString(EXTRA_KEY_QUERY, mQueryString);
517        outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab);
518    }
519
520    /**
521     * Clears the focus from the {@link SearchView} if we are in search mode.
522     * This will suppress the IME if it is visible.
523     */
524    public void clearFocusOnSearchView() {
525        if (isSearchMode()) {
526            if (mSearchView != null) {
527                mSearchView.clearFocus();
528            }
529        }
530    }
531
532    public void setFocusOnSearchView() {
533        mSearchView.requestFocus();
534        mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue.
535    }
536
537    private void saveLastTabPreference(int tab) {
538        mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply();
539    }
540
541    private int loadLastTabPreference() {
542        try {
543            return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT);
544        } catch (IllegalArgumentException e) {
545            // Preference is corrupt?
546            return TabState.DEFAULT;
547        }
548    }
549}
550