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