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 
17 package com.android.systemui.statusbar;
18 
19 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
20 
21 import java.util.ArrayList;
22 
23 import com.android.systemui.Interpolators;
24 import com.android.systemui.R;
25 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
26 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
27 import com.android.systemui.statusbar.NotificationGuts.GutsContent;
28 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
29 
30 import android.animation.Animator;
31 import android.animation.AnimatorListenerAdapter;
32 import android.animation.ValueAnimator;
33 import android.app.Notification;
34 import android.content.Context;
35 import android.content.res.Resources;
36 import android.graphics.drawable.Drawable;
37 import android.os.Handler;
38 import android.os.Looper;
39 import android.util.Log;
40 import android.service.notification.StatusBarNotification;
41 import android.view.LayoutInflater;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.FrameLayout;
46 import android.widget.FrameLayout.LayoutParams;
47 
48 public 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 
NotificationMenuRow(Context context)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
getMenuItems(Context context)112     public ArrayList<MenuItem> getMenuItems(Context context) {
113         return mMenuItems;
114     }
115 
116     @Override
getLongpressMenuItem(Context context)117     public MenuItem getLongpressMenuItem(Context context) {
118         return mInfoItem;
119     }
120 
121     @Override
setSwipeActionHelper(NotificationSwipeActionHelper helper)122     public void setSwipeActionHelper(NotificationSwipeActionHelper helper) {
123         mSwipeHelper = helper;
124     }
125 
126     @Override
setMenuClickListener(OnMenuEventListener listener)127     public void setMenuClickListener(OnMenuEventListener listener) {
128         mMenuListener = listener;
129     }
130 
131     @Override
createMenu(ViewGroup parent)132     public void createMenu(ViewGroup parent) {
133         mParent = (ExpandableNotificationRow) parent;
134         createMenuViews(true /* resetState */);
135     }
136 
137     @Override
isMenuVisible()138     public boolean isMenuVisible() {
139         return mAlpha > 0;
140     }
141 
142     @Override
getMenuView()143     public View getMenuView() {
144         return mMenuContainer;
145     }
146 
147     @Override
resetMenu()148     public void resetMenu() {
149         resetState(true);
150     }
151 
152     @Override
onNotificationUpdated()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
onConfigurationChanged()162     public void onConfigurationChanged() {
163         mParent.setLayoutListener(this);
164     }
165 
166     @Override
onLayout()167     public void onLayout() {
168         mIconsPlaced = false; // Force icons to be re-placed
169         setMenuLocation();
170         mParent.removeListener();
171     }
172 
createMenuViews(boolean resetState)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 
resetState(boolean notify)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
onTouchEvent(View view, MotionEvent ev, float velocity)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 
handleUpEvent(MotionEvent ev, View animView, float velocity)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