Merge "Refactor QuickStepController into Gestures"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
index 814324e..99cc3a3 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/phone/NavGesture.java
@@ -36,7 +36,7 @@
 
         public boolean onInterceptTouchEvent(MotionEvent event);
 
-        public void setBarState(boolean vertical, boolean isRtl);
+        public void setBarState(boolean isRtl, int navBarPosition);
 
         public void onDraw(Canvas canvas);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
index 4eca6bb..119f01a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -263,6 +263,16 @@
         }
     }
 
+    public void setTranslation(int x, int y, int z) {
+        final int N = mViews.size();
+        for (int i = 0; i < N; i++) {
+            final View view = mViews.get(i);
+            view.setTranslationX(x);
+            view.setTranslationY(y);
+            view.setTranslationZ(z);
+        }
+    }
+
     public ArrayList<View> getViews() {
         return mViews;
     }
@@ -276,6 +286,11 @@
         if (mImageDrawable != null) {
             mImageDrawable.setCallback(mCurrentView);
         }
+        if (mCurrentView != null) {
+            mCurrentView.setTranslationX(0);
+            mCurrentView.setTranslationY(0);
+            mCurrentView.setTranslationZ(0);
+        }
     }
 
     public void setVertical(boolean vertical) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java
new file mode 100644
index 0000000..1002f9e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBackAction.java
@@ -0,0 +1,131 @@
+/*
+ * 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.phone;
+
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+
+import android.annotation.NonNull;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.systemui.recents.OverviewProxyService;
+
+/**
+ * A back action when triggered will execute a back command
+ */
+public class NavigationBackAction extends NavigationGestureAction {
+
+    private static final String PULL_HOME_GO_BACK_PROP = "quickstepcontroller_homegoesback";
+    private static final String BACK_AFTER_END_PROP =
+            "quickstepcontroller_homegoesbackwhenend";
+    private static final String NAVBAR_EXPERIMENTS_DISABLED = "navbarexperiments_disabled";
+    private static final long BACK_BUTTON_FADE_OUT_ALPHA = 60;
+    private static final long BACK_GESTURE_POLL_TIMEOUT = 1000;
+
+    private final Handler mHandler = new Handler();
+
+    private final Runnable mExecuteBackRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (isEnabled() && canPerformAction()) {
+                performBack();
+                mHandler.postDelayed(this, BACK_GESTURE_POLL_TIMEOUT);
+            }
+        }
+    };
+
+    public NavigationBackAction(@NonNull NavigationBarView navigationBarView,
+            @NonNull OverviewProxyService service) {
+        super(navigationBarView, service);
+    }
+
+    @Override
+    public int requiresTouchDownHitTarget() {
+        return HIT_TARGET_HOME;
+    }
+
+    @Override
+    public boolean requiresDragWithHitTarget() {
+        return true;
+    }
+
+    @Override
+    public boolean canPerformAction() {
+        return mProxySender.getBackButtonAlpha() > 0;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return swipeHomeGoBackGestureEnabled();
+    }
+
+    @Override
+    protected void onGestureStart(MotionEvent event) {
+        if (!QuickStepController.shouldhideBackButton(getContext())) {
+            mNavigationBarView.getBackButton().setAlpha(0 /* alpha */, true /* animate */,
+                    BACK_BUTTON_FADE_OUT_ALPHA);
+        }
+        mHandler.removeCallbacks(mExecuteBackRunnable);
+        if (!shouldExecuteBackOnUp()) {
+            performBack();
+            mHandler.postDelayed(mExecuteBackRunnable, BACK_GESTURE_POLL_TIMEOUT);
+        }
+    }
+
+    @Override
+    protected void onGestureEnd() {
+        mHandler.removeCallbacks(mExecuteBackRunnable);
+        if (!QuickStepController.shouldhideBackButton(getContext())) {
+            mNavigationBarView.getBackButton().setAlpha(
+                    mProxySender.getBackButtonAlpha(), true /* animate */);
+        }
+        if (shouldExecuteBackOnUp()) {
+            performBack();
+        }
+    }
+
+    private void performBack() {
+        sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
+        sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
+        mNavigationBarView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+    }
+
+    private boolean swipeHomeGoBackGestureEnabled() {
+        return !getGlobalBoolean(NAVBAR_EXPERIMENTS_DISABLED)
+                && getGlobalBoolean(PULL_HOME_GO_BACK_PROP);
+    }
+
+    private boolean shouldExecuteBackOnUp() {
+        return !getGlobalBoolean(NAVBAR_EXPERIMENTS_DISABLED)
+                && getGlobalBoolean(BACK_AFTER_END_PROP);
+    }
+
+    private void sendEvent(int action, int code) {
+        long when = SystemClock.uptimeMillis();
+        final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
+                0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
+                KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+                InputDevice.SOURCE_KEYBOARD);
+        InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 6728f08..2c3c27f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -38,9 +38,11 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.MotionEvent;
@@ -49,6 +51,7 @@
 import android.view.ViewGroup;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.inputmethod.InputMethodManager;
@@ -143,6 +146,10 @@
     private RecentsOnboarding mRecentsOnboarding;
     private NotificationPanelView mPanelView;
 
+    private QuickScrubAction mQuickScrubAction;
+    private QuickStepAction mQuickStepAction;
+    private NavigationBackAction mBackAction;
+
     /**
      * Helper that is responsible for showing the right toast when a disallowed activity operation
      * occurred. In pinned mode, we show instructions on how to break out of this mode, whilst in
@@ -299,6 +306,10 @@
         mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton);
         mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup);
         mDeadZone = new DeadZone(this);
+
+        mQuickScrubAction = new QuickScrubAction(this, mOverviewProxyService);
+        mQuickStepAction = new QuickStepAction(this, mOverviewProxyService);
+        mBackAction = new NavigationBackAction(this, mOverviewProxyService);
     }
 
     public BarTransitions getBarTransitions() {
@@ -313,6 +324,8 @@
         mPanelView = panel;
         if (mGestureHelper instanceof QuickStepController) {
             ((QuickStepController) mGestureHelper).setComponents(this);
+            ((QuickStepController) mGestureHelper).setGestureActions(mQuickStepAction,
+                    null /* swipeDownAction*/, mBackAction, mQuickScrubAction);
         }
     }
 
@@ -756,24 +769,6 @@
         mRecentsOnboarding.hide(true);
     }
 
