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