Merge changes from topic "state-supplier" into ub-launcher3-master

* changes:
  Translate recents slightly while dragging after pausing
  Two-zone model: swipe up from nav bar vs above it
  Change LauncherState to Supplier<LauncherState> in tests
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 4b5ba95..c359423 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.OverviewToAllAppsTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
@@ -224,7 +225,11 @@
         if (mode == NO_BUTTON) {
             list.add(new NoButtonQuickSwitchTouchController(this));
             list.add(new NavBarToHomeTouchController(this));
-            list.add(new FlingAndHoldTouchController(this));
+            if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+                list.add(new NoButtonNavbarToOverviewTouchController(this));
+            } else {
+                list.add(new FlingAndHoldTouchController(this));
+            }
         } else {
             if (getDeviceProfile().isVerticalBarLayout()) {
                 list.add(new OverviewToAllAppsTouchController(this));
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 73c0c97..9c78af9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -30,11 +30,13 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 
 import android.graphics.Rect;
 import android.view.View;
+import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
@@ -44,7 +46,6 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
@@ -116,6 +117,15 @@
     }
 
     @Override
+    public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
+        if (this == OVERVIEW && ENABLE_OVERVIEW_ACTIONS.get()) {
+            // Treat the QSB as part of the hotseat so they move together.
+            return getHotseatScaleAndTranslation(launcher);
+        }
+        return super.getQsbScaleAndTranslation(launcher);
+    }
+
+    @Override
     public void onStateEnabled(Launcher launcher) {
         AbstractFloatingView.closeAllOpenViews(launcher);
     }
@@ -141,7 +151,7 @@
         if (launcher.getDeviceProfile().isVerticalBarLayout()) {
             return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
         } else {
-            if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            if (ENABLE_OVERVIEW_ACTIONS.get()) {
                 return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
             }
 
@@ -195,9 +205,10 @@
     @Override
     public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
             AnimatorSetBuilder builder) {
-        if (fromState == NORMAL && this == OVERVIEW) {
+        if ((fromState == NORMAL || fromState == HINT_STATE) && this == OVERVIEW) {
             if (SysUINavigationMode.getMode(launcher) == SysUINavigationMode.Mode.NO_BUTTON) {
-                builder.setInterpolator(ANIM_WORKSPACE_SCALE, ACCEL);
+                builder.setInterpolator(ANIM_WORKSPACE_SCALE,
+                        fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
                 builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
             } else {
                 builder.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
@@ -210,8 +221,11 @@
             }
             builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
             builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_1_7);
+            Interpolator translationInterpolator = ENABLE_OVERVIEW_ACTIONS.get()
+                    ? OVERSHOOT_1_2
+                    : OVERSHOOT_1_7;
+            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator);
+            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, translationInterpolator);
             builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
         }
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
index d388f49..ff1b5f6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -60,7 +60,7 @@
     private static final long PEEK_OUT_ANIM_DURATION = 100;
     private static final float MAX_DISPLACEMENT_PERCENT = 0.75f;
 
-    private final MotionPauseDetector mMotionPauseDetector;
+    protected final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
     private final float mMotionPauseMaxDisplacement;
 
@@ -85,37 +85,39 @@
         super.onDragStart(start);
 
         if (handlingOverviewAnim()) {
-            mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
-                RecentsView recentsView = mLauncher.getOverviewPanel();
-                recentsView.setOverviewStateEnabled(isPaused);
-                if (mPeekAnim != null) {
-                    mPeekAnim.cancel();
-                }
-                LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK;
-                LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL;
-                long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION;
-                mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(fromState, toState,
-                        new AnimatorSetBuilder(), ATOMIC_OVERVIEW_PEEK_COMPONENT, peekDuration);
-                mPeekAnim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mPeekAnim = null;
-                    }
-                });
-                mPeekAnim.start();
-                VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
-
-                mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
-                        peekDuration, 0);
-            });
+            mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
         }
     }
 
+    protected void onMotionPauseChanged(boolean isPaused) {
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        recentsView.setOverviewStateEnabled(isPaused);
+        if (mPeekAnim != null) {
+            mPeekAnim.cancel();
+        }
+        LauncherState fromState = isPaused ? NORMAL : OVERVIEW_PEEK;
+        LauncherState toState = isPaused ? OVERVIEW_PEEK : NORMAL;
+        long peekDuration = isPaused ? PEEK_IN_ANIM_DURATION : PEEK_OUT_ANIM_DURATION;
+        mPeekAnim = mLauncher.getStateManager().createAtomicAnimation(fromState, toState,
+                new AnimatorSetBuilder(), ATOMIC_OVERVIEW_PEEK_COMPONENT, peekDuration);
+        mPeekAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mPeekAnim = null;
+            }
+        });
+        mPeekAnim.start();
+        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+
+        mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
+                peekDuration, 0);
+    }
+
     /**
      * @return Whether we are handling the overview animation, rather than
      * having it as part of the existing animation to the target state.
      */