-    /**
-     * @return the button at the given {@param x} and {@param y}.
-     */
-    ButtonDispatcher getButtonAtPosition(int x, int y) {
-        for (int i = 0; i < mButtonDispatchers.size(); i++) {
-            ButtonDispatcher button = mButtonDispatchers.valueAt(i);
-            View buttonView = button.getCurrentView();
-            if (buttonView != null) {
-                buttonView.getHitRect(mTmpRect);
-                offsetDescendantRectToMyCoords(buttonView, mTmpRect);
-                if (mTmpRect.contains(x, y)) {
-                    return button;
-                }
-            }
-        }
-        return null;
-    }
-
     @Override
     public void onFinishInflate() {
         mNavigationInflaterView = findViewById(R.id.navigation_inflater);
@@ -908,7 +903,13 @@
     private void updateTaskSwitchHelper() {
         if (mGestureHelper == null) return;
         boolean isRtl = (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
-        mGestureHelper.setBarState(mVertical, isRtl);
+        int navBarPos = 0;
+        try {
+            navBarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to get nav bar position.", e);
+        }
+        mGestureHelper.setBarState(isRtl, navBarPos);
     }
 
     @Override
@@ -1112,6 +1113,14 @@
 
         mContextualButtonGroup.dump(pw);
         if (mGestureHelper != null) {
+            pw.println("Navigation Gesture Actions {");
+            pw.print("    "); pw.println("QuickScrub Enabled=" + mQuickScrubAction.isEnabled());
+            pw.print("    "); pw.println("QuickScrub Active=" + mQuickScrubAction.isActive());
+            pw.print("    "); pw.println("QuickStep Enabled=" + mQuickStepAction.isEnabled());
+            pw.print("    "); pw.println("QuickStep Active=" + mQuickStepAction.isActive());
+            pw.print("    "); pw.println("Back Gesture Enabled=" + mBackAction.isEnabled());
+            pw.print("    "); pw.println("Back Gesture Active=" + mBackAction.isActive());
+            pw.println("}");
             mGestureHelper.dump(pw);
         }
         mRecentsOnboarding.dump(pw);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java
new file mode 100644
index 0000000..593bfae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationGestureAction.java
@@ -0,0 +1,181 @@
+/*
+ * 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.phone;
+
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT;
+
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+import android.view.WindowManagerPolicyConstants;
+import com.android.systemui.recents.OverviewProxyService;
+
+/**
+ * A gesture action that would be triggered and reassigned by {@link QuickStepController}
+ */
+public abstract class NavigationGestureAction {
+
+    protected final NavigationBarView mNavigationBarView;
+    protected final OverviewProxyService mProxySender;
+
+    protected int mNavigationBarPosition;
+    protected boolean mDragHorizontalPositive;
+    protected boolean mDragVerticalPositive;
+    private boolean mIsActive;
+
+    public NavigationGestureAction(@NonNull NavigationBarView navigationBarView,
+            @NonNull OverviewProxyService service) {
+        mNavigationBarView = navigationBarView;
+        mProxySender = service;
+    }
+
+    /**
+     * Pass event that the state of the bar (such as rotation) has changed
+     * @param changed if rotation or drag positive direction (such as ltr) has changed
+     * @param navBarPos position of navigation bar
+     * @param dragHorPositive direction of positive horizontal drag, could change with ltr changes
+     * @param dragVerPositive direction of positive vertical drag, could change with ltr changes
+     */
+    public void setBarState(boolean changed, int navBarPos, boolean dragHorPositive,
+            boolean dragVerPositive) {
+        mNavigationBarPosition = navBarPos;
+        mDragHorizontalPositive = dragHorPositive;
+        mDragVerticalPositive = dragVerPositive;
+    }
+
+    /**
+     * Resets the state of the action. Called when touch down occurs over the Navigation Bar.
+     */
+    public void reset() {
+        mIsActive = false;
+    }
+
+    /**
+     * Start the gesture and the action will be active
+     * @param event the event that caused the gesture
+     */
+    public void startGesture(MotionEvent event) {
+        mIsActive = true;
+        onGestureStart(event);
+    }
+
+    /**
+     * Gesture has ended with action cancel or up and this action will not be active
+     */
+    public void endGesture() {
+        mIsActive = false;
+        onGestureEnd();
+    }
+
+    /**
+     * If the action is currently active based on the gesture that triggered it. Only one action
+     * can occur at a time
+     * @return whether or not if this action has been triggered
+     */
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    /**
+     * @return whether or not this action can run if notification shade is shown
+     */
+    public boolean canRunWhenNotificationsShowing() {
+        return true;
+    }
+
+    /**
+     * @return whether or not this action triggers when starting a gesture from a certain hit target
+     * If {@link HIT_TARGET_NONE} is specified then action does not need to be triggered by button
+     */
+    public int requiresTouchDownHitTarget() {
+        return HIT_TARGET_NONE;
+    }
+
+    /**
+     * @return whether or not to move the button that started gesture over with user input drag
+     */
+    public boolean requiresDragWithHitTarget() {
+        return false;
+    }
+
+    /**
+     * Tell if the action is able to execute. Note that {@link #isEnabled()} must be true for this
+     * to be checked. The difference between this and {@link #isEnabled()} is that this dependent
+     * on the state of the navigation bar
+     * @return true if action can execute after gesture activates based on current states
+     */
+    public boolean canPerformAction() {
+        return true;
+    }
+
+    /**
+     * Tell if action is enabled. Compared to {@link #canPerformAction()} this is based on settings
+     * if the action is disabled for a particular gesture. For example a back action can be enabled
+     * however if there is nothing to back to then {@link #canPerformAction()} should return false.
+     * In this way if the action requires {@link #requiresDragWithHitTarget()} then if enabled, the
+     * button can be dragged with a large dampening factor during the gesture but will not activate
+     * the action.
+     * @return true if this action is enabled and can run
+     */
+    public abstract boolean isEnabled();
+
+    protected void onDarkIntensityChange(float intensity) {
+    }
+
+    protected void onDraw(Canvas canvas) {
+    }
+
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+    }
+
+    /**
+     * When gesture starts, this will run to execute the action
+     * @param event the event that triggered the gesture
+     */
+    protected abstract void onGestureStart(MotionEvent event);
+
+    /**
+     * Channels motion move events to the action to track the user inputs
+     * @param x the x position
+     * @param y the y position
+     */
+    public void onGestureMove(int x, int y) {
+    }
+
+    /**
+     * When gesture ends, this will run from action up or cancel
+     */
+    protected void onGestureEnd() {
+    }
+
+    protected Context getContext() {
+        return mNavigationBarView.getContext();
+    }
+
+    protected boolean isNavBarVertical() {
+        return mNavigationBarPosition == NAV_BAR_LEFT || mNavigationBarPosition == NAV_BAR_RIGHT;
+    }
+
+    protected boolean getGlobalBoolean(@NonNull String key) {
+        return QuickStepController.getBoolGlobalSetting(getContext(), key);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java
new file mode 100644
index 0000000..c64e124
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubAction.java
@@ -0,0 +1,329 @@
+/*
+ * 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.phone;
+
+import static com.android.systemui.Interpolators.ALPHA_IN;
+import static com.android.systemui.Interpolators.ALPHA_OUT;
+import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
+import static com.android.systemui.recents.OverviewProxyService.TAG_OPS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.annotation.NonNull;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.os.RemoteException;
+
+import android.util.FloatProperty;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.shared.recents.utilities.Utilities;
+
+/**
+ * QuickScrub action to send to launcher to start quickscrub gesture
+ */
+public class QuickScrubAction extends NavigationGestureAction {
+    private static final String TAG = "QuickScrubAction";
+
+    private static final float TRACK_SCALE = 0.95f;
+    private static final float GRADIENT_WIDTH = .75f;
+    private static final int ANIM_IN_DURATION_MS = 150;
+    private static final int ANIM_OUT_DURATION_MS = 134;
+
+    private AnimatorSet mTrackAnimator;
+    private View mCurrentNavigationBarView;
+
+    private float mTrackScale = TRACK_SCALE;
+    private float mTrackAlpha;
+    private float mHighlightCenter;
+    private float mDarkIntensity;
+
+    private final int mTrackThickness;
+    private final int mTrackEndPadding;
+    private final Paint mTrackPaint = new Paint();
+    private final Rect mTrackRect = new Rect();
+
+    private final FloatProperty<QuickScrubAction> mTrackAlphaProperty =
+            new FloatProperty<QuickScrubAction>("TrackAlpha") {
+        @Override
+        public void setValue(QuickScrubAction action, float alpha) {
+            mTrackAlpha = alpha;
+            mNavigationBarView.invalidate();
+        }
+
+        @Override
+        public Float get(QuickScrubAction action) {
+            return mTrackAlpha;
+        }
+    };
+
+    private final FloatProperty<QuickScrubAction> mTrackScaleProperty =
+            new FloatProperty<QuickScrubAction>("TrackScale") {
+        @Override
+        public void setValue(QuickScrubAction action, float scale) {
+            mTrackScale = scale;
+            mNavigationBarView.invalidate();
+        }
+
+        @Override
+        public Float get(QuickScrubAction action) {
+            return mTrackScale;
+        }
+    };
+
+    private final FloatProperty<QuickScrubAction> mNavBarAlphaProperty =
+            new FloatProperty<QuickScrubAction>("NavBarAlpha") {
+        @Override
+        public void setValue(QuickScrubAction action, float alpha) {
+            if (mCurrentNavigationBarView != null) {
+                mCurrentNavigationBarView.setAlpha(alpha);
+            }
+        }
+
+        @Override
+        public Float get(QuickScrubAction action) {
+            if (mCurrentNavigationBarView != null) {
+                return mCurrentNavigationBarView.getAlpha();
+            }
+            return 1f;
+        }
+    };
+
+    private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (mCurrentNavigationBarView != null) {
+                mCurrentNavigationBarView.setAlpha(1f);
+            }
+            mCurrentNavigationBarView = null;
+            updateHighlight();
+        }
+    };
+
+    public QuickScrubAction(@NonNull NavigationBarView navigationBarView,
+            @NonNull OverviewProxyService service) {
+        super(navigationBarView, service);
+        mTrackPaint.setAntiAlias(true);
+        mTrackPaint.setDither(true);
+
+        final Resources res = navigationBarView.getResources();
+        mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness);
+        mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding);
+    }
+
+    @Override
+    public void setBarState(boolean changed, int navBarPos, boolean dragHorPositive,
+            boolean dragVerPositive) {
+        super.setBarState(changed, navBarPos, dragHorPositive, dragVerPositive);
+        if (changed && isActive()) {
+            // End quickscrub if the state changes mid-transition
+            endQuickScrub(false /* animate */);
+        }
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+
+        // End any existing quickscrub animations before starting the new transition
+        if (mTrackAnimator != null) {
+            mTrackAnimator.end();
+            mTrackAnimator = null;
+        }
+        mCurrentNavigationBarView = mNavigationBarView.getCurrentView();
+    }
+
+    @Override
+    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int paddingLeft = mNavigationBarView.getPaddingLeft();
+        final int paddingTop = mNavigationBarView.getPaddingTop();
+        final int paddingRight = mNavigationBarView.getPaddingRight();
+        final int paddingBottom = mNavigationBarView.getPaddingBottom();
+        final int width = (right - left) - paddingRight - paddingLeft;
+        final int height = (bottom - top) - paddingBottom - paddingTop;
+        final int x1, x2, y1, y2;
+        if (isNavBarVertical()) {
+            x1 = (width - mTrackThickness) / 2 + paddingLeft;
+            x2 = x1 + mTrackThickness;
+            y1 = paddingTop + mTrackEndPadding;
+            y2 = y1 + height - 2 * mTrackEndPadding;
+        } else {
+            y1 = (height - mTrackThickness) / 2 + paddingTop;
+            y2 = y1 + mTrackThickness;
+            x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding;
+            x2 = x1 + width - 2 * mTrackEndPadding;
+        }
+        mTrackRect.set(x1, y1, x2, y2);
+    }
+
+    @Override
+    public void onDarkIntensityChange(float intensity) {
+        mDarkIntensity = intensity;
+        updateHighlight();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        if (!isEnabled()) {
+            return;
+        }
+        mTrackPaint.setAlpha(Math.round(255f * mTrackAlpha));
+
+        // Scale the track, but apply the inverse scale from the nav bar
+        final float radius = mTrackRect.height() / 2;
+        canvas.save();
+        float translate = Utilities.clamp(mHighlightCenter, mTrackRect.left, mTrackRect.right);
+        canvas.translate(translate, 0);
+        canvas.scale(mTrackScale / mNavigationBarView.getScaleX(),
+                1f / mNavigationBarView.getScaleY(),
+                mTrackRect.centerX(), mTrackRect.centerY());
+        canvas.drawRoundRect(mTrackRect.left - translate, mTrackRect.top,
+                mTrackRect.right - translate, mTrackRect.bottom, radius, radius, mTrackPaint);
+        canvas.restore();
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mNavigationBarView.isQuickScrubEnabled();
+    }
+
+    @Override
+    protected void onGestureStart(MotionEvent event) {
+        updateHighlight();
+        ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
+                PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f),
+                PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f));
+        trackAnimator.setInterpolator(ALPHA_IN);
+        trackAnimator.setDuration(ANIM_IN_DURATION_MS);
+        ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f);
+        navBarAnimator.setInterpolator(ALPHA_OUT);
+        navBarAnimator.setDuration(ANIM_OUT_DURATION_MS);
+        mTrackAnimator = new AnimatorSet();
+        mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
+        mTrackAnimator.start();
+
+        // Disable slippery for quick scrub to not cancel outside the nav bar
+        mNavigationBarView.updateSlippery();
+
+        try {
+            mProxySender.getProxy().onQuickScrubStart();
+            if (DEBUG_OVERVIEW_PROXY) {
+                Log.d(TAG_OPS, "Quick Scrub Start");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to send start of quick scrub.", e);
+        }
+        mProxySender.notifyQuickScrubStarted();
+    }
+
+    @Override
+    public void onGestureMove(int x, int y) {
+        int trackSize, offset;
+        if (isNavBarVertical()) {
+            trackSize = mTrackRect.height();
+            offset = y - mTrackRect.top;
+        } else {
+            offset = x - mTrackRect.left;
+            trackSize = mTrackRect.width();
+        }
+        if (!mDragHorizontalPositive || !mDragVerticalPositive) {
+            offset -= isNavBarVertical() ? mTrackRect.height() : mTrackRect.width();
+        }
+        float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
+        try {
+            mProxySender.getProxy().onQuickScrubProgress(scrubFraction);
+            if (DEBUG_OVERVIEW_PROXY) {
+                Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to send progress of quick scrub.", e);
+        }
+        mHighlightCenter = x;
+        mNavigationBarView.invalidate();
+    }
+
+    @Override
+    protected void onGestureEnd() {
+        endQuickScrub(true /* animate */);
+    }
+
+    private void endQuickScrub(boolean animate) {
+        animateEnd();
+        try {
+            mProxySender.getProxy().onQuickScrubEnd();
+            if (DEBUG_OVERVIEW_PROXY) {
+                Log.d(TAG_OPS, "Quick Scrub End");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to send end of quick scrub.", e);
+        }
+        if (!animate) {
+            if (mTrackAnimator != null) {
+                mTrackAnimator.end();
+                mTrackAnimator = null;
+            }
+        }
+    }
+
+    private void updateHighlight() {
+        if (mTrackRect.isEmpty()) {
+            return;
+        }
+        int colorBase, colorGrad;
+        if (mDarkIntensity > 0.5f) {
+            colorBase = getContext().getColor(R.color.quick_step_track_background_background_dark);
+            colorGrad = getContext().getColor(R.color.quick_step_track_background_foreground_dark);
+        } else {
+            colorBase = getContext().getColor(R.color.quick_step_track_background_background_light);
+            colorGrad = getContext().getColor(R.color.quick_step_track_background_foreground_light);
+        }
+        final RadialGradient mHighlight = new RadialGradient(0, mTrackRect.height() / 2,
+                mTrackRect.width() * GRADIENT_WIDTH, colorGrad, colorBase,
+                Shader.TileMode.CLAMP);
+        mTrackPaint.setShader(mHighlight);
+    }
+
+    private void animateEnd() {
+        if (mTrackAnimator != null) {
+            mTrackAnimator.cancel();
+        }
+
+        ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
+                PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f),
+                PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE));
+        trackAnimator.setInterpolator(ALPHA_OUT);
+        trackAnimator.setDuration(ANIM_OUT_DURATION_MS);
+        ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f);
+        navBarAnimator.setInterpolator(ALPHA_IN);
+        navBarAnimator.setDuration(ANIM_IN_DURATION_MS);
+        mTrackAnimator = new AnimatorSet();
+        mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
+        mTrackAnimator.addListener(mQuickScrubEndListener);
+        mTrackAnimator.start();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java
new file mode 100644
index 0000000..b18b79e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepAction.java
@@ -0,0 +1,62 @@
+/*
+ * 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.phone;
+
+import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
+import static com.android.systemui.recents.OverviewProxyService.TAG_OPS;
+
+import android.annotation.NonNull;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.systemui.recents.OverviewProxyService;
+
+/**
+ * QuickStep action to send to launcher to start overview
+ */
+public class QuickStepAction extends NavigationGestureAction {
+    private static final String TAG = "QuickStepAction";
+
+    public QuickStepAction(@NonNull NavigationBarView navigationBarView,
+            @NonNull OverviewProxyService service) {
+        super(navigationBarView, service);
+    }
+
+    @Override
+    public boolean canRunWhenNotificationsShowing() {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mNavigationBarView.isQuickStepSwipeUpEnabled();
+    }
+
+    @Override
+    public void onGestureStart(MotionEvent event) {
+        try {
+            mProxySender.getProxy().onQuickStep(event);
+            if (DEBUG_OVERVIEW_PROXY) {
+                Log.d(TAG_OPS, "Quick Step Start");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to send quick step started.", e);
+        }
+        mProxySender.notifyQuickStepStarted();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
index 3980126..c03800e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStepController.java
@@ -18,187 +18,96 @@
 
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
-import static com.android.systemui.Interpolators.ALPHA_IN;
-import static com.android.systemui.Interpolators.ALPHA_OUT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT;
+
 import static com.android.systemui.recents.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
 import static com.android.systemui.recents.OverviewProxyService.TAG_OPS;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_DEAD_ZONE;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_OVERVIEW;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.RadialGradient;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.hardware.input.InputManager;
-import android.os.Handler;
 import android.os.RemoteException;
-import android.os.SystemClock;
 import android.provider.Settings;
-import android.util.FloatProperty;
 import android.util.Log;
-import android.util.Slog;
-import android.view.HapticFeedbackConstants;
-import android.view.InputDevice;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewPropertyAnimator;
-import android.view.WindowManagerGlobal;
+
 import com.android.systemui.Dependency;
 import com.android.systemui.Interpolators;
-import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.R;
-import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.R;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.SysUiServiceProvider;
 import com.android.systemui.shared.recents.IOverviewProxy;
-import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.system.NavigationBarCompat;
+
 import java.io.PrintWriter;
 
 /**
  * Class to detect gestures on the navigation bar and implement quick scrub.
+ * Note that the variables in this class horizontal and vertical represents horizontal always
+ * aligned with along the navigation bar).
  */
 public class QuickStepController implements GestureHelper {
 
     private static final String TAG = "QuickStepController";
-    private static final int ANIM_IN_DURATION_MS = 150;
-    private static final int ANIM_OUT_DURATION_MS = 134;
-    private static final float TRACK_SCALE = 0.95f;
-    private static final float GRADIENT_WIDTH = .75f;
 
     /** Experiment to swipe home button left to execute a back key press */
-    private static final String PULL_HOME_GO_BACK_PROP = "quickstepcontroller_homegoesback";
     private static final String HIDE_BACK_BUTTON_PROP = "quickstepcontroller_hideback";
-    private static final String BACK_AFTER_END_PROP
-            = "quickstepcontroller_homegoesbackwhenend";
-    private static final String NAVBAR_EXPERIMENTS_DISABLED = "navbarexperiments_disabled";
-    private static final long BACK_BUTTON_FADE_OUT_ALPHA = 60;
     private static final long BACK_BUTTON_FADE_IN_ALPHA = 150;
-    private static final long BACK_GESTURE_POLL_TIMEOUT = 1000;
 
     /** When the home-swipe-back gesture is disallowed, make it harder to pull */
     private static final float DISALLOW_GESTURE_DAMPING_FACTOR = 0.16f;
 
+    private static final int ACTION_SWIPE_UP_INDEX = 0;
+    private static final int ACTION_SWIPE_DOWN_INDEX = 1;
+    private static final int ACTION_SWIPE_LEFT_INDEX = 2;
+    private static final int ACTION_SWIPE_RIGHT_INDEX = 3;
+    private static final int MAX_GESTURES = 4;
+
     private NavigationBarView mNavigationBarView;
 
-    private boolean mQuickScrubActive;
     private boolean mAllowGestureDetection;
-    private boolean mBackGestureActive;
-    private boolean mCanPerformBack;
-    private boolean mQuickStepStarted;
     private boolean mNotificationsVisibleOnDown;
     private int mTouchDownX;
     private int mTouchDownY;
-    private boolean mDragPositive;
-    private boolean mIsVertical;
+    private boolean mDragHPositive;
+    private boolean mDragVPositive;
     private boolean mIsRTL;
-    private float mTrackAlpha;
-    private float mTrackScale = TRACK_SCALE;
+    private int mNavBarPosition;
     private float mDarkIntensity;
-    private RadialGradient mHighlight;
-    private float mHighlightCenter;
-    private AnimatorSet mTrackAnimator;
-    private ViewPropertyAnimator mHomeAnimator;
+    private ViewPropertyAnimator mDragBtnAnimator;
     private ButtonDispatcher mHitTarget;
-    private View mCurrentNavigationBarView;
     private boolean mIsInScreenPinning;
+    private boolean mGestureHorizontalDragsButton;
+    private boolean mGestureVerticalDragsButton;
+    private boolean mGestureTrackPositive;
 
-    private final Handler mHandler = new Handler();
-    private final Rect mTrackRect = new Rect();
+    private NavigationGestureAction mCurrentAction;
+    private NavigationGestureAction[] mGestureActions = new NavigationGestureAction[MAX_GESTURES];
+
     private final OverviewProxyService mOverviewEventSender;
-    private final int mTrackThickness;
-    private final int mTrackEndPadding;
     private final int mHomeBackGestureDragLimit;
     private final Context mContext;
     private final StatusBar mStatusBar;
     private final Matrix mTransformGlobalMatrix = new Matrix();
     private final Matrix mTransformLocalMatrix = new Matrix();
-    private final Paint mTrackPaint = new Paint();
-
-    private final FloatProperty<QuickStepController> mTrackAlphaProperty =
-            new FloatProperty<QuickStepController>("TrackAlpha") {
-        @Override
-        public void setValue(QuickStepController controller, float alpha) {
-            mTrackAlpha = alpha;
-            mNavigationBarView.invalidate();
-        }
-
-        @Override
-        public Float get(QuickStepController controller) {
-            return mTrackAlpha;
-        }
-    };
-
-    private final FloatProperty<QuickStepController> mTrackScaleProperty =
-            new FloatProperty<QuickStepController>("TrackScale") {
-        @Override
-        public void setValue(QuickStepController controller, float scale) {
-            mTrackScale = scale;
-            mNavigationBarView.invalidate();
-        }
-
-        @Override
-        public Float get(QuickStepController controller) {
-            return mTrackScale;
-        }
-    };
-
-    private final FloatProperty<QuickStepController> mNavBarAlphaProperty =
-            new FloatProperty<QuickStepController>("NavBarAlpha") {
-        @Override
-        public void setValue(QuickStepController controller, float alpha) {
-            if (mCurrentNavigationBarView != null) {
-                mCurrentNavigationBarView.setAlpha(alpha);
-            }
-        }
-
-        @Override
-        public Float get(QuickStepController controller) {
-            if (mCurrentNavigationBarView != null) {
-                return mCurrentNavigationBarView.getAlpha();
-            }
-            return 1f;
-        }
-    };
-
-    private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            resetQuickScrub();
-        }
-    };
-
-    private final Runnable mExecuteBackRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (canPerformHomeBackGesture()) {
-                performBack();
-                mHandler.postDelayed(this, BACK_GESTURE_POLL_TIMEOUT);
-            }
-        }
-    };
 
     public QuickStepController(Context context) {
         final Resources res = context.getResources();
         mContext = context;
         mStatusBar = SysUiServiceProvider.getComponent(context, StatusBar.class);
         mOverviewEventSender = Dependency.get(OverviewProxyService.class);
-        mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness);
-        mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding);
         mHomeBackGestureDragLimit =
                 res.getDimensionPixelSize(R.dimen.nav_home_back_gesture_drag_limit);
