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