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