Extract NotificationSwipeHelper and related state from NSSL.

Change-Id: Ic0a1178e4f3d6f2addd9bae1c31ec57dffee8eba
Test: Automated tests should pass. Manual testing should also show that notifications behave the same as before (swiping, tapping, dismissing, snapping/unsnapping menu, etc.)
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index a3b5395..0215fda 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -61,13 +61,14 @@
     public static final float SWIPED_FAR_ENOUGH_SIZE_FRACTION = 0.6f;
     static final float MAX_SCROLL_SIZE_FRACTION = 0.3f;
 
+    protected final Handler mHandler;
+
     private float mMinSwipeProgress = 0f;
     private float mMaxSwipeProgress = 1f;
 
     private final FlingAnimationUtils mFlingAnimationUtils;
     private float mPagingTouchSlop;
     private final Callback mCallback;
-    private final Handler mHandler;
     private final int mSwipeDirection;
     private final VelocityTracker mVelocityTracker;
     private final FalsingManager mFalsingManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 72c2c0b..9978ec3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -87,7 +87,6 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
-import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.DragDownHelper.DragDownCallback;
@@ -109,6 +108,7 @@
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
 import com.android.systemui.statusbar.StatusBarStateController;
@@ -146,11 +146,9 @@
  * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
  */
 public class NotificationStackScrollLayout extends ViewGroup