-    private boolean handlingOverviewAnim() {
+    protected boolean handlingOverviewAnim() {
         int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
         return mStartState == NORMAL && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
     }
@@ -162,7 +164,8 @@
     @Override
     public boolean onDrag(float displacement, MotionEvent event) {
         float upDisplacement = -displacement;
-        mMotionPauseDetector.setDisallowPause(upDisplacement < mMotionPauseMinDisplacement
+        mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim()
+                || upDisplacement < mMotionPauseMinDisplacement
                 || upDisplacement > mMotionPauseMaxDisplacement);
         mMotionPauseDetector.addPosition(displacement, event.getEventTime());
         return super.onDrag(displacement, event);
@@ -171,19 +174,7 @@
     @Override
     public void onDragEnd(float velocity) {
         if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) {
-            if (mPeekAnim != null) {
-                mPeekAnim.cancel();
-            }
-
-            Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation(
-                    INDEX_PAUSE_TO_OVERVIEW_ANIM);
-            overviewAnim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
-                }
-            });
-            overviewAnim.start();
+            goToOverviewOnDragEnd(velocity);
         } else {
             super.onDragEnd(velocity);
         }
@@ -195,6 +186,22 @@
         mMotionPauseDetector.clear();
     }
 
+    protected void goToOverviewOnDragEnd(float velocity) {
+        if (mPeekAnim != null) {
+            mPeekAnim.cancel();
+        }
+
+        Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation(
+                INDEX_PAUSE_TO_OVERVIEW_ANIM);
+        overviewAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
+            }
+        });
+        overviewAnim.start();
+    }
+
     @Override
     protected void goToTargetState(LauncherState targetState, int logAction) {
         if (mPeekAnim != null && mPeekAnim.isStarted()) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
new file mode 100644
index 0000000..2ac2d2d
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2020 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.launcher3.uioverrides.touchcontrollers;
+
+import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_PEEK_COMPONENT;
+import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above
+ * the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the
+ * first home screen instead of to Overview.
+ */
+public class NoButtonNavbarToOverviewTouchController extends FlingAndHoldTouchController {
+
+
+    // How much of the movement to use for translating overview after swipe and hold.
+    private static final float OVERVIEW_MOVEMENT_FACTOR = 0.25f;
+    private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
+    private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
+
+    private final RecentsView mRecentsView;
+
+    private boolean mDidTouchStartInNavBar;
+    private boolean mReachedOverview;
+    // The last recorded displacement before we reached overview.
+    private PointF mStartDisplacement = new PointF();
+
+    public NoButtonNavbarToOverviewTouchController(Launcher l) {
+        super(l);
+        mRecentsView = l.getOverviewPanel();
+    }
+
+    @Override
+    protected boolean canInterceptTouch(MotionEvent ev) {
+        mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+        return super.canInterceptTouch(ev);
+    }
+
+    @Override
+    protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
+        if (fromState == NORMAL && mDidTouchStartInNavBar) {
+            return HINT_STATE;
+        } else if (fromState == OVERVIEW && isDragTowardPositive) {
+            // Don't allow swiping up to all apps.
+            return OVERVIEW;
+        }
+        return super.getTargetState(fromState, isDragTowardPositive);
+    }
+
+    @Override
+    protected float initCurrentAnimation(int animComponents) {
+        float progressMultiplier = super.initCurrentAnimation(animComponents);
+        if (mToState == HINT_STATE) {
+            // Track the drag across the entire height of the screen.
+            progressMultiplier = -1 / getShiftRange();
+        }
+        return progressMultiplier;
+    }
+
+    @Override
+    public void onDragStart(boolean start) {
+        super.onDragStart(start);
+
+        mReachedOverview = false;
+    }
+
+    @Override
+    protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
+            LauncherState targetState, float velocity, boolean isFling) {
+        super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity,
+                isFling);
+        if (targetState == HINT_STATE) {
+            // Normally we compute the duration based on the velocity and distance to the given
+            // state, but since the hint state tracks the entire screen without a clear endpoint, we
+            // need to manually set the duration to a reasonable value.
+            animator.setDuration(HINT_STATE.transitionDuration);
+        }
+    }
+
+    @Override
+    protected void onMotionPauseChanged(boolean isPaused) {
+        if (mCurrentAnimation == null) {
+            return;
+        }
+        mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> {
+            mLauncher.getStateManager().goToState(OVERVIEW, true, () -> {
+                mReachedOverview = true;
+                maybeSwipeInteractionToOverviewComplete();
+            });
+        });
+        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+    }
+
+    private void maybeSwipeInteractionToOverviewComplete() {
+        if (mReachedOverview && mDetector.isSettlingState()) {
+            onSwipeInteractionCompleted(OVERVIEW, Touch.SWIPE);
+        }
+    }
+
+    @Override
+    protected boolean handlingOverviewAnim() {
+        return mDidTouchStartInNavBar && super.handlingOverviewAnim();
+    }
+
+    @Override
+    public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
+        if (mMotionPauseDetector.isPaused()) {
+            if (!mReachedOverview) {
+                mStartDisplacement.set(xDisplacement, yDisplacement);
+            } else {
+                mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
+                        * OVERVIEW_MOVEMENT_FACTOR);
+                mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
+                        * OVERVIEW_MOVEMENT_FACTOR);
+            }
+            // Stay in Overview.
+            return true;
+        }
+        return super.onDrag(yDisplacement, xDisplacement, event);
+    }
+
+    @Override
+    protected void goToOverviewOnDragEnd(float velocity) {
+        float velocityDp = dpiFromPx(velocity);
+        boolean isFling = Math.abs(velocityDp) > 1;
+        LauncherStateManager stateManager = mLauncher.getStateManager();
+        if (isFling) {
+            // When flinging, go back to home instead of overview.
+            if (velocity > 0) {
+                stateManager.goToState(NORMAL, true,
+                        () -> onSwipeInteractionCompleted(NORMAL, Touch.FLING));
+            } else {
+                StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
+                        mLauncher, velocity, false /* animateOverviewScrim */);
+                staggeredWorkspaceAnim.start();
+
+                // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here.
+                stateManager.cancelAnimation();
+                AnimatorSetBuilder builder = new AnimatorSetBuilder();
+                long duration = OVERVIEW.transitionDuration;
+                AnimatorSet anim = stateManager.createAtomicAnimation(
+                        stateManager.getState(), NORMAL, builder,
+                        ATOMIC_OVERVIEW_PEEK_COMPONENT, duration);
+                anim.addListener(new AnimationSuccessListener() {
+                    @Override
+                    public void onAnimationSuccess(Animator animator) {
+                        onSwipeInteractionCompleted(NORMAL, Touch.SWIPE);
+                    }
+                });
+                anim.start();
+            }
+        } else {
+            if (mReachedOverview) {
+                float distanceDp = dpiFromPx(Math.max(
+                        Math.abs(mRecentsView.getTranslationX()),
+                        Math.abs(mRecentsView.getTranslationY())));
+                long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
+                        distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
+                mRecentsView.animate()
+                        .translationX(0)
+                        .translationY(0)
+                        .setInterpolator(ACCEL_DEACCEL)
+                        .setDuration(duration)
+                        .withEndAction(this::maybeSwipeInteractionToOverviewComplete);
+            }
+        }
+    }
+
+    private float dpiFromPx(float pixels) {
+        return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics());
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 4e08df9..8628db0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -62,6 +62,7 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.BothAxesSwipeDetector;
@@ -181,6 +182,12 @@
 
     @Override
     public void onMotionPauseChanged(boolean isPaused) {
+        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+
+        if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            return;
+        }
+
         ShelfAnimState shelfState = isPaused ? PEEK : HIDE;
         if (shelfState == PEEK) {
             // Some shelf elements (e.g. qsb) were hidden, but we need them visible when peeking.
@@ -197,7 +204,6 @@
         }
         mShelfPeekAnim.setShelfState(shelfState, ShelfPeekAnim.INTERPOLATOR,
                 ShelfPeekAnim.DURATION);
