Home | History | Annotate | Download | only in statusbar
      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 
    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