| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts.activities; |
| |
| import android.app.ActionBar; |
| import android.app.ActionBar.LayoutParams; |
| import android.app.ActionBar.Tab; |
| import android.app.FragmentTransaction; |
| import android.content.Context; |
| import android.content.SharedPreferences; |
| import android.os.Bundle; |
| import android.preference.PreferenceManager; |
| import android.text.TextUtils; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.ArrayAdapter; |
| import android.widget.SearchView; |
| import android.widget.SearchView.OnCloseListener; |
| import android.widget.SearchView.OnQueryTextListener; |
| import android.widget.TextView; |
| |
| import com.android.contacts.R; |
| import com.android.contacts.activities.ActionBarAdapter.Listener.Action; |
| import com.android.contacts.list.ContactsRequest; |
| |
| /** |
| * Adapter for the action bar at the top of the Contacts activity. |
| */ |
| public class ActionBarAdapter implements OnQueryTextListener, OnCloseListener { |
| |
| public interface Listener { |
| public abstract class Action { |
| public static final int CHANGE_SEARCH_QUERY = 0; |
| public static final int START_SEARCH_MODE = 1; |
| public static final int STOP_SEARCH_MODE = 2; |
| } |
| |
| void onAction(int action); |
| |
| /** |
| * Called when the user selects a tab. The new tab can be obtained using |
| * {@link #getCurrentTab}. |
| */ |
| void onSelectedTabChanged(); |
| } |
| |
| private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode"; |
| private static final String EXTRA_KEY_QUERY = "navBar.query"; |
| private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab"; |
| |
| private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab"; |
| |
| private boolean mSearchMode; |
| private String mQueryString; |
| |
| private SearchView mSearchView; |
| |
| private final Context mContext; |
| private final SharedPreferences mPrefs; |
| |
| private Listener mListener; |
| |
| private final ActionBar mActionBar; |
| private final int mActionBarNavigationMode; |
| private final MyTabListener mTabListener; |
| private final MyNavigationListener mNavigationListener; |
| |
| private boolean mShowHomeIcon; |
| private boolean mShowTabsAsText; |
| |
| public interface TabState { |
| public static int GROUPS = 0; |
| public static int ALL = 1; |
| public static int FAVORITES = 2; |
| |
| public static int COUNT = 3; |
| public static int DEFAULT = ALL; |
| } |
| |
| private int mCurrentTab = TabState.DEFAULT; |
| |
| /** |
| * Extension of ArrayAdapter to be used for the action bar navigation drop list. It is not |
| * possible to change the text appearance of a text item that is in the spinner header or |
| * in the drop down list using a selector xml file. The only way to differentiate the two |
| * is if the view is gotten via {@link #getView(int, View, ViewGroup)} or |
| * {@link #getDropDownView(int, View, ViewGroup)}. |
| */ |
| private class CustomArrayAdapter extends ArrayAdapter<String> { |
| |
| public CustomArrayAdapter(Context context, int textResId) { |
| super(context, textResId); |
| } |
| |
| public View getView (int position, View convertView, ViewGroup parent) { |
| TextView textView = (TextView) super.getView(position, convertView, parent); |
| textView.setTextAppearance(mContext, |
| R.style.PeopleNavigationDropDownHeaderTextAppearance); |
| return textView; |
| } |
| |
| public View getDropDownView (int position, View convertView, ViewGroup parent) { |
| TextView textView = (TextView) super.getDropDownView(position, convertView, parent); |
| textView.setTextAppearance(mContext, |
| R.style.PeopleNavigationDropDownTextAppearance); |
| return textView; |
| } |
| } |
| |
| public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar, |
| boolean isUsingTwoPanes) { |
| mContext = context; |
| mListener = listener; |
| mActionBar = actionBar; |
| mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); |
| |
| mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon); |
| |
| // On wide screens, show the tabs as text (instead of icons) |
| mShowTabsAsText = isUsingTwoPanes; |
| if (isUsingTwoPanes) { |
| mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_LIST; |
| mTabListener = null; |
| mNavigationListener = new MyNavigationListener(); |
| } else { |
| mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_TABS; |
| mTabListener = new MyTabListener(); |
| mNavigationListener = null; |
| } |
| |
| // Set up search view. |
| View customSearchView = LayoutInflater.from(mActionBar.getThemedContext()).inflate( |
| R.layout.custom_action_bar, null); |
| int searchViewWidth = mContext.getResources().getDimensionPixelSize( |
| R.dimen.search_view_width); |
| if (searchViewWidth == 0) { |
| searchViewWidth = LayoutParams.MATCH_PARENT; |
| } |
| LayoutParams layoutParams = new LayoutParams(searchViewWidth, LayoutParams.WRAP_CONTENT); |
| mSearchView = (SearchView) customSearchView.findViewById(R.id.search_view); |
| // Since the {@link SearchView} in this app is "click-to-expand", set the below mode on the |
| // {@link SearchView} so that the magnifying glass icon appears inside the editable text |
| // field. (In the "click-to-expand" search pattern, the user must explicitly expand the |
| // search field and already knows a search is being conducted, so the icon is redundant |
| // and can go away once the user starts typing.) |
| mSearchView.setIconifiedByDefault(true); |
| mSearchView.setQueryHint(mContext.getString(R.string.hint_findContacts)); |
| mSearchView.setOnQueryTextListener(this); |
| mSearchView.setOnCloseListener(this); |
| mSearchView.setQuery(mQueryString, false); |
| mActionBar.setCustomView(customSearchView, layoutParams); |
| |
| // Set up tabs or navigation list |
| switch(mActionBarNavigationMode) { |
| case ActionBar.NAVIGATION_MODE_TABS: |
| setupTabs(); |
| break; |
| case ActionBar.NAVIGATION_MODE_LIST: |
| setupNavigationList(); |
| break; |
| } |
| } |
| |
| private void setupTabs() { |
| addTab(TabState.GROUPS, R.drawable.ic_tab_groups, R.string.contactsGroupsLabel); |
| addTab(TabState.ALL, R.drawable.ic_tab_all, R.string.contactsAllLabel); |
| addTab(TabState.FAVORITES, R.drawable.ic_tab_starred, R.string.contactsFavoritesLabel); |
| } |
| |
| private void setupNavigationList() { |
| ArrayAdapter<String> navAdapter = new CustomArrayAdapter(mContext, |
| R.layout.people_navigation_item); |
| navAdapter.add(mContext.getString(R.string.contactsAllLabel)); |
| navAdapter.add(mContext.getString(R.string.contactsFavoritesLabel)); |
| navAdapter.add(mContext.getString(R.string.contactsGroupsLabel)); |
| mActionBar.setListNavigationCallbacks(navAdapter, mNavigationListener); |
| } |
| |
| /** |
| * Because the navigation list items are in a different order than tab items, this returns |
| * the appropriate tab from the navigation item position. |
| */ |
| private int getTabPositionFromNavigationItemPosition(int navItemPos) { |
| switch(navItemPos) { |
| case 0: |
| return TabState.ALL; |
| case 1: |
| return TabState.FAVORITES; |
| case 2: |
| return TabState.GROUPS; |
| } |
| throw new IllegalArgumentException( |
| "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1) |
| + " inclusive."); |
| } |
| |
| /** |
| * This is the inverse of {@link getTabPositionFromNavigationItemPosition}. |
| */ |
| private int getNavigationItemPositionFromTabPosition(int tabPos) { |
| switch(tabPos) { |
| case TabState.ALL: |
| return 0; |
| case TabState.FAVORITES: |
| return 1; |
| case TabState.GROUPS: |
| return 2; |
| } |
| throw new IllegalArgumentException( |
| "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1) |
| + " inclusive."); |
| } |
| |
| public void initialize(Bundle savedState, ContactsRequest request) { |
| if (savedState == null) { |
| mSearchMode = request.isSearchMode(); |
| mQueryString = request.getQueryString(); |
| mCurrentTab = loadLastTabPreference(); |
| } else { |
| mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE); |
| mQueryString = savedState.getString(EXTRA_KEY_QUERY); |
| |
| // Just set to the field here. The listener will be notified by update(). |
| mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB); |
| } |
| // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in |
| // search mode. |
| update(); |
| // Expanding the {@link SearchView} clears the query, so set the query from the |
| // {@link ContactsRequest} after it has been expanded, if applicable. |
| if (mSearchMode && !TextUtils.isEmpty(mQueryString)) { |
| setQueryString(mQueryString); |
| } |
| } |
| |
| public void setListener(Listener listener) { |
| mListener = listener; |
| } |
| |
| private void addTab(int expectedTabIndex, int icon, int description) { |
| final Tab tab = mActionBar.newTab(); |
| tab.setTabListener(mTabListener); |
| if (mShowTabsAsText) { |
| tab.setText(description); |
| } else { |
| tab.setIcon(icon); |
| tab.setContentDescription(description); |
| } |
| mActionBar.addTab(tab); |
| if (expectedTabIndex != tab.getPosition()) { |
| throw new IllegalStateException("Tabs must be created in the right order"); |
| } |
| } |
| |
| private class MyTabListener implements ActionBar.TabListener { |
| /** |
| * If true, it won't call {@link #setCurrentTab} in {@link #onTabSelected}. |
| * This flag is used when we want to programmatically update the current tab without |
| * {@link #onTabSelected} getting called. |
| */ |
| public boolean mIgnoreTabSelected; |
| |
| @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { } |
| @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { } |
| |
| @Override public void onTabSelected(Tab tab, FragmentTransaction ft) { |
| if (!mIgnoreTabSelected) { |
| setCurrentTab(tab.getPosition()); |
| } |
| } |
| } |
| |
| private class MyNavigationListener implements ActionBar.OnNavigationListener { |
| public boolean mIgnoreNavigationItemSelected; |
| |
| public boolean onNavigationItemSelected(int itemPosition, long itemId) { |
| if (!mIgnoreNavigationItemSelected) { |
| setCurrentTab(getTabPositionFromNavigationItemPosition(itemPosition)); |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * Change the current tab, and notify the listener. |
| */ |
| public void setCurrentTab(int tab) { |
| setCurrentTab(tab, true); |
| } |
| |
| /** |
| * Change the current tab |
| */ |
| public void setCurrentTab(int tab, boolean notifyListener) { |
| if (tab == mCurrentTab) { |
| return; |
| } |
| mCurrentTab = tab; |
| |
| final int actionBarSelectedNavIndex = mActionBar.getSelectedNavigationIndex(); |
| switch(mActionBar.getNavigationMode()) { |
| case ActionBar.NAVIGATION_MODE_TABS: |
| if (mCurrentTab != actionBarSelectedNavIndex) { |
| mActionBar.setSelectedNavigationItem(mCurrentTab); |
| } |
| break; |
| case ActionBar.NAVIGATION_MODE_LIST: |
| if (mCurrentTab != getTabPositionFromNavigationItemPosition( |
| actionBarSelectedNavIndex)) { |
| mActionBar.setSelectedNavigationItem( |
| getNavigationItemPositionFromTabPosition(mCurrentTab)); |
| } |
| break; |
| } |
| |
| if (notifyListener && mListener != null) mListener.onSelectedTabChanged(); |
| saveLastTabPreference(mCurrentTab); |
| } |
| |
| public int getCurrentTab() { |
| return mCurrentTab; |
| } |
| |
| /** |
| * @return Whether in search mode, i.e. if the search view is visible/expanded. |
| * |
| * Note even if the action bar is in search mode, if the query is empty, the search fragment |
| * will not be in search mode. |
| */ |
| public boolean isSearchMode() { |
| return mSearchMode; |
| } |
| |
| public void setSearchMode(boolean flag) { |
| if (mSearchMode != flag) { |
| mSearchMode = flag; |
| update(); |
| if (mSearchView == null) { |
| return; |
| } |
| if (mSearchMode) { |
| setFocusOnSearchView(); |
| } else { |
| mSearchView.setQuery(null, false); |
| } |
| } else if (flag) { |
| // Everything is already set up. Still make sure the keyboard is up |
| if (mSearchView != null) setFocusOnSearchView(); |
| } |
| } |
| |
| public String getQueryString() { |
| return mSearchMode ? mQueryString : null; |
| } |
| |
| public void setQueryString(String query) { |
| mQueryString = query; |
| if (mSearchView != null) { |
| mSearchView.setQuery(query, false); |
| } |
| } |
| |
| /** @return true if the "UP" icon is showing. */ |
| public boolean isUpShowing() { |
| return mSearchMode; // Only shown on the search mode. |
| } |
| |
| private void updateDisplayOptions() { |
| // All the flags we may change in this method. |
| final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME |
| | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM; |
| |
| // The current flags set to the action bar. (only the ones that we may change here) |
| final int current = mActionBar.getDisplayOptions() & MASK; |
| |
| // Build the new flags... |
| int newFlags = 0; |
| newFlags |= ActionBar.DISPLAY_SHOW_TITLE; |
| if (mShowHomeIcon) { |
| newFlags |= ActionBar.DISPLAY_SHOW_HOME; |
| } |
| if (mSearchMode) { |
| newFlags |= ActionBar.DISPLAY_SHOW_HOME; |
| newFlags |= ActionBar.DISPLAY_HOME_AS_UP; |
| newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM; |
| } |
| mActionBar.setHomeButtonEnabled(mSearchMode); |
| |
| if (current != newFlags) { |
| // Pass the mask here to preserve other flags that we're not interested here. |
| mActionBar.setDisplayOptions(newFlags, MASK); |
| } |
| } |
| |
| private void update() { |
| boolean isIconifiedChanging = mSearchView.isIconified() == mSearchMode; |
| if (mSearchMode) { |
| setFocusOnSearchView(); |
| // Since we have the {@link SearchView} in a custom action bar, we must manually handle |
| // expanding the {@link SearchView} when a search is initiated. Note that a side effect |
| // of this method is that the {@link SearchView} query text is set to empty string. |
| if (isIconifiedChanging) { |
| mSearchView.onActionViewExpanded(); |
| } |
| if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_STANDARD) { |
| mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); |
| } |
| if (mListener != null) { |
| mListener.onAction(Action.START_SEARCH_MODE); |
| } |
| } else { |
| final int currentNavigationMode = mActionBar.getNavigationMode(); |
| if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_TABS |
| && currentNavigationMode != ActionBar.NAVIGATION_MODE_TABS) { |
| // setNavigationMode will trigger onTabSelected() with the tab which was previously |
| // selected. |
| // The issue is that when we're first switching to the tab navigation mode after |
| // screen orientation changes, onTabSelected() will get called with the first tab |
| // (i.e. favorite), which would results in mCurrentTab getting set to FAVORITES and |
| // we'd lose restored tab. |
| // So let's just disable the callback here temporarily. We'll notify the listener |
| // after this anyway. |
| mTabListener.mIgnoreTabSelected = true; |
| mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); |
| mActionBar.setSelectedNavigationItem(mCurrentTab); |
| mTabListener.mIgnoreTabSelected = false; |
| } else if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_LIST |
| && currentNavigationMode != ActionBar.NAVIGATION_MODE_LIST) { |
| mNavigationListener.mIgnoreNavigationItemSelected = true; |
| mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); |
| mActionBar.setSelectedNavigationItem( |
| getNavigationItemPositionFromTabPosition(mCurrentTab)); |
| mNavigationListener.mIgnoreNavigationItemSelected = false; |
| } |
| mActionBar.setTitle(null); |
| // Since we have the {@link SearchView} in a custom action bar, we must manually handle |
| // collapsing the {@link SearchView} when search mode is exited. |
| if (isIconifiedChanging) { |
| mSearchView.onActionViewCollapsed(); |
| } |
| if (mListener != null) { |
| mListener.onAction(Action.STOP_SEARCH_MODE); |
| mListener.onSelectedTabChanged(); |
| } |
| } |
| updateDisplayOptions(); |
| } |
| |
| @Override |
| public boolean onQueryTextChange(String queryString) { |
| // TODO: Clean up SearchView code because it keeps setting the SearchView query, |
| // invoking onQueryChanged, setting up the fragment again, invalidating the options menu, |
| // storing the SearchView again, and etc... unless we add in the early return statements. |
| if (queryString.equals(mQueryString)) { |
| return false; |
| } |
| mQueryString = queryString; |
| if (!mSearchMode) { |
| if (!TextUtils.isEmpty(queryString)) { |
| setSearchMode(true); |
| } |
| } else if (mListener != null) { |
| mListener.onAction(Action.CHANGE_SEARCH_QUERY); |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public boolean onQueryTextSubmit(String query) { |
| // When the search is "committed" by the user, then hide the keyboard so the user can |
| // more easily browse the list of results. |
| if (mSearchView != null) { |
| InputMethodManager imm = (InputMethodManager) mContext.getSystemService( |
| Context.INPUT_METHOD_SERVICE); |
| if (imm != null) { |
| imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0); |
| } |
| mSearchView.clearFocus(); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onClose() { |
| setSearchMode(false); |
| return false; |
| } |
| |
| public void onSaveInstanceState(Bundle outState) { |
| outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode); |
| outState.putString(EXTRA_KEY_QUERY, mQueryString); |
| outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab); |
| } |
| |
| /** |
| * Clears the focus from the {@link SearchView} if we are in search mode. |
| * This will suppress the IME if it is visible. |
| */ |
| public void clearFocusOnSearchView() { |
| if (isSearchMode()) { |
| if (mSearchView != null) { |
| mSearchView.clearFocus(); |
| } |
| } |
| } |
| |
| public void setFocusOnSearchView() { |
| mSearchView.requestFocus(); |
| mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue. |
| } |
| |
| private void saveLastTabPreference(int tab) { |
| mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply(); |
| } |
| |
| private int loadLastTabPreference() { |
| try { |
| return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT); |
| } catch (IllegalArgumentException e) { |
| // Preference is corrupt? |
| return TabState.DEFAULT; |
| } |
| } |
| } |