-        VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
     }
 
     private void setupAnimators() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
index 41be683..217eca5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
@@ -25,6 +25,7 @@
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.states.OverviewState;
 
 /**
@@ -48,7 +49,7 @@
      * Animates to the given state, canceling the previous animation if it was still running.
      */
     public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
-        if (mShelfState == shelfState) {
+        if (mShelfState == shelfState || FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
             return;
         }
         mLauncher.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index befeee0..9817e32 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -15,6 +15,11 @@
  */
 package com.android.quickstep;
 
+import static android.content.Context.MODE_PRIVATE;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.UserManager;
@@ -22,6 +27,7 @@
 
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.MainProcessInitializer;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.systemui.shared.system.ThreadedRendererCompat;
 
 @SuppressWarnings("unused")
@@ -50,5 +56,21 @@
 
         // Elevate GPU priority for Quickstep and Remote animations.
         ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
+
+        // Force disable some feature flags based on the system ui navigation mode.
+        SysUINavigationMode.Mode currMode = SysUINavigationMode.INSTANCE.get(context)
+                .addModeChangeListener(mode -> disableFeatureFlagsForSysuiNavMode(context, mode));
+        disableFeatureFlagsForSysuiNavMode(context, currMode);
+    }
+
+    private void disableFeatureFlagsForSysuiNavMode(Context ctx, SysUINavigationMode.Mode mode) {
+        if (mode == SysUINavigationMode.Mode.TWO_BUTTONS) {
+            ctx.getSharedPreferences(FLAGS_PREF_NAME, MODE_PRIVATE)
+                    .edit()
+                    .putBoolean(ENABLE_OVERVIEW_ACTIONS.key, false)
+                    .apply();
+
+            FeatureFlags.initialize(ctx);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index 7885f5c..7ed1e21 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -41,6 +41,8 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
@@ -155,12 +157,18 @@
             mRemainingScreenPathValid = false;
             mShiftRange = mLauncher.getAllAppsController().getShiftRange();
 
+            Context context = getContext();
             if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) {
-                mMidProgress = 1;
                 mDragHandleProgress = 1;
-                mMidAlpha = 0;
+                if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+                    // Fade in all apps background quickly to distinguish from swiping from nav bar.
+                    mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha);
+                    mMidProgress = OverviewState.getDefaultVerticalProgress(mLauncher);
+                } else {
+                    mMidAlpha = 0;
+                    mMidProgress = 1;
+                }
             } else {
-                Context context = getContext();
                 mMidAlpha = Themes.getAttrInteger(context, R.attr.allAppsInterimScrimAlpha);
                 mMidProgress =  OVERVIEW.getVerticalProgress(mLauncher);
                 Rect hotseatPadding = dp.getHotseatLayoutPadding();
diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index a7c33a9..3e84a76 100644
--- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -68,7 +68,7 @@
 
     private DigitalWellBeingToast getToast() {
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(OVERVIEW));
-        waitForState("Launcher internal state didn't switch to Overview", OVERVIEW);
+        waitForState("Launcher internal state didn't switch to Overview", () -> OVERVIEW);
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index e34ea4a..c99df10 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -85,7 +85,8 @@
     @Ignore // Enable after b/131115533
     public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
         mDevice.pressRecentApps();
-        waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
+        waitForState("Launcher internal state didn't switch to Overview",
+                () -> LauncherState.OVERVIEW);
 
         assertNotNull("getOverview() returned null", mLauncher.getOverview());
     }
