Two-zone model: swipe up from nav bar vs above it

When ENABLE_OVERVIEW_ACTIONS flag is enabled, swiping up from the nav
bar goes to overview if you hold, or the first home screen if you don't.

- Added NoButtonNavBarToOverviewTouchController, which extends
  FlingAndHoldTouchController but only works if you start from the nav
  bar. Otherwise it falls back to PortraitStatesTouchController to
  handle normal state transition to all apps.
- Added HintState. This is where the workspace/hotseat/qsb scale down
  when you swipe up from the nav bar, to hint that you're about to
  either go to overview or the first home screen.
  - Added getQsbScaleAndTranslation() to allow Overview and Hint states
    to treat it as part of the hotseat.
  - Since Overview needs to show above the QSB as it's animating, bring
    Overview to the front and update OverviewScrim to handle the case
    where there's no view above Overview to draw the scrim beneath.
- ENABLE_OVERVIEW_ACTIONS is always false in 2-button mode, since the
  shelf is crucial to that mode.

Bug: 143361609
Change-Id: I743481bb239dc77f7024dc98ba68a43534da2637
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 af9a1b4..8bfc028 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -75,7 +75,6 @@
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
 import java.lang.reflect.Method;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
@@ -382,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 3278960..c73a5e1 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -3256,7 +3256,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);
     }