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