@@ -96,7 +97,8 @@
     public void testWorkspaceSwitchToAllApps() {
         assertNotNull("switchToAllApps() returned null",
                 mLauncher.getWorkspace().switchToAllApps());
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
     }
 
     @Test
@@ -115,7 +117,7 @@
         // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
         executeOnLauncher(
                 launcher -> assertTrue("Don't have at least 3 tasks", getTaskCount(launcher) >= 3));
 
@@ -124,14 +126,16 @@
                 0, getCurrentOverviewPage(launcher)));
 
         overview.flingForward();
-        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
+        assertTrue("Launcher internal state is not Overview",
+                isInState(() -> LauncherState.OVERVIEW));
         final Integer currentTaskAfterFlingForward = getFromLauncher(
                 launcher -> getCurrentOverviewPage(launcher));
         executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0",
                 currentTaskAfterFlingForward > 0));
 
         overview.flingBackward();
-        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
+        assertTrue("Launcher internal state is not Overview",
+                isInState(() -> LauncherState.OVERVIEW));
         executeOnLauncher(launcher -> assertTrue("Flinging back in Overview did nothing",
                 getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward));
 
@@ -150,7 +154,7 @@
         // Test dismissing a task.
         overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
         final Integer numTasks = getFromLauncher(launcher -> getTaskCount(launcher));
         task = overview.getCurrentTask();
         assertNotNull("overview.getCurrentTask() returned null (2)", task);
@@ -165,29 +169,29 @@
             final AllAppsFromOverview allApps = overview.switchToAllApps();
             assertNotNull("overview.switchToAllApps() returned null (1)", allApps);
             assertTrue("Launcher internal state is not All Apps (1)",
-                    isInState(LauncherState.ALL_APPS));
+                    isInState(() -> LauncherState.ALL_APPS));
 
             overview = allApps.switchBackToOverview();
             assertNotNull("allApps.switchBackToOverview() returned null", overview);
             assertTrue("Launcher internal state didn't switch to Overview",
-                    isInState(LauncherState.OVERVIEW));
+                    isInState(() -> LauncherState.OVERVIEW));
 
             // Test UIDevice.pressBack()
             overview.switchToAllApps();
             assertNotNull("overview.switchToAllApps() returned null (2)", allApps);
             assertTrue("Launcher internal state is not All Apps (2)",
-                    isInState(LauncherState.ALL_APPS));
+                    isInState(() -> LauncherState.ALL_APPS));
             mDevice.pressBack();
             mLauncher.getOverview();
         }
 
         // Test UIDevice.pressHome, once we are in AllApps.
         mDevice.pressHome();
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
 
         // Test dismissing all tasks.
         mLauncher.getWorkspace().switchToOverview().dismissAllTasks();
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
         executeOnLauncher(
                 launcher -> assertEquals("Still have tasks after dismissing all",
                         0, getTaskCount(launcher)));
@@ -205,7 +209,8 @@
     public void testAppIconLaunchFromAllAppsFromOverview() throws Exception {
         final AllApps allApps =
                 mLauncher.getWorkspace().switchToOverview().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
 
         TaplTestsLauncher3.runIconLaunchFromAllAppsTest(this, allApps);
     }
@@ -217,7 +222,7 @@
         assertNotNull("Workspace.switchToOverview() returned null",
                 mLauncher.pressHome().switchToOverview());
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
     }
 
     @Test
@@ -229,7 +234,7 @@
 
         assertNotNull("Background.switchToOverview() returned null", background.switchToOverview());
         assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
+                isInState(() -> LauncherState.OVERVIEW));
     }
 
     private Background getAndAssertBackground() {
@@ -252,9 +257,11 @@
         TaplTestsLauncher3.runAllAppsTest(this, mLauncher.getAllApps());
 
         // Testing pressHome.
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
         assertNotNull("pressHome returned null", mLauncher.pressHome());
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        assertTrue("Launcher internal state is not Home",
+                isInState(() -> LauncherState.NORMAL));
         assertNotNull("getHome returned null", mLauncher.getWorkspace());
     }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 06f3453..2ccdd6f 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1180,6 +1180,11 @@
         mDropTargetBar.setup(mDragController);
 
         mAllAppsController.setupViews(mAppsView);
