NotificationMenuRow.java revision bd707492a2ef3641a9e9a64ef72b3b031dac0bb6
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 (!mSwipeHelper.isFalseGesture(ev) 334 && (swipedEnoughToShowMenu() && (!gestureFastEnough || showMenuForSlowOnGoing)) 335 || (gestureTowardsMenu && !mSwipeHelper.isDismissGesture(ev))) { 336 // Menu has not been snapped to previously and this is menu revealing gesture 337 showMenu(animView, menuSnapTarget, velocity); 338 } else if (mSwipeHelper.isDismissGesture(ev) && !gestureTowardsMenu) { 339 dismiss(animView, velocity); 340 } else { 341 snapBack(animView, velocity); 342 } 343 return true; 344 } 345 346 private void showMenu(View animView, float targetLeft, float velocity) { 347 mMenuSnappedTo = true; 348 mMenuSnappedOnLeft = mOnLeft; 349 mMenuListener.onMenuShown(animView); 350 mSwipeHelper.snap(animView, targetLeft, velocity); 351 } 352 353 private void snapBack(View animView, float velocity) { 354 if (mFadeAnimator != null) { 355 mFadeAnimator.cancel(); 356 } 357 mHandler.removeCallbacks(mCheckForDrag); 358 mMenuSnappedTo = false; 359 mSnapping = true; 360 mSwipeHelper.snap(animView, 0 /* leftTarget */, velocity); 361 } 362 363 private void dismiss(View animView, float velocity) { 364 if (mFadeAnimator != null) { 365 mFadeAnimator.cancel(); 366 } 367 mHandler.removeCallbacks(mCheckForDrag); 368 mMenuSnappedTo = false; 369 mDismissing = true; 370 mSwipeHelper.dismiss(animView, velocity); 371 } 372 373 /** 374 * @return whether the notification has been translated enough to show the menu and not enough 375 * to be dismissed. 376 */ 377 private boolean swipedEnoughToShowMenu() { 378 final float multiplier = mParent.canViewBeDismissed() 379 ? SWIPED_FAR_ENOUGH_MENU_FRACTION 380 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION; 381 final float minimumSwipeDistance = mHorizSpaceForIcon * multiplier; 382 return !mSwipeHelper.swipedFarEnough(0, 0) && isMenuVisible() 383 && (mOnLeft ? mTranslation > minimumSwipeDistance 384 : mTranslation < -minimumSwipeDistance); 385 } 386 387 /** 388 * Returns whether the gesture is towards the menu location or not. 389 */ 390 private boolean isTowardsMenu(float movement) { 391 return isMenuVisible() 392 && ((mOnLeft && movement <= 0) 393 || (!mOnLeft && movement >= 0)); 394 } 395 396 @Override 397 public void setAppName(String appName) { 398 if (appName == null) { 399 return; 400 } 401 Resources res = mContext.getResources(); 402 final int count = mMenuItems.size(); 403 for (int i = 0; i < count; i++) { 404 MenuItem item = mMenuItems.get(i); 405 String description = String.format( 406 res.getString(R.string.notification_menu_accessibility), 407 appName, item.getContentDescription()); 408 View menuView = item.getMenuView(); 409 if (menuView != null) { 410 menuView.setContentDescription(description); 411 } 412 } 413 } 414 415 @Override 416 public void onHeightUpdate() { 417 if (mParent == null || mMenuItems.size() == 0) { 418 return; 419 } 420 int parentHeight = mParent.getCollapsedHeight(); 421 float translationY; 422 if (parentHeight < mVertSpaceForIcons) { 423 translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2); 424 } else { 425 translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2; 426 } 427 mMenuContainer.setTranslationY(translationY); 428 } 429 430 @Override 431 public void onTranslationUpdate(float translation) { 432 mTranslation = translation; 433 if (mAnimating || !mMenuFadedIn) { 434 // Don't adjust when animating, or if the menu hasn't been shown yet. 435 return; 436 } 437 final float fadeThreshold = mParent.getWidth() * 0.3f; 438 final float absTrans = Math.abs(translation); 439 float desiredAlpha = 0; 440 if (absTrans == 0) { 441 desiredAlpha = 0; 442 } else if (absTrans <= fadeThreshold) { 443 desiredAlpha = 1; 444 } else { 445 desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold)); 446 } 447 setMenuAlpha(desiredAlpha); 448 } 449 450 @Override 451 public void onClick(View v) { 452 if (mMenuListener == null) { 453 // Nothing to do 454 return; 455 } 456 v.getLocationOnScreen(mIconLocation); 457 mParent.getLocationOnScreen(mParentLocation); 458 final int centerX = (int) (mHorizSpaceForIcon / 2); 459 final int centerY = v.getHeight() / 2; 460 final int x = mIconLocation[0] - mParentLocation[0] + centerX; 461 final int y = mIconLocation[1] - mParentLocation[1] + centerY; 462 final int index = mMenuContainer.indexOfChild(v); 463 mMenuListener.onMenuClicked(mParent, x, y, mMenuItems.get(index)); 464 } 465 466 private boolean isMenuLocationChange() { 467 boolean onLeft = mTranslation > mIconPadding; 468 boolean onRight = mTranslation < -mIconPadding; 469 if ((mOnLeft && onRight) || (!mOnLeft && onLeft)) { 470 return true; 471 } 472 return false; 473 } 474 475 private void setMenuLocation() { 476 boolean showOnLeft = mTranslation > 0; 477 if ((mIconsPlaced && showOnLeft == mOnLeft) || mSnapping 478 || !mMenuContainer.isAttachedToWindow()) { 479 // Do nothing 480 return; 481 } 482 final int count = mMenuContainer.getChildCount(); 483 for (int i = 0; i < count; i++) { 484 final View v = mMenuContainer.getChildAt(i); 485 final float left = i * mHorizSpaceForIcon; 486 final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1)); 487 v.setX(showOnLeft ? left : right); 488 } 489 mOnLeft = showOnLeft; 490 mIconsPlaced = true; 491 } 492 493 private void setMenuAlpha(float alpha) { 494 mAlpha = alpha; 495 if (mMenuContainer == null) { 496 return; 497 } 498 if (alpha == 0) { 499 mMenuFadedIn = false; // Can fade in again once it's gone. 500 mMenuContainer.setVisibility(View.INVISIBLE); 501 } else { 502 mMenuContainer.setVisibility(View.VISIBLE); 503 } 504 final int count = mMenuContainer.getChildCount(); 505 for (int i = 0; i < count; i++) { 506 mMenuContainer.getChildAt(i).setAlpha(mAlpha); 507 } 508 } 509 510 /** 511 * Returns the horizontal space in pixels required to display the menu. 512 */ 513 private float getSpaceForMenu() { 514 return mHorizSpaceForIcon * mMenuContainer.getChildCount(); 515 } 516 517 private final class CheckForDrag implements Runnable { 518 @Override 519 public void run() { 520 final float absTransX = Math.abs(mTranslation); 521 final float bounceBackToMenuWidth = getSpaceForMenu(); 522 final float notiThreshold = mParent.getWidth() * 0.4f; 523 if ((!isMenuVisible() || isMenuLocationChange()) 524 && absTransX >= bounceBackToMenuWidth * 0.4 525 && absTransX < notiThreshold) { 526 fadeInMenu(notiThreshold); 527 } 528 } 529 } 530 531 private void fadeInMenu(final float notiThreshold) { 532 if (mDismissing || mAnimating) { 533 return; 534 } 535 if (isMenuLocationChange()) { 536 setMenuAlpha(0f); 537 } 538 final float transX = mTranslation; 539 final boolean fromLeft = mTranslation > 0; 540 setMenuLocation(); 541 mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1); 542 mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 543 @Override 544 public void onAnimationUpdate(ValueAnimator animation) { 545 final float absTrans = Math.abs(transX); 546 547 boolean pastMenu = (fromLeft && transX <= notiThreshold) 548 || (!fromLeft && absTrans <= notiThreshold); 549 if (pastMenu && !mMenuFadedIn) { 550 setMenuAlpha((float) animation.getAnimatedValue()); 551 } 552 } 553 }); 554 mFadeAnimator.addListener(new AnimatorListenerAdapter() { 555 @Override 556 public void onAnimationStart(Animator animation) { 557 mAnimating = true; 558 } 559 560 @Override 561 public void onAnimationCancel(Animator animation) { 562 // TODO should animate back to 0f from current alpha 563 setMenuAlpha(0f); 564 } 565 566 @Override 567 public void onAnimationEnd(Animator animation) { 568 mAnimating = false; 569 mMenuFadedIn = mAlpha == 1; 570 } 571 }); 572 mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN); 573 mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION); 574 mFadeAnimator.start(); 575 } 576 577 @Override 578 public void setMenuItems(ArrayList<MenuItem> items) { 579 // Do nothing we use our own for now. 580 // TODO -- handle / allow custom menu items! 581 } 582 583 public static MenuItem createSnoozeItem(Context context) { 584 Resources res = context.getResources(); 585 NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context) 586 .inflate(R.layout.notification_snooze, null, false); 587 String snoozeDescription = res.getString(R.string.notification_menu_snooze_description); 588 MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content, 589 R.drawable.ic_snooze); 590 return snooze; 591 } 592 593 public static MenuItem createInfoItem(Context context) { 594 Resources res = context.getResources(); 595 String infoDescription = res.getString(R.string.notification_menu_gear_description); 596 NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( 597 R.layout.notification_info, null, false); 598 MenuItem info = new NotificationMenuItem(context, infoDescription, infoContent, 599 R.drawable.ic_settings); 600 return info; 601 } 602 603 private void addMenuView(MenuItem item, ViewGroup parent) { 604 View menuView = item.getMenuView(); 605 if (menuView != null) { 606 parent.addView(menuView); 607 menuView.setOnClickListener(this); 608 FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams(); 609 lp.width = (int) mHorizSpaceForIcon; 610 lp.height = (int) mHorizSpaceForIcon; 611 menuView.setLayoutParams(lp); 612 } 613 } 614 615 public static class NotificationMenuItem implements MenuItem { 616 View mMenuView; 617 GutsContent mGutsContent; 618 String mContentDescription; 619 620 public NotificationMenuItem(Context context, String s, GutsContent content, int iconResId) { 621 Resources res = context.getResources(); 622 int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); 623 int tint = res.getColor(R.color.notification_gear_color); 624 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context); 625 iv.setPadding(padding, padding, padding, padding); 626 Drawable icon = context.getResources().getDrawable(iconResId); 627 iv.setImageDrawable(icon); 628 iv.setColorFilter(tint); 629 iv.setAlpha(1f); 630 mMenuView = iv; 631 mContentDescription = s; 632 mGutsContent = content; 633 } 634 635 @Override 636 public View getMenuView() { 637 return mMenuView; 638 } 639 640 @Override 641 public View getGutsView() { 642 return mGutsContent.getContentView(); 643 } 644 645 @Override 646 public String getContentDescription() { 647 return mContentDescription; 648 } 649 } 650} 651