NotificationMenuRow.java revision 97d52db8541de930871322847a36467c7a293e3b
1/* 2 * Copyright (C) 2016 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 17package com.android.systemui.statusbar; 18 19import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION; 20 21import java.util.ArrayList; 22 23import com.android.systemui.Interpolators; 24import com.android.systemui.R; 25import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 26import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; 27import com.android.systemui.statusbar.NotificationGuts.GutsContent; 28import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 29 30import android.animation.Animator; 31import android.animation.AnimatorListenerAdapter; 32import android.animation.ValueAnimator; 33import android.app.Notification; 34import android.content.Context; 35import android.content.res.Resources; 36import android.graphics.drawable.Drawable; 37import android.os.Handler; 38import android.os.Looper; 39import android.util.Log; 40import android.service.notification.StatusBarNotification; 41import android.view.LayoutInflater; 42import android.view.MotionEvent; 43import android.view.View; 44import android.view.ViewGroup; 45import android.widget.FrameLayout; 46import android.widget.FrameLayout.LayoutParams; 47 48public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener, 49 ExpandableNotificationRow.LayoutListener { 50 51 private static final boolean DEBUG = false; 52 private static final String TAG = "swipe"; 53 54 private static final int ICON_ALPHA_ANIM_DURATION = 200; 55 private static final long SHOW_MENU_DELAY = 60; 56 private static final long SWIPE_MENU_TIMING = 200; 57 58 // Notification must be swiped at least this fraction of a single menu item to show menu 59 private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f; 60 private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f; 61 62 // When the menu is displayed, the notification must be swiped within this fraction of a single 63 // menu item to snap back to menu (else it will cover the menu or it'll be dismissed) 64 private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f; 65 66 private ExpandableNotificationRow mParent; 67 68 private Context mContext; 69 private FrameLayout mMenuContainer; 70 private MenuItem mSnoozeItem; 71 private MenuItem mInfoItem; 72 private ArrayList<MenuItem> mMenuItems; 73 private OnMenuEventListener mMenuListener; 74 75 private ValueAnimator mFadeAnimator; 76 private boolean mAnimating; 77 private boolean mMenuFadedIn; 78 79 private boolean mOnLeft; 80 private boolean mIconsPlaced; 81 82 private boolean mDismissing; 83 private boolean mSnapping; 84 private float mTranslation; 85 86 private int[] mIconLocation = new int[2]; 87 private int[] mParentLocation = new int[2]; 88 89 private float mHorizSpaceForIcon; 90 private int mVertSpaceForIcons; 91 private int mIconPadding; 92 93 private float mAlpha = 0f; 94 private float mPrevX; 95 96 private CheckForDrag mCheckForDrag; 97 private Handler mHandler; 98 99 private boolean mMenuSnappedTo; 100 private boolean mMenuSnappedOnLeft; 101 private boolean mShouldShowMenu; 102 103 private NotificationSwipeActionHelper mSwipeHelper; 104 105 public NotificationMenuRow(Context context) { 106 mContext = context; 107 final Resources res = context.getResources(); 108 mShouldShowMenu = res.getBoolean(R.bool.config_showNotificationGear); 109 mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size); 110 mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height); 111 mIconPadding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 112 mHandler = new Handler(Looper.getMainLooper()); 113 mMenuItems = new ArrayList<>(); 114 mSnoozeItem = createSnoozeItem(context); 115 mInfoItem = createInfoItem(context); 116 mMenuItems.add(mSnoozeItem); 117 mMenuItems.add(mInfoItem); 118 } 119 120 @Override 121 public ArrayList<MenuItem> getMenuItems(Context context) { 122 return mMenuItems; 123 } 124 125 @Override 126 public MenuItem getLongpressMenuItem(Context context) { 127 return mInfoItem; 128 } 129 130 @Override 131 public void setSwipeActionHelper(NotificationSwipeActionHelper helper) { 132 mSwipeHelper = helper; 133 } 134 135 @Override 136 public void setMenuClickListener(OnMenuEventListener listener) { 137 mMenuListener = listener; 138 } 139 140 @Override 141 public void createMenu(ViewGroup parent) { 142 mParent = (ExpandableNotificationRow) parent; 143 createMenuViews(); 144 } 145 146 @Override 147 public boolean isMenuVisible() { 148 return mAlpha > 0; 149 } 150 151 @Override 152 public View getMenuView() { 153 return mMenuContainer; 154 } 155 156 @Override 157 public void resetMenu() { 158 resetState(true); 159 } 160 161 @Override 162 public void onNotificationUpdated() { 163 if (mMenuContainer == null) { 164 // Menu hasn't been created yet, no need to do anything. 165 return; 166 } 167 createMenuViews(); 168 } 169 170 @Override 171 public void onConfigurationChanged() { 172 mParent.setLayoutListener(this); 173 } 174 175 @Override 176 public void onLayout() { 177 mIconsPlaced = false; // Force icons to be re-placed 178 setMenuLocation(); 179 mParent.removeListener(); 180 } 181 182 private void createMenuViews() { 183 // Filter the menu items based on the notification 184 if (mParent != null && mParent.getStatusBarNotification() != null) { 185 int flags = mParent.getStatusBarNotification().getNotification().flags; 186 boolean isForeground = (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; 187 if (isForeground) { 188 // Don't show snooze for foreground services 189 mMenuItems.remove(mSnoozeItem); 190 } else if (!mMenuItems.contains(mSnoozeItem)) { 191 // Was a foreground service but is no longer, add snooze back 192 mMenuItems.add(mSnoozeItem); 193 } 194 } 195 // Recreate the menu 196 if (mMenuContainer != null) { 197 mMenuContainer.removeAllViews(); 198 } else { 199 mMenuContainer = new FrameLayout(mContext); 200 } 201 for (int i = 0; i < mMenuItems.size(); i++) { 202 addMenuView(mMenuItems.get(i), mMenuContainer); 203 } 204 resetState(false /* notify */); 205 } 206 207 private void resetState(boolean notify) { 208 setMenuAlpha(0f); 209 mIconsPlaced = false; 210 mMenuFadedIn = false; 211 mAnimating = false; 212 mSnapping = false; 213 mDismissing = false; 214 mMenuSnappedTo = false; 215 setMenuLocation(); 216 if (mMenuListener != null && notify) { 217 mMenuListener.onMenuReset(mParent); 218 } 219 } 220 221 @Override 222 public boolean onTouchEvent(View view, MotionEvent ev, float velocity) { 223 final int action = ev.getActionMasked(); 224 switch (action) { 225 case MotionEvent.ACTION_DOWN: 226 mSnapping = false; 227 if (mFadeAnimator != null) { 228 mFadeAnimator.cancel(); 229 } 230 mHandler.removeCallbacks(mCheckForDrag); 231 mCheckForDrag = null; 232 mPrevX = ev.getRawX(); 233 break; 234 235 case MotionEvent.ACTION_MOVE: 236 mSnapping = false; 237 float diffX = ev.getRawX() - mPrevX; 238 mPrevX = ev.getRawX(); 239 if (!isTowardsMenu(diffX) && isMenuLocationChange()) { 240 // Don't consider it "snapped" if location has changed. 241 mMenuSnappedTo = false; 242 243 // Changed directions, make sure we check to fade in icon again. 244 if (!mHandler.hasCallbacks(mCheckForDrag)) { 245 // No check scheduled, set null to schedule a new one. 246 mCheckForDrag = null; 247 } else { 248 // Check scheduled, reset alpha and update location; check will fade it in 249 setMenuAlpha(0f); 250 setMenuLocation(); 251 } 252 } 253 if (mShouldShowMenu 254 && !NotificationStackScrollLayout.isPinnedHeadsUp(view) 255 && !mParent.areGutsExposed() 256 && !mParent.isDark() 257 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) { 258 // Only show the menu if we're not a heads up view and guts aren't exposed. 259 mCheckForDrag = new CheckForDrag(); 260 mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY); 261 } 262 break; 263 264 case MotionEvent.ACTION_UP: 265 return handleUpEvent(ev, view, velocity); 266 } 267 return false; 268 } 269 270 private boolean handleUpEvent(MotionEvent ev, View animView, float velocity) { 271 // If the menu should not be shown, then there is no need to check if the a swipe 272 // should result in a snapping to the menu. As a result, just check if the swipe 273 // was enough to dismiss the notification. 274 if (!mShouldShowMenu) { 275 if (mSwipeHelper.isDismissGesture(ev)) { 276 dismiss(animView, velocity); 277 } else { 278 snapBack(animView, velocity); 279 } 280 return true; 281 } 282 283 final boolean gestureTowardsMenu = isTowardsMenu(velocity); 284 final boolean gestureFastEnough = 285 mSwipeHelper.getMinDismissVelocity() <= Math.abs(velocity); 286 final boolean gestureFarEnough = 287 mSwipeHelper.swipedFarEnough(mTranslation, mParent.getWidth()); 288 final double timeForGesture = ev.getEventTime() - ev.getDownTime(); 289 final boolean showMenuForSlowOnGoing = !mParent.canViewBeDismissed() 290 && timeForGesture >= SWIPE_MENU_TIMING; 291 final float menuSnapTarget = mOnLeft ? getSpaceForMenu() : -getSpaceForMenu(); 292 293 if (DEBUG) { 294 Log.d(TAG, "mTranslation= " + mTranslation 295 + " mAlpha= " + mAlpha 296 + " velocity= " + velocity 297 + " mMenuSnappedTo= " + mMenuSnappedTo 298 + " mMenuSnappedOnLeft= " + mMenuSnappedOnLeft 299 + " mOnLeft= " + mOnLeft 300 + " minDismissVel= " + mSwipeHelper.getMinDismissVelocity() 301 + " isDismissGesture= " + mSwipeHelper.isDismissGesture(ev) 302 + " gestureTowardsMenu= " + gestureTowardsMenu 303 + " gestureFastEnough= " + gestureFastEnough 304 + " gestureFarEnough= " + gestureFarEnough); 305 } 306 307 if (mMenuSnappedTo && isMenuVisible() && mMenuSnappedOnLeft == mOnLeft) { 308 // Menu was snapped to previously and we're on the same side, figure out if 309 // we should stick to the menu, snap back into place, or dismiss 310 final float maximumSwipeDistance = mHorizSpaceForIcon 311 * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION; 312 final float targetLeft = getSpaceForMenu() - maximumSwipeDistance; 313 final float targetRight = mParent.getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION; 314 boolean withinSnapMenuThreshold = mOnLeft 315 ? mTranslation > targetLeft && mTranslation < targetRight 316 : mTranslation < -targetLeft && mTranslation > -targetRight; 317 boolean shouldSnapTo = mOnLeft ? mTranslation < targetLeft : mTranslation > -targetLeft; 318 if (DEBUG) { 319 Log.d(TAG, " withinSnapMenuThreshold= " + withinSnapMenuThreshold 320 + " shouldSnapTo= " + shouldSnapTo 321 + " targetLeft= " + targetLeft 322 + " targetRight= " + targetRight); 323 } 324 if (withinSnapMenuThreshold && !mSwipeHelper.isDismissGesture(ev)) { 325 // Haven't moved enough to unsnap from the menu 326 showMenu(animView, menuSnapTarget, velocity); 327 } else if (mSwipeHelper.isDismissGesture(ev) && !shouldSnapTo) { 328 // Only dismiss if we're not moving towards the menu 329 dismiss(animView, velocity); 330 } else { 331 snapBack(animView, velocity); 332 } 333 } else if ((swipedEnoughToShowMenu() && (!gestureFastEnough || showMenuForSlowOnGoing)) 334 || (gestureTowardsMenu && !mSwipeHelper.isDismissGesture(ev))) { 335 // Menu has not been snapped to previously and this is menu revealing gesture 336 showMenu(animView, menuSnapTarget, velocity); 337 } else if (mSwipeHelper.isDismissGesture(ev) && !gestureTowardsMenu) { 338 dismiss(animView, velocity); 339 } else { 340 snapBack(animView, velocity); 341 } 342 return true; 343 } 344 345 private void showMenu(View animView, float targetLeft, float velocity) { 346 mMenuSnappedTo = true; 347 mMenuSnappedOnLeft = mOnLeft; 348 mMenuListener.onMenuShown(animView); 349 mSwipeHelper.snap(animView, targetLeft, velocity); 350 } 351 352 private void snapBack(View animView, float velocity) { 353 if (mFadeAnimator != null) { 354 mFadeAnimator.cancel(); 355 } 356 mHandler.removeCallbacks(mCheckForDrag); 357 mMenuSnappedTo = false; 358 mSnapping = true; 359 mSwipeHelper.snap(animView, 0 /* leftTarget */, velocity); 360 } 361 362 private void dismiss(View animView, float velocity) { 363 if (mFadeAnimator != null) { 364 mFadeAnimator.cancel(); 365 } 366 mHandler.removeCallbacks(mCheckForDrag); 367 mMenuSnappedTo = false; 368 mDismissing = true; 369 mSwipeHelper.dismiss(animView, velocity); 370 } 371 372 /** 373 * @return whether the notification has been translated enough to show the menu and not enough 374 * to be dismissed. 375 */ 376 private boolean swipedEnoughToShowMenu() { 377 final float multiplier = mParent.canViewBeDismissed() 378 ? SWIPED_FAR_ENOUGH_MENU_FRACTION 379 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION; 380 final float minimumSwipeDistance = mHorizSpaceForIcon * multiplier; 381 return !mSwipeHelper.swipedFarEnough(0, 0) && isMenuVisible() 382 && (mOnLeft ? mTranslation > minimumSwipeDistance 383 : mTranslation < -minimumSwipeDistance); 384 } 385 386 /** 387 * Returns whether the gesture is towards the menu location or not. 388 */ 389 private boolean isTowardsMenu(float movement) { 390 return isMenuVisible() 391 && ((mOnLeft && movement <= 0) 392 || (!mOnLeft && movement >= 0)); 393 } 394 395 @Override 396 public void setAppName(String appName) { 397 if (appName == null) { 398 return; 399 } 400 Resources res = mContext.getResources(); 401 final int count = mMenuItems.size(); 402 for (int i = 0; i < count; i++) { 403 MenuItem item = mMenuItems.get(i); 404 String description = String.format( 405 res.getString(R.string.notification_menu_accessibility), 406 appName, item.getContentDescription()); 407 View menuView = item.getMenuView(); 408 if (menuView != null) { 409 menuView.setContentDescription(description); 410 } 411 } 412 } 413 414 @Override 415 public void onHeightUpdate() { 416 if (mParent == null || mMenuItems.size() == 0) { 417 return; 418 } 419 int parentHeight = mParent.getCollapsedHeight(); 420 float translationY; 421 if (parentHeight < mVertSpaceForIcons) { 422 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); 423 } else { 424 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; 425 } 426 mMenuContainer.setTranslationY(translationY); 427 } 428 429 @Override 430 public void onTranslationUpdate(float translation) { 431 mTranslation = translation; 432 if (mAnimating || !mMenuFadedIn) { 433 // Don't adjust when animating, or if the menu hasn't been shown yet. 434 return; 435 } 436 final float fadeThreshold = mParent.getWidth() * 0.3f; 437 final float absTrans = Math.abs(translation); 438 float desiredAlpha = 0; 439 if (absTrans == 0) { 440 desiredAlpha = 0; 441 } else if (absTrans <= fadeThreshold) { 442 desiredAlpha = 1; 443 } else { 444 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold)); 445 } 446 setMenuAlpha(desiredAlpha); 447 } 448 449 @Override 450 public void onClick(View v) { 451 if (mMenuListener == null) { 452 // Nothing to do 453 return; 454 } 455 v.getLocationOnScreen(mIconLocation); 456 mParent.getLocationOnScreen(mParentLocation); 457 final int centerX = (int) (mHorizSpaceForIcon / 2); 458 final int centerY = v.getHeight() / 2; 459 final int x = mIconLocation[0] - mParentLocation[0] + centerX; 460 final int y = mIconLocation[1] - mParentLocation[1] + centerY; 461 final int index = mMenuContainer.indexOfChild(v); 462 mMenuListener.onMenuClicked(mParent, x, y, mMenuItems.get(index)); 463 } 464 465 private boolean isMenuLocationChange() { 466 boolean onLeft = mTranslation > mIconPadding; 467 boolean onRight = mTranslation < -mIconPadding; 468 if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) { 469 return true; 470 } 471 return false; 472 } 473 474 private void setMenuLocation() { 475 boolean showOnLeft = mTranslation > 0; 476 if ((mIconsPlaced && showOnLeft == mOnLeft) || mSnapping 477 || !mMenuContainer.isAttachedToWindow()) { 478 // Do nothing 479 return; 480 } 481 final int count = mMenuContainer.getChildCount(); 482 for (int i = 0; i < count; i++) { 483 final View v = mMenuContainer.getChildAt(i); 484 final float left = i * mHorizSpaceForIcon; 485 final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1)); 486 v.setX(showOnLeft ? left : right); 487 } 488 mOnLeft = showOnLeft; 489 mIconsPlaced = true; 490 } 491 492 private void setMenuAlpha(float alpha) { 493 mAlpha = alpha; 494 if (mMenuContainer == null) { 495 return; 496 } 497 if (alpha == 0) { 498 mMenuFadedIn = false; // Can fade in again once it's gone. 499 mMenuContainer.setVisibility(View.INVISIBLE); 500 } else { 501 mMenuContainer.setVisibility(View.VISIBLE); 502 } 503 final int count = mMenuContainer.getChildCount(); 504 for (int i = 0; i < count; i++) { 505 mMenuContainer.getChildAt(i).setAlpha(mAlpha); 506 } 507 } 508 509 /** 510 * Returns the horizontal space in pixels required to display the menu. 511 */ 512 private float getSpaceForMenu() { 513 return mHorizSpaceForIcon * mMenuContainer.getChildCount(); 514 } 515 516 private final class CheckForDrag implements Runnable { 517 @Override 518 public void run() { 519 final float absTransX = Math.abs(mTranslation); 520 final float bounceBackToMenuWidth = getSpaceForMenu(); 521 final float notiThreshold = mParent.getWidth() * 0.4f; 522 if ((!isMenuVisible() || isMenuLocationChange()) 523 && absTransX >= bounceBackToMenuWidth * 0.4 524 && absTransX < notiThreshold) { 525 fadeInMenu(notiThreshold); 526 } 527 } 528 } 529 530 private void fadeInMenu(final float notiThreshold) { 531 if (mDismissing || mAnimating) { 532 return; 533 } 534 if (isMenuLocationChange()) { 535 setMenuAlpha(0f); 536 } 537 final float transX = mTranslation; 538 final boolean fromLeft = mTranslation > 0; 539 setMenuLocation(); 540 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1); 541 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 542 @Override 543 public void onAnimationUpdate(ValueAnimator animation) { 544 final float absTrans = Math.abs(transX); 545 546 boolean pastMenu = (fromLeft && transX <= notiThreshold) 547 || (!fromLeft && absTrans <= notiThreshold); 548 if (pastMenu && !mMenuFadedIn) { 549 setMenuAlpha((float) animation.getAnimatedValue()); 550 } 551 } 552 }); 553 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 554 @Override 555 public void onAnimationStart(Animator animation) { 556 mAnimating = true; 557 } 558 559 @Override 560 public void onAnimationCancel(Animator animation) { 561 // TODO should animate back to 0f from current alpha 562 setMenuAlpha(0f); 563 } 564 565 @Override 566 public void onAnimationEnd(Animator animation) { 567 mAnimating = false; 568 mMenuFadedIn = mAlpha == 1; 569 } 570 }); 571 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); 572 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION); 573 mFadeAnimator.start(); 574 } 575 576 @Override 577 public void setMenuItems(ArrayList<MenuItem> items) { 578 // Do nothing we use our own for now. 579 // TODO -- handle / allow custom menu items! 580 } 581 582 public static MenuItem createSnoozeItem(Context context) { 583 Resources res = context.getResources(); 584 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context) 585 .inflate(R.layout.notification_snooze, null, false); 586 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description); 587 MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content, 588 R.drawable.ic_snooze); 589 return snooze; 590 } 591 592 public static MenuItem createInfoItem(Context context) { 593 Resources res = context.getResources(); 594 String infoDescription = res.getString(R.string.notification_menu_gear_description); 595 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( 596 R.layout.notification_info, null, false); 597 MenuItem info = new NotificationMenuItem(context, infoDescription, infoContent, 598 R.drawable.ic_settings); 599 return info; 600 } 601 602 private void addMenuView(MenuItem item, ViewGroup parent) { 603 View menuView = item.getMenuView(); 604 if (menuView != null) { 605 parent.addView(menuView); 606 menuView.setOnClickListener(this); 607 FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); 608 lp.width = (int) mHorizSpaceForIcon; 609 lp.height = (int) mHorizSpaceForIcon; 610 menuView.setLayoutParams(lp); 611 } 612 } 613 614 public static class NotificationMenuItem implements MenuItem { 615 View mMenuView; 616 GutsContent mGutsContent; 617 String mContentDescription; 618 619 public NotificationMenuItem(Context context, String s, GutsContent content, int iconResId) { 620 Resources res = context.getResources(); 621 int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 622 int tint = res.getColor(R.color.notification_gear_color); 623 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); 624 iv.setPadding(padding, padding, padding, padding); 625 Drawable icon = context.getResources().getDrawable(iconResId); 626 iv.setImageDrawable(icon); 627 iv.setColorFilter(tint); 628 iv.setAlpha(1f); 629 mMenuView = iv; 630 mContentDescription = s; 631 mGutsContent = content; 632 } 633 634 @Override 635 public View getMenuView() { 636 return mMenuView; 637 } 638 639 @Override 640 public View getGutsView() { 641 return mGutsContent.getContentView(); 642 } 643 644 @Override 645 public String getContentDescription() { 646 return mContentDescription; 647 } 648 } 649} 650