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 17 package com.android.contacts.activities; 18 19 import com.android.contacts.R; 20 import com.android.contacts.activities.ActionBarAdapter.Listener.Action; 21 import com.android.contacts.list.ContactsRequest; 22 23 import android.app.ActionBar; 24 import android.app.ActionBar.LayoutParams; 25 import android.app.ActionBar.Tab; 26 import android.app.FragmentTransaction; 27 import android.content.Context; 28 import android.content.SharedPreferences; 29 import android.graphics.Color; 30 import android.os.Bundle; 31 import android.preference.PreferenceManager; 32 import android.text.TextUtils; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.inputmethod.InputMethodManager; 37 import android.widget.ArrayAdapter; 38 import android.widget.SearchView; 39 import android.widget.SearchView.OnCloseListener; 40 import android.widget.SearchView.OnQueryTextListener; 41 import android.widget.TextView; 42 43 /** 44 * Adapter for the action bar at the top of the Contacts activity. 45 */ 46 public class ActionBarAdapter implements OnQueryTextListener, OnCloseListener { 47 48 public interface Listener { 49 public abstract class Action { 50 public static final int CHANGE_SEARCH_QUERY = 0; 51 public static final int START_SEARCH_MODE = 1; 52 public static final int STOP_SEARCH_MODE = 2; 53 } 54 55 void onAction(int action); 56 57 /** 58 * Called when the user selects a tab. The new tab can be obtained using 59 * {@link #getCurrentTab}. 60 */ 61 void onSelectedTabChanged(); 62 } 63 64 private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode"; 65 private static final String EXTRA_KEY_QUERY = "navBar.query"; 66 private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab"; 67 68 private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab"; 69 70 private boolean mSearchMode; 71 private String mQueryString; 72 73 private SearchView mSearchView; 74 75 private final Context mContext; 76 private final SharedPreferences mPrefs; 77 78 private Listener mListener; 79 80 private final ActionBar mActionBar; 81 private final int mActionBarNavigationMode; 82 private final MyTabListener mTabListener; 83 private final MyNavigationListener mNavigationListener; 84 85 private boolean mShowHomeIcon; 86 private boolean mShowTabsAsText; 87 88 public interface TabState { 89 public static int GROUPS = 0; 90 public static int ALL = 1; 91 public static int FAVORITES = 2; 92 93 public static int COUNT = 3; 94 public static int DEFAULT = ALL; 95 } 96 97 private int mCurrentTab = TabState.DEFAULT; 98 99 /** 100 * Extension of ArrayAdapter to be used for the action bar navigation drop list. It is not 101 * possible to change the text appearance of a text item that is in the spinner header or 102 * in the drop down list using a selector xml file. The only way to differentiate the two 103 * is if the view is gotten via {@link #getView(int, View, ViewGroup)} or 104 * {@link #getDropDownView(int, View, ViewGroup)}. 105 */ 106 private class CustomArrayAdapter extends ArrayAdapter<String> { 107 108 public CustomArrayAdapter(Context context, int textResId) { 109 super(context, textResId); 110 } 111 112 public View getView (int position, View convertView, ViewGroup parent) { 113 TextView textView = (TextView) super.getView(position, convertView, parent); 114 textView.setTextAppearance(mContext, 115 R.style.PeopleNavigationDropDownHeaderTextAppearance); 116 return textView; 117 } 118 119 public View getDropDownView (int position, View convertView, ViewGroup parent) { 120 TextView textView = (TextView) super.getDropDownView(position, convertView, parent); 121 textView.setTextAppearance(mContext, 122 R.style.PeopleNavigationDropDownTextAppearance); 123 return textView; 124 } 125 } 126 127 public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar, 128 boolean isUsingTwoPanes) { 129 mContext = context; 130 mListener = listener; 131 mActionBar = actionBar; 132 mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); 133 134 mShowHomeIcon = mContext.getResources().getBoolean(R.bool.show_home_icon); 135 136 // On wide screens, show the tabs as text (instead of icons) 137 mShowTabsAsText = isUsingTwoPanes; 138 if (isUsingTwoPanes) { 139 mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_LIST; 140 mTabListener = null; 141 mNavigationListener = new MyNavigationListener(); 142 } else { 143 mActionBarNavigationMode = ActionBar.NAVIGATION_MODE_TABS; 144 mTabListener = new MyTabListener(); 145 mNavigationListener = null; 146 } 147 148 // Set up search view. 149 View customSearchView = LayoutInflater.from(mActionBar.getThemedContext()).inflate( 150 R.layout.custom_action_bar, null); 151 int searchViewWidth = mContext.getResources().getDimensionPixelSize( 152 R.dimen.search_view_width); 153 if (searchViewWidth == 0) { 154 searchViewWidth = LayoutParams.MATCH_PARENT; 155 } 156 LayoutParams layoutParams = new LayoutParams(searchViewWidth, LayoutParams.WRAP_CONTENT); 157 mSearchView = (SearchView) customSearchView.findViewById(R.id.search_view); 158 // Since the {@link SearchView} in this app is "click-to-expand", set the below mode on the 159 // {@link SearchView} so that the magnifying glass icon appears inside the editable text 160 // field. (In the "click-to-expand" search pattern, the user must explicitly expand the 161 // search field and already knows a search is being conducted, so the icon is redundant 162 // and can go away once the user starts typing.) 163 mSearchView.setIconifiedByDefault(true); 164 mSearchView.setQueryHint(mContext.getString(R.string.hint_findContacts)); 165 mSearchView.setOnQueryTextListener(this); 166 mSearchView.setOnCloseListener(this); 167 mSearchView.setQuery(mQueryString, false); 168 mActionBar.setCustomView(customSearchView, layoutParams); 169 170 // Set up tabs or navigation list 171 switch(mActionBarNavigationMode) { 172 case ActionBar.NAVIGATION_MODE_TABS: 173 setupTabs(); 174 break; 175 case ActionBar.NAVIGATION_MODE_LIST: 176 setupNavigationList(); 177 break; 178 } 179 } 180 181 private void setupTabs() { 182 addTab(TabState.GROUPS, R.drawable.ic_tab_groups, R.string.contactsGroupsLabel); 183 addTab(TabState.ALL, R.drawable.ic_tab_all, R.string.contactsAllLabel); 184 addTab(TabState.FAVORITES, R.drawable.ic_tab_starred, R.string.contactsFavoritesLabel); 185 } 186 187 private void setupNavigationList() { 188 ArrayAdapter<String> navAdapter = new CustomArrayAdapter(mContext, 189 R.layout.people_navigation_item); 190 navAdapter.add(mContext.getString(R.string.contactsAllLabel)); 191 navAdapter.add(mContext.getString(R.string.contactsFavoritesLabel)); 192 navAdapter.add(mContext.getString(R.string.contactsGroupsLabel)); 193 mActionBar.setListNavigationCallbacks(navAdapter, mNavigationListener); 194 } 195 196 /** 197 * Because the navigation list items are in a different order than tab items, this returns 198 * the appropriate tab from the navigation item position. 199 */ 200 private int getTabPositionFromNavigationItemPosition(int navItemPos) { 201 switch(navItemPos) { 202 case 0: 203 return TabState.ALL; 204 case 1: 205 return TabState.FAVORITES; 206 case 2: 207 return TabState.GROUPS; 208 } 209 throw new IllegalArgumentException( 210 "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1) 211 + " inclusive."); 212 } 213 214 /** 215 * This is the inverse of {@link getTabPositionFromNavigationItemPosition}. 216 */ 217 private int getNavigationItemPositionFromTabPosition(int tabPos) { 218 switch(tabPos) { 219 case TabState.ALL: 220 return 0; 221 case TabState.FAVORITES: 222 return 1; 223 case TabState.GROUPS: 224 return 2; 225 } 226 throw new IllegalArgumentException( 227 "Parameter must be between 0 and " + Integer.toString(TabState.COUNT-1) 228 + " inclusive."); 229 } 230 231 public void initialize(Bundle savedState, ContactsRequest request) { 232 if (savedState == null) { 233 mSearchMode = request.isSearchMode(); 234 mQueryString = request.getQueryString(); 235 mCurrentTab = loadLastTabPreference(); 236 } else { 237 mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE); 238 mQueryString = savedState.getString(EXTRA_KEY_QUERY); 239 240 // Just set to the field here. The listener will be notified by update(). 241 mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB); 242 } 243 // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in 244 // search mode. 245 update(); 246 // Expanding the {@link SearchView} clears the query, so set the query from the 247 // {@link ContactsRequest} after it has been expanded, if applicable. 248 if (mSearchMode && !TextUtils.isEmpty(mQueryString)) { 249 setQueryString(mQueryString); 250 } 251 } 252 253 public void setListener(Listener listener) { 254 mListener = listener; 255 } 256 257 private void addTab(int expectedTabIndex, int icon, int description) { 258 final Tab tab = mActionBar.newTab(); 259 tab.setTabListener(mTabListener); 260 if (mShowTabsAsText) { 261 tab.setText(description); 262 } else { 263 tab.setIcon(icon); 264 tab.setContentDescription(description); 265 } 266 mActionBar.addTab(tab); 267 if (expectedTabIndex != tab.getPosition()) { 268 throw new IllegalStateException("Tabs must be created in the right order"); 269 } 270 } 271 272 private class MyTabListener implements ActionBar.TabListener { 273 /** 274 * If true, it won't call {@link #setCurrentTab} in {@link #onTabSelected}. 275 * This flag is used when we want to programmatically update the current tab without 276 * {@link #onTabSelected} getting called. 277 */ 278 public boolean mIgnoreTabSelected; 279 280 @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { } 281 @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { } 282 283 @Override public void onTabSelected(Tab tab, FragmentTransaction ft) { 284 if (!mIgnoreTabSelected) { 285 setCurrentTab(tab.getPosition()); 286 } 287 } 288 } 289 290 private class MyNavigationListener implements ActionBar.OnNavigationListener { 291 public boolean mIgnoreNavigationItemSelected; 292 293 public boolean onNavigationItemSelected(int itemPosition, long itemId) { 294 if (!mIgnoreNavigationItemSelected) { 295 setCurrentTab(getTabPositionFromNavigationItemPosition(itemPosition)); 296 } 297 return true; 298 } 299 } 300 301 /** 302 * Change the current tab, and notify the listener. 303 */ 304 public void setCurrentTab(int tab) { 305 setCurrentTab(tab, true); 306 } 307 308 /** 309 * Change the current tab 310 */ 311 public void setCurrentTab(int tab, boolean notifyListener) { 312 if (tab == mCurrentTab) { 313 return; 314 } 315 mCurrentTab = tab; 316 317 final int actionBarSelectedNavIndex = mActionBar.getSelectedNavigationIndex(); 318 switch(mActionBar.getNavigationMode()) { 319 case ActionBar.NAVIGATION_MODE_TABS: 320 if (mCurrentTab != actionBarSelectedNavIndex) { 321 mActionBar.setSelectedNavigationItem(mCurrentTab); 322 } 323 break; 324 case ActionBar.NAVIGATION_MODE_LIST: 325 if (mCurrentTab != getTabPositionFromNavigationItemPosition( 326 actionBarSelectedNavIndex)) { 327 mActionBar.setSelectedNavigationItem( 328 getNavigationItemPositionFromTabPosition(mCurrentTab)); 329 } 330 break; 331 } 332 333 if (notifyListener && mListener != null) mListener.onSelectedTabChanged(); 334 saveLastTabPreference(mCurrentTab); 335 } 336 337 public int getCurrentTab() { 338 return mCurrentTab; 339 } 340 341 /** 342 * @return Whether in search mode, i.e. if the search view is visible/expanded. 343 * 344 * Note even if the action bar is in search mode, if the query is empty, the search fragment 345 * will not be in search mode. 346 */ 347 public boolean isSearchMode() { 348 return mSearchMode; 349 } 350 351 public void setSearchMode(boolean flag) { 352 if (mSearchMode != flag) { 353 mSearchMode = flag; 354 update(); 355 if (mSearchView == null) { 356 return; 357 } 358 if (mSearchMode) { 359 setFocusOnSearchView(); 360 } else { 361 mSearchView.setQuery(null, false); 362 } 363 } else if (flag) { 364 // Everything is already set up. Still make sure the keyboard is up 365 if (mSearchView != null) setFocusOnSearchView(); 366 } 367 } 368 369 public String getQueryString() { 370 return mSearchMode ? mQueryString : null; 371 } 372 373 public void setQueryString(String query) { 374 mQueryString = query; 375 if (mSearchView != null) { 376 mSearchView.setQuery(query, false); 377 } 378 } 379 380 /** @return true if the "UP" icon is showing. */ 381 public boolean isUpShowing() { 382 return mSearchMode; // Only shown on the search mode. 383 } 384 385 private void updateDisplayOptions() { 386 // All the flags we may change in this method. 387 final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME 388 | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_CUSTOM; 389 390 // The current flags set to the action bar. (only the ones that we may change here) 391 final int current = mActionBar.getDisplayOptions() & MASK; 392 393 // Build the new flags... 394 int newFlags = 0; 395 newFlags |= ActionBar.DISPLAY_SHOW_TITLE; 396 if (mShowHomeIcon) { 397 newFlags |= ActionBar.DISPLAY_SHOW_HOME; 398 } 399 if (mSearchMode) { 400 newFlags |= ActionBar.DISPLAY_SHOW_HOME; 401 newFlags |= ActionBar.DISPLAY_HOME_AS_UP; 402 newFlags |= ActionBar.DISPLAY_SHOW_CUSTOM; 403 } 404 mActionBar.setHomeButtonEnabled(mSearchMode); 405 406 if (current != newFlags) { 407 // Pass the mask here to preserve other flags that we're not interested here. 408 mActionBar.setDisplayOptions(newFlags, MASK); 409 } 410 } 411 412 private void update() { 413 boolean isIconifiedChanging = mSearchView.isIconified() == mSearchMode; 414 if (mSearchMode) { 415 setFocusOnSearchView(); 416 // Since we have the {@link SearchView} in a custom action bar, we must manually handle 417 // expanding the {@link SearchView} when a search is initiated. Note that a side effect 418 // of this method is that the {@link SearchView} query text is set to empty string. 419 if (isIconifiedChanging) { 420 mSearchView.onActionViewExpanded(); 421 } 422 if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_STANDARD) { 423 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); 424 } 425 if (mListener != null) { 426 mListener.onAction(Action.START_SEARCH_MODE); 427 } 428 } else { 429 final int currentNavigationMode = mActionBar.getNavigationMode(); 430 if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_TABS 431 && currentNavigationMode != ActionBar.NAVIGATION_MODE_TABS) { 432 // setNavigationMode will trigger onTabSelected() with the tab which was previously 433 // selected. 434 // The issue is that when we're first switching to the tab navigation mode after 435 // screen orientation changes, onTabSelected() will get called with the first tab 436 // (i.e. favorite), which would results in mCurrentTab getting set to FAVORITES and 437 // we'd lose restored tab. 438 // So let's just disable the callback here temporarily. We'll notify the listener 439 // after this anyway. 440 mTabListener.mIgnoreTabSelected = true; 441 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); 442 mActionBar.setSelectedNavigationItem(mCurrentTab); 443 mTabListener.mIgnoreTabSelected = false; 444 } else if (mActionBarNavigationMode == ActionBar.NAVIGATION_MODE_LIST 445 && currentNavigationMode != ActionBar.NAVIGATION_MODE_LIST) { 446 mNavigationListener.mIgnoreNavigationItemSelected = true; 447 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); 448 mActionBar.setSelectedNavigationItem( 449 getNavigationItemPositionFromTabPosition(mCurrentTab)); 450 mNavigationListener.mIgnoreNavigationItemSelected = false; 451 } 452 mActionBar.setTitle(null); 453 // Since we have the {@link SearchView} in a custom action bar, we must manually handle 454 // collapsing the {@link SearchView} when search mode is exited. 455 if (isIconifiedChanging) { 456 mSearchView.onActionViewCollapsed(); 457 } 458 if (mListener != null) { 459 mListener.onAction(Action.STOP_SEARCH_MODE); 460 mListener.onSelectedTabChanged(); 461 } 462 } 463 updateDisplayOptions(); 464 } 465 466 @Override 467 public boolean onQueryTextChange(String queryString) { 468 // TODO: Clean up SearchView code because it keeps setting the SearchView query, 469 // invoking onQueryChanged, setting up the fragment again, invalidating the options menu, 470 // storing the SearchView again, and etc... unless we add in the early return statements. 471 if (queryString.equals(mQueryString)) { 472 return false; 473 } 474 mQueryString = queryString; 475 if (!mSearchMode) { 476 if (!TextUtils.isEmpty(queryString)) { 477 setSearchMode(true); 478 } 479 } else if (mListener != null) { 480 mListener.onAction(Action.CHANGE_SEARCH_QUERY); 481 } 482 483 return true; 484 } 485 486 @Override 487 public boolean onQueryTextSubmit(String query) { 488 // When the search is "committed" by the user, then hide the keyboard so the user can 489 // more easily browse the list of results. 490 if (mSearchView != null) { 491 InputMethodManager imm = (InputMethodManager) mContext.getSystemService( 492 Context.INPUT_METHOD_SERVICE); 493 if (imm != null) { 494 imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0); 495 } 496 mSearchView.clearFocus(); 497 } 498 return true; 499 } 500 501 @Override 502 public boolean onClose() { 503 setSearchMode(false); 504 return false; 505 } 506 507 public void onSaveInstanceState(Bundle outState) { 508 outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode); 509 outState.putString(EXTRA_KEY_QUERY, mQueryString); 510 outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab); 511 } 512 513 /** 514 * Clears the focus from the {@link SearchView} if we are in search mode. 515 * This will suppress the IME if it is visible. 516 */ 517 public void clearFocusOnSearchView() { 518 if (isSearchMode()) { 519 if (mSearchView != null) { 520 mSearchView.clearFocus(); 521 } 522 } 523 } 524 525 public void setFocusOnSearchView() { 526 mSearchView.requestFocus(); 527 mSearchView.setIconified(false); // Workaround for the "IME not popping up" issue. 528 } 529 530 private void saveLastTabPreference(int tab) { 531 mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply(); 532 } 533 534 private int loadLastTabPreference() { 535 try { 536 return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT); 537 } catch (IllegalArgumentException e) { 538 // Preference is corrupt? 539 return TabState.DEFAULT; 540 } 541 } 542 } 543