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