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