-        mTrackPaint.setAntiAlias(true);
-        mTrackPaint.setDither(true);
     }
 
     public void setComponents(NavigationBarView navigationBarView) {
@@ -210,6 +119,31 @@
     }
 
     /**
+     * Set each gesture an action. After set the gestures triggered will run the actions attached.
+     * @param swipeUpAction action after swiping up
+     * @param swipeDownAction action after swiping down
+     * @param swipeLeftAction action after swiping left
+     * @param swipeRightAction action after swiping right
+     */
+    public void setGestureActions(@Nullable NavigationGestureAction swipeUpAction,
+            @Nullable NavigationGestureAction swipeDownAction,
+            @Nullable NavigationGestureAction swipeLeftAction,
+            @Nullable NavigationGestureAction swipeRightAction) {
+        mGestureActions[ACTION_SWIPE_UP_INDEX] = swipeUpAction;
+        mGestureActions[ACTION_SWIPE_DOWN_INDEX] = swipeDownAction;
+        mGestureActions[ACTION_SWIPE_LEFT_INDEX] = swipeLeftAction;
+        mGestureActions[ACTION_SWIPE_RIGHT_INDEX] = swipeRightAction;
+
+        // Set the current state to all actions
+        for (NavigationGestureAction action: mGestureActions) {
+            if (action != null) {
+                action.setBarState(true, mNavBarPosition, mDragHPositive, mDragVPositive);
+                action.onDarkIntensityChange(mDarkIntensity);
+            }
+        }
+    }
+
+    /**
      * @return true if we want to intercept touch events for quick scrub and prevent proxying the
      *         event to the overview service.
      */