+
+        if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            // Overview is above all other launcher elements, including qsb, so move it to the top.
+            mOverviewPanel.bringToFront();
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index 74362ed..cdfd257 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+
 import android.graphics.drawable.Drawable;
 import android.util.FloatProperty;
 import android.util.Property;
@@ -28,9 +30,10 @@
      * easier access from static classes and enums
      */
     public static final int ALL_APPS_TRANSITION_MS = 320;
-    public static final int OVERVIEW_TRANSITION_MS = 250;
+    public static final int OVERVIEW_TRANSITION_MS = ENABLE_OVERVIEW_ACTIONS.get() ? 380 : 250;
     public static final int SPRING_LOADED_TRANSITION_MS = 150;
     public static final int SPRING_LOADED_EXIT_DELAY = 500;
+    public static final int HINT_TRANSITION_MS = 80;
 
     // The progress of an animation to all apps must be at least this far along to snap to all apps.
     public static final float MIN_PROGRESS_TO_ALL_APPS = 0.5f;
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index d2b447b..8b80cba 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -30,9 +30,11 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
 import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
+import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_PEEK_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
@@ -42,6 +44,7 @@
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.states.HintState;
 import com.android.launcher3.states.SpringLoadedState;
 import com.android.launcher3.uioverrides.states.AllAppsState;
 import com.android.launcher3.uioverrides.states.OverviewState;
@@ -88,7 +91,7 @@
                 }
             };
 
-    private static final LauncherState[] sAllStates = new LauncherState[7];
+    private static final LauncherState[] sAllStates = new LauncherState[8];
 
     /**
      * TODO: Create a separate class for NORMAL state.
@@ -104,6 +107,7 @@
     public static final LauncherState SPRING_LOADED = new SpringLoadedState(
             SPRING_LOADED_STATE_ORDINAL);
     public static final LauncherState ALL_APPS = new AllAppsState(ALL_APPS_STATE_ORDINAL);
+    public static final LauncherState HINT_STATE = new HintState(HINT_STATE_ORDINAL);
 
     public static final LauncherState OVERVIEW = new OverviewState(OVERVIEW_STATE_ORDINAL);
     public static final LauncherState OVERVIEW_PEEK =
@@ -212,6 +216,10 @@
         return launcher.getOverviewScaleAndTranslationForNormalState();
     }
 
+    public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
+        return new ScaleAndTranslation(1, 0, 0);
+    }
+
     public float getOverviewFullscreenProgress() {
         return 0;
     }
@@ -319,6 +327,10 @@
             if (!isHotseatVisible) {
                 hotseat.setScaleX(0.92f);
                 hotseat.setScaleY(0.92f);
+                if (ENABLE_OVERVIEW_ACTIONS.get()) {
+                    launcher.getAppsView().setScaleX(0.92f);
+                    launcher.getAppsView().setScaleY(0.92f);
+                }
             }
         } else if (this == NORMAL && fromState == OVERVIEW_PEEK) {
             // Keep fully visible until the very end (when overview is offscreen) to make invisible.
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 6c0d95e..7f443b6 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -381,7 +381,7 @@
         return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
     }
 
-    public static float dpiFromPx(int size, DisplayMetrics metrics){
+    public static float dpiFromPx(float size, DisplayMetrics metrics) {
         float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
         return (size / densityRatio);
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index abbf59d..670363e 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -3255,7 +3255,8 @@
         return mOverlayShown;
     }
 
-    void moveToDefaultScreen() {
+    /** Calls {@link #snapToPage(int)} on the {@link #DEFAULT_PAGE}, then requests focus on it. */
+    public void moveToDefaultScreen() {
         int page = DEFAULT_PAGE;
         if (!workspaceInModalState() && getNextPage() != page) {
             snapToPage(page);
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 7a7e1fe..c33392d 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.LauncherState.PageAlphaProvider;
 import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
+import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
@@ -77,6 +78,7 @@
         ScaleAndTranslation scaleAndTranslation = state.getWorkspaceScaleAndTranslation(mLauncher);
         ScaleAndTranslation hotseatScaleAndTranslation = state.getHotseatScaleAndTranslation(
                 mLauncher);
+        ScaleAndTranslation qsbScaleAndTranslation = state.getQsbScaleAndTranslation(mLauncher);
         mNewScale = scaleAndTranslation.scale;
         PageAlphaProvider pageAlphaProvider = state.getWorkspacePageAlphaProvider(mLauncher);
         final int childCount = mWorkspace.getChildCount();
@@ -90,24 +92,24 @@
                 pageAlphaProvider.interpolator);
         boolean playAtomicComponent = config.playAtomicOverviewScaleComponent();
         Hotseat hotseat = mWorkspace.getHotseat();
+        // Since we set the pivot relative to mWorkspace, we need to scale a sibling of Workspace.
+        AllAppsContainerView qsbScaleView = mLauncher.getAppsView();
+        View qsbView = qsbScaleView.getSearchView();
         if (playAtomicComponent) {
             Interpolator scaleInterpolator = builder.getInterpolator(ANIM_WORKSPACE_SCALE, ZOOM_OUT);
             propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
 
             if (!hotseat.getRotationMode().isTransposed) {
-                // Set the hotseat's pivot point to match the workspace's, so that it scales
-                // together. Since both hotseat and workspace can move, transform the point
-                // manually instead of using dragLayer.getDescendantCoordRelativeToSelf and
-                // related methods.
-                hotseat.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop() - hotseat.getTop());
-                hotseat.setPivotX(mWorkspace.getPivotX()
-                        + mWorkspace.getLeft() - hotseat.getLeft());
+                setPivotToScaleWithWorkspace(hotseat);
+                setPivotToScaleWithWorkspace(qsbScaleView);
             }
             float hotseatScale = hotseatScaleAndTranslation.scale;
             Interpolator hotseatScaleInterpolator = builder.getInterpolator(ANIM_HOTSEAT_SCALE,
                     scaleInterpolator);
             propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
                     hotseatScaleInterpolator);