-        implements Callback, ExpandHelper.Callback, ScrollAdapter,
-        OnHeightChangedListener, OnGroupChangeListener,
-        OnMenuEventListener, VisibilityLocationProvider,
-        NotificationListContainer, ConfigurationListener, DragDownCallback, AnimationStateHandler,
-        Dumpable {
+        implements ExpandHelper.Callback, ScrollAdapter, OnHeightChangedListener,
+        OnGroupChangeListener, VisibilityLocationProvider, NotificationListContainer,
+        ConfigurationListener, DragDownCallback, AnimationStateHandler, Dumpable {
 
     public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
     private static final String TAG = "StackScroller";
@@ -164,7 +162,7 @@
     private static final int INVALID_POINTER = -1;
 
     private ExpandHelper mExpandHelper;
-    private NotificationSwipeHelper mSwipeHelper;
+    private final NotificationSwipeHelper mSwipeHelper;
     private boolean mSwipingInProgress;
     private int mCurrentStackHeight = Integer.MAX_VALUE;
     private final Paint mBackgroundPaint = new Paint();
@@ -291,10 +289,6 @@
      */
     private int mMaxScrollAfterExpand;
     private ExpandableNotificationRow.LongPressListener mLongPressListener;
-
-    private NotificationMenuRowPlugin mCurrMenuRow;
-    private View mTranslatingParentView;
-    private View mMenuExposedView;
     boolean mCheckForLeavebehind;
 
     /**
@@ -466,6 +460,9 @@
     private Interpolator mDarkXInterpolator = Interpolators.FAST_OUT_SLOW_IN;
     private NotificationPanelView mNotificationPanel;
 
+    private final NotificationGutsManager
+            mNotificationGutsManager = Dependency.get(NotificationGutsManager.class);
+
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public NotificationStackScrollLayout(Context context) {
         this(context, null);
@@ -495,7 +492,8 @@
                 minHeight, maxHeight);
         mExpandHelper.setEventSource(this);
         mExpandHelper.setScrollAdapter(this);
-        mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, this, getContext());
+        mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, new SwipeHelperCallback(),
+                getContext(), new NotificationMenuListener());
         mStackScrollAlgorithm = createStackScrollAlgorithm(context);
         initView(context);
         mFalsingManager = FalsingManager.getInstance(context);
@@ -639,41 +637,6 @@
     }
 
     @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onMenuClicked(View view, int x, int y, MenuItem item) {
-        if (mLongPressListener == null) {
-            return;
-        }
-        if (view instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
-            MetricsLogger.action(mContext, MetricsEvent.ACTION_TOUCH_GEAR,
-                    row.getStatusBarNotification().getPackageName());
-        }
-        mLongPressListener.onLongPress(view, x, y, item);
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onMenuReset(View row) {
-        if (mTranslatingParentView != null && row == mTranslatingParentView) {
-            mMenuExposedView = null;
-            mTranslatingParentView = null;
-        }
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onMenuShown(View row) {
-        mMenuExposedView = mTranslatingParentView;
-        if (row instanceof ExpandableNotificationRow) {
-            MetricsLogger.action(mContext, MetricsEvent.ACTION_REVEAL_GEAR,
-                    ((ExpandableNotificationRow) row).getStatusBarNotification()
-                            .getPackageName());
-        }
-        mSwipeHelper.onMenuShown(row);
-    }
-
-    @Override
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void onUiModeChanged() {
         mBgColor = mContext.getColor(R.color.notification_shade_background_color);
@@ -1295,111 +1258,6 @@
         mQsContainer = qsContainer;
     }
 
-    /**
-     * Handles cleanup after the given {@code view} has been fully swiped out (including
-     * re-invoking dismiss logic in case the notification has not made its way out yet).
-     */
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void onChildDismissed(View view) {
-        ExpandableNotificationRow row = (ExpandableNotificationRow) view;
-        if (!row.isDismissed()) {
-            handleChildViewDismissed(view);
-        }
-        ViewGroup transientContainer = row.getTransientContainer();
-        if (transientContainer != null) {
-            transientContainer.removeTransientView(view);
-        }
-    }
-
-    /**
-     * Starts up notification dismiss and tells the notification, if any, to remove itself from
-     * layout.
-     *
-     * @param view view (e.g. notification) to dismiss from the layout
-     */
-
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    private void handleChildViewDismissed(View view) {
-        if (mDismissAllInProgress) {
-            return;
-        }
-
-        boolean isBlockingHelperShown = false;
-
-        setSwipingInProgress(false);
-        if (mDragAnimPendingChildren.contains(view)) {
-            // We start the swipe and finish it in the same frame; we don't want a drag animation.
-            mDragAnimPendingChildren.remove(view);
-        }
-        mAmbientState.onDragFinished(view);
-        updateContinuousShadowDrawing();
-
-        if (view instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
-            if (row.isHeadsUp()) {
-                mHeadsUpManager.addSwipedOutNotification(row.getStatusBarNotification().getKey());
-            }
-            isBlockingHelperShown =
-                    row.performDismissWithBlockingHelper(false /* fromAccessibility */);
-        }
-
-        if (!isBlockingHelperShown) {
-            mSwipedOutViews.add(view);
-        }
-        mFalsingManager.onNotificationDismissed();
-        if (mFalsingManager.shouldEnforceBouncer()) {
-            mStatusBar.executeRunnableDismissingKeyguard(
-                    null,
-                    null /* cancelAction */,
-                    false /* dismissShade */,
-                    true /* afterKeyguardGone */,
-                    false /* deferred */);
-        }
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void onChildSnappedBack(View animView, float targetLeft) {
-        mAmbientState.onDragFinished(animView);
-        updateContinuousShadowDrawing();
-        if (!mDragAnimPendingChildren.contains(animView)) {
-            if (mAnimationsEnabled) {
-                mSnappedBackChildren.add(animView);
-                mNeedsAnimation = true;
-            }
-            requestChildrenUpdate();
-        } else {
-            // We start the swipe and snap back in the same frame, we don't want any animation
-            mDragAnimPendingChildren.remove(animView);
-        }
-        if (mCurrMenuRow != null && targetLeft == 0) {
-            mCurrMenuRow.resetMenu();
-            mCurrMenuRow = null;
-        }
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
-        // Returning true prevents alpha fading.
-        return !mFadeNotificationsOnDismiss;
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onBeginDrag(View v) {
-        mFalsingManager.onNotificatonStartDismissing();
-        setSwipingInProgress(true);
-        mAmbientState.onBeginDrag(v);
-        updateContinuousShadowDrawing();
-        if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) {
-            mDragAnimPendingChildren.add(v);
-            mNeedsAnimation = true;
-        }
-        requestChildrenUpdate();
-    }
-
     @ShadeViewRefactor(RefactorComponent.ADAPTER)
     public static boolean isPinnedHeadsUp(View v) {
         if (v instanceof ExpandableNotificationRow) {
@@ -1418,41 +1276,6 @@
         return false;
     }
 
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onDragCancelled(View v) {
-        mFalsingManager.onNotificatonStopDismissing();
-        setSwipingInProgress(false);
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public float getFalsingThresholdFactor() {
-        return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public View getChildAtPosition(MotionEvent ev) {
-        View child = getChildAtPosition(ev.getX(), ev.getY());
-        if (child instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-            ExpandableNotificationRow parent = row.getNotificationParent();
-            if (parent != null && parent.areChildrenExpanded()
-                    && (parent.areGutsExposed()
-                    || mMenuExposedView == parent
-                    || (parent.getNotificationChildren().size() == 1
-                    && parent.isClearable()))) {
-                // In this case the group is expanded and showing the menu for the
-                // group, further interaction should apply to the group, not any
-                // child notifications so we use the parent of the child. We also do the same
-                // if we only have a single child.
-                child = parent;
-            }
-        }
-        return child;
-    }
-
     @ShadeViewRefactor(RefactorComponent.INPUT)
     public ExpandableView getClosestChildAtRawPosition(float touchX, float touchY) {
         getLocationOnScreen(mTempInt2);
@@ -1696,18 +1519,11 @@
         return mScrollingEnabled;
     }
 
-    @Override
     @ShadeViewRefactor(RefactorComponent.ADAPTER)
-    public boolean canChildBeDismissed(View v) {
+    private boolean canChildBeDismissed(View v) {
         return StackScrollAlgorithm.canChildBeDismissed(v);
     }
 
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean isAntiFalsingNeeded() {
-        return onKeyguard();
-    }
-
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private boolean onKeyguard() {
         return mStatusBarState == StatusBarState.KEYGUARD;
@@ -1787,8 +1603,8 @@
         }
 
         // Check if we need to clear any snooze leavebehinds
-        NotificationGuts guts = mStatusBar.getGutsManager().getExposedGuts();
-        if (guts != null && !isTouchInView(ev, guts)
+        NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
+        if (guts != null && !NotificationSwipeHelper.isTouchInView(ev, guts)
                 && guts.getGutsContent() instanceof NotificationSnooze) {
             NotificationSnooze ns = (NotificationSnooze) guts.getGutsContent();
             if ((ns.isExpanded() && isCancelOrUp)
@@ -3013,11 +2829,11 @@
         }
         // Check if we need to clear any snooze leavebehinds
         boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP;
-        NotificationGuts guts = mStatusBar.getGutsManager().getExposedGuts();
-        if (!isTouchInView(ev, guts) && isUp && !swipeWantsIt && !expandWantsIt
-                && !scrollWantsIt) {
+        NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
+        if (!NotificationSwipeHelper.isTouchInView(ev, guts) && isUp && !swipeWantsIt &&
+                !expandWantsIt && !scrollWantsIt) {
             mCheckForLeavebehind = false;
-            mStatusBar.getGutsManager().closeAndSaveGuts(true /* removeLeavebehind */,
+            mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */,
                     false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */,
                     false /* resetMenu */);
         }
@@ -3077,8 +2893,8 @@
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     @Override
     public void cleanUpViewState(View child) {
-        if (child == mTranslatingParentView) {
-            mTranslatingParentView = null;
+        if (child == mSwipeHelper.getTranslatingParentView()) {
+            mSwipeHelper.clearTranslatingParentView();
         }
         mCurrentStackScrollState.removeViewStateForView(child);
     }
@@ -3986,7 +3802,7 @@
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void checkSnoozeLeavebehind() {
         if (mCheckForLeavebehind) {
-            mStatusBar.getGutsManager().closeAndSaveGuts(true /* removeLeavebehind */,
+            mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */,
                     false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */,
                     false /* resetMenu */);
             mCheckForLeavebehind = false;
@@ -4068,7 +3884,7 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private void setIsExpanded(boolean isExpanded) {
+    public void setIsExpanded(boolean isExpanded) {
         boolean changed = isExpanded != mIsExpanded;
         mIsExpanded = isExpanded;
         mStackScrollAlgorithm.setIsExpanded(isExpanded);
@@ -5242,8 +5058,8 @@
         setFooterView(footerView);
     }
 
-  @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-  private void inflateEmptyShadeView() {
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    private void inflateEmptyShadeView() {
         EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
                 R.layout.status_bar_no_notifications, this, false);
         view.setText(R.string.empty_shade_text);
@@ -5274,8 +5090,8 @@
         mScrimController.setNotificationCount(getNotGoneChildCount());
     }
 
-  @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-  public void setNotificationPanel(NotificationPanelView notificationPanelView) {
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    public void setNotificationPanel(NotificationPanelView notificationPanelView) {
         mNotificationPanel = notificationPanelView;
     }
 
@@ -5293,306 +5109,29 @@
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public interface OnOverscrollTopChangedListener {
 
-        /**
-         * Notifies a listener that the overscroll has changed.
-         *
-         * @param amount         the amount of overscroll, in pixels
-         * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
-         *                       unrubberbanded motion to directly expand overscroll view (e.g
-         *                       expand
-         *                       QS)
-         */
-        void onOverscrollTopChanged(float amount, boolean isRubberbanded);
+    /**
+     * Notifies a listener that the overscroll has changed.
+     *
+     * @param amount         the amount of overscroll, in pixels
+     * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
+     *                       unrubberbanded motion to directly expand overscroll view (e.g
+     *                       expand
+     *                       QS)
+     */
+    void onOverscrollTopChanged(float amount, boolean isRubberbanded);
 
-        /**
-         * Notify a listener that the scroller wants to escape from the scrolling motion and
-         * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
-         *
-         * @param velocity The velocity that the Scroller had when over flinging
-         * @param open     Should the fling open or close the overscroll view.
-         */
-        void flingTopOverscroll(float velocity, boolean open);
-    }
+    /**
+     * Notify a listener that the scroller wants to escape from the scrolling motion and
+     * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
+     *
+     * @param velocity The velocity that the Scroller had when over flinging
+     * @param open     Should the fling open or close the overscroll view.
+     */
+    void flingTopOverscroll(float velocity, boolean open);
+  }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private class NotificationSwipeHelper extends SwipeHelper
-            implements NotificationSwipeActionHelper {
-        private static final long COVER_MENU_DELAY = 4000;
-        private Runnable mFalsingCheck;
-        private Handler mHandler;
-
-        private static final long SWIPE_MENU_TIMING = 200;
-
-        public NotificationSwipeHelper(int swipeDirection, Callback callback, Context context) {
-            super(swipeDirection, callback, context);
-            mHandler = new Handler();
-            mFalsingCheck = new Runnable() {
-                @Override
-                public void run() {
-                    resetExposedMenuView(true /* animate */, true /* force */);
-                }
-            };
-        }
-
-        @Override
-        public void onDownUpdate(View currView, MotionEvent ev) {
-            mTranslatingParentView = currView;
-            if (mCurrMenuRow != null) {
-                mCurrMenuRow.onTouchStart();
-            }
-            mCurrMenuRow = null;
-            mHandler.removeCallbacks(mFalsingCheck);
-
-            // Slide back any notifications that might be showing a menu
-            resetExposedMenuView(true /* animate */, false /* force */);
-
-            if (currView instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) currView;
-
-                if (row.getEntry().hasFinishedInitialization()) {
-                    mCurrMenuRow = row.createMenu();
-                    mCurrMenuRow.setMenuClickListener(NotificationStackScrollLayout.this);
-                    mCurrMenuRow.onTouchStart();
-                }
-            }
-        }
-
-        private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) {
-            return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu();
-        }
-
-        @Override
-        public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) {
-            mHandler.removeCallbacks(mFalsingCheck);
-            if (mCurrMenuRow != null) {
-                mCurrMenuRow.onTouchMove(delta);
-            }
-        }
-
-        @Override
-        public boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
-                float translation) {
-            if (mCurrMenuRow != null) {
-                mCurrMenuRow.onTouchEnd();
-                handleMenuRowSwipe(ev, animView, velocity, mCurrMenuRow);
-                return true;
-            }
-            return false;
-        }
-
-        @Override
-        public boolean swipedFarEnough(float translation, float viewSize) {
-            return swipedFarEnough();
-        }
-
-        private void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity,
-                NotificationMenuRowPlugin menuRow) {
-            if (!menuRow.shouldShowMenu()) {
-                // If the menu should not be shown, then there is no need to check if the a swipe
-                // should result in a snapping to the menu. As a result, just check if the swipe
-                // was enough to dismiss the notification.
-                if (isDismissGesture(ev)) {
-                    dismiss(animView, velocity);
-                } else {
-                    snapBack(animView, velocity);
-                    menuRow.onSnapClosed();
-                }
-                return;
-            }
-
-            if (menuRow.isSnappedAndOnSameSide()) {
-                // Menu was snapped to previously and we're on the same side
-                handleSwipeFromSnap(ev, animView, velocity, menuRow);
-            } else {
-                // Menu has not been snapped, or was snapped previously but is now on
-                // the opposite side.
-                handleSwipeFromNonSnap(ev, animView, velocity, menuRow);
-            }
-        }
-
-        private void handleSwipeFromNonSnap(MotionEvent ev, View animView, float velocity,
-                NotificationMenuRowPlugin menuRow) {
-            boolean isDismissGesture = isDismissGesture(ev);
-            final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity);
-            final boolean gestureFastEnough =
-                    mSwipeHelper.getMinDismissVelocity() <= Math.abs(velocity);
-
-            final double timeForGesture = ev.getEventTime() - ev.getDownTime();
-            final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed()
-                    && timeForGesture >= SWIPE_MENU_TIMING;
-
-            if (!isFalseGesture(ev)
-                    && (swipedEnoughToShowMenu(menuRow)
-                    && (!gestureFastEnough || showMenuForSlowOnGoing))
-                    || (gestureTowardsMenu && !isDismissGesture)) {
-                // Menu has not been snapped to previously and this is menu revealing gesture
-                snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
-                menuRow.onSnapOpen();
-            } else if (isDismissGesture(ev) && !gestureTowardsMenu) {
-                dismiss(animView, velocity);
-                menuRow.onDismiss();
-            } else {
-                snapBack(animView, velocity);
-                menuRow.onSnapClosed();
-            }
-        }
-
-        private void handleSwipeFromSnap(MotionEvent ev, View animView, float velocity,
-                NotificationMenuRowPlugin menuRow) {
-            boolean isDismissGesture = isDismissGesture(ev);
-
-            final boolean withinSnapMenuThreshold =
-                    menuRow.isWithinSnapMenuThreshold();
-
-            if (withinSnapMenuThreshold && !isDismissGesture) {
-                // Haven't moved enough to unsnap from the menu
-                menuRow.onSnapOpen();
-                snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
-            } else if (isDismissGesture && !menuRow.shouldSnapBack()) {
-                // Only dismiss if we're not moving towards the menu
-                dismiss(animView, velocity);
-                menuRow.onDismiss();
-            } else {
-                snapBack(animView, velocity);
-                menuRow.onSnapClosed();
-            }
-        }
-
-        @Override
-        public void dismissChild(final View view, float velocity,
-                boolean useAccelerateInterpolator) {
-            super.dismissChild(view, velocity, useAccelerateInterpolator);
-            if (mIsExpanded) {
-                // We don't want to quick-dismiss when it's a heads up as this might lead to closing
-                // of the panel early.
-                handleChildViewDismissed(view);
-            }
-            mStatusBar.getGutsManager().closeAndSaveGuts(true /* removeLeavebehind */,
-                    false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */,
-                    false /* resetMenu */);
-            handleMenuCoveredOrDismissed();
-        }
-
-        @Override
-        public void snapChild(final View animView, final float targetLeft, float velocity) {
-            super.snapChild(animView, targetLeft, velocity);
-            onDragCancelled(animView);
-            if (targetLeft == 0) {
-                handleMenuCoveredOrDismissed();
-            }
-        }
-
-        @Override
-        public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
-            mStatusBar.setNotificationSnoozed(sbn, snoozeOption);
-        }
-
-        private void handleMenuCoveredOrDismissed() {
-            if (mMenuExposedView != null && mMenuExposedView == mTranslatingParentView) {
-                mMenuExposedView = null;
-            }
-        }
-
-        @Override
-        public Animator getViewTranslationAnimator(View v, float target,
-                AnimatorUpdateListener listener) {
-            if (v instanceof ExpandableNotificationRow) {
-                return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener);
-            } else {
-                return super.getViewTranslationAnimator(v, target, listener);
-            }
-        }
-
-        @Override
-        public void setTranslation(View v, float translate) {
-            ((ExpandableView) v).setTranslation(translate);
-        }
-
-        @Override
-        public float getTranslation(View v) {
-            return ((ExpandableView) v).getTranslation();
-        }
-
-        @Override
-        public void dismiss(View animView, float velocity) {
-            dismissChild(animView, velocity,
-                    !swipedFastEnough(0, 0) /* useAccelerateInterpolator */);
-        }
-
-        @Override
-        public void snapOpen(View animView, int targetLeft, float velocity) {
-            snapChild(animView, targetLeft, velocity);
-        }
-
-        private void snapBack(View animView, float velocity) {
-            snapChild(animView, 0, velocity);
-        }
-
-        @Override
-        public boolean swipedFastEnough(float translation, float velocity) {
-            return swipedFastEnough();
-        }
-
-        @Override
-        public float getMinDismissVelocity() {
-            return getEscapeVelocity();
-        }
-
-        public void onMenuShown(View animView) {
-            onDragCancelled(animView);
-
-            // If we're on the lockscreen we want to false this.
-            if (isAntiFalsingNeeded()) {
-                mHandler.removeCallbacks(mFalsingCheck);
-                mHandler.postDelayed(mFalsingCheck, COVER_MENU_DELAY);
-            }
-        }
-
-        public void closeControlsIfOutsideTouch(MotionEvent ev) {
-            NotificationGuts guts = mStatusBar.getGutsManager().getExposedGuts();
-            View view = null;
-            if (guts != null && !guts.getGutsContent().isLeavebehind()) {
-                // Only close visible guts if they're not a leavebehind.
-                view = guts;
-            } else if (mCurrMenuRow != null && mCurrMenuRow.isMenuVisible()
-                    && mTranslatingParentView != null) {
-                // Checking menu
-                view = mTranslatingParentView;
-            }
-            if (view != null && !isTouchInView(ev, view)) {
-                // Touch was outside visible guts / menu notification, close what's visible
-                mStatusBar.getGutsManager().closeAndSaveGuts(false /* removeLeavebehind */,
-                        false /* force */, true /* removeControls */, -1 /* x */, -1 /* y */,
-                        false /* resetMenu */);
-                resetExposedMenuView(true /* animate */, true /* force */);
-            }
-        }
-
-        public void resetExposedMenuView(boolean animate, boolean force) {
-            if (mMenuExposedView == null
-                    || (!force && mMenuExposedView == mTranslatingParentView)) {
-                // If no menu is showing or it's showing for this view we do nothing.
-                return;
-            }
-            final View prevMenuExposedView = mMenuExposedView;
-            if (animate) {
-                Animator anim = getViewTranslationAnimator(prevMenuExposedView,
-                        0 /* leftTarget */, null /* updateListener */);
-                if (anim != null) {
-                    anim.start();
-                }
-            } else if (mMenuExposedView instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) mMenuExposedView;
-                if (!row.isRemoved()) {
-                    row.resetTranslation();
-                }
-            }
-            mMenuExposedView = null;
-        }
-    }
-
-  @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-  public boolean hasActiveNotifications() {
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    public boolean hasActiveNotifications() {
         return !mEntryManager.getNotificationData().getActiveNotifications().isEmpty();
     }
 
@@ -5656,8 +5195,8 @@
         return mStatusBarState == StatusBarState.KEYGUARD;
     }
 
-  @ShadeViewRefactor(RefactorComponent.INPUT)
-  public void updateSpeedBumpIndex() {
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void updateSpeedBumpIndex() {
         int speedBumpIndex = 0;
         int currentIndex = 0;
         final int N = getChildCount();
@@ -5677,24 +5216,6 @@
         updateSpeedBumpIndex(speedBumpIndex, noAmbient);
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private boolean isTouchInView(MotionEvent ev, View view) {
-        if (view == null) {
-            return false;
-        }
-        final int height = (view instanceof ExpandableView)
-                ? ((ExpandableView) view).getActualHeight()
-                : view.getHeight();
-        final int rx = (int) ev.getRawX();
-        final int ry = (int) ev.getRawY();
-        view.getLocationOnScreen(mTempInt2);
-        final int x = mTempInt2[0];
-        final int y = mTempInt2[1];
-        Rect rect = new Rect(x, y, x + view.getWidth(), y + height);
-        boolean ret = rect.contains(rx, ry);
-        return ret;
-    }
-
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private void updateContinuousShadowDrawing() {
         boolean continuousShadowUpdate = mAnimationRunning
@@ -5718,11 +5239,29 @@
 
     @ShadeViewRefactor(RefactorComponent.INPUT)
     public void closeControlsIfOutsideTouch(MotionEvent ev) {
-        mSwipeHelper.closeControlsIfOutsideTouch(ev);
+        NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
+        NotificationMenuRowPlugin menuRow = mSwipeHelper.getCurrentMenuRow();
+        View translatingParentView = mSwipeHelper.getTranslatingParentView();
+        View view = null;
+        if (guts != null && !guts.getGutsContent().isLeavebehind()) {
+            // Only close visible guts if they're not a leavebehind.
+            view = guts;
+        } else if (menuRow != null && menuRow.isMenuVisible()
+                && translatingParentView != null) {
+            // Checking menu
+            view = translatingParentView;
+        }
+        if (view != null && !NotificationSwipeHelper.isTouchInView(ev, view)) {
+            // Touch was outside visible guts / menu notification, close what's visible
+            mNotificationGutsManager.closeAndSaveGuts(false /* removeLeavebehind */,
+                    false /* force */, true /* removeControls */, -1 /* x */, -1 /* y */,
+                    false /* resetMenu */);
+            resetExposedMenuView(true /* animate */, true /* force */);
+        }
     }
 
-  @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
-  static class AnimationEvent {
+    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
+    static class AnimationEvent {
 
         static AnimationFilter[] FILTERS = new AnimationFilter[]{
 
@@ -6022,8 +5561,8 @@
         }
     }
 
-  @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
-  private final StateListener mStateListener = new StateListener() {
+    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
+    private final StateListener mStateListener = new StateListener() {
         @Override
         public void onStatePreChange(int oldState, int newState) {
             if (oldState == StatusBarState.SHADE_LOCKED && newState == StatusBarState.KEYGUARD) {
@@ -6036,9 +5575,222 @@
             setStatusBarState(newState);
         }
 
-      @Override
-      public void onStatePostChange() {
+        @Override
+        public void onStatePostChange() {
           NotificationStackScrollLayout.this.onStatePostChange();
       }
-  };
+    };
+
+    class NotificationMenuListener implements NotificationMenuRowPlugin.OnMenuEventListener {
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public void onMenuClicked(View view, int x, int y, MenuItem item) {
+            if (mLongPressListener == null) {
+                return;
+            }
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                MetricsLogger.action(mContext, MetricsEvent.ACTION_TOUCH_GEAR,
+                        row.getStatusBarNotification().getPackageName());
+            }
+            mLongPressListener.onLongPress(view, x, y, item);
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public void onMenuReset(View row) {
+            View translatingParentView = mSwipeHelper.getTranslatingParentView();
+            if (translatingParentView != null && row == translatingParentView) {
+                mSwipeHelper.clearExposedMenuView();
+                mSwipeHelper.clearTranslatingParentView();
+            }
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public void onMenuShown(View row) {
+            if (row instanceof ExpandableNotificationRow) {
+                MetricsLogger.action(mContext, MetricsEvent.ACTION_REVEAL_GEAR,
+                        ((ExpandableNotificationRow) row).getStatusBarNotification()
+                                .getPackageName());
+            }
+            mSwipeHelper.onMenuShown(row);
+        }
+    }
+
+    class SwipeHelperCallback implements NotificationSwipeHelper.NotificationCallback {
+        @Override
+        public void onDismiss() {
+            mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */,
+                    false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */,
+                    false /* resetMenu */);
+        }
+
+        @Override
+        public void onSnooze(StatusBarNotification sbn,
+                NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
+            mStatusBar.setNotificationSnoozed(sbn, snoozeOption);
+        }
+
+        @Override
+        public boolean isExpanded() {
+            return NotificationStackScrollLayout.this.isExpanded();
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public void onDragCancelled(View v) {
+            mFalsingManager.onNotificatonStopDismissing();
+            setSwipingInProgress(false);
+        }
+
+        /**
+         * Handles cleanup after the given {@code view} has been fully swiped out (including
+         * re-invoking dismiss logic in case the notification has not made its way out yet).
+         */
+        @Override
+        @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+        public void onChildDismissed(View view) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+            if (!row.isDismissed()) {
+                handleChildViewDismissed(view);
+            }
+            ViewGroup transientContainer = row.getTransientContainer();
+            if (transientContainer != null) {
+                transientContainer.removeTransientView(view);
+            }
+        }
+
+        /**
+         * Starts up notification dismiss and tells the notification, if any, to remove itself from
+         * layout.
+         *
+         * @param view view (e.g. notification) to dismiss from the layout
+         */
+
+        @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+        public void handleChildViewDismissed(View view) {
+            if (mDismissAllInProgress) {
+                return;
+            }
+
+            boolean isBlockingHelperShown = false;
+
+            setSwipingInProgress(false);
+            if (mDragAnimPendingChildren.contains(view)) {
+                // We start the swipe and finish it in the same frame; we don't want a drag
+                // animation.
+                mDragAnimPendingChildren.remove(view);
+            }
+            mAmbientState.onDragFinished(view);
+            updateContinuousShadowDrawing();
+
+            if (view instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) view;
+                if (row.isHeadsUp()) {
+                    mHeadsUpManager.addSwipedOutNotification(
+                            row.getStatusBarNotification().getKey());
+                }
+                isBlockingHelperShown =
+                        row.performDismissWithBlockingHelper(false /* fromAccessibility */);
+            }
+
+            if (!isBlockingHelperShown) {
+                mSwipedOutViews.add(view);
+            }
+            mFalsingManager.onNotificationDismissed();
+            if (mFalsingManager.shouldEnforceBouncer()) {
+                mStatusBar.executeRunnableDismissingKeyguard(
+                        null,
+                        null /* cancelAction */,
+                        false /* dismissShade */,
+                        true /* afterKeyguardGone */,
+                        false /* deferred */);
+            }
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public boolean isAntiFalsingNeeded() {
+            return onKeyguard();
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public View getChildAtPosition(MotionEvent ev) {
+            View child = NotificationStackScrollLayout.this.getChildAtPosition(ev.getX(),
+                    ev.getY());
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                ExpandableNotificationRow parent = row.getNotificationParent();
+                if (parent != null && parent.areChildrenExpanded()
+                        && (parent.areGutsExposed()
+                        || mSwipeHelper.getExposedMenuView() == parent
+                        || (parent.getNotificationChildren().size() == 1
+                        && parent.isClearable()))) {
+                    // In this case the group is expanded and showing the menu for the
+                    // group, further interaction should apply to the group, not any
+                    // child notifications so we use the parent of the child. We also do the same
+                    // if we only have a single child.
+                    child = parent;
+                }
+            }
+            return child;
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public void onBeginDrag(View v) {
+            mFalsingManager.onNotificatonStartDismissing();
+            setSwipingInProgress(true);
+            mAmbientState.onBeginDrag(v);
+            updateContinuousShadowDrawing();
+            if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) {
+                mDragAnimPendingChildren.add(v);
+                mNeedsAnimation = true;
+            }
+            requestChildrenUpdate();
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+        public void onChildSnappedBack(View animView, float targetLeft) {
+            mAmbientState.onDragFinished(animView);
+            updateContinuousShadowDrawing();
+            if (!mDragAnimPendingChildren.contains(animView)) {
+                if (mAnimationsEnabled) {
+                    mSnappedBackChildren.add(animView);
+                    mNeedsAnimation = true;
+                }
+                requestChildrenUpdate();
+            } else {
+                // We start the swipe and snap back in the same frame, we don't want any animation
+                mDragAnimPendingChildren.remove(animView);
+            }
+            NotificationMenuRowPlugin menuRow = mSwipeHelper.getCurrentMenuRow();
+            if (menuRow != null && targetLeft == 0) {
+                menuRow.resetMenu();
+                mSwipeHelper.clearCurrentMenuRow();
+            }
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public boolean updateSwipeProgress(View animView, boolean dismissable,
+                float swipeProgress) {
+            // Returning true prevents alpha fading.
+            return !mFadeNotificationsOnDismiss;
+        }
+
+        @Override
+        @ShadeViewRefactor(RefactorComponent.INPUT)
+        public float getFalsingThresholdFactor() {
+            return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
+        }
+
+        @Override
+        public boolean canChildBeDismissed(View v) {
+            return NotificationStackScrollLayout.this.canChildBeDismissed(v);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
new file mode 100644
index 0000000..028957d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the Licen
+ */
+
+
+package com.android.systemui.statusbar.notification.stack;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.notification.ShadeViewRefactor;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.ExpandableView;
+
+@ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.INPUT)
+class NotificationSwipeHelper extends SwipeHelper
+        implements NotificationSwipeActionHelper {
+    @VisibleForTesting
+    protected static final long COVER_MENU_DELAY = 4000;
+    private static final String TAG = "NotificationSwipeHelper";
+    private final Runnable mFalsingCheck;
+    private View mTranslatingParentView;
+    private View mMenuExposedView;
+    private final NotificationCallback mCallback;
+    private final NotificationMenuRowPlugin.OnMenuEventListener mMenuListener;
+
+    private static final long SWIPE_MENU_TIMING = 200;
+
+    private NotificationMenuRowPlugin mCurrMenuRow;
+
+    public NotificationSwipeHelper(int swipeDirection, NotificationCallback callback,
+            Context context, NotificationMenuRowPlugin.OnMenuEventListener menuListener) {
+        super(swipeDirection, callback, context);
+        mMenuListener = menuListener;
+        mCallback = callback;
+        mFalsingCheck = new Runnable() {
+            @Override
+            public void run() {
+                resetExposedMenuView(true /* animate */, true /* force */);
+            }
+        };
+    }
+
+    public View getTranslatingParentView() {
+        return mTranslatingParentView;
+    }
+
+    public void clearTranslatingParentView() { setTranslatingParentView(null); }
+
+    @VisibleForTesting
+    protected void setTranslatingParentView(View view) { mTranslatingParentView = view; };
+
+    public void setExposedMenuView(View view) {
+        mMenuExposedView = view;
+    }
+
+    public void clearExposedMenuView() { setExposedMenuView(null); }
+
+    public void clearCurrentMenuRow() { setCurrentMenuRow(null); }
+
+    public View getExposedMenuView() {
+        return mMenuExposedView;
+    }
+
+    public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) {
+        mCurrMenuRow = menuRow;
+    }
+
+    public NotificationMenuRowPlugin getCurrentMenuRow() {  return mCurrMenuRow; }
+
+    @VisibleForTesting
+    protected Handler getHandler() { return mHandler; }
+
+    @VisibleForTesting
+    protected Runnable getFalsingCheck() { return mFalsingCheck; };
+
+    @Override
+    public void onDownUpdate(View currView, MotionEvent ev) {
+        mTranslatingParentView = currView;
+        NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
+        if (menuRow != null) {
+            menuRow.onTouchStart();
+        }
+        clearCurrentMenuRow();
+        getHandler().removeCallbacks(getFalsingCheck());
+
+        // Slide back any notifications that might be showing a menu
+        resetExposedMenuView(true /* animate */, false /* force */);
+
+        if (currView instanceof ExpandableNotificationRow) {
+            initializeRow((ExpandableNotificationRow) currView);
+        }
+    }
+
+    @VisibleForTesting
+    protected void initializeRow(ExpandableNotificationRow row) {
+        if (row.getEntry().hasFinishedInitialization()) {
+            mCurrMenuRow = row.createMenu();
+            mCurrMenuRow.setMenuClickListener(mMenuListener);
+            mCurrMenuRow.onTouchStart();
+        }
+    }
+
+    private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) {
+        return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu();
+    }
+
+    @Override
+    public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) {
+        getHandler().removeCallbacks(getFalsingCheck());
+        NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
+        if (menuRow != null) {
+            menuRow.onTouchMove(delta);
+        }
+    }
+
+    @Override
+    public boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
+            float translation) {
+        NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
+        if (menuRow != null) {
+            menuRow.onTouchEnd();
+            handleMenuRowSwipe(ev, animView, velocity, menuRow);
+            return true;
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity,
+            NotificationMenuRowPlugin menuRow) {
+        if (!menuRow.shouldShowMenu()) {
+            // If the menu should not be shown, then there is no need to check if the a swipe
+            // should result in a snapping to the menu. As a result, just check if the swipe
+            // was enough to dismiss the notification.
+            if (isDismissGesture(ev)) {
+                dismiss(animView, velocity);
+            } else {
+                snapClosed(animView, velocity);
+                menuRow.onSnapClosed();
+            }
+            return;
+        }
+
+        if (menuRow.isSnappedAndOnSameSide()) {
+            // Menu was snapped to previously and we're on the same side
+            handleSwipeFromSnap(ev, animView, velocity, menuRow);
+        } else {
+            // Menu has not been snapped, or was snapped previously but is now on
+            // the opposite side.
+            handleSwipeFromNonSnap(ev, animView, velocity, menuRow);
+        }
+    }
+
+    private void handleSwipeFromNonSnap(MotionEvent ev, View animView, float velocity,
+            NotificationMenuRowPlugin menuRow) {
+        boolean isDismissGesture = isDismissGesture(ev);
+        final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity);
+        final boolean gestureFastEnough = getEscapeVelocity() <= Math.abs(velocity);
+
+        final double timeForGesture = ev.getEventTime() - ev.getDownTime();
+        final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed()
+                && timeForGesture >= SWIPE_MENU_TIMING;
+
+        if (!isFalseGesture(ev)
+                && (swipedEnoughToShowMenu(menuRow)
+                && (!gestureFastEnough || showMenuForSlowOnGoing))
+                || (gestureTowardsMenu && !isDismissGesture)) {
+            // Menu has not been snapped to previously and this is menu revealing gesture
+            snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
+            menuRow.onSnapOpen();
+        } else if (isDismissGesture(ev) && !gestureTowardsMenu) {
+            dismiss(animView, velocity);
+            menuRow.onDismiss();
+        } else {
+            snapClosed(animView, velocity);
+            menuRow.onSnapClosed();
+        }
+    }
+
+    private void handleSwipeFromSnap(MotionEvent ev, View animView, float velocity,
+            NotificationMenuRowPlugin menuRow) {
+        boolean isDismissGesture = isDismissGesture(ev);
+
+        final boolean withinSnapMenuThreshold =
+                menuRow.isWithinSnapMenuThreshold();
+
+        if (withinSnapMenuThreshold && !isDismissGesture) {
+            // Haven't moved enough to unsnap from the menu
+            menuRow.onSnapOpen();
+            snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
+        } else if (isDismissGesture && !menuRow.shouldSnapBack()) {
+            // Only dismiss if we're not moving towards the menu
+            dismiss(animView, velocity);
+            menuRow.onDismiss();
+        } else {
+            snapClosed(animView, velocity);
+            menuRow.onSnapClosed();
+        }
+    }
+
+    @Override
+    public void dismissChild(final View view, float velocity,
+            boolean useAccelerateInterpolator) {
+        superDismissChild(view, velocity, useAccelerateInterpolator);
+        if (mCallback.isExpanded()) {
+            // We don't want to quick-dismiss when it's a heads up as this might lead to closing
+            // of the panel early.
+            mCallback.handleChildViewDismissed(view);
+        }
+        mCallback.onDismiss();
+        handleMenuCoveredOrDismissed();
+    }
+
+    @VisibleForTesting
+    protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
+        super.dismissChild(view, velocity, useAccelerateInterpolator);
+    }
+
+    @VisibleForTesting
+    protected void superSnapChild(final View animView, final float targetLeft, float velocity) {
+        super.snapChild(animView, targetLeft, velocity);
+    }
+
+    @Override
+    public void snapChild(final View animView, final float targetLeft, float velocity) {
+        superSnapChild(animView, targetLeft, velocity);
+        mCallback.onDragCancelled(animView);
+        if (targetLeft == 0) {
+            handleMenuCoveredOrDismissed();
+        }
+    }
+
+    @Override
+    public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
+        mCallback.onSnooze(sbn, snoozeOption);
+    }
+
+    @VisibleForTesting
+    protected void handleMenuCoveredOrDismissed() {
+        View exposedMenuView = getExposedMenuView();
+        if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) {
+            clearExposedMenuView();
+        }
+    }
+
+    @VisibleForTesting
+    protected Animator superGetViewTranslationAnimator(View v, float target,
+            ValueAnimator.AnimatorUpdateListener listener) {
+        return super.getViewTranslationAnimator(v, target, listener);
+    }
+
+    @Override
+    public Animator getViewTranslationAnimator(View v, float target,
+            ValueAnimator.AnimatorUpdateListener listener) {
+        if (v instanceof ExpandableNotificationRow) {
+            return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener);
+        } else {
+            return superGetViewTranslationAnimator(v, target, listener);
+        }
+    }
+
+    @Override
+    public void setTranslation(View v, float translate) {
+        if (v instanceof ExpandableNotificationRow) {
+            ((ExpandableNotificationRow) v).setTranslation(translate);
+        } else {
+            Log.wtf(TAG, "setTranslation should only be called on an ExpandableNotificationRow.");
+        }
+    }
+
+    @Override
+    public float getTranslation(View v) {
+        if (v instanceof ExpandableNotificationRow) {
+            return ((ExpandableNotificationRow) v).getTranslation();
+        }
+        else {
+            Log.wtf(TAG, "getTranslation should only be called on an ExpandableNotificationRow.");
+            return 0f;
+        }
+    }
+
+    @Override
+    public boolean swipedFastEnough(float translation, float viewSize) {
+        return swipedFastEnough();
+    }
+
+    @Override
+    @VisibleForTesting
+    protected boolean swipedFastEnough() {
+        return super.swipedFastEnough();
+    }
+
+    @Override
+    public boolean swipedFarEnough(float translation, float viewSize) {
+        return swipedFarEnough();
+    }
+
+    @Override
+    @VisibleForTesting
+    protected boolean swipedFarEnough() {
+        return super.swipedFarEnough();
+    }
+
+    @Override
+    public void dismiss(View animView, float velocity) {
+        dismissChild(animView, velocity,
+                !swipedFastEnough() /* useAccelerateInterpolator */);
+    }
+
+    @Override
+    public void snapOpen(View animView, int targetLeft, float velocity) {
+        snapChild(animView, targetLeft, velocity);
+    }
+
+    @VisibleForTesting
+    protected void snapClosed(View animView, float velocity) {
+        snapChild(animView, 0, velocity);
+    }
+
+    @Override
+    @VisibleForTesting
+    protected float getEscapeVelocity() {
+        return super.getEscapeVelocity();
+    }
+
+    @Override
+    public float getMinDismissVelocity() {
+        return getEscapeVelocity();
+    }
+
+    public void onMenuShown(View animView) {
+        setExposedMenuView(getTranslatingParentView());
+        mCallback.onDragCancelled(animView);
+        Handler handler = getHandler();
+
+        // If we're on the lockscreen we want to false this.
+        if (mCallback.isAntiFalsingNeeded()) {
+            handler.removeCallbacks(getFalsingCheck());
+            handler.postDelayed(getFalsingCheck(), COVER_MENU_DELAY);
+        }
+    }
+
+    @VisibleForTesting
+    protected boolean shouldResetMenu(boolean force) {
+        if (mMenuExposedView == null
+                || (!force && mMenuExposedView == mTranslatingParentView)) {
+            // If no menu is showing or it's showing for this view we do nothing.
+            return false;
+        }
+        return true;
+    }
+
+    public void resetExposedMenuView(boolean animate, boolean force) {
+        if (!shouldResetMenu(force)) {
+            return;
+        }
+        final View prevMenuExposedView = getExposedMenuView();
+        if (animate) {
+            Animator anim = getViewTranslationAnimator(prevMenuExposedView,
+                    0 /* leftTarget */, null /* updateListener */);
+            if (anim != null) {
+                anim.start();
+            }
+        } else if (prevMenuExposedView instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = (ExpandableNotificationRow) prevMenuExposedView;
+            if (!row.isRemoved()) {
+                row.resetTranslation();
+            }
+        }
+        clearExposedMenuView();
+    }
+
+    public static boolean isTouchInView(MotionEvent ev, View view) {
+        if (view == null) {
+            return false;
+        }
+        final int height = (view instanceof ExpandableView)
+                ? ((ExpandableView) view).getActualHeight()
+                : view.getHeight();
+        final int rx = (int) ev.getRawX();
+        final int ry = (int) ev.getRawY();
+        int[] temp = new int[2];
+        view.getLocationOnScreen(temp);
+        final int x = temp[0];
+        final int y = temp[1];
+        Rect rect = new Rect(x, y, x + view.getWidth(), y + height);
+        boolean ret = rect.contains(rx, ry);
+        return ret;
+    }
+
+    public interface NotificationCallback extends SwipeHelper.Callback{
+        boolean isExpanded();
+
+        void handleChildViewDismissed(View view);
+
+        void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption);
+
+        void onDismiss();
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
new file mode 100644
index 0000000..b5f67c06
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at * *      http://www.apache.org/licenses/LICENSE-2.0 *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification.stack;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockitoSession;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.IPowerManager;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.service.notification.StatusBarNotification;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.MotionEvent;
+
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.NotificationMenuRow;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link NotificationSwipeHelper}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationSwipeHelperTest extends SysuiTestCase {
+
+    private NotificationSwipeHelper mSwipeHelper;
+    private NotificationSwipeHelper.NotificationCallback mCallback;
+    private NotificationMenuRowPlugin.OnMenuEventListener mListener;
+    private View mView;
+    private MotionEvent mEvent;
+    private NotificationMenuRowPlugin mMenuRow;
+    private Handler mHandler;
+    private ExpandableNotificationRow mNotificationRow;
+    private Runnable mFalsingCheck;
+
+    @Rule public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Before
+    @UiThreadTest
+    public void setUp() throws Exception {
+        mCallback = mock(NotificationSwipeHelper.NotificationCallback.class);
+        mListener = mock(NotificationMenuRowPlugin.OnMenuEventListener.class);
+        mSwipeHelper = spy(new NotificationSwipeHelper(SwipeHelper.X, mCallback, mContext, mListener));
+        mView = mock(View.class);
+        mEvent = mock(MotionEvent.class);
+        mMenuRow = mock(NotificationMenuRowPlugin.class);
+        mNotificationRow = mock(ExpandableNotificationRow.class);
+        mHandler = mock(Handler.class);
+        mFalsingCheck = mock(Runnable.class);
+    }
+
+    @Test
+    public void testSetExposedMenuView() {
+        assertEquals("intialized with null exposed menu view", null,
+                mSwipeHelper.getExposedMenuView());
+        mSwipeHelper.setExposedMenuView(mView);
+        assertEquals("swipe helper has correct exposedMenuView after setExposedMenuView to a view",
+                mView, mSwipeHelper.getExposedMenuView());
+        mSwipeHelper.setExposedMenuView(null);
+        assertEquals("swipe helper has null exposedMenuView after setExposedMenuView to null",
+                null, mSwipeHelper.getExposedMenuView());
+    }
+
+    @Test
+    public void testClearExposedMenuView() {
+        doNothing().when(mSwipeHelper).setExposedMenuView(mView);
+        mSwipeHelper.clearExposedMenuView();
+        verify(mSwipeHelper, times(1)).setExposedMenuView(null);
+    }
+
+    @Test
+    public void testGetTranslatingParentView() {
+        assertEquals("intialized with null translating parent view", null,
+                mSwipeHelper.getTranslatingParentView());
+        mSwipeHelper.setTranslatingParentView(mView);
+        assertEquals("has translating parent view after setTranslatingParentView with a view",
+                mView, mSwipeHelper.getTranslatingParentView());
+    }
+
+    @Test
+    public void testClearTranslatingParentView() {
+        doNothing().when(mSwipeHelper).setTranslatingParentView(null);
+        mSwipeHelper.clearTranslatingParentView();
+        verify(mSwipeHelper, times(1)).setTranslatingParentView(null);
+    }
+
+    @Test
+    public void testSetCurrentMenuRow() {
+        assertEquals("currentMenuRow initializes to null", null,
+                mSwipeHelper.getCurrentMenuRow());
+        mSwipeHelper.setCurrentMenuRow(mMenuRow);
+        assertEquals("currentMenuRow set correctly after setCurrentMenuRow", mMenuRow,
+                mSwipeHelper.getCurrentMenuRow());
+        mSwipeHelper.setCurrentMenuRow(null);
+        assertEquals("currentMenuRow set to null after setCurrentMenuRow to null",
+                null, mSwipeHelper.getCurrentMenuRow());
+    }
+
+    @Test
+    public void testClearCurrentMenuRow() {
+        doNothing().when(mSwipeHelper).setCurrentMenuRow(null);
+        mSwipeHelper.clearCurrentMenuRow();
+        verify(mSwipeHelper, times(1)).setCurrentMenuRow(null);
+    }
+
+    @Test
+    public void testOnDownUpdate_ExpandableNotificationRow() {
+        when(mSwipeHelper.getHandler()).thenReturn(mHandler);
+        when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck);
+        doNothing().when(mSwipeHelper).resetExposedMenuView(true, false);
+        doNothing().when(mSwipeHelper).clearCurrentMenuRow();
+        doNothing().when(mSwipeHelper).initializeRow(any());
+
+        mSwipeHelper.onDownUpdate(mNotificationRow, mEvent);
+
+        verify(mSwipeHelper, times(1)).clearCurrentMenuRow();
+        verify(mHandler, times(1)).removeCallbacks(mFalsingCheck);
+        verify(mSwipeHelper, times(1)).resetExposedMenuView(true, false);
+        verify(mSwipeHelper, times(1)).initializeRow(mNotificationRow);
+    }
+
+    @Test
+    public void testOnDownUpdate_notExpandableNotificationRow() {
+        when(mSwipeHelper.getHandler()).thenReturn(mHandler);
+        when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck);
+        doNothing().when(mSwipeHelper).resetExposedMenuView(true, false);
+        doNothing().when(mSwipeHelper).clearCurrentMenuRow();
+        doNothing().when(mSwipeHelper).initializeRow(any());
+
+        mSwipeHelper.onDownUpdate(mView, mEvent);
+
+        verify(mSwipeHelper, times(1)).clearCurrentMenuRow();
+        verify(mHandler, times(1)).removeCallbacks(mFalsingCheck);
+        verify(mSwipeHelper, times(1)).resetExposedMenuView(true, false);
+        verify(mSwipeHelper, times(0)).initializeRow(any());
+    }
+
+    @Test
+    public void testOnMoveUpdate_menuRow() {
+        when(mSwipeHelper.getCurrentMenuRow()).thenReturn(mMenuRow);
+        when(mSwipeHelper.getHandler()).thenReturn(mHandler);
+        when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck);
+
+        mSwipeHelper.onMoveUpdate(mView, mEvent, 0, 10);
+
+        verify(mHandler, times(1)).removeCallbacks(mFalsingCheck);
+        verify(mMenuRow, times(1)).onTouchMove(10);
+    }
+
+    @Test
+    public void testOnMoveUpdate_noMenuRow() {
+        when(mSwipeHelper.getHandler()).thenReturn(mHandler);
+        when(mSwipeHelper.getFalsingCheck()).thenReturn(mFalsingCheck);
+
+        mSwipeHelper.onMoveUpdate(mView, mEvent, 0, 10);
+
+        verify(mHandler, times(1)).removeCallbacks(mFalsingCheck);
+    }
+
+    @Test
+    public void testHandleUpEvent_noMenuRow() {
+        assertFalse("Menu row does not exist",
+                mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
+    }
+
+    @Test
+    public void testHandleUpEvent_menuRow() {
+        when(mSwipeHelper.getCurrentMenuRow()).thenReturn(mMenuRow);
+        doNothing().when(mSwipeHelper).handleMenuRowSwipe(mEvent, mView, 0, mMenuRow);
+
+        assertTrue("Menu row exists",
+                mSwipeHelper.handleUpEvent(mEvent, mView, 0, 0));
+        verify(mMenuRow, times(1)).onTouchEnd();
+        verify(mSwipeHelper, times(1)).handleMenuRowSwipe(mEvent, mView, 0, mMenuRow);
+    }
+
+    @Test
+    public void testDismissChild_notExpanded() {
+        when(mCallback.isExpanded()).thenReturn(false);
+        doNothing().when(mSwipeHelper).superDismissChild(mView, 0, false);
+        doNothing().when(mSwipeHelper).handleMenuCoveredOrDismissed();
+
+        mSwipeHelper.dismissChild(mView, 0, false);
+
+        verify(mSwipeHelper, times(1)).superDismissChild(mView, 0, false);
+        verify(mCallback, times(0)).handleChildViewDismissed(mView);
+        verify(mCallback, times(1)).onDismiss();
+        verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
+    }
+
+    @Test
+    public void testSnapchild_targetIsZero() {
+        doNothing().when(mSwipeHelper).superSnapChild(mView, 0, 0);
+        mSwipeHelper.snapChild(mView, 0, 0);
+
+        verify(mCallback, times(1)).onDragCancelled(mView);
+        verify(mSwipeHelper, times(1)).superSnapChild(mView, 0, 0);
+        verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
+    }
+
+
+    @Test
+    public void testSnapchild_targetNotZero() {
+        doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0);
+        mSwipeHelper.snapChild(mView, 10, 0);
+
+        verify(mCallback, times(1)).onDragCancelled(mView);
+        verify(mSwipeHelper, times(1)).superSnapChild(mView, 10, 0);
+        verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
+    }
+
+    @Test
+    public void testSnooze() {
+        StatusBarNotification sbn = mock(StatusBarNotification.class);
+        SnoozeOption snoozeOption = mock(SnoozeOption.class);
+        mSwipeHelper.snooze(sbn, snoozeOption);
+        verify(mCallback, times(1)).onSnooze(sbn, snoozeOption);
+    }
+
+    @Test
+    public void testGetViewTranslationAnimator_notExpandableNotificationRow() {
+        Animator animator = mock(Animator.class);
+        AnimatorUpdateListener listener = mock(AnimatorUpdateListener.class);
+        doReturn(animator).when(mSwipeHelper).superGetViewTranslationAnimator(mView, 0, listener);
+
+        assertEquals("returns the correct animator from super", animator,
+                mSwipeHelper.getViewTranslationAnimator(mView, 0, listener));
+
+        verify(mSwipeHelper, times(1)).superGetViewTranslationAnimator(mView, 0, listener);
+    }
+
+    @Test
+    public void testGetViewTranslationAnimator_expandableNotificationRow() {
+        Animator animator = mock(Animator.class);
+        AnimatorUpdateListener listener = mock(AnimatorUpdateListener.class);
+        doReturn(animator).when(mNotificationRow).getTranslateViewAnimator(0, listener);
+
+        assertEquals("returns the correct animator from super when view is an ENR", animator,
+                mSwipeHelper.getViewTranslationAnimator(mNotificationRow, 0, listener));
+
+        verify(mNotificationRow, times(1)).getTranslateViewAnimator(0, listener);
+    }
+
+    @Test
+    public void testSetTranslation() {
+        mSwipeHelper.setTranslation(mNotificationRow, 0);
+        verify(mNotificationRow, times(1)).setTranslation(0);
+    }
+
+    @Test
+    public void testGetTranslation() {
+        doReturn(30f).when(mNotificationRow).getTranslation();
+
+        assertEquals("Returns getTranslation for the ENR",
+                mSwipeHelper.getTranslation(mNotificationRow), 30f);
+
+        verify(mNotificationRow, times(1)).getTranslation();
+    }
+
+    @Test
+    public void testDismiss() {
+        doNothing().when(mSwipeHelper).dismissChild(mView, 0, true);
+        doReturn(false).when(mSwipeHelper).swipedFastEnough();
+
+        mSwipeHelper.dismiss(mView, 0);
+
+        verify(mSwipeHelper, times(1)).swipedFastEnough();
+        verify(mSwipeHelper, times(1)).dismissChild(mView, 0, true);
+    }
+
+    @Test
+    public void testSnapOpen() {
+        doNothing().when(mSwipeHelper).snapChild(mView, 30, 0);
+
+        mSwipeHelper.snapOpen(mView, 30, 0);
+
+        verify(mSwipeHelper, times(1)).snapChild(mView, 30, 0);
+    }
+
+    @Test
+    public void testSnapClosed() {
+        doNothing().when(mSwipeHelper).snapChild(mView, 0, 0);
+
+        mSwipeHelper.snapClosed(mView, 0);
+
+        verify(mSwipeHelper, times(1)).snapChild(mView, 0, 0);
+    }
+
+    @Test
+    public void testGetMinDismissVelocity() {
+        doReturn(30f).when(mSwipeHelper).getEscapeVelocity();
+
+        assertEquals("Returns getEscapeVelocity", 30f, mSwipeHelper.getMinDismissVelocity());
+    }
+
+    @Test
+    public void onMenuShown_noAntiFalsing() {
+        doNothing().when(mSwipeHelper).setExposedMenuView(mView);
+        doReturn(mView).when(mSwipeHelper).getTranslatingParentView();
+        doReturn(mHandler).when(mSwipeHelper).getHandler();
+        doReturn(false).when(mCallback).isAntiFalsingNeeded();
+        doReturn(mFalsingCheck).when(mSwipeHelper).getFalsingCheck();
+
+        mSwipeHelper.onMenuShown(mView);
+
+        verify(mSwipeHelper, times(1)).setExposedMenuView(mView);
+        verify(mCallback, times(1)).onDragCancelled(mView);
+        verify(mCallback, times(1)).isAntiFalsingNeeded();
+
+        verify(mHandler, times(0)).removeCallbacks(mFalsingCheck);
+        verify(mHandler, times(0)).postDelayed(mFalsingCheck, mSwipeHelper.COVER_MENU_DELAY);
+    }
+
+    @Test
+    public void onMenuShown_antiFalsing() {
+        doNothing().when(mSwipeHelper).setExposedMenuView(mView);
+        doReturn(mView).when(mSwipeHelper).getTranslatingParentView();
+        doReturn(mHandler).when(mSwipeHelper).getHandler();
+        doReturn(true).when(mCallback).isAntiFalsingNeeded();
+        doReturn(mFalsingCheck).when(mSwipeHelper).getFalsingCheck();
+
+        mSwipeHelper.onMenuShown(mView);
+
+        verify(mSwipeHelper, times(1)).setExposedMenuView(mView);
+        verify(mCallback, times(1)).onDragCancelled(mView);
+        verify(mCallback, times(1)).isAntiFalsingNeeded();
+
+        verify(mHandler, times(1)).removeCallbacks(mFalsingCheck);
+        verify(mHandler, times(1)).postDelayed(mFalsingCheck, mSwipeHelper.COVER_MENU_DELAY);
+    }
+
+    @Test
+    public void testResetExposedMenuView_noReset() {
+        doReturn(false).when(mSwipeHelper).shouldResetMenu(false);
+        doNothing().when(mSwipeHelper).clearExposedMenuView();
+
+        mSwipeHelper.resetExposedMenuView(false, false);
+
+        verify(mSwipeHelper, times(1)).shouldResetMenu(false);
+
+        // should not clear exposed menu row
+        verify(mSwipeHelper, times(0)).clearExposedMenuView();
+    }
+
+    @Test
+    public void testResetExposedMenuView_animate() {
+        Animator animator = mock(Animator.class);
+
+        doReturn(true).when(mSwipeHelper).shouldResetMenu(false);
+        doReturn(mNotificationRow).when(mSwipeHelper).getExposedMenuView();
+        doReturn(false).when(mNotificationRow).isRemoved();
+        doReturn(animator).when(mSwipeHelper).getViewTranslationAnimator(mNotificationRow, 0, null);
+        doNothing().when(mSwipeHelper).clearExposedMenuView();
+
+        mSwipeHelper.resetExposedMenuView(true, false);
+
+        verify(mSwipeHelper, times(1)).shouldResetMenu(false);
+
+        // should retrieve and start animator
+        verify(mSwipeHelper, times(1)).getViewTranslationAnimator(mNotificationRow, 0, null);
+        verify(animator, times(1)).start();
+
+        // should not reset translation on row directly
+        verify(mNotificationRow, times(0)).resetTranslation();
+
+        // should clear exposed menu row
+        verify(mSwipeHelper, times(1)).clearExposedMenuView();
+    }
+
+
+    @Test
+    public void testResetExposedMenuView_noAnimate() {
+        Animator animator = mock(Animator.class);
+
+        doReturn(true).when(mSwipeHelper).shouldResetMenu(false);
+        doReturn(mNotificationRow).when(mSwipeHelper).getExposedMenuView();
+        doReturn(false).when(mNotificationRow).isRemoved();
+        doReturn(animator).when(mSwipeHelper).getViewTranslationAnimator(mNotificationRow, 0, null);
+        doNothing().when(mSwipeHelper).clearExposedMenuView();
+
+        mSwipeHelper.resetExposedMenuView(false, false);
+
+        verify(mSwipeHelper, times(1)).shouldResetMenu(false);
+
+        // should not retrieve and start animator
+        verify(mSwipeHelper, times(0)).getViewTranslationAnimator(mNotificationRow, 0, null);
+        verify(animator, times(0)).start();
+
+        // should reset translation on row directly
+        verify(mNotificationRow, times(1)).resetTranslation();
+
+        // should clear exposed menu row
+        verify(mSwipeHelper, times(1)).clearExposedMenuView();
+    }
+
+    @Test
+    public void testIsTouchInView() {
+        assertEquals("returns false when view is null", false,
+                NotificationSwipeHelper.isTouchInView(mEvent, null));
+
+        doReturn(5f).when(mEvent).getRawX();
+        doReturn(10f).when(mEvent).getRawY();
+
+        doReturn(20).when(mView).getWidth();
+        doReturn(20).when(mView).getHeight();
+
+        Answer answer = (Answer) invocation -> {
+            int[] arr = invocation.getArgument(0);
+            arr[0] = 0;
+            arr[1] = 0;
+            return null;
+        };
+        doAnswer(answer).when(mView).getLocationOnScreen(any());
+
+        assertTrue("Touch is within the view",
+                mSwipeHelper.isTouchInView(mEvent, mView));
+
+        doReturn(50f).when(mEvent).getRawX();
+
+        assertFalse("Touch is not within the view",
+                mSwipeHelper.isTouchInView(mEvent, mView));
+    }
+
+    @Test
+    public void testIsTouchInView_expandable() {
+        assertEquals("returns false when view is null", false,
+                NotificationSwipeHelper.isTouchInView(mEvent, null));
+
+        doReturn(5f).when(mEvent).getRawX();
+        doReturn(10f).when(mEvent).getRawY();
+
+        doReturn(20).when(mNotificationRow).getWidth();
+        doReturn(20).when(mNotificationRow).getActualHeight();
+
+        Answer answer = (Answer) invocation -> {
+            int[] arr = invocation.getArgument(0);
+            arr[0] = 0;
+            arr[1] = 0;
+            return null;
+        };
+        doAnswer(answer).when(mNotificationRow).getLocationOnScreen(any());
+
+        assertTrue("Touch is within the view",
+                mSwipeHelper.isTouchInView(mEvent, mNotificationRow));
+
+        doReturn(50f).when(mEvent).getRawX();
+
+        assertFalse("Touch is not within the view",
+                mSwipeHelper.isTouchInView(mEvent, mNotificationRow));
+    }
+}