@@ -242,8 +176,10 @@
     private boolean handleTouchEvent(MotionEvent event) {
         final boolean deadZoneConsumed =
                 mNavigationBarView.getDownHitTarget() == HIT_TARGET_DEAD_ZONE;
-        if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled()
-                && !mNavigationBarView.isQuickStepSwipeUpEnabled())) {
+
+        // Requires proxy and an active gesture or able to perform any gesture to continue
+        if (mOverviewEventSender.getProxy() == null
+                || (mCurrentAction == null && !canPerformAnyAction())) {
             return deadZoneConsumed;
         }
         mNavigationBarView.requestUnbufferedDispatch(event);
@@ -255,33 +191,45 @@
                 int y = (int) event.getY();
                 mIsInScreenPinning = mNavigationBarView.inScreenPinning();
 
-                // End any existing quickscrub animations before starting the new transition
-                if (mTrackAnimator != null) {
-                    mTrackAnimator.end();
-                    mTrackAnimator = null;
+                for (NavigationGestureAction gestureAction: mGestureActions) {
+                    if (gestureAction != null) {
+                        gestureAction.reset();
+                    }
                 }
 
-                mCurrentNavigationBarView = mNavigationBarView.getCurrentView();
-                mHitTarget = mNavigationBarView.getButtonAtPosition(x, y);
+                // Valid buttons to drag over
+                switch (mNavigationBarView.getDownHitTarget()) {
+                    case HIT_TARGET_BACK:
+                        mHitTarget = mNavigationBarView.getBackButton();
+                        break;
+                    case HIT_TARGET_HOME:
+                        mHitTarget = mNavigationBarView.getHomeButton();
+                        break;
+                    case HIT_TARGET_OVERVIEW:
+                        mHitTarget = mNavigationBarView.getRecentsButton();
+                        break;
+                    default:
+                        mHitTarget = null;
+                        break;
+                }
                 if (mHitTarget != null) {
                     // Pre-emptively delay the touch feedback for the button that we just touched
                     mHitTarget.setDelayTouchFeedback(true);
                 }
                 mTouchDownX = x;
                 mTouchDownY = y;
+                mGestureHorizontalDragsButton = false;
+                mGestureVerticalDragsButton = false;
                 mTransformGlobalMatrix.set(Matrix.IDENTITY_MATRIX);
                 mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX);
                 mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix);
                 mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix);
-                mQuickStepStarted = false;
-                mBackGestureActive = false;
                 mAllowGestureDetection = true;
                 mNotificationsVisibleOnDown = !mNavigationBarView.isNotificationsFullyCollapsed();
