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