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.animation.ValueAnimator; 20 import android.app.ActionBar; 21 import android.app.Activity; 22 import android.content.Context; 23 import android.content.SharedPreferences; 24 import android.content.res.TypedArray; 25 import android.os.Bundle; 26 import android.preference.PreferenceManager; 27 import android.text.Editable; 28 import android.text.TextUtils; 29 import android.text.TextWatcher; 30 import android.view.Gravity; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.inputmethod.InputMethodManager; 35 import android.widget.FrameLayout; 36 import android.widget.LinearLayout.LayoutParams; 37 import android.widget.SearchView.OnCloseListener; 38 import android.view.View.OnClickListener; 39 import android.widget.EditText; 40 import android.widget.TextView; 41 import android.widget.Toolbar; 42 43 import com.android.contacts.R; 44 import com.android.contacts.activities.ActionBarAdapter.Listener.Action; 45 import com.android.contacts.list.ContactsRequest; 46 47 /** 48 * Adapter for the action bar at the top of the Contacts activity. 49 */ 50 public class ActionBarAdapter implements OnCloseListener { 51 52 public interface Listener { 53 public abstract class Action { 54 public static final int CHANGE_SEARCH_QUERY = 0; 55 public static final int START_SEARCH_MODE = 1; 56 public static final int START_SELECTION_MODE = 2; 57 public static final int STOP_SEARCH_AND_SELECTION_MODE = 3; 58 public static final int BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE = 4; 59 } 60 onAction(int action)61 void onAction(int action); 62 63 /** 64 * Called when the user selects a tab. The new tab can be obtained using 65 * {@link #getCurrentTab}. 66 */ onSelectedTabChanged()67 void onSelectedTabChanged(); 68 onUpButtonPressed()69 void onUpButtonPressed(); 70 } 71 72 private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode"; 73 private static final String EXTRA_KEY_QUERY = "navBar.query"; 74 private static final String EXTRA_KEY_SELECTED_TAB = "navBar.selectedTab"; 75 private static final String EXTRA_KEY_SELECTED_MODE = "navBar.selectionMode"; 76 77 private static final String PERSISTENT_LAST_TAB = "actionBarAdapter.lastTab"; 78 79 private boolean mSelectionMode; 80 private boolean mSearchMode; 81 private String mQueryString; 82 83 private EditText mSearchView; 84 private View mClearSearchView; 85 /** The view that represents tabs when we are in portrait mode **/ 86 private View mPortraitTabs; 87 /** The view that represents tabs when we are in landscape mode **/ 88 private View mLandscapeTabs; 89 private View mSearchContainer; 90 private View mSelectionContainer; 91 92 private int mMaxPortraitTabHeight; 93 private int mMaxToolbarContentInsetStart; 94 95 private final Activity mActivity; 96 private final SharedPreferences mPrefs; 97 98 private Listener mListener; 99 100 private final ActionBar mActionBar; 101 private final Toolbar mToolbar; 102 /** 103 * Frame that contains the toolbar and draws the toolbar's background color. This is useful 104 * for placing things behind the toolbar. 105 */ 106 private final FrameLayout mToolBarFrame; 107 108 private boolean mShowHomeIcon; 109 110 public interface TabState { 111 public static int FAVORITES = 0; 112 public static int ALL = 1; 113 114 public static int COUNT = 2; 115 public static int DEFAULT = ALL; 116 } 117 118 private int mCurrentTab = TabState.DEFAULT; 119 ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, View portraitTabs, View landscapeTabs, Toolbar toolbar)120 public ActionBarAdapter(Activity activity, Listener listener, ActionBar actionBar, 121 View portraitTabs, View landscapeTabs, Toolbar toolbar) { 122 mActivity = activity; 123 mListener = listener; 124 mActionBar = actionBar; 125 mPrefs = PreferenceManager.getDefaultSharedPreferences(mActivity); 126 mPortraitTabs = portraitTabs; 127 mLandscapeTabs = landscapeTabs; 128 mToolbar = toolbar; 129 mToolBarFrame = (FrameLayout) mToolbar.getParent(); 130 mMaxToolbarContentInsetStart = mToolbar.getContentInsetStart(); 131 mShowHomeIcon = mActivity.getResources().getBoolean(R.bool.show_home_icon); 132 133 setupSearchAndSelectionViews(); 134 setupTabs(mActivity); 135 } 136 setupTabs(Context context)137 private void setupTabs(Context context) { 138 final TypedArray attributeArray = context.obtainStyledAttributes( 139 new int[]{android.R.attr.actionBarSize}); 140 mMaxPortraitTabHeight = attributeArray.getDimensionPixelSize(0, 0); 141 // Hide tabs initially 142 setPortraitTabHeight(0); 143 } 144 setupSearchAndSelectionViews()145 private void setupSearchAndSelectionViews() { 146 final LayoutInflater inflater = (LayoutInflater) mToolbar.getContext().getSystemService( 147 Context.LAYOUT_INFLATER_SERVICE); 148 149 // Setup search bar 150 mSearchContainer = inflater.inflate(R.layout.search_bar_expanded, mToolbar, 151 /* attachToRoot = */ false); 152 mSearchContainer.setVisibility(View.VISIBLE); 153 mToolbar.addView(mSearchContainer); 154 mSearchContainer.setBackgroundColor(mActivity.getResources().getColor( 155 R.color.searchbox_background_color)); 156 mSearchView = (EditText) mSearchContainer.findViewById(R.id.search_view); 157 mSearchView.setHint(mActivity.getString(R.string.hint_findContacts)); 158 mSearchView.addTextChangedListener(new SearchTextWatcher()); 159 mSearchContainer.findViewById(R.id.search_back_button).setOnClickListener( 160 new OnClickListener() { 161 @Override 162 public void onClick(View v) { 163 if (mListener != null) { 164 mListener.onUpButtonPressed(); 165 } 166 } 167 }); 168 169 mClearSearchView = mSearchContainer.findViewById(R.id.search_close_button); 170 mClearSearchView.setOnClickListener( 171 new OnClickListener() { 172 @Override 173 public void onClick(View v) { 174 setQueryString(null); 175 } 176 }); 177 178 // Setup selection bar 179 mSelectionContainer = inflater.inflate(R.layout.selection_bar, mToolbar, 180 /* attachToRoot = */ false); 181 // Insert the selection container into mToolBarFrame behind the Toolbar, so that 182 // the Toolbar's MenuItems can appear on top of the selection container. 183 mToolBarFrame.addView(mSelectionContainer, 0); 184 mSelectionContainer.findViewById(R.id.selection_close).setOnClickListener( 185 new OnClickListener() { 186 @Override 187 public void onClick(View v) { 188 if (mListener != null) { 189 mListener.onUpButtonPressed(); 190 } 191 } 192 }); 193 } 194 initialize(Bundle savedState, ContactsRequest request)195 public void initialize(Bundle savedState, ContactsRequest request) { 196 if (savedState == null) { 197 mSearchMode = request.isSearchMode(); 198 mQueryString = request.getQueryString(); 199 mCurrentTab = loadLastTabPreference(); 200 mSelectionMode = false; 201 } else { 202 mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE); 203 mSelectionMode = savedState.getBoolean(EXTRA_KEY_SELECTED_MODE); 204 mQueryString = savedState.getString(EXTRA_KEY_QUERY); 205 206 // Just set to the field here. The listener will be notified by update(). 207 mCurrentTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB); 208 } 209 if (mCurrentTab >= TabState.COUNT || mCurrentTab < 0) { 210 // Invalid tab index was saved (b/12938207). Restore the default. 211 mCurrentTab = TabState.DEFAULT; 212 } 213 // Show tabs or the expanded {@link SearchView}, depending on whether or not we are in 214 // search mode. 215 update(true /* skipAnimation */); 216 // Expanding the {@link SearchView} clears the query, so set the query from the 217 // {@link ContactsRequest} after it has been expanded, if applicable. 218 if (mSearchMode && !TextUtils.isEmpty(mQueryString)) { 219 setQueryString(mQueryString); 220 } 221 } 222 setListener(Listener listener)223 public void setListener(Listener listener) { 224 mListener = listener; 225 } 226 227 private class SearchTextWatcher implements TextWatcher { 228 229 @Override onTextChanged(CharSequence queryString, int start, int before, int count)230 public void onTextChanged(CharSequence queryString, int start, int before, int count) { 231 if (queryString.equals(mQueryString)) { 232 return; 233 } 234 mQueryString = queryString.toString(); 235 if (!mSearchMode) { 236 if (!TextUtils.isEmpty(queryString)) { 237 setSearchMode(true); 238 } 239 } else if (mListener != null) { 240 mListener.onAction(Action.CHANGE_SEARCH_QUERY); 241 } 242 mClearSearchView.setVisibility( 243 TextUtils.isEmpty(queryString) ? View.GONE : View.VISIBLE); 244 } 245 246 @Override afterTextChanged(Editable s)247 public void afterTextChanged(Editable s) {} 248 249 @Override beforeTextChanged(CharSequence s, int start, int count, int after)250 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 251 } 252 253 /** 254 * Save the current tab selection, and notify the listener. 255 */ setCurrentTab(int tab)256 public void setCurrentTab(int tab) { 257 setCurrentTab(tab, true); 258 } 259 260 /** 261 * Save the current tab selection. 262 */ setCurrentTab(int tab, boolean notifyListener)263 public void setCurrentTab(int tab, boolean notifyListener) { 264 if (tab == mCurrentTab) { 265 return; 266 } 267 mCurrentTab = tab; 268 269 if (notifyListener && mListener != null) mListener.onSelectedTabChanged(); 270 saveLastTabPreference(mCurrentTab); 271 } 272 getCurrentTab()273 public int getCurrentTab() { 274 return mCurrentTab; 275 } 276 277 /** 278 * @return Whether in search mode, i.e. if the search view is visible/expanded. 279 * 280 * Note even if the action bar is in search mode, if the query is empty, the search fragment 281 * will not be in search mode. 282 */ isSearchMode()283 public boolean isSearchMode() { 284 return mSearchMode; 285 } 286 287 /** 288 * @return Whether in selection mode, i.e. if the selection view is visible/expanded. 289 */ isSelectionMode()290 public boolean isSelectionMode() { 291 return mSelectionMode; 292 } 293 setSearchMode(boolean flag)294 public void setSearchMode(boolean flag) { 295 if (mSearchMode != flag) { 296 mSearchMode = flag; 297 update(false /* skipAnimation */); 298 if (mSearchView == null) { 299 return; 300 } 301 if (mSearchMode) { 302 mSearchView.setEnabled(true); 303 setFocusOnSearchView(); 304 } else { 305 // Disable search view, so that it doesn't keep the IME visible. 306 mSearchView.setEnabled(false); 307 } 308 setQueryString(null); 309 } else if (flag) { 310 // Everything is already set up. Still make sure the keyboard is up 311 if (mSearchView != null) setFocusOnSearchView(); 312 } 313 } 314 setSelectionMode(boolean flag)315 public void setSelectionMode(boolean flag) { 316 if (mSelectionMode != flag) { 317 mSelectionMode = flag; 318 update(false /* skipAnimation */); 319 } 320 } 321 getQueryString()322 public String getQueryString() { 323 return mSearchMode ? mQueryString : null; 324 } 325 setQueryString(String query)326 public void setQueryString(String query) { 327 mQueryString = query; 328 if (mSearchView != null) { 329 mSearchView.setText(query); 330 // When programmatically entering text into the search view, the most reasonable 331 // place for the cursor is after all the text. 332 mSearchView.setSelection(mSearchView.getText() == null ? 333 0 : mSearchView.getText().length()); 334 } 335 } 336 337 /** @return true if the "UP" icon is showing. */ isUpShowing()338 public boolean isUpShowing() { 339 return mSearchMode; // Only shown on the search mode. 340 } 341 updateDisplayOptionsInner()342 private void updateDisplayOptionsInner() { 343 // All the flags we may change in this method. 344 final int MASK = ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_HOME 345 | ActionBar.DISPLAY_HOME_AS_UP; 346 347 // The current flags set to the action bar. (only the ones that we may change here) 348 final int current = mActionBar.getDisplayOptions() & MASK; 349 350 final boolean isSearchOrSelectionMode = mSearchMode || mSelectionMode; 351 352 // Build the new flags... 353 int newFlags = 0; 354 if (mShowHomeIcon && !isSearchOrSelectionMode) { 355 newFlags |= ActionBar.DISPLAY_SHOW_HOME; 356 } 357 if (mSearchMode && !mSelectionMode) { 358 // The search container is placed inside the toolbar. So we need to disable the 359 // Toolbar's content inset in order to allow the search container to be the width of 360 // the window. 361 mToolbar.setContentInsetsRelative(0, mToolbar.getContentInsetEnd()); 362 } 363 if (!isSearchOrSelectionMode) { 364 newFlags |= ActionBar.DISPLAY_SHOW_TITLE; 365 mToolbar.setContentInsetsRelative(mMaxToolbarContentInsetStart, 366 mToolbar.getContentInsetEnd()); 367 } 368 369 if (mSelectionMode) { 370 // Minimize the horizontal width of the Toolbar since the selection container is placed 371 // behind the toolbar and its left hand side needs to be clickable. 372 FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams(); 373 params.width = LayoutParams.WRAP_CONTENT; 374 params.gravity = Gravity.END; 375 mToolbar.setLayoutParams(params); 376 } else { 377 FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mToolbar.getLayoutParams(); 378 params.width = LayoutParams.MATCH_PARENT; 379 params.gravity = Gravity.END; 380 mToolbar.setLayoutParams(params); 381 } 382 383 if (current != newFlags) { 384 // Pass the mask here to preserve other flags that we're not interested here. 385 mActionBar.setDisplayOptions(newFlags, MASK); 386 } 387 } 388 update(boolean skipAnimation)389 private void update(boolean skipAnimation) { 390 updateStatusBarColor(); 391 392 final boolean isSelectionModeChanging 393 = (mSelectionContainer.getParent() == null) == mSelectionMode; 394 final boolean isSwitchingFromSearchToSelection = 395 mSearchMode && isSelectionModeChanging || mSearchMode && mSelectionMode; 396 final boolean isSearchModeChanging 397 = (mSearchContainer.getParent() == null) == mSearchMode; 398 final boolean isTabHeightChanging = isSearchModeChanging || isSelectionModeChanging; 399 400 // When skipAnimation=true, it is possible that we will switch from search mode 401 // to selection mode directly. So we need to remove the undesired container in addition 402 // to adding the desired container. 403 if (skipAnimation || isSwitchingFromSearchToSelection) { 404 if (isTabHeightChanging || isSwitchingFromSearchToSelection) { 405 mToolbar.removeView(mLandscapeTabs); 406 mToolbar.removeView(mSearchContainer); 407 mToolBarFrame.removeView(mSelectionContainer); 408 if (mSelectionMode) { 409 setPortraitTabHeight(0); 410 addSelectionContainer(); 411 } else if (mSearchMode) { 412 setPortraitTabHeight(0); 413 addSearchContainer(); 414 } else { 415 setPortraitTabHeight(mMaxPortraitTabHeight); 416 addLandscapeViewPagerTabs(); 417 } 418 updateDisplayOptions(isSearchModeChanging); 419 } 420 return; 421 } 422 423 // Handle a switch to/from selection mode, due to UI interaction. 424 if (isSelectionModeChanging) { 425 mToolbar.removeView(mLandscapeTabs); 426 if (mSelectionMode) { 427 addSelectionContainer(); 428 mSelectionContainer.setAlpha(0); 429 mSelectionContainer.animate().alpha(1); 430 animateTabHeightChange(mMaxPortraitTabHeight, 0); 431 updateDisplayOptions(isSearchModeChanging); 432 } else { 433 if (mListener != null) { 434 mListener.onAction(Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE); 435 } 436 mSelectionContainer.setAlpha(1); 437 animateTabHeightChange(0, mMaxPortraitTabHeight); 438 mSelectionContainer.animate().alpha(0).withEndAction(new Runnable() { 439 @Override 440 public void run() { 441 updateDisplayOptions(isSearchModeChanging); 442 addLandscapeViewPagerTabs(); 443 mToolBarFrame.removeView(mSelectionContainer); 444 } 445 }); 446 } 447 } 448 449 // Handle a switch to/from search mode, due to UI interaction. 450 if (isSearchModeChanging) { 451 mToolbar.removeView(mLandscapeTabs); 452 if (mSearchMode) { 453 addSearchContainer(); 454 mSearchContainer.setAlpha(0); 455 mSearchContainer.animate().alpha(1); 456 animateTabHeightChange(mMaxPortraitTabHeight, 0); 457 updateDisplayOptions(isSearchModeChanging); 458 } else { 459 mSearchContainer.setAlpha(1); 460 animateTabHeightChange(0, mMaxPortraitTabHeight); 461 mSearchContainer.animate().alpha(0).withEndAction(new Runnable() { 462 @Override 463 public void run() { 464 updateDisplayOptions(isSearchModeChanging); 465 addLandscapeViewPagerTabs(); 466 mToolbar.removeView(mSearchContainer); 467 } 468 }); 469 } 470 } 471 } 472 setSelectionCount(int selectionCount)473 public void setSelectionCount(int selectionCount) { 474 TextView textView = (TextView) mSelectionContainer.findViewById(R.id.selection_count_text); 475 if (selectionCount == 0) { 476 textView.setVisibility(View.GONE); 477 } else { 478 textView.setVisibility(View.VISIBLE); 479 } 480 textView.setText(String.valueOf(selectionCount)); 481 } 482 updateStatusBarColor()483 private void updateStatusBarColor() { 484 if (mSelectionMode) { 485 int cabStatusBarColor = mActivity.getResources().getColor( 486 R.color.contextual_selection_bar_status_bar_color); 487 mActivity.getWindow().setStatusBarColor(cabStatusBarColor); 488 } else { 489 int normalStatusBarColor = mActivity.getColor(R.color.primary_color_dark); 490 mActivity.getWindow().setStatusBarColor(normalStatusBarColor); 491 } 492 } 493 addLandscapeViewPagerTabs()494 private void addLandscapeViewPagerTabs() { 495 if (mLandscapeTabs != null) { 496 mToolbar.removeView(mLandscapeTabs); 497 mToolbar.addView(mLandscapeTabs); 498 } 499 } 500 addSearchContainer()501 private void addSearchContainer() { 502 mToolbar.removeView(mSearchContainer); 503 mToolbar.addView(mSearchContainer); 504 mSearchContainer.setAlpha(1); 505 } 506 addSelectionContainer()507 private void addSelectionContainer() { 508 mToolBarFrame.removeView(mSelectionContainer); 509 mToolBarFrame.addView(mSelectionContainer, 0); 510 mSelectionContainer.setAlpha(1); 511 } 512 updateDisplayOptions(boolean isSearchModeChanging)513 private void updateDisplayOptions(boolean isSearchModeChanging) { 514 if (mSearchMode && !mSelectionMode) { 515 setFocusOnSearchView(); 516 // Since we have the {@link SearchView} in a custom action bar, we must manually handle 517 // expanding the {@link SearchView} when a search is initiated. Note that a side effect 518 // of this method is that the {@link SearchView} query text is set to empty string. 519 if (isSearchModeChanging) { 520 final CharSequence queryText = mSearchView.getText(); 521 if (!TextUtils.isEmpty(queryText)) { 522 mSearchView.setText(queryText); 523 } 524 } 525 } 526 if (mListener != null) { 527 if (mSearchMode) { 528 mListener.onAction(Action.START_SEARCH_MODE); 529 } 530 if (mSelectionMode) { 531 mListener.onAction(Action.START_SELECTION_MODE); 532 } 533 if (!mSearchMode && !mSelectionMode) { 534 mListener.onAction(Action.STOP_SEARCH_AND_SELECTION_MODE); 535 mListener.onSelectedTabChanged(); 536 } 537 } 538 updateDisplayOptionsInner(); 539 } 540 541 @Override onClose()542 public boolean onClose() { 543 setSearchMode(false); 544 return false; 545 } 546 onSaveInstanceState(Bundle outState)547 public void onSaveInstanceState(Bundle outState) { 548 outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode); 549 outState.putBoolean(EXTRA_KEY_SELECTED_MODE, mSelectionMode); 550 outState.putString(EXTRA_KEY_QUERY, mQueryString); 551 outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab); 552 } 553 setFocusOnSearchView()554 public void setFocusOnSearchView() { 555 mSearchView.requestFocus(); 556 showInputMethod(mSearchView); // Workaround for the "IME not popping up" issue. 557 } 558 showInputMethod(View view)559 private void showInputMethod(View view) { 560 final InputMethodManager imm = (InputMethodManager) mActivity.getSystemService( 561 Context.INPUT_METHOD_SERVICE); 562 if (imm != null) { 563 imm.showSoftInput(view, 0); 564 } 565 } 566 saveLastTabPreference(int tab)567 private void saveLastTabPreference(int tab) { 568 mPrefs.edit().putInt(PERSISTENT_LAST_TAB, tab).apply(); 569 } 570 loadLastTabPreference()571 private int loadLastTabPreference() { 572 try { 573 return mPrefs.getInt(PERSISTENT_LAST_TAB, TabState.DEFAULT); 574 } catch (IllegalArgumentException e) { 575 // Preference is corrupt? 576 return TabState.DEFAULT; 577 } 578 } 579 animateTabHeightChange(int start, int end)580 private void animateTabHeightChange(int start, int end) { 581 if (mPortraitTabs == null) { 582 return; 583 } 584 final ValueAnimator animator = ValueAnimator.ofInt(start, end); 585 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 586 @Override 587 public void onAnimationUpdate(ValueAnimator valueAnimator) { 588 int value = (Integer) valueAnimator.getAnimatedValue(); 589 setPortraitTabHeight(value); 590 } 591 }); 592 animator.setDuration(100).start(); 593 } 594 setPortraitTabHeight(int height)595 private void setPortraitTabHeight(int height) { 596 if (mPortraitTabs == null) { 597 return; 598 } 599 ViewGroup.LayoutParams layoutParams = mPortraitTabs.getLayoutParams(); 600 layoutParams.height = height; 601 mPortraitTabs.setLayoutParams(layoutParams); 602 } 603 } 604