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