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