Clean up interfaces for StackScrollLayout.

Test: Automated tests should pass.
Change-Id: Ibf2718bfaacff0adcfca7ef8ca2f3eb2b867debf
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 0215fda..3fe9944 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -87,6 +87,8 @@
     private Runnable mWatchLongPress;
     private final long mLongPressTimeout;
 
+    protected boolean mSwipingInProgress;
+
     final private int[] mTmpPos = new int[2];
     private final int mFalsingThreshold;
     private boolean mTouchAboveFalsingThreshold;
@@ -127,6 +129,10 @@
         mDisableHwLayers = disableHwLayers;
     }
 
+    public boolean isSwipingInProgress() {
+        return mSwipingInProgress;
+    }
+
     private float getPos(MotionEvent ev) {
         return mSwipeDirection == X ? ev.getX() : ev.getY();
     }
@@ -318,6 +324,7 @@
                     if (Math.abs(delta) > mPagingTouchSlop
                             && Math.abs(delta) > Math.abs(deltaPerpendicular)) {
                         if (mCallback.canChildBeDragged(mCurrView)) {
+                            mSwipingInProgress = true;
                             mCallback.onBeginDrag(mCurrView);
                             mDragging = true;
                             mInitialTouchPos = getPos(ev);
@@ -437,6 +444,7 @@
                     wasRemoved = row.isRemoved();
                 }
                 if (!mCancelled || wasRemoved) {
+                    mSwipingInProgress = false;
                     mCallback.onChildDismissed(animView);
                 }
                 if (endAction != null) {
@@ -626,6 +634,7 @@
                                 !swipedFastEnough() /* useAccelerateInterpolator */);
                     } else {
                         // snappity
+                        mSwipingInProgress = false;
                         mCallback.onDragCancelled(mCurrView);
                         snapChild(mCurrView, 0 /* leftTarget */, velocity);
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
index 81208c4..53ebe74 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/VisibilityLocationProvider.java
@@ -24,7 +24,10 @@
 public interface VisibilityLocationProvider {
 
     /**
-     * @return whether the view is in a visible location right now.
+     * Returns whether an ExpandableNotificationRow is in a visible location or not.
+     *
+     * @param row
+     * @return true if row is in a visible location
      */
     boolean isInVisibleLocation(ExpandableNotificationRow row);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
index fa75c71..cfb6d99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
@@ -22,6 +22,7 @@
 import android.view.ViewGroup;
 
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.NotificationData;
@@ -31,7 +32,8 @@
  * Interface representing the entity that contains notifications. It can have
  * notification views added and removed from it, and will manage displaying them to the user.
  */
-public interface NotificationListContainer {
+public interface NotificationListContainer extends ExpandableView.OnHeightChangedListener,
+        VisibilityLocationProvider {
 
     /**
      * Called when a child is being transferred.
@@ -128,14 +130,6 @@
     ViewGroup getViewParentForNotification(NotificationData.Entry entry);
 
     /**
-     * Called when the height of an expandable view changes.
-     *
-     * @param view view whose height changed
-     * @param animate whether this change should be animated
-     */
-    void onHeightChanged(ExpandableView view, boolean animate);
-
-    /**
      * Resets the currently exposed menu view.
      *
      * @param animate whether to animate the closing/change of menu view
@@ -158,13 +152,6 @@
      */
     void cleanUpViewState(View view);
 
-    /**
-     * Returns whether an ExpandableNotificationRow is in a visible location or not.
-     *
-     * @param row
-     * @return true if row is in a visible location
-     */
-    boolean isInVisibleLocation(ExpandableNotificationRow row);
 
     /**
      * Sets a listener to listen for changes in notification locations.
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 bac42ff..0bc54a3 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
@@ -25,6 +25,8 @@
 import android.animation.PropertyValuesHolder;
 import android.animation.TimeAnimator;
 import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.WallpaperManager;
 import android.content.Context;
@@ -43,10 +45,6 @@
 import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.graphics.ColorUtils;
-
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -69,6 +67,8 @@
 import android.widget.OverScroller;
 import android.widget.ScrollView;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.ColorUtils;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.statusbar.IStatusBarService;
@@ -84,6 +84,7 @@
 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;
@@ -117,7 +118,7 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
-import com.android.systemui.statusbar.phone.HeadsUpManagerPhone.AnimationStateHandler;
+import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.NotificationGroupManager.OnGroupChangeListener;
@@ -142,10 +143,8 @@
 /**
  * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
  */
-public class NotificationStackScrollLayout extends ViewGroup
-        implements ExpandHelper.Callback, ScrollAdapter, OnHeightChangedListener,
-        OnGroupChangeListener, VisibilityLocationProvider, NotificationListContainer,
-        ConfigurationListener, DragDownCallback, AnimationStateHandler, Dumpable {
+public class NotificationStackScrollLayout extends ViewGroup implements ScrollAdapter,
+        NotificationListContainer, ConfigurationListener, Dumpable {
 
     public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
     private static final String TAG = "StackScroller";
@@ -160,7 +159,6 @@
 
     private ExpandHelper mExpandHelper;
     private final NotificationSwipeHelper mSwipeHelper;
-    private boolean mSwipingInProgress;
     private int mCurrentStackHeight = Integer.MAX_VALUE;
     private final Paint mBackgroundPaint = new Paint();
     private final boolean mShouldDrawNotificationBackground;
@@ -344,7 +342,7 @@
     private float mDimAmount;
     private ValueAnimator mDimAnimator;
     private ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
-    private Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
+    private final Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
         @Override
         public void onAnimationEnd(Animator animation) {
             mDimAnimator = null;
@@ -485,12 +483,12 @@
         mBgColor = context.getColor(R.color.notification_shade_background_color);
         int minHeight = res.getDimensionPixelSize(R.dimen.notification_min_height);
         int maxHeight = res.getDimensionPixelSize(R.dimen.notification_max_height);
-        mExpandHelper = new ExpandHelper(getContext(), this,
+        mExpandHelper = new ExpandHelper(getContext(), mExpandHelperCallback,
                 minHeight, maxHeight);
         mExpandHelper.setEventSource(this);
         mExpandHelper.setScrollAdapter(this);
-        mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, new SwipeHelperCallback(),
-                getContext(), new NotificationMenuListener());
+        mSwipeHelper = new NotificationSwipeHelper(SwipeHelper.X, mNotificationCallback,
+                getContext(), mMenuEventListener);
         mStackScrollAlgorithm = createStackScrollAlgorithm(context);
         initView(context);
         mFalsingManager = FalsingManager.getInstance(context);
@@ -530,7 +528,7 @@
 
         inflateEmptyShadeView();
         inflateFooterView();
-        mVisualStabilityManager.setVisibilityLocationProvider(this);
+        mVisualStabilityManager.setVisibilityLocationProvider(this::isInVisibleLocation);
         setLongPressListener(mEntryManager.getNotificationLongClicker());
     }
 
@@ -589,7 +587,7 @@
         return false;
     }
 
-  @ShadeViewRefactor(RefactorComponent.INPUT)
+  @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
   public RemoteInputController.Delegate createDelegate() {
         return new RemoteInputController.Delegate() {
             public void setRemoteInputActive(NotificationData.Entry entry,
@@ -628,7 +626,7 @@
     }
 
     @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public NotificationSwipeActionHelper getSwipeActionHelper() {
         return mSwipeHelper;
     }
@@ -1245,11 +1243,6 @@
         return firstChild != null ? firstChild.getMinHeight() : mCollapsedSize;
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void setLongPressListener(ExpandableNotificationRow.LongPressListener listener) {
-        mLongPressListener = listener;
-    }
-
     @ShadeViewRefactor(RefactorComponent.ADAPTER)
     public void setQsContainer(ViewGroup qsContainer) {
         mQsContainer = qsContainer;
@@ -1273,7 +1266,7 @@
         return false;
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     public ExpandableView getClosestChildAtRawPosition(float touchX, float touchY) {
         getLocationOnScreen(mTempInt2);
         float localTouchY = touchY - mTempInt2[1];
@@ -1303,16 +1296,8 @@
         return closestChild;
     }
 
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
-        getLocationOnScreen(mTempInt2);
-        return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public ExpandableView getChildAtPosition(float touchX, float touchY) {
+    @ShadeViewRefactor(RefactorComponent.COORDINATOR)
+    private ExpandableView getChildAtPosition(float touchX, float touchY) {
         return getChildAtPosition(touchX, touchY, true /* requireMinHeight */);
 
     }
@@ -1325,7 +1310,7 @@
      * @param requireMinHeight Whether a minimum height is required for a child to be returned.
      * @return the child at the given location.
      */
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     private ExpandableView getChildAtPosition(float touchX, float touchY,
             boolean requireMinHeight) {
         // find the view under the pointer, accounting for GONE views
@@ -1365,71 +1350,9 @@
         return null;
     }
 
-    @Override
-    @ShadeViewRefactor(RefactorComponent.ADAPTER)
-    public boolean canChildBeExpanded(View v) {
-        return v instanceof ExpandableNotificationRow
-                && ((ExpandableNotificationRow) v).isExpandable()
-                && !((ExpandableNotificationRow) v).areGutsExposed()
-                && (mIsExpanded || !((ExpandableNotificationRow) v).isPinned());
-    }
-
-    /* Only ever called as a consequence of an expansion gesture in the shade. */
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void setUserExpandedChild(View v, boolean userExpanded) {
-        if (v instanceof ExpandableNotificationRow) {
-            ExpandableNotificationRow row = (ExpandableNotificationRow) v;
-            if (userExpanded && onKeyguard()) {
-                // Due to a race when locking the screen while touching, a notification may be
-                // expanded even after we went back to keyguard. An example of this happens if
-                // you click in the empty space while expanding a group.
-
-                // We also need to un-user lock it here, since otherwise the content height
-                // calculated might be wrong. We also can't invert the two calls since
-                // un-userlocking it will trigger a layout switch in the content view.
-                row.setUserLocked(false);
-                updateContentHeight();
-                notifyHeightChangeListener(row);
-                return;
-            }
-            row.setUserExpanded(userExpanded, true /* allowChildrenExpansion */);
-            row.onExpandedByGesture(userExpanded);
-        }
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void setExpansionCancelled(View v) {
-        if (v instanceof ExpandableNotificationRow) {
-            ((ExpandableNotificationRow) v).setGroupExpansionChanging(false);
-        }
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void setUserLockedChild(View v, boolean userLocked) {
-        if (v instanceof ExpandableNotificationRow) {
-            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
-        }
-        cancelLongPress();
-        requestDisallowInterceptTouchEvent(true);
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
-    public void expansionStateChanged(boolean isExpanding) {
-        mExpandingNotification = isExpanding;
-        if (!mExpandedInThisMotion) {
-            mMaxScrollAfterExpand = mOwnScrollY;
-            mExpandedInThisMotion = true;
-        }
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    public int getMaxExpandHeight(ExpandableView view) {
-        return view.getMaxContentHeight();
+    private ExpandableView getChildAtRawPosition(float touchX, float touchY) {
+        getLocationOnScreen(mTempInt2);
+        return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -1526,14 +1449,6 @@
         return mStatusBarState == StatusBarState.KEYGUARD;
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void setSwipingInProgress(boolean isSwiped) {
-        mSwipingInProgress = isSwiped;
-        if (isSwiped) {
-            requestDisallowInterceptTouchEvent(true);
-        }
-    }
-
     @Override
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     protected void onConfigurationChanged(Configuration newConfig) {
@@ -1567,249 +1482,6 @@
         return this;
     }
 
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean onTouchEvent(MotionEvent ev) {
-        boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
-                || ev.getActionMasked() == MotionEvent.ACTION_UP;
-        handleEmptySpaceClick(ev);
-        boolean expandWantsIt = false;
-        if (mIsExpanded && !mSwipingInProgress && !mOnlyScrollingInThisMotion) {
-            if (isCancelOrUp) {
-                mExpandHelper.onlyObserveMovements(false);
-            }
-            boolean wasExpandingBefore = mExpandingNotification;
-            expandWantsIt = mExpandHelper.onTouchEvent(ev);
-            if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore
-                    && !mDisallowScrollingInThisMotion) {
-                dispatchDownEventToScroller(ev);
-            }
-        }
-        boolean scrollerWantsIt = false;
-        if (mIsExpanded && !mSwipingInProgress && !mExpandingNotification
-                && !mDisallowScrollingInThisMotion) {
-            scrollerWantsIt = onScrollTouch(ev);
-        }
-        boolean horizontalSwipeWantsIt = false;
-        if (!mIsBeingDragged
-                && !mExpandingNotification
-                && !mExpandedInThisMotion
-                && !mOnlyScrollingInThisMotion
-                && !mDisallowDismissInThisMotion) {
-            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
-        }
-
-        // Check if we need to clear any snooze leavebehinds
-        NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
-        if (guts != null && !NotificationSwipeHelper.isTouchInView(ev, guts)
-                && guts.getGutsContent() instanceof NotificationSnooze) {
-            NotificationSnooze ns = (NotificationSnooze) guts.getGutsContent();
-            if ((ns.isExpanded() && isCancelOrUp)
-                    || (!horizontalSwipeWantsIt && scrollerWantsIt)) {
-                // If the leavebehind is expanded we clear it on the next up event, otherwise we
-                // clear it on the next non-horizontal swipe or expand event.
-                checkSnoozeLeavebehind();
-            }
-        }
-        if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
-            mCheckForLeavebehind = true;
-        }
-        return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void dispatchDownEventToScroller(MotionEvent ev) {
-        MotionEvent downEvent = MotionEvent.obtain(ev);
-        downEvent.setAction(MotionEvent.ACTION_DOWN);
-        onScrollTouch(downEvent);
-        downEvent.recycle();
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean onGenericMotionEvent(MotionEvent event) {
-        if (!isScrollingEnabled() || !mIsExpanded || mSwipingInProgress || mExpandingNotification
-                || mDisallowScrollingInThisMotion) {
-            return false;
-        }
-        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
-            switch (event.getAction()) {
-                case MotionEvent.ACTION_SCROLL: {
-                    if (!mIsBeingDragged) {
-                        final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
-                        if (vscroll != 0) {
-                            final int delta = (int) (vscroll * getVerticalScrollFactor());
-                            final int range = getScrollRange();
-                            int oldScrollY = mOwnScrollY;
-                            int newScrollY = oldScrollY - delta;
-                            if (newScrollY < 0) {
-                                newScrollY = 0;
-                            } else if (newScrollY > range) {
-                                newScrollY = range;
-                            }
-                            if (newScrollY != oldScrollY) {
-                                setOwnScrollY(newScrollY);
-                                return true;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        return super.onGenericMotionEvent(event);
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private boolean onScrollTouch(MotionEvent ev) {
-        if (!isScrollingEnabled()) {
-            return false;
-        }
-        if (isInsideQsContainer(ev) && !mIsBeingDragged) {
-            return false;
-        }
-        mForcedScroll = null;
-        initVelocityTrackerIfNotExists();
-        mVelocityTracker.addMovement(ev);
-
-        final int action = ev.getAction();
-
-        switch (action & MotionEvent.ACTION_MASK) {
-            case MotionEvent.ACTION_DOWN: {
-                if (getChildCount() == 0 || !isInContentBounds(ev)) {
-                    return false;
-                }
-                boolean isBeingDragged = !mScroller.isFinished();
-                setIsBeingDragged(isBeingDragged);
-                /*
-                 * If being flinged and user touches, stop the fling. isFinished
-                 * will be false if being flinged.
-                 */
-                if (!mScroller.isFinished()) {
-                    mScroller.forceFinished(true);
-                }
-
-                // Remember where the motion event started
-                mLastMotionY = (int) ev.getY();
-                mDownX = (int) ev.getX();
-                mActivePointerId = ev.getPointerId(0);
-                break;
-            }
-            case MotionEvent.ACTION_MOVE:
-                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
-                if (activePointerIndex == -1) {
-                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
-                    break;
-                }
-
-                final int y = (int) ev.getY(activePointerIndex);
-                final int x = (int) ev.getX(activePointerIndex);
-                int deltaY = mLastMotionY - y;
-                final int xDiff = Math.abs(x - mDownX);
-                final int yDiff = Math.abs(deltaY);
-                if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) {
-                    setIsBeingDragged(true);
-                    if (deltaY > 0) {
-                        deltaY -= mTouchSlop;
-                    } else {
-                        deltaY += mTouchSlop;
-                    }
-                }
-                if (mIsBeingDragged) {
-                    // Scroll to follow the motion event
-                    mLastMotionY = y;
-                    int range = getScrollRange();
-                    if (mExpandedInThisMotion) {
-                        range = Math.min(range, mMaxScrollAfterExpand);
-                    }
-
-                    float scrollAmount;
-                    if (deltaY < 0) {
-                        scrollAmount = overScrollDown(deltaY);
-                    } else {
-                        scrollAmount = overScrollUp(deltaY, range);
-                    }
-
-                    // Calling customOverScrollBy will call onCustomOverScrolled, which
-                    // sets the scrolling if applicable.
-                    if (scrollAmount != 0.0f) {
-                        // The scrolling motion could not be compensated with the
-                        // existing overScroll, we have to scroll the view
-                        customOverScrollBy((int) scrollAmount, mOwnScrollY,
-                                range, getHeight() / 2);
-                        // If we're scrolling, leavebehinds should be dismissed
-                        checkSnoozeLeavebehind();
-                    }
-                }
-                break;
-            case MotionEvent.ACTION_UP:
-                if (mIsBeingDragged) {
-                    final VelocityTracker velocityTracker = mVelocityTracker;
-                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
-
-                    if (shouldOverScrollFling(initialVelocity)) {
-                        onOverScrollFling(true, initialVelocity);
-                    } else {
-                        if (getChildCount() > 0) {
-                            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
-                                float currentOverScrollTop = getCurrentOverScrollAmount(true);
-                                if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
-                                    fling(-initialVelocity);
-                                } else {
-                                    onOverScrollFling(false, initialVelocity);
-                                }
-                            } else {
-                                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
-                                        getScrollRange())) {
-                                    animateScroll();
-                                }
-                            }
-                        }
-                    }
-                    mActivePointerId = INVALID_POINTER;
-                    endDrag();
-                }
-
-                break;
-            case MotionEvent.ACTION_CANCEL:
-                if (mIsBeingDragged && getChildCount() > 0) {
-                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
-                        animateScroll();
-                    }
-                    mActivePointerId = INVALID_POINTER;
-                    endDrag();
-                }
-                break;
-            case MotionEvent.ACTION_POINTER_DOWN: {
-                final int index = ev.getActionIndex();
-                mLastMotionY = (int) ev.getY(index);
-                mDownX = (int) ev.getX(index);
-                mActivePointerId = ev.getPointerId(index);
-                break;
-            }
-            case MotionEvent.ACTION_POINTER_UP:
-                onSecondaryPointerUp(ev);
-                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
-                mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
-                break;
-        }
-        return true;
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    protected boolean isInsideQsContainer(MotionEvent ev) {
-        return ev.getY() < mQsContainer.getBottom();
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void onOverScrollFling(boolean open, int initialVelocity) {
-        if (mOverscrollTopChangedListener != null) {
-            mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
-        }
-        mDontReportNextOverScroll = true;
-        setOverScrollAmount(0.0f, true, false);
-    }
-
     /**
      * Perform a scroll upwards and adapt the overscroll amounts accordingly
      *
@@ -1817,7 +1489,7 @@
      * @return The amount of scrolling to be performed by the scroller,
      * not handled by the overScroll amount.
      */
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private float overScrollUp(int deltaY, int range) {
         deltaY = Math.max(deltaY, 0);
         float currentTopAmount = getCurrentOverScrollAmount(true);
@@ -1876,24 +1548,6 @@
         return scrollAmount;
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void onSecondaryPointerUp(MotionEvent ev) {
-        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
-                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-        final int pointerId = ev.getPointerId(pointerIndex);
-        if (pointerId == mActivePointerId) {
-            // This was our active pointer going up. Choose a new
-            // active pointer and adjust accordingly.
-            // TODO: Make this decision more intelligent.
-            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
-            mLastMotionY = (int) ev.getY(newPointerIndex);
-            mActivePointerId = ev.getPointerId(newPointerIndex);
-            if (mVelocityTracker != null) {
-                mVelocityTracker.clear();
-            }
-        }
-    }
-
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private void initVelocityTrackerIfNotExists() {
         if (mVelocityTracker == null) {
@@ -2636,7 +2290,7 @@
      *                  numbers mean that the finger/cursor is moving down the screen,
      *                  which means we want to scroll towards the top.
      */
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     protected void fling(int velocityY) {
         if (getChildCount() > 0) {
             int scrollRange = getScrollRange();
@@ -2674,7 +2328,7 @@
      * @return Whether a fling performed on the top overscroll edge lead to the expanded
      * overScroll view (i.e QS).
      */
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private boolean shouldOverScrollFling(int initialVelocity) {
         float topOverScroll = getCurrentOverScrollAmount(true);
         return mScrolledToTopOnFirstDown
@@ -2757,7 +2411,7 @@
         return Math.max(desiredPadding, mIntrinsicPadding);
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private float getRubberBandFactor(boolean onTop) {
         if (!onTop) {
             return RUBBER_BAND_FACTOR_NORMAL;
@@ -2777,99 +2431,13 @@
      * rubberbanded, false if it is technically an overscroll but rather a motion to expand the
      * overscroll view (e.g. expand QS).
      */
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private boolean isRubberbanded(boolean onTop) {
         return !onTop || mExpandedInThisMotion || mIsExpansionChanging || mPanelTracking
                 || !mScrolledToTopOnFirstDown;
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void endDrag() {
-        setIsBeingDragged(false);
 
-        recycleVelocityTracker();
-
-        if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
-            setOverScrollAmount(0, true /* onTop */, true /* animate */);
-        }
-        if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
-            setOverScrollAmount(0, false /* onTop */, true /* animate */);
-        }
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) {
-        ev.offsetLocation(sourceView.getX(), sourceView.getY());
-        ev.offsetLocation(-targetView.getX(), -targetView.getY());
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        initDownStates(ev);
-        handleEmptySpaceClick(ev);
-        boolean expandWantsIt = false;
-        if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) {
-            expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
-        }
-        boolean scrollWantsIt = false;
-        if (!mSwipingInProgress && !mExpandingNotification) {
-            scrollWantsIt = onInterceptTouchEventScroll(ev);
-        }
-        boolean swipeWantsIt = false;
-        if (!mIsBeingDragged
-                && !mExpandingNotification
-                && !mExpandedInThisMotion
-                && !mOnlyScrollingInThisMotion
-                && !mDisallowDismissInThisMotion) {
-            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
-        }
-        // Check if we need to clear any snooze leavebehinds
-        boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP;
-        NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
-        if (!NotificationSwipeHelper.isTouchInView(ev, guts) && isUp && !swipeWantsIt &&
-                !expandWantsIt && !scrollWantsIt) {
-            mCheckForLeavebehind = false;
-            mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */,
-                    false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */,
-                    false /* resetMenu */);
-        }
-        if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
-            mCheckForLeavebehind = true;
-        }
-        return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev);
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void handleEmptySpaceClick(MotionEvent ev) {
-        switch (ev.getActionMasked()) {
-            case MotionEvent.ACTION_MOVE:
-                if (mTouchIsClick && (Math.abs(ev.getY() - mInitialTouchY) > mTouchSlop
-                        || Math.abs(ev.getX() - mInitialTouchX) > mTouchSlop)) {
-                    mTouchIsClick = false;
-                }
-                break;
-            case MotionEvent.ACTION_UP:
-                if (mStatusBarState != StatusBarState.KEYGUARD && mTouchIsClick &&
-                        isBelowLastNotification(mInitialTouchX, mInitialTouchY)) {
-                    mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY);
-                }
-                break;
-        }
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    private void initDownStates(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mExpandedInThisMotion = false;
-            mOnlyScrollingInThisMotion = !mScroller.isFinished();
-            mDisallowScrollingInThisMotion = false;
-            mDisallowDismissInThisMotion = false;
-            mTouchIsClick = true;
-            mInitialTouchX = ev.getX();
-            mInitialTouchY = ev.getY();
-        }
-    }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setChildTransferInProgress(boolean childTransferInProgress) {
@@ -2896,15 +2464,6 @@
         mCurrentStackScrollState.removeViewStateForView(child);
     }
 
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-        super.requestDisallowInterceptTouchEvent(disallowIntercept);
-        if (disallowIntercept) {
-            cancelLongPress();
-        }
-    }
-
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     private void onViewRemovedInternal(View child, ViewGroup container) {
         if (mChangePositionInProgress) {
@@ -3600,6 +3159,385 @@
         mGoToFullShadeNeedsAnimation = false;
     }
 
+    @ShadeViewRefactor(RefactorComponent.LAYOUT_ALGORITHM)
+    protected StackScrollAlgorithm createStackScrollAlgorithm(Context context) {
+        return new StackScrollAlgorithm(context);
+    }
+
+    /**
+     * @return Whether a y coordinate is inside the content.
+     */
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    public boolean isInContentBounds(float y) {
+        return y < getHeight() - getEmptyBottomMargin();
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void setLongPressListener(ExpandableNotificationRow.LongPressListener listener) {
+        mLongPressListener = listener;
+    }
+
+    @Override
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
+                || ev.getActionMasked() == MotionEvent.ACTION_UP;
+        handleEmptySpaceClick(ev);
+        boolean expandWantsIt = false;
+        boolean swipingInProgress = mSwipeHelper.isSwipingInProgress();
+        if (mIsExpanded && !swipingInProgress && !mOnlyScrollingInThisMotion) {
+            if (isCancelOrUp) {
+                mExpandHelper.onlyObserveMovements(false);
+            }
+            boolean wasExpandingBefore = mExpandingNotification;
+            expandWantsIt = mExpandHelper.onTouchEvent(ev);
+            if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore
+                    && !mDisallowScrollingInThisMotion) {
+                dispatchDownEventToScroller(ev);
+            }
+        }
+        boolean scrollerWantsIt = false;
+        if (mIsExpanded && !swipingInProgress && !mExpandingNotification
+                && !mDisallowScrollingInThisMotion) {
+            scrollerWantsIt = onScrollTouch(ev);
+        }
+        boolean horizontalSwipeWantsIt = false;
+        if (!mIsBeingDragged
+                && !mExpandingNotification
+                && !mExpandedInThisMotion
+                && !mOnlyScrollingInThisMotion
+                && !mDisallowDismissInThisMotion) {
+            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+        }
+
+        // Check if we need to clear any snooze leavebehinds
+        NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
+        if (guts != null && !NotificationSwipeHelper.isTouchInView(ev, guts)
+                && guts.getGutsContent() instanceof NotificationSnooze) {
+            NotificationSnooze ns = (NotificationSnooze) guts.getGutsContent();
+            if ((ns.isExpanded() && isCancelOrUp)
+                    || (!horizontalSwipeWantsIt && scrollerWantsIt)) {
+                // If the leavebehind is expanded we clear it on the next up event, otherwise we
+                // clear it on the next non-horizontal swipe or expand event.
+                checkSnoozeLeavebehind();
+            }
+        }
+        if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+            mCheckForLeavebehind = true;
+        }
+        return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private void dispatchDownEventToScroller(MotionEvent ev) {
+        MotionEvent downEvent = MotionEvent.obtain(ev);
+        downEvent.setAction(MotionEvent.ACTION_DOWN);
+        onScrollTouch(downEvent);
+        downEvent.recycle();
+    }
+
+    @Override
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if (!isScrollingEnabled() || !mIsExpanded || mSwipeHelper.isSwipingInProgress() || mExpandingNotification
+                || mDisallowScrollingInThisMotion) {
+            return false;
+        }
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    if (!mIsBeingDragged) {
+                        final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        if (vscroll != 0) {
+                            final int delta = (int) (vscroll * getVerticalScrollFactor());
+                            final int range = getScrollRange();
+                            int oldScrollY = mOwnScrollY;
+                            int newScrollY = oldScrollY - delta;
+                            if (newScrollY < 0) {
+                                newScrollY = 0;
+                            } else if (newScrollY > range) {
+                                newScrollY = range;
+                            }
+                            if (newScrollY != oldScrollY) {
+                                setOwnScrollY(newScrollY);
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private boolean onScrollTouch(MotionEvent ev) {
+        if (!isScrollingEnabled()) {
+            return false;
+        }
+        if (isInsideQsContainer(ev) && !mIsBeingDragged) {
+            return false;
+        }
+        mForcedScroll = null;
+        initVelocityTrackerIfNotExists();
+        mVelocityTracker.addMovement(ev);
+
+        final int action = ev.getAction();
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                if (getChildCount() == 0 || !isInContentBounds(ev)) {
+                    return false;
+                }
+                boolean isBeingDragged = !mScroller.isFinished();
+                setIsBeingDragged(isBeingDragged);
+                /*
+                 * If being flinged and user touches, stop the fling. isFinished
+                 * will be false if being flinged.
+                 */
+                if (!mScroller.isFinished()) {
+                    mScroller.forceFinished(true);
+                }
+
+                // Remember where the motion event started
+                mLastMotionY = (int) ev.getY();
+                mDownX = (int) ev.getX();
+                mActivePointerId = ev.getPointerId(0);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (activePointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+                    break;
+                }
+
+                final int y = (int) ev.getY(activePointerIndex);
+                final int x = (int) ev.getX(activePointerIndex);
+                int deltaY = mLastMotionY - y;
+                final int xDiff = Math.abs(x - mDownX);
+                final int yDiff = Math.abs(deltaY);
+                if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) {
+                    setIsBeingDragged(true);
+                    if (deltaY > 0) {
+                        deltaY -= mTouchSlop;
+                    } else {
+                        deltaY += mTouchSlop;
+                    }
+                }
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    mLastMotionY = y;
+                    int range = getScrollRange();
+                    if (mExpandedInThisMotion) {
+                        range = Math.min(range, mMaxScrollAfterExpand);
+                    }
+
+                    float scrollAmount;
+                    if (deltaY < 0) {
+                        scrollAmount = overScrollDown(deltaY);
+                    } else {
+                        scrollAmount = overScrollUp(deltaY, range);
+                    }
+
+                    // Calling customOverScrollBy will call onCustomOverScrolled, which
+                    // sets the scrolling if applicable.
+                    if (scrollAmount != 0.0f) {
+                        // The scrolling motion could not be compensated with the
+                        // existing overScroll, we have to scroll the view
+                        customOverScrollBy((int) scrollAmount, mOwnScrollY,
+                                range, getHeight() / 2);
+                        // If we're scrolling, leavebehinds should be dismissed
+                        checkSnoozeLeavebehind();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+                    if (shouldOverScrollFling(initialVelocity)) {
+                        onOverScrollFling(true, initialVelocity);
+                    } else {
+                        if (getChildCount() > 0) {
+                            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                                float currentOverScrollTop = getCurrentOverScrollAmount(true);
+                                if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
+                                    fling(-initialVelocity);
+                                } else {
+                                    onOverScrollFling(false, initialVelocity);
+                                }
+                            } else {
+                                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
+                                        getScrollRange())) {
+                                    animateScroll();
+                                }
+                            }
+                        }
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsBeingDragged && getChildCount() > 0) {
+                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+                        animateScroll();
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int index = ev.getActionIndex();
+                mLastMotionY = (int) ev.getY(index);
+                mDownX = (int) ev.getX(index);
+                mActivePointerId = ev.getPointerId(index);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
+                mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
+                break;
+        }
+        return true;
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    protected boolean isInsideQsContainer(MotionEvent ev) {
+        return ev.getY() < mQsContainer.getBottom();
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private void onOverScrollFling(boolean open, int initialVelocity) {
+        if (mOverscrollTopChangedListener != null) {
+            mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
+        }
+        mDontReportNextOverScroll = true;
+        setOverScrollAmount(0.0f, true, false);
+    }
+
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionY = (int) ev.getY(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private void endDrag() {
+        setIsBeingDragged(false);
+
+        recycleVelocityTracker();
+
+        if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
+            setOverScrollAmount(0, true /* onTop */, true /* animate */);
+        }
+        if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
+            setOverScrollAmount(0, false /* onTop */, true /* animate */);
+        }
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) {
+        ev.offsetLocation(sourceView.getX(), sourceView.getY());
+        ev.offsetLocation(-targetView.getX(), -targetView.getY());
+    }
+
+    @Override
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        initDownStates(ev);
+        handleEmptySpaceClick(ev);
+        boolean expandWantsIt = false;
+        boolean swipingInProgress = mSwipeHelper.isSwipingInProgress();
+        if (!swipingInProgress && !mOnlyScrollingInThisMotion) {
+            expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
+        }
+        boolean scrollWantsIt = false;
+        if (!swipingInProgress && !mExpandingNotification) {
+            scrollWantsIt = onInterceptTouchEventScroll(ev);
+        }
+        boolean swipeWantsIt = false;
+        if (!mIsBeingDragged
+                && !mExpandingNotification
+                && !mExpandedInThisMotion
+                && !mOnlyScrollingInThisMotion
+                && !mDisallowDismissInThisMotion) {
+            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
+        }
+        // Check if we need to clear any snooze leavebehinds
+        boolean isUp = ev.getActionMasked() == MotionEvent.ACTION_UP;
+        NotificationGuts guts = mNotificationGutsManager.getExposedGuts();
+        if (!NotificationSwipeHelper.isTouchInView(ev, guts) && isUp && !swipeWantsIt &&
+                !expandWantsIt && !scrollWantsIt) {
+            mCheckForLeavebehind = false;
+            mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */,
+                    false /* force */, false /* removeControls */, -1 /* x */, -1 /* y */,
+                    false /* resetMenu */);
+        }
+        if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
+            mCheckForLeavebehind = true;
+        }
+        return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev);
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private void handleEmptySpaceClick(MotionEvent ev) {
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_MOVE:
+                if (mTouchIsClick && (Math.abs(ev.getY() - mInitialTouchY) > mTouchSlop
+                        || Math.abs(ev.getX() - mInitialTouchX) > mTouchSlop)) {
+                    mTouchIsClick = false;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mStatusBarState != StatusBarState.KEYGUARD && mTouchIsClick &&
+                        isBelowLastNotification(mInitialTouchX, mInitialTouchY)) {
+                    mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY);
+                }
+                break;
+        }
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private void initDownStates(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mExpandedInThisMotion = false;
+            mOnlyScrollingInThisMotion = !mScroller.isFinished();
+            mDisallowScrollingInThisMotion = false;
+            mDisallowDismissInThisMotion = false;
+            mTouchIsClick = true;
+            mInitialTouchX = ev.getX();
+            mInitialTouchY = ev.getY();
+        }
+    }
+
+    @Override
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+        if (disallowIntercept) {
+            cancelLongPress();
+        }
+    }
+
     @ShadeViewRefactor(RefactorComponent.INPUT)
     private boolean onInterceptTouchEventScroll(MotionEvent ev) {
         if (!isScrollingEnabled()) {
@@ -3710,11 +3648,6 @@
         return mIsBeingDragged;
     }
 
-    @ShadeViewRefactor(RefactorComponent.LAYOUT_ALGORITHM)
-    protected StackScrollAlgorithm createStackScrollAlgorithm(Context context) {
-        return new StackScrollAlgorithm(context);
-    }
-
     /**
      * @return Whether the specified motion event is actually happening over the content.
      */
@@ -3723,13 +3656,6 @@
         return isInContentBounds(event.getY());
     }
 
-    /**
-     * @return Whether a y coordinate is inside the content.
-     */
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean isInContentBounds(float y) {
-        return y < getHeight() - getEmptyBottomMargin();
-    }
 
     @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.INPUT)
@@ -3742,6 +3668,83 @@
         }
     }
 
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void requestDisallowLongPress() {
+        cancelLongPress();
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void requestDisallowDismiss() {
+        mDisallowDismissInThisMotion = true;
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void cancelLongPress() {
+        mSwipeHelper.cancelLongPress();
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener) {
+        mOnEmptySpaceClickListener = listener;
+    }
+
+    /** @hide */
+    @Override
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (!isEnabled()) {
+            return false;
+        }
+        int direction = -1;
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+                // fall through
+            case android.R.id.accessibilityActionScrollDown:
+                direction = 1;
+                // fall through
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+                // fall through
+            case android.R.id.accessibilityActionScrollUp:
+                final int viewportHeight = getHeight() - mPaddingBottom - mTopPadding - mPaddingTop
+                        - mShelf.getIntrinsicHeight();
+                final int targetScrollY = Math.max(0,
+                        Math.min(mOwnScrollY + direction * viewportHeight, getScrollRange()));
+                if (targetScrollY != mOwnScrollY) {
+                    mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScrollY - mOwnScrollY);
+                    animateScroll();
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    public void closeControlsIfOutsideTouch(MotionEvent 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 */);
+        }
+    }
+
     @Override
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void onWindowFocusChanged(boolean hasWindowFocus) {
@@ -3760,21 +3763,6 @@
         }
     }
 
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void requestDisallowLongPress() {
-        cancelLongPress();
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void requestDisallowDismiss() {
-        mDisallowDismissInThisMotion = true;
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void cancelLongPress() {
-        mSwipeHelper.cancelLongPress();
-    }
-
     @Override
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     public boolean isScrolledToTop() {
@@ -3916,7 +3904,6 @@
     }
 
     @Override
-    @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
         updateContentHeight();
         updateScrollPositionOnExpandInBottom(view);
@@ -3936,7 +3923,6 @@
     }
 
     @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void onReset(ExpandableView view) {
         updateAnimationState(view);
         updateChronometerForChild(view);
@@ -3969,13 +3955,8 @@
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setOnHeightChangedListener(
-            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
-        this.mOnHeightChangedListener = mOnHeightChangedListener;
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void setOnEmptySpaceClickListener(OnEmptySpaceClickListener listener) {
-        mOnEmptySpaceClickListener = listener;
+            ExpandableView.OnHeightChangedListener onHeightChangedListener) {
+        this.mOnHeightChangedListener = onHeightChangedListener;
     }
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -4465,7 +4446,7 @@
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setGroupManager(NotificationGroupManager groupManager) {
         this.mGroupManager = groupManager;
-        mGroupManager.setOnGroupChangeListener(this);
+        mGroupManager.setOnGroupChangeListener(mOnGroupChangeListener);
     }
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -4508,33 +4489,6 @@
         return touchY > mTopPadding + mStackTranslation;
     }
 
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) {
-        boolean animated = !mGroupExpandedForMeasure && mAnimationsEnabled
-                && (mIsExpanded || changedRow.isPinned());
-        if (animated) {
-            mExpandedGroupView = changedRow;
-            mNeedsAnimation = true;
-        }
-        changedRow.setChildrenExpanded(expanded, animated);
-        if (!mGroupExpandedForMeasure) {
-            onHeightChanged(changedRow, false /* needsAnimation */);
-        }
-        runAfterAnimationFinished(new Runnable() {
-            @Override
-            public void run() {
-                changedRow.onFinishedExpansionChange();
-            }
-        });
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) {
-        mStatusBar.requestNotificationUpdate();
-    }
-
     /** @hide */
     @Override
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -4567,46 +4521,6 @@
         info.setClassName(ScrollView.class.getName());
     }
 
-    /** @hide */
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
-        if (super.performAccessibilityActionInternal(action, arguments)) {
-            return true;
-        }
-        if (!isEnabled()) {
-            return false;
-        }
-        int direction = -1;
-        switch (action) {
-            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
-                // fall through
-            case android.R.id.accessibilityActionScrollDown:
-                direction = 1;
-                // fall through
-            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
-                // fall through
-            case android.R.id.accessibilityActionScrollUp:
-                final int viewportHeight = getHeight() - mPaddingBottom - mTopPadding - mPaddingTop
-                        - mShelf.getIntrinsicHeight();
-                final int targetScrollY = Math.max(0,
-                        Math.min(mOwnScrollY + direction * viewportHeight, getScrollRange()));
-                if (targetScrollY != mOwnScrollY) {
-                    mScroller.startScroll(mScrollX, mOwnScrollY, 0, targetScrollY - mOwnScrollY);
-                    animateScroll();
-                    return true;
-                }
-                break;
-        }
-        return false;
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public void onGroupsChanged() {
-        mStatusBar.requestNotificationUpdate();
-    }
-
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     public void generateChildOrderChangedEvent() {
         if (mIsExpanded && mAnimationsEnabled) {
@@ -4649,7 +4563,7 @@
     public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
         mHeadsUpManager = headsUpManager;
         mHeadsUpManager.addListener(mRoundnessManager);
-        mHeadsUpManager.setAnimationStateHandler(this);
+        mHeadsUpManager.setAnimationStateHandler(this::setHeadsUpGoingAwayAnimationsAllowed);
     }
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -5168,67 +5082,7 @@
         return !mEntryManager.getNotificationData().getActiveNotifications().isEmpty();
     }
 
-    // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------
-
-
-    /* Only ever called as a consequence of a lockscreen expansion gesture. */
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean onDraggedDown(View startingChild, int dragLengthY) {
-        if (mStatusBarState == StatusBarState.KEYGUARD
-                && hasActiveNotifications() && (!mStatusBar.isDozing() || mStatusBar.isPulsing())) {
-            mLockscreenGestureLogger.write(
-                    MetricsEvent.ACTION_LS_SHADE,
-                    (int) (dragLengthY / mDisplayMetrics.density),
-                    0 /* velocityDp - N/A */);
-
-            // We have notifications, go to locked shade.
-            mStatusBar.goToLockedShade(startingChild);
-            if (startingChild instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) startingChild;
-                row.onExpandedByGesture(true /* drag down is always an open */);
-            }
-            return true;
-        } else {
-            // abort gesture.
-            return false;
-        }
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onDragDownReset() {
-        setDimmed(true /* dimmed */, true /* animated */);
-        resetScrollPosition();
-        resetCheckSnoozeLeavebehind();
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onCrossedThreshold(boolean above) {
-        setDimmed(!above /* dimmed */, true /* animate */);
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void onTouchSlopExceeded() {
-        cancelLongPress();
-        checkSnoozeLeavebehind();
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void setEmptyDragAmount(float amount) {
-        mNotificationPanel.setEmptyDragAmount(amount);
-    }
-
-    @Override
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public boolean isFalsingCheckNeeded() {
-        return mStatusBarState == StatusBarState.KEYGUARD;
-    }
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void updateSpeedBumpIndex() {
         int speedBumpIndex = 0;
         int currentIndex = 0;
@@ -5269,30 +5123,6 @@
         mSwipeHelper.resetExposedMenuView(animate, force);
     }
 
-
-    @ShadeViewRefactor(RefactorComponent.INPUT)
-    public void closeControlsIfOutsideTouch(MotionEvent 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 {
 
@@ -5614,9 +5444,9 @@
       }
     };
 
-    class NotificationMenuListener implements NotificationMenuRowPlugin.OnMenuEventListener {
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private final OnMenuEventListener mMenuEventListener = new OnMenuEventListener() {
         @Override
-        @ShadeViewRefactor(RefactorComponent.INPUT)
         public void onMenuClicked(View view, int x, int y, MenuItem item) {
             if (mLongPressListener == null) {
                 return;
@@ -5630,7 +5460,6 @@
         }
 
         @Override
-        @ShadeViewRefactor(RefactorComponent.INPUT)
         public void onMenuReset(View row) {
             View translatingParentView = mSwipeHelper.getTranslatingParentView();
             if (translatingParentView != null && row == translatingParentView) {
@@ -5640,7 +5469,6 @@
         }
 
         @Override
-        @ShadeViewRefactor(RefactorComponent.INPUT)
         public void onMenuShown(View row) {
             if (row instanceof ExpandableNotificationRow) {
                 MetricsLogger.action(mContext, MetricsEvent.ACTION_REVEAL_GEAR,
@@ -5649,9 +5477,11 @@
             }
             mSwipeHelper.onMenuShown(row);
         }
-    }
+    };
 
-    class SwipeHelperCallback implements NotificationSwipeHelper.NotificationCallback {
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private final NotificationSwipeHelper.NotificationCallback mNotificationCallback =
+            new NotificationSwipeHelper.NotificationCallback() {
         @Override
         public void onDismiss() {
             mNotificationGutsManager.closeAndSaveGuts(true /* removeLeavebehind */,
@@ -5671,10 +5501,8 @@
         }
 
         @Override
-        @ShadeViewRefactor(RefactorComponent.INPUT)
         public void onDragCancelled(View v) {
             mFalsingManager.onNotificatonStopDismissing();
-            setSwipingInProgress(false);
         }
 
         /**
@@ -5682,7 +5510,6 @@
          * 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()) {
@@ -5701,7 +5528,6 @@
          * @param view view (e.g. notification) to dismiss from the layout
          */
 
-        @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
         public void handleChildViewDismissed(View view) {
             if (mDismissAllInProgress) {
                 return;
@@ -5709,7 +5535,6 @@
 
             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.
@@ -5743,13 +5568,11 @@
         }
 
         @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());
@@ -5772,10 +5595,8 @@
         }
 
         @Override
-        @ShadeViewRefactor(RefactorComponent.INPUT)
         public void onBeginDrag(View v) {
             mFalsingManager.onNotificatonStartDismissing();
-            setSwipingInProgress(true);
             mAmbientState.onBeginDrag(v);
             updateContinuousShadowDrawing();
             if (mAnimationsEnabled && (mIsExpanded || !isPinnedHeadsUp(v))) {
@@ -5786,7 +5607,6 @@
         }
 
         @Override
-        @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
         public void onChildSnappedBack(View animView, float targetLeft) {
             mAmbientState.onDragFinished(animView);
             updateContinuousShadowDrawing();
@@ -5808,7 +5628,6 @@
         }
 
         @Override
-        @ShadeViewRefactor(RefactorComponent.INPUT)
         public boolean updateSwipeProgress(View animView, boolean dismissable,
                 float swipeProgress) {
             // Returning true prevents alpha fading.
@@ -5816,7 +5635,6 @@
         }
 
         @Override
-        @ShadeViewRefactor(RefactorComponent.INPUT)
         public float getFalsingThresholdFactor() {
             return mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
         }
@@ -5825,5 +5643,197 @@
         public boolean canChildBeDismissed(View v) {
             return NotificationStackScrollLayout.this.canChildBeDismissed(v);
         }
+    };
+
+    // ---------------------- DragDownHelper.OnDragDownListener ------------------------------------
+
+    @ShadeViewRefactor(RefactorComponent.INPUT)
+    private final DragDownCallback mDragDownCallback = new DragDownCallback() {
+
+        /* Only ever called as a consequence of a lockscreen expansion gesture. */
+        @Override
+        public boolean onDraggedDown(View startingChild, int dragLengthY) {
+            if (mStatusBarState == StatusBarState.KEYGUARD
+                    && hasActiveNotifications() && (!mStatusBar.isDozing()
+                    || mStatusBar.isPulsing())) {
+                mLockscreenGestureLogger.write(
+                        MetricsEvent.ACTION_LS_SHADE,
+                        (int) (dragLengthY / mDisplayMetrics.density),
+                        0 /* velocityDp - N/A */);
+
+                // We have notifications, go to locked shade.
+                mStatusBar.goToLockedShade(startingChild);
+                if (startingChild instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row = (ExpandableNotificationRow) startingChild;
+                    row.onExpandedByGesture(true /* drag down is always an open */);
+                }
+                return true;
+            } else {
+                // abort gesture.
+                return false;
+            }
+        }
+
+        @Override
+        public void onDragDownReset() {
+            setDimmed(true /* dimmed */, true /* animated */);
+            resetScrollPosition();
+            resetCheckSnoozeLeavebehind();
+        }
+
+        @Override
+        public void onCrossedThreshold(boolean above) {
+            setDimmed(!above /* dimmed */, true /* animate */);
+        }
+
+        @Override
+        public void onTouchSlopExceeded() {
+            cancelLongPress();
+            checkSnoozeLeavebehind();
+        }
+
+        @Override
+        public void setEmptyDragAmount(float amount) {
+            mNotificationPanel.setEmptyDragAmount(amount);
+        }
+
+        @Override
+        public boolean isFalsingCheckNeeded() {
+            return mStatusBarState == StatusBarState.KEYGUARD;
+        }
+    };
+
+    public DragDownCallback getDragDownCallback() { return mDragDownCallback; }
+
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    private final HeadsUpTouchHelper.Callback mHeadsUpCallback = new HeadsUpTouchHelper.Callback() {
+        @Override
+        public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
+            return NotificationStackScrollLayout.this.getChildAtRawPosition(touchX, touchY);
+        }
+
+        @Override
+        public boolean isExpanded() {
+            return mIsExpanded;
+        }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
+    };
+
+    public HeadsUpTouchHelper.Callback getHeadsUpCallback() { return mHeadsUpCallback; }
+
+
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    private final OnGroupChangeListener mOnGroupChangeListener = new OnGroupChangeListener() {
+        @Override
+        public void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded) {
+            boolean animated = !mGroupExpandedForMeasure && mAnimationsEnabled
+                    && (mIsExpanded || changedRow.isPinned());
+            if (animated) {
+                mExpandedGroupView = changedRow;
+                mNeedsAnimation = true;
+            }
+            changedRow.setChildrenExpanded(expanded, animated);
+            if (!mGroupExpandedForMeasure) {
+                onHeightChanged(changedRow, false /* needsAnimation */);
+            }
+            runAfterAnimationFinished(new Runnable() {
+                @Override
+                public void run() {
+                    changedRow.onFinishedExpansionChange();
+                }
+            });
+        }
+
+        @Override
+        public void onGroupCreatedFromChildren(NotificationGroupManager.NotificationGroup group) {
+            mStatusBar.requestNotificationUpdate();
+        }
+
+        @Override
+        public void onGroupsChanged() {
+            mStatusBar.requestNotificationUpdate();
+        }
+    };
+
+    @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
+    private ExpandHelper.Callback mExpandHelperCallback = new ExpandHelper.Callback() {
+        @Override
+        public ExpandableView getChildAtPosition(float touchX, float touchY) {
+            return NotificationStackScrollLayout.this.getChildAtPosition(touchX, touchY);
+        }
+
+        @Override
+        public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
+            return NotificationStackScrollLayout.this.getChildAtRawPosition(touchX, touchY);
+        }
+
+        @Override
+        public boolean canChildBeExpanded(View v) {
+            return v instanceof ExpandableNotificationRow
+                    && ((ExpandableNotificationRow) v).isExpandable()
+                    && !((ExpandableNotificationRow) v).areGutsExposed()
+                    && (mIsExpanded || !((ExpandableNotificationRow) v).isPinned());
+        }
+
+        /* Only ever called as a consequence of an expansion gesture in the shade. */
+        @Override
+        public void setUserExpandedChild(View v, boolean userExpanded) {
+            if (v instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+                if (userExpanded && onKeyguard()) {
+                    // Due to a race when locking the screen while touching, a notification may be
+                    // expanded even after we went back to keyguard. An example of this happens if
+                    // you click in the empty space while expanding a group.
+
+                    // We also need to un-user lock it here, since otherwise the content height
+                    // calculated might be wrong. We also can't invert the two calls since
+                    // un-userlocking it will trigger a layout switch in the content view.
+                    row.setUserLocked(false);
+                    updateContentHeight();
+                    notifyHeightChangeListener(row);
+                    return;
+                }
+                row.setUserExpanded(userExpanded, true /* allowChildrenExpansion */);
+                row.onExpandedByGesture(userExpanded);
+            }
+        }
+
+        @Override
+        public void setExpansionCancelled(View v) {
+            if (v instanceof ExpandableNotificationRow) {
+                ((ExpandableNotificationRow) v).setGroupExpansionChanging(false);
+            }
+        }
+
+        @Override
+        public void setUserLockedChild(View v, boolean userLocked) {
+            if (v instanceof ExpandableNotificationRow) {
+                ((ExpandableNotificationRow) v).setUserLocked(userLocked);
+            }
+            cancelLongPress();
+            requestDisallowInterceptTouchEvent(true);
+        }
+
+        @Override
+        public void expansionStateChanged(boolean isExpanding) {
+            mExpandingNotification = isExpanding;
+            if (!mExpandedInThisMotion) {
+                mMaxScrollAfterExpand = mOwnScrollY;
+                mExpandedInThisMotion = true;
+            }
+        }
+
+        @Override
+        public int getMaxExpandHeight(ExpandableView view) {
+            return view.getMaxContentHeight();
+        }
+    };
+
+    public ExpandHelper.Callback getExpandHelperCallback() {
+        return mExpandHelperCallback;
     }
 }
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
index 028957d..599da3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -31,11 +31,9 @@
 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
@@ -229,6 +227,7 @@
         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.
+            mSwipingInProgress = false;
             mCallback.handleChildViewDismissed(view);
         }
         mCallback.onDismiss();
@@ -248,6 +247,7 @@
     @Override
     public void snapChild(final View animView, final float targetLeft, float velocity) {
         superSnapChild(animView, targetLeft, velocity);
+        mSwipingInProgress = false;
         mCallback.onDragCancelled(animView);
         if (targetLeft == 0) {
             handleMenuCoveredOrDismissed();
@@ -354,6 +354,7 @@
 
     public void onMenuShown(View animView) {
         setExposedMenuView(getTranslatingParentView());
+        mSwipingInProgress = false;
         mCallback.onDragCancelled(animView);
         Handler handler = getHandler();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
index 4df1e3b..e4a5caa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java
@@ -32,7 +32,7 @@
 public class HeadsUpTouchHelper implements Gefingerpoken {
 
     private HeadsUpManagerPhone mHeadsUpManager;
-    private NotificationStackScrollLayout mStackScroller;
+    private Callback mCallback;
     private int mTrackingPointer;
     private float mTouchSlop;
     private float mInitialTouchX;
@@ -44,12 +44,12 @@
     private ExpandableNotificationRow mPickedChild;
 
     public HeadsUpTouchHelper(HeadsUpManagerPhone headsUpManager,
-            NotificationStackScrollLayout stackScroller,
+            Callback callback,
             NotificationPanelView notificationPanelView) {
         mHeadsUpManager = headsUpManager;
-        mStackScroller = stackScroller;
+        mCallback = callback;
         mPanel = notificationPanelView;
-        Context context = stackScroller.getContext();
+        Context context = mCallback.getContext();
         final ViewConfiguration configuration = ViewConfiguration.get(context);
         mTouchSlop = configuration.getScaledTouchSlop();
     }
@@ -75,13 +75,13 @@
                 mInitialTouchY = y;
                 mInitialTouchX = x;
                 setTrackingHeadsUp(false);
-                ExpandableView child = mStackScroller.getChildAtRawPosition(x, y);
+                ExpandableView child = mCallback.getChildAtRawPosition(x, y);
                 mTouchingHeadsUpView = false;
                 if (child instanceof ExpandableNotificationRow) {
                     mPickedChild = (ExpandableNotificationRow) child;
-                    mTouchingHeadsUpView = !mStackScroller.isExpanded()
+                    mTouchingHeadsUpView = !mCallback.isExpanded()
                             && mPickedChild.isHeadsUp() && mPickedChild.isPinned();
-                } else if (child == null && !mStackScroller.isExpanded()) {
+                } else if (child == null && !mCallback.isExpanded()) {
                     // We might touch above the visible heads up child, but then we still would
                     // like to capture it.
                     NotificationData.Entry topEntry = mHeadsUpManager.getTopEntry();
@@ -174,4 +174,10 @@
         mPickedChild = null;
         mTouchingHeadsUpView = false;
     }
+
+    public interface Callback {
+        ExpandableView getChildAtRawPosition(float touchX, float touchY);
+        boolean isExpanded();
+        Context getContext();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 6d53cd3..7507702 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -2521,8 +2521,8 @@
     @Override
     public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
         super.setHeadsUpManager(headsUpManager);
-        mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScroller,
-                this);
+        mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager,
+                mNotificationStackScroller.getHeadsUpCallback(), this);
     }
 
     public void setTrackedHeadsUp(ExpandableNotificationRow pickedChild) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
index 45b32c7..ad9b9b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -58,6 +58,7 @@
 import com.android.internal.view.FloatingActionMode;
 import com.android.internal.widget.FloatingToolbar;
 import com.android.systemui.Dependency;
+import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingManager;
 import com.android.systemui.statusbar.DragDownHelper;
@@ -182,6 +183,11 @@
         }
     }
 
+    @VisibleForTesting
+    protected NotificationStackScrollLayout getStackScrollLayout() {
+        return mStackScrollLayout;
+    }
+
     @Override
     public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
         return new LayoutParams(getContext(), attrs);
@@ -215,8 +221,11 @@
 
     public void setService(StatusBar service) {
         mService = service;
-        setDragDownHelper(new DragDownHelper(getContext(), this, mStackScrollLayout,
-                mStackScrollLayout));
+        NotificationStackScrollLayout stackScrollLayout = getStackScrollLayout();
+        ExpandHelper.Callback expandHelperCallback = stackScrollLayout.getExpandHelperCallback();
+        DragDownHelper.DragDownCallback dragDownCallback = stackScrollLayout.getDragDownCallback();
+        setDragDownHelper(new DragDownHelper(getContext(), this, expandHelperCallback,
+                dragDownCallback));
     }
 
     @VisibleForTesting
@@ -309,7 +318,7 @@
             }
         }
         if (isDown) {
-            mStackScrollLayout.closeControlsIfOutsideTouch(ev);
+            getStackScrollLayout().closeControlsIfOutsideTouch(ev);
         }
         if (mService.isDozing()) {
             mService.mDozeScrimController.extendPulse();
@@ -331,13 +340,14 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (mService.isDozing() && !mStackScrollLayout.hasPulsingNotifications()) {
+        NotificationStackScrollLayout stackScrollLayout = getStackScrollLayout();
+        if (mService.isDozing() && !stackScrollLayout.hasPulsingNotifications()) {
             // Capture all touch events in always-on.
             return true;
         }
         boolean intercept = false;
         if (mNotificationPanel.isFullyExpanded()
-                && mStackScrollLayout.getVisibility() == View.VISIBLE
+                && stackScrollLayout.getVisibility() == View.VISIBLE
                 && mStatusBarStateController.getState() == StatusBarState.KEYGUARD
                 && !mService.isBouncerShowing()
                 && !mService.isDozing()) {
@@ -349,7 +359,7 @@
         if (intercept) {
             MotionEvent cancellation = MotionEvent.obtain(ev);
             cancellation.setAction(MotionEvent.ACTION_CANCEL);
-            mStackScrollLayout.onInterceptTouchEvent(cancellation);
+            stackScrollLayout.onInterceptTouchEvent(cancellation);
             mNotificationPanel.onInterceptTouchEvent(cancellation);
             cancellation.recycle();
         }
@@ -391,8 +401,9 @@
     }
 
     public void cancelExpandHelper() {
-        if (mStackScrollLayout != null) {
-            mStackScrollLayout.cancelExpandHelper();
+        NotificationStackScrollLayout stackScrollLayout = getStackScrollLayout();
+        if (stackScrollLayout != null) {
+            stackScrollLayout.cancelExpandHelper();
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
index 15c18e9..6b4ccc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
@@ -218,6 +218,9 @@
         public void generateChildOrderChangedEvent() {}
 
         @Override
+        public void onReset(ExpandableView view) {}
+
+        @Override
         public int getContainerChildCount() {
             return mRows.size();
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarWindowViewTest.java
index 445a194..46335dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarWindowViewTest.java
@@ -17,11 +17,11 @@
 package com.android.systemui.statusbar.phone;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.view.MotionEvent;
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,11 +44,14 @@
     private StatusBarWindowView mView;
     private StatusBar mStatusBar;
     private DragDownHelper mDragDownHelper;
+    private NotificationStackScrollLayout mStackScrollLayout;
 
     @Before
     public void setUp() {
         mDependency.injectMockDependency(StatusBarStateController.class);
-        mView = new StatusBarWindowView(getContext(), null);
+        mView = spy(new StatusBarWindowView(getContext(), null));
+        mStackScrollLayout = mock(NotificationStackScrollLayout.class);
+        when(mView.getStackScrollLayout()).thenReturn(mStackScrollLayout);
         mStatusBar = mock(StatusBar.class);
         mView.setService(mStatusBar);
         mDragDownHelper = mock(DragDownHelper.class);