ActionBarAdapter.java revision 3c877e33cb7fecc7a63af1cf3c25061d53811bf6
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.os.Bundle;
30import android.preference.PreferenceManager;
31import android.text.TextUtils;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.inputmethod.InputMethodManager;
35import android.widget.SearchView;
36import android.widget.SearchView.OnCloseListener;
37import android.widget.SearchView.OnQueryTextListener;
38
39/**
40 * Adapter for the action bar at the top of the Contacts activity.
41 */
42public class ActionBarAdapter implements OnQueryTextListener, OnCloseListener {
43
44    public interface Listener {
45        public enum Action {
46            CHANGE_SEARCH_QUERY, START_SEARCH_MODE, STOP_SEARCH_MODE
47        }
48
49        void onAction(Action action);
50
51        /**
52         * Called when the user selects a tab.  The new tab can be obtained using
53         * {@link #getCurrentTab}.
54         */
55        void onSelectedTabChanged();
56    }
57
58    private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
59    private static final String EXTRA_KEY_QUERY = "navBar.query";
60    private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab";
61
62    private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab";
63
64    private boolean mSearchMode;
65    private String mQueryString;
66
67    private SearchView mSearchView;
68
69    private final Context mContext;
70    private final SharedPreferences mPrefs;
71
72    private Listener mListener;
73
74    private final ActionBar mActionBar;
75    private final MyTabListener mTabListener = new MyTabListener();
76
77    private boolean mShowHomeIcon;
78    private boolean mShowTabsAsText;
79
80    public enum TabState {
81        GROUPS,
82        ALL,
83        FAVORITES;
84
85        public static TabState fromInt(int value) {
86            if (GROUPS.ordinal() == value) {
87                return GROUPS;
88            }
89            if (ALL.ordinal() == value) {
90                return ALL;
91            }
92            if (FAVORITES.ordinal() == value) {
93                return FAVORITES;
94            }
95            throw new IllegalArgumentException("Invalid value: " + value);
96        }
97    }
98
99    private static final TabState DEFAULT_TAB = TabState.ALL;
100    private TabState mCurrentTab = DEFAULT_TAB;
101
102    public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar,
103            boolean isUsingTwoPanes) {
104        mContext = context;
105        mListener = listener;
106        mActionBar = actionBar;
107        mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
108
109        mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon);
110
111        // On wide screens, show the tabs as text (instead of icons)
112        mShowTabsAsText = isUsingTwoPanes;
113
114        // Set up search view.
115        View customSearchView = LayoutInflater.from(mActionBar.getThemedContext()).inflate(
116                R.layout.custom_action_bar, null);
117        int searchViewWidth = mContext.getResources().getDimensionPixelSize(
118                R.dimen.search_view_width);
119        if (searchViewWidth == 0) {
120            searchViewWidth = LayoutParams.MATCH_PARENT;
121        }
122        LayoutParams layoutParams = new LayoutParams(searchViewWidth, LayoutParams.WRAP_CONTENT);
123        mSearchView = (SearchView) customSearchView.findViewById(R.id.search_view);
124        // Since the {@link SearchView} in this app is "click-to-expand", set the below mode on the
125        // {@link SearchView} so that the magnifying glass icon appears inside the editable text
126        // field. (In the "click-to-expand" search pattern, the user must explicitly expand the
127        // search field and already knows a search is being conducted, so the icon is redundant
128        // and can go away once the user starts typing.)
129        mSearchView.setIconifiedByDefault(true);
130        mSearchView.setQueryHint(mContext.getString(R.string.hint_findContacts));
131        mSearchView.setOnQueryTextListener(this);
132        mSearchView.setOnCloseListener(this);
133        mSearchView.setQuery(mQueryString, false);
134        mActionBar.setCustomView(customSearchView, layoutParams);
135
136        // Set up tabs
137        addTab(TabState.GROUPS, R.drawable.ic_tab_groups, R.string.contactsGroupsLabel);
138        addTab(TabState.ALL, R.drawable.ic_tab_all, R.string.contactsAllLabel);
139        addTab(TabState.FAVORITES, R.drawable.ic_tab_starred, R.string.contactsFavoritesLabel);
140    }
141
142    public void initialize(Bundle savedState, ContactsRequest request) {
143        if (savedState == null) {
144            mSearchMode = request.isSearchMode();
145            mQueryString = request.getQueryString();
146            mCurrentTab = loadLastTabPreference();
147        } else {
148            mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
149            mQueryString = savedState.getString(EXTRA_KEY_QUERY);
150
151            // Just set to the field here.  The listener will be notified by update().
152            mCurrentTab = TabState.fromInt(savedState.getInt(EXTRA_KEY_SELECTED_TAB));
153        }
154        // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in
155        // search mode.
156        update();
157        // Expanding the {@link SearchView} clears the query, so set the query from the
158        // {@link ContactsRequest} after it has been expanded, if applicable.
159        if (mSearchMode && !TextUtils.isEmpty(mQueryString)) {
160            setQueryString(mQueryString);
161        }
162    }
163
164    public void setListener(Listener listener) {
165        mListener = listener;
166    }
167
168    private void addTab(TabState tabState, int icon, int description) {
169        final Tab tab = mActionBar.newTab();
170        tab.setTag(tabState);
171        tab.setTabListener(mTabListener);
172        if (mShowTabsAsText) {
173            tab.setText(description);
174        } else {
175            tab.setIcon(icon);
176            tab.setContentDescription(description);
177        }
178        mActionBar.addTab(tab);
179    }
180
181    private class MyTabListener implements ActionBar.TabListener {
182        /**
183         * If true, it won't call {@link #setCurrentTab} in {@link #onTabSelected}.
184         * This flag is used when we want to programmatically update the current tab without
185         * {@link #onTabSelected} getting called.
186         */
187        public boolean mIgnoreTabSelected;
188
189        @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { }
190        @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { }
191
192        @Override public void onTabSelected(Tab tab, FragmentTransaction ft) {
193            if (!mIgnoreTabSelected) {
194                setCurrentTab((TabState)tab.getTag());
195            }
196        }
197    }
198
199    /**
200     * Change the current tab, and notify the listener.
201     */
202    public void setCurrentTab(TabState tab) {
203        setCurrentTab(tab, true);
204    }
205
206    /**
207     * Change the current tab
208     */
209    public void setCurrentTab(TabState tab, boolean notifyListener) {
210        if (tab == null) throw new NullPointerException();
211        if (tab == mCurrentTab) {
212            return;
213        }
214        mCurrentTab = tab;
215
216        int index = mCurrentTab.ordinal();
217        if ((mActionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_TABS)
218                && (index != mActionBar.getSelectedNavigationIndex())) {
219            mActionBar.setSelectedNavigationItem(index);
220        }
221
222        if (notifyListener && mListener != null) mListener.onSelectedTabChanged();
223        saveLastTabPreference(mCurrentTab);
224    }
225
226    public TabState getCurrentTab() {
227        return mCurrentTab;
228    }
229
230    /**
231     * @return Whether in search mode, i.e. if the search view is visible/expanded.
232     *
233     * Note even if the action bar is in search mode, if the query is empty, the search fragment
234     * will not be in search mode.
235     */
236    public boolean isSearchMode() {
237        return mSearchMode;
238    }
239
240    public void setSearchMode(boolean flag) {
241        if (mSearchMode != flag) {
242            mSearchMode = flag;
243            update();
244            if (mSearchView == null) {
245                return;
246            }
247            if (mSearchMode) {
248                setFocusOnSearchView();
249            } else {
250                mSearchView.setQuery(null, false);
251            }
252        }
253    }
254
255    public String getQueryString() {
256        return mSearchMode ? mQueryString : null;
257    }
258
259    public void setQueryString(String query) {
260        mQueryString = query;
261        if (mSearchView != null) {
262            mSearchView.setQuery(query, false);
263        }
264    }
265
266    /** @return true if the "UP" icon is showing. */
267    public boolean isUpShowing() {
268        return mSearchMode; // Only shown on the search mode.
269    }
270
271    private void updateDisplayOptions() {
272        // All the flags we may change in this method.
273        final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME
274                | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM;
275
276        // The current flags set to the action bar.  (only the ones that we may change here)
277        final int current = mActionBar.getDisplayOptions() & MASK;
278
279        // Build the new flags...
280        int newFlags = 0;
281        newFlags |= ActionBar.DISPLAY_SHOW_TITLE;
282        if (mShowHomeIcon) {
283            newFlags |= ActionBar.DISPLAY_SHOW_HOME;
284        }
285        if (mSearchMode) {
286            newFlags |= ActionBar.DISPLAY_SHOW_HOME;
287            newFlags |= ActionBar.DISPLAY_HOME_AS_UP;
288            newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM;
289        }
290        mActionBar.setHomeButtonEnabled(mSearchMode);
291
292        if (current != newFlags) {
293            // Pass the mask here to preserve other flags that we're not interested here.
294            mActionBar.setDisplayOptions(newFlags, MASK);
295        }
296    }
297
298    private void update() {
299        if (mSearchMode) {
300            setFocusOnSearchView();
301            // Since we have the {@link SearchView} in a custom action bar, we must manually handle
302            // expanding the {@link SearchView} when a search is initiated. Note that a side effect
303            // of this method is that the {@link SearchView} query text is set to empty string.
304            mSearchView.onActionViewExpanded();
305            if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_STANDARD) {
306                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
307            }
308            if (mListener != null) {
309                mListener.onAction(Action.START_SEARCH_MODE);
310            }
311        } else {
312            if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_TABS) {
313                // setNavigationMode will trigger onTabSelected() with the tab which was previously
314                // selected.
315                // The issue is that when we're first switching to the tab navigation mode after
316                // screen orientation changes, onTabSelected() will get called with the first tab
317                // (i.e. favorite), which would results in mCurrentTab getting set to FAVORITES and
318                // we'd lose restored tab.
319                // So let's just disable the callback here temporarily.  We'll notify the listener
320                // after this anyway.
321                mTabListener.mIgnoreTabSelected = true;
322                mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
323                mActionBar.setSelectedNavigationItem(mCurrentTab.ordinal());
324                mTabListener.mIgnoreTabSelected = false;
325            }
326            mActionBar.setTitle(null);
327            // Since we have the {@link SearchView} in a custom action bar, we must manually handle
328            // collapsing the {@link SearchView} when search mode is exited.
329            mSearchView.onActionViewCollapsed();
330            if (mListener != null) {
331                mListener.onAction(Action.STOP_SEARCH_MODE);
332                mListener.onSelectedTabChanged();
333            }
334        }
335        updateDisplayOptions();
336    }
337
338    @Override
339    public boolean onQueryTextChange(String queryString) {
340        // TODO: Clean up SearchView code because it keeps setting the SearchView query,
341        // invoking onQueryChanged, setting up the fragment again, invalidating the options menu,
342        // storing the SearchView again, and etc... unless we add in the early return statements.
343        if (queryString.equals(mQueryString)) {
344            return false;
345        }
346        mQueryString = queryString;
347        if (!mSearchMode) {
348            if (!TextUtils.isEmpty(queryString)) {
349                setSearchMode(true);
350            }
351        } else if (mListener != null) {
352            mListener.onAction(Action.CHANGE_SEARCH_QUERY);
353        }
354
355        return true;
356    }
357
358    @Override
359    public boolean onQueryTextSubmit(String query) {
360        // When the search is "committed" by the user, then hide the keyboard so the user can
361        // more easily browse the list of results.
362        if (mSearchView != null) {
363            InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
364                    Context.INPUT_METHOD_SERVICE);
365            if (imm != null) {
366                imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
367            }
368            mSearchView.clearFocus();
369        }
370        return true;
371    }
372
373    @Override
374    public boolean onClose() {
375        setSearchMode(false);
376        return false;
377    }
378
379    public void onSaveInstanceState(Bundle outState) {
380        outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
381        outState.putString(EXTRA_KEY_QUERY, mQueryString);
382        outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab.ordinal());
383    }
384
385    /**
386     * Clears the focus from the {@link SearchView} if we are in search mode.
387     * This will suppress the IME if it is visible.
388     */
389    public void clearFocusOnSearchView() {
390        if (isSearchMode()) {
391            if (mSearchView != null) {
392                mSearchView.clearFocus();
393            }
394        }
395    }
396
397    private void setFocusOnSearchView() {
398        mSearchView.requestFocus();
399        mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue.
400    }
401
402    private void saveLastTabPreference(TabState tab) {
403        mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab.ordinal()).apply();
404    }
405
406    private TabState loadLastTabPreference() {
407        try {
408            return TabState.fromInt(mPrefs.getInt(PERSISTENT_LAST_TAB, DEFAULT_TAB.ordinal()));
409        } catch (IllegalArgumentException e) {
410            // Preference is corrupt?
411            return DEFAULT_TAB;
412        }
413    }
414}
415