+            propertySetter.setFloat(qsbScaleView, SCALE_PROPERTY, qsbScaleAndTranslation.scale,
+                    hotseatScaleInterpolator);
 
             float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
             propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator);
@@ -134,10 +136,24 @@
                 hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator);
         propertySetter.setFloat(mWorkspace.getPageIndicator(), View.TRANSLATION_Y,
                 hotseatScaleAndTranslation.translationY, hotseatTranslationInterpolator);
+        propertySetter.setFloat(qsbView, View.TRANSLATION_Y,
+                qsbScaleAndTranslation.translationY, hotseatTranslationInterpolator);
 
         setScrim(propertySetter, state);
     }
 
+    /**
+     * Set the given view's pivot point to match the workspace's, so that it scales together. Since
+     * both this view and workspace can move, transform the point manually instead of using
+     * dragLayer.getDescendantCoordRelativeToSelf and related methods.
+     */
+    private void setPivotToScaleWithWorkspace(View sibling) {
+        sibling.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop()
+                - sibling.getTop() - sibling.getTranslationY());
+        sibling.setPivotX(mWorkspace.getPivotX() + mWorkspace.getLeft()
+                - sibling.getLeft() - sibling.getTranslationX());
+    }
+
     public void setScrim(PropertySetter propertySetter, LauncherState state) {
         WorkspaceAndHotseatScrim scrim = mLauncher.getDragLayer().getScrim();
         propertySetter.setFloat(scrim, SCRIM_PROGRESS, state.getWorkspaceScrimAlpha(mLauncher),
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 4a52795..1c277ab 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -26,6 +26,7 @@
 import android.animation.ValueAnimator;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringAnimation;
 
@@ -250,14 +251,24 @@
         }
     }
 
+    /** @see #dispatchOnCancelWithoutCancelRunnable(Runnable) */
+    public void dispatchOnCancelWithoutCancelRunnable() {
+        dispatchOnCancelWithoutCancelRunnable(null);
+    }
+
     /**
      * Sets mOnCancelRunnable = null before dispatching the cancel and restoring the runnable. This
      * is intended to be used only if you need to cancel but want to defer cleaning up yourself.
+     * @param callback An optional callback to run after dispatching the cancel but before resetting
+     *                 the onCancelRunnable.
      */