-                mCanPerformBack = canPerformHomeBackGesture();
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
-                if (mQuickStepStarted || !mAllowGestureDetection){
+                if (!mAllowGestureDetection) {
                     break;
                 }
                 int x = (int) event.getX();
@@ -289,108 +237,132 @@
                 int xDiff = Math.abs(x - mTouchDownX);
                 int yDiff = Math.abs(y - mTouchDownY);
 
-                boolean exceededScrubTouchSlop, exceededSwipeUpTouchSlop;
-                int pos, touchDown, offset, trackSize;
+                boolean exceededSwipeHorizontalTouchSlop, exceededSwipeVerticalTouchSlop;
+                int posH, touchDownH, posV, touchDownV;
 
-                if (mIsVertical) {
-                    exceededScrubTouchSlop =
+                if (isNavBarVertical()) {
+                    exceededSwipeHorizontalTouchSlop =
                             yDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && yDiff > xDiff;
-                    exceededSwipeUpTouchSlop =
+                    exceededSwipeVerticalTouchSlop =
                             xDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && xDiff > yDiff;
-                    pos = y;
-                    touchDown = mTouchDownY;
-                    offset = pos - mTrackRect.top;
-                    trackSize = mTrackRect.height();
+                    posH = y;
+                    touchDownH = mTouchDownY;
+                    posV = x;
+                    touchDownV = mTouchDownX;
                 } else {
-                    exceededScrubTouchSlop =
+                    exceededSwipeHorizontalTouchSlop =
                             xDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && xDiff > yDiff;
-                    exceededSwipeUpTouchSlop =
+                    exceededSwipeVerticalTouchSlop =
                             yDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && yDiff > xDiff;
-                    pos = x;
-                    touchDown = mTouchDownX;
-                    offset = pos - mTrackRect.left;
-                    trackSize = mTrackRect.width();
-                }
-                // Decide to start quickstep if dragging away from the navigation bar, otherwise in
-                // the parallel direction, decide to start quickscrub. Only one may run.
-                if (!mBackGestureActive && !mQuickScrubActive && exceededSwipeUpTouchSlop) {
-                    if (mNavigationBarView.isQuickStepSwipeUpEnabled()
-                            && !mNotificationsVisibleOnDown) {
-                        startQuickStep(event);
-                    }
-                    break;
+                    posH = x;
+                    touchDownH = mTouchDownX;
+                    posV = y;
+                    touchDownV = mTouchDownY;
                 }
 
-                // Do not handle quick scrub if disabled
-                if (!mNavigationBarView.isQuickScrubEnabled()) {
-                    break;
-                }
-
-                if (!mDragPositive) {
-                    offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
-                }
-
-                final boolean allowDrag = !mDragPositive
-                        ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
-                float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
-                if (!mQuickScrubActive && !mBackGestureActive && exceededScrubTouchSlop) {
-                    // Passing the drag slop then touch slop will start quick step
-                    if (allowDrag) {
-                        startQuickScrub();
-                    } else if (swipeHomeGoBackGestureEnabled(mContext)
-                            && mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME
-                            && mDragPositive ? pos < touchDown : pos > touchDown) {
-                        startBackGesture();
-                    }
-                }
-
-                if (mQuickScrubActive && (mDragPositive && offset >= 0
-                        || !mDragPositive && offset <= 0)) {
-                    try {
-                        mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
-                        if (DEBUG_OVERVIEW_PROXY) {
-                            Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+                if (mCurrentAction != null) {
+                    // Gesture started, provide positions to the current action
+                    mCurrentAction.onGestureMove(x, y);
+                } else {
+                    // Detect gesture and try to execute an action, only one can run at a time
+                    if (exceededSwipeVerticalTouchSlop) {
+                        if (mDragVPositive ? (posV < touchDownV) : (posV > touchDownV)) {
+                            // Swiping up gesture
+                            tryToStartGesture(mGestureActions[ACTION_SWIPE_UP_INDEX],
+                                    false /* alignedWithNavBar */, false /* positiveDirection */,
+                                    event);
+                        } else {
+                            // Swiping down gesture
+                            tryToStartGesture(mGestureActions[ACTION_SWIPE_DOWN_INDEX],
+                                    false /* alignedWithNavBar */, true /* positiveDirection */,
+                                    event);
                         }
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Failed to send progress of quick scrub.", e);
-                    }
-                    mHighlightCenter = x;
-                    mNavigationBarView.invalidate();
-                } else if (mBackGestureActive) {
-                    int diff = pos - touchDown;
-                    // If dragging the incorrect direction after starting back gesture or unable
-                    // to execute back functionality, then move home but dampen its distance
-                    if (!mCanPerformBack || (mDragPositive ? diff > 0 : diff < 0)) {
-                        diff *= DISALLOW_GESTURE_DAMPING_FACTOR;
-                    } if (Math.abs(diff) > mHomeBackGestureDragLimit) {
-                        // Once the user drags the home button past a certain limit, the distance
-                        // will lessen as the home button dampens showing that it was pulled too far
-                        float distanceAfterDragLimit = (Math.abs(diff) - mHomeBackGestureDragLimit)
-                                * DISALLOW_GESTURE_DAMPING_FACTOR;
-                        diff = (int)(distanceAfterDragLimit + mHomeBackGestureDragLimit);
-                        if (mDragPositive) {
-                            diff *= -1;
+                    } else if (exceededSwipeHorizontalTouchSlop) {
+                        if (mDragHPositive ? (posH < touchDownH) : (posH > touchDownH)) {
+                            // Swiping left (ltr) gesture
+                            tryToStartGesture(mGestureActions[ACTION_SWIPE_LEFT_INDEX],
+                                    true /* alignedWithNavBar */, false /* positiveDirection */,
+                                    event);
+                        } else {
+                            // Swiping right (ltr) gesture
+                            tryToStartGesture(mGestureActions[ACTION_SWIPE_RIGHT_INDEX],
+                                    true /* alignedWithNavBar */, true /* positiveDirection */,
+                                    event);
                         }
                     }
-                    moveHomeButton(diff);
                 }
+
+                handleDragHitTarget(mGestureHorizontalDragsButton ? posH : posV,
+                        mGestureHorizontalDragsButton ? touchDownH : touchDownV);
                 break;
             }
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
-                endQuickScrub(true /* animate */);
-                endBackGesture();
+                if (mCurrentAction != null) {
+                    mCurrentAction.endGesture();
+                    mCurrentAction = null;
+                }
+
+                // Return the hit target back to its original position
+                if (mHitTarget != null) {
+                    final View button = mHitTarget.getCurrentView();
+                    if (mGestureHorizontalDragsButton || mGestureVerticalDragsButton) {
+                        mDragBtnAnimator = button.animate().setDuration(BACK_BUTTON_FADE_IN_ALPHA)
+                                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+                        if (mGestureVerticalDragsButton ^ isNavBarVertical()) {
+                            mDragBtnAnimator.translationY(0);
+                        } else {
+                            mDragBtnAnimator.translationX(0);
+                        }
+                        mDragBtnAnimator.start();
+                    }
+                }
                 break;
         }
 
         if (shouldProxyEvents(action)) {
             proxyMotionEvents(event);
         }
-        return mBackGestureActive || mQuickScrubActive || mQuickStepStarted || deadZoneConsumed;
+        return mCurrentAction != null || deadZoneConsumed;
+    }
+
+    private void handleDragHitTarget(int position, int touchDown) {
+        // Drag the hit target if gesture action requires it
+        if (mHitTarget != null && (mGestureVerticalDragsButton || mGestureHorizontalDragsButton)) {
+            final View button = mHitTarget.getCurrentView();
+            if (mDragBtnAnimator != null) {
+                mDragBtnAnimator.cancel();
+                mDragBtnAnimator = null;
+            }
+
+            int diff = position - touchDown;
+            // If dragging the incorrect direction after starting gesture or unable to
+            // execute tried action, then move the button but dampen its distance
+            if (mCurrentAction == null || (mGestureTrackPositive ? diff < 0 : diff > 0)) {
+                diff *= DISALLOW_GESTURE_DAMPING_FACTOR;
+            } else if (Math.abs(diff) > mHomeBackGestureDragLimit) {
+                // Once the user drags the button past a certain limit, the distance will
+                // lessen as the button dampens that it was pulled too far
+                float distanceAfterDragLimit = (Math.abs(diff) - mHomeBackGestureDragLimit)
+                        * DISALLOW_GESTURE_DAMPING_FACTOR;
+                diff = (int) (distanceAfterDragLimit + mHomeBackGestureDragLimit);
+                if (!mGestureTrackPositive) {
+                    diff *= -1;
+                }
+            }
+            if (mGestureVerticalDragsButton ^ isNavBarVertical()) {
+                button.setTranslationY(diff);
+            } else {
+                button.setTranslationX(diff);
+            }
+        }
     }
 
     private boolean shouldProxyEvents(int action) {
-        if (!mBackGestureActive && !mQuickScrubActive && !mIsInScreenPinning) {
+        final boolean actionValid = (mCurrentAction == null
+                || (mGestureActions[ACTION_SWIPE_UP_INDEX] != null
+                        && mGestureActions[ACTION_SWIPE_UP_INDEX].isActive()));
+        if (actionValid && !mIsInScreenPinning) {
             // Allow down, cancel and up events, move and other events are passed if notifications
             // are not showing and disabled gestures (such as long press) are not executed
             switch (action) {
@@ -407,46 +379,18 @@
 
     @Override
     public void onDraw(Canvas canvas) {
-        if (!mNavigationBarView.isQuickScrubEnabled()) {
-            return;
+        if (mCurrentAction != null) {
+            mCurrentAction.onDraw(canvas);
         }
-        mTrackPaint.setAlpha(Math.round(255f * mTrackAlpha));
-
-        // Scale the track, but apply the inverse scale from the nav bar
-        final float radius = mTrackRect.height() / 2;
-        canvas.save();
-        float translate = Utilities.clamp(mHighlightCenter, mTrackRect.left, mTrackRect.right);
-        canvas.translate(translate, 0);
-        canvas.scale(mTrackScale / mNavigationBarView.getScaleX(),
-                1f / mNavigationBarView.getScaleY(),
-                mTrackRect.centerX(), mTrackRect.centerY());
-        canvas.drawRoundRect(mTrackRect.left - translate, mTrackRect.top,
-                mTrackRect.right - translate, mTrackRect.bottom, radius, radius, mTrackPaint);
-        canvas.restore();
     }
 
     @Override
     public void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        final int paddingLeft = mNavigationBarView.getPaddingLeft();
-        final int paddingTop = mNavigationBarView.getPaddingTop();
-        final int paddingRight = mNavigationBarView.getPaddingRight();
-        final int paddingBottom = mNavigationBarView.getPaddingBottom();
-        final int width = (right - left) - paddingRight - paddingLeft;
-        final int height = (bottom - top) - paddingBottom - paddingTop;
-        final int x1, x2, y1, y2;
-        if (mIsVertical) {
-            x1 = (width - mTrackThickness) / 2 + paddingLeft;
-            x2 = x1 + mTrackThickness;
-            y1 = paddingTop + mTrackEndPadding;
-            y2 = y1 + height - 2 * mTrackEndPadding;
-        } else {
-            y1 = (height - mTrackThickness) / 2 + paddingTop;
-            y2 = y1 + mTrackThickness;
-            x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding;
-            x2 = x1 + width - 2 * mTrackEndPadding;
+        for (NavigationGestureAction action: mGestureActions) {
+            if (action != null) {
+                action.onLayout(changed, left, top, right, bottom);
+            }
         }
-        mTrackRect.set(x1, y1, x2, y2);
-        updateHighlight();
     }
 
     @Override
@@ -456,119 +400,104 @@
 
         // When in quick scrub, invalidate gradient if changing intensity from black to white and
         // vice-versa
-        if (mNavigationBarView.isQuickScrubEnabled()
+        if (mCurrentAction != null && mNavigationBarView.isQuickScrubEnabled()
                 && Math.round(intensity) != Math.round(oldIntensity)) {
-            updateHighlight();
+            mCurrentAction.onDarkIntensityChange(mDarkIntensity);
         }
         mNavigationBarView.invalidate();
     }
 
     @Override
-    public void setBarState(boolean isVertical, boolean isRTL) {
-        final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL);
-        if (changed) {
-            // End quickscrub if the state changes mid-transition
-            endQuickScrub(false /* animate */);
-        }
-        mIsVertical = isVertical;
+    public void setBarState(boolean isRTL, int navBarPosition) {
+        final boolean changed = (mIsRTL != isRTL) || (mNavBarPosition != navBarPosition);
         mIsRTL = isRTL;
-        try {
-            int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
-            mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
-            if (isRTL) {
-                mDragPositive = !mDragPositive;
+        mNavBarPosition = navBarPosition;
+
+        // Determine the drag directions depending on location of nav bar
+        switch (navBarPosition) {
+            case NAV_BAR_LEFT:
+                mDragHPositive = !isRTL;
+                mDragVPositive = false;
+                break;
+            case NAV_BAR_RIGHT:
+                mDragHPositive = isRTL;
+                mDragVPositive = true;
+                break;
+            case NAV_BAR_BOTTOM:
+                mDragHPositive = !isRTL;
+                mDragVPositive = true;
+                break;
+        }
+
+        for (NavigationGestureAction action: mGestureActions) {
+            if (action != null) {
+                action.setBarState(changed, mNavBarPosition, mDragHPositive, mDragVPositive);
             }
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Failed to get nav bar position.", e);
         }
     }
 
     @Override
     public void onNavigationButtonLongPress(View v) {
         mAllowGestureDetection = false;
-        mHandler.removeCallbacksAndMessages(null);
     }
 
     @Override
     public void dump(PrintWriter pw) {
         pw.println("QuickStepController {");
-        pw.print("    "); pw.println("mQuickScrubActive=" + mQuickScrubActive);
-        pw.print("    "); pw.println("mQuickStepStarted=" + mQuickStepStarted);
         pw.print("    "); pw.println("mAllowGestureDetection=" + mAllowGestureDetection);
-        pw.print("    "); pw.println("mBackGestureActive=" + mBackGestureActive);
-        pw.print("    "); pw.println("mCanPerformBack=" + mCanPerformBack);
         pw.print("    "); pw.println("mNotificationsVisibleOnDown=" + mNotificationsVisibleOnDown);
-        pw.print("    "); pw.println("mIsVertical=" + mIsVertical);
+        pw.print("    "); pw.println("mNavBarPosition=" + mNavBarPosition);
         pw.print("    "); pw.println("mIsRTL=" + mIsRTL);
         pw.print("    "); pw.println("mIsInScreenPinning=" + mIsInScreenPinning);
         pw.println("}");
     }
 
-    private void startQuickStep(MotionEvent event) {
-        if (mIsInScreenPinning) {
-            mNavigationBarView.showPinningEscapeToast();
-            mAllowGestureDetection = false;
-            return;
-        }
-
-        mQuickStepStarted = true;
-        event.transform(mTransformGlobalMatrix);
-        try {
-            mOverviewEventSender.getProxy().onQuickStep(event);
-            if (DEBUG_OVERVIEW_PROXY) {
-                Log.d(TAG_OPS, "Quick Step Start");
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to send quick step started.", e);
-        } finally {
-            event.transform(mTransformLocalMatrix);
-        }
-        mOverviewEventSender.notifyQuickStepStarted();
-        mHandler.removeCallbacksAndMessages(null);
-
-        if (mHitTarget != null) {
-            mHitTarget.abortCurrentGesture();
-        }
-
-        if (mQuickScrubActive) {
-            animateEnd();
-        }
+    public NavigationGestureAction getCurrentAction() {
+        return mCurrentAction;
     }
 
-    private void startQuickScrub() {
+    private void tryToStartGesture(NavigationGestureAction action, boolean alignedWithNavBar,
+            boolean positiveDirection, MotionEvent event) {
+        if (action == null) {
+            return;
+        }
         if (mIsInScreenPinning) {
             mNavigationBarView.showPinningEscapeToast();
             mAllowGestureDetection = false;
             return;
         }
 
-        if (!mQuickScrubActive) {
-            updateHighlight();
-            mQuickScrubActive = true;
-            ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
-                    PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f),
-                    PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f));
-            trackAnimator.setInterpolator(ALPHA_IN);
-            trackAnimator.setDuration(ANIM_IN_DURATION_MS);
-            ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f);
-            navBarAnimator.setInterpolator(ALPHA_OUT);
-            navBarAnimator.setDuration(ANIM_OUT_DURATION_MS);
-            mTrackAnimator = new AnimatorSet();
-            mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
-            mTrackAnimator.start();
-
-            // Disable slippery for quick scrub to not cancel outside the nav bar
-            mNavigationBarView.updateSlippery();
-
-            try {
-                mOverviewEventSender.getProxy().onQuickScrubStart();
-                if (DEBUG_OVERVIEW_PROXY) {
-                    Log.d(TAG_OPS, "Quick Scrub Start");
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to send start of quick scrub.", e);
+        // Start new action from gesture if is able to start and depending on notifications
+        // visibility and starting touch down target. If the action is enabled, then also check if
+        // can perform the action so that if action requires the button to be dragged, then the
+        // gesture will have a large dampening factor and prevent action from running.
+        final boolean validHitTarget = action.requiresTouchDownHitTarget() == HIT_TARGET_NONE
+                || action.requiresTouchDownHitTarget() == mNavigationBarView.getDownHitTarget();
+        if (mCurrentAction == null && validHitTarget && action.isEnabled()
+                && (!mNotificationsVisibleOnDown || action.canRunWhenNotificationsShowing())) {
+            if (action.canPerformAction()) {
+                mCurrentAction = action;
+                event.transform(mTransformGlobalMatrix);
+                action.startGesture(event);
+                event.transform(mTransformLocalMatrix);
             }
-            mOverviewEventSender.notifyQuickScrubStarted();
+
+            // Handle direction of the hit target drag from the axis that started the gesture
+            if (action.requiresDragWithHitTarget()) {
+                if (alignedWithNavBar) {
+                    mGestureHorizontalDragsButton = true;
+                    mGestureVerticalDragsButton = false;
+                    if (positiveDirection) {
+                        mGestureTrackPositive = mDragHPositive;
+                    }
+                } else {
+                    mGestureVerticalDragsButton = true;
+                    mGestureHorizontalDragsButton = false;
+                    if (positiveDirection) {
+                        mGestureTrackPositive = mDragVPositive;
+                    }
+                }
+            }
 
             if (mHitTarget != null) {
                 mHitTarget.abortCurrentGesture();
@@ -576,148 +505,13 @@
         }
     }
 
-    private void endQuickScrub(boolean animate) {
-        if (mQuickScrubActive) {
-            animateEnd();
-            try {
-                mOverviewEventSender.getProxy().onQuickScrubEnd();
-                if (DEBUG_OVERVIEW_PROXY) {
-                    Log.d(TAG_OPS, "Quick Scrub End");
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to send end of quick scrub.", e);
+    private boolean canPerformAnyAction() {
+        for (NavigationGestureAction action: mGestureActions) {
+            if (action != null && action.isEnabled()) {
+                return true;
             }
         }
-        if (!animate) {
-            if (mTrackAnimator != null) {
-                mTrackAnimator.end();
-                mTrackAnimator = null;
-            }
-        }
-    }
-
-    private void startBackGesture() {
-        if (!mBackGestureActive) {
-            mBackGestureActive = true;
-            mNavigationBarView.getHomeButton().abortCurrentGesture();
-            final boolean runBackMidGesture = !shouldExecuteBackOnUp(mContext);
-            if (mCanPerformBack) {
-                if (!shouldhideBackButton(mContext)) {
-                    mNavigationBarView.getBackButton().setAlpha(0 /* alpha */, true /* animate */,
-                            BACK_BUTTON_FADE_OUT_ALPHA);
-                }
-                if (runBackMidGesture) {
-                    performBack();
-                }
-            }
-            mHandler.removeCallbacks(mExecuteBackRunnable);
-            if (runBackMidGesture) {
-                mHandler.postDelayed(mExecuteBackRunnable, BACK_GESTURE_POLL_TIMEOUT);
-            }
-        }
-    }
-
-    private void endBackGesture() {
-        if (mBackGestureActive) {
-            mHandler.removeCallbacks(mExecuteBackRunnable);
-            mHomeAnimator = mNavigationBarView.getHomeButton().getCurrentView()
-                    .animate()
-                    .setDuration(BACK_BUTTON_FADE_IN_ALPHA)
-                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            if (mIsVertical) {
-                mHomeAnimator.translationY(0);
-            } else {
-                mHomeAnimator.translationX(0);
-            }
-            mHomeAnimator.start();
-            if (!shouldhideBackButton(mContext)) {
-                mNavigationBarView.getBackButton().setAlpha(
-                        mOverviewEventSender.getBackButtonAlpha(), true /* animate */);
-            }
-            if (shouldExecuteBackOnUp(mContext)) {
-                performBack();
-            }
-        }
-    }
-
-    private void animateEnd() {
-        if (mTrackAnimator != null) {
-            mTrackAnimator.cancel();
-        }
-
-        ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
-                PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f),
-                PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE));
-        trackAnimator.setInterpolator(ALPHA_OUT);
-        trackAnimator.setDuration(ANIM_OUT_DURATION_MS);
-        ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f);
-        navBarAnimator.setInterpolator(ALPHA_IN);
-        navBarAnimator.setDuration(ANIM_IN_DURATION_MS);
-        mTrackAnimator = new AnimatorSet();
-        mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
-        mTrackAnimator.addListener(mQuickScrubEndListener);
-        mTrackAnimator.start();
-    }
-
-    private void resetQuickScrub() {
-        mQuickScrubActive = false;
-        mAllowGestureDetection = false;
-        if (mCurrentNavigationBarView != null) {
-            mCurrentNavigationBarView.setAlpha(1f);
-        }
-        mCurrentNavigationBarView = null;
-        updateHighlight();
-    }
-
-    private void moveHomeButton(float pos) {
-        if (mHomeAnimator != null) {
-            mHomeAnimator.cancel();
-            mHomeAnimator = null;
-        }
-        final View homeButton = mNavigationBarView.getHomeButton().getCurrentView();
-        if (mIsVertical) {
-            homeButton.setTranslationY(pos);
-        } else {
-            homeButton.setTranslationX(pos);
-        }
-    }
-
-    private void updateHighlight() {
-        if (mTrackRect.isEmpty()) {
-            return;
-        }
-        int colorBase, colorGrad;
-        if (mDarkIntensity > 0.5f) {
-            colorBase = mContext.getColor(R.color.quick_step_track_background_background_dark);
-            colorGrad = mContext.getColor(R.color.quick_step_track_background_foreground_dark);
-        } else {
-            colorBase = mContext.getColor(R.color.quick_step_track_background_background_light);
-            colorGrad = mContext.getColor(R.color.quick_step_track_background_foreground_light);
-        }
-        mHighlight = new RadialGradient(0, mTrackRect.height() / 2,
-                mTrackRect.width() * GRADIENT_WIDTH, colorGrad, colorBase,
-                Shader.TileMode.CLAMP);
-        mTrackPaint.setShader(mHighlight);
-    }
-
-    private boolean canPerformHomeBackGesture() {
-        return swipeHomeGoBackGestureEnabled(mContext)
-                && mOverviewEventSender.getBackButtonAlpha() > 0;
-    }
-
-    private void performBack() {
-        sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
-        sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
-        mNavigationBarView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
-    }
-
-    private void sendEvent(int action, int code) {
-        long when = SystemClock.uptimeMillis();
-        final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
-                0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
-                KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
-                InputDevice.SOURCE_KEYBOARD);
-        InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
+        return false;
     }
 
     private boolean proxyMotionEvents(MotionEvent event) {
@@ -740,22 +534,15 @@
         return false;
     }
 
-    private static boolean getBoolGlobalSetting(Context context, String key) {
+    protected boolean isNavBarVertical() {
+        return mNavBarPosition == NAV_BAR_LEFT || mNavBarPosition == NAV_BAR_RIGHT;
+    }
+
+    static boolean getBoolGlobalSetting(Context context, String key) {
         return Settings.Global.getInt(context.getContentResolver(), key, 0) != 0;
     }
 
-    public static boolean swipeHomeGoBackGestureEnabled(Context context) {
-        return !getBoolGlobalSetting(context, NAVBAR_EXPERIMENTS_DISABLED)
-            && getBoolGlobalSetting(context, PULL_HOME_GO_BACK_PROP);
-    }
-
     public static boolean shouldhideBackButton(Context context) {
-        return swipeHomeGoBackGestureEnabled(context)
-            && getBoolGlobalSetting(context, HIDE_BACK_BUTTON_PROP);
-    }
-
-    public static boolean shouldExecuteBackOnUp(Context context) {
-        return !getBoolGlobalSetting(context, NAVBAR_EXPERIMENTS_DISABLED)
-            && getBoolGlobalSetting(context, BACK_AFTER_END_PROP);
+        return getBoolGlobalSetting(context, HIDE_BACK_BUTTON_PROP);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java
new file mode 100644
index 0000000..0781602
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/QuickStepControllerTest.java
@@ -0,0 +1,618 @@
+/*
+ * 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.phone;
+
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT;
+
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_DEAD_ZONE;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+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 com.android.systemui.R;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.SysuiTestCase;
+
+import android.content.res.Resources;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper.RunWithLooper;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockitoAnnotations;
+
+/** atest QuickStepControllerTest */
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class QuickStepControllerTest extends SysuiTestCase {
+    private QuickStepController mController;
+    private NavigationBarView mNavigationBarView;
+    private StatusBar mStatusBar;
+    private OverviewProxyService mProxyService;
+    private IOverviewProxy mProxy;
+    private Resources mResources;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        final ButtonDispatcher backButton = mock(ButtonDispatcher.class);
+        mResources = mock(Resources.class);
+
+        mProxyService = mock(OverviewProxyService.class);
+        mProxy = mock(IOverviewProxy.Stub.class);
+        doReturn(mProxy).when(mProxyService).getProxy();
+        mDependency.injectTestDependency(OverviewProxyService.class, mProxyService);
+
+        mStatusBar = mock(StatusBar.class);
+        doReturn(false).when(mStatusBar).isKeyguardShowing();
+        mContext.putComponent(StatusBar.class, mStatusBar);
+
+        mNavigationBarView = mock(NavigationBarView.class);
+        doReturn(false).when(mNavigationBarView).inScreenPinning();
+        doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed();
+        doReturn(true).when(mNavigationBarView).isQuickScrubEnabled();
+        doReturn(HIT_TARGET_NONE).when(mNavigationBarView).getDownHitTarget();
+        doReturn(backButton).when(mNavigationBarView).getBackButton();
+        doReturn(mResources).when(mNavigationBarView).getResources();
+
+        mController = new QuickStepController(mContext);
+        mController.setComponents(mNavigationBarView);
+        mController.setBarState(false /* isRTL */, NAV_BAR_BOTTOM);
+    }
+
+    @Test
+    public void testNoActionsNoGestures() throws Exception {
+        MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+        assertFalse(mController.onInterceptTouchEvent(ev));
+        verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev);
+        assertNull(mController.getCurrentAction());
+    }
+
+    @Test
+    public void testHasActionDetectGesturesTouchdown() throws Exception {
+        MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+
+        // Add enabled gesture action
+        NavigationGestureAction action = mockAction(true);
+        mController.setGestureActions(action, null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        assertFalse(mController.onInterceptTouchEvent(ev));
+        verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev);
+        verify(action, times(1)).reset();
+        verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+        verify(mProxy, times(1)).onMotionEvent(ev);
+        assertNull(mController.getCurrentAction());
+    }
+
+    @Test
+    public void testProxyDisconnectedNoGestures() throws Exception {
+        MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+
+        // Add enabled gesture action
+        mController.setGestureActions(mockAction(true), null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        // Set the gesture on deadzone
+        doReturn(null).when(mProxyService).getProxy();
+
+        assertFalse(mController.onInterceptTouchEvent(ev));
+        verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev);
+        assertNull(mController.getCurrentAction());
+    }
+
+    @Test
+    public void testNoActionsNoGesturesOverDeadzone() throws Exception {
+        MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+
+        // Touched over deadzone
+        doReturn(HIT_TARGET_DEAD_ZONE).when(mNavigationBarView).getDownHitTarget();
+
+        assertTrue(mController.onInterceptTouchEvent(ev));
+        verify(mNavigationBarView, never()).requestUnbufferedDispatch(ev);
+        assertNull(mController.getCurrentAction());
+    }
+
+    @Test
+    public void testOnTouchIgnoredDownEventAfterOnIntercept() {
+        mController.setGestureActions(mockAction(true), null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        MotionEvent ev = event(MotionEvent.ACTION_DOWN, 1, 1);
+        assertFalse(touch(ev));
+        verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev);
+
+        // OnTouch event for down is ignored, so requestUnbufferedDispatch ran once from before
+        assertFalse(mNavigationBarView.onTouchEvent(ev));
+        verify(mNavigationBarView, times(1)).requestUnbufferedDispatch(ev);
+    }
+
+    @Test
+    public void testGesturesCallCorrectAction() throws Exception {
+        NavigationGestureAction swipeUp = mockAction(true);
+        NavigationGestureAction swipeDown = mockAction(true);
+        NavigationGestureAction swipeLeft = mockAction(true);
+        NavigationGestureAction swipeRight = mockAction(true);
+        mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+        // Swipe Up
+        assertGestureTriggersAction(swipeUp, 1, 100, 5, 1);
+        // Swipe Down
+        assertGestureTriggersAction(swipeDown, 1, 1, 5, 100);
+        // Swipe Left
+        assertGestureTriggersAction(swipeLeft, 100, 1, 5, 1);
+        // Swipe Right
+        assertGestureTriggersAction(swipeRight, 1, 1, 100, 5);
+    }
+
+    @Test
+    public void testGesturesCallCorrectActionLandscape() throws Exception {
+        NavigationGestureAction swipeUp = mockAction(true);
+        NavigationGestureAction swipeDown = mockAction(true);
+        NavigationGestureAction swipeLeft = mockAction(true);
+        NavigationGestureAction swipeRight = mockAction(true);
+        mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+        // In landscape
+        mController.setBarState(false /* isRTL */, NAV_BAR_RIGHT);
+
+        // Swipe Up
+        assertGestureTriggersAction(swipeRight, 1, 100, 5, 1);
+        // Swipe Down
+        assertGestureTriggersAction(swipeLeft, 1, 1, 5, 100);
+        // Swipe Left
+        assertGestureTriggersAction(swipeUp, 100, 1, 5, 1);
+        // Swipe Right
+        assertGestureTriggersAction(swipeDown, 1, 1, 100, 5);
+    }
+
+    @Test
+    public void testGesturesCallCorrectActionSeascape() throws Exception {
+        mController.setBarState(false /* isRTL */, NAV_BAR_LEFT);
+        NavigationGestureAction swipeUp = mockAction(true);
+        NavigationGestureAction swipeDown = mockAction(true);
+        NavigationGestureAction swipeLeft = mockAction(true);
+        NavigationGestureAction swipeRight = mockAction(true);
+        mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+        // Swipe Up
+        assertGestureTriggersAction(swipeLeft, 1, 100, 5, 1);
+        // Swipe Down
+        assertGestureTriggersAction(swipeRight, 1, 1, 5, 100);
+        // Swipe Left
+        assertGestureTriggersAction(swipeDown, 100, 1, 5, 1);
+        // Swipe Right
+        assertGestureTriggersAction(swipeUp, 1, 1, 100, 5);
+    }
+
+    @Test
+    public void testGesturesCallCorrectActionRTL() throws Exception {
+        mController.setBarState(true /* isRTL */, NAV_BAR_BOTTOM);
+
+        // The swipe gestures below are for LTR, so RTL in portrait will be swapped
+        NavigationGestureAction swipeUp = mockAction(true);
+        NavigationGestureAction swipeDown = mockAction(true);
+        NavigationGestureAction swipeLeft = mockAction(true);
+        NavigationGestureAction swipeRight = mockAction(true);
+        mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+        // Swipe Up in RTL
+        assertGestureTriggersAction(swipeUp, 1, 100, 5, 1);
+        // Swipe Down in RTL
+        assertGestureTriggersAction(swipeDown, 1, 1, 5, 100);
+        // Swipe Left in RTL
+        assertGestureTriggersAction(swipeRight, 100, 1, 5, 1);
+        // Swipe Right in RTL
+        assertGestureTriggersAction(swipeLeft, 1, 1, 100, 5);
+    }
+
+    @Test
+    public void testGesturesCallCorrectActionLandscapeRTL() throws Exception {
+        mController.setBarState(true /* isRTL */, NAV_BAR_RIGHT);
+
+        // The swipe gestures below are for LTR, so RTL in landscape will be swapped
+        NavigationGestureAction swipeUp = mockAction(true);
+        NavigationGestureAction swipeDown = mockAction(true);
+        NavigationGestureAction swipeLeft = mockAction(true);
+        NavigationGestureAction swipeRight = mockAction(true);
+        mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+        // Swipe Up
+        assertGestureTriggersAction(swipeLeft, 1, 100, 5, 1);
+        // Swipe Down
+        assertGestureTriggersAction(swipeRight, 1, 1, 5, 100);
+        // Swipe Left
+        assertGestureTriggersAction(swipeUp, 100, 1, 5, 1);
+        // Swipe Right
+        assertGestureTriggersAction(swipeDown, 1, 1, 100, 5);
+    }
+
+    @Test
+    public void testGesturesCallCorrectActionSeascapeRTL() throws Exception {
+        mController.setBarState(true /* isRTL */, NAV_BAR_LEFT);
+
+        // The swipe gestures below are for LTR, so RTL in seascape will be swapped
+        NavigationGestureAction swipeUp = mockAction(true);
+        NavigationGestureAction swipeDown = mockAction(true);
+        NavigationGestureAction swipeLeft = mockAction(true);
+        NavigationGestureAction swipeRight = mockAction(true);
+        mController.setGestureActions(swipeUp, swipeDown, swipeLeft, swipeRight);
+
+        // Swipe Up
+        assertGestureTriggersAction(swipeRight, 1, 100, 5, 1);
+        // Swipe Down
+        assertGestureTriggersAction(swipeLeft, 1, 1, 5, 100);
+        // Swipe Left
+        assertGestureTriggersAction(swipeDown, 100, 1, 5, 1);
+        // Swipe Right
+        assertGestureTriggersAction(swipeUp, 1, 1, 100, 5);
+    }
+
+    @Test
+    public void testActionPreventByPinnedState() throws Exception {
+        // Screen is pinned
+        doReturn(true).when(mNavigationBarView).inScreenPinning();
+
+        // Add enabled gesture action
+        NavigationGestureAction action = mockAction(true);
+        mController.setGestureActions(action, null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        // Touch down to begin swipe
+        MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 1, 100);
+        assertFalse(touch(downEvent));
+        verify(mProxy, never()).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+        verify(mProxy, never()).onMotionEvent(downEvent);
+
+        // Move to start gesture, but pinned so it should not trigger action
+        MotionEvent moveEvent = event(MotionEvent.ACTION_MOVE, 1, 1);
+        assertFalse(touch(moveEvent));
+        assertNull(mController.getCurrentAction());
+        verify(mNavigationBarView, times(1)).showPinningEscapeToast();
+        verify(action, never()).onGestureStart(moveEvent);
+    }
+
+    @Test
+    public void testActionPreventedNotificationsShown() throws Exception {
+        NavigationGestureAction action = mockAction(true);
+        doReturn(false).when(action).canRunWhenNotificationsShowing();
+        mController.setGestureActions(action, null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        // Show the notifications
+        doReturn(false).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+        // Swipe up
+        assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+        assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1));
+        assertNull(mController.getCurrentAction());
+        assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+
+        // Hide the notifications
+        doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+        // Swipe up
+        assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+        assertTrue(touch(MotionEvent.ACTION_MOVE, 1, 1));
+        assertEquals(action, mController.getCurrentAction());
+        assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+    }
+
+    @Test
+    public void testActionCannotPerform() throws Exception {
+        NavigationGestureAction action = mockAction(true);
+        mController.setGestureActions(action, null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        // Cannot perform action
+        doReturn(false).when(action).canPerformAction();
+
+        // Swipe up
+        assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+        assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1));
+        assertNull(mController.getCurrentAction());
+        assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+
+        // Cannot perform action
+        doReturn(true).when(action).canPerformAction();
+
+        // Swipe up
+        assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+        assertTrue(touch(MotionEvent.ACTION_MOVE, 1, 1));
+        assertEquals(action, mController.getCurrentAction());
+        assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+    }
+
+    @Test
+    public void testQuickScrub() throws Exception {
+        QuickScrubAction action = spy(new QuickScrubAction(mNavigationBarView, mProxyService));
+        mController.setGestureActions(null /* swipeUpAction */, null /* swipeDownAction */,
+                null /* swipeLeftAction */, action);
+        int y = 20;
+
+        // Set the layout and other padding to make sure the scrub fraction is calculated correctly
+        action.onLayout(true, 0, 0, 400, 100);
+        doReturn(0).when(mNavigationBarView).getPaddingLeft();
+        doReturn(0).when(mNavigationBarView).getPaddingRight();
+        doReturn(0).when(mNavigationBarView).getPaddingStart();
+        doReturn(0).when(mResources)
+                .getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding);
+
+        // Quickscrub disabled, so the action should be disabled
+        doReturn(false).when(mNavigationBarView).isQuickScrubEnabled();
+        assertFalse(action.isEnabled());
+        doReturn(true).when(mNavigationBarView).isQuickScrubEnabled();
+
+        // Touch down
+        MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 0, y);
+        assertFalse(touch(downEvent));
+        assertNull(mController.getCurrentAction());
+        verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+        verify(mProxy, times(1)).onMotionEvent(downEvent);
+
+        // Move to start trigger action from gesture
+        MotionEvent moveEvent1 = event(MotionEvent.ACTION_MOVE, 100, y);
+        assertTrue(touch(moveEvent1));
+        assertEquals(action, mController.getCurrentAction());
+        verify(action, times(1)).onGestureStart(moveEvent1);
+        verify(mProxy, times(1)).onQuickScrubStart();
+        verify(mProxyService, times(1)).notifyQuickScrubStarted();
+        verify(mNavigationBarView, times(1)).updateSlippery();
+
+        // Move again for scrub
+        MotionEvent moveEvent2 = event(MotionEvent.ACTION_MOVE, 200, y);
+        assertTrue(touch(moveEvent2));
+        assertEquals(action, mController.getCurrentAction());
+        verify(action, times(1)).onGestureMove(200, y);
+        verify(mProxy, times(1)).onQuickScrubProgress(1f / 2);
+
+        // Action up
+        MotionEvent upEvent = event(MotionEvent.ACTION_UP, 1, y);
+        assertFalse(touch(upEvent));
+        assertNull(mController.getCurrentAction());
+        verify(action, times(1)).onGestureEnd();
+        verify(mProxy, times(1)).onQuickScrubEnd();
+    }
+
+    @Test
+    public void testQuickStep() throws Exception {
+        QuickStepAction action = new QuickStepAction(mNavigationBarView, mProxyService);
+        mController.setGestureActions(action, null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        // Notifications are up, should prevent quickstep
+        doReturn(false).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+        // Swipe up
+        assertFalse(touch(MotionEvent.ACTION_DOWN, 1, 100));
+        assertNull(mController.getCurrentAction());
+        assertFalse(touch(MotionEvent.ACTION_MOVE, 1, 1));
+        assertNull(mController.getCurrentAction());
+        assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+        doReturn(true).when(mNavigationBarView).isNotificationsFullyCollapsed();
+
+        // Quickstep disabled, so the action should be disabled
+        doReturn(false).when(mNavigationBarView).isQuickStepSwipeUpEnabled();
+        assertFalse(action.isEnabled());
+        doReturn(true).when(mNavigationBarView).isQuickStepSwipeUpEnabled();
+
+        // Swipe up should call proxy events
+        MotionEvent downEvent = event(MotionEvent.ACTION_DOWN, 1, 100);
+        assertFalse(touch(downEvent));
+        assertNull(mController.getCurrentAction());
+        verify(mProxy, times(1)).onPreMotionEvent(mNavigationBarView.getDownHitTarget());
+        verify(mProxy, times(1)).onMotionEvent(downEvent);
+
+        MotionEvent moveEvent = event(MotionEvent.ACTION_MOVE, 1, 1);
+        assertTrue(touch(moveEvent));
+        assertEquals(action, mController.getCurrentAction());
+        verify(mProxy, times(1)).onQuickStep(moveEvent);
+        verify(mProxyService, times(1)).notifyQuickStepStarted();
+    }
+
+    @Test
+    public void testLongPressPreventDetection() throws Exception {
+        NavigationGestureAction action = mockAction(true);
+        mController.setGestureActions(action, null /* swipeDownAction */,
+                null /* swipeLeftAction */, null /* swipeRightAction */);
+
+        // Start the drag up
+        assertFalse(touch(MotionEvent.ACTION_DOWN, 100, 1));
+        assertNull(mController.getCurrentAction());
+
+        // Long press something on the navigation bar such as Home button
+        mNavigationBarView.onNavigationButtonLongPress(mock(View.class));
+
+        // Swipe right will not start any gestures
+        MotionEvent motionMoveEvent = event(MotionEvent.ACTION_MOVE, 1, 1);
+        assertFalse(touch(motionMoveEvent));
+        assertNull(mController.getCurrentAction());
+        verify(action, never()).startGesture(motionMoveEvent);
+
+        // Touch up
+        assertFalse(touch(MotionEvent.ACTION_UP, 1, 1));
+        verify(action, never()).endGesture();
+    }
+
+    @Test
+    public void testHitTargetDragged() throws Exception {
+        ButtonDispatcher button = mock(ButtonDispatcher.class);
+        View buttonView = spy(new View(mContext));
+        doReturn(buttonView).when(button).getCurrentView();
+
+        NavigationGestureAction action = mockAction(true);
+        mController.setGestureActions(action, action, action, action);
+
+        // Setup getting the hit target
+        doReturn(HIT_TARGET_HOME).when(action).requiresTouchDownHitTarget();
+        doReturn(true).when(action).requiresDragWithHitTarget();
+        doReturn(HIT_TARGET_HOME).when(mNavigationBarView).getDownHitTarget();
+        doReturn(button).when(mNavigationBarView).getHomeButton();
+
+        // Portrait
+        assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_BOTTOM);
+
+        // Portrait RTL
+        assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_BOTTOM);
+
+        // Landscape
+        assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_RIGHT);
+
+        // Landscape RTL
+        assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_RIGHT);
+
+        // Seascape
+        assertGestureDragsHitTargetAllDirections(buttonView, false /* isRTL */, NAV_BAR_LEFT);
+
+        // Seascape RTL
+        assertGestureDragsHitTargetAllDirections(buttonView, true /* isRTL */, NAV_BAR_LEFT);
+    }
+
+    private void assertGestureDragsHitTargetAllDirections(View buttonView, boolean isRTL,
+            int navPos) {
+        mController.setBarState(isRTL, navPos);
+
+        // Swipe up
+        assertGestureDragsHitTarget(buttonView, 10 /* x1 */, 200 /* y1 */, 0 /* x2 */, 0 /* y2 */,
+                0 /* dx */, -1 /* dy */);
+        // Swipe left
+        assertGestureDragsHitTarget(buttonView, 200 /* x1 */, 10 /* y1 */, 0 /* x2 */, 0 /* y2 */,
+                -1 /* dx */, 0 /* dy */);
+        // Swipe right
+        assertGestureDragsHitTarget(buttonView, 0 /* x1 */, 0 /* y1 */, 200 /* x2 */, 10 /* y2 */,
+                1 /* dx */, 0 /* dy */);
+        // Swipe down
+        assertGestureDragsHitTarget(buttonView, 0 /* x1 */, 0 /* y1 */, 10 /* x2 */, 200 /* y2 */,
+                0 /* dx */, 1 /* dy */);
+    }
+
+    /**
+     * Asserts the gesture actually moves the hit target
+     * @param buttonView button to check if moved, use Mockito.spy on a real object
+     * @param x1 start x
+     * @param x2 start y
+     * @param y1 end x
+     * @param y2 end y
+     * @param dx diff in x, if not 0, its sign determines direction, value does not matter
+     * @param dy diff in y, if not 0, its sign determines direction, value does not matter
+     */
+    private void assertGestureDragsHitTarget(View buttonView, int x1, int y1, int x2, int y2,
+            int dx, int dy) {
+        ArgumentCaptor<Float> captor = ArgumentCaptor.forClass(Float.class);
+        assertFalse(touch(MotionEvent.ACTION_DOWN, x1, y1));
+        assertTrue(touch(MotionEvent.ACTION_MOVE, x2, y2));
+
+        // Verify positions of the button drag
+        if (dx == 0) {
+            verify(buttonView, never()).setTranslationX(anyFloat());
+        } else {
+            verify(buttonView).setTranslationX(captor.capture());
+            if (dx < 0) {
+                assertTrue("Button should have moved left", (float) captor.getValue() < 0);
+            } else {
+                assertTrue("Button should have moved right", (float) captor.getValue() > 0);
+            }
+        }
+        if (dy == 0) {
+            verify(buttonView, never()).setTranslationY(anyFloat());
+        } else {
+            verify(buttonView).setTranslationY(captor.capture());
+            if (dy < 0) {
+                assertTrue("Button should have moved up", (float) captor.getValue() < 0);
+            } else {
+                assertTrue("Button should have moved down", (float) captor.getValue() > 0);
+            }
+        }
+
+        // Touch up
+        assertFalse(touch(MotionEvent.ACTION_UP, x2, y2));
+        verify(buttonView, times(1)).animate();
+
+        // Reset button state
+        reset(buttonView);
+    }
+
+
+    private MotionEvent event(int action, float x, float y) {
+        final MotionEvent event = mock(MotionEvent.class);
+        doReturn(x).when(event).getX();
+        doReturn(y).when(event).getY();
+        doReturn(action & MotionEvent.ACTION_MASK).when(event).getActionMasked();
+        doReturn(action).when(event).getAction();
+        return event;
+    }
+
+    private boolean touch(int action, float x, float y) {
+        return touch(event(action, x, y));
+    }
+
+    private boolean touch(MotionEvent event) {
+        return mController.onInterceptTouchEvent(event);
+    }
+
+    private NavigationGestureAction mockAction(boolean enabled) {
+        final NavigationGestureAction action = mock(NavigationGestureAction.class);
+        doReturn(enabled).when(action).isEnabled();
+        doReturn(HIT_TARGET_NONE).when(action).requiresTouchDownHitTarget();
+        doReturn(true).when(action).canPerformAction();
+        return action;
+    }
+
+    private void assertGestureTriggersAction(NavigationGestureAction action, int x1, int y1,
+            int x2, int y2) {
+        // Start the drag
+        assertFalse(touch(MotionEvent.ACTION_DOWN, x1, y1));
+        assertNull(mController.getCurrentAction());
+
+        // Swipe
+        MotionEvent motionMoveEvent = event(MotionEvent.ACTION_MOVE, x2, y2);
+        assertTrue(touch(motionMoveEvent));
+        assertEquals(action, mController.getCurrentAction());
+        verify(action, times(1)).startGesture(motionMoveEvent);
+
+        // Move again
+        assertTrue(touch(MotionEvent.ACTION_MOVE, x2, y2));
+        verify(action, times(1)).onGestureMove(x2, y2);
+
+        // Touch up
+        assertFalse(touch(MotionEvent.ACTION_UP, x2, y2));
+        assertNull(mController.getCurrentAction());
+        verify(action, times(1)).endGesture();
+    }
+}