ActionBarAdapter.java revision 7f5220e23b37b76cf484b28c182a0c98749b59e2
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 com.android.contacts.R;
20import com.android.contacts.activities.ActionBarAdapter.Listener.Action;
21import com.android.contacts.list.ContactsRequest;
22
23import android.app.ActionBar;
24import android.app.ActionBar.LayoutParams;
25import android.app.ActionBar.Tab;
26import android.app.FragmentTransaction;
27import android.content.Context;
28import android.content.SharedPreferences;
29import android.graphics.Color;
30import android.os.Bundle;
31import android.preference.PreferenceManager;
32import android.text.TextUtils;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.inputmethod.InputMethodManager;
37import android.widget.ArrayAdapter;
38import android.widget.SearchView;
39import android.widget.SearchView.OnCloseListener;
40import android.widget.SearchView.OnQueryTextListener;
41import android.widget.TextView;
42
43/**
44 * Adapter for the action bar at the top of the Contacts activity.
45 */
46public 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