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