-    public void dispatchOnCancelWithoutCancelRunnable() {
+    public void dispatchOnCancelWithoutCancelRunnable(@Nullable Runnable callback) {
         Runnable onCancel = mOnCancelRunnable;
         setOnCancelRunnable(null);
         dispatchOnCancel();
+        if (callback != null) {
+            callback.run();
+        }
         setOnCancelRunnable(onCancel);
     }
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 8ccb369..b1a2c33 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -117,7 +117,8 @@
             "Show launcher preview in grid picker");
 
     public static final BooleanFlag ENABLE_OVERVIEW_ACTIONS = getDebugFlag(
-            "ENABLE_OVERVIEW_ACTIONS", false, "Show app actions in Overview");
+            "ENABLE_OVERVIEW_ACTIONS", false, "Show app actions instead of the shelf in Overview."
+            + " As part of this decoupling, also distinguish swipe up from nav bar vs above it.");
 
     public static final BooleanFlag ENABLE_DATABASE_RESTORE = getDebugFlag(
             "ENABLE_DATABASE_RESTORE", true,
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 8823bde..92f35e2 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -536,6 +536,9 @@
         mOverviewScrim.updateCurrentScrimmedView(this);
         mFocusIndicatorHelper.draw(canvas);
         super.dispatchDraw(canvas);
+        if (mOverviewScrim.getScrimmedView() == null) {
+            mOverviewScrim.draw(canvas);
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/graphics/OverviewScrim.java b/src/com/android/launcher3/graphics/OverviewScrim.java
index d707403..94acbfd 100644
--- a/src/com/android/launcher3/graphics/OverviewScrim.java
+++ b/src/com/android/launcher3/graphics/OverviewScrim.java
@@ -55,16 +55,17 @@
         mCurrentScrimmedView = mStableScrimmedView;
         int currentIndex = root.indexOfChild(mCurrentScrimmedView);
         final int childCount = root.getChildCount();
-        while (mCurrentScrimmedView.getVisibility() != VISIBLE && currentIndex < childCount) {
+        while (mCurrentScrimmedView != null && mCurrentScrimmedView.getVisibility() != VISIBLE
+                && currentIndex < childCount) {
             currentIndex++;
             mCurrentScrimmedView = root.getChildAt(currentIndex);
         }
     }
 
     /**
-     * @return The view to draw the scrim behind.
+     * @return The view to draw the scrim behind, or null if all visible views should be scrimmed.
      */
-    public View getScrimmedView() {
+    public @Nullable View getScrimmedView() {
         return mCurrentScrimmedView;
     }
 }
diff --git a/src/com/android/launcher3/states/HintState.java b/src/com/android/launcher3/states/HintState.java
new file mode 100644
index 0000000..cb56097
--- /dev/null
+++ b/src/com/android/launcher3/states/HintState.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 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.launcher3.states;
+
+import static com.android.launcher3.LauncherAnimUtils.HINT_TRANSITION_MS;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+
+/**
+ * Scale down workspace/hotseat to hint at going to either overview (on pause) or first home screen.
+ */
+public class HintState extends LauncherState {
+
+    private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY | FLAG_DISABLE_RESTORE
+            | FLAG_HAS_SYS_UI_SCRIM;
+
+    public HintState(int id) {
+        super(id, ContainerType.DEFAULT_CONTAINERTYPE, HINT_TRANSITION_MS, STATE_FLAGS);
+    }
+
+    @Override
+    public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
+        return new ScaleAndTranslation(0.9f, 0, 0);
+    }
+
+    @Override
+    public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
+        // Treat the QSB as part of the hotseat so they move together.
+        return getHotseatScaleAndTranslation(launcher);
+    }
+
+    @Override
+    public void onStateTransitionEnd(Launcher launcher) {
+        launcher.getStateManager().goToState(NORMAL);
+        Workspace workspace = launcher.getWorkspace();
+        workspace.post(workspace::moveToDefaultScreen);
+    }
+}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 01c207f..5c20050 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -31,6 +31,7 @@
     public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
     public static final int ALL_APPS_STATE_ORDINAL = 5;
     public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+    public static final int HINT_STATE_ORDINAL = 7;
     public static final String TAPL_EVENTS_TAG = "TaplEvents";
 
     public static String stateOrdinalToString(int ordinal) {
@@ -49,6 +50,8 @@
                 return "AllApps";
             case BACKGROUND_APP_STATE_ORDINAL:
                 return "Background";
+            case HINT_STATE_ORDINAL:
+                return "Hint";
             default:
                 return null;
         }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index d193bef..3ec480d 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -525,7 +525,11 @@
         if (targetState != mStartState) {
             logReachedState(logAction, targetState);
         }
-        mLauncher.getStateManager().goToState(targetState, false /* animated */);
+        if (!mLauncher.isInState(targetState)) {
+            // If we're already in the target state, don't jump to it at the end of the animation in
+            // case the user started interacting with it before the animation finished.
+            mLauncher.getStateManager().goToState(targetState, false /* animated */);
+        }
         mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(1, 0, 0);
     }
 
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index f2ebc45..9d406f3 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -54,8 +54,8 @@
         }
 
         @Override
-        boolean canScrollStart(PointF displacement, float touchSlop) {
-            return Math.abs(displacement.y) >= Math.max(Math.abs(displacement.x), touchSlop);
+        float extractOrthogonalDirection(PointF direction) {
+            return direction.x;
         }
 
     };
@@ -80,9 +80,10 @@
         }
 
         @Override
-        boolean canScrollStart(PointF displacement, float touchSlop) {
-            return Math.abs(displacement.x) >= Math.max(Math.abs(displacement.y), touchSlop);
+        float extractOrthogonalDirection(PointF direction) {
+            return direction.y;
         }
+
     };
 
     private final Direction mDir;
@@ -126,7 +127,9 @@
     @Override
     protected boolean shouldScrollStart(PointF displacement) {
         // Reject cases where the angle or slop condition is not met.
-        if (!mDir.canScrollStart(displacement, mTouchSlop)) {
+        float minDisplacement = Math.max(mTouchSlop,
+                Math.abs(mDir.extractOrthogonalDirection(displacement)));
+        if (Math.abs(mDir.extractDirection(displacement)) < minDisplacement) {
             return false;
         }
 
@@ -150,7 +153,8 @@
 
     @Override
     protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
-        mListener.onDrag(mDir.extractDirection(displacement), event);
+        mListener.onDrag(mDir.extractDirection(displacement),
+                mDir.extractOrthogonalDirection(displacement), event);
     }
 
     @Override
@@ -164,13 +168,16 @@
         /** @param start whether this was the original drag start, as opposed to a recatch. */
         void onDragStart(boolean start);
 
-        // TODO remove
         boolean onDrag(float displacement);
 
         default boolean onDrag(float displacement, MotionEvent event) {
             return onDrag(displacement);
         }
 
+        default boolean onDrag(float displacement, float orthogonalDisplacement, MotionEvent ev) {
+            return onDrag(displacement, ev);
+        }
+
         void onDragEnd(float velocity);
     }
 
