• 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.notification.row;
18 
19 import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
20 import static android.view.HapticFeedbackConstants.CLOCK_TICK;
21 
22 import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
23 
24 import android.animation.Animator;
25 import android.animation.AnimatorListenerAdapter;
26 import android.animation.ValueAnimator;
27 import android.annotation.Nullable;
28 import android.content.Context;
29 import android.content.res.Resources;
30 import android.graphics.Point;
31 import android.graphics.drawable.Drawable;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.provider.Settings;
35 import android.service.notification.StatusBarNotification;
36 import android.util.ArrayMap;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.FrameLayout;
41 import android.widget.FrameLayout.LayoutParams;
42 
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.systemui.R;
45 import com.android.systemui.animation.Interpolators;
46 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
47 import com.android.systemui.statusbar.AlphaOptimizedImageView;
48 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
49 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
50 import com.android.systemui.statusbar.notification.row.NotificationGuts.GutsContent;
51 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
52 
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.Map;
56 
57 public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener,
58         ExpandableNotificationRow.LayoutListener {
59 
60     private static final boolean DEBUG = false;
61     private static final String TAG = "swipe";
62 
63     // Notification must be swiped at least this fraction of a single menu item to show menu
64     private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f;
65     private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f;
66 
67     // When the menu is displayed, the notification must be swiped within this fraction of a single
68     // menu item to snap back to menu (else it will cover the menu or it'll be dismissed)
69     private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f;
70 
71     private static final int ICON_ALPHA_ANIM_DURATION = 200;
72     private static final long SHOW_MENU_DELAY = 60;
73 
74     private ExpandableNotificationRow mParent;
75 
76     private Context mContext;
77     private FrameLayout mMenuContainer;
78     private NotificationMenuItem mInfoItem;
79     private MenuItem mFeedbackItem;
80     private MenuItem mSnoozeItem;
81     private ArrayList<MenuItem> mLeftMenuItems;
82     private ArrayList<MenuItem> mRightMenuItems;
83     private final Map<View, MenuItem> mMenuItemsByView = new ArrayMap<>();
84     private OnMenuEventListener mMenuListener;
85     private boolean mDismissRtl;
86 
87     private ValueAnimator mFadeAnimator;
88     private boolean mAnimating;
89     private boolean mMenuFadedIn;
90 
91     private boolean mOnLeft;
92     private boolean mIconsPlaced;
93 
94     private boolean mDismissing;
95     private boolean mSnapping;
96     private float mTranslation;
97 
98     private int[] mIconLocation = new int[2];
99     private int[] mParentLocation = new int[2];
100 
101     private int mHorizSpaceForIcon = -1;
102     private int mVertSpaceForIcons = -1;
103     private int mIconPadding = -1;
104     private int mSidePadding;
105 
106     private float mAlpha = 0f;
107 
108     private CheckForDrag mCheckForDrag;
109     private Handler mHandler;
110 
111     private boolean mMenuSnapped;
112     private boolean mMenuSnappedOnLeft;
113     private boolean mShouldShowMenu;
114 
115     private boolean mIsUserTouching;
116 
117     private boolean mSnappingToDismiss;
118 
119     private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
120 
NotificationMenuRow(Context context, PeopleNotificationIdentifier peopleNotificationIdentifier)121     public NotificationMenuRow(Context context,
122             PeopleNotificationIdentifier peopleNotificationIdentifier) {
123         mContext = context;
124         mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear);
125         mHandler = new Handler(Looper.getMainLooper());
126         mLeftMenuItems = new ArrayList<>();
127         mRightMenuItems = new ArrayList<>();
128         mPeopleNotificationIdentifier = peopleNotificationIdentifier;
129     }
130 
131     @Override
getMenuItems(Context context)132     public ArrayList<MenuItem> getMenuItems(Context context) {
133         return mOnLeft ? mLeftMenuItems : mRightMenuItems;
134     }
135 
136     @Override
getLongpressMenuItem(Context context)137     public MenuItem getLongpressMenuItem(Context context) {
138         return mInfoItem;
139     }
140 
141     @Override
getFeedbackMenuItem(Context context)142     public MenuItem getFeedbackMenuItem(Context context) {
143         return mFeedbackItem;
144     }
145 
146     @Override
getSnoozeMenuItem(Context context)147     public MenuItem getSnoozeMenuItem(Context context) {
148         return mSnoozeItem;
149     }
150 
151     @VisibleForTesting
getParent()152     protected ExpandableNotificationRow getParent() {
153         return mParent;
154     }
155 
156     @VisibleForTesting
isMenuOnLeft()157     protected boolean isMenuOnLeft() {
158         return mOnLeft;
159     }
160 
161     @VisibleForTesting
isMenuSnappedOnLeft()162     protected boolean isMenuSnappedOnLeft() {
163         return mMenuSnappedOnLeft;
164     }
165 
166     @VisibleForTesting
isMenuSnapped()167     protected boolean isMenuSnapped() {
168         return mMenuSnapped;
169     }
170 
171     @VisibleForTesting
isDismissing()172     protected boolean isDismissing() {
173         return mDismissing;
174     }
175 
176     @VisibleForTesting
isSnapping()177     protected boolean isSnapping() {
178         return mSnapping;
179     }
180 
181     @VisibleForTesting
isSnappingToDismiss()182     protected boolean isSnappingToDismiss() {
183         return mSnappingToDismiss;
184     }
185 
186     @Override
setMenuClickListener(OnMenuEventListener listener)187     public void setMenuClickListener(OnMenuEventListener listener) {
188         mMenuListener = listener;
189     }
190 
191     @Override
createMenu(ViewGroup parent, StatusBarNotification sbn)192     public void createMenu(ViewGroup parent, StatusBarNotification sbn) {
193         mParent = (ExpandableNotificationRow) parent;
194         createMenuViews(true /* resetState */);
195     }
196 
197     @Override
isMenuVisible()198     public boolean isMenuVisible() {
199         return mAlpha > 0;
200     }
201 
202     @VisibleForTesting
isUserTouching()203     protected boolean isUserTouching() {
204         return mIsUserTouching;
205     }
206 
207     @Override
shouldShowMenu()208     public boolean shouldShowMenu() {
209         return mShouldShowMenu;
210     }
211 
212     @Override
getMenuView()213     public View getMenuView() {
214         return mMenuContainer;
215     }
216 
217     @VisibleForTesting
getTranslation()218     protected float getTranslation() {
219         return mTranslation;
220     }
221 
222     @Override
resetMenu()223     public void resetMenu() {
224         resetState(true);
225     }
226 
227     @Override
onTouchEnd()228     public void onTouchEnd() {
229         mIsUserTouching = false;
230     }
231 
232     @Override
onNotificationUpdated(StatusBarNotification sbn)233     public void onNotificationUpdated(StatusBarNotification sbn) {
234         if (mMenuContainer == null) {
235             // Menu hasn't been created yet, no need to do anything.
236             return;
237         }
238         createMenuViews(!isMenuVisible() /* resetState */);
239     }
240 
241     @Override
onConfigurationChanged()242     public void onConfigurationChanged() {
243         mParent.setLayoutListener(this);
244     }
245 
246     @Override
onLayout()247     public void onLayout() {
248         mIconsPlaced = false; // Force icons to be re-placed
249         setMenuLocation();
250         mParent.removeListener();
251     }
252 
createMenuViews(boolean resetState)253     private void createMenuViews(boolean resetState) {
254         final Resources res = mContext.getResources();
255         mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
256         mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
257         mLeftMenuItems.clear();
258         mRightMenuItems.clear();
259 
260         boolean showSnooze = Settings.Secure.getInt(mContext.getContentResolver(),
261                 SHOW_NOTIFICATION_SNOOZE, 0) == 1;
262 
263         // Construct the menu items based on the notification
264         if (showSnooze) {
265             // Only show snooze for non-foreground notifications, and if the setting is on
266             mSnoozeItem = createSnoozeItem(mContext);
267         }
268         mFeedbackItem = createFeedbackItem(mContext);
269         NotificationEntry entry = mParent.getEntry();
270         int personNotifType = mPeopleNotificationIdentifier.getPeopleNotificationType(entry);
271         if (personNotifType == PeopleNotificationIdentifier.TYPE_PERSON) {
272             mInfoItem = createPartialConversationItem(mContext);
273         } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) {
274             mInfoItem = createConversationItem(mContext);
275         } else {
276             mInfoItem = createInfoItem(mContext);
277         }
278 
279         if (showSnooze) {
280             mRightMenuItems.add(mSnoozeItem);
281         }
282         mRightMenuItems.add(mInfoItem);
283         mRightMenuItems.add(mFeedbackItem);
284         mLeftMenuItems.addAll(mRightMenuItems);
285 
286         populateMenuViews();
287         if (resetState) {
288             resetState(false /* notify */);
289         } else {
290             mIconsPlaced = false;
291             setMenuLocation();
292             if (!mIsUserTouching) {
293                 onSnapOpen();
294             }
295         }
296     }
297 
populateMenuViews()298     private void populateMenuViews() {
299         if (mMenuContainer != null) {
300             mMenuContainer.removeAllViews();
301             mMenuItemsByView.clear();
302         } else {
303             mMenuContainer = new FrameLayout(mContext);
304         }
305         // The setting can win (which is needed for tests) but if not set, then use the flag
306         final int showDismissSetting =  Settings.Global.getInt(mContext.getContentResolver(),
307                 Settings.Global.SHOW_NEW_NOTIF_DISMISS, -1);
308         final boolean newFlowHideShelf = showDismissSetting == -1
309                 ? mContext.getResources().getBoolean(R.bool.flag_notif_updates)
310                 : showDismissSetting == 1;
311         if (newFlowHideShelf) {
312             return;
313         }
314         List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
315         for (int i = 0; i < menuItems.size(); i++) {
316             addMenuView(menuItems.get(i), mMenuContainer);
317         }
318     }
319 
resetState(boolean notify)320     private void resetState(boolean notify) {
321         setMenuAlpha(0f);
322         mIconsPlaced = false;
323         mMenuFadedIn = false;
324         mAnimating = false;
325         mSnapping = false;
326         mDismissing = false;
327         mMenuSnapped = false;
328         setMenuLocation();
329         if (mMenuListener != null && notify) {
330             mMenuListener.onMenuReset(mParent);
331         }
332     }
333 
334     @Override
onTouchMove(float delta)335     public void onTouchMove(float delta) {
336         mSnapping = false;
337 
338         if (!isTowardsMenu(delta) && isMenuLocationChange()) {
339             // Don't consider it "snapped" if location has changed.
340             mMenuSnapped = false;
341 
342             // Changed directions, make sure we check to fade in icon again.
343             if (!mHandler.hasCallbacks(mCheckForDrag)) {
344                 // No check scheduled, set null to schedule a new one.
345                 mCheckForDrag = null;
346             } else {
347                 // Check scheduled, reset alpha and update location; check will fade it in
348                 setMenuAlpha(0f);
349                 setMenuLocation();
350             }
351         }
352         if (mShouldShowMenu
353                 && !NotificationStackScrollLayout.isPinnedHeadsUp(getParent())
354                 && !mParent.areGutsExposed()
355                 && !mParent.showingPulsing()
356                 && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) {
357             // Only show the menu if we're not a heads up view and guts aren't exposed.
358             mCheckForDrag = new CheckForDrag();
359             mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY);
360         }
361         if (canBeDismissed()) {
362             final float dismissThreshold = getDismissThreshold();
363             final boolean snappingToDismiss = delta < -dismissThreshold || delta > dismissThreshold;
364             if (mSnappingToDismiss != snappingToDismiss) {
365                 getMenuView().performHapticFeedback(CLOCK_TICK);
366             }
367             mSnappingToDismiss = snappingToDismiss;
368         }
369     }
370 
371     @VisibleForTesting
beginDrag()372     protected void beginDrag() {
373         mSnapping = false;
374         if (mFadeAnimator != null) {
375             mFadeAnimator.cancel();
376         }
377         mHandler.removeCallbacks(mCheckForDrag);
378         mCheckForDrag = null;
379         mIsUserTouching = true;
380     }
381 
382     @Override
onTouchStart()383     public void onTouchStart() {
384         beginDrag();
385         mSnappingToDismiss = false;
386     }
387 
388     @Override
onSnapOpen()389     public void onSnapOpen() {
390         mMenuSnapped = true;
391         mMenuSnappedOnLeft = isMenuOnLeft();
392         if (mAlpha == 0f && mParent != null) {
393             fadeInMenu(mParent.getWidth());
394         }
395         if (mMenuListener != null) {
396             mMenuListener.onMenuShown(getParent());
397         }
398     }
399 
400     @Override
onSnapClosed()401     public void onSnapClosed() {
402         cancelDrag();
403         mMenuSnapped = false;
404         mSnapping = true;
405     }
406 
407     @Override
onDismiss()408     public void onDismiss() {
409         cancelDrag();
410         mMenuSnapped = false;
411         mDismissing = true;
412     }
413 
414     @VisibleForTesting
cancelDrag()415     protected void cancelDrag() {
416         if (mFadeAnimator != null) {
417             mFadeAnimator.cancel();
418         }
419         mHandler.removeCallbacks(mCheckForDrag);
420     }
421 
422     @VisibleForTesting
getMinimumSwipeDistance()423     protected float getMinimumSwipeDistance() {
424         final float multiplier = getParent().canViewBeDismissed()
425                 ? SWIPED_FAR_ENOUGH_MENU_FRACTION
426                 : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION;
427         return mHorizSpaceForIcon * multiplier;
428     }
429 
430     @VisibleForTesting
getMaximumSwipeDistance()431     protected float getMaximumSwipeDistance() {
432         return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION;
433     }
434 
435     /**
436      * Returns whether the gesture is towards the menu location or not.
437      */
438     @Override
isTowardsMenu(float movement)439     public boolean isTowardsMenu(float movement) {
440         return isMenuVisible()
441                 && ((isMenuOnLeft() && movement <= 0)
442                         || (!isMenuOnLeft() && movement >= 0));
443     }
444 
445     @Override
setAppName(String appName)446     public void setAppName(String appName) {
447         if (appName == null) {
448             return;
449         }
450         setAppName(appName, mLeftMenuItems);
451         setAppName(appName, mRightMenuItems);
452     }
453 
setAppName(String appName, ArrayList<MenuItem> menuItems)454     private void setAppName(String appName,
455             ArrayList<MenuItem> menuItems) {
456         Resources res = mContext.getResources();
457         final int count = menuItems.size();
458         for (int i = 0; i < count; i++) {
459             MenuItem item = menuItems.get(i);
460             String description = String.format(
461                     res.getString(R.string.notification_menu_accessibility),
462                     appName, item.getContentDescription());
463             View menuView = item.getMenuView();
464             if (menuView != null) {
465                 menuView.setContentDescription(description);
466             }
467         }
468     }
469 
470     @Override
onParentHeightUpdate()471     public void onParentHeightUpdate() {
472         if (mParent == null
473                 || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty())
474                 || mMenuContainer == null) {
475             return;
476         }
477         int parentHeight = mParent.getActualHeight();
478         float translationY;
479         if (parentHeight < mVertSpaceForIcons) {
480             translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
481         } else {
482             translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
483         }
484         mMenuContainer.setTranslationY(translationY);
485     }
486 
487     @Override
onParentTranslationUpdate(float translation)488     public void onParentTranslationUpdate(float translation) {
489         mTranslation = translation;
490         if (mAnimating || !mMenuFadedIn) {
491             // Don't adjust when animating, or if the menu hasn't been shown yet.
492             return;
493         }
494         final float fadeThreshold = mParent.getWidth() * 0.3f;
495         final float absTrans = Math.abs(translation);
496         float desiredAlpha = 0;
497         if (absTrans == 0) {
498             desiredAlpha = 0;
499         } else if (absTrans <= fadeThreshold) {
500             desiredAlpha = 1;
501         } else {
502             desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold));
503         }
504         setMenuAlpha(desiredAlpha);
505     }
506 
507     @Override
onClick(View v)508     public void onClick(View v) {
509         if (mMenuListener == null) {
510             // Nothing to do
511             return;
512         }
513         v.getLocationOnScreen(mIconLocation);
514         mParent.getLocationOnScreen(mParentLocation);
515         final int centerX = mHorizSpaceForIcon / 2;
516         final int centerY = v.getHeight() / 2;
517         final int x = mIconLocation[0] - mParentLocation[0] + centerX;
518         final int y = mIconLocation[1] - mParentLocation[1] + centerY;
519         if (mMenuItemsByView.containsKey(v)) {
520             mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v));
521         }
522     }
523 
isMenuLocationChange()524     private boolean isMenuLocationChange() {
525         boolean onLeft = mTranslation > mIconPadding;
526         boolean onRight = mTranslation < -mIconPadding;
527         if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) {
528             return true;
529         }
530         return false;
531     }
532 
533     private void setMenuLocation() {
534         boolean showOnLeft = mTranslation > 0;
535         if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null
536                 || !mMenuContainer.isAttachedToWindow()) {
537             // Do nothing
538             return;
539         }
540         boolean wasOnLeft = mOnLeft;
541         mOnLeft = showOnLeft;
542         if (wasOnLeft != showOnLeft) {
543             populateMenuViews();
544         }
545         final int count = mMenuContainer.getChildCount();
546         for (int i = 0; i < count; i++) {
547             final View v = mMenuContainer.getChildAt(i);
548             final float left = i * mHorizSpaceForIcon;
549             final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
550             v.setX(showOnLeft ? left : right);
551         }
552         mIconsPlaced = true;
553     }
554 
555     @VisibleForTesting
setMenuAlpha(float alpha)556     protected void setMenuAlpha(float alpha) {
557         mAlpha = alpha;
558         if (mMenuContainer == null) {
559             return;
560         }
561         if (alpha == 0) {
562             mMenuFadedIn = false; // Can fade in again once it's gone.
563             mMenuContainer.setVisibility(View.INVISIBLE);
564         } else {
565             mMenuContainer.setVisibility(View.VISIBLE);
566         }
567         final int count = mMenuContainer.getChildCount();
568         for (int i = 0; i < count; i++) {
569             mMenuContainer.getChildAt(i).setAlpha(mAlpha);
570         }
571     }
572 
573     /**
574      * Returns the horizontal space in pixels required to display the menu.
575      */
576     @VisibleForTesting
getSpaceForMenu()577     protected int getSpaceForMenu() {
578         return mHorizSpaceForIcon * mMenuContainer.getChildCount();
579     }
580 
581     private final class CheckForDrag implements Runnable {
582         @Override
run()583         public void run() {
584             final float absTransX = Math.abs(mTranslation);
585             final float bounceBackToMenuWidth = getSpaceForMenu();
586             final float notiThreshold = mParent.getWidth() * 0.4f;
587             if ((!isMenuVisible() || isMenuLocationChange())
588                     && absTransX >= bounceBackToMenuWidth * 0.4
589                     && absTransX < notiThreshold) {
590                 fadeInMenu(notiThreshold);
591             }
592         }
593     }
594 
fadeInMenu(final float notiThreshold)595     private void fadeInMenu(final float notiThreshold) {
596         if (mDismissing || mAnimating) {
597             return;
598         }
599         if (isMenuLocationChange()) {
600             setMenuAlpha(0f);
601         }
602         final float transX = mTranslation;
603         final boolean fromLeft = mTranslation > 0;
604         setMenuLocation();
605         mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
606         mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
607             @Override
608             public void onAnimationUpdate(ValueAnimator animation) {
609                 final float absTrans = Math.abs(transX);
610 
611                 boolean pastMenu = (fromLeft && transX <= notiThreshold)
612                         || (!fromLeft && absTrans <= notiThreshold);
613                 if (pastMenu && !mMenuFadedIn) {
614                     setMenuAlpha((float) animation.getAnimatedValue());
615                 }
616             }
617         });
618         mFadeAnimator.addListener(new AnimatorListenerAdapter() {
619             @Override
620             public void onAnimationStart(Animator animation) {
621                 mAnimating = true;
622             }
623 
624             @Override
625             public void onAnimationCancel(Animator animation) {
626                 // TODO should animate back to 0f from current alpha
627                 setMenuAlpha(0f);
628             }
629 
630             @Override
631             public void onAnimationEnd(Animator animation) {
632                 mAnimating = false;
633                 mMenuFadedIn = mAlpha == 1;
634             }
635         });
636         mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
637         mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
638         mFadeAnimator.start();
639     }
640 
641     @Override
setMenuItems(ArrayList<MenuItem> items)642     public void setMenuItems(ArrayList<MenuItem> items) {
643         // Do nothing we use our own for now.
644         // TODO -- handle / allow custom menu items!
645     }
646 
647     @Override
shouldShowGutsOnSnapOpen()648     public boolean shouldShowGutsOnSnapOpen() {
649         return false;
650     }
651 
652     @Override
menuItemToExposeOnSnap()653     public MenuItem menuItemToExposeOnSnap() {
654         return null;
655     }
656 
657     @Override
getRevealAnimationOrigin()658     public Point getRevealAnimationOrigin() {
659         View v = mInfoItem.getMenuView();
660         int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2);
661         int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2);
662         if (isMenuOnLeft()) {
663             return new Point(menuX, menuY);
664         } else {
665             menuX = mParent.getRight() - menuX;
666             return new Point(menuX, menuY);
667         }
668     }
669 
createSnoozeItem(Context context)670     static MenuItem createSnoozeItem(Context context) {
671         Resources res = context.getResources();
672         NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context)
673                 .inflate(R.layout.notification_snooze, null, false);
674         String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
675         MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content,
676                 R.drawable.ic_snooze);
677         return snooze;
678     }
679 
createConversationItem(Context context)680     static NotificationMenuItem createConversationItem(Context context) {
681         Resources res = context.getResources();
682         String infoDescription = res.getString(R.string.notification_menu_gear_description);
683         NotificationConversationInfo infoContent =
684                 (NotificationConversationInfo) LayoutInflater.from(context).inflate(
685                         R.layout.notification_conversation_info, null, false);
686         return new NotificationMenuItem(context, infoDescription, infoContent,
687                 R.drawable.ic_settings);
688     }
689 
createPartialConversationItem(Context context)690     static NotificationMenuItem createPartialConversationItem(Context context) {
691         Resources res = context.getResources();
692         String infoDescription = res.getString(R.string.notification_menu_gear_description);
693         PartialConversationInfo infoContent =
694                 (PartialConversationInfo) LayoutInflater.from(context).inflate(
695                         R.layout.partial_conversation_info, null, false);
696         return new NotificationMenuItem(context, infoDescription, infoContent,
697                 R.drawable.ic_settings);
698     }
699 
createInfoItem(Context context)700     static NotificationMenuItem createInfoItem(Context context) {
701         Resources res = context.getResources();
702         String infoDescription = res.getString(R.string.notification_menu_gear_description);
703         NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
704                 R.layout.notification_info, null, false);
705         return new NotificationMenuItem(context, infoDescription, infoContent,
706                 R.drawable.ic_settings);
707     }
708 
createFeedbackItem(Context context)709     static MenuItem createFeedbackItem(Context context) {
710         FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate(
711                 R.layout.feedback_info, null, false);
712         MenuItem info = new NotificationMenuItem(context, null, feedbackContent,
713                 -1 /*don't show in slow swipe menu */);
714         return info;
715     }
716 
addMenuView(MenuItem item, ViewGroup parent)717     private void addMenuView(MenuItem item, ViewGroup parent) {
718         View menuView = item.getMenuView();
719         if (menuView != null) {
720             menuView.setAlpha(mAlpha);
721             parent.addView(menuView);
722             menuView.setOnClickListener(this);
723             FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams();
724             lp.width = mHorizSpaceForIcon;
725             lp.height = mHorizSpaceForIcon;
726             menuView.setLayoutParams(lp);
727         }
728         mMenuItemsByView.put(menuView, item);
729     }
730 
731     @VisibleForTesting
732     /**
733      * Determine the minimum offset below which the menu should snap back closed.
734      */
getSnapBackThreshold()735     protected float getSnapBackThreshold() {
736         return getSpaceForMenu() - getMaximumSwipeDistance();
737     }
738 
739     /**
740      * Determine the maximum offset above which the parent notification should be dismissed.
741      * @return
742      */
743     @VisibleForTesting
getDismissThreshold()744     protected float getDismissThreshold() {
745         return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION;
746     }
747 
748     @Override
isWithinSnapMenuThreshold()749     public boolean isWithinSnapMenuThreshold() {
750         float translation = getTranslation();
751         float snapBackThreshold = getSnapBackThreshold();
752         float targetRight = getDismissThreshold();
753         return isMenuOnLeft()
754                 ? translation > snapBackThreshold && translation < targetRight
755                 : translation < -snapBackThreshold && translation > -targetRight;
756     }
757 
758     @Override
isSwipedEnoughToShowMenu()759     public boolean isSwipedEnoughToShowMenu() {
760         final float minimumSwipeDistance = getMinimumSwipeDistance();
761         final float translation = getTranslation();
762         return isMenuVisible() && (isMenuOnLeft() ?
763                 translation > minimumSwipeDistance
764                 : translation < -minimumSwipeDistance);
765     }
766 
767     @Override
getMenuSnapTarget()768     public int getMenuSnapTarget() {
769         return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu();
770     }
771 
772     @Override
shouldSnapBack()773     public boolean shouldSnapBack() {
774         float translation = getTranslation();
775         float targetLeft = getSnapBackThreshold();
776         return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft;
777     }
778 
779     @Override
isSnappedAndOnSameSide()780     public boolean isSnappedAndOnSameSide() {
781         return isMenuSnapped() && isMenuVisible()
782                 && isMenuSnappedOnLeft() == isMenuOnLeft();
783     }
784 
785     @Override
canBeDismissed()786     public boolean canBeDismissed() {
787         return getParent().canViewBeDismissed();
788     }
789 
790     @Override
setDismissRtl(boolean dismissRtl)791     public void setDismissRtl(boolean dismissRtl) {
792         mDismissRtl = dismissRtl;
793         if (mMenuContainer != null) {
794             createMenuViews(true);
795         }
796     }
797 
798     public static class NotificationMenuItem implements MenuItem {
799         View mMenuView;
800         GutsContent mGutsContent;
801         String mContentDescription;
802 
803         /**
804          * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu
805          * but can still be exposed via other affordances.
806          */
NotificationMenuItem(Context context, String contentDescription, GutsContent content, int iconResId)807         public NotificationMenuItem(Context context, String contentDescription, GutsContent content,
808                 int iconResId) {
809             Resources res = context.getResources();
810             int padding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
811             int tint = res.getColor(R.color.notification_gear_color);
812             if (iconResId >= 0) {
813                 AlphaOptimizedImageView iv = new AlphaOptimizedImageView(context);
814                 iv.setPadding(padding, padding, padding, padding);
815                 Drawable icon = context.getResources().getDrawable(iconResId);
816                 iv.setImageDrawable(icon);
817                 iv.setColorFilter(tint);
818                 iv.setAlpha(1f);
819                 mMenuView = iv;
820             }
821             mContentDescription = contentDescription;
822             mGutsContent = content;
823         }
824 
825         @Override
826         @Nullable
getMenuView()827         public View getMenuView() {
828             return mMenuView;
829         }
830 
831         @Override
getGutsView()832         public View getGutsView() {
833             return mGutsContent.getContentView();
834         }
835 
836         @Override
getContentDescription()837         public String getContentDescription() {
838             return mContentDescription;
839         }
840     }
841 }
842