@@ -183,8 +190,7 @@
         /** Returns the part of the given {@link PointF} that is relevant to this direction. */
         abstract float extractDirection(PointF point);
 
-        /** Reject cases where the angle or slop condition is not met. */
-        abstract boolean canScrollStart(PointF displacement, float touchSlop);
+        abstract float extractOrthogonalDirection(PointF point);
 
     }
 }
diff --git a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
index efbd9c9..33066e4 100644
--- a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
@@ -49,7 +49,7 @@
         super.setUp();
         mDevice.pressHome();
         waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
         mSessionId = -1;
     }
 
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 6d463b5..b0ece77 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -22,15 +22,16 @@
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyFloat;
-import static org.mockito.Matchers.anyObject;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
 import androidx.test.InstrumentationRegistry;
@@ -158,7 +159,7 @@
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDrag(anyFloat(), anyObject());
+        verify(mMockListener).onDrag(anyFloat(), anyFloat(), any(MotionEvent.class));
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 19997eb..25670e7 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -84,6 +84,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Supplier;
 
 /**
  * Base class for all instrumentation tests providing various utility methods.
@@ -281,9 +282,9 @@
 
     // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call expecting
     // the results of that gesture because the wait can hide flakeness.
-    protected void waitForState(String message, LauncherState state) {
+    protected void waitForState(String message, Supplier<LauncherState> state) {
         waitForLauncherCondition(message,
-                launcher -> launcher.getStateManager().getCurrentStableState() == state);
+                launcher -> launcher.getStateManager().getCurrentStableState() == state.get());
     }
 
     protected void waitForResumed(String message) {
@@ -430,9 +431,9 @@
         return !launcher.hasBeenResumed();
     }
 
-    protected boolean isInState(LauncherState state) {
+    protected boolean isInState(Supplier<LauncherState> state) {
         if (!TestHelpers.isInLauncherProcess()) return true;
-        return getFromLauncher(launcher -> launcher.getStateManager().getState() == state);
+        return getFromLauncher(launcher -> launcher.getStateManager().getState() == state.get());
     }
 
     protected int getAllAppsScroll(Launcher launcher) {
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 9b4023e..4b72882 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -76,7 +76,8 @@
         test.clearLauncherData();
         test.mDevice.pressHome();
         test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
-        test.waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        test.waitForState("Launcher internal state didn't switch to Home",
+                () -> LauncherState.NORMAL);
         test.waitForResumed("Launcher internal state is still Background");
         // Check that we switched to home.
         test.mLauncher.getWorkspace();
@@ -146,7 +147,7 @@
 
             assertTrue(
                     "Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
 
             // Test flinging forward and backward.
             test.executeOnLauncher(launcher -> assertEquals(
@@ -155,7 +156,7 @@
 
             allApps.flingForward();
             assertTrue("Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
             final Integer flingForwardY = test.getFromLauncher(
                     launcher -> test.getAllAppsScroll(launcher));
             test.executeOnLauncher(
@@ -165,7 +166,7 @@
             allApps.flingBackward();
             assertTrue(
                     "Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
             final Integer flingBackwardY = test.getFromLauncher(
                     launcher -> test.getAllAppsScroll(launcher));
             test.executeOnLauncher(launcher -> assertTrue("flingBackward() didn't scroll App Apps",
@@ -182,7 +183,7 @@
 
             assertTrue(
                     "Launcher internal state is not All Apps",
-                    test.isInState(LauncherState.ALL_APPS));
+                    test.isInState(() -> LauncherState.ALL_APPS));
         } finally {
             allApps.unfreeze();
         }
@@ -193,7 +194,8 @@
     public void testWorkspaceSwitchToAllApps() {
         assertNotNull("switchToAllApps() returned null",
                 mLauncher.getWorkspace().switchToAllApps());
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
     }
 
     @Test
@@ -219,7 +221,7 @@
 
         // Test flinging workspace.
         workspace.flingBackward();
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL));
         executeOnLauncher(
                 launcher -> assertEquals("Flinging back didn't switch workspace to page #0",
                         0, getCurrentWorkspacePage(launcher)));
@@ -228,7 +230,7 @@
         executeOnLauncher(
                 launcher -> assertEquals("Flinging forward didn't switch workspace to page #1",
                         1, getCurrentWorkspacePage(launcher)));
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL));
 
         // Test starting a workspace app.
         final AppIcon app = workspace.getWorkspaceAppIcon("Chrome");
@@ -254,7 +256,8 @@
     @PortraitLandscape
     public void testAppIconLaunchFromAllAppsFromHome() throws Exception {
         final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertTrue("Launcher internal state is not All Apps",
+                isInState(() -> LauncherState.ALL_APPS));
 
         runIconLaunchFromAllAppsTest(this, allApps);
     }