[automerger skipped] Import translations. DO NOT MERGE
am: 885dfb7134 -s ours
am skip reason: subject contains skip directive

Change-Id: I515adc3e8a0a534fde521ec1f0ff909b53c28057
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index fe159b5..92900f2 100644
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -25,6 +25,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
+import android.content.Context;
 import android.os.Handler;
 import android.util.Log;
 
@@ -151,7 +152,7 @@
     }
 
     @Override
-    public ActivityOptions toActivityOptions(Handler handler, long duration) {
+    public ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
         LauncherAnimationRunner runner = new LauncherAnimationRunner(handler,
                 false /* startAtFrontOfQueue */) {
 
@@ -165,7 +166,7 @@
                     );
                     return;
                 }
-                result.setAnimation(createWindowAnimation(targetCompats));
+                result.setAnimation(createWindowAnimation(targetCompats), context);
             }
         };
         return ActivityOptionsCompat.makeRemoteAnimation(
diff --git a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java b/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 900b94e..577b175 100644
--- a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -34,6 +34,8 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.UiThreadHelper;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
@@ -137,6 +139,9 @@
         return sConnected;
     }
 
+    public static final LooperExecutor BACKGROUND_EXECUTOR =
+            new LooperExecutor(UiThreadHelper.getBackgroundLooper());
+
     private RecentsModel mRecentsModel;
     private OverviewComponentObserver mOverviewComponentObserver;
     private OverviewCommandHelper mOverviewCommandHelper;
@@ -180,4 +185,8 @@
         }
         return mMyBinder;
     }
+
+    public static boolean isInitialized() {
+        return true;
+    }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index db5515b..fc7d6b3 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -22,6 +22,7 @@
 import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
+import androidx.annotation.NonNull;
 
 /**
  * This class will be moved to androidx library. There shouldn't be any dependency outside
@@ -154,7 +155,7 @@
      * @param scale                     returns the scale result from normalization
      * @return a bitmap suitable for disaplaying as an icon at various system UIs.
      */
-    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+    public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user,
             boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) {
         if (scale == null) {
             scale = new float[1];
@@ -207,8 +208,11 @@
         mDisableColorExtractor = true;
     }
 
-    private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, boolean shrinkNonAdaptiveIcons,
-            RectF outIconBounds, float[] outScale) {
+    private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
+            boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
+        if (icon == null) {
+            return null;
+        }
         float scale = 1f;
 
         if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
@@ -264,7 +268,7 @@
      * @param icon drawable that should be flattened to a bitmap
      * @param scale the scale to apply before drawing {@param icon} on the canvas
      */
-    public Bitmap createIconBitmap(Drawable icon, float scale, int size) {
+    public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) {
         Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
         if (icon == null) {
             return bitmap;
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 332e0fa..5465480 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -23,6 +23,7 @@
     package="com.android.launcher3" >
 
     <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
 
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
index 311db21..425fb13 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -31,6 +31,9 @@
 import android.view.View;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.ColorInt;
+import androidx.core.content.ContextCompat;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
@@ -41,9 +44,6 @@
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.util.Themes;
 
-import androidx.annotation.ColorInt;
-import androidx.core.content.ContextCompat;
-
 /**
  * A view which shows a horizontal divider
  */
@@ -288,10 +288,10 @@
     }
 
     @Override
-    public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
-            PropertySetter setter, Interpolator fadeInterpolator) {
+    public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
+            PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
         // Don't use setViewAlpha as we want to control the visibility ourselves.
-        setter.setFloat(this, ALPHA, hasContent ? 1 : 0, fadeInterpolator);
+        setter.setFloat(this, ALPHA, hasAllAppsContent ? 1 : 0, allAppsFade);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
index cb5cbdd..0c7ba9c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -32,6 +32,9 @@
 import android.view.animation.Interpolator;
 import android.widget.LinearLayout;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
@@ -62,9 +65,6 @@
 import java.util.Collections;
 import java.util.List;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 @TargetApi(Build.VERSION_CODES.P)
 public class PredictionRowView extends LinearLayout implements
         LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow {
@@ -80,7 +80,7 @@
 
                 @Override
                 public Integer get(PredictionRowView view) {
-                    return view.mIconCurrentTextAlpha;
+                    return view.mIconLastSetTextAlpha;
                 }
             };
 
@@ -103,6 +103,8 @@
 
     private final int mIconTextColor;
     private final int mIconFullTextAlpha;
+    private int mIconLastSetTextAlpha;
+    // Might use mIconFullTextAlpha instead of mIconLastSetTextAlpha if we are translucent.
     private int mIconCurrentTextAlpha;
 
     private FloatingHeaderView mParent;
@@ -315,8 +317,14 @@
         }
     }
 
-    public void setTextAlpha(int alpha) {
-        mIconCurrentTextAlpha = alpha;
+    public void setTextAlpha(int textAlpha) {
+        mIconLastSetTextAlpha = textAlpha;
+        if (getAlpha() < 1 && textAlpha > 0) {
+            // If the entire header is translucent, make sure the text is at full opacity so it's
+            // not double-translucent. However, we support keeping the text invisible (alpha == 0).
+            textAlpha = mIconFullTextAlpha;
+        }
+        mIconCurrentTextAlpha = textAlpha;
         int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha);
         for (int i = 0; i < getChildCount(); i++) {
             ((BubbleTextView) getChildAt(i)).setTextColor(iconColor);
@@ -324,6 +332,13 @@
     }
 
     @Override
+    public void setAlpha(float alpha) {
+        super.setAlpha(alpha);
+        // Reapply text alpha so that we update it to be full alpha if the row is now translucent.
+        setTextAlpha(mIconLastSetTextAlpha);
+    }
+
+    @Override
     public boolean hasOverlappingRendering() {
         return false;
     }
@@ -351,23 +366,15 @@
     }
 
     @Override
-    public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
-            PropertySetter setter, Interpolator fadeInterpolator) {
-        boolean isDrawn = getAlpha() > 0;
-        int textAlpha = hasHeaderExtra
-                ? (hasContent ? mIconFullTextAlpha : 0) // Text follows the content visibility
-                : mIconCurrentTextAlpha; // Leave as before
-        if (!isDrawn) {
-            // If the header is not drawn, no need to animate the text alpha
-            setTextAlpha(textAlpha);
-        } else {
-            setter.setInt(this, TEXT_ALPHA, textAlpha, fadeInterpolator);
-        }
-
+    public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
+            PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
+        // Text follows all apps visibility
+        int textAlpha = hasHeaderExtra && hasAllAppsContent ? mIconFullTextAlpha : 0;
+        setter.setInt(this, TEXT_ALPHA, textAlpha, allAppsFade);
         setter.setFloat(mOverviewScrollFactor, AnimatedFloat.VALUE,
-                (hasHeaderExtra && !hasContent) ? 1 : 0, LINEAR);
+                (hasHeaderExtra && !hasAllAppsContent) ? 1 : 0, LINEAR);
         setter.setFloat(mContentAlphaFactor, AnimatedFloat.VALUE, hasHeaderExtra ? 1 : 0,
-                fadeInterpolator);
+                headerFade);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
index a3c2e3c..596bc4f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
@@ -164,6 +164,11 @@
             }
         }
 
+        if (FeatureFlags.PULL_DOWN_STATUS_BAR
+                && !launcher.getDeviceProfile().isMultiWindowMode) {
+            list.add(new StatusBarTouchController(launcher));
+        }
+
         list.add(new LauncherTaskViewController(launcher));
         return list.toArray(new TouchController[list.size()]);
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 5ee08c1..50cfac8 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -19,6 +19,7 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.LayoutUtils;
@@ -68,8 +69,8 @@
         if (taskCount == 0) {
             return super.getOverviewScaleAndTranslation(launcher);
         }
-        TaskView dummyTask = recentsView.getTaskViewAt(Math.max(taskCount - 1,
-                recentsView.getCurrentPage()));
+        TaskView dummyTask = recentsView.getTaskViewAt(Utilities.boundToRange(
+                recentsView.getCurrentPage(), 0, taskCount - 1));
         return recentsView.getTempClipAnimationHelper().updateForFullscreenOverview(dummyTask)
                 .getScaleAndTranslation();
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
index c954762..427206a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
@@ -15,7 +15,9 @@
  */
 package com.android.launcher3.uioverrides.states;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCRIM_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
 
@@ -43,6 +45,7 @@
         if (this == OVERVIEW_PEEK && fromState == NORMAL) {
             builder.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
             builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
+            builder.setInterpolator(ANIM_OVERVIEW_SCRIM_FADE, FAST_OUT_SLOW_IN);
         }
     }
 }
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 5543860..151ceb8 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
@@ -43,7 +43,6 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
@@ -128,14 +127,15 @@
         if (launcher.getDeviceProfile().isVerticalBarLayout()) {
             return VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON;
         } else {
+            boolean hasAllAppsHeaderExtra = launcher.getAppsView() != null
+                    && launcher.getAppsView().getFloatingHeaderView().hasVisibleContent();
             return HOTSEAT_SEARCH_BOX | VERTICAL_SWIPE_INDICATOR | RECENTS_CLEAR_ALL_BUTTON |
-                    (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent()
-                            ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
+                    (hasAllAppsHeaderExtra ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS);
         }
     }
 
     @Override
-    public float getWorkspaceScrimAlpha(Launcher launcher) {
+    public float getOverviewScrimAlpha(Launcher launcher) {
         return 0.5f;
     }
 
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 3fe4bcf..ab346c0 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
@@ -23,13 +23,17 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
 import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
 import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_PEEK_COMPONENT;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_HEADER_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_SCALE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_HOTSEAT_TRANSLATE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
@@ -43,6 +47,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.util.MotionPauseDetector;
@@ -102,6 +107,9 @@
                 mPeekAnim.start();
                 recentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+
+                mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
+                        peekDuration, 0);
             });
         }
     }
@@ -120,6 +128,13 @@
             LauncherState toState) {
         if (fromState == NORMAL && toState == ALL_APPS) {
             AnimatorSetBuilder builder = new AnimatorSetBuilder();
+            // Fade in prediction icons quickly, then rest of all apps after reaching overview.
+            float progressToReachOverview = NORMAL.getVerticalProgress(mLauncher)
+                    - OVERVIEW.getVerticalProgress(mLauncher);
+            builder.setInterpolator(ANIM_ALL_APPS_HEADER_FADE, Interpolators.clampToProgress(ACCEL,
+                    0, ALL_APPS_CONTENT_FADE_THRESHOLD));
+            builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(LINEAR,
+                    progressToReachOverview, 1));
 
             // Get workspace out of the way quickly, to prepare for potential pause.
             builder.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL_3);
@@ -168,6 +183,21 @@
     }
 
     @Override
+    protected void goToTargetState(LauncherState targetState, int logAction) {
+        if (mPeekAnim != null && mPeekAnim.isStarted()) {
+            // Don't jump to the target state until overview is no longer peeking.
+            mPeekAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    FlingAndHoldTouchController.super.goToTargetState(targetState, logAction);
+                }
+            });
+        } else {
+            super.goToTargetState(targetState, logAction);
+        }
+    }
+
+    @Override
     protected void updateAnimatorBuilderOnReinit(AnimatorSetBuilder builder) {
         if (handlingOverviewAnim()) {
             // We don't want the state transition to all apps to animate overview,
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 8e32bb3..00e4f58 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -16,10 +16,10 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -266,8 +266,8 @@
             animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
         }
 
-        float nextFrameProgress = Utilities.boundToRange(
-                progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f);
+        float nextFrameProgress = Utilities.boundToRange(progress
+                + velocity * getSingleFrameMs(mActivity) / Math.abs(mEndDisplacement), 0f, 1f);
 
         mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
new file mode 100644
index 0000000..d627a7f
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright (C) 2019 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.quickstep;
+
+import static android.os.VibrationEffect.EFFECT_CLICK;
+import static android.os.VibrationEffect.createPredefined;
+
+import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
+import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+
+import android.animation.Animator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.inputconsumers.InputConsumer;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.InputConsumerController;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+
+import java.util.function.Consumer;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Base class for swipe up handler with some utility methods
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public abstract class BaseSwipeUpHandler<T extends BaseDraggingActivity, Q extends RecentsView>
+        implements SwipeAnimationListener {
+
+    private static final String TAG = "BaseSwipeUpHandler";
+    protected static final Rect TEMP_RECT = new Rect();
+
+    // Start resisting when swiping past this factor of mTransitionDragLength.
+    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
+    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
+    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
+    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
+
+    // The distance needed to drag to reach the task size in recents.
+    protected int mTransitionDragLength;
+    // How much further we can drag past recents, as a factor of mTransitionDragLength.
+    protected float mDragLengthFactor = 1;
+
+    protected final Context mContext;
+    protected final OverviewComponentObserver mOverviewComponentObserver;
+    protected final ActivityControlHelper<T> mActivityControlHelper;
+    protected final RecentsModel mRecentsModel;
+    protected final int mRunningTaskId;
+
+    protected final ClipAnimationHelper mClipAnimationHelper;
+    protected final TransformParams mTransformParams = new TransformParams();
+
+    private final Vibrator mVibrator;
+    protected final Mode mMode;
+
+    // Shift in the range of [0, 1].
+    // 0 => preview snapShot is completely visible, and hotseat is completely translated down
+    // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
+    // visible.
+    protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+
+    protected final ActivityInitListener mActivityInitListener;
+    protected final RecentsAnimationWrapper mRecentsAnimationWrapper;
+
+    protected T mActivity;
+    protected Q mRecentsView;
+    protected DeviceProfile mDp;
+    private final int mPageSpacing;
+
+    protected Runnable mGestureEndCallback;
+
+    protected final Handler mMainThreadHandler = MAIN_THREAD_EXECUTOR.getHandler();
+    protected MultiStateCallback mStateCallback;
+
+    protected boolean mCanceled;
+    protected int mFinishingRecentsAnimationForNewTaskId = -1;
+
+    protected BaseSwipeUpHandler(Context context,
+            OverviewComponentObserver overviewComponentObserver,
+            RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) {
+        mContext = context;
+        mOverviewComponentObserver = overviewComponentObserver;
+        mActivityControlHelper = overviewComponentObserver.getActivityControlHelper();
+        mRecentsModel = recentsModel;
+        mActivityInitListener =
+                mActivityControlHelper.createActivityInitListener(this::onActivityInit);
+        mRunningTaskId = runningTaskId;
+        mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer,
+                this::createNewInputProxyHandler);
+        mMode = SysUINavigationMode.getMode(context);
+
+        mClipAnimationHelper = new ClipAnimationHelper(context);
+        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+        mVibrator = context.getSystemService(Vibrator.class);
+        initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
+                .getDeviceProfile(mContext));
+    }
+
+    protected void setStateOnUiThread(int stateFlag) {
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+            mStateCallback.setState(stateFlag);
+        } else {
+            postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
+        }
+    }
+
+    protected void performHapticFeedback() {
+        if (!mVibrator.hasVibrator()) {
+            return;
+        }
+        if (Settings.System.getInt(
+                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 0) {
+            return;
+        }
+
+        VibrationEffect effect = createPredefined(EFFECT_CLICK);
+        if (effect == null) {
+            return;
+        }
+        BACKGROUND_EXECUTOR.execute(() -> mVibrator.vibrate(effect));
+    }
+
+    public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
+        return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null;
+    }
+
+    @UiThread
+    public void updateDisplacement(float displacement) {
+        // We are moving in the negative x/y direction
+        displacement = -displacement;
+        float shift;
+        if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
+            shift = mDragLengthFactor;
+        } else {
+            float translation = Math.max(displacement, 0);
+            shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
+            if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) {
+                float pullbackProgress = Utilities.getProgress(shift,
+                        DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor);
+                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
+                shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress
+                        * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK);
+            }
+        }
+
+        mCurrentShift.updateValue(shift);
+    }
+
+    public void setGestureEndCallback(Runnable gestureEndCallback) {
+        mGestureEndCallback = gestureEndCallback;
+    }
+
+    public abstract Intent getLaunchIntent();
+
+    protected void linkRecentsViewScroll() {
+        SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> {
+            mTransformParams.setSyncTransactionApplier(applier);
+            mRecentsAnimationWrapper.runOnInit(() ->
+                    mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier));
+        });
+
+        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+            if (moveWindowWithRecentsScroll()) {
+                updateFinalShift();
+            }
+        });
+        mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper);
+        mRecentsView.setClipAnimationHelper(mClipAnimationHelper);
+    }
+
+    protected void startNewTask(int successStateFlag, Consumer<Boolean> resultCallback) {
+        // Launch the task user scrolled to (mRecentsView.getNextPage()).
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            // We finish recents animation inside launchTask() when live tile is enabled.
+            mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false /* animate */,
+                    true /* freezeTaskList */);
+        } else {
+            int taskId = mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).getTask().key.id;
+            mFinishingRecentsAnimationForNewTaskId = taskId;
+            mRecentsAnimationWrapper.finish(true /* toRecents */, () -> {
+                if (!mCanceled) {
+                    TaskView nextTask = mRecentsView.getTaskView(taskId);
+                    if (nextTask != null) {
+                        nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
+                                success -> {
+                                    resultCallback.accept(success);
+                                    if (!success) {
+                                        mActivityControlHelper.onLaunchTaskFailed(mActivity);
+                                        nextTask.notifyTaskLaunchFailed(TAG);
+                                    } else {
+                                        mActivityControlHelper.onLaunchTaskSuccess(mActivity);
+                                    }
+                                }, mMainThreadHandler);
+                    }
+                    setStateOnUiThread(successStateFlag);
+                }
+                mCanceled = false;
+                mFinishingRecentsAnimationForNewTaskId = -1;
+            });
+        }
+        TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
+    }
+
+    @Override
+    public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
+        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
+        final Rect overviewStackBounds;
+        RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
+
+        if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) {
+            overviewStackBounds = mActivityControlHelper
+                    .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget);
+            dp = dp.getMultiWindowProfile(mContext, new Point(
+                    overviewStackBounds.width(), overviewStackBounds.height()));
+        } else {
+            // If we are not in multi-window mode, home insets should be same as system insets.
+            dp = dp.copy(mContext);
+            overviewStackBounds = getStackBounds(dp);
+        }
+        dp.updateInsets(targetSet.homeContentInsets);
+        dp.updateIsSeascape(mContext.getSystemService(WindowManager.class));
+        if (runningTaskTarget != null) {
+            mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+        }
+
+        mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */);
+        initTransitionEndpoints(dp);
+
+        mRecentsAnimationWrapper.setController(targetSet);
+    }
+
+    private Rect getStackBounds(DeviceProfile dp) {
+        if (mActivity != null) {
+            int loc[] = new int[2];
+            View rootView = mActivity.getRootView();
+            rootView.getLocationOnScreen(loc);
+            return new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(),
+                    loc[1] + rootView.getHeight());
+        } else {
+            return new Rect(0, 0, dp.widthPx, dp.heightPx);
+        }
+    }
+
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        mDp = dp;
+
+        mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
+                dp, mContext, TEMP_RECT);
+        if (!dp.isMultiWindowMode) {
+            // When updating the target rect, also update the home bounds since the location on
+            // screen of the launcher window may be stale (position is not updated until first
+            // traversal after the window is resized).  We only do this for non-multiwindow because
+            // we otherwise use the minimized home bounds provided by the system.
+            mClipAnimationHelper.updateHomeBounds(getStackBounds(dp));
+        }
+        mClipAnimationHelper.updateTargetRect(TEMP_RECT);
+        if (mMode == Mode.NO_BUTTON) {
+            // We can drag all the way to the top of the screen.
+            mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
+        }
+    }
+
+    /**
+     * Return true if the window should be translated horizontally if the recents view scrolls
+     */
+    protected abstract boolean moveWindowWithRecentsScroll();
+
+    protected abstract boolean onActivityInit(final T activity, Boolean alreadyOnHome);
+
+    /**
+     * Called to create a input proxy for the running task
+     */
+    @UiThread
+    protected abstract InputConsumer createNewInputProxyHandler();
+
+    /**
+     * Called when the value of {@link #mCurrentShift} changes
+     */
+    @UiThread
+    public abstract void updateFinalShift();
+
+    /**
+     * Called when motion pause is detected
+     */
+    public abstract void onMotionPauseChanged(boolean isPaused);
+
+    @UiThread
+    public void onGestureStarted() { }
+
+    @UiThread
+    public abstract void onGestureCancelled();
+
+    @UiThread
+    public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
+
+    public abstract void onConsumerAboutToBeSwitched(SwipeSharedState sharedState);
+
+    public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
+
+    public void initWhenReady() {
+        // Preload the plan
+        mRecentsModel.getTasks(null);
+
+        mActivityInitListener.register();
+    }
+
+    /**
+     * Applies the transform on the recents animation without any additional null checks
+     */
+    protected void applyTransformUnchecked() {
+        float shift = mCurrentShift.value;
+        float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
+        float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
+                mClipAnimationHelper.getTargetRect().width());
+        mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale);
+        mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
+                mTransformParams);
+    }
+
+    private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
+        float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + mPageSpacing;
+        float interpolation = Math.min(1, offsetX / distanceToReachEdge);
+        return TaskView.getCurveScaleForInterpolation(interpolation);
+    }
+
+    /**
+     * Creates an animation that transforms the current app window into the home app.
+     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+     * @param homeAnimationFactory The home animation factory.
+     */
+    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
+            HomeAnimationFactory homeAnimationFactory) {
+        final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
+        final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
+                mTransformParams.setProgress(startProgress), false /* launcherOnTop */));
+        final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
+
+        final View floatingView = homeAnimationFactory.getFloatingView();
+        final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
+        RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext.getResources());
+        if (isFloatingIconView) {
+            FloatingIconView fiv = (FloatingIconView) floatingView;
+            anim.addAnimatorListener(fiv);
+            fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
+        }
+
+        AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
+
+        // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
+        // rounding at the end of the animation.
+        float startRadius = mClipAnimationHelper.getCurrentCornerRadius();
+        float endRadius = startRect.width() / 6f;
+        // We want the window alpha to be 0 once this threshold is met, so that the
+        // FolderIconView can be seen morphing into the icon shape.
+        final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
+        anim.addOnUpdateListener(new RectFSpringAnim.OnUpdateListener() {
+
+            // Alpha interpolates between [1, 0] between progress values [start, end]
+            final float start = 0f;
+            final float end = 0.85f;
+
+            private float getWindowAlpha(float progress) {
+                if (progress <= start) {
+                    return 1f;
+                }
+                if (progress >= end) {
+                    return 0f;
+                }
+                return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
+            }
+
+            @Override
+            public void onUpdate(RectF currentRect, float progress) {
+                homeAnim.setPlayFraction(progress);
+
+                mTransformParams.setProgress(progress)
+                        .setCurrentRectAndTargetAlpha(currentRect, getWindowAlpha(progress));
+                if (isFloatingIconView) {
+                    mTransformParams.setCornerRadius(endRadius * progress + startRadius
+                            * (1f - progress));
+                }
+                mClipAnimationHelper.applyTransform(targetSet, mTransformParams,
+                        false /* launcherOnTop */);
+
+                if (isFloatingIconView) {
+                    ((FloatingIconView) floatingView).update(currentRect, 1f, progress,
+                            windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(),
+                            false);
+                }
+            }
+
+            @Override
+            public void onCancel() {
+                if (isFloatingIconView) {
+                    ((FloatingIconView) floatingView).fastFinish();
+                }
+            }
+        });
+        anim.addAnimatorListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                homeAnim.dispatchOnStart();
+            }
+
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                homeAnim.getAnimationPlayer().end();
+            }
+        });
+        return anim;
+    }
+
+    public interface Factory {
+
+        BaseSwipeUpHandler newHandler(RunningTaskInfo runningTask,
+                long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask);
+    }
+
+    protected interface RunningWindowAnim {
+        void end();
+
+        void cancel();
+
+        static RunningWindowAnim wrap(Animator animator) {
+            return new RunningWindowAnim() {
+                @Override
+                public void end() {
+                    animator.end();
+                }
+
+                @Override
+                public void cancel() {
+                    animator.cancel();
+                }
+            };
+        }
+
+        static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
+            return new RunningWindowAnim() {
+                @Override
+                public void end() {
+                    rectFSpringAnim.end();
+                }
+
+                @Override
+                public void cancel() {
+                    rectFSpringAnim.cancel();
+                }
+            };
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
index c43155b..8c5a788 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
@@ -80,7 +80,9 @@
 
     @Override
     public void onAssistantVisibilityChanged(float visibility) {
-        // TODO:
+        // This class becomes active when the screen is locked.
+        // Rather than having it handle assistant visibility changes, the assistant visibility is
+        // set to zero prior to this class becoming active.
     }
 
     @NonNull
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index b2a71a4..295585e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -151,16 +151,10 @@
             @NonNull
             @Override
             public RectF getWindowTargetRect() {
-                final int halfIconSize = dp.iconSizePx / 2;
-                final float targetCenterX = dp.availableWidthPx / 2f;
-                final float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
-
                 if (canUseWorkspaceView) {
                     return iconLocation;
                 } else {
-                    // Fallback to animate to center of screen.
-                    return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
-                            targetCenterX + halfIconSize, targetCenterY + halfIconSize);
+                    return HomeAnimationFactory.getDefaultWindowTargetRect(dp);
                 }
             }
 
@@ -194,9 +188,6 @@
     @Override
     public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
             boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "prepareRecentsUI");
-        }
         final LauncherState startState = activity.getStateManager().getState();
 
         LauncherState resetState = startState;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
new file mode 100644
index 0000000..4eb9df2
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -0,0 +1,83 @@
+package com.android.quickstep;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.testing.TestInformationHandler;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.uioverrides.states.OverviewState;
+import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
+import com.android.quickstep.util.LayoutUtils;
+import com.android.quickstep.views.RecentsView;
+
+import java.util.concurrent.ExecutionException;
+
+public class QuickstepTestInformationHandler extends TestInformationHandler {
+
+    public QuickstepTestInformationHandler(Context context) {
+    }
+
+    @Override
+    public Bundle call(String method) {
+        final Bundle response = new Bundle();
+        switch (method) {
+            case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
+                final float swipeHeight =
+                        OverviewState.getDefaultSwipeHeight(mDeviceProfile);
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
+                return response;
+            }
+
+            case TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT: {
+                final float swipeHeight =
+                        LayoutUtils.getShelfTrackingDistance(mContext, mDeviceProfile);
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
+                return response;
+            }
+
+            case TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        TouchInteractionService.isInitialized());
+                return response;
+            }
+
+            case TestProtocol.REQUEST_HOTSEAT_TOP: {
+                if (mLauncher == null) return null;
+
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        PortraitStatesTouchController.getHotseatTop(mLauncher));
+                return response;
+            }
+
+            case TestProtocol.REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN: {
+                try {
+                    final int leftMargin = new MainThreadExecutor().submit(() ->
+                            mLauncher.<RecentsView>getOverviewPanel().getLeftGestureMargin()).get();
+                    response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, leftMargin);
+                } catch (ExecutionException e) {
+                    e.printStackTrace();
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                return response;
+            }
+
+            case TestProtocol.REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN: {
+                try {
+                    final int rightMargin = new MainThreadExecutor().submit(() ->
+                            mLauncher.<RecentsView>getOverviewPanel().getRightGestureMargin()).
+                            get();
+                    response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, rightMargin);
+                } catch (ExecutionException e) {
+                    e.printStackTrace();
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                return response;
+            }
+        }
+
+        return super.call(method);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index fc29a56..f08ae4a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -28,8 +28,10 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.app.ActivityOptions;
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.view.View;
 
@@ -42,7 +44,9 @@
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsRootView;
 import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ObjectWrapper;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
@@ -54,6 +58,9 @@
  */
 public final class RecentsActivity extends BaseRecentsActivity {
 
+    public static final String EXTRA_THUMBNAIL = "thumbnailData";
+    public static final String EXTRA_TASK_ID = "taskID";
+
     private Handler mUiHandler = new Handler(Looper.getMainLooper());
     private RecentsRootView mRecentsRootView;
     private FallbackRecentsView mFallbackRecentsView;
@@ -79,6 +86,22 @@
     }
 
     @Override
+    protected void onNewIntent(Intent intent) {
+        if (intent.getExtras() != null) {
+            int taskID = intent.getIntExtra(EXTRA_TASK_ID, 0);
+            IBinder thumbnail = intent.getExtras().getBinder(EXTRA_THUMBNAIL);
+            if (taskID != 0 && thumbnail instanceof ObjectWrapper) {
+                ThumbnailData thumbnailData = ((ObjectWrapper<ThumbnailData>) thumbnail).get();
+                mFallbackRecentsView.showCurrentTask(taskID);
+                mFallbackRecentsView.updateThumbnail(taskID, thumbnailData);
+            }
+        }
+        intent.removeExtra(EXTRA_TASK_ID);
+        intent.removeExtra(EXTRA_THUMBNAIL);
+        super.onNewIntent(intent);
+    }
+
+    @Override
     protected void onHandleConfigChanged() {
         super.onHandleConfigChanged();
         mRecentsRootView.setup();
@@ -132,7 +155,7 @@
                         mFallbackRecentsView.resetViewUI();
                     }
                 });
-                result.setAnimation(anim);
+                result.setAnimation(anim, RecentsActivity.this);
             }
         };
         return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
@@ -178,6 +201,12 @@
         mFallbackRecentsView.resetTaskVisuals();
     }
 
+    @Override
+    protected void onStop() {
+        super.onStop();
+        mFallbackRecentsView.reset();
+    }
+
     public void onTaskLaunched() {
         mFallbackRecentsView.resetTaskVisuals();
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
index ddd28a3..e51ba63 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -19,12 +19,12 @@
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
 
-import static com.android.launcher3.Utilities.FLAG_NO_GESTURES;
-
 import android.view.InputEvent;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
+import androidx.annotation.UiThread;
+
 import com.android.launcher3.util.Preconditions;
 import com.android.quickstep.inputconsumers.InputConsumer;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
@@ -33,8 +33,6 @@
 import java.util.ArrayList;
 import java.util.function.Supplier;
 
-import androidx.annotation.UiThread;
-
 /**
  * Wrapper around RecentsAnimationController to help with some synchronization
  */
@@ -184,18 +182,15 @@
             }
         }
         if (mInputConsumer != null) {
-            int flags = ev.getEdgeFlags();
-            ev.setEdgeFlags(flags | FLAG_NO_GESTURES);
             mInputConsumer.onMotionEvent(ev);
-            ev.setEdgeFlags(flags);
         }
 
         return true;
     }
 
-    public void setCancelWithDeferredScreenshot(boolean deferredWithScreenshot) {
+    public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
         if (targetSet != null) {
-            targetSet.controller.setCancelWithDeferredScreenshot(deferredWithScreenshot);
+            targetSet.controller.setDeferCancelUntilNextTransition(defer, screenshot);
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
index 213c5d3..fd45923 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
@@ -207,8 +207,7 @@
                         }
                     };
                     WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
-                            future, animStartedListener, mHandler, true /* scaleUp */,
-                            v.getDisplay().getDisplayId());
+                            future, animStartedListener, mHandler, true /* scaleUp */, displayId);
                 }
             });
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 53da0f9..86ba855 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -31,18 +31,22 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.Service;
+import android.app.TaskInfo;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.RectF;
 import android.graphics.Region;
@@ -65,6 +69,7 @@
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.MainThreadExecutor;
@@ -74,6 +79,9 @@
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.logging.EventLogArray;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.model.AppLaunchTracker;
+import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -96,8 +104,10 @@
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.systemui.shared.system.RecentsAnimationListener;
 import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
 
+import com.android.systemui.shared.system.TaskInfoCompat;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Arrays;
@@ -119,10 +129,6 @@
     public String nextArg() {
         return pollFirst().toLowerCase();
     }
-
-    public String nextArgExact() {
-        return pollFirst();
-    }
 }
 
 /**
@@ -141,6 +147,11 @@
 
     private static final String TAG = "TouchInteractionService";
 
+    private static final String KEY_BACK_NOTIFICATION_COUNT = "backNotificationCount";
+    private static final String NOTIFY_ACTION_BACK = "com.android.quickstep.action.BACK_GESTURE";
+    private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
+    private int mBackGestureNotificationCounter = -1;
+
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
         public void onActiveNavBarRegionChanges(Region region) {
@@ -152,6 +163,8 @@
                     .asInterface(bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
             MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::initInputMonitor);
             MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::onSystemUiProxySet);
+            MAIN_THREAD_EXECUTOR.execute(() -> preloadOverview(true /* fromInit */));
+            sIsInitialized = true;
         }
 
         @Override
@@ -199,6 +212,10 @@
                     mOverviewComponentObserver.getActivityControlHelper();
             UserEventDispatcher.newInstance(getBaseContext()).logActionBack(completed, downX, downY,
                     isButton, gestureSwipeLeft, activityControl.getContainerType());
+
+            if (completed && !isButton && shouldNotifyBackGesture()) {
+                BACKGROUND_EXECUTOR.execute(TouchInteractionService.this::tryNotifyBackGesture);
+            }
         }
 
         public void onSystemUiStateChanged(int stateFlags) {
@@ -225,12 +242,17 @@
     };
 
     private static boolean sConnected = false;
+    private static boolean sIsInitialized = false;
     private static final SwipeSharedState sSwipeSharedState = new SwipeSharedState();
 
     public static boolean isConnected() {
         return sConnected;
     }
 
+    public static boolean isInitialized() {
+        return sIsInitialized;
+    }
+
     public static SwipeSharedState getSwipeSharedState() {
         return sSwipeSharedState;
     }
@@ -238,6 +260,11 @@
     private final InputConsumer mResetGestureInputConsumer =
             new ResetGestureInputConsumer(sSwipeSharedState);
 
+    private final BaseSwipeUpHandler.Factory mWindowTreansformFactory =
+            this::createWindowTransformSwipeHandler;
+    private final BaseSwipeUpHandler.Factory mFallbackNoButtonFactory =
+            this::createFallbackNoButtonSwipeHandler;
+
     private ActivityManagerWrapper mAM;
     private RecentsModel mRecentsModel;
     private ISystemUiProxy mISystemUiProxy;
@@ -319,6 +346,9 @@
         if (mInputEventReceiver != null) {
             mInputEventReceiver.dispose();
             mInputEventReceiver = null;
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "disposeEventHandlers");
+            }
         }
         if (mInputMonitorCompat != null) {
             mInputMonitorCompat.dispose();
@@ -327,16 +357,25 @@
     }
 
     private void initInputMonitor() {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 1");
+        }
         if (!mMode.hasGestures || mISystemUiProxy == null) {
             return;
         }
         disposeEventHandlers();
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 2");
+        }
 
         try {
             mInputMonitorCompat = InputMonitorCompat.fromBundle(mISystemUiProxy
                     .monitorGestureInput("swipe-up", mDefaultDisplayId), KEY_EXTRA_INPUT_MONITOR);
             mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
                     mMainChoreographer, this::onInputEvent);
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 3");
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "Unable to create input monitor", e);
         }
@@ -394,6 +433,9 @@
 
     @Override
     public void onNavigationModeChanged(Mode newMode) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onNavigationModeChanged " + newMode);
+        }
         if (mMode.hasGestures != newMode.hasGestures) {
             if (newMode.hasGestures) {
                 getSystemService(DisplayManager.class).registerDisplayListener(
@@ -448,6 +490,8 @@
 
         // Temporarily disable model preload
         // new ModelPreload().start(this);
+        mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this)
+                .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT));
 
         Utilities.unregisterReceiverSafely(this, mUserUnlockedReceiver);
     }
@@ -478,6 +522,7 @@
 
     @Override
     public void onDestroy() {
+        sIsInitialized = false;
         if (mIsUserUnlocked) {
             mInputConsumer.unregisterInputConsumer();
             mOverviewComponentObserver.onDestroy();
@@ -502,6 +547,9 @@
     }
 
     private void onInputEvent(InputEvent ev) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onInputEvent " + ev);
+        }
         if (!(ev instanceof MotionEvent)) {
             Log.e(TAG, "Unknown event " + ev);
             return;
@@ -534,6 +582,7 @@
     private boolean validSystemUiFlags() {
         return (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
                 && (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
+                && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
                 && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
                         || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0);
     }
@@ -553,7 +602,7 @@
             if (isInValidSystemUiState) {
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
-                return createDeviceLockedInputConsumer(mAM.getRunningTask(0));
+                return createDeviceLockedInputConsumer(mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
             } else {
                 return mResetGestureInputConsumer;
             }
@@ -591,7 +640,7 @@
     }
 
     private InputConsumer newBaseConsumer(boolean useSharedState, MotionEvent event) {
-        final RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0);
+        RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0);
         if (!useSharedState) {
             sSwipeSharedState.clearAllState(false /* finishAnimation */);
         }
@@ -603,6 +652,19 @@
         final ActivityControlHelper activityControl =
                 mOverviewComponentObserver.getActivityControlHelper();
 
+        boolean forceOverviewInputConsumer = false;
+        if (isExcludedAssistant(runningTaskInfo)) {
+            // In the case where we are in the excluded assistant state, ignore it and treat the
+            // running activity as the task behind the assistant
+            runningTaskInfo = mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT);
+            if (!ActivityManagerWrapper.isHomeTask(runningTaskInfo)) {
+                final ComponentName homeComponent =
+                    mOverviewComponentObserver.getHomeIntent().getComponent();
+                forceOverviewInputConsumer =
+                    runningTaskInfo.baseIntent.getComponent().equals(homeComponent);
+            }
+        }
+
         if (runningTaskInfo == null && !sSwipeSharedState.goingToLauncher
                 && !sSwipeSharedState.recentsAnimationFinishInterrupted) {
             return mResetGestureInputConsumer;
@@ -612,22 +674,25 @@
             RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
             info.id = sSwipeSharedState.nextRunningTaskId;
             return createOtherActivityInputConsumer(event, info);
-        } else if (sSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
+        } else if (sSwipeSharedState.goingToLauncher || activityControl.isResumed()
+                || forceOverviewInputConsumer) {
             return createOverviewInputConsumer(event);
         } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && activityControl.isInLiveTileMode()) {
             return createOverviewInputConsumer(event);
         } else if (mGestureBlockingActivity != null && runningTaskInfo != null
                 && mGestureBlockingActivity.equals(runningTaskInfo.topActivity)) {
             return mResetGestureInputConsumer;
-        } else if (mMode == Mode.NO_BUTTON && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
-            return new FallbackNoButtonInputConsumer(this, activityControl,
-                    mInputMonitorCompat, sSwipeSharedState, mSwipeTouchRegion,
-                    mOverviewComponentObserver, disableHorizontalSwipe(event), runningTaskInfo);
         } else {
             return createOtherActivityInputConsumer(event, runningTaskInfo);
         }
     }
 
+    private boolean isExcludedAssistant(TaskInfo info) {
+        return info != null
+                && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
+                && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+    }
+
     private boolean disableHorizontalSwipe(MotionEvent event) {
         // mExclusionRegion can change on binder thread, use a local instance here.
         Region exclusionRegion = mExclusionRegion;
@@ -635,17 +700,25 @@
                 && exclusionRegion.contains((int) event.getX(), (int) event.getY());
     }
 
-    private OtherActivityInputConsumer createOtherActivityInputConsumer(MotionEvent event,
+    private InputConsumer createOtherActivityInputConsumer(MotionEvent event,
             RunningTaskInfo runningTaskInfo) {
-        final ActivityControlHelper activityControl =
-                mOverviewComponentObserver.getActivityControlHelper();
-        boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event);
 
-        return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel,
-                mOverviewComponentObserver.getOverviewIntent(), activityControl,
-                shouldDefer, mOverviewCallbacks, mInputConsumer, this::onConsumerInactive,
+        final boolean shouldDefer;
+        final BaseSwipeUpHandler.Factory factory;
+
+        if (mMode == Mode.NO_BUTTON && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+            shouldDefer = !sSwipeSharedState.recentsAnimationFinishInterrupted;
+            factory = mFallbackNoButtonFactory;
+        } else {
+            shouldDefer = mOverviewComponentObserver.getActivityControlHelper()
+                    .deferStartingActivity(mActiveNavBarRegion, event);
+            factory = mWindowTreansformFactory;
+        }
+
+        return new OtherActivityInputConsumer(this, runningTaskInfo,
+                shouldDefer, mOverviewCallbacks, this::onConsumerInactive,
                 sSwipeSharedState, mInputMonitorCompat, mSwipeTouchRegion,
-                disableHorizontalSwipe(event));
+                disableHorizontalSwipe(event), factory);
     }
 
     private InputConsumer createDeviceLockedInputConsumer(RunningTaskInfo taskInfo) {
@@ -669,7 +742,7 @@
             return new OverviewInputConsumer(activity, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
-            return new OverviewWithoutFocusInputConsumer(this, mInputMonitorCompat,
+            return new OverviewWithoutFocusInputConsumer(activity, mInputMonitorCompat,
                     disableHorizontalSwipe(event));
         }
     }
@@ -684,6 +757,60 @@
         }
     }
 
+    private void preloadOverview(boolean fromInit) {
+        if (!mIsUserUnlocked) {
+            return;
+        }
+        if (!mMode.hasGestures && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+            // Prevent the overview from being started before the real home on first boot.
+            return;
+        }
+
+        if (RestoreDbTask.isPending(this)) {
+            // Preloading while a restore is pending may cause launcher to start the restore
+            // too early.
+            return;
+        }
+
+        final ActivityControlHelper<BaseDraggingActivity> activityControl =
+                mOverviewComponentObserver.getActivityControlHelper();
+        if (activityControl.getCreatedActivity() == null) {
+            // Make sure that UI states will be initialized.
+            activityControl.createActivityInitListener((activity, wasVisible) -> {
+                AppLaunchTracker.INSTANCE.get(activity);
+                return false;
+            }).register();
+        } else if (fromInit) {
+            // The activity has been created before the initialization of overview service. It is
+            // usually happens when booting or launcher is the top activity, so we should already
+            // have the latest state.
+            return;
+        }
+
+        // Pass null animation handler to indicate this start is preload.
+        startRecentsActivityAsync(mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState(), null);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        if (!mIsUserUnlocked) {
+            return;
+        }
+        final ActivityControlHelper activityControl =
+                mOverviewComponentObserver.getActivityControlHelper();
+        final BaseDraggingActivity activity = activityControl.getCreatedActivity();
+        if (activity == null || activity.isStarted()) {
+            // We only care about the existing background activity.
+            return;
+        }
+        if (mOverviewComponentObserver.canHandleConfigChanges(activity.getComponentName(),
+                activity.getResources().getConfiguration().diff(newConfig))) {
+            return;
+        }
+
+        preloadOverview(false /* fromInit */);
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] rawArgs) {
         if (rawArgs.length > 0 && Utilities.IS_DEBUG_DEVICE) {
@@ -708,8 +835,9 @@
             pw.println("  assistantAvailable=" + mAssistantAvailable);
             pw.println("  assistantDisabled="
                     + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
-            pw.println("  resumed="
-                    + mOverviewComponentObserver.getActivityControlHelper().isResumed());
+            boolean resumed = mOverviewComponentObserver != null
+                    && mOverviewComponentObserver.getActivityControlHelper().isResumed();
+            pw.println("  resumed=" + resumed);
             pw.println("  useSharedState=" + mConsumer.useSharedSwipeState());
             if (mConsumer.useSharedSwipeState()) {
                 sSwipeSharedState.dump("    ", pw);
@@ -739,4 +867,37 @@
                 break;
         }
     }
+
+    private BaseSwipeUpHandler createWindowTransformSwipeHandler(RunningTaskInfo runningTask,
+            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
+        return  new WindowTransformSwipeHandler(runningTask, this, touchTimeMs,
+                mOverviewComponentObserver, continuingLastGesture, mInputConsumer, mRecentsModel);
+    }
+
+    private BaseSwipeUpHandler createFallbackNoButtonSwipeHandler(RunningTaskInfo runningTask,
+            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
+        return new FallbackNoButtonInputConsumer(this, mOverviewComponentObserver, runningTask,
+                mRecentsModel, mInputConsumer, isLikelyToStartNewTask, continuingLastGesture);
+    }
+
+    protected boolean shouldNotifyBackGesture() {
+        return mBackGestureNotificationCounter > 0 &&
+                mGestureBlockingActivity != null;
+    }
+
+    @WorkerThread
+    protected void tryNotifyBackGesture() {
+        if (shouldNotifyBackGesture()) {
+            mBackGestureNotificationCounter--;
+            Utilities.getDevicePrefs(this).edit()
+                    .putInt(KEY_BACK_NOTIFICATION_COUNT, mBackGestureNotificationCounter).apply();
+            sendBroadcast(new Intent(NOTIFY_ACTION_BACK).setPackage(
+                    mGestureBlockingActivity.getPackageName()));
+        }
+    }
+
+    public static void startRecentsActivityAsync(Intent intent, RecentsAnimationListener listener) {
+        BACKGROUND_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
+                .startRecentsActivity(intent, null, listener, null, null));
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index af18bbf..0d29e5d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -17,22 +17,18 @@
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
-import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
-import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.HIDE;
 import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.PEEK;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME;
 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK;
@@ -43,65 +39,49 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.Canvas;
-import android.graphics.Point;
 import android.graphics.PointF;
-import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.SystemClock;
-import android.util.Log;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 import android.view.WindowInsets;
-import android.view.WindowManager;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.RaceConditionTracker;
 import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.views.FloatingIconView;
-import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
 import com.android.quickstep.ActivityControlHelper.AnimationFactory;
 import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState;
 import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.inputconsumers.InputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
-import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ClipAnimationHelper.TargetAlphaProvider;
 import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.RemoteAnimationTargetSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -109,19 +89,14 @@
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.LatencyTrackerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.WindowCallbacksCompat;
 
-import java.util.function.BiFunction;
-import java.util.function.Consumer;
-
 @TargetApi(Build.VERSION_CODES.O)
 public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
-        implements SwipeAnimationListener, OnApplyWindowInsetsListener {
+        extends BaseSwipeUpHandler<T, RecentsView>
+        implements OnApplyWindowInsetsListener {
     private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
 
-    private static final Rect TEMP_RECT = new Rect();
-
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
     private static int getFlagForIndex(int index, String name) {
@@ -220,64 +195,30 @@
     private static final long SHELF_ANIM_DURATION = 240;
     public static final long RECENTS_ATTACH_DURATION = 300;
 
-    // Start resisting when swiping past this factor of mTransitionDragLength.
-    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
-    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
-    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
-    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
-
     /**
      * Used as the page index for logging when we return to the last task at the end of the gesture.
      */
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
-    private final ClipAnimationHelper mClipAnimationHelper;
-    private final ClipAnimationHelper.TransformParams mTransformParams;
-
-    private Runnable mGestureEndCallback;
     private GestureEndTarget mGestureEndTarget;
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim mRunningWindowAnim;
     private boolean mIsShelfPeeking;
-    private DeviceProfile mDp;
-    // The distance needed to drag to reach the task size in recents.
-    private int mTransitionDragLength;
-    // How much further we can drag past recents, as a factor of mTransitionDragLength.
-    private float mDragLengthFactor = 1;
 
-    // Shift in the range of [0, 1].
-    // 0 => preview snapShot is completely visible, and hotseat is completely translated down
-    // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
-    // visible.
-    private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
     private boolean mContinuingLastGesture;
     // To avoid UI jump when gesture is started, we offset the animation by the threshold.
     private float mShiftAtGestureStart = 0;
 
-    private final Handler mMainThreadHandler = MAIN_THREAD_EXECUTOR.getHandler();
-
-    private final Context mContext;
-    private final ActivityControlHelper<T> mActivityControlHelper;
-    private final ActivityInitListener mActivityInitListener;
-
-    private final SysUINavigationMode.Mode mMode;
-
-    private final int mRunningTaskId;
     private ThumbnailData mTaskSnapshot;
 
-    private MultiStateCallback mStateCallback;
     // Used to control launcher components throughout the swipe gesture.
     private AnimatorPlaybackController mLauncherTransitionController;
     private boolean mHasLauncherTransitionControllerStarted;
 
-    private T mActivity;
-    private RecentsView mRecentsView;
     private AnimationFactory mAnimationFactory = (t) -> { };
     private LiveTileOverlay mLiveTileOverlay = new LiveTileOverlay();
 
-    private boolean mCanceled;
     private boolean mWasLauncherAlreadyVisible;
-    private int mFinishingRecentsAnimationForNewTaskId = -1;
 
     private boolean mPassedOverviewThreshold;
     private boolean mGestureStarted;
@@ -286,31 +227,17 @@
     private PointF mDownPos;
     private boolean mIsLikelyToStartNewTask;
 
-    private final RecentsAnimationWrapper mRecentsAnimationWrapper;
-
     private final long mTouchTimeMs;
     private long mLauncherFrameDrawnTime;
 
     public WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context,
-            long touchTimeMs, ActivityControlHelper<T> controller, boolean continuingLastGesture,
-            InputConsumerController inputConsumer) {
-        mContext = context;
-        mRunningTaskId = runningTaskInfo.id;
+            long touchTimeMs, OverviewComponentObserver overviewComponentObserver,
+            boolean continuingLastGesture,
+            InputConsumerController inputConsumer, RecentsModel recentsModel) {
+        super(context, overviewComponentObserver, recentsModel, inputConsumer, runningTaskInfo.id);
         mTouchTimeMs = touchTimeMs;
-        mActivityControlHelper = controller;
-        mActivityInitListener = mActivityControlHelper
-                .createActivityInitListener(this::onActivityInit);
         mContinuingLastGesture = continuingLastGesture;
-        mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer,
-                this::createNewInputProxyHandler);
-        mClipAnimationHelper = new ClipAnimationHelper(context);
-        mTransformParams = new ClipAnimationHelper.TransformParams();
-
-        mMode = SysUINavigationMode.getMode(context);
         initStateCallbacks();
-
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
-        initTransitionEndpoints(dp);
     }
 
     private void initStateCallbacks() {
@@ -373,44 +300,8 @@
         }
     }
 
-    private void setStateOnUiThread(int stateFlag) {
-        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
-            mStateCallback.setState(stateFlag);
-        } else {
-            postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
-        }
-    }
-
-    private void initTransitionEndpoints(DeviceProfile dp) {
-        mDp = dp;
-
-        Rect tempRect = new Rect();
-        mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
-                dp, mContext, tempRect);
-        mClipAnimationHelper.updateTargetRect(tempRect);
-        if (mMode == Mode.NO_BUTTON) {
-            // We can drag all the way to the top of the screen.
-            mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
-        }
-    }
-
-    private long getFadeInDuration() {
-        if (mCurrentShift.getCurrentAnimation() != null) {
-            ObjectAnimator anim = mCurrentShift.getCurrentAnimation();
-            long theirDuration = anim.getDuration() - anim.getCurrentPlayTime();
-
-            // TODO: Find a better heuristic
-            return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION));
-        } else {
-            return MAX_SWIPE_DURATION;
-        }
-    }
-
-    public void initWhenReady() {
-        mActivityInitListener.register();
-    }
-
-    private boolean onActivityInit(final T activity, Boolean alreadyOnHome) {
+    @Override
+    protected boolean onActivityInit(final T activity, Boolean alreadyOnHome) {
         if (mActivity == activity) {
             return true;
         }
@@ -431,19 +322,7 @@
         }
 
         mRecentsView = activity.getOverviewPanel();
-        SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> {
-            mTransformParams.setSyncTransactionApplier(applier);
-            mRecentsAnimationWrapper.runOnInit(() ->
-                    mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier));
-            });
-
-        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
-            if (mGestureEndTarget != HOME) {
-                updateFinalShift();
-            }
-        });
-        mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper);
-        mRecentsView.setClipAnimationHelper(mClipAnimationHelper);
+        linkRecentsViewScroll();
         mRecentsView.setLiveTileOverlay(mLiveTileOverlay);
         mActivity.getRootView().getOverlay().add(mLiveTileOverlay);
 
@@ -458,33 +337,23 @@
         return true;
     }
 
+    @Override
+    protected boolean moveWindowWithRecentsScroll() {
+        return mGestureEndTarget != HOME;
+    }
+
     private void onLauncherStart(final T activity) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart");
-        }
         if (mActivity != activity) {
             return;
         }
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 1");
-        }
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return;
         }
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 2");
-        }
 
         // If we've already ended the gesture and are going home, don't prepare recents UI,
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
         if (mGestureEndTarget != HOME) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 3");
-            }
             Runnable initAnimFactory = () -> {
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 4");
-                }
                 mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity,
                         mWasLauncherAlreadyVisible, true,
                         this::onAnimatorPlaybackControllerCreated);
@@ -494,18 +363,13 @@
                 // Launcher is visible, but might be about to stop. Thus, if we prepare recents
                 // now, it might get overridden by moveToRestState() in onStop(). To avoid this,
                 // wait until the next gesture (and possibly launcher) starts.
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 5");
-                }
                 mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory);
             } else {
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 6");
-                }
                 initAnimFactory.run();
             }
         }
-        AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible);
+        AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible,
+                AbstractFloatingView.TYPE_LISTENER);
 
         if (mWasLauncherAlreadyVisible) {
             mStateCallback.setState(STATE_LAUNCHER_DRAWN);
@@ -576,30 +440,7 @@
         return TaskView.getCurveScaleForInterpolation(interpolation);
     }
 
-    public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
-        return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null;
-    }
-
-    @UiThread
-    public void updateDisplacement(float displacement) {
-        // We are moving in the negative x/y direction
-        displacement = -displacement;
-        if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
-            mCurrentShift.updateValue(mDragLengthFactor);
-        } else {
-            float translation = Math.max(displacement, 0);
-            float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
-            if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) {
-                float pullbackProgress = Utilities.getProgress(shift,
-                        DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor);
-                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
-                shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress
-                        * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK);
-            }
-            mCurrentShift.updateValue(shift);
-        }
-    }
-
+    @Override
     public void onMotionPauseChanged(boolean isPaused) {
         setShelfState(isPaused ? PEEK : HIDE, OVERSHOOT_1_2, SHELF_ANIM_DURATION);
     }
@@ -650,6 +491,7 @@
         mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
     }
 
+    @Override
     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
         if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) {
             mIsLikelyToStartNewTask = isLikelyToStartNewTask;
@@ -665,9 +507,8 @@
         if (mIsShelfPeeking != wasShelfPeeking) {
             maybeUpdateRecentsAttachedState();
         }
-        if (mRecentsView != null && shelfState.shouldPreformHaptic) {
-            mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+        if (shelfState.shouldPreformHaptic) {
+            performHapticFeedback();
         }
     }
 
@@ -696,19 +537,18 @@
         updateLauncherTransitionProgress();
     }
 
-    @UiThread
-    private void updateFinalShift() {
-        float shift = mCurrentShift.value;
+    @Override
+    public Intent getLaunchIntent() {
+        return mOverviewComponentObserver.getOverviewIntent();
+    }
+
+    @Override
+    public void updateFinalShift() {
 
         SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController();
         if (controller != null) {
-            float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
-            float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
-                    mClipAnimationHelper.getTargetRect().width());
-            mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale);
-            mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
-                    mTransformParams);
-            updateSysUiFlags(shift);
+            applyTransformUnchecked();
+            updateSysUiFlags(mCurrentShift.value);
         }
 
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
@@ -721,9 +561,8 @@
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
         if (passed != mPassedOverviewThreshold) {
             mPassedOverviewThreshold = passed;
-            if (mRecentsView != null && mMode != Mode.NO_BUTTON) {
-                mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+            if (mMode != Mode.NO_BUTTON) {
+                performHapticFeedback();
             }
         }
 
@@ -766,39 +605,7 @@
 
     @Override
     public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
-        final Rect overviewStackBounds;
-        RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
-
-        if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) {
-            overviewStackBounds = mActivityControlHelper
-                    .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget);
-            dp = dp.getMultiWindowProfile(mContext, new Point(
-                    targetSet.minimizedHomeBounds.width(), targetSet.minimizedHomeBounds.height()));
-            dp.updateInsets(targetSet.homeContentInsets);
-        } else {
-            if (mActivity != null) {
-                int loc[] = new int[2];
-                View rootView = mActivity.getRootView();
-                rootView.getLocationOnScreen(loc);
-                overviewStackBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(),
-                        loc[1] + rootView.getHeight());
-            } else {
-                overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx);
-            }
-            // If we are not in multi-window mode, home insets should be same as system insets.
-            dp = dp.copy(mContext);
-            dp.updateInsets(targetSet.homeContentInsets);
-        }
-        dp.updateIsSeascape(mContext.getSystemService(WindowManager.class));
-
-        if (runningTaskTarget != null) {
-            mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
-        }
-        mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */);
-        initTransitionEndpoints(dp);
-
-        mRecentsAnimationWrapper.setController(targetSet);
+        super.onRecentsAnimationStart(targetSet);
         TOUCH_INTERACTION_LOG.addLog("startRecentsAnimationCallback", targetSet.apps.length);
         setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
 
@@ -813,7 +620,7 @@
         TOUCH_INTERACTION_LOG.addLog("cancelRecentsAnimation");
     }
 
-    @UiThread
+    @Override
     public void onGestureStarted() {
         notifyGestureStartedAsync();
         mShiftAtGestureStart = mCurrentShift.value;
@@ -837,7 +644,7 @@
     /**
      * Called as a result on ACTION_CANCEL to return the UI to the start state.
      */
-    @UiThread
+    @Override
     public void onGestureCancelled() {
         updateDisplacement(0);
         setStateOnUiThread(STATE_GESTURE_COMPLETED);
@@ -850,7 +657,7 @@
      * @param velocity The x and y components of the velocity when the gesture ends.
      * @param downPos The x and y value of where the gesture started.
      */
-    @UiThread
+    @Override
     public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_velocity);
@@ -868,9 +675,9 @@
         handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
     }
 
-    @UiThread
-    private InputConsumer createNewInputProxyHandler() {
-        endRunningWindowAnim();
+    @Override
+    protected InputConsumer createNewInputProxyHandler() {
+        endRunningWindowAnim(mGestureEndTarget == HOME /* cancel */);
         endLauncherTransitionController();
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             // Hide the task view, if not already hidden
@@ -882,9 +689,13 @@
                 ? InputConsumer.NO_OP : new OverviewInputConsumer(activity, null, true);
     }
 
-    private void endRunningWindowAnim() {
+    private void endRunningWindowAnim(boolean cancel) {
         if (mRunningWindowAnim != null) {
-            mRunningWindowAnim.end();
+            if (cancel) {
+                mRunningWindowAnim.cancel();
+            } else {
+                mRunningWindowAnim.end();
+            }
         }
     }
 
@@ -925,18 +736,19 @@
                                 : LAST_TASK;
             }
         } else {
-            if (mMode == Mode.NO_BUTTON && endVelocity < 0 && !mIsShelfPeeking) {
+            // If swiping at a diagonal, base end target on the faster velocity.
+            boolean isSwipeUp = endVelocity < 0;
+            boolean willGoToNewTaskOnSwipeUp =
+                    goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity);
+
+            if (mMode == Mode.NO_BUTTON && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
+                endTarget = HOME;
+            } else if (mMode == Mode.NO_BUTTON && isSwipeUp && !mIsShelfPeeking) {
                 // If swiping at a diagonal, base end target on the faster velocity.
-                endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity)
-                        ? NEW_TASK : HOME;
-            } else if (endVelocity < 0) {
-                if (reachedOverviewThreshold) {
-                    endTarget = RECENTS;
-                } else {
-                    // If swiping at a diagonal, base end target on the faster velocity.
-                    endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity)
-                            ? NEW_TASK : RECENTS;
-                }
+                endTarget = NEW_TASK;
+            } else if (isSwipeUp) {
+                endTarget = !reachedOverviewThreshold && willGoToNewTaskOnSwipeUp
+                        ? NEW_TASK : RECENTS;
             } else {
                 endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
             }
@@ -969,14 +781,14 @@
             interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
         } else {
             startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y
-                    * SINGLE_FRAME_MS / mTransitionDragLength, 0, mDragLengthFactor);
+                    * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
             float minFlingVelocity = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
                 if (endTarget == RECENTS && mMode != Mode.NO_BUTTON) {
                     Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
-                            startShift, endShift, endShift, velocityPxPerMs.y,
-                            mTransitionDragLength);
+                            startShift, endShift, endShift, endVelocity / 1000,
+                            mTransitionDragLength, mContext);
                     endShift = overshoot.end;
                     interpolator = overshoot.interpolator;
                     duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION,
@@ -1150,57 +962,15 @@
      * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
      * @param homeAnimationFactory The home animation factory.
      */
-    private RectFSpringAnim createWindowAnimationToHome(float startProgress,
+    @Override
+    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
             HomeAnimationFactory homeAnimationFactory) {
-        final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
-        final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
-                mTransformParams.setProgress(startProgress), false /* launcherOnTop */));
-        final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
-
-        final View floatingView = homeAnimationFactory.getFloatingView();
-        final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
-        RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mActivity.getResources());
-        if (isFloatingIconView) {
-            FloatingIconView fiv = (FloatingIconView) floatingView;
-            anim.addAnimatorListener(fiv);
-            fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
-        }
-
-        AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
-
-        // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
-        // rounding at the end of the animation.
-        float startRadius = mClipAnimationHelper.getCurrentCornerRadius();
-        float endRadius = startRect.width() / 6f;
-        // We want the window alpha to be 0 once this threshold is met, so that the
-        // FolderIconView can be seen morphing into the icon shape.
-        final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
-        anim.addOnUpdateListener((currentRect, progress) -> {
-            homeAnim.setPlayFraction(progress);
-
-            float alphaProgress = ACCEL_1_5.getInterpolation(progress);
-            float windowAlpha = Utilities.boundToRange(Utilities.mapToRange(alphaProgress, 0,
-                    windowAlphaThreshold, 1.5f, 0f, Interpolators.LINEAR), 0, 1);
-            mTransformParams.setProgress(progress)
-                    .setCurrentRectAndTargetAlpha(currentRect, windowAlpha);
-            if (isFloatingIconView) {
-                mTransformParams.setCornerRadius(endRadius * progress + startRadius
-                        * (1f - progress));
-            }
-            mClipAnimationHelper.applyTransform(targetSet, mTransformParams,
-                    false /* launcherOnTop */);
-
-            if (isFloatingIconView) {
-                ((FloatingIconView) floatingView).update(currentRect, 1f, progress,
-                        windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(), false);
-            }
-
-            updateSysUiFlags(Math.max(progress, mCurrentShift.value));
-        });
+        RectFSpringAnim anim =
+                super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
+        anim.addOnUpdateListener((r, p) -> updateSysUiFlags(Math.max(p, mCurrentShift.value)));
         anim.addAnimatorListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(Animator animation) {
-                homeAnim.dispatchOnStart();
                 if (mActivity != null) {
                     mActivity.getRootView().getOverlay().remove(mLiveTileOverlay);
                 }
@@ -1208,7 +978,6 @@
 
             @Override
             public void onAnimationSuccess(Animator animator) {
-                homeAnim.getAnimationPlayer().end();
                 if (mRecentsView != null) {
                     mRecentsView.post(mRecentsView::resetTaskVisuals);
                 }
@@ -1220,11 +989,18 @@
         return anim;
     }
 
-    /**
-     * @return The GestureEndTarget if the gesture has ended, else null.
-     */
-    public @Nullable GestureEndTarget getGestureEndTarget() {
-        return mGestureEndTarget;
+    @Override
+    public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) {
+        if (mGestureEndTarget != null) {
+            sharedState.canGestureBeContinued = mGestureEndTarget.canBeContinued;
+            sharedState.goingToLauncher = mGestureEndTarget.isLauncher;
+        }
+
+        if (sharedState.canGestureBeContinued) {
+            cancelCurrentAnimation(sharedState);
+        } else {
+            reset();
+        }
     }
 
     @UiThread
@@ -1237,43 +1013,18 @@
 
     @UiThread
     private void startNewTask() {
-        // Launch the task user scrolled to (mRecentsView.getNextPage()).
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            // We finish recents animation inside launchTask() when live tile is enabled.
-            mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false /* animate */,
-                    true /* freezeTaskList */);
-        } else {
-            int taskId = mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).getTask().key.id;
-            mFinishingRecentsAnimationForNewTaskId = taskId;
-            mRecentsAnimationWrapper.finish(true /* toRecents */, () -> {
-                if (!mCanceled) {
-                    TaskView nextTask = mRecentsView.getTaskView(taskId);
-                    if (nextTask != null) {
-                        nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
-                                success -> {
-                            if (!success) {
-                                // We couldn't launch the task, so take user to overview so they can
-                                // decide what to do instead of staying in this broken state.
-                                endLauncherTransitionController();
-                                mActivityControlHelper.onLaunchTaskFailed(mActivity);
-                                nextTask.notifyTaskLaunchFailed(TAG);
-                                updateSysUiFlags(1 /* windowProgress == overview */);
-                            } else {
-                                mActivityControlHelper.onLaunchTaskSuccess(mActivity);
-                            }
-                        }, mMainThreadHandler);
-                        doLogGesture(NEW_TASK);
-                    }
-                    reset();
-                }
-                mCanceled = false;
-                mFinishingRecentsAnimationForNewTaskId = -1;
-            });
-        }
-        TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
+        startNewTask(STATE_HANDLER_INVALIDATED, success -> {
+            if (!success) {
+                // We couldn't launch the task, so take user to overview so they can
+                // decide what to do instead of staying in this broken state.
+                endLauncherTransitionController();
+                updateSysUiFlags(1 /* windowProgress == overview */);
+            }
+            doLogGesture(NEW_TASK);
+        });
     }
 
-    public void reset() {
+    private void reset() {
         setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
 
@@ -1281,7 +1032,7 @@
      * Cancels any running animation so that the active target can be overriden by a new swipe
      * handle (in case of quick switch).
      */
-    public void cancelCurrentAnimation(SwipeSharedState sharedState) {
+    private void cancelCurrentAnimation(SwipeSharedState sharedState) {
         mCanceled = true;
         mCurrentShift.cancelAnimation();
         if (mLauncherTransitionController != null && mLauncherTransitionController
@@ -1304,7 +1055,7 @@
     }
 
     private void invalidateHandler() {
-        endRunningWindowAnim();
+        endRunningWindowAnim(false /* cancel */);
 
         if (mGestureEndCallback != null) {
             mGestureEndCallback.run();
@@ -1435,7 +1186,8 @@
     private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
         endLauncherTransitionController();
         mActivityControlHelper.onSwipeUpToRecentsComplete(mActivity);
-        mRecentsAnimationWrapper.setCancelWithDeferredScreenshot(true);
+        mRecentsAnimationWrapper.setDeferCancelUntilNextTransition(true /* defer */,
+                true /* screenshot */);
         mRecentsView.onSwipeUpAnimationSuccess();
 
         RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG);
@@ -1444,17 +1196,12 @@
         reset();
     }
 
-    public void setGestureEndCallback(Runnable gestureEndCallback) {
-        mGestureEndCallback = gestureEndCallback;
-    }
-
-    private void setTargetAlphaProvider(
-            BiFunction<RemoteAnimationTargetCompat, Float, Float> provider) {
+    private void setTargetAlphaProvider(TargetAlphaProvider provider) {
         mClipAnimationHelper.setTaskAlphaCallback(provider);
         updateFinalShift();
     }
 
-    public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) {
+    public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, float expectedAlpha) {
         if (!isNotInRecents(app)) {
             return 0;
         }
@@ -1465,16 +1212,4 @@
         return app.isNotInRecents
                 || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
     }
-
-    private interface RunningWindowAnim {
-        void end();
-
-        static RunningWindowAnim wrap(Animator animator) {
-            return animator::end;
-        }
-
-        static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
-            return rectFSpringAnim::end;
-        }
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index c2876180..2e9c0a3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 
+import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
@@ -31,6 +32,10 @@
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+
+import java.util.ArrayList;
 
 public class FallbackRecentsView extends RecentsView<RecentsActivity> {
 
@@ -54,6 +59,8 @@
     private float mZoomScale = 1f;
     private float mZoomTranslationY = 0f;
 
+    private RunningTaskInfo mRunningTaskInfo;
+
     public FallbackRecentsView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -88,6 +95,12 @@
     }
 
     @Override
+    public void reset() {
+        super.reset();
+        resetViewUI();
+    }
+
+    @Override
     protected void getTaskSize(DeviceProfile dp, Rect outRect) {
         LayoutUtils.calculateFallbackTaskSize(getContext(), dp, outRect);
     }
@@ -115,6 +128,12 @@
     }
 
     @Override
+    public void resetTaskVisuals() {
+        super.resetTaskVisuals();
+        setFullscreenProgress(mFullscreenProgress);
+    }
+
+    @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
 
@@ -139,4 +158,41 @@
         TRANSLATION_Y.set(this, Utilities.mapRange(mZoomInProgress, 0, mZoomTranslationY));
         FULLSCREEN_PROGRESS.set(this, mZoomInProgress);
     }
+
+    public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) {
+        mRunningTaskInfo = runningTaskInfo;
+        onGestureAnimationStart(runningTaskInfo == null ? -1 : runningTaskInfo.taskId);
+    }
+
+    @Override
+    public void setCurrentTask(int runningTaskId) {
+        super.setCurrentTask(runningTaskId);
+        if (mRunningTaskInfo != null && mRunningTaskInfo.taskId != runningTaskId) {
+            mRunningTaskInfo = null;
+        }
+    }
+
+    @Override
+    protected void applyLoadPlan(ArrayList<Task> tasks) {
+        // When quick-switching on 3p-launcher, we add a "dummy" tile corresponding to Launcher
+        // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
+        // track the index of the next task appropriately, as it we are switching on any other app.
+        if (mRunningTaskInfo != null && mRunningTaskInfo.taskId == mRunningTaskId) {
+            // Check if the task list has running task
+            boolean found = false;
+            for (Task t : tasks) {
+                if (t.key.id == mRunningTaskId) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                ArrayList<Task> newList = new ArrayList<>(tasks.size() + 1);
+                newList.addAll(tasks);
+                newList.add(Task.from(new TaskKey(mRunningTaskInfo), mRunningTaskInfo, false));
+                tasks = newList;
+            }
+        }
+        super.applyLoadPlan(tasks);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index db2af59..3d763ab 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.Utilities.squaredTouchSlop;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -44,8 +45,6 @@
 import com.android.quickstep.util.ClipAnimationHelper;
 import com.android.quickstep.util.RecentsAnimationListenerSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -209,9 +208,7 @@
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 
         mInputMonitorCompat.pilferPointers();
-        BackgroundExecutor.get().submit(
-                () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
-                        intent, null, newListenerSet, null, null));
+        startRecentsActivityAsync(intent, newListenerSet);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
index d05ca2a..6ec1da0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
@@ -15,344 +15,443 @@
  */
 package com.android.quickstep.inputconsumers;
 
-import static android.view.MotionEvent.ACTION_CANCEL;
-import static android.view.MotionEvent.ACTION_DOWN;
-import static android.view.MotionEvent.ACTION_MOVE;
-import static android.view.MotionEvent.ACTION_POINTER_DOWN;
-import static android.view.MotionEvent.ACTION_POINTER_UP;
-import static android.view.MotionEvent.ACTION_UP;
-import static android.view.MotionEvent.INVALID_POINTER_ID;
-
-import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION;
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.RecentsActivity.EXTRA_TASK_ID;
+import static com.android.quickstep.RecentsActivity.EXTRA_THUMBNAIL;
 import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_SWIPE_DURATION;
-import static com.android.quickstep.inputconsumers.OtherActivityInputConsumer.QUICKSTEP_TOUCH_SLOP_RATIO;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.HOME;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.NEW_TASK;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.RECENTS;
+import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
+import android.animation.AnimatorSet;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
-import android.graphics.Rect;
 import android.graphics.RectF;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
-import android.view.WindowManager;
+import android.os.Bundle;
 
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
-import com.android.quickstep.ActivityControlHelper;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.BaseSwipeUpHandler;
+import com.android.quickstep.MultiStateCallback;
 import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SwipeSharedState;
-import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
-import com.android.quickstep.util.NavBarPosition;
-import com.android.quickstep.util.RecentsAnimationListenerSet;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.util.ObjectWrapper;
+import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
-import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.BackgroundExecutor;
-import com.android.systemui.shared.system.InputMonitorCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.InputConsumerController;
 
-public class FallbackNoButtonInputConsumer implements InputConsumer, SwipeAnimationListener {
+public class FallbackNoButtonInputConsumer extends
+        BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
 
-    private static final int STATE_NOT_FINISHED = 0;
-    private static final int STATE_FINISHED_TO_HOME = 1;
-    private static final int STATE_FINISHED_TO_APP = 2;
+    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[5] : null;
 
-    private static final float PROGRESS_TO_END_GESTURE = -2;
+    private static int getFlagForIndex(int index, String name) {
+        if (DEBUG_STATES) {
+            STATE_NAMES[index] = name;
+        }
+        return 1 << index;
+    }
 
-    private final ActivityControlHelper mActivityControlHelper;
-    private final InputMonitorCompat mInputMonitor;
-    private final Context mContext;
-    private final NavBarPosition mNavBarPosition;
-    private final SwipeSharedState mSwipeSharedState;
-    private final OverviewComponentObserver mOverviewComponentObserver;
-    private final int mRunningTaskId;
+    private static final int STATE_RECENTS_PRESENT =
+            getFlagForIndex(0, "STATE_RECENTS_PRESENT");
+    private static final int STATE_HANDLER_INVALIDATED =
+            getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
 
-    private final ClipAnimationHelper mClipAnimationHelper;
-    private final TransformParams mTransformParams = new TransformParams();
-    private final float mTransitionDragLength;
-    private final DeviceProfile mDP;
+    private static final int STATE_GESTURE_CANCELLED =
+            getFlagForIndex(2, "STATE_GESTURE_CANCELLED");
+    private static final int STATE_GESTURE_COMPLETED =
+            getFlagForIndex(3, "STATE_GESTURE_COMPLETED");
+    private static final int STATE_APP_CONTROLLER_RECEIVED =
+            getFlagForIndex(4, "STATE_APP_CONTROLLER_RECEIVED");
 
-    private final RectF mSwipeTouchRegion;
-    private final boolean mDisableHorizontalSwipe;
+    public enum GestureEndTarget {
+        HOME(3, 100, 1),
+        RECENTS(1, 300, 0),
+        LAST_TASK(0, 150, 1),
+        NEW_TASK(0, 150, 1);
 
-    private final PointF mDownPos = new PointF();
-    private final PointF mLastPos = new PointF();
+        private final float mEndProgress;
+        private final long mDurationMultiplier;
+        private final float mLauncherAlpha;
 
-    private int mActivePointerId = -1;
-    // Slop used to determine when we say that the gesture has started.
-    private boolean mPassedPilferInputSlop;
+        GestureEndTarget(float endProgress, long durationMultiplier, float launcherAlpha) {
+            mEndProgress = endProgress;
+            mDurationMultiplier = durationMultiplier;
+            mLauncherAlpha = launcherAlpha;
+        }
+    }
 
-    private VelocityTracker mVelocityTracker;
+    private final AnimatedFloat mLauncherAlpha = new AnimatedFloat(this::onLauncherAlphaChanged);
 
-    // Distance after which we start dragging the window.
-    private final float mTouchSlop;
+    private boolean mIsMotionPaused = false;
+    private GestureEndTarget mEndTarget;
 
-    // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
-    private float mStartDisplacement;
-    private SwipeAnimationTargetSet mSwipeAnimationTargetSet;
-    private float mProgress;
+    private final boolean mInQuickSwitchMode;
+    private final boolean mContinuingLastGesture;
+    private final boolean mRunningOverHome;
+    private final boolean mSwipeUpOverHome;
 
-    private int mState = STATE_NOT_FINISHED;
+    private final RunningTaskInfo mRunningTaskInfo;
+
+    private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
+    private RunningWindowAnim mFinishAnimation;
 
     public FallbackNoButtonInputConsumer(Context context,
-            ActivityControlHelper activityControlHelper, InputMonitorCompat inputMonitor,
-            SwipeSharedState swipeSharedState, RectF swipeTouchRegion,
             OverviewComponentObserver overviewComponentObserver,
-            boolean disableHorizontalSwipe, RunningTaskInfo runningTaskInfo) {
-        mContext = context;
-        mActivityControlHelper = activityControlHelper;
-        mInputMonitor = inputMonitor;
-        mOverviewComponentObserver = overviewComponentObserver;
-        mRunningTaskId = runningTaskInfo.id;
+            RunningTaskInfo runningTaskInfo, RecentsModel recentsModel,
+            InputConsumerController inputConsumer,
+            boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
+        super(context, overviewComponentObserver, recentsModel, inputConsumer, runningTaskInfo.id);
+        mLauncherAlpha.value = 1;
 
-        mSwipeSharedState = swipeSharedState;
-        mSwipeTouchRegion = swipeTouchRegion;
-        mDisableHorizontalSwipe = disableHorizontalSwipe;
+        mRunningTaskInfo = runningTaskInfo;
+        mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
+        mContinuingLastGesture = continuingLastGesture;
+        mRunningOverHome = ActivityManagerWrapper.isHomeTask(runningTaskInfo);
+        mSwipeUpOverHome = mRunningOverHome && !mInQuickSwitchMode;
 
-        mNavBarPosition = new NavBarPosition(context);
-        mVelocityTracker = VelocityTracker.obtain();
-
-        mTouchSlop = QUICKSTEP_TOUCH_SLOP_RATIO
-                * ViewConfiguration.get(context).getScaledTouchSlop();
-
-        mClipAnimationHelper = new ClipAnimationHelper(context);
-
-        mDP = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context).copy(context);
-        Rect tempRect = new Rect();
-        mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
-                mDP, context, tempRect);
-        mClipAnimationHelper.updateTargetRect(tempRect);
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_FALLBACK_NO_BUTTON;
-    }
-
-    @Override
-    public void onMotionEvent(MotionEvent ev) {
-        if (mVelocityTracker == null) {
-            return;
-        }
-
-        mVelocityTracker.addMovement(ev);
-        if (ev.getActionMasked() == ACTION_POINTER_UP) {
-            mVelocityTracker.clear();
-        }
-
-        switch (ev.getActionMasked()) {
-            case ACTION_DOWN: {
-                mActivePointerId = ev.getPointerId(0);
-                mDownPos.set(ev.getX(), ev.getY());
-                mLastPos.set(mDownPos);
-                break;
-            }
-            case ACTION_POINTER_DOWN: {
-                if (!mPassedPilferInputSlop) {
-                    // Cancel interaction in case of multi-touch interaction
-                    int ptrIdx = ev.getActionIndex();
-                    if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) {
-                        forceCancelGesture(ev);
-                    }
-                }
-                break;
-            }
-            case ACTION_POINTER_UP: {
-                int ptrIdx = ev.getActionIndex();
-                int ptrId = ev.getPointerId(ptrIdx);
-                if (ptrId == mActivePointerId) {
-                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
-                    mDownPos.set(
-                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
-                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
-                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
-                    mActivePointerId = ev.getPointerId(newPointerIdx);
-                }
-                break;
-            }
-            case ACTION_MOVE: {
-                int pointerIndex = ev.findPointerIndex(mActivePointerId);
-                if (pointerIndex == INVALID_POINTER_ID) {
-                    break;
-                }
-                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
-                float displacement = getDisplacement(ev);
-
-                if (!mPassedPilferInputSlop) {
-                    if (mDisableHorizontalSwipe && Math.abs(mLastPos.x - mDownPos.x)
-                            > Math.abs(mLastPos.y - mDownPos.y)) {
-                        // Horizontal gesture is not allowed in this region
-                        forceCancelGesture(ev);
-                        break;
-                    }
-
-                    if (Math.abs(displacement) >= mTouchSlop) {
-                        mPassedPilferInputSlop = true;
-
-                        // Deferred gesture, start the animation and gesture tracking once
-                        // we pass the actual touch slop
-                        startTouchTrackingForWindowAnimation(displacement);
-                    }
-                } else {
-                    updateDisplacement(displacement - mStartDisplacement);
-                }
-                break;
-            }
-            case ACTION_CANCEL:
-            case ACTION_UP: {
-                finishTouchTracking(ev);
-                break;
-            }
-        }
-    }
-
-    private void startTouchTrackingForWindowAnimation(float displacement) {
-        mStartDisplacement = Math.min(displacement, -mTouchSlop);
-
-        RecentsAnimationListenerSet listenerSet =
-                mSwipeSharedState.newRecentsAnimationListenerSet();
-        listenerSet.addListener(this);
-        Intent homeIntent = mOverviewComponentObserver.getHomeIntent();
-        BackgroundExecutor.get().submit(
-                () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
-                        homeIntent, null, listenerSet, null, null));
-
-        ActivityManagerWrapper.getInstance().closeSystemWindows(
-                CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-        mInputMonitor.pilferPointers();
-    }
-
-    private void updateDisplacement(float displacement) {
-        mProgress = displacement / mTransitionDragLength;
-        mTransformParams.setProgress(mProgress);
-
-        if (mSwipeAnimationTargetSet != null) {
-            mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
-        }
-    }
-
-    private void forceCancelGesture(MotionEvent ev) {
-        int action = ev.getAction();
-        ev.setAction(ACTION_CANCEL);
-        finishTouchTracking(ev);
-        ev.setAction(action);
-    }
-
-    /**
-     * Called when the gesture has ended. Does not correlate to the completion of the interaction as
-     * the animation can still be running.
-     */
-    private void finishTouchTracking(MotionEvent ev) {
-        if (ev.getAction() == ACTION_CANCEL) {
-            mState = STATE_FINISHED_TO_APP;
+        if (mSwipeUpOverHome) {
+            mClipAnimationHelper.setBaseAlphaCallback((t, a) -> 1 - mLauncherAlpha.value);
         } else {
-            mVelocityTracker.computeCurrentVelocity(1000,
-                    ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
-            float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
-            float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
-            float velocity = mNavBarPosition.isRightEdge() ? velocityX
-                    : mNavBarPosition.isLeftEdge() ? -velocityX
-                            : velocityY;
+            mClipAnimationHelper.setBaseAlphaCallback((t, a) -> mLauncherAlpha.value);
+        }
+
+        initStateCallbacks();
+    }
+
+    private void initStateCallbacks() {
+        mStateCallback = new MultiStateCallback(STATE_NAMES);
+
+        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED,
+                this::onHandlerInvalidated);
+        mStateCallback.addCallback(STATE_RECENTS_PRESENT | STATE_HANDLER_INVALIDATED,
+                this::onHandlerInvalidatedWithRecents);
+
+        mStateCallback.addCallback(STATE_GESTURE_CANCELLED | STATE_APP_CONTROLLER_RECEIVED,
+                this::finishAnimationTargetSetAnimationComplete);
+
+        if (mInQuickSwitchMode) {
+            mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED
+                            | STATE_RECENTS_PRESENT,
+                    this::finishAnimationTargetSet);
+        } else {
+            mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED,
+                    this::finishAnimationTargetSet);
+        }
+    }
+
+    private void onLauncherAlphaChanged() {
+        if (mRecentsAnimationWrapper.targetSet != null && mEndTarget == null) {
+            applyTransformUnchecked();
+        }
+    }
+
+    @Override
+    protected boolean onActivityInit(final RecentsActivity activity, Boolean alreadyOnHome) {
+        mActivity = activity;
+        mRecentsView = activity.getOverviewPanel();
+        linkRecentsViewScroll();
+        mRecentsView.setDisallowScrollToClearAll(true);
+        mRecentsView.getClearAllButton().setVisibilityAlpha(0);
+
+        mRecentsView.setZoomProgress(1);
+
+        if (!mContinuingLastGesture) {
+            if (mRunningOverHome) {
+                mRecentsView.onGestureAnimationStart(mRunningTaskInfo);
+            } else {
+                mRecentsView.onGestureAnimationStart(mRunningTaskId);
+            }
+        }
+        setStateOnUiThread(STATE_RECENTS_PRESENT);
+        return true;
+    }
+
+    @Override
+    protected boolean moveWindowWithRecentsScroll() {
+        return mInQuickSwitchMode;
+    }
+
+    @Override
+    public void initWhenReady() {
+        if (mInQuickSwitchMode) {
+            // Only init if we are in quickswitch mode
+            super.initWhenReady();
+        }
+    }
+
+    @Override
+    public void updateDisplacement(float displacement) {
+        if (!mInQuickSwitchMode) {
+            super.updateDisplacement(displacement);
+        }
+    }
+
+    @Override
+    protected InputConsumer createNewInputProxyHandler() {
+        // Just consume all input on the active task
+        return InputConsumer.NO_OP;
+    }
+
+    @Override
+    public void onMotionPauseChanged(boolean isPaused) {
+        if (!mInQuickSwitchMode) {
+            mIsMotionPaused = isPaused;
+            mLauncherAlpha.animateToValue(mLauncherAlpha.value, isPaused ? 0 : 1)
+                    .setDuration(150).start();
+            performHapticFeedback();
+        }
+    }
+
+    @Override
+    public Intent getLaunchIntent() {
+        if (mInQuickSwitchMode || mSwipeUpOverHome) {
+            return mOverviewComponentObserver.getOverviewIntent();
+        } else {
+            return mOverviewComponentObserver.getHomeIntent();
+        }
+    }
+
+    @Override
+    public void updateFinalShift() {
+        mTransformParams.setProgress(mCurrentShift.value);
+        mRecentsAnimationWrapper.setWindowThresholdCrossed(!mInQuickSwitchMode
+                && (mCurrentShift.value > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD));
+        if (mRecentsAnimationWrapper.targetSet != null) {
+            applyTransformUnchecked();
+        }
+    }
+
+    @Override
+    public void onGestureCancelled() {
+        updateDisplacement(0);
+        mEndTarget = LAST_TASK;
+        setStateOnUiThread(STATE_GESTURE_CANCELLED);
+    }
+
+    @Override
+    public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
+        mEndVelocityPxPerMs.set(0, velocity.y / 1000);
+        if (mInQuickSwitchMode) {
+            // For now set it to non-null, it will be reset before starting the animation
+            mEndTarget = LAST_TASK;
+        } else {
             float flingThreshold = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_threshold_velocity);
-            boolean isFling = Math.abs(velocity) > flingThreshold;
+            boolean isFling = Math.abs(endVelocity) > flingThreshold;
 
-            boolean goingHome;
-            if (!isFling) {
-                goingHome = -mProgress >= MIN_PROGRESS_FOR_OVERVIEW;
+            if (isFling) {
+                mEndTarget = endVelocity < 0 ? HOME : LAST_TASK;
+            } else if (mIsMotionPaused) {
+                mEndTarget = RECENTS;
             } else {
-                goingHome = velocity < 0;
+                mEndTarget = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? HOME : LAST_TASK;
+            }
+        }
+        setStateOnUiThread(STATE_GESTURE_COMPLETED);
+    }
+
+    @Override
+    public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) {
+        if (mInQuickSwitchMode && mEndTarget != null) {
+            sharedState.canGestureBeContinued = true;
+            sharedState.goingToLauncher = false;
+
+            mCanceled = true;
+            mCurrentShift.cancelAnimation();
+            if (mFinishAnimation != null) {
+                mFinishAnimation.cancel();
             }
 
-            if (goingHome) {
-                mState = STATE_FINISHED_TO_HOME;
-            } else {
-                mState = STATE_FINISHED_TO_APP;
+            if (mRecentsView != null) {
+                if (mFinishingRecentsAnimationForNewTaskId != -1) {
+                    TaskView newRunningTaskView = mRecentsView.getTaskView(
+                            mFinishingRecentsAnimationForNewTaskId);
+                    int newRunningTaskId = newRunningTaskView != null
+                            ? newRunningTaskView.getTask().key.id
+                            : -1;
+                    mRecentsView.setCurrentTask(newRunningTaskId);
+                    sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId);
+                }
+                mRecentsView.setOnScrollChangeListener(null);
+            }
+        } else {
+            setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+        }
+    }
+
+    private void onHandlerInvalidated() {
+        mActivityInitListener.unregister();
+        if (mGestureEndCallback != null) {
+            mGestureEndCallback.run();
+        }
+        if (mFinishAnimation != null) {
+            mFinishAnimation.end();
+        }
+    }
+
+    private void onHandlerInvalidatedWithRecents() {
+        mRecentsView.onGestureAnimationEnd();
+        mRecentsView.setDisallowScrollToClearAll(false);
+        mRecentsView.getClearAllButton().setVisibilityAlpha(1);
+    }
+
+    private void finishAnimationTargetSetAnimationComplete() {
+        switch (mEndTarget) {
+            case HOME: {
+                if (mSwipeUpOverHome) {
+                    mRecentsAnimationWrapper.finish(false, null, false);
+                    // Send a home intent to clear the task stack
+                    mContext.startActivity(mOverviewComponentObserver.getHomeIntent());
+                } else {
+                    mRecentsAnimationWrapper.finish(true, null, true);
+                }
+                break;
+            }
+            case LAST_TASK:
+                mRecentsAnimationWrapper.finish(false, null, false);
+                break;
+            case RECENTS: {
+                if (mSwipeUpOverHome) {
+                    mRecentsAnimationWrapper.finish(true, null, true);
+                    break;
+                }
+
+                ThumbnailData thumbnail =
+                        mRecentsAnimationWrapper.targetSet.controller.screenshotTask(mRunningTaskId);
+                mRecentsAnimationWrapper.setDeferCancelUntilNextTransition(true /* defer */,
+                        false /* screenshot */);
+
+                ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
+                ActivityOptionsCompat.setFreezeRecentTasksList(options);
+
+                Bundle extras = new Bundle();
+                extras.putBinder(EXTRA_THUMBNAIL, new ObjectWrapper<>(thumbnail));
+                extras.putInt(EXTRA_TASK_ID, mRunningTaskId);
+
+                Intent intent = new Intent(mOverviewComponentObserver.getOverviewIntent())
+                        .putExtras(extras);
+                mContext.startActivity(intent, options.toBundle());
+                mRecentsAnimationWrapper.targetSet.controller.cleanupScreenshot();
+                break;
+            }
+            case NEW_TASK: {
+                startNewTask(STATE_HANDLER_INVALIDATED, b -> {});
+                break;
             }
         }
 
-        if (mSwipeAnimationTargetSet != null) {
-            finishAnimationTargetSet();
-        }
+        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
 
     private void finishAnimationTargetSet() {
-        if (mState == STATE_FINISHED_TO_APP) {
-            mSwipeAnimationTargetSet.finishController(false, null, false);
-        } else {
-            if (mProgress < PROGRESS_TO_END_GESTURE) {
-                mSwipeAnimationTargetSet.finishController(true, null, true);
+        if (mInQuickSwitchMode) {
+            // Recalculate the end target, some views might have been initialized after
+            // gesture has ended.
+            if (mRecentsView == null || !mRecentsAnimationWrapper.hasTargets()) {
+                mEndTarget = LAST_TASK;
             } else {
-                long duration = (long) (Math.min(mProgress - PROGRESS_TO_END_GESTURE, 1)
-                        * MAX_SWIPE_DURATION / Math.abs(PROGRESS_TO_END_GESTURE));
-                if (duration < 0) {
-                    duration = MIN_SWIPE_DURATION;
-                }
-
-                ValueAnimator anim = ValueAnimator.ofFloat(mProgress, PROGRESS_TO_END_GESTURE);
-                anim.addUpdateListener(a -> {
-                    float p = (Float) anim.getAnimatedValue();
-                    mTransformParams.setProgress(p);
-                    mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
-                });
-                anim.setDuration(duration);
-                anim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mSwipeAnimationTargetSet.finishController(true, null, true);
-                    }
-                });
-                anim.start();
+                final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
+                final int taskToLaunch = mRecentsView.getNextPage();
+                mEndTarget = (runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex)
+                        ? NEW_TASK : LAST_TASK;
             }
         }
+
+        float endProgress = mEndTarget.mEndProgress;
+        long duration = (long) (mEndTarget.mDurationMultiplier *
+                Math.abs(endProgress - mCurrentShift.value));
+        if (mRecentsView != null) {
+            duration = Math.max(duration, mRecentsView.getScroller().getDuration());
+        }
+        if (mCurrentShift.value != endProgress || mInQuickSwitchMode) {
+            AnimationSuccessListener endListener = new AnimationSuccessListener() {
+
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    finishAnimationTargetSetAnimationComplete();
+                    mFinishAnimation = null;
+                }
+            };
+
+            if (mEndTarget == HOME && !mRunningOverHome) {
+                RectFSpringAnim anim = createWindowAnimationToHome(mCurrentShift.value, duration);
+                anim.addAnimatorListener(endListener);
+                anim.start(mEndVelocityPxPerMs);
+                mFinishAnimation = RunningWindowAnim.wrap(anim);
+            } else {
+
+                AnimatorSet anim = new AnimatorSet();
+                anim.play(mLauncherAlpha.animateToValue(
+                        mLauncherAlpha.value, mEndTarget.mLauncherAlpha));
+                anim.play(mCurrentShift.animateToValue(mCurrentShift.value, endProgress));
+
+                anim.setDuration(duration);
+                anim.addListener(endListener);
+                anim.start();
+                mFinishAnimation = RunningWindowAnim.wrap(anim);
+            }
+
+        } else {
+            finishAnimationTargetSetAnimationComplete();
+        }
     }
 
     @Override
     public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
-        mSwipeAnimationTargetSet = targetSet;
-        Rect overviewStackBounds = new Rect(0, 0, mDP.widthPx, mDP.heightPx);
-        RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
+        super.onRecentsAnimationStart(targetSet);
+        mRecentsAnimationWrapper.enableInputConsumer();
 
-        mDP.updateIsSeascape(mContext.getSystemService(WindowManager.class));
-        if (runningTaskTarget != null) {
-            mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+        if (mRunningOverHome) {
+            mClipAnimationHelper.prepareAnimation(mDp, true);
         }
-        mClipAnimationHelper.prepareAnimation(mDP, false /* isOpening */);
+        applyTransformUnchecked();
 
-        overviewStackBounds
-                .inset(-overviewStackBounds.width() / 5, -overviewStackBounds.height() / 5);
-        mClipAnimationHelper.updateTargetRect(overviewStackBounds);
-        mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
-
-        if (mState != STATE_NOT_FINISHED) {
-            finishAnimationTargetSet();
-        }
+        setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
     }
 
     @Override
-    public void onRecentsAnimationCanceled() { }
-
-    private float getDisplacement(MotionEvent ev) {
-        if (mNavBarPosition.isRightEdge()) {
-            return ev.getX() - mDownPos.x;
-        } else if (mNavBarPosition.isLeftEdge()) {
-            return mDownPos.x - ev.getX();
-        } else {
-            return ev.getY() - mDownPos.y;
-        }
+    public void onRecentsAnimationCanceled() {
+        mRecentsAnimationWrapper.setController(null);
+        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
 
-    @Override
-    public boolean allowInterceptByParent() {
-        return !mPassedPilferInputSlop;
+    /**
+     * Creates an animation that transforms the current app window into the home app.
+     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+     */
+    private RectFSpringAnim createWindowAnimationToHome(float startProgress, long duration) {
+        HomeAnimationFactory factory = new HomeAnimationFactory() {
+            @Override
+            public RectF getWindowTargetRect() {
+                return HomeAnimationFactory.getDefaultWindowTargetRect(mDp);
+            }
+
+            @Override
+            public AnimatorPlaybackController createActivityAnimationToHome() {
+                AnimatorSet anim = new AnimatorSet();
+                anim.play(mLauncherAlpha.animateToValue(mLauncherAlpha.value, 1));
+                anim.setDuration(duration);
+                return AnimatorPlaybackController.wrap(anim, duration);
+            }
+        };
+        return createWindowAnimationToHome(startProgress, factory);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java
index f5cf654..a1e5d47 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/InputConsumer.java
@@ -33,7 +33,6 @@
     int TYPE_SCREEN_PINNED = 1 << 6;
     int TYPE_OVERVIEW_WITHOUT_FOCUS = 1 << 7;
     int TYPE_RESET_GESTURE = 1 << 8;
-    int TYPE_FALLBACK_NO_BUTTON = 1 << 9;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -45,7 +44,6 @@
             "TYPE_SCREEN_PINNED",           // 6
             "TYPE_OVERVIEW_WITHOUT_FOCUS",  // 7
             "TYPE_RESET_GESTURE",           // 8
-            "TYPE_FALLBACK_NO_BUTTON",      // 9
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 4c137d3..86766d9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -28,13 +28,13 @@
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.content.Intent;
 import android.graphics.PointF;
 import android.graphics.RectF;
 import android.os.Build;
@@ -48,21 +48,16 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RaceConditionTracker;
 import com.android.launcher3.util.TraceHelper;
-import com.android.quickstep.ActivityControlHelper;
+import com.android.quickstep.BaseSwipeUpHandler;
 import com.android.quickstep.OverviewCallbacks;
-import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SwipeSharedState;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.WindowTransformSwipeHandler;
-import com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget;
 import com.android.quickstep.util.CachedEventDispatcher;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.quickstep.util.RecentsAnimationListenerSet;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.BackgroundExecutor;
-import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 import java.util.function.Consumer;
@@ -83,16 +78,14 @@
 
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
     private final RunningTaskInfo mRunningTask;
-    private final RecentsModel mRecentsModel;
-    private final Intent mHomeIntent;
-    private final ActivityControlHelper mActivityControlHelper;
     private final OverviewCallbacks mOverviewCallbacks;
-    private final InputConsumerController mInputConsumer;
     private final SwipeSharedState mSwipeSharedState;
     private final InputMonitorCompat mInputMonitorCompat;
     private final SysUINavigationMode.Mode mMode;
     private final RectF mSwipeTouchRegion;
 
+    private final BaseSwipeUpHandler.Factory mHandlerFactory;
+
     private final NavBarPosition mNavBarPosition;
 
     private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
@@ -100,7 +93,7 @@
     private final float mMotionPauseMinDisplacement;
     private VelocityTracker mVelocityTracker;
 
-    private WindowTransformSwipeHandler mInteractionHandler;
+    private BaseSwipeUpHandler mInteractionHandler;
 
     private final boolean mIsDeferredDownTarget;
     private final PointF mDownPos = new PointF();
@@ -114,7 +107,7 @@
     private final boolean mDisableHorizontalSwipe;
 
     // Slop used to check when we start moving window.
-    private boolean mPaddedWindowMoveSlop;
+    private boolean mPassedWindowMoveSlop;
     // Slop used to determine when we say that the gesture has started.
     private boolean mPassedPilferInputSlop;
 
@@ -128,20 +121,18 @@
     };
 
     public OtherActivityInputConsumer(Context base, RunningTaskInfo runningTaskInfo,
-            RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl,
             boolean isDeferredDownTarget, OverviewCallbacks overviewCallbacks,
-            InputConsumerController inputConsumer,
             Consumer<OtherActivityInputConsumer> onCompleteCallback,
             SwipeSharedState swipeSharedState, InputMonitorCompat inputMonitorCompat,
-            RectF swipeTouchRegion, boolean disableHorizontalSwipe) {
+            RectF swipeTouchRegion, boolean disableHorizontalSwipe,
+            BaseSwipeUpHandler.Factory handlerFactory) {
         super(base);
 
         mMainThreadHandler = new Handler(Looper.getMainLooper());
         mRunningTask = runningTaskInfo;
-        mRecentsModel = recentsModel;
-        mHomeIntent = homeIntent;
         mMode = SysUINavigationMode.getMode(base);
         mSwipeTouchRegion = swipeTouchRegion;
+        mHandlerFactory = handlerFactory;
 
         mMotionPauseDetector = new MotionPauseDetector(base);
         mMotionPauseMinDisplacement = base.getResources().getDimension(
@@ -150,11 +141,9 @@
         mVelocityTracker = VelocityTracker.obtain();
         mInputMonitorCompat = inputMonitorCompat;
 
-        mActivityControlHelper = activityControl;
         boolean continuingPreviousGesture = swipeSharedState.getActiveListener() != null;
         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
         mOverviewCallbacks = overviewCallbacks;
-        mInputConsumer = inputConsumer;
         mSwipeSharedState = swipeSharedState;
 
         mNavBarPosition = new NavBarPosition(base);
@@ -163,7 +152,7 @@
         float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop;
         mSquaredTouchSlop = slop * slop;
 
-        mPassedPilferInputSlop = mPaddedWindowMoveSlop = continuingPreviousGesture;
+        mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
     }
 
@@ -186,7 +175,7 @@
         }
 
         // Proxy events to recents view
-        if (mPaddedWindowMoveSlop && mInteractionHandler != null
+        if (mPassedWindowMoveSlop && mInteractionHandler != null
                 && !mRecentsViewDispatcher.hasConsumer()) {
             mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher(
                     mNavBarPosition.getRotationMode()));
@@ -213,7 +202,7 @@
                 // Start the window animation on down to give more time for launcher to draw if the
                 // user didn't start the gesture over the back button
                 if (!mIsDeferredDownTarget) {
-                    startTouchTrackingForWindowAnimation(ev.getEventTime());
+                    startTouchTrackingForWindowAnimation(ev.getEventTime(), false);
                 }
 
                 RaceConditionTracker.onEvent(DOWN_EVT, EXIT);
@@ -251,17 +240,21 @@
                 float displacement = getDisplacement(ev);
                 float displacementX = mLastPos.x - mDownPos.x;
 
-                if (!mPaddedWindowMoveSlop) {
+                if (!mPassedWindowMoveSlop) {
                     if (!mIsDeferredDownTarget) {
                         // Normal gesture, ensure we pass the drag slop before we start tracking
                         // the gesture
                         if (Math.abs(displacement) > mTouchSlop) {
-                            mPaddedWindowMoveSlop = true;
+                            mPassedWindowMoveSlop = true;
                             mStartDisplacement = Math.min(displacement, -mTouchSlop);
                         }
                     }
                 }
 
+                float horizontalDist = Math.abs(displacementX);
+                float upDist = -displacement;
+                boolean isLikelyToStartNewTask = horizontalDist > upDist;
+
                 if (!mPassedPilferInputSlop) {
                     float displacementY = mLastPos.y - mDownPos.y;
                     if (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop) {
@@ -277,10 +270,11 @@
                         if (mIsDeferredDownTarget) {
                             // Deferred gesture, start the animation and gesture tracking once
                             // we pass the actual touch slop
-                            startTouchTrackingForWindowAnimation(ev.getEventTime());
+                            startTouchTrackingForWindowAnimation(
+                                    ev.getEventTime(), isLikelyToStartNewTask);
                         }
-                        if (!mPaddedWindowMoveSlop) {
-                            mPaddedWindowMoveSlop = true;
+                        if (!mPassedWindowMoveSlop) {
+                            mPassedWindowMoveSlop = true;
                             mStartDisplacement = Math.min(displacement, -mTouchSlop);
 
                         }
@@ -289,15 +283,12 @@
                 }
 
                 if (mInteractionHandler != null) {
-                    if (mPaddedWindowMoveSlop) {
+                    if (mPassedWindowMoveSlop) {
                         // Move
                         mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
                     }
 
                     if (mMode == Mode.NO_BUTTON) {
-                        float horizontalDist = Math.abs(displacementX);
-                        float upDist = -displacement;
-                        boolean isLikelyToStartNewTask = horizontalDist > upDist;
                         mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
                                 || isLikelyToStartNewTask);
                         mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
@@ -329,16 +320,14 @@
         mInteractionHandler.onGestureStarted();
     }
 
-    private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
+    private void startTouchTrackingForWindowAnimation(
+            long touchTimeMs, boolean isLikelyToStartNewTask) {
         TOUCH_INTERACTION_LOG.addLog("startRecentsAnimation");
 
         RecentsAnimationListenerSet listenerSet = mSwipeSharedState.getActiveListener();
-        final WindowTransformSwipeHandler handler = new WindowTransformSwipeHandler(
-                mRunningTask, this, touchTimeMs, mActivityControlHelper,
-                listenerSet != null, mInputConsumer);
+        final BaseSwipeUpHandler handler = mHandlerFactory.newHandler(mRunningTask, touchTimeMs,
+                listenerSet != null, isLikelyToStartNewTask);
 
-        // Preload the plan
-        mRecentsModel.getTasks(null);
         mInteractionHandler = handler;
         handler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(handler::onMotionPauseChanged);
@@ -352,9 +341,7 @@
             RecentsAnimationListenerSet newListenerSet =
                     mSwipeSharedState.newRecentsAnimationListenerSet();
             newListenerSet.addListener(handler);
-            BackgroundExecutor.get().submit(
-                    () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
-                            mHomeIntent, null, newListenerSet, null, null));
+            startRecentsActivityAsync(handler.getLaunchIntent(), newListenerSet);
         }
     }
 
@@ -366,7 +353,7 @@
         RaceConditionTracker.onEvent(UP_EVT, ENTER);
         TraceHelper.endSection("TouchInt");
 
-        if (mPaddedWindowMoveSlop && mInteractionHandler != null) {
+        if (mPassedWindowMoveSlop && mInteractionHandler != null) {
             if (ev.getActionMasked() == ACTION_CANCEL) {
                 mInteractionHandler.onGestureCancelled();
             } else {
@@ -409,14 +396,7 @@
             // The consumer is being switched while we are active. Set up the shared state to be
             // used by the next animation
             removeListener();
-            GestureEndTarget endTarget = mInteractionHandler.getGestureEndTarget();
-            mSwipeSharedState.canGestureBeContinued = endTarget != null && endTarget.canBeContinued;
-            mSwipeSharedState.goingToLauncher = endTarget != null && endTarget.isLauncher;
-            if (mSwipeSharedState.canGestureBeContinued) {
-                mInteractionHandler.cancelCurrentAnimation(mSwipeSharedState);
-            } else {
-                mInteractionHandler.reset();
-            }
+            mInteractionHandler.onConsumerAboutToBeSwitched(mSwipeSharedState);
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index 56db209..e553891 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -19,6 +19,7 @@
 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
@@ -26,6 +27,7 @@
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.OverviewCallbacks;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 425b8b6..05cbb78 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -30,7 +30,13 @@
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.StatsLogUtils;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.quickstep.OverviewCallbacks;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -131,12 +137,14 @@
                 ? -velocityX : (mNavBarPosition.isLeftEdge() ? velocityX : -velocityY);
 
         final boolean triggerQuickstep;
+        int touch = Touch.FLING;
         if (Math.abs(velocity) >= ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) {
             triggerQuickstep = velocity > 0;
         } else {
             float displacementX = mDisableHorizontalSwipe ? 0 : (ev.getX() - mDownPos.x);
             float displacementY = ev.getY() - mDownPos.y;
             triggerQuickstep = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
+            touch = Touch.SWIPE;
         }
 
         if (triggerQuickstep) {
@@ -144,6 +152,13 @@
             ActivityManagerWrapper.getInstance()
                     .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
             TOUCH_INTERACTION_LOG.addLog("startQuickstep");
+            BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
+            int pageIndex = -1; // This number doesn't reflect workspace page index.
+                                // It only indicates that launcher client screen was shown.
+            int containerType = StatsLogUtils.getContainerTypeFromState(activity.getCurrentState());
+            activity.getUserEventDispatcher().logActionOnContainer(
+                    touch, Direction.UP, containerType, pageIndex);
+            activity.getUserEventDispatcher().setPreviousHomeGesture(true);
         } else {
             // ignore
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
index de671e0..cae273a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -99,8 +99,8 @@
     // Whether to boost the opening animation target layers, or the closing
     private int mBoostModeTargetLayers = -1;
 
-    private BiFunction<RemoteAnimationTargetCompat, Float, Float> mTaskAlphaCallback =
-            (t, a1) -> a1;
+    private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
+    private TargetAlphaProvider mBaseAlphaCallback = (t, a) -> 1;
 
     public ClipAnimationHelper(Context context) {
         mWindowCornerRadius = getWindowCornerRadius(context.getResources());
@@ -119,8 +119,12 @@
     }
 
     public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
-        mHomeStackBounds.set(homeStackBounds);
         updateSourceStack(target);
+        updateHomeBounds(homeStackBounds);
+    }
+
+    public void updateHomeBounds(Rect homeStackBounds) {
+        mHomeStackBounds.set(homeStackBounds);
     }
 
     public void updateTargetRect(Rect targetRect) {
@@ -187,12 +191,12 @@
             Rect crop = mTmpRect;
             crop.set(app.sourceContainerBounds);
             crop.offsetTo(0, 0);
-            float alpha = 1f;
+            float alpha;
             int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
             float cornerRadius = 0f;
             float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width();
             if (app.mode == targetSet.targetMode) {
-                alpha = mTaskAlphaCallback.apply(app, params.targetAlpha);
+                alpha = mTaskAlphaCallback.getAlpha(app, params.targetAlpha);
                 if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
                     mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
                     mTmpMatrix.postTranslate(app.position.x, app.position.y);
@@ -214,9 +218,12 @@
                     // home target.
                     alpha = 1 - (progress * params.targetAlpha);
                 }
-            } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
-                crop = null;
-                layer = Integer.MAX_VALUE;
+            } else {
+                alpha = mBaseAlphaCallback.getAlpha(app, progress);
+                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
+                    crop = null;
+                    layer = Integer.MAX_VALUE;
+                }
             }
 
             // Since radius is in Surface space, but we draw the rounded corners in screen space, we
@@ -247,11 +254,14 @@
         }
     }
 
-    public void setTaskAlphaCallback(
-            BiFunction<RemoteAnimationTargetCompat, Float, Float> callback) {
+    public void setTaskAlphaCallback(TargetAlphaProvider callback) {
         mTaskAlphaCallback = callback;
     }
 
+    public void setBaseAlphaCallback(TargetAlphaProvider callback) {
+        mBaseAlphaCallback = callback;
+    }
+
     public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
         fromTaskThumbnailView(ttv, rv, null);
     }
@@ -357,6 +367,10 @@
         return mCurrentCornerRadius;
     }
 
+    public interface TargetAlphaProvider {
+        float getAlpha(RemoteAnimationTargetCompat target, float expectedAlpha);
+    }
+
     public static class TransformParams {
         float progress;
         public float offsetX;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java
new file mode 100644
index 0000000..abfe3ad
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.quickstep.util;
+
+import android.os.Binder;
+import android.os.IBinder;
+
+/**
+ * Utility class to pass non-parcealable objects within same process using parcealable payload.
+ *
+ * It wraps the object in a binder as binders are singleton within a process
+ */
+public class ObjectWrapper<T> extends Binder {
+
+    private final T mObject;
+
+    public ObjectWrapper(T object) {
+        mObject = object;
+    }
+
+    public T get() {
+        return mObject;
+    }
+
+    public static IBinder wrap(Object obj) {
+        return new ObjectWrapper<>(obj);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
index 77dc6f3..c6eafe6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -225,7 +225,18 @@
         }
     }
 
+    public void cancel() {
+        if (mAnimsStarted) {
+            for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
+                onUpdateListener.onCancel();
+            }
+        }
+        end();
+    }
+
     public interface OnUpdateListener {
         void onUpdate(RectF currentRect, float progress);
+
+        default void onCancel() { }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 07e9686..1069bed 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -15,7 +15,14 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
 import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.view.View;
 import android.view.ViewGroup;
@@ -29,18 +36,15 @@
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.Workspace;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.anim.SpringObjectAnimator;
+import com.android.launcher3.graphics.OverviewScrim;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-
 /**
  * Creates an animation where all the workspace items are moved into their final location,
  * staggered row by row from the bottom up.
@@ -79,9 +83,19 @@
                 .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y);
 
         DeviceProfile grid = launcher.getDeviceProfile();
-        ShortcutAndWidgetContainer currentPage = ((CellLayout) launcher.getWorkspace()
-                .getChildAt(launcher.getWorkspace().getCurrentPage()))
-                .getShortcutsAndWidgets();
+        Workspace workspace = launcher.getWorkspace();
+        CellLayout cellLayout = (CellLayout) workspace.getChildAt(workspace.getCurrentPage());
+        ShortcutAndWidgetContainer currentPage = cellLayout.getShortcutsAndWidgets();
+
+        boolean workspaceClipChildren = workspace.getClipChildren();
+        boolean workspaceClipToPadding = workspace.getClipToPadding();
+        boolean cellLayoutClipChildren = cellLayout.getClipChildren();
+        boolean cellLayoutClipToPadding = cellLayout.getClipToPadding();
+
+        workspace.setClipChildren(false);
+        workspace.setClipToPadding(false);
+        cellLayout.setClipChildren(false);
+        cellLayout.setClipToPadding(false);
 
         // Hotseat and QSB takes up two additional rows.
         int totalRows = grid.inv.numRows + (grid.isVerticalBarLayout() ? 0 : 2);
@@ -109,8 +123,29 @@
             addStaggeredAnimationForView(qsb, grid.inv.numRows + 2, totalRows);
         }
 
-        addWorkspaceScrimAnimationForState(launcher, BACKGROUND_APP, 0);
-        addWorkspaceScrimAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+        addScrimAnimationForState(launcher, BACKGROUND_APP, 0);
+        addScrimAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
+
+        AnimatorListener resetClipListener = new AnimatorListenerAdapter() {
+            int numAnimations = mAnimators.size();
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                numAnimations--;
+                if (numAnimations > 0) {
+                    return;
+                }
+
+                workspace.setClipChildren(workspaceClipChildren);
+                workspace.setClipToPadding(workspaceClipToPadding);
+                cellLayout.setClipChildren(cellLayoutClipChildren);
+                cellLayout.setClipToPadding(cellLayoutClipToPadding);
+            }
+        };
+
+        for (Animator a : mAnimators) {
+            a.addListener(resetClipListener);
+        }
     }
 
     /**
@@ -134,10 +169,6 @@
      * @param totalRows Total number of rows.
      */
     private void addStaggeredAnimationForView(View v, int row, int totalRows) {
-        if (v == mViewToIgnore) {
-            return;
-        }
-
         // Invert the rows, because we stagger starting from the bottom of the screen.
         int invertedRow = totalRows - row;
         // Add 1 to the inverted row so that the bottom most row has a start delay.
@@ -149,6 +180,10 @@
         springTransY.setStartDelay(startDelay);
         mAnimators.add(springTransY);
 
+        if (v == mViewToIgnore) {
+            return;
+        }
+
         v.setAlpha(0);
         ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
         alpha.setInterpolator(LINEAR);
@@ -157,13 +192,17 @@
         mAnimators.add(alpha);
     }
 
-    private void addWorkspaceScrimAnimationForState(Launcher launcher, LauncherState state,
-            long duration) {
+    private void addScrimAnimationForState(Launcher launcher, LauncherState state, long duration) {
         AnimatorSetBuilder scrimAnimBuilder = new AnimatorSetBuilder();
         AnimationConfig scrimAnimConfig = new AnimationConfig();
         scrimAnimConfig.duration = duration;
         PropertySetter scrimPropertySetter = scrimAnimConfig.getPropertySetter(scrimAnimBuilder);
         launcher.getWorkspace().getStateTransitionAnimation().setScrim(scrimPropertySetter, state);
         mAnimators.add(scrimAnimBuilder.build());
+        Animator fadeOverviewScrim = ObjectAnimator.ofFloat(
+                launcher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
+                state.getOverviewScrimAlpha(launcher));
+        fadeOverviewScrim.setDuration(duration);
+        mAnimators.add(fadeOverviewScrim);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index a98df0f..9b157d1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -37,6 +37,7 @@
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -72,6 +73,7 @@
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ListView;
@@ -108,7 +110,6 @@
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.LauncherEventUtil;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
@@ -226,7 +227,7 @@
                 return;
             }
 
-            BackgroundExecutor.get().submit(() -> {
+            BACKGROUND_EXECUTOR.execute(() -> {
                 TaskView taskView = getTaskView(taskId);
                 if (taskView == null) {
                     return;
@@ -269,7 +270,7 @@
     private int mTaskListChangeId = -1;
 
     // Only valid until the launcher state changes to NORMAL
-    private int mRunningTaskId = -1;
+    protected int mRunningTaskId = -1;
     private boolean mRunningTaskTileHidden;
     private Task mTmpRunningTask;
 
@@ -289,7 +290,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private float mContentAlpha = 1;
     @ViewDebug.ExportedProperty(category = "launcher")
-    private float mFullscreenProgress = 0;
+    protected float mFullscreenProgress = 0;
 
     // Keeps track of task id whose visual state should not be reset
     private int mIgnoreResetTaskId = -1;
@@ -527,7 +528,7 @@
         return true;
     }
 
-    private void applyLoadPlan(ArrayList<Task> tasks) {
+    protected void applyLoadPlan(ArrayList<Task> tasks) {
         if (mPendingAnimation != null) {
             mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(tasks));
             return;
@@ -599,6 +600,7 @@
             TaskView taskView = (TaskView) getChildAt(i);
             if (mIgnoreResetTaskId != taskView.getTask().key.id) {
                 taskView.resetVisualProperties();
+                taskView.setStableAlpha(mContentAlpha);
             }
         }
         if (mRunningTaskTileHidden) {
@@ -783,6 +785,7 @@
         unloadVisibleTaskData();
         setCurrentPage(0);
         mDwbToastShown = false;
+        mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
     }
 
     public @Nullable TaskView getRunningTaskView() {
@@ -848,12 +851,14 @@
      * is called.  Also scrolls the view to this task.
      */
     public void showCurrentTask(int runningTaskId) {
-        if (getChildCount() == 0) {
+        if (getTaskView(runningTaskId) == null) {
+            boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
-            addView(taskView);
-            addView(mClearAllButton);
-
+            addView(taskView, 0);
+            if (wasEmpty) {
+                addView(mClearAllButton);
+            }
             // The temporary running task is only used for the duration between the start of the
             // gesture and the task list is loaded and applied
             mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(),
@@ -1687,6 +1692,9 @@
      * @return How many pixels the running task is offset on the x-axis due to the current scrollX.
      */
     public float getScrollOffset() {
+        if (getRunningTaskIndex() == -1) {
+            return 0;
+        }
         int startScroll = getScrollForPage(getRunningTaskIndex());
         int offsetX = startScroll - getScrollX();
         offsetX *= getScaleX();
@@ -1733,4 +1741,14 @@
             updateEnabledOverlays();
         }
     }
+
+    public int getLeftGestureMargin() {
+        final WindowInsets insets = getRootWindowInsets();
+        return Math.max(insets.getSystemGestureInsets().left, insets.getSystemWindowInsetLeft());
+    }
+
+    public int getRightGestureMargin() {
+        final WindowInsets insets = getRootWindowInsets();
+        return Math.max(insets.getSystemGestureInsets().right, insets.getSystemWindowInsetRight());
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index d55a520..7f1e898 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -410,4 +410,11 @@
 
         return new ColorMatrixColorFilter(COLOR_MATRIX);
     }
+
+    public Bitmap getThumbnail() {
+        if (mThumbnailData == null) {
+            return null;
+        }
+        return mThumbnailData.thumbnail;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index b26fdce..2211eb4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -192,9 +192,6 @@
         super(context, attrs, defStyleAttr);
         mActivity = BaseDraggingActivity.fromContext(context);
         setOnClickListener((view) -> {
-            if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-                android.util.Log.d(TestProtocol.NO_START_TASK_TAG, "TaskView onClick");
-            }
             if (getTask() == null) {
                 return;
             }
@@ -291,9 +288,6 @@
 
     public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback,
             Handler resultCallbackHandler) {
-        if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_START_TASK_TAG, "launchTask");
-        }
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (isRunningTask()) {
                 getRecentsView().finishRecentsAnimation(false /* toRecents */,
@@ -308,9 +302,6 @@
 
     private void launchTaskInternal(boolean animate, boolean freezeTaskList,
             Consumer<Boolean> resultCallback, Handler resultCallbackHandler) {
-        if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_START_TASK_TAG, "launchTaskInternal");
-        }
         if (mTask != null) {
             final ActivityOptions opts;
             if (animate) {
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 78f6ffa..a8e2956 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -15,8 +15,8 @@
  */
 package com.android.launcher3;
 
-import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 import static com.android.systemui.shared.recents.utilities.Utilities
         .postAtFrontOfQueueAsynchronously;
 
@@ -24,6 +24,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.annotation.TargetApi;
+import android.content.Context;
 import android.os.Build;
 import android.os.Handler;
 
@@ -66,7 +67,7 @@
 
     /**
      * Called on the UI thread when the animation targets are received. The implementation must
-     * call {@link AnimationResult#setAnimation(AnimatorSet)} with the target animation to be run.
+     * call {@link AnimationResult#setAnimation} with the target animation to be run.
      */
     @UiThread
     public abstract void onCreateAnimation(
@@ -110,7 +111,7 @@
         }
 
         @UiThread
-        public void setAnimation(AnimatorSet animation) {
+        public void setAnimation(AnimatorSet animation, Context context) {
             if (mInitialized) {
                 throw new IllegalStateException("Animation already initialized");
             }
@@ -134,7 +135,7 @@
 
                 // Because t=0 has the app icon in its original spot, we can skip the
                 // first frame and have the same movement one frame earlier.
-                mAnimator.setCurrentPlayTime(SINGLE_FRAME_MS);
+                mAnimator.setCurrentPlayTime(getSingleFrameMs(context));
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index c5c5323..b9ce1ce 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -85,7 +85,7 @@
 
         register();
 
-        Bundle options = animProvider.toActivityOptions(handler, duration).toBundle();
+        Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
         context.startActivity(addToIntent(new Intent((intent))), options);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index b60a017..991408c 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -219,7 +219,7 @@
                         anim.addListener(mForceInvisibleListener);
                     }
 
-                    result.setAnimation(anim);
+                    result.setAnimation(anim, mLauncher);
                 }
             };
 
@@ -822,7 +822,7 @@
             }
 
             mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
-            result.setAnimation(anim);
+            result.setAnimation(anim, mLauncher);
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index f0204b9..174e49b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -19,16 +19,20 @@
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCRIM_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.anim.AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
 
 import android.util.FloatProperty;
 import android.view.View;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherState.ScaleAndTranslation;
@@ -36,8 +40,7 @@
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
-
-import androidx.annotation.NonNull;
+import com.android.launcher3.graphics.OverviewScrim;
 
 /**
  * State handler for recents view. Manages UI changes and animations for recents view based off the
@@ -67,6 +70,8 @@
         mRecentsView.setTranslationX(translationX);
         mRecentsView.setTranslationY(scaleAndTranslation.translationY);
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
+        OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
+        SCRIM_PROGRESS.set(scrim, state.getOverviewScrimAlpha(mLauncher));
     }
 
     @Override
@@ -110,6 +115,9 @@
                 translateYInterpolator);
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
                 builder.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
+        OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
+        setter.setFloat(scrim, SCRIM_PROGRESS, toState.getOverviewScrimAlpha(mLauncher),
+                builder.getInterpolator(ANIM_OVERVIEW_SCRIM_FADE, LINEAR));
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 6030cea..a55f36b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -51,7 +51,6 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.systemui.shared.system.QuickStepContract;
 
 /**
  * Touch controller for handling various state transitions in portrait UI.
@@ -63,7 +62,7 @@
     /**
      * The progress at which all apps content will be fully visible when swiping up from overview.
      */
-    private static final float ALL_APPS_CONTENT_FADE_THRESHOLD = 0.08f;
+    protected static final float ALL_APPS_CONTENT_FADE_THRESHOLD = 0.08f;
 
     /**
      * The progress at which recents will begin fading out when swiping up from overview.
@@ -296,9 +295,13 @@
      * @return true if the event is over the hotseat
      */
     static boolean isTouchOverHotseat(Launcher launcher, MotionEvent ev) {
+        return (ev.getY() >= getHotseatTop(launcher));
+    }
+
+    public static int getHotseatTop(Launcher launcher) {
         DeviceProfile dp = launcher.getDeviceProfile();
         int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
-        return (ev.getY() >= (launcher.getDragLayer().getHeight() - hotseatHeight));
+        return launcher.getDragLayer().getHeight() - hotseatHeight;
     }
 
     private static class InterpolatorWrapper implements Interpolator {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index fee1820..11a8043 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -17,17 +17,25 @@
 
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+import static android.view.MotionEvent.ACTION_CANCEL;
 
+import android.graphics.PointF;
 import android.os.RemoteException;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
+import android.view.Window;
+import android.view.WindowManager;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.touch.TouchEventTranslator;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.TouchController;
 import com.android.quickstep.RecentsModel;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -36,18 +44,29 @@
 
 /**
  * TouchController for handling touch events that get sent to the StatusBar. Once the
- * Once the event delta y passes the touch slop, the events start getting forwarded.
+ * Once the event delta mDownY passes the touch slop, the events start getting forwarded.
  * All events are offset by initial Y value of the pointer.
  */
 public class StatusBarTouchController implements TouchController {
 
     private static final String TAG = "StatusBarController";
 
+    /**
+     * Window flag: Enable touches to slide out of a window into neighboring
+     * windows in mid-gesture instead of being captured for the duration of
+     * the gesture.
+     *
+     * This flag changes the behavior of touch focus for this window only.
+     * Touches can slide out of the window but they cannot necessarily slide
+     * back in (unless the other window with touch focus permits it).
+     */
+    private static final int FLAG_SLIPPERY = 0x20000000;
+
     protected final Launcher mLauncher;
-    protected final TouchEventTranslator mTranslator;
     private final float mTouchSlop;
     private ISystemUiProxy mSysUiProxy;
     private int mLastAction;
+    private final SparseArray<PointF> mDownEvents;
 
     /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/
     private boolean mCanIntercept;
@@ -56,7 +75,7 @@
         mLauncher = l;
         // Guard against TAPs by increasing the touch slop.
         mTouchSlop = 2 * ViewConfiguration.get(l).getScaledTouchSlop();
-        mTranslator = new TouchEventTranslator((MotionEvent ev)-> dispatchTouchEvent(ev));
+        mDownEvents = new SparseArray<>();
     }
 
     @Override
@@ -64,7 +83,6 @@
         writer.println(prefix + "mCanIntercept:" + mCanIntercept);
         writer.println(prefix + "mLastAction:" + MotionEvent.actionToString(mLastAction));
         writer.println(prefix + "mSysUiProxy available:" + (mSysUiProxy != null));
-
     }
 
     private void dispatchTouchEvent(MotionEvent ev) {
@@ -81,26 +99,31 @@
     @Override
     public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         int action = ev.getActionMasked();
+        int idx = ev.getActionIndex();
+        int pid = ev.getPointerId(idx);
         if (action == ACTION_DOWN) {
             mCanIntercept = canInterceptTouch(ev);
             if (!mCanIntercept) {
                 return false;
             }
-            mTranslator.reset();
-            mTranslator.setDownParameters(0, ev);
+            mDownEvents.put(pid, new PointF(ev.getX(), ev.getY()));
         } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
-            // Check!! should only set it only when threshold is not entered.
-            mTranslator.setDownParameters(ev.getActionIndex(), ev);
+           // Check!! should only set it only when threshold is not entered.
+           mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx)));
         }
         if (!mCanIntercept) {
             return false;
         }
         if (action == ACTION_MOVE) {
-            float dy = ev.getY() - mTranslator.getDownY();
-            float dx = ev.getX() - mTranslator.getDownX();
-            if (dy > mTouchSlop && dy > Math.abs(dx)) {
-                mTranslator.dispatchDownEvents(ev);
-                mTranslator.processMotionEvent(ev);
+            float dy = ev.getY(idx) - mDownEvents.get(pid).y;
+            float dx = ev.getX(idx) - mDownEvents.get(pid).x;
+            // Currently input dispatcher will not do touch transfer if there are more than
+            // one touch pointer. Hence, even if slope passed, only set the slippery flag
+            // when there is single touch event. (context: InputDispatcher.cpp line 1445)
+            if (dy > mTouchSlop && dy > Math.abs(dx) && ev.getPointerCount() == 1) {
+                ev.setAction(ACTION_DOWN);
+                dispatchTouchEvent(ev);
+                setWindowSlippery(true);
                 return true;
             }
             if (Math.abs(dx) > mTouchSlop) {
@@ -110,13 +133,31 @@
         return false;
     }
 
-
     @Override
     public final boolean onControllerTouchEvent(MotionEvent ev) {
-        mTranslator.processMotionEvent(ev);
+        int action = ev.getAction();
+        if (action == ACTION_UP || action == ACTION_CANCEL) {
+            dispatchTouchEvent(ev);
+            mLauncher.getUserEventDispatcher().logActionOnContainer(action == ACTION_UP ?
+                    Touch.FLING : Touch.SWIPE, Direction.DOWN, ContainerType.WORKSPACE,
+                    mLauncher.getWorkspace().getCurrentPage());
+            setWindowSlippery(false);
+            return true;
+        }
         return true;
     }
 
+    private void setWindowSlippery(boolean enable) {
+        Window w = mLauncher.getWindow();
+        WindowManager.LayoutParams wlp = w.getAttributes();
+        if (enable) {
+            wlp.flags |= FLAG_SLIPPERY;
+        } else {
+            wlp.flags &= ~FLAG_SLIPPERY;
+        }
+        w.setAttributes(wlp);
+    }
+
     private boolean canInterceptTouch(MotionEvent ev) {
         if (!mLauncher.isInState(LauncherState.NORMAL) ||
                 AbstractFloatingView.getTopOpenViewWithType(mLauncher,
@@ -132,4 +173,4 @@
         mSysUiProxy = RecentsModel.INSTANCE.get(mLauncher).getSystemUiProxy();
         return mSysUiProxy != null;
     }
-}
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index cd2c9cb..5c9c7d4 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -152,5 +152,15 @@
         default void playAtomicAnimation(float velocity) {
             // No-op
         }
+
+        static RectF getDefaultWindowTargetRect(DeviceProfile dp) {
+            final int halfIconSize = dp.iconSizePx / 2;
+            final float targetCenterX = dp.availableWidthPx / 2f;
+            final float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
+            // Fallback to animate to center of screen.
+            return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
+                    targetCenterX + halfIconSize, targetCenterY + halfIconSize);
+        }
+
     }
 }
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 0738aff..88a4eb6 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -29,11 +29,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.util.SparseIntArray;
 
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
 import java.util.ArrayList;
+import java.util.Objects;
 
 /**
  * Class to keep track of the current overview component based off user preferences and app updates
@@ -53,22 +57,41 @@
         }
     };
     private final Context mContext;
-    private final ComponentName mMyHomeComponent;
+    private final Intent mCurrentHomeIntent;
+    private final Intent mMyHomeIntent;
+    private final Intent mFallbackIntent;
+    private final SparseIntArray mConfigChangesMap = new SparseIntArray();
     private String mUpdateRegisteredPackage;
     private ActivityControlHelper mActivityControlHelper;
     private Intent mOverviewIntent;
-    private Intent mHomeIntent;
     private int mSystemUiStateFlags;
     private boolean mIsHomeAndOverviewSame;
+    private boolean mIsDefaultHome;
 
     public OverviewComponentObserver(Context context) {
         mContext = context;
 
-        Intent myHomeIntent = new Intent(Intent.ACTION_MAIN)
+        mCurrentHomeIntent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
-                .setPackage(mContext.getPackageName());
-        ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0);
-        mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name);
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mMyHomeIntent = new Intent(mCurrentHomeIntent).setPackage(mContext.getPackageName());
+        ResolveInfo info = context.getPackageManager().resolveActivity(mMyHomeIntent, 0);
+        ComponentName myHomeComponent =
+                new ComponentName(context.getPackageName(), info.activityInfo.name);
+        mMyHomeIntent.setComponent(myHomeComponent);
+        mConfigChangesMap.append(myHomeComponent.hashCode(), info.activityInfo.configChanges);
+
+        ComponentName fallbackComponent = new ComponentName(mContext, RecentsActivity.class);
+        mFallbackIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_DEFAULT)
+                .setComponent(fallbackComponent)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        try {
+            ActivityInfo fallbackInfo = context.getPackageManager().getActivityInfo(
+                    mFallbackIntent.getComponent(), 0 /* flags */);
+            mConfigChangesMap.append(fallbackComponent.hashCode(), fallbackInfo.configChanges);
+        } catch (PackageManager.NameNotFoundException ignored) { /* Impossible */ }
 
         mContext.registerReceiver(mUserPreferenceChangeReceiver,
                 new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED));
@@ -92,17 +115,22 @@
         ComponentName defaultHome = PackageManagerWrapper.getInstance()
                 .getHomeActivities(new ArrayList<>());
 
-        final String overviewIntentCategory;
-        ComponentName overviewComponent;
-        mHomeIntent = null;
+        mIsDefaultHome = Objects.equals(mMyHomeIntent.getComponent(), defaultHome);
 
-        if ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0 &&
-                (defaultHome == null || mMyHomeComponent.equals(defaultHome))) {
+        // Set assistant visibility to 0 from launcher's perspective, ensures any elements that
+        // launcher made invisible become visible again before the new activity control helper
+        // becomes active.
+        if (mActivityControlHelper != null) {
+            mActivityControlHelper.onAssistantVisibilityChanged(0.f);
+        }
+
+        if ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
+                && (defaultHome == null || mIsDefaultHome)) {
             // User default home is same as out home app. Use Overview integrated in Launcher.
-            overviewComponent = mMyHomeComponent;
             mActivityControlHelper = new LauncherActivityControllerHelper();
             mIsHomeAndOverviewSame = true;
-            overviewIntentCategory = Intent.CATEGORY_HOME;
+            mOverviewIntent = mMyHomeIntent;
+            mCurrentHomeIntent.setComponent(mMyHomeIntent.getComponent());
 
             if (mUpdateRegisteredPackage != null) {
                 // Remove any update listener as we don't care about other packages.
@@ -111,14 +139,12 @@
             }
         } else {
             // The default home app is a different launcher. Use the fallback Overview instead.
-            overviewComponent = new ComponentName(mContext, RecentsActivity.class);
+
             mActivityControlHelper = new FallbackActivityControllerHelper();
             mIsHomeAndOverviewSame = false;
-            overviewIntentCategory = Intent.CATEGORY_DEFAULT;
+            mOverviewIntent = mFallbackIntent;
+            mCurrentHomeIntent.setComponent(defaultHome);
 
-            mHomeIntent = new Intent(Intent.ACTION_MAIN)
-                    .addCategory(Intent.CATEGORY_HOME)
-                    .setComponent(defaultHome);
             // User's default home app can change as a result of package updates of this app (such
             // as uninstalling the app or removing the "Launcher" feature in an update).
             // Listen for package updates of this app (and remove any previously attached
@@ -138,14 +164,6 @@
                         ACTION_PACKAGE_REMOVED));
             }
         }
-
-        mOverviewIntent = new Intent(Intent.ACTION_MAIN)
-                .addCategory(overviewIntentCategory)
-                .setComponent(overviewComponent)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        if (mHomeIntent == null) {
-            mHomeIntent = mOverviewIntent;
-        }
     }
 
     /**
@@ -161,6 +179,32 @@
     }
 
     /**
+     * @return {@code true} if the overview component is able to handle the configuration changes.
+     */
+    boolean canHandleConfigChanges(ComponentName component, int changes) {
+        final int orientationChange =
+                ActivityInfo.CONFIG_ORIENTATION | ActivityInfo.CONFIG_SCREEN_SIZE;
+        if ((changes & orientationChange) == orientationChange) {
+            // This is just an approximate guess for simple orientation change because the changes
+            // may contain non-public bits (e.g. window configuration).
+            return true;
+        }
+
+        int configMask = mConfigChangesMap.get(component.hashCode());
+        return configMask != 0 && (~configMask & changes) == 0;
+    }
+
+    /**
+     * Get the intent for overview activity. It is used when lockscreen is shown and home was died
+     * in background, we still want to restart the one that will be used after unlock.
+     *
+     * @return the overview intent
+     */
+    Intent getOverviewIntentIgnoreSysUiState() {
+        return mIsDefaultHome ? mMyHomeIntent : mOverviewIntent;
+    }
+
+    /**
      * Get the current intent for going to the overview activity.
      *
      * @return the overview intent
@@ -173,7 +217,7 @@
      * Get the current intent for going to the home activity.
      */
     public Intent getHomeIntent() {
-        return mHomeIntent;
+        return mCurrentHomeIntent;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 7bfa9a0..befeee0 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep;
 
-import android.app.ActivityManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.UserManager;
@@ -23,29 +22,17 @@
 
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.MainProcessInitializer;
-import com.android.launcher3.Utilities;
 import com.android.systemui.shared.system.ThreadedRendererCompat;
 
 @SuppressWarnings("unused")
 public class QuickstepProcessInitializer extends MainProcessInitializer {
 
     private static final String TAG = "QuickstepProcessInitializer";
-    private static final int HEAP_LIMIT_MB = 250;
 
     public QuickstepProcessInitializer(Context context) { }
 
     @Override
     protected void init(Context context) {
-        if (Utilities.IS_DEBUG_DEVICE) {
-            try {
-                // Trigger a heap dump if the PSS reaches beyond the target heap limit
-                final ActivityManager am = context.getSystemService(ActivityManager.class);
-                am.setWatchHeapLimit(HEAP_LIMIT_MB * 1024 * 1024);
-            } catch (SecurityException e) {
-                // Do nothing
-            }
-        }
-
         // Workaround for b/120550382, an external app can cause the launcher process to start for
         // a work profile user which we do not support. Disable the application immediately when we
         // detect this to be the case.
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
deleted file mode 100644
index 8951363..0000000
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.android.quickstep;
-
-import android.content.Context;
-import android.os.Bundle;
-
-import com.android.launcher3.testing.TestInformationHandler;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.uioverrides.states.OverviewState;
-import com.android.quickstep.util.LayoutUtils;
-
-public class QuickstepTestInformationHandler extends TestInformationHandler {
-
-    public QuickstepTestInformationHandler(Context context) { }
-
-    @Override
-    public Bundle call(String method) {
-        final Bundle response = new Bundle();
-        switch (method) {
-            case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
-                final float swipeHeight =
-                        OverviewState.getDefaultSwipeHeight(mDeviceProfile);
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
-                return response;
-            }
-
-            case TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT: {
-                final float swipeHeight =
-                        LayoutUtils.getShelfTrackingDistance(mContext, mDeviceProfile);
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
-                return response;
-            }
-        }
-
-        return super.call(method);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index f27ba85..e41dba9 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep;
 
+import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
+
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.content.Context;
@@ -25,10 +27,7 @@
 import com.android.launcher3.MainThreadExecutor;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.KeyguardManagerCompat;
-import com.android.systemui.shared.system.RecentTaskInfoCompat;
-import com.android.systemui.shared.system.TaskDescriptionCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -43,7 +42,6 @@
 
     private final KeyguardManagerCompat mKeyguardManager;
     private final MainThreadExecutor mMainThreadExecutor;
-    private final BackgroundExecutor mBgThreadExecutor;
 
     // The list change id, increments as the task list changes in the system
     private int mChangeId;
@@ -56,7 +54,6 @@
 
     public RecentTasksList(Context context) {
         mMainThreadExecutor = new MainThreadExecutor();
-        mBgThreadExecutor = BackgroundExecutor.get();
         mKeyguardManager = new KeyguardManagerCompat(context);
         mChangeId = 1;
         ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
@@ -67,7 +64,7 @@
      */
     public void getTaskKeys(int numTasks, Consumer<ArrayList<Task>> callback) {
         // Kick off task loading in the background
-        mBgThreadExecutor.submit(() -> {
+        BACKGROUND_EXECUTOR.execute(() -> {
             ArrayList<Task> tasks = loadTasksInBackground(numTasks, true /* loadKeysOnly */);
             mMainThreadExecutor.execute(() -> callback.accept(tasks));
         });
@@ -87,13 +84,14 @@
                 : () -> callback.accept(copyOf(mTasks));
 
         if (mLastLoadedId == mChangeId && (!mLastLoadHadKeysOnly || loadKeysOnly)) {
-            // The list is up to date, callback with the same list
-            mMainThreadExecutor.execute(resultCallback);
+            // The list is up to date, send the callback on the next frame,
+            // so that requestID can be returned first.
+            mMainThreadExecutor.getHandler().post(resultCallback);
             return requestLoadId;
         }
 
         // Kick off task loading in the background
-        mBgThreadExecutor.submit(() -> {
+        BACKGROUND_EXECUTOR.execute(() -> {
             ArrayList<Task> tasks = loadTasksInBackground(Integer.MAX_VALUE, loadKeysOnly);
 
             mMainThreadExecutor.execute(() -> {
@@ -121,12 +119,7 @@
 
     @Override
     public void onTaskRemoved(int taskId) {
-        for (int i = mTasks.size() - 1; i >= 0; i--) {
-            if (mTasks.get(i).key.id == taskId) {
-                mTasks.remove(i);
-                return;
-            }
-        }
+        mTasks = loadTasksInBackground(Integer.MAX_VALUE, false);
     }
 
     @Override
@@ -166,15 +159,11 @@
         int taskCount = rawTasks.size();
         for (int i = 0; i < taskCount; i++) {
             ActivityManager.RecentTaskInfo rawTask = rawTasks.get(i);
-            RecentTaskInfoCompat t = new RecentTaskInfoCompat(rawTask);
             Task.TaskKey taskKey = new Task.TaskKey(rawTask);
             Task task;
             if (!loadKeysOnly) {
-                ActivityManager.TaskDescription rawTd = t.getTaskDescription();
-                TaskDescriptionCompat td = new TaskDescriptionCompat(rawTd);
-                boolean isLocked = tmpLockedUsers.get(t.getUserId());
-                task = new Task(taskKey, td.getPrimaryColor(), td.getBackgroundColor(),
-                        t.supportsSplitScreenMultiWindow(), isLocked, rawTd, t.getTopActivity());
+                boolean isLocked = tmpLockedUsers.get(taskKey.userId);
+                task = Task.from(taskKey, rawTask, isLocked);
             } else {
                 task = new Task(taskKey);
             }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
index 0822e61..f9d2f11 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
@@ -68,7 +68,7 @@
             Context context, Handler handler, long duration) {
         register();
 
-        Bundle options = animProvider.toActivityOptions(handler, duration).toBundle();
+        Bundle options = animProvider.toActivityOptions(handler, duration, context).toBundle();
         context.startActivity(intent, options);
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index a7e6d74..4503a43 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -17,6 +17,7 @@
 
 import android.animation.AnimatorSet;
 import android.app.ActivityOptions;
+import android.content.Context;
 import android.os.Handler;
 
 import com.android.launcher3.LauncherAnimationRunner;
@@ -32,14 +33,14 @@
 
     AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets);
 
-    default ActivityOptions toActivityOptions(Handler handler, long duration) {
+    default ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
         LauncherAnimationRunner runner = new LauncherAnimationRunner(handler,
                 false /* startAtFrontOfQueue */) {
 
             @Override
             public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
                     AnimationResult result) {
-                result.setAnimation(createWindowAnimation(targetCompats));
+                result.setAnimation(createWindowAnimation(targetCompats), context);
             }
         };
         return ActivityOptionsCompat.makeRemoteAnimation(
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index 63c8023..3747f9a 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -74,6 +74,9 @@
     private int mMidAlpha;
     private float mMidProgress;
 
+    // The progress at which the drag handle starts moving up with the shelf.
+    private float mDragHandleProgress;
+
     private Interpolator mBeforeMidProgressColorInterpolator = ACCEL;
     private Interpolator mAfterMidProgressColorInterpolator = ACCEL;
 
@@ -95,7 +98,7 @@
 
     public ShelfScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mMaxScrimAlpha = Math.round(OVERVIEW.getWorkspaceScrimAlpha(mLauncher) * 255);
+        mMaxScrimAlpha = Math.round(OVERVIEW.getOverviewScrimAlpha(mLauncher) * 255);
 
         mEndAlpha = Color.alpha(mEndScrim);
         mRadius = BOTTOM_CORNER_RADIUS_RATIO * Themes.getDialogCornerRadius(context);
@@ -150,15 +153,16 @@
 
             if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) {
                 mMidProgress = 1;
+                mDragHandleProgress = 1;
                 mMidAlpha = 0;
             } else {
                 mMidAlpha = Themes.getAttrInteger(getContext(), R.attr.allAppsInterimScrimAlpha);
+                mMidProgress =  OVERVIEW.getVerticalProgress(mLauncher);
                 Rect hotseatPadding = dp.getHotseatLayoutPadding();
                 int hotseatSize = dp.hotseatBarSizePx + dp.getInsets().bottom
                         - hotseatPadding.bottom - hotseatPadding.top;
-                float arrowTop = Math.min(hotseatSize, OverviewState.getDefaultSwipeHeight(dp));
-                mMidProgress =  1 - (arrowTop / mShiftRange);
-
+                float dragHandleTop = Math.min(hotseatSize, OverviewState.getDefaultSwipeHeight(dp));
+                mDragHandleProgress =  1 - (dragHandleTop / mShiftRange);
             }
             mTopOffset = dp.getInsets().top - mShelfOffset;
             mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset;
@@ -199,8 +203,6 @@
                     mProgress, mMidProgress, 1, mMidAlpha, 0, mBeforeMidProgressColorInterpolator));
             mShelfColor = setColorAlphaBound(mEndScrim, alpha);
         } else {
-            mDragHandleOffset += mShiftRange * (mMidProgress - mProgress);
-
             // Note that these ranges and interpolators are inverted because progress goes 1 to 0.
             int alpha = Math.round(
                     Utilities.mapToRange(mProgress, (float) 0, mMidProgress, (float) mEndAlpha,
@@ -212,6 +214,10 @@
                             (float) 0, LINEAR));
             mRemainingScreenColor = setColorAlphaBound(mScrimColor, remainingScrimAlpha);
         }
+
+        if (mProgress < mDragHandleProgress) {
+            mDragHandleOffset += mShiftRange * (mDragHandleProgress - mProgress);
+        }
     }
 
     @Override
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
index d9fcf4d..d0956d1 100644
--- a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
+++ b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
@@ -88,7 +88,7 @@
      */
     @Test
     public void testPredictionExistsInAllApps() {
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
         mLauncher.pressHome().switchToAllApps();
 
         // Dispatch an update
@@ -103,7 +103,7 @@
      */
     @Test
     public void testPredictionsDeferredUntilHome() {
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
         sendPredictionUpdate(mSampleApp1, mSampleApp2);
         mLauncher.pressHome().switchToAllApps();
         waitForLauncherCondition("Predictions were not updated in loading state",
@@ -120,7 +120,7 @@
 
     @Test
     public void testPredictionsDisabled() {
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
         sendPredictionUpdate();
         mLauncher.pressHome().switchToAllApps();
 
diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index 0c5a6f5..ec3d49a 100644
--- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -51,7 +51,7 @@
             mLauncher.pressHome();
             final DigitalWellBeingToast toast = getToast();
 
-            assertTrue("Toast is not visible", toast.hasLimit());
+            waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit());
             assertEquals("Toast text: ", "5 minutes left today", toast.getText());
 
             // Unset time limit for app.
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 3b35c86..f27f400 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -27,6 +27,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.NAV_BAR_MODE_GESTURAL_OVERLAY;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.util.Log;
 
 import androidx.test.uiautomator.UiDevice;
@@ -80,6 +81,7 @@
             return new Statement() {
                 @Override
                 public void evaluate() throws Throwable {
+                    mLauncher.enableDebugTracing();
                     final Context context = getInstrumentation().getContext();
                     final int currentInteractionMode =
                             LauncherInstrumentation.getCurrentInteractionMode(context);
@@ -101,35 +103,55 @@
                         if (mode == THREE_BUTTON || mode == ALL) {
                             evaluateWithThreeButtons();
                         }
+                    } catch (Exception e) {
+                        Log.e(TAG, "Exception", e);
+                        throw e;
                     } finally {
-                        setActiveOverlay(prevOverlayPkg, originalMode);
+                        Assert.assertTrue(setActiveOverlay(prevOverlayPkg, originalMode));
                     }
-                }
-
-                public void evaluateWithoutChangingSetting(Statement base) throws Throwable {
-                    base.evaluate();
+                    mLauncher.disableDebugTracing();
                 }
 
                 private void evaluateWithThreeButtons() throws Throwable {
-                    setActiveOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY,
-                            LauncherInstrumentation.NavigationModel.THREE_BUTTON);
-                    evaluateWithoutChangingSetting(base);
+                    if (setActiveOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY,
+                            LauncherInstrumentation.NavigationModel.THREE_BUTTON)) {
+                        base.evaluate();
+                    }
                 }
 
                 private void evaluateWithTwoButtons() throws Throwable {
-                    setActiveOverlay(NAV_BAR_MODE_2BUTTON_OVERLAY,
-                            LauncherInstrumentation.NavigationModel.TWO_BUTTON);
-                    base.evaluate();
+                    if (setActiveOverlay(NAV_BAR_MODE_2BUTTON_OVERLAY,
+                            LauncherInstrumentation.NavigationModel.TWO_BUTTON)) {
+                        base.evaluate();
+                    }
                 }
 
                 private void evaluateWithZeroButtons() throws Throwable {
-                    setActiveOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY,
-                            LauncherInstrumentation.NavigationModel.ZERO_BUTTON);
-                    base.evaluate();
+                    if (setActiveOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY,
+                            LauncherInstrumentation.NavigationModel.ZERO_BUTTON)) {
+                        base.evaluate();
+                    }
                 }
 
-                private void setActiveOverlay(String overlayPackage,
+                private boolean packageExists(String packageName) {
+                    try {
+                        PackageManager pm = getInstrumentation().getContext().getPackageManager();
+                        if (pm.getApplicationInfo(packageName, 0 /* flags */) == null) {
+                            return false;
+                        }
+                    } catch (PackageManager.NameNotFoundException e) {
+                        return false;
+                    }
+                    return true;
+                }
+
+                private boolean setActiveOverlay(String overlayPackage,
                         LauncherInstrumentation.NavigationModel expectedMode) throws Exception {
+                    if (!packageExists(overlayPackage)) {
+                        Log.d(TAG, "setActiveOverlay: " + overlayPackage + " pkg does not exist");
+                        return false;
+                    }
+
                     setOverlayPackageEnabled(NAV_BAR_MODE_3BUTTON_OVERLAY,
                             overlayPackage == NAV_BAR_MODE_3BUTTON_OVERLAY);
                     setOverlayPackageEnabled(NAV_BAR_MODE_2BUTTON_OVERLAY,
@@ -173,6 +195,7 @@
                     Assert.assertTrue("Switching nav mode: " + error, error == null);
 
                     Thread.sleep(5000);
+                    return true;
                 }
 
                 private void setOverlayPackageEnabled(String overlayPackage, boolean enable)
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 2111e2c..c5b560c 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -25,6 +25,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.util.RaceConditionReproducer;
 import com.android.quickstep.NavigationModeSwitchRule.Mode;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -79,6 +80,8 @@
     @Test
     @NavigationModeSwitch
     public void testStressPressHome() {
+        if (LauncherInstrumentation.isAvd()) return; // b/136278866
+
         for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
             // Destroy Launcher activity.
             closeLauncherActivity();
@@ -91,6 +94,8 @@
     @Test
     @NavigationModeSwitch
     public void testStressSwipeToOverview() {
+        if (LauncherInstrumentation.isAvd()) return; // b/136278866
+
         for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
             // Destroy Launcher activity.
             closeLauncherActivity();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 9e3bf2f..885fdbf 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -17,7 +17,6 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -208,7 +207,7 @@
 
     @Test
     @NavigationModeSwitch
-//    @PortraitLandscape
+    @PortraitLandscape
     public void testBackground() throws Exception {
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         final Background background = mLauncher.getBackground();
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 881f65d..339aef5 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -104,6 +104,7 @@
     </style>
 
     <style name="LauncherTheme.Dark.DarkText" parent="@style/LauncherTheme.Dark">
+        <item name="android:colorControlHighlight">#75212121</item>
         <item name="allAppsInterimScrimAlpha">25</item>
         <item name="folderFillColor">#CDFFFFFF</item>
         <item name="folderTextColor">?attr/workspaceTextColor</item>
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index d949141..e3ef5d6 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -11,6 +11,7 @@
 import android.util.Log;
 
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.provider.RestoreDbTask;
@@ -18,6 +19,8 @@
 
 import androidx.annotation.WorkerThread;
 
+import static android.os.Process.myUserHandle;
+
 public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
 
     private static final String TAG = "AWRestoredReceiver";
@@ -77,9 +80,14 @@
                 state = LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
             }
 
-            String[] widgetIdParams = new String[] { Integer.toString(oldWidgetIds[i]) };
+            // b/135926478: Work profile widget restore is broken in platform. This forces us to
+            // recreate the widget during loading with the correct host provider.
+            long mainProfileId = UserManagerCompat.getInstance(context)
+                    .getSerialNumberForUser(myUserHandle());
+            String oldWidgetId = Integer.toString(oldWidgetIds[i]);
             int result = new ContentWriter(context, new ContentWriter.CommitParams(
-                    "appWidgetId=? and (restored & 1) = 1", widgetIdParams))
+                    "appWidgetId=? and (restored & 1) = 1 and profileId=?",
+                    new String[] { oldWidgetId, Long.toString(mainProfileId) }))
                     .put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i])
                     .put(LauncherSettings.Favorites.RESTORED, state)
                     .commit();
@@ -87,7 +95,7 @@
             if (result == 0) {
                 Cursor cursor = cr.query(Favorites.CONTENT_URI,
                         new String[] {Favorites.APPWIDGET_ID},
-                        "appWidgetId=?", widgetIdParams, null);
+                        "appWidgetId=?", new String[] { oldWidgetId }, null);
                 try {
                     if (!cursor.moveToFirst()) {
                         // The widget no long exists.
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index f69b172..f0b3afd 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -135,10 +135,6 @@
 
     public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item,
             @Nullable String sourceContainer) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_START_TAG,
-                    "startActivitySafely 1");
-        }
         if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) {
             Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
             return false;
@@ -162,10 +158,6 @@
                 startShortcutIntentSafely(intent, optsBundle, item, sourceContainer);
             } else if (user == null || user.equals(Process.myUserHandle())) {
                 // Could be launching some bookkeeping activity
-                if (TestProtocol.sDebugTracing) {
-                    android.util.Log.d(TestProtocol.NO_START_TAG,
-                            "startActivitySafely 2");
-                }
                 startActivity(intent, optsBundle);
                 AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(),
                         Process.myUserHandle(), sourceContainer);
@@ -178,7 +170,7 @@
             getUserEventDispatcher().logAppLaunch(v, intent);
             getStatsLogManager().logAppLaunch(v, intent);
             return true;
-        } catch (ActivityNotFoundException|SecurityException e) {
+        } catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
             Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e);
         }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 1619e36..22c69f5 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -319,9 +319,6 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_START_TAG, "BubbleTextView.onTouchEvent " + event);
-        }
         // Call the superclass onTouchEvent first, because sometimes it changes the state to
         // isPressed() on an ACTION_UP
         boolean result = super.onTouchEvent(event);
diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
index c967a96..6c5bc40 100644
--- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java
+++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3;
 
-import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -108,17 +108,20 @@
                     // For the second frame, if the first frame took more than 16ms,
                     // adjust the start time and pretend it took only 16ms anyway. This
                     // prevents a large jump in the animation due to an expensive first frame
-                } else if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY &&
-                        !mAdjustedSecondFrameTime &&
-                        currentTime > mStartTime + SINGLE_FRAME_MS &&
-                        currentPlayTime > SINGLE_FRAME_MS) {
-                    animation.setCurrentPlayTime(SINGLE_FRAME_MS);
-                    mAdjustedSecondFrameTime = true;
                 } else {
-                    if (frameNum > 1) {
-                        mRootView.post(() -> animation.removeUpdateListener(this));
+                    int singleFrameMS = getSingleFrameMs(mRootView.getContext());
+                    if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY &&
+                            !mAdjustedSecondFrameTime &&
+                            currentTime > mStartTime + singleFrameMS &&
+                            currentPlayTime > singleFrameMS) {
+                        animation.setCurrentPlayTime(singleFrameMS);
+                        mAdjustedSecondFrameTime = true;
+                    } else {
+                        if (frameNum > 1) {
+                            mRootView.post(() -> animation.removeUpdateListener(this));
+                        }
+                        if (DEBUG) print(animation);
                     }
-                    if (DEBUG) print(animation);
                 }
                 mHandlingOnAnimationUpdate = false;
             } else {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index bc3aa7e..5b38261 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -890,9 +890,6 @@
 
     @Override
     protected void onStart() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "Launcher.onStart");
-        }
         RaceConditionTracker.onEvent(ON_START_EVT, ENTER);
         super.onStart();
         if (mLauncherCallbacks != null) {
@@ -1809,11 +1806,6 @@
 
     public boolean startActivitySafely(View v, Intent intent, ItemInfo item,
             @Nullable String sourceContainer) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_START_TAG,
-                    "startActivitySafely outer");
-        }
-
         if (!hasBeenResumed()) {
             // Workaround an issue where the WM launch animation is clobbered when finishing the
             // recents animation into launcher. Defer launching the activity until Launcher is
@@ -1904,6 +1896,10 @@
         if (mPendingExecutor != null) {
             mPendingExecutor.markCompleted();
             mPendingExecutor = null;
+
+            // We might have set this flag previously and forgot to clear it.
+            mAppsView.getAppsStore()
+                    .disableDeferUpdatesSilently(AllAppsStore.DEFER_UPDATES_NEXT_DRAW);
         }
     }
 
@@ -2257,9 +2253,7 @@
 
     @Override
     public void executeOnNextDraw(ViewOnDrawExecutor executor) {
-        if (mPendingExecutor != null) {
-            mPendingExecutor.markCompleted();
-        }
+        clearPendingBinds();
         mPendingExecutor = executor;
         if (!isInState(ALL_APPS)) {
             mAppsView.getAppsStore().enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW);
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index dcfd272..6e2626b 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -243,6 +243,10 @@
         return 0;
     }
 
+    public float getOverviewScrimAlpha(Launcher launcher) {
+        return 0;
+    }
+
     public String getDescription(Launcher launcher) {
         return launcher.getWorkspace().getCurrentPageDescription();
     }
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index 2c8c208..d66e581 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -403,10 +403,6 @@
     }
 
     private void onStateTransitionStart(LauncherState state) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                    "onStateTransitionStart");
-        }
         if (mState != state) {
             mState.onStateDisabled(mLauncher);
         }
@@ -429,11 +425,6 @@
         // Only change the stable states after the transitions have finished
         if (state != mCurrentStableState) {
             mLastStableState = state.getHistoryForState(mCurrentStableState);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG,
-                        "mCurrentStableState = " + state.getClass().getSimpleName() + " @ " +
-                                android.util.Log.getStackTraceString(new Throwable()));
-            }
             mCurrentStableState = state;
         }
 
@@ -580,10 +571,6 @@
         private final AnimatorSet mAnim;
 
         public StartAnimRunnable(AnimatorSet anim) {
-            if (TestProtocol.sDebugTracing) {
-                android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                        "StartAnimRunnable");
-            }
             mAnim = anim;
         }
 
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 2eeb132..d2b8d4e 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.Utilities.shouldDisableGestures;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
@@ -48,6 +47,7 @@
 import android.widget.ScrollView;
 
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.touch.OverScroll;
@@ -368,6 +368,7 @@
      */
     protected void onPageEndTransition() {
         mWasInOverscroll = false;
+        AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
     }
 
     protected int getUnboundedScrollX() {
@@ -847,7 +848,7 @@
          */
 
         // Skip touch handling if there are no pages to swipe
-        if (getChildCount() <= 0 || shouldDisableGestures(ev)) return false;
+        if (getChildCount() <= 0) return false;
 
         acquireVelocityTrackerAndAddMovement(ev);
 
@@ -1095,7 +1096,7 @@
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         // Skip touch handling if there are no pages to swipe
-        if (getChildCount() <= 0 || shouldDisableGestures(ev)) return false;
+        if (getChildCount() <= 0) return false;
 
         acquireVelocityTrackerAndAddMovement(ev);
 
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 65aa3a7..fc5cd8a 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.ItemInfoWithIcon.FLAG_ICON_BADGED;
+
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -92,8 +94,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import static com.android.launcher3.ItemInfoWithIcon.FLAG_ICON_BADGED;
-
 /**
  * Various utilities shared amongst the Launcher's classes.
  */
@@ -128,16 +128,6 @@
     public static final int EDGE_NAV_BAR = 1 << 8;
 
     /**
-     * Set on a motion event do disallow any gestures and only handle touch.
-     * See {@link MotionEvent#setEdgeFlags(int)}.
-     */
-    public static final int FLAG_NO_GESTURES = 1 << 9;
-
-    public static boolean shouldDisableGestures(MotionEvent ev) {
-        return (ev.getEdgeFlags() & FLAG_NO_GESTURES) == FLAG_NO_GESTURES;
-    }
-
-    /**
      * Indicates if the device has a debug build. Should only be used to store additional info or
      * add extra logging and not for changing the app behavior.
      */
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 9f846bb..269a591 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -81,10 +81,10 @@
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.WorkspaceTouchListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -370,10 +370,6 @@
 
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                    "onDragStart 1");
-        }
         if (ENFORCE_DRAG_EVENT_ORDER) {
             enforceDragParity("onDragStart", 0, 0);
         }
@@ -424,10 +420,6 @@
         }
 
         // Always enter the spring loaded mode
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                    "onDragStart 2");
-        }
         mLauncher.getStateManager().goToState(SPRING_LOADED);
     }
 
@@ -1061,8 +1053,11 @@
             // Not announcing the overlay page for accessibility since it announces itself.
         } else if (Float.compare(scroll, 0f) == 0) {
             if (mOverlayShown) {
-                mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
+                UserEventDispatcher ued = mLauncher.getUserEventDispatcher();
+                if (!ued.isPreviousHomeGesture()) {
+                    mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.SWIPE,
                         Action.Direction.RIGHT, ContainerType.WORKSPACE, -1);
+                }
             } else if (Float.compare(mOverlayTranslation, 0f) != 0) {
                 // When arriving to 0 overscroll from non-zero overscroll, announce page for
                 // accessibility since default announcements were disabled while in overscroll
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index ea9b077..4a2109e 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -50,6 +50,7 @@
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -297,7 +298,11 @@
 
     @Override
     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        // This is filled in {@link AllAppsRecyclerView}
+        if (getApps().hasFilter()) {
+            targetParent.containerType = ContainerType.SEARCHRESULT;
+        } else {
+            targetParent.containerType = ContainerType.ALLAPPS;
+        }
     }
 
     @Override
@@ -626,9 +631,6 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_START_TAG, "AllAppsContainerView.dispatchTouchEvent " + ev);
-        }
         final boolean result = super.dispatchTouchEvent(ev);
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 160042e..267363f 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -80,6 +80,10 @@
         }
     }
 
+    public void disableDeferUpdatesSilently(int flag) {
+        mDeferUpdatesFlags &= ~flag;
+    }
+
     public int getDeferUpdatesFlags() {
         return mDeferUpdatesFlags;
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 4683893..5b3beec 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -7,6 +7,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
+import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_HEADER_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
@@ -168,10 +169,6 @@
     @Override
     public void setStateWithAnimation(LauncherState toState,
             AnimatorSetBuilder builder, AnimationConfig config) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG,
-                    "setStateWithAnimation " + toState.getClass().getSimpleName());
-        }
         float targetProgress = toState.getVerticalProgress(mLauncher);
         if (Float.compare(mProgress, targetProgress) == 0) {
             setAlphas(toState, config, builder);
@@ -212,13 +209,14 @@
         PropertySetter setter = config == null ? NO_ANIM_PROPERTY_SETTER
                 : config.getPropertySetter(builder);
         boolean hasHeaderExtra = (visibleElements & ALL_APPS_HEADER_EXTRA) != 0;
-        boolean hasContent = (visibleElements & ALL_APPS_CONTENT) != 0;
+        boolean hasAllAppsContent = (visibleElements & ALL_APPS_CONTENT) != 0;
 
         Interpolator allAppsFade = builder.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
-        setter.setViewAlpha(mAppsView.getContentView(), hasContent ? 1 : 0, allAppsFade);
-        setter.setViewAlpha(mAppsView.getScrollBar(), hasContent ? 1 : 0, allAppsFade);
-        mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra, hasContent, setter,
-                allAppsFade);
+        Interpolator headerFade = builder.getInterpolator(ANIM_ALL_APPS_HEADER_FADE, allAppsFade);
+        setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
+        setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
+        mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra, hasAllAppsContent,
+                setter, headerFade, allAppsFade);
         mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
 
         setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA,
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 2ad92e1..1369441 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -301,11 +301,6 @@
     }
 
     private void refreshRecyclerView() {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_START_TAG,
-                    "refreshRecyclerView @ " + android.util.Log.getStackTraceString(
-                            new Throwable()));
-        }
         if (mAdapter != null) {
             mAdapter.notifyDataSetChanged();
         }
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderRow.java b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
index 922e4f1..f899587 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderRow.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
@@ -46,8 +46,8 @@
      */
     boolean hasVisibleContent();
 
-    void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
-            PropertySetter setter, Interpolator fadeInterpolator);
+    void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
+            PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade);
 
     /**
      * Scrolls the content vertically.
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 66dced9..42a0eee 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -27,6 +27,10 @@
 import android.view.animation.Interpolator;
 import android.widget.LinearLayout;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
@@ -40,10 +44,6 @@
 import java.util.ArrayList;
 import java.util.Map;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-
 public class FloatingHeaderView extends LinearLayout implements
         ValueAnimator.AnimatorUpdateListener, PluginListener<AllAppsRow>, Insettable,
         OnHeightUpdatedListener {
@@ -363,14 +363,14 @@
         p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
     }
 
-    public void setContentVisibility(boolean hasHeader, boolean hasContent, PropertySetter setter,
-            Interpolator fadeInterpolator) {
+    public void setContentVisibility(boolean hasHeader, boolean hasAllAppsContent,
+            PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
         for (FloatingHeaderRow row : mAllRows) {
-            row.setContentVisibility(hasHeader, hasContent, setter, fadeInterpolator);
+            row.setContentVisibility(hasHeader, hasAllAppsContent, setter, headerFade, allAppsFade);
         }
 
-        allowTouchForwarding(hasContent);
-        setter.setFloat(mTabLayout, ALPHA, hasContent ? 1 : 0, fadeInterpolator);
+        allowTouchForwarding(hasAllAppsContent);
+        setter.setFloat(mTabLayout, ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
     }
 
     protected void allowTouchForwarding(boolean allow) {
diff --git a/src/com/android/launcher3/allapps/PluginHeaderRow.java b/src/com/android/launcher3/allapps/PluginHeaderRow.java
index b283ff4..535ef54 100644
--- a/src/com/android/launcher3/allapps/PluginHeaderRow.java
+++ b/src/com/android/launcher3/allapps/PluginHeaderRow.java
@@ -64,10 +64,10 @@
     }
 
     @Override
-    public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent,
-            PropertySetter setter, Interpolator fadeInterpolator) {
+    public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent,
+            PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) {
         // Don't use setViewAlpha as we want to control the visibility ourselves.
-        setter.setFloat(mView, ALPHA, hasContent ? 1 : 0, fadeInterpolator);
+        setter.setFloat(mView, ALPHA, hasAllAppsContent ? 1 : 0, headerFade);
     }
 
     @Override
diff --git a/src/com/android/launcher3/anim/AnimatorSetBuilder.java b/src/com/android/launcher3/anim/AnimatorSetBuilder.java
index 52a896e..cd30dea 100644
--- a/src/com/android/launcher3/anim/AnimatorSetBuilder.java
+++ b/src/com/android/launcher3/anim/AnimatorSetBuilder.java
@@ -39,6 +39,8 @@
     public static final int ANIM_OVERVIEW_TRANSLATE_Y = 8;
     public static final int ANIM_OVERVIEW_FADE = 9;
     public static final int ANIM_ALL_APPS_FADE = 10;
+    public static final int ANIM_OVERVIEW_SCRIM_FADE = 11;
+    public static final int ANIM_ALL_APPS_HEADER_FADE = 12; // e.g. predictions
 
     public static final int FLAG_DONT_ANIMATE_OVERVIEW = 1 << 0;
 
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index b169cb8..c45cd85 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -16,8 +16,9 @@
 
 package com.android.launcher3.anim;
 
-import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
+import android.content.Context;
 import android.graphics.Path;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.AccelerateInterpolator;
@@ -144,7 +145,8 @@
     public static Interpolator clampToProgress(Interpolator interpolator, float lowerBound,
             float upperBound) {
         if (upperBound <= lowerBound) {
-            throw new IllegalArgumentException("lowerBound must be less than upperBound");
+            throw new IllegalArgumentException(String.format(
+                    "lowerBound (%f) must be less than upperBound (%f)", lowerBound, upperBound));
         }
         return t -> {
             if (t < lowerBound) {
@@ -187,13 +189,13 @@
          * @param totalDistancePx The distance against which progress is calculated.
          */
         public OvershootParams(float startProgress, float overshootPastProgress,
-                float endProgress, float velocityPxPerMs, int totalDistancePx) {
+                float endProgress, float velocityPxPerMs, int totalDistancePx, Context context) {
             velocityPxPerMs = Math.abs(velocityPxPerMs);
             start = startProgress;
             int startPx = (int) (start * totalDistancePx);
             // Overshoot by about half a frame.
             float overshootBy = OVERSHOOT_FACTOR * velocityPxPerMs *
-                    SINGLE_FRAME_MS / totalDistancePx / 2;
+                    getSingleFrameMs(context) / totalDistancePx / 2;
             overshootBy = Utilities.boundToRange(overshootBy, 0.02f, 0.15f);
             end = overshootPastProgress + overshootBy;
             int endPx = (int) (end  * totalDistancePx);
diff --git a/src/com/android/launcher3/anim/SpringObjectAnimator.java b/src/com/android/launcher3/anim/SpringObjectAnimator.java
index 395fed2..91a3106 100644
--- a/src/com/android/launcher3/anim/SpringObjectAnimator.java
+++ b/src/com/android/launcher3/anim/SpringObjectAnimator.java
@@ -96,7 +96,10 @@
             }
         });
 
-        mSpring.addUpdateListener((animation, value, velocity) -> mSpringEnded = false);
+        mSpring.addUpdateListener((animation, value, velocity) -> {
+            mSpringEnded = false;
+            mEnded = false;
+        });
         mSpring.addEndListener((animation, canceled, value, velocity) -> {
             mSpringEnded = true;
             tryEnding();
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 43ae651..81c95cb 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -53,9 +53,6 @@
     }
 
     public static void sendStateEventToTest(Context context, int stateOrdinal) {
-        if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-            android.util.Log.e(TestProtocol.NO_ALLAPPS_EVENT_TAG, "sendStateEventToTest");
-        }
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java
index 7dad7e9..4f4d641 100644
--- a/src/com/android/launcher3/compat/PackageInstallerCompat.java
+++ b/src/com/android/launcher3/compat/PackageInstallerCompat.java
@@ -19,6 +19,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageInstaller;
+import android.os.UserHandle;
 
 import java.util.HashMap;
 import java.util.List;
@@ -48,6 +49,11 @@
      */
     public abstract HashMap<String, PackageInstaller.SessionInfo> updateAndGetActiveSessionCache();
 
+    /**
+     * @return an active SessionInfo for {@param pkg} or null if none exists.
+     */
+    public abstract PackageInstaller.SessionInfo getActiveSessionInfo(UserHandle user, String pkg);
+
     public abstract void onStop();
 
     public static final class PackageInstallInfo {
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
index a34ca50..8a5eabc 100644
--- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
+++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
@@ -64,9 +64,9 @@
     @Override
     public HashMap<String, SessionInfo> updateAndGetActiveSessionCache() {
         HashMap<String, SessionInfo> activePackages = new HashMap<>();
-        UserHandle user = Process.myUserHandle();
+        UserHandle primaryUser = Process.myUserHandle();
         for (SessionInfo info : getAllVerifiedSessions()) {
-            addSessionInfoToCache(info, user);
+            addSessionInfoToCache(info, Utilities.ATLEAST_Q ? info.getUser() : primaryUser);
             if (info.getAppPackageName() != null) {
                 activePackages.put(info.getAppPackageName(), info);
                 mActiveSessions.put(info.getSessionId(), info.getAppPackageName());
@@ -75,6 +75,19 @@
         return activePackages;
     }
 
+    public SessionInfo getActiveSessionInfo(UserHandle user, String pkg) {
+        for (SessionInfo info : getAllVerifiedSessions()) {
+            boolean match = pkg.equals(info.getAppPackageName());
+            if (Utilities.ATLEAST_Q && !user.equals(info.getUser())) {
+                match = false;
+            }
+            if (match) {
+                return info;
+            }
+        }
+        return null;
+    }
+
     @Thunk void addSessionInfoToCache(SessionInfo info, UserHandle user) {
         String packageName = info.getAppPackageName();
         if (packageName != null) {
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 45639e0..54efcb7 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -105,7 +105,7 @@
             "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
 
     public static final TogglableFlag ENABLE_HINTS_IN_OVERVIEW = new TogglableFlag(
-            "ENABLE_HINTS_IN_OVERVIEW", false,
+            "ENABLE_HINTS_IN_OVERVIEW", true,
             "Show chip hints and gleams on the overview screen");
 
     public static final TogglableFlag FAKE_LANDSCAPE_UI = new TogglableFlag(
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index a72089d..a2dcbf8 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -153,16 +153,6 @@
         PinItemDragListener listener = new PinItemDragListener(mRequest, bounds,
                 img.getBitmap().getWidth(), img.getWidth());
 
-        Intent homeIntent = listener.addToIntent(
-                new Intent(Intent.ACTION_MAIN)
-                        .addCategory(Intent.CATEGORY_HOME)
-                        .setPackage(getPackageName())
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-
-        listener.initWhenReady();
-        startActivity(homeIntent,
-                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out).toBundle());
-        mFinishOnPause = true;
 
         // Start a system drag and drop. We use a transparent bitmap as preview for system drag
         // as the preview is handled internally by launcher.
@@ -179,6 +169,18 @@
                 outShadowTouchPoint.set(SHADOW_SIZE / 2, SHADOW_SIZE / 2);
             }
         }, null, View.DRAG_FLAG_GLOBAL);
+
+
+        Intent homeIntent = listener.addToIntent(
+                new Intent(Intent.ACTION_MAIN)
+                        .addCategory(Intent.CATEGORY_HOME)
+                        .setPackage(getPackageName())
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+
+        listener.initWhenReady();
+        startActivity(homeIntent,
+                ActivityOptions.makeCustomAnimation(this, 0, android.R.anim.fade_out).toBundle());
+        mFinishOnPause = true;
         return false;
     }
 
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index c719c1c..1b08723 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -137,9 +137,6 @@
 
     @Override
     public boolean shouldStartDrag(double distanceDragged) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TAG, "BIDL.shouldStartDrag");
-        }
         // Stay in pre-drag mode, if workspace is locked.
         return !mLauncher.isWorkspaceLocked();
     }
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 72a1abb..d32dd2e 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -474,10 +474,6 @@
     }
 
     private void handleMoveEvent(int x, int y) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                    "handleMoveEvent 1");
-        }
         mDragObject.dragView.move(x, y);
 
         // Drop on someone?
@@ -492,22 +488,8 @@
         mLastTouch[0] = x;
         mLastTouch[1] = y;
 
-        if (TestProtocol.sDebugTracing) {
-           Log.d(TestProtocol.NO_DRAG_TAG,
-                    "handleMoveEvent Conditions " +
-                            mIsInPreDrag + ", " +
-                            (mIsInPreDrag && mOptions.preDragCondition != null) + ", " +
-                            (mIsInPreDrag && mOptions.preDragCondition != null
-                                    && mOptions.preDragCondition.shouldStartDrag(
-                                    mDistanceSinceScroll)));
-        }
-
         if (mIsInPreDrag && mOptions.preDragCondition != null
                 && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) {
-            if (TestProtocol.sDebugTracing) {
-                android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                        "handleMoveEvent 2");
-            }
             callOnDragStart();
         }
     }
@@ -545,10 +527,6 @@
      * Call this from a drag source view.
      */
     public boolean onControllerTouchEvent(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                    "onControllerTouchEvent");
-        }
         if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) {
             return false;
         }
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 6ba015b..b59164a 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -48,12 +48,13 @@
 import com.android.launcher3.DropTargetBar;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.graphics.OverviewScrim;
+import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.uioverrides.UiFactory;
@@ -92,7 +93,8 @@
 
     // Related to adjacent page hints
     private final ViewGroupFocusHelper mFocusIndicatorHelper;
-    private final WorkspaceAndHotseatScrim mScrim;
+    private final WorkspaceAndHotseatScrim mWorkspaceScrim;
+    private final OverviewScrim mOverviewScrim;
 
     /**
      * Used to create a new DragLayer from XML.
@@ -108,12 +110,13 @@
         setChildrenDrawingOrderEnabled(true);
 
         mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
-        mScrim = new WorkspaceAndHotseatScrim(this);
+        mWorkspaceScrim = new WorkspaceAndHotseatScrim(this);
+        mOverviewScrim = new OverviewScrim(this);
     }
 
     public void setup(DragController dragController, Workspace workspace) {
         mDragController = dragController;
-        mScrim.setWorkspace(workspace);
+        mWorkspaceScrim.setWorkspace(workspace);
         recreateControllers();
     }
 
@@ -529,25 +532,39 @@
     @Override
     protected void dispatchDraw(Canvas canvas) {
         // Draw the background below children.
-        mScrim.draw(canvas);
+        mWorkspaceScrim.draw(canvas);
+        mOverviewScrim.updateCurrentScrimmedView(this);
         mFocusIndicatorHelper.draw(canvas);
         super.dispatchDraw(canvas);
     }
 
     @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        if (child == mOverviewScrim.getScrimmedView()) {
+            mOverviewScrim.draw(canvas);
+        }
+        return super.drawChild(canvas, child, drawingTime);
+    }
+
+    @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
-        mScrim.setSize(w, h);
+        mWorkspaceScrim.setSize(w, h);
     }
 
     @Override
     public void setInsets(Rect insets) {
         super.setInsets(insets);
-        mScrim.onInsetsChanged(insets);
+        mWorkspaceScrim.onInsetsChanged(insets);
+        mOverviewScrim.onInsetsChanged(insets);
     }
 
     public WorkspaceAndHotseatScrim getScrim() {
-        return mScrim;
+        return mWorkspaceScrim;
+    }
+
+    public OverviewScrim getOverviewScrim() {
+        return mOverviewScrim;
     }
 
     @Override
diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
index 0c5a1fc..d8a1f99 100644
--- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
+++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
@@ -28,6 +28,8 @@
 import android.os.Build;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
@@ -66,15 +68,19 @@
         return mBadge;
     }
 
-    public static FolderAdaptiveIcon createFolderAdaptiveIcon(
+    public static @Nullable FolderAdaptiveIcon createFolderAdaptiveIcon(
             Launcher launcher, int folderId, Point dragViewSize) {
         Preconditions.assertNonUiThread();
         int margin = launcher.getResources()
                 .getDimensionPixelSize(R.dimen.blur_size_medium_outline);
 
         // Allocate various bitmaps on the background thread, because why not!
-        final Bitmap badge = Bitmap.createBitmap(
-                dragViewSize.x - margin, dragViewSize.y - margin, Bitmap.Config.ARGB_8888);
+        int width = dragViewSize.x - margin;
+        int height = dragViewSize.y - margin;
+        if (width <= 0 || height <= 0) {
+            return null;
+        }
+        final Bitmap badge = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
 
         // Create the actual drawable on the UI thread to avoid race conditions with
         // FolderIcon draw pass
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index 7eb4015..9263a2a 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -29,6 +29,7 @@
 import android.view.View;
 
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
@@ -87,6 +88,9 @@
             Rect bounds = getDrawableBounds(d);
             destCanvas.translate(blurSizeOutline / 2 - bounds.left,
                     blurSizeOutline / 2 - bounds.top);
+            if (d instanceof FastBitmapDrawable) {
+                ((FastBitmapDrawable) d).setScale(1);
+            }
             d.draw(destCanvas);
         } else {
             final Rect clipRect = mTempRect;
diff --git a/src/com/android/launcher3/graphics/OverviewScrim.java b/src/com/android/launcher3/graphics/OverviewScrim.java
new file mode 100644
index 0000000..d707403
--- /dev/null
+++ b/src/com/android/launcher3/graphics/OverviewScrim.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.graphics;
+
+import static android.view.View.VISIBLE;
+
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * View scrim which draws behind overview (recent apps).
+ */
+public class OverviewScrim extends Scrim {
+
+    private @NonNull View mStableScrimmedView;
+    // Might be higher up if mStableScrimmedView is invisible.
+    private @Nullable View mCurrentScrimmedView;
+
+    public OverviewScrim(View view) {
+        super(view);
+        mStableScrimmedView = mCurrentScrimmedView = mLauncher.getOverviewPanel();
+
+        onExtractedColorsChanged(mWallpaperColorInfo);
+    }
+
+    public void onInsetsChanged(Rect insets) {
+        mStableScrimmedView = (OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0
+                ? mLauncher.getHotseat()
+                : mLauncher.getOverviewPanel();
+    }
+
+    public void updateCurrentScrimmedView(ViewGroup root) {
+        // Find the lowest view that is at or above the view we want to show the scrim behind.
+        mCurrentScrimmedView = mStableScrimmedView;
+        int currentIndex = root.indexOfChild(mCurrentScrimmedView);
+        final int childCount = root.getChildCount();
+        while (mCurrentScrimmedView.getVisibility() != VISIBLE && currentIndex < childCount) {
+            currentIndex++;
+            mCurrentScrimmedView = root.getChildAt(currentIndex);
+        }
+    }
+
+    /**
+     * @return The view to draw the scrim behind.
+     */
+    public View getScrimmedView() {
+        return mCurrentScrimmedView;
+    }
+}
diff --git a/src/com/android/launcher3/graphics/Scrim.java b/src/com/android/launcher3/graphics/Scrim.java
new file mode 100644
index 0000000..5c14f8d
--- /dev/null
+++ b/src/com/android/launcher3/graphics/Scrim.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 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.graphics;
+
+import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+
+import android.graphics.Canvas;
+import android.util.Property;
+import android.view.View;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.uioverrides.WallpaperColorInfo;
+
+/**
+ * Contains general scrim properties such as wallpaper-extracted color that subclasses can use.
+ */
+public class Scrim implements View.OnAttachStateChangeListener,
+        WallpaperColorInfo.OnChangeListener {
+
+    public static Property<Scrim, Float> SCRIM_PROGRESS =
+            new Property<Scrim, Float>(Float.TYPE, "scrimProgress") {
+                @Override
+                public Float get(Scrim scrim) {
+                    return scrim.mScrimProgress;
+                }
+
+                @Override
+                public void set(Scrim scrim, Float value) {
+                    scrim.setScrimProgress(value);
+                }
+            };
+
+    protected final Launcher mLauncher;
+    protected final WallpaperColorInfo mWallpaperColorInfo;
+    protected final View mRoot;
+
+    protected float mScrimProgress;
+    protected int mScrimColor;
+    protected int mScrimAlpha = 0;
+
+    public Scrim(View view) {
+        mRoot = view;
+        mLauncher = Launcher.getLauncher(view.getContext());
+        mWallpaperColorInfo = WallpaperColorInfo.getInstance(mLauncher);
+
+        view.addOnAttachStateChangeListener(this);
+    }
+
+    public void draw(Canvas canvas) {
+        canvas.drawColor(setColorAlphaBound(mScrimColor, mScrimAlpha));
+    }
+
+    private void setScrimProgress(float progress) {
+        if (mScrimProgress != progress) {
+            mScrimProgress = progress;
+            mScrimAlpha = Math.round(255 * mScrimProgress);
+            invalidate();
+        }
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View view) {
+        mWallpaperColorInfo.addOnChangeListener(this);
+        onExtractedColorsChanged(mWallpaperColorInfo);
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View view) {
+        mWallpaperColorInfo.removeOnChangeListener(this);
+    }
+
+    @Override
+    public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
+        mScrimColor = wallpaperColorInfo.getMainColor();
+        if (mScrimAlpha > 0) {
+            invalidate();
+        }
+    }
+
+    public void invalidate() {
+        mRoot.invalidate();
+    }
+}
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
index c0aa75f..6740fa1 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
@@ -40,34 +40,19 @@
 import android.util.Property;
 import android.view.View;
 
+import androidx.core.graphics.ColorUtils;
+
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.util.Themes;
 
-import androidx.core.graphics.ColorUtils;
-
 /**
  * View scrim which draws behind hotseat and workspace
  */
-public class WorkspaceAndHotseatScrim implements
-        View.OnAttachStateChangeListener, WallpaperColorInfo.OnChangeListener {
-
-    public static Property<WorkspaceAndHotseatScrim, Float> SCRIM_PROGRESS =
-            new Property<WorkspaceAndHotseatScrim, Float>(Float.TYPE, "scrimProgress") {
-                @Override
-                public Float get(WorkspaceAndHotseatScrim scrim) {
-                    return scrim.mScrimProgress;
-                }
-
-                @Override
-                public void set(WorkspaceAndHotseatScrim scrim, Float value) {
-                    scrim.setScrimProgress(value);
-                }
-            };
+public class WorkspaceAndHotseatScrim extends Scrim {
 
     public static Property<WorkspaceAndHotseatScrim, Float> SYSUI_PROGRESS =
             new Property<WorkspaceAndHotseatScrim, Float>(Float.TYPE, "sysUiProgress") {
@@ -117,9 +102,6 @@
     private static final int ALPHA_MASK_WIDTH_DP = 2;
 
     private final Rect mHighlightRect = new Rect();
-    private final Launcher mLauncher;
-    private final WallpaperColorInfo mWallpaperColorInfo;
-    private final View mRoot;
 
     private Workspace mWorkspace;
 
@@ -132,11 +114,6 @@
 
     private final Drawable mTopScrim;
 
-    private int mFullScrimColor;
-
-    private float mScrimProgress;
-    private int mScrimAlpha = 0;
-
     private float mSysUiProgress = 1;
     private boolean mHideSysUiScrim;
 
@@ -144,9 +121,7 @@
     private float mSysUiAnimMultiplier = 1;
 
     public WorkspaceAndHotseatScrim(View view) {
-        mRoot = view;
-        mLauncher = Launcher.getLauncher(view.getContext());
-        mWallpaperColorInfo = WallpaperColorInfo.getInstance(mLauncher);
+        super(view);
 
         mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
                 view.getResources().getDisplayMetrics());
@@ -154,7 +129,6 @@
         mBottomMask = mTopScrim == null ? null : createDitheredAlphaMask();
         mHideSysUiScrim = mTopScrim == null;
 
-        view.addOnAttachStateChangeListener(this);
         onExtractedColorsChanged(mWallpaperColorInfo);
     }
 
@@ -176,7 +150,7 @@
                 canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
             }
 
-            canvas.drawColor(setColorAlphaBound(mFullScrimColor, mScrimAlpha));
+            super.draw(canvas);
             canvas.restore();
         }
 
@@ -190,11 +164,8 @@
                 mSysUiAnimMultiplier = 0;
                 reapplySysUiAlphaNoInvalidate();
 
-                ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, 1);
-                anim.setAutoCancel(true);
-                anim.setDuration(600);
-                anim.setStartDelay(mLauncher.getWindow().getTransitionBackgroundFadeDuration());
-                anim.start();
+                animateToSysuiMultiplier(1, 600,
+                        mLauncher.getWindow().getTransitionBackgroundFadeDuration());
                 mAnimateScrimOnNextDraw = false;
             }
 
@@ -207,24 +178,24 @@
         }
     }
 
+    public void animateToSysuiMultiplier(float toMultiplier, long duration,
+            long startDelay) {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SYSUI_ANIM_MULTIPLIER, toMultiplier);
+        anim.setAutoCancel(true);
+        anim.setDuration(duration);
+        anim.setStartDelay(startDelay);
+        anim.start();
+    }
+
     public void onInsetsChanged(Rect insets) {
         mDrawTopScrim = mTopScrim != null && insets.top > 0;
         mDrawBottomScrim = mBottomMask != null &&
                 !mLauncher.getDeviceProfile().isVerticalBarLayout();
     }
 
-    private void setScrimProgress(float progress) {
-        if (mScrimProgress != progress) {
-            mScrimProgress = progress;
-            mScrimAlpha = Math.round(255 * mScrimProgress);
-            invalidate();
-        }
-    }
-
     @Override
     public void onViewAttachedToWindow(View view) {
-        mWallpaperColorInfo.addOnChangeListener(this);
-        onExtractedColorsChanged(mWallpaperColorInfo);
+        super.onViewAttachedToWindow(view);
 
         if (mTopScrim != null) {
             IntentFilter filter = new IntentFilter(ACTION_SCREEN_OFF);
@@ -235,7 +206,7 @@
 
     @Override
     public void onViewDetachedFromWindow(View view) {
-        mWallpaperColorInfo.removeOnChangeListener(this);
+        super.onViewDetachedFromWindow(view);
         if (mTopScrim != null) {
             mRoot.getContext().unregisterReceiver(mReceiver);
         }
@@ -248,10 +219,7 @@
         mBottomMaskPaint.setColor(ColorUtils.compositeColors(DARK_SCRIM_COLOR,
                 wallpaperColorInfo.getMainColor()));
         reapplySysUiAlpha();
-        mFullScrimColor = wallpaperColorInfo.getMainColor();
-        if (mScrimAlpha > 0) {
-            invalidate();
-        }
+        super.onExtractedColorsChanged(wallpaperColorInfo);
     }
 
     public void setSize(int w, int h) {
@@ -291,10 +259,6 @@
         }
     }
 
-    public void invalidate() {
-        mRoot.invalidate();
-    }
-
     public Bitmap createDitheredAlphaMask() {
         DisplayMetrics dm = mLauncher.getResources().getDisplayMetrics();
         int width = ResourceUtils.pxFromDp(ALPHA_MASK_WIDTH_DP, dm);
diff --git a/src/com/android/launcher3/logging/StatsLogUtils.java b/src/com/android/launcher3/logging/StatsLogUtils.java
index 647f255..b02a050 100644
--- a/src/com/android/launcher3/logging/StatsLogUtils.java
+++ b/src/com/android/launcher3/logging/StatsLogUtils.java
@@ -1,9 +1,12 @@
 package com.android.launcher3.logging;
 
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.DEFAULT_CONTAINERTYPE;
+
 import android.view.View;
 import android.view.ViewParent;
 
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 
 import androidx.annotation.Nullable;
@@ -64,4 +67,20 @@
         }
         return null;
     }
+
+    public static int getContainerTypeFromState(int state) {
+        int containerType = DEFAULT_CONTAINERTYPE;
+        switch (state) {
+            case StatsLogUtils.LAUNCHER_STATE_ALLAPPS:
+                containerType = ContainerType.ALLAPPS;
+                break;
+            case StatsLogUtils.LAUNCHER_STATE_HOME:
+                containerType = ContainerType.WORKSPACE;
+                break;
+            case StatsLogUtils.LAUNCHER_STATE_OVERVIEW:
+                containerType = ContainerType.OVERVIEW;
+                break;
+        }
+        return containerType;
+    }
 }
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index bd785a1..d81020e 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -115,6 +115,7 @@
     protected InstantAppResolver mInstantAppResolver;
     private boolean mAppOrTaskLaunch;
     private UserEventDelegate mDelegate;
+    private boolean mPreviousHomeGesture;
 
     //                      APP_ICON    SHORTCUT    WIDGET
     // --------------------------------------------------------------
@@ -399,11 +400,22 @@
         mElapsedContainerMillis = SystemClock.uptimeMillis();
     }
 
+    public final void setPreviousHomeGesture(boolean homeGesture) {
+        mPreviousHomeGesture = homeGesture;
+    }
+
+    public final boolean isPreviousHomeGesture() {
+        return mPreviousHomeGesture;
+    }
+
     public final void resetActionDurationMillis() {
         mActionDurationMillis = SystemClock.uptimeMillis();
     }
 
     public void dispatchUserEvent(LauncherEvent ev, Intent intent) {
+        if (mPreviousHomeGesture) {
+            mPreviousHomeGesture = false;
+        }
         mAppOrTaskLaunch = false;
         ev.elapsedContainerMillis = SystemClock.uptimeMillis() - mElapsedContainerMillis;
         ev.elapsedSessionMillis = SystemClock.uptimeMillis() - mElapsedSessionMillis;
@@ -426,6 +438,7 @@
                 ev.actionDurationMillis);
         log += "\n\n";
         Log.d(TAG, log);
+        return;
     }
 
     private static String getTargetsStr(Target[] targets) {
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 9719a18..25d9f79 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -449,11 +449,6 @@
 
             @Override
             public boolean shouldStartDrag(double distanceDragged) {
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_DRAG_TAG,
-                            "createPreDragCondition().shouldStartDrag " + distanceDragged + ", "
-                                    + mStartDragThreshold);
-                }
                 return distanceDragged > mStartDragThreshold;
             }
 
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index d2e1961..bab454f 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -74,6 +74,11 @@
                 break;
             }
 
+            case TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, true);
+                break;
+            }
+
             case TestProtocol.REQUEST_ENABLE_DEBUG_TRACING:
                 TestProtocol.sDebugTracing = true;
                 break;
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 6ffc2d9..9846a04 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -66,16 +66,17 @@
             "all-apps-to-overview-swipe-height";
     public static final String REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT =
             "home-to-all-apps-swipe-height";
+    public static final String REQUEST_HOTSEAT_TOP = "hotseat-top";
+    public static final String REQUEST_IS_LAUNCHER_INITIALIZED = "is-launcher-initialized";
     public static final String REQUEST_FREEZE_APP_LIST = "freeze-app-list";
     public static final String REQUEST_UNFREEZE_APP_LIST = "unfreeze-app-list";
     public static final String REQUEST_APP_LIST_FREEZE_FLAGS = "app-list-freeze-flags";
+    public static final String REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN = "overview-left-margin";
+    public static final String REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN = "overview-right-margin";
 
     public static boolean sDebugTracing = false;
     public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
     public static final String REQUEST_DISABLE_DEBUG_TRACING = "disable-debug-tracing";
-    public static final String NO_ALLAPPS_EVENT_TAG = "b/133867119";
-    public static final String NO_DRAG_TAG = "b/133009122";
-    public static final String NO_START_TAG = "b/132900132";
-    public static final String NO_START_TASK_TAG = "b/133765434";
-    public static final String NO_OVERVIEW_EVENT_TAG = "b/134532571";
+
+    public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 6f53140..0545344 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -22,16 +22,15 @@
 import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
 import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_SCALE_COMPONENT;
 import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
-import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.os.SystemClock;
-import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 
@@ -43,7 +42,6 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -57,8 +55,6 @@
 public abstract class AbstractStateChangeTouchController
         implements TouchController, SwipeDetector.Listener {
 
-    private static final String TAG = "ASCTouchController";
-
     // Progress after which the transition is assumed to be a success in case user does not fling
     public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
 
@@ -118,9 +114,6 @@
 
     @Override
     public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onControllerInterceptTouchEvent 1 " + ev);
-        }
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mNoIntercept = !canInterceptTouch(ev);
             if (mNoIntercept) {
@@ -150,9 +143,6 @@
             return false;
         }
 
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onControllerInterceptTouchEvent 2 ");
-        }
         onControllerTouchEvent(ev);
         return mDetector.isDraggingOrSettling();
     }
@@ -240,9 +230,6 @@
 
     @Override
     public void onDragStart(boolean start) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onDragStart 1 " + start);
-        }
         mStartState = mLauncher.getStateManager().getState();
         if (mStartState == ALL_APPS) {
             mStartContainerType = LauncherLogProto.ContainerType.ALLAPPS;
@@ -252,9 +239,6 @@
             mStartContainerType = LauncherLogProto.ContainerType.TASKSWITCHER;
         }
         if (mCurrentAnimation == null) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onDragStart 2");
-            }
             mFromState = mStartState;
             mToState = null;
             cancelAnimationControllers();
@@ -376,9 +360,6 @@
 
     @Override
     public void onDragEnd(float velocity, boolean fling) {
-        if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-            android.util.Log.e(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onDragEnd");
-        }
         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
 
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -413,8 +394,8 @@
                 duration = 0;
                 startProgress = 1;
             } else {
-                startProgress = Utilities.boundToRange(
-                        progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
+                startProgress = Utilities.boundToRange(progress
+                        + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
                 duration = SwipeDetector.calculateDuration(velocity,
                         endProgress - Math.max(progress, 0)) * durationMultiplier;
             }
@@ -431,8 +412,8 @@
                 duration = 0;
                 startProgress = 0;
             } else {
-                startProgress = Utilities.boundToRange(
-                        progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
+                startProgress = Utilities.boundToRange(progress
+                        + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
                 duration = SwipeDetector.calculateDuration(velocity,
                         Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
@@ -515,9 +496,6 @@
     }
 
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
-        if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-            android.util.Log.e(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onSwipeInteractionCompleted 1");
-        }
         if (mAtomicComponentsController != null) {
             mAtomicComponentsController.getAnimationPlayer().end();
             mAtomicComponentsController = null;
@@ -531,18 +509,18 @@
             shouldGoToTargetState = !reachedTarget;
         }
         if (shouldGoToTargetState) {
-            if (targetState != mStartState) {
-                logReachedState(logAction, targetState);
-            }
-            mLauncher.getStateManager().goToState(targetState, false /* animated */);
-
-            if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-                android.util.Log.e(
-                        TestProtocol.NO_ALLAPPS_EVENT_TAG, "onSwipeInteractionCompleted 2");
-            }
+            goToTargetState(targetState, logAction);
         }
     }
 
+    protected void goToTargetState(LauncherState targetState, int logAction) {
+        if (targetState != mStartState) {
+            logReachedState(logAction, targetState);
+        }
+        mLauncher.getStateManager().goToState(targetState, false /* animated */);
+        mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(1, 0, 0);
+    }
+
     private void logReachedState(int logAction, LauncherState targetState) {
         // Transition complete. log the action
         mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
@@ -563,9 +541,6 @@
     }
 
     private void cancelAnimationControllers() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "cancelAnimationControllers");
-        }
         mCurrentAnimation = null;
         cancelAtomicComponentsController();
         mDetector.finishedScrolling();
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 85f763d..03493a5 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -25,9 +25,15 @@
 import static com.android.launcher3.model.AppLaunchTracker.CONTAINER_ALL_APPS;
 
 import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageInstaller.SessionInfo;
 import android.os.Process;
+import android.os.UserHandle;
 import android.text.TextUtils;
+import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Toast;
@@ -43,11 +49,12 @@
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.PromiseAppInfo;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
@@ -58,6 +65,8 @@
  */
 public class ItemClickHandler {
 
+    private static final String TAG = ItemClickHandler.class.getSimpleName();
+
     /**
      * Instance used for click handling on items
      */
@@ -68,28 +77,12 @@
     }
 
     private static void onClick(View v, String sourceContainer) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_START_TAG,
-                    "onClick 1");
-        }
         // Make sure that rogue clicks don't get through while allapps is launching, or after the
         // view has detached (it's possible for this to happen if the view is removed mid touch).
-        if (v.getWindowToken() == null) {
-            if (TestProtocol.sDebugTracing) {
-                android.util.Log.d(TestProtocol.NO_START_TAG,
-                        "onClick 2");
-            }
-            return;
-        }
+        if (v.getWindowToken() == null) return;
 
         Launcher launcher = Launcher.getLauncher(v.getContext());
-        if (!launcher.getWorkspace().isFinishedSwitchingState()) {
-            if (TestProtocol.sDebugTracing) {
-                android.util.Log.d(TestProtocol.NO_START_TAG,
-                        "onClick 3");
-            }
-            return;
-        }
+        if (!launcher.getWorkspace().isFinishedSwitchingState()) return;
 
         Object tag = v.getTag();
         if (tag instanceof WorkspaceItemInfo) {
@@ -99,10 +92,6 @@
                 onClickFolderIcon(v);
             }
         } else if (tag instanceof AppInfo) {
-            if (TestProtocol.sDebugTracing) {
-                android.util.Log.d(TestProtocol.NO_START_TAG,
-                        "onClick 4");
-            }
             startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher,
                     sourceContainer == null ? CONTAINER_ALL_APPS: sourceContainer);
         } else if (tag instanceof LauncherAppWidgetInfo) {
@@ -166,6 +155,8 @@
             startMarketIntentForPackage(v, launcher, packageName);
             return;
         }
+        UserHandle user = v.getTag() instanceof ItemInfo
+                ? ((ItemInfo) v.getTag()).user : Process.myUserHandle();
         new AlertDialog.Builder(launcher)
                 .setTitle(R.string.abandoned_promises_title)
                 .setMessage(R.string.abandoned_promise_explanation)
@@ -173,12 +164,28 @@
                         (d, i) -> startMarketIntentForPackage(v, launcher, packageName))
                 .setNeutralButton(R.string.abandoned_clean_this,
                         (d, i) -> launcher.getWorkspace()
-                                .removeAbandonedPromise(packageName, Process.myUserHandle()))
+                                .removeAbandonedPromise(packageName, user))
                 .create().show();
     }
 
     private static void startMarketIntentForPackage(View v, Launcher launcher, String packageName) {
         ItemInfo item = (ItemInfo) v.getTag();
+        if (Utilities.ATLEAST_Q) {
+            PackageInstallerCompat pkgInstaller = PackageInstallerCompat.getInstance(launcher);
+            SessionInfo sessionInfo = pkgInstaller.getActiveSessionInfo(item.user, packageName);
+            if (sessionInfo != null) {
+                LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
+                try {
+                    launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
+                            launcher.getActivityLaunchOptionsAsBundle(v));
+                    return;
+                } catch (Exception e) {
+                    Log.e(TAG, "Unable to launch market intent for package=" + packageName, e);
+                }
+            }
+        }
+
+        // Fallback to using custom market intent.
         Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName);
         launcher.startActivitySafely(v, intent, item, null);
     }
@@ -234,10 +241,6 @@
 
     private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher,
             @Nullable String sourceContainer) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_START_TAG,
-                    "startAppShortcutOrInfoActivity");
-        }
         Intent intent;
         if (item instanceof PromiseAppInfo) {
             PromiseAppInfo promiseAppInfo = (PromiseAppInfo) item;
diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java
index 3d45404..3777a41 100644
--- a/src/com/android/launcher3/touch/SwipeDetector.java
+++ b/src/com/android/launcher3/touch/SwipeDetector.java
@@ -158,9 +158,6 @@
     // SETTLING -> (View settled) -> IDLE
 
     private void setState(ScrollState newState) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "setState -- start: " + newState);
-        }
         if (DBG) {
             Log.d(TAG, "setState:" + mState + "->" + newState);
         }
@@ -168,9 +165,6 @@
         if (newState == ScrollState.DRAGGING) {
             initializeDragging();
             if (mState == ScrollState.IDLE) {
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "setState -- 1: " + newState);
-                }
                 reportDragStart(false /* recatch */);
             } else if (mState == ScrollState.SETTLING) {
                 reportDragStart(true /* recatch */);
@@ -181,11 +175,6 @@
         }
 
         mState = newState;
-        if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
-            android.util.Log.e(TestProtocol.NO_ALLAPPS_EVENT_TAG,
-                    "setState: " + newState + " @ " + android.util.Log.getStackTraceString(
-                            new Throwable()));
-        }
     }
 
     public boolean isDraggingOrSettling() {
@@ -324,15 +313,9 @@
                     break;
                 }
                 mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos, mIsRtl);
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onTouchEvent 1");
-                }
 
                 // handle state and listener calls.
                 if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
-                    if (TestProtocol.sDebugTracing) {
-                        Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onTouchEvent 2");
-                    }
                     setState(ScrollState.DRAGGING);
                 }
                 if (mState == ScrollState.DRAGGING) {
diff --git a/src/com/android/launcher3/touch/TouchEventTranslator.java b/src/com/android/launcher3/touch/TouchEventTranslator.java
deleted file mode 100644
index 3fcda90..0000000
--- a/src/com/android/launcher3/touch/TouchEventTranslator.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * 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.launcher3.touch;
-
-import android.graphics.PointF;
-import android.util.Log;
-import android.util.Pair;
-import android.util.SparseArray;
-import android.view.MotionEvent;
-import android.view.MotionEvent.PointerCoords;
-import android.view.MotionEvent.PointerProperties;
-
-import java.util.function.Consumer;
-
-/**
- * To minimize the size of the MotionEvent, historic events are not copied and passed via the
- * listener.
- */
-public class TouchEventTranslator {
-
-    private static final String TAG = "TouchEventTranslator";
-    private static final boolean DEBUG = false;
-
-    private class DownState {
-        long timeStamp;
-        float downX;
-        float downY;
-        public DownState(long timeStamp, float downX, float downY) {
-            this.timeStamp = timeStamp;
-            this.downX = downX;
-            this.downY = downY;
-        }
-    };
-    private final DownState ZERO = new DownState(0, 0f, 0f);
-
-    private final Consumer<MotionEvent> mListener;
-
-    private final SparseArray<DownState> mDownEvents;
-    private final SparseArray<PointF> mFingers;
-
-    private final SparseArray<Pair<PointerProperties[], PointerCoords[]>> mCache;
-
-    public TouchEventTranslator(Consumer<MotionEvent> listener) {
-        mDownEvents = new SparseArray<>();
-        mFingers = new SparseArray<>();
-        mCache = new SparseArray<>();
-
-        mListener = listener;
-    }
-
-    public void reset() {
-        mDownEvents.clear();
-        mFingers.clear();
-    }
-
-    public float getDownX() {
-        return mDownEvents.get(0).downX;
-    }
-
-    public float getDownY() {
-        return mDownEvents.get(0).downY;
-    }
-
-    public void setDownParameters(int idx, MotionEvent e) {
-        DownState ev = new DownState(e.getEventTime(), e.getX(idx), e.getY(idx));
-        mDownEvents.append(idx, ev);
-    }
-
-    public void dispatchDownEvents(MotionEvent ev) {
-        for(int i = 0; i < ev.getPointerCount() && i < mDownEvents.size(); i++) {
-            int pid = ev.getPointerId(i);
-            put(pid, i, ev.getX(i), 0, mDownEvents.get(i).timeStamp, ev);
-        }
-    }
-
-    public void processMotionEvent(MotionEvent ev) {
-        if (DEBUG) {
-            printSamples(TAG + " processMotionEvent", ev);
-        }
-        int index = ev.getActionIndex();
-        float x = ev.getX(index);
-        float y = ev.getY(index) - mDownEvents.get(index, ZERO).downY;
-        switch (ev.getActionMasked()) {
-            case MotionEvent.ACTION_POINTER_DOWN:
-                int pid = ev.getPointerId(index);
-                if(mFingers.get(pid, null) != null) {
-                    for(int i=0; i < ev.getPointerCount(); i++) {
-                        pid = ev.getPointerId(i);
-                        position(pid, x, y);
-                    }
-                    generateEvent(ev.getAction(), ev);
-                } else {
-                    put(pid, index, x, y, ev);
-                }
-                break;
-            case MotionEvent.ACTION_MOVE:
-                for(int i=0; i < ev.getPointerCount(); i++) {
-                    pid = ev.getPointerId(i);
-                    position(pid, x, y);
-                }
-                generateEvent(ev.getAction(), ev);
-                break;
-            case MotionEvent.ACTION_POINTER_UP:
-            case MotionEvent.ACTION_UP:
-                pid = ev.getPointerId(index);
-                lift(pid, index, x, y, ev);
-                break;
-            case MotionEvent.ACTION_CANCEL:
-                cancel(ev);
-                break;
-            default:
-                Log.v(TAG, "Didn't process ");
-                printSamples(TAG, ev);
-
-        }
-    }
-
-    private TouchEventTranslator put(int id, int index, float x, float y, MotionEvent ev) {
-        return put(id, index, x, y, ev.getEventTime(), ev);
-    }
-
-    private TouchEventTranslator put(int id, int index, float x, float y, long ms, MotionEvent ev) {
-        checkFingerExistence(id, false);
-        boolean isInitialDown = (mFingers.size() == 0);
-
-        mFingers.put(id, new PointF(x, y));
-        int n = mFingers.size();
-
-        if (mCache.get(n) == null) {
-            PointerProperties[] properties = new PointerProperties[n];
-            PointerCoords[] coords = new PointerCoords[n];
-            for (int i = 0; i < n; i++) {
-                properties[i] = new PointerProperties();
-                coords[i] = new PointerCoords();
-            }
-            mCache.put(n, new Pair(properties, coords));
-        }
-
-        int action;
-        if (isInitialDown) {
-            action = MotionEvent.ACTION_DOWN;
-        } else {
-            action = MotionEvent.ACTION_POINTER_DOWN;
-            // Set the id of the changed pointer.
-            action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-        }
-        generateEvent(action, ms, ev);
-        return this;
-    }
-
-    public TouchEventTranslator position(int id, float x, float y) {
-        checkFingerExistence(id, true);
-        mFingers.get(id).set(x, y);
-        return this;
-    }
-
-    private TouchEventTranslator lift(int id, int index, MotionEvent ev) {
-        checkFingerExistence(id, true);
-        boolean isFinalUp = (mFingers.size() == 1);
-        int action;
-        if (isFinalUp) {
-            action = MotionEvent.ACTION_UP;
-        } else {
-            action = MotionEvent.ACTION_POINTER_UP;
-            // Set the id of the changed pointer.
-            action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
-        }
-        generateEvent(action, ev);
-        mFingers.remove(id);
-        return this;
-    }
-
-    private TouchEventTranslator lift(int id, int index, float x, float y, MotionEvent ev) {
-        checkFingerExistence(id, true);
-        mFingers.get(id).set(x, y);
-        return lift(id, index, ev);
-    }
-
-    public TouchEventTranslator cancel(MotionEvent ev) {
-        generateEvent(MotionEvent.ACTION_CANCEL, ev);
-        mFingers.clear();
-        return this;
-    }
-
-    private void checkFingerExistence(int id, boolean shouldExist) {
-        if (shouldExist != (mFingers.get(id, null) != null)) {
-            throw new IllegalArgumentException(
-                    shouldExist ? "Finger does not exist" : "Finger already exists");
-        }
-    }
-
-
-    /**
-     * Used to debug MotionEvents being sent/received.
-     */
-    public void printSamples(String msg, MotionEvent ev) {
-        System.out.printf("%s %s", msg, MotionEvent.actionToString(ev.getActionMasked()));
-        final int pointerCount = ev.getPointerCount();
-        System.out.printf("#%d/%d", ev.getActionIndex(), pointerCount);
-        System.out.printf(" t=%d:", ev.getEventTime());
-        for (int p = 0; p < pointerCount; p++) {
-            System.out.printf("  id=%d: (%f,%f)",
-                    ev.getPointerId(p), ev.getX(p), ev.getY(p));
-        }
-        System.out.println();
-    }
-
-    private void generateEvent(int action, MotionEvent ev) {
-        generateEvent(action, ev.getEventTime(), ev);
-    }
-
-    private void generateEvent(int action, long ms, MotionEvent ev) {
-        Pair<PointerProperties[], PointerCoords[]> state = getFingerState();
-        MotionEvent event = MotionEvent.obtain(
-                mDownEvents.get(0).timeStamp,
-                ms,
-                action,
-                state.first.length,
-                state.first,
-                state.second,
-                ev.getMetaState(),
-                ev.getButtonState() /* buttonState */,
-                ev.getXPrecision() /* xPrecision */,
-                ev.getYPrecision() /* yPrecision */,
-                ev.getDeviceId(),
-                ev.getEdgeFlags(),
-                ev.getSource(),
-                ev.getFlags() /* flags */);
-        if (DEBUG) {
-            printSamples(TAG + " generateEvent", event);
-        }
-        if (event.getPointerId(event.getActionIndex()) < 0) {
-            printSamples(TAG + "generateEvent", event);
-            throw new IllegalStateException(event.getActionIndex() + " not found in MotionEvent");
-        }
-        mListener.accept(event);
-        event.recycle();
-    }
-
-    /**
-     * Returns the description of the fingers' state expected by MotionEvent.
-     */
-    private Pair<PointerProperties[], PointerCoords[]> getFingerState() {
-        int nFingers = mFingers.size();
-
-        Pair<PointerProperties[], PointerCoords[]> result = mCache.get(nFingers);
-        PointerProperties[] properties = result.first;
-        PointerCoords[] coordinates = result.second;
-
-        int index = 0;
-        for (int i = 0; i < mFingers.size(); i++) {
-            int id = mFingers.keyAt(i);
-            PointF location = mFingers.get(id);
-
-            PointerProperties property = properties[i];
-            property.id = id;
-            property.toolType = MotionEvent.TOOL_TYPE_FINGER;
-            properties[index] = property;
-
-            PointerCoords coordinate = coordinates[i];
-            coordinate.x = location.x;
-            coordinate.y = location.y;
-            coordinate.pressure = 1.0f;
-            coordinates[index] = coordinate;
-
-            index++;
-        }
-        return mCache.get(nFingers);
-    }
-}
diff --git a/src/com/android/launcher3/util/DefaultDisplay.java b/src/com/android/launcher3/util/DefaultDisplay.java
new file mode 100644
index 0000000..7719f08
--- /dev/null
+++ b/src/com/android/launcher3/util/DefaultDisplay.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to cache properties of default display to avoid a system RPC on every call.
+ */
+public class DefaultDisplay implements DisplayListener {
+
+    public static final MainThreadInitializedObject<DefaultDisplay> INSTANCE =
+            new MainThreadInitializedObject<>(DefaultDisplay::new);
+
+    private static final String TAG = "DefaultDisplay";
+
+    public static final int CHANGE_SIZE = 1 << 0;
+    public static final int CHANGE_ROTATION = 1 << 1;
+    public static final int CHANGE_FRAME_DELAY = 1 << 2;
+
+    private final Context mContext;
+    private final int mId;
+    private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
+    private final Handler mChangeHandler;
+    private Info mInfo;
+
+    private DefaultDisplay(Context context) {
+        mContext = context;
+        mInfo = new Info(context);
+        mId = mInfo.id;
+        mChangeHandler = new Handler(this::onChange);
+
+        context.getSystemService(DisplayManager.class)
+                .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
+    }
+
+    @Override
+    public final void onDisplayAdded(int displayId) {  }
+
+    @Override
+    public final void onDisplayRemoved(int displayId) { }
+
+    @Override
+    public final void onDisplayChanged(int displayId) {
+        if (displayId != mId) {
+            return;
+        }
+
+        Info oldInfo = mInfo;
+        Info info = new Info(mContext);
+
+        int change = 0;
+        if (info.hasDifferentSize(oldInfo)) {
+            change |= CHANGE_SIZE;
+        }
+        if (oldInfo.rotation != info.rotation) {
+            change |= CHANGE_ROTATION;
+        }
+        if (info.singleFrameMs != oldInfo.singleFrameMs) {
+            change |= CHANGE_FRAME_DELAY;
+        }
+
+        if (change != 0) {
+            mInfo = info;
+            mChangeHandler.sendEmptyMessage(change);
+        }
+    }
+
+    public static int getSingleFrameMs(Context context) {
+        return INSTANCE.get(context).getInfo().singleFrameMs;
+    }
+
+    public Info getInfo() {
+        return mInfo;
+    }
+
+    public void addChangeListener(DisplayInfoChangeListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeChangeListener(DisplayInfoChangeListener listener) {
+        mListeners.remove(listener);
+    }
+
+    private boolean onChange(Message msg) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            mListeners.get(i).onDisplayInfoChanged(mInfo, msg.what);
+        }
+        return true;
+    }
+
+    public static class Info {
+
+        public final int id;
+        public final int rotation;
+        public final int singleFrameMs;
+
+        public final Point realSize;
+        public final Point smallestSize;
+        public final Point largestSize;
+
+        private Info(Context context) {
+            Display display = context.getSystemService(WindowManager.class).getDefaultDisplay();
+
+            id = display.getDisplayId();
+            rotation = display.getRotation();
+
+            float refreshRate = display.getRefreshRate();
+            singleFrameMs = refreshRate > 0 ? (int) (1000 / refreshRate) : 16;
+
+            realSize = new Point();
+            smallestSize = new Point();
+            largestSize = new Point();
+            display.getRealSize(realSize);
+            display.getCurrentSizeRange(smallestSize, largestSize);
+        }
+
+        private boolean hasDifferentSize(Info info) {
+            if (!realSize.equals(info.realSize)
+                    && !realSize.equals(info.realSize.y, info.realSize.x)) {
+                Log.d(TAG, String.format("Display size changed from %s to %s",
+                        info.realSize, realSize));
+                return true;
+            }
+
+            if (!smallestSize.equals(info.smallestSize) || !largestSize.equals(info.largestSize)) {
+                Log.d(TAG, String.format("Available size changed from [%s, %s] to [%s, %s]",
+                        smallestSize, largestSize, info.smallestSize, info.largestSize));
+                return true;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Interface for listening for display changes
+     */
+    public interface DisplayInfoChangeListener {
+
+        void onDisplayInfoChanged(Info info, int flags);
+    }
+}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index c1ba780..799762d 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -20,8 +20,7 @@
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
 
-import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
-import static com.android.launcher3.Utilities.shouldDisableGestures;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -30,7 +29,6 @@
 import android.graphics.RectF;
 import android.os.Build;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.Property;
 import android.view.MotionEvent;
 import android.view.View;
@@ -43,7 +41,6 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.util.TouchController;
@@ -152,8 +149,6 @@
     }
 
     private TouchController findControllerToHandleTouch(MotionEvent ev) {
-        if (shouldDisableGestures(ev)) return null;
-
         AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
         if (topView != null && topView.onControllerInterceptTouchEvent(ev)) {
             return topView;
@@ -221,21 +216,15 @@
         if (child instanceof AbstractFloatingView) {
             // Handles the case where the view is removed without being properly closed.
             // This can happen if something goes wrong during a state change/transition.
-            postDelayed(() -> {
-                AbstractFloatingView floatingView = (AbstractFloatingView) child;
-                if (floatingView.isOpen()) {
-                    floatingView.close(false);
-                }
-            }, SINGLE_FRAME_MS);
+            AbstractFloatingView floatingView = (AbstractFloatingView) child;
+            if (floatingView.isOpen()) {
+                postDelayed(() -> floatingView.close(false), getSingleFrameMs(getContext()));
+            }
         }
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                    "onTouchEvent " + ev);
-        }
         int action = ev.getAction();
         if (action == ACTION_UP || action == ACTION_CANCEL) {
             if (mTouchCompleteListener != null) {
@@ -245,10 +234,6 @@
         }
 
         if (mActiveController != null) {
-            if (TestProtocol.sDebugTracing) {
-                android.util.Log.d(TestProtocol.NO_DRAG_TAG,
-                        "onTouchEvent 1");
-            }
             return mActiveController.onControllerTouchEvent(ev);
         } else {
             // In case no child view handled the touch event, we may not get onIntercept anymore
@@ -258,9 +243,6 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_START_TAG, "BaseDragLayer.dispatchTouchEvent " + ev);
-        }
         switch (ev.getAction()) {
             case ACTION_DOWN: {
                 float x = ev.getX();
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index b6c4fed..e09a9e8 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA;
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.Utilities.getFullDrawable;
+import static com.android.launcher3.Utilities.isRtl;
 import static com.android.launcher3.Utilities.mapToRange;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
@@ -129,6 +130,7 @@
 
     private final Launcher mLauncher;
     private final int mBlurSizeOutline;
+    private final boolean mIsRtl;
 
     private boolean mIsVerticalBarLayout = false;
     private boolean mIsAdaptiveIcon = false;
@@ -174,6 +176,7 @@
         mLauncher = Launcher.getLauncher(context);
         mBlurSizeOutline = getResources().getDimensionPixelSize(
                 R.dimen.blur_size_medium_outline);
+        mIsRtl = Utilities.isRtl(getResources());
         mListenerView = new ListenerView(context, attrs);
 
         mFgSpringX = new SpringAnimation(this, mFgTransXProperty)
@@ -213,7 +216,10 @@
         setAlpha(alpha);
 
         LayoutParams lp = (LayoutParams) getLayoutParams();
-        float dX = rect.left - lp.leftMargin;
+        float dX = mIsRtl
+                ? rect.left
+                - (mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width)
+                : rect.left - lp.getMarginStart();
         float dY = rect.top - lp.topMargin;
         setTranslationX(dX);
         setTranslationY(dY);
@@ -323,14 +329,18 @@
         mPositionOut.set(position);
         lp.ignoreInsets = true;
         // Position the floating view exactly on top of the original
-        lp.leftMargin = Math.round(position.left);
         lp.topMargin = Math.round(position.top);
-
+        if (mIsRtl) {
+            lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - position.right));
+        } else {
+            lp.setMarginStart(Math.round(position.left));
+        }
         // Set the properties here already to make sure they are available when running the first
         // animation frame.
-        layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin
-                + lp.height);
-
+        int left = mIsRtl
+                ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
+                : lp.leftMargin;
+        layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
     }
 
     /**
@@ -514,8 +524,11 @@
             } else {
                 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
             }
-            layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin
-                    + lp.height);
+
+            int left = mIsRtl
+                    ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
+                    : lp.leftMargin;
+            layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
 
             float scale = Math.max((float) lp.height / originalHeight,
                     (float) lp.width / originalWidth);
@@ -574,17 +587,17 @@
                     if (cancellationSignal.isCanceled()) {
                         return;
                     }
-                    if (mIconLoadResult.isIconLoaded) {
-                        setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
-                                mIconLoadResult.iconOffset);
-                    }
+
+                    setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
+                            mIconLoadResult.iconOffset);
+
                     // Delay swapping views until the icon is loaded to prevent a flash.
                     setVisibility(VISIBLE);
                     originalView.setVisibility(INVISIBLE);
                 };
+                mLoadIconSignal = cancellationSignal;
             }
         }
-        mLoadIconSignal = cancellationSignal;
     }
 
     private void setBackgroundDrawableBounds(float scale) {
@@ -656,8 +669,7 @@
         canvas.restoreToCount(count);
     }
 
-    public void onListenerViewClosed() {
-        // Fast finish here.
+    public void fastFinish() {
         if (mEndRunnable != null) {
             mEndRunnable.run();
             mEndRunnable = null;
@@ -757,7 +769,7 @@
         view.setVisibility(INVISIBLE);
         parent.addView(view);
         dragLayer.addView(view.mListenerView);
-        view.mListenerView.setListener(view::onListenerViewClosed);
+        view.mListenerView.setListener(view::fastFinish);
 
         view.mEndRunnable = () -> {
             view.mEndRunnable = null;
diff --git a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
index fa23b8d..6a6916e 100644
--- a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
+++ b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java
@@ -32,13 +32,14 @@
 import android.os.ParcelFileDescriptor;
 import android.util.Base64;
 
+import androidx.test.InstrumentationRegistry;
+
+import com.android.launcher3.tapl.TestHelpers;
+
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 
-import androidx.test.InstrumentationRegistry;
-
 /**
  * Content provider to receive commands from tests
  */
@@ -47,6 +48,7 @@
     public static final String ENABLE_TEST_LAUNCHER = "enable-test-launcher";
     public static final String DISABLE_TEST_LAUNCHER = "disable-test-launcher";
     public static final String KILL_PROCESS = "kill-process";
+    public static final String GET_SYSTEM_HEALTH_MESSAGE = "get-system-health-message";
 
     @Override
     public boolean onCreate() {
@@ -99,6 +101,12 @@
                         killBackgroundProcesses(arg);
                 return null;
             }
+
+            case GET_SYSTEM_HEALTH_MESSAGE: {
+                final Bundle response = new Bundle();
+                response.putString("result", TestHelpers.getSystemHealthMessage(getContext()));
+                return response;
+            }
         }
         return super.call(method, arg, extras);
     }
@@ -122,7 +130,8 @@
             // Create an empty file so that we can pass its descriptor
             try {
                 file.createNewFile();
-            } catch (IOException e) { }
+            } catch (IOException e) {
+            }
         }
 
         return ParcelFileDescriptor.open(file, MODE_READ_WRITE);
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 4a0ca5c..8dc8cea 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -17,10 +17,10 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import static java.lang.System.exit;
 
@@ -38,10 +38,7 @@
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.Launcher;
@@ -49,13 +46,16 @@
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.MainThreadExecutor;
-import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.testcomponent.TestCommandReceiver;
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.launcher3.util.rule.LauncherActivityRule;
@@ -86,8 +86,7 @@
     public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
     public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
 
-    public static final long SHORT_UI_TIMEOUT = 300;
-    public static final long DEFAULT_UI_TIMEOUT = 10000;
+    public static final long DEFAULT_UI_TIMEOUT = 60000; // b/136278866
     private static final String TAG = "AbstractLauncherUiTest";
 
     protected MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
@@ -103,7 +102,15 @@
         } catch (RemoteException e) {
             throw new RuntimeException(e);
         }
-        if (TestHelpers.isInLauncherProcess()) Utilities.enableRunningInTestHarnessForTests();
+        if (TestHelpers.isInLauncherProcess()) {
+            Utilities.enableRunningInTestHarnessForTests();
+            mLauncher.setSystemHealthSupplier(() -> TestCommandReceiver.callCommand(
+                    TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE).getString("result"));
+            mLauncher.setOnSettledStateAction(
+                    containerType -> executeOnLauncher(
+                            launcher ->
+                                    checkLauncherIntegrity(launcher, containerType)));
+        }
     }
 
     protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
@@ -116,6 +123,23 @@
     public ShellCommandRule mDisableHeadsUpNotification =
             ShellCommandRule.disableHeadsUpNotification();
 
+    protected void clearPackageData(String pkg) throws IOException, InterruptedException {
+        final CountDownLatch count = new CountDownLatch(2);
+        final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                count.countDown();
+            }
+        };
+        mTargetContext.registerReceiver(broadcastReceiver,
+                PackageManagerHelper.getPackageFilter(pkg,
+                        Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED));
+
+        mDevice.executeShellCommand("pm clear " + pkg);
+        assertTrue(pkg + " didn't restart", count.await(10, TimeUnit.SECONDS));
+        mTargetContext.unregisterReceiver(broadcastReceiver);
+    }
+
     // Annotation for tests that need to be run in portrait and landscape modes.
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
@@ -170,38 +194,13 @@
         }
     }
 
-    protected void clearLauncherData() throws IOException {
+    protected void clearLauncherData() throws IOException, InterruptedException {
         if (TestHelpers.isInLauncherProcess()) {
             LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
                     LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
             resetLoaderState();
         } else {
-            mDevice.executeShellCommand("pm clear " + mDevice.getLauncherPackageName());
-        }
-    }
-
-    /**
-     * Scrolls the {@param container} until it finds an object matching {@param condition}.
-     *
-     * @return the matching object.
-     */
-    protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) {
-        final int margin = ResourceUtils.getNavbarSize(
-                ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1;
-        container.setGestureMargins(0, 0, 0, margin);
-
-        int i = 0;
-        for (; ; ) {
-            // findObject can only execute after spring settles.
-            mDevice.wait(Until.findObject(condition), SHORT_UI_TIMEOUT);
-            UiObject2 widget = container.findObject(condition);
-            if (widget != null && widget.getVisibleBounds().intersects(
-                    0, 0, mDevice.getDisplayWidth(),
-                    mDevice.getDisplayHeight() - margin)) {
-                return widget;
-            }
-            if (++i > 40) fail("Too many attempts");
-            container.scroll(Direction.DOWN, 1f);
+            clearPackageData(mDevice.getLauncherPackageName());
         }
     }
 
@@ -387,4 +386,68 @@
     protected int getAllAppsScroll(Launcher launcher) {
         return launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
     }
+
+    private static void checkLauncherIntegrity(
+            Launcher launcher, ContainerType expectedContainerType) {
+        if (launcher != null) {
+            final LauncherStateManager stateManager = launcher.getStateManager();
+            final LauncherState stableState = stateManager.getCurrentStableState();
+
+            assertTrue("Stable state != state: " + stableState.getClass().getSimpleName() + ", "
+                            + stateManager.getState().getClass().getSimpleName(),
+                    stableState == stateManager.getState());
+
+            final boolean isResumed = launcher.hasBeenResumed();
+            assertTrue("hasBeenResumed() != isStarted(), hasBeenResumed(): " + isResumed,
+                    isResumed == launcher.isStarted());
+            assertTrue("hasBeenResumed() != isUserActive(), hasBeenResumed(): " + isResumed,
+                    isResumed == launcher.isUserActive());
+
+            final int ordinal = stableState.ordinal;
+
+            switch (expectedContainerType) {
+                case WORKSPACE:
+                case WIDGETS: {
+                    assertTrue(
+                            "Launcher is not resumed in state: " + expectedContainerType,
+                            isResumed);
+                    assertTrue(TestProtocol.stateOrdinalToString(ordinal),
+                            ordinal == TestProtocol.NORMAL_STATE_ORDINAL);
+                    break;
+                }
+                case ALL_APPS: {
+                    assertTrue(
+                            "Launcher is not resumed in state: " + expectedContainerType,
+                            isResumed);
+                    assertTrue(TestProtocol.stateOrdinalToString(ordinal),
+                            ordinal == TestProtocol.ALL_APPS_STATE_ORDINAL);
+                    break;
+                }
+                case OVERVIEW: {
+                    assertTrue(
+                            "Launcher is not resumed in state: " + expectedContainerType,
+                            isResumed);
+                    assertTrue(TestProtocol.stateOrdinalToString(ordinal),
+                            ordinal == TestProtocol.OVERVIEW_STATE_ORDINAL);
+                    break;
+                }
+                case BACKGROUND: {
+                    assertTrue("Launcher is resumed in state: " + expectedContainerType,
+                            !isResumed);
+                    assertTrue(TestProtocol.stateOrdinalToString(ordinal),
+                            ordinal == TestProtocol.NORMAL_STATE_ORDINAL);
+                    break;
+                }
+                default:
+                    throw new IllegalArgumentException(
+                            "Illegal container: " + expectedContainerType);
+            }
+        } else {
+            assertTrue(
+                    "Container type is not BACKGROUND or FALLBACK_OVERVIEW: "
+                            + expectedContainerType,
+                    expectedContainerType == ContainerType.BACKGROUND ||
+                            expectedContainerType == ContainerType.FALLBACK_OVERVIEW);
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java b/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
index 58c74ce..a76b4a4 100644
--- a/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
+++ b/tests/src/com/android/launcher3/ui/DefaultLayoutProviderTest.java
@@ -72,7 +72,7 @@
         writeLayout(new LauncherLayoutBuilder().atHotseat(0).putApp(SETTINGS_APP, SETTINGS_APP));
 
         // Launch the home activity
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
         waitForModelLoaded();
 
         mLauncher.getWorkspace().getHotseatAppIcon(getSettingsApp().getLabel().toString());
@@ -88,7 +88,7 @@
                         info.getComponent().getClassName(), 2, 2));
 
         // Launch the home activity
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
         waitForModelLoaded();
 
         // Verify widget present
@@ -105,7 +105,7 @@
                 .build());
 
         // Launch the home activity
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
         waitForModelLoaded();
 
         mLauncher.getWorkspace().getHotseatFolder("Folder: Copy");
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 0f36292..ddcb4da 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -1,5 +1,6 @@
 package com.android.launcher3.ui;
 
+import android.util.Log;
 import android.view.Surface;
 
 import com.android.launcher3.tapl.TestHelpers;
@@ -9,6 +10,7 @@
 import org.junit.runners.model.Statement;
 
 class PortraitLandscapeRunner implements TestRule {
+    private static final String TAG = "PortraitLandscapeRunner";
     private AbstractLauncherUiTest mTest;
 
     public PortraitLandscapeRunner(AbstractLauncherUiTest test) {
@@ -36,6 +38,9 @@
 
                     evaluateInPortrait();
                     evaluateInLandscape();
+                } catch (Exception e) {
+                    Log.e(TAG, "Exception", e);
+                    throw e;
                 } finally {
                     mTest.mDevice.setOrientationNatural();
                     mTest.executeOnLauncher(launcher ->
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index c3168f8..4dab44f 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -216,7 +216,8 @@
             final AppIcon app = allApps.getAppIcon("TestActivity7");
             assertNotNull("AppIcon.launch returned null", app.launch(getAppPackageName()));
             test.executeOnLauncher(launcher -> assertTrue(
-                    "Launcher activity is the top activity; expecting another activity to be the top "
+                    "Launcher activity is the top activity; expecting another activity to be the "
+                            + "top "
                             + "one",
                     test.isInBackground(launcher)));
         } finally {
@@ -304,11 +305,8 @@
                 switchToAllApps();
         allApps.freeze();
         try {
-            allApps.
-                    getAppIcon(APP_NAME).
-                    dragToWorkspace().
-                    getWorkspaceAppIcon(APP_NAME).
-                    launch(getAppPackageName());
+            allApps.getAppIcon(APP_NAME).dragToWorkspace();
+            mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName());
         } finally {
             allApps.unfreeze();
         }
@@ -335,13 +333,8 @@
                     getMenuItem(0);
             final String shortcutName = menuItem.getText();
 
-            // 4. Drag the first shortcut to the home screen.
-            // 5. Verify that the shortcut works on home screen
-            //    (the app opens and has the same text as the shortcut).
-            menuItem.
-                    dragToWorkspace().
-                    getWorkspaceAppIcon(shortcutName).
-                    launch(getAppPackageName());
+            menuItem.dragToWorkspace();
+            mLauncher.getWorkspace().getWorkspaceAppIcon(shortcutName).launch(getAppPackageName());
         } finally {
             allApps.unfreeze();
         }
diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
index d13d319..d0df664 100644
--- a/tests/src/com/android/launcher3/ui/TestViewHelpers.java
+++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
@@ -18,28 +18,15 @@
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 import static androidx.test.InstrumentationRegistry.getTargetContext;
 
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
 import android.content.ComponentName;
-import android.content.Context;
-import android.graphics.Point;
 import android.os.Process;
-import android.os.SystemClock;
 import android.util.Log;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiDevice;
-import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.R;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.testcomponent.AppWidgetNoConfig;
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
@@ -50,21 +37,6 @@
 public class TestViewHelpers {
     private static final String TAG = "TestViewHelpers";
 
-    private static UiDevice getDevice() {
-        return UiDevice.getInstance(getInstrumentation());
-    }
-
-    public static UiObject2 findViewById(int id) {
-        return getDevice().wait(Until.findObject(getSelectorForId(id)),
-                AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT);
-    }
-
-    public static BySelector getSelectorForId(int id) {
-        final Context targetContext = getTargetContext();
-        String name = targetContext.getResources().getResourceEntryName(id);
-        return By.res(targetContext.getPackageName(), name);
-    }
-
     /**
      * Finds a widget provider which can fit on the home screen.
      *
@@ -91,92 +63,6 @@
         return info;
     }
 
-    /**
-     * Drags an icon to the center of homescreen.
-     *
-     * @param icon object that is either app icon or shortcut icon
-     */
-    public static void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) {
-        Point center = icon.getVisibleCenter();
-
-        // Action Down
-        final long downTime = SystemClock.uptimeMillis();
-        sendPointer(downTime, MotionEvent.ACTION_DOWN, center);
-
-        UiObject2 dragLayer = findViewById(R.id.drag_layer);
-
-        if (expectedToShowShortcuts) {
-            // Make sure shortcuts show up, and then move a bit to hide them.
-            assertNotNull(findViewById(R.id.deep_shortcuts_container));
-
-            Point moveLocation = new Point(center);
-            int distanceToMove =
-                    getTargetContext().getResources().getDimensionPixelSize(
-                            R.dimen.deep_shortcuts_start_drag_threshold) + 50;
-            if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) {
-                moveLocation.y -= distanceToMove;
-            } else {
-                moveLocation.y += distanceToMove;
-            }
-            movePointer(downTime, center, moveLocation);
-
-            assertNull(findViewById(R.id.deep_shortcuts_container));
-        }
-
-        // Wait until Remove/Delete target is visible
-        assertNotNull(findViewById(R.id.delete_target_text));
-
-        Point moveLocation = dragLayer.getVisibleCenter();
-
-        // Move to center
-        movePointer(downTime, center, moveLocation);
-        sendPointer(downTime, MotionEvent.ACTION_UP, moveLocation);
-
-        // Wait until remove target is gone.
-        getDevice().wait(Until.gone(getSelectorForId(R.id.delete_target_text)),
-                AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT);
-    }
-
-    private static void movePointer(long downTime, Point from, Point to) {
-        while (!from.equals(to)) {
-            try {
-                Thread.sleep(20);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
-            from.x = getNextMoveValue(to.x, from.x);
-            from.y = getNextMoveValue(to.y, from.y);
-            sendPointer(downTime, MotionEvent.ACTION_MOVE, from);
-        }
-    }
-
-    private static int getNextMoveValue(int targetValue, int oldValue) {
-        if (targetValue - oldValue > 10) {
-            return oldValue + 10;
-        } else if (targetValue - oldValue < -10) {
-            return oldValue - 10;
-        } else {
-            return targetValue;
-        }
-    }
-
-    public static void sendPointer(long downTime, int action, Point point) {
-        MotionEvent event = MotionEvent.obtain(downTime,
-                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
-        getInstrumentation().sendPointerSync(event);
-        event.recycle();
-    }
-
-    /**
-     * Opens widget tray and returns the recycler view.
-     */
-    public static UiObject2 openWidgetsTray() {
-        final UiDevice device = getDevice();
-        device.pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
-        device.waitForIdle();
-        return findViewById(R.id.widgets_list_view);
-    }
-
     public static View findChildView(ViewGroup parent, Function<View, Boolean> condition) {
         for (int i = 0; i < parent.getChildCount(); ++i) {
             final View child = parent.getChildAt(i);
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index c93c20a..d9edc35 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -53,8 +53,8 @@
 
     @Test
     public void workTabExists() {
-        mActivityMonitor.startLauncher();
-
+        mDevice.pressHome();
+        waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
 
         /*
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 5eb5f19..3206a69 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -27,20 +27,18 @@
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.UiObject2;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.testcomponent.WidgetConfigActivity;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.Condition;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.widget.WidgetCell;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -71,7 +69,6 @@
     }
 
     @Test
-    // Convert test to TAPL b/131116002
     public void testWidgetConfig() throws Throwable {
         runTest(false, true);
     }
@@ -83,7 +80,6 @@
     }
 
     @Test
-    // Convert test to TAPL b/131116002
     public void testConfigCancelled() throws Throwable {
         runTest(false, false);
     }
@@ -102,17 +98,15 @@
         lockRotation(true);
 
         clearHomescreen();
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
 
-        // Open widget tray and wait for load complete.
-        final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray();
-        Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT);
+        final Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
 
         // Drag widget to homescreen
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
-        UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
-                .hasDescendant(By.text(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))));
-        TestViewHelpers.dragToWorkspace(widget, false);
+        widgets.
+                getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager())).
+                dragToWorkspace();
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
 
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 0061568..276c614 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -19,20 +19,12 @@
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.UiObject2;
-import android.view.View;
 
-import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.Workspace.ItemOperator;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
-import com.android.launcher3.util.Condition;
-import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.widget.WidgetCell;
 
 import org.junit.Ignore;
 import org.junit.Rule;
@@ -61,30 +53,22 @@
         performTest();
     }
 
-    // Convert to TAPL b/131116002
     private void performTest() throws Throwable {
         clearHomescreen();
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
 
         final LauncherAppWidgetProviderInfo widgetInfo =
                 TestViewHelpers.findWidgetProvider(this, false /* hasConfigureScreen */);
 
-        // Open widget tray and wait for load complete.
-        final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray();
-        Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT);
+        mLauncher.
+                getWorkspace().
+                openAllWidgets().
+                getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager())).
+                dragToWorkspace();
 
-        // Drag widget to homescreen
-        UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
-                .hasDescendant(By.text(widgetInfo.getLabel(mTargetContext.getPackageManager()))));
-        TestViewHelpers.dragToWorkspace(widget, false);
-
-        assertTrue(mActivityMonitor.itemExists(new ItemOperator() {
-            @Override
-            public boolean evaluate(ItemInfo info, View view) {
-                return info instanceof LauncherAppWidgetInfo &&
+        assertTrue(mActivityMonitor.itemExists(
+                (info, view) -> info instanceof LauncherAppWidgetInfo &&
                         ((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
-                                widgetInfo.provider.getClassName());
-            }
-        }).call());
+                                widgetInfo.provider.getClassName())).call());
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index d36126b..3a7df64 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -267,7 +267,7 @@
         resetLoaderState();
 
         // Launch the home activity
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
         waitForModelLoaded();
     }
 
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 6122dae..a9a5090 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -131,7 +131,7 @@
         lockRotation(true);
 
         clearHomescreen();
-        mActivityMonitor.startLauncher();
+        mDevice.pressHome();
 
         // Open Pin item activity
         BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
diff --git a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
index 2aba7a5..2042403 100644
--- a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
@@ -15,11 +15,6 @@
  */
 package com.android.launcher3.util.rule;
 
-import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage;
-
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-import static androidx.test.InstrumentationRegistry.getTargetContext;
-
 import android.app.Activity;
 import android.app.Application;
 import android.app.Application.ActivityLifecycleCallbacks;
@@ -52,26 +47,15 @@
     }
 
     public Callable<Boolean> itemExists(final ItemOperator op) {
-        return new Callable<Boolean>() {
-
-            @Override
-            public Boolean call() throws Exception {
-                Launcher launcher = getActivity();
-                if (launcher == null) {
-                    return false;
-                }
-                return launcher.getWorkspace().getFirstMatch(op) != null;
+        return () -> {
+            Launcher launcher = getActivity();
+            if (launcher == null) {
+                return false;
             }
+            return launcher.getWorkspace().getFirstMatch(op) != null;
         };
     }
 
-    /**
-     * Starts the launcher activity in the target package.
-     */
-    public void startLauncher() {
-        getInstrumentation().startActivitySync(getHomeIntentInPackage(getTargetContext()));
-    }
-
     private class MyStatement extends Statement implements ActivityLifecycleCallbacks {
 
         private final Statement mBase;
diff --git a/tests/tapl/AndroidManifest.xml b/tests/tapl/AndroidManifest.xml
index 0207e2b..1065446 100644
--- a/tests/tapl/AndroidManifest.xml
+++ b/tests/tapl/AndroidManifest.xml
@@ -23,4 +23,6 @@
 >
 
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+    <uses-permission android:name="android.permission.READ_LOGS"/>
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
 </manifest>
diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
index 7f561a2..03d1600 100644
--- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
+++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java
@@ -16,10 +16,16 @@
 
 package com.android.launcher3.tapl;
 
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
 
+import java.util.regex.Pattern;
+
 public class AddToHomeScreenPrompt {
+    private static final Pattern ADD_AUTOMATICALLY =
+            Pattern.compile("^Add automatically$", CASE_INSENSITIVE);
     private final LauncherInstrumentation mLauncher;
     private final UiObject2 mWidgetCell;
 
@@ -33,9 +39,6 @@
     public void addAutomatically() {
         mLauncher.waitForObjectInContainer(
                 mWidgetCell.getParent().getParent().getParent().getParent(),
-                By.text(LauncherInstrumentation.isAvd()
-                        ? "ADD AUTOMATICALLY"
-                        : "Add automatically")).
-                click();
+                By.text(ADD_AUTOMATICALLY)).click();
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index c9eaf27..bcce8ef 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -54,12 +54,12 @@
                 "want to switch from background to overview")) {
             verifyActiveContainer();
             goToOverviewUnchecked(BACKGROUND_APP_STATE_ORDINAL);
-            return new BaseOverview(mLauncher);
+            return mLauncher.isFallbackOverview() ?
+                    new BaseOverview(mLauncher) : new Overview(mLauncher);
         }
     }
 
     protected void goToOverviewUnchecked(int expectedState) {
-        mLauncher.getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
         switch (mLauncher.getNavigationModel()) {
             case ZERO_BUTTON: {
                 final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
@@ -112,7 +112,6 @@
                 mLauncher.waitForSystemUiObject("recent_apps").click();
                 break;
         }
-        mLauncher.getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
     }
 
     protected String getSwipeHeightRequestName() {
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index ace49e9..bbd2c29 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -16,11 +16,15 @@
 
 package com.android.launcher3.tapl;
 
+import android.graphics.Rect;
+
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.TestProtocol;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -28,7 +32,6 @@
  * Common overview pane for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
-    private static final int FLING_SPEED = LauncherInstrumentation.isAvd() ? 500 : 1500;
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
 
     BaseOverview(LauncherInstrumentation launcher) {
@@ -38,7 +41,7 @@
 
     @Override
     protected LauncherInstrumentation.ContainerType getContainerType() {
-        return LauncherInstrumentation.ContainerType.BASE_OVERVIEW;
+        return LauncherInstrumentation.ContainerType.FALLBACK_OVERVIEW;
     }
 
     /**
@@ -49,9 +52,10 @@
                      mLauncher.addContextLayer("want to fling forward in overview")) {
             LauncherInstrumentation.log("Overview.flingForward before fling");
             final UiObject2 overview = verifyActiveContainer();
-            overview.setGestureMargins(mLauncher.getEdgeSensitivityWidth(), 0, 0, 0);
-            overview.fling(Direction.LEFT, (int) (FLING_SPEED * mLauncher.getDisplayDensity()));
-            mLauncher.waitForIdle();
+            final int leftMargin = mLauncher.getTestInfo(
+                    TestProtocol.REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN).
+                    getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+            mLauncher.scroll(overview, Direction.LEFT, 1, new Rect(leftMargin, 0, 0, 0), 20);
             verifyActiveContainer();
         }
     }
@@ -86,9 +90,10 @@
                      mLauncher.addContextLayer("want to fling backward in overview")) {
             LauncherInstrumentation.log("Overview.flingBackward before fling");
             final UiObject2 overview = verifyActiveContainer();
-            overview.setGestureMargins(0, 0, mLauncher.getEdgeSensitivityWidth(), 0);
-            overview.fling(Direction.RIGHT, (int) (FLING_SPEED * mLauncher.getDisplayDensity()));
-            mLauncher.waitForIdle();
+            final int rightMargin = mLauncher.getTestInfo(
+                    TestProtocol.REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN).
+                    getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+            mLauncher.scroll(overview, Direction.RIGHT, 1, new Rect(0, 0, rightMargin, 0), 20);
             verifyActiveContainer();
         }
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index d4bdafa..82af7b0 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -53,11 +53,9 @@
     private Background launch(BySelector selector) {
         LauncherInstrumentation.log("Launchable.launch before click " +
                 mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
-        mLauncher.getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
         mLauncher.assertTrue(
                 "Launching an app didn't open a new window: " + mObject.getText(),
                 mObject.clickAndWait(Until.newWindow(), WAIT_TIME_MS));
-        mLauncher.getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
         mLauncher.assertTrue(
                 "App didn't start: " + selector,
                 mLauncher.getDevice().wait(Until.hasObject(selector),
@@ -68,7 +66,7 @@
     /**
      * Drags an object to the center of homescreen.
      */
-    public Workspace dragToWorkspace() {
+    public void dragToWorkspace() {
         final Point launchableCenter = getObject().getVisibleCenter();
         final Point displaySize = mLauncher.getRealDisplaySize();
         final int width = displaySize.x / 2;
@@ -80,10 +78,6 @@
                                 launchableCenter.x - width / 2 : launchableCenter.x + width / 2,
                         displaySize.y / 2),
                 getLongPressIndicator());
-        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                "dragged launchable to workspace")) {
-            return new Workspace(mLauncher);
-        }
     }
 
     protected abstract String getLongPressIndicator();
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index a7e6336..fe7401c 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -72,6 +72,8 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 /**
  * The main tapl object. The only object that can be explicitly constructed by the using code. It
@@ -85,8 +87,8 @@
 
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
-    enum ContainerType {
-        WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, BASE_OVERVIEW
+    public enum ContainerType {
+        WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, FALLBACK_OVERVIEW
     }
 
     public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
@@ -132,6 +134,9 @@
     private int mExpectedRotation = Surface.ROTATION_0;
     private final Uri mTestProviderUri;
     private final Deque<String> mDiagnosticContext = new LinkedList<>();
+    private Supplier<String> mSystemHealthSupplier;
+
+    private Consumer<ContainerType> mOnSettledStateAction;
 
     /**
      * Constructs the root of TAPL hierarchy. You get all other objects from it.
@@ -206,7 +211,7 @@
         try {
             // Workaround, use constructed context because both the instrumentation context and the
             // app context are not constructed with resources that take overlays into account
-            final Context ctx = baseContext.createPackageContext("android", 0);
+            final Context ctx = baseContext.createPackageContext(getLauncherPackageName(), 0);
             for (int i = 0; i < 100; ++i) {
                 final int currentInteractionMode = getCurrentInteractionMode(ctx);
                 final NavigationModel model = getNavigationModel(currentInteractionMode);
@@ -263,10 +268,79 @@
         }
     }
 
+    private String getAnomalyMessage() {
+        UiObject2 object = mDevice.findObject(By.res("android", "alertTitle"));
+        if (object != null) {
+            return "System alert popup is visible: " + object.getText();
+        }
+
+        object = mDevice.findObject(By.res("android", "message"));
+        if (object != null) {
+            return "Message popup by " + object.getApplicationPackage() + " is visible: "
+                    + object.getText();
+        }
+
+        if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
+
+        if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
+
+        return null;
+    }
+
+    private String getVisibleStateMessage() {
+        if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
+        if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
+        if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
+        if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
+        return "Background";
+    }
+
+    public void setSystemHealthSupplier(Supplier<String> supplier) {
+        this.mSystemHealthSupplier = supplier;
+    }
+
+    public void setOnSettledStateAction(Consumer<ContainerType> onSettledStateAction) {
+        mOnSettledStateAction = onSettledStateAction;
+    }
+
+    private String getSystemHealthMessage() {
+        final String testPackage = getContext().getPackageName();
+        try {
+            mDevice.executeShellCommand("pm grant " + testPackage +
+                    " android.permission.READ_LOGS");
+            mDevice.executeShellCommand("pm grant " + testPackage +
+                    " android.permission.PACKAGE_USAGE_STATS");
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        return mSystemHealthSupplier != null
+                ? mSystemHealthSupplier.get()
+                : TestHelpers.getSystemHealthMessage(getContext());
+    }
+
     private void fail(String message) {
-        log("Hierarchy dump for: " + getContextDescription() + message);
+        message = "http://go/tapl : " + getContextDescription() + message;
+
+        final String anomaly = getAnomalyMessage();
+        if (anomaly != null) {
+            message = anomaly + ", which causes:\n" + message;
+        } else {
+            message = message + " (visible state: " + getVisibleStateMessage() + ")";
+        }
+
+        final String systemHealth = getSystemHealthMessage();
+        if (systemHealth != null) {
+            message = message
+                    + ", which might be a consequence of system health "
+                    + "problems:\n<<<<<<<<<<<<<<<<<<\n"
+                    + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+        }
+
+        log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
-        Assert.fail("http://go/tapl : " + getContextDescription() + message);
+
+        Assert.fail(message);
     }
 
     private String getContextDescription() {
@@ -331,12 +405,33 @@
     }
 
     private UiObject2 verifyContainerType(ContainerType containerType) {
+        waitForLauncherInitialized();
+
         assertEquals("Unexpected display rotation",
                 mExpectedRotation, mDevice.getDisplayRotation());
+
+        // b/136278866
+        for (int i = 0; i != 100; ++i) {
+            if (getNavigationModeMismatchError() == null) break;
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+
         final String error = getNavigationModeMismatchError();
         assertTrue(error, error == null);
         log("verifyContainerType: " + containerType);
 
+        final UiObject2 container = verifyVisibleObjects(containerType);
+
+        if (mOnSettledStateAction != null) mOnSettledStateAction.accept(containerType);
+
+        return container;
+    }
+
+    private UiObject2 verifyVisibleObjects(ContainerType containerType) {
         try (Closable c = addContextLayer(
                 "but the current state is not " + containerType.name())) {
             switch (containerType) {
@@ -373,7 +468,7 @@
 
                     return waitForLauncherObject(OVERVIEW_RES_ID);
                 }
-                case BASE_OVERVIEW: {
+                case FALLBACK_OVERVIEW: {
                     return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
                 }
                 case BACKGROUND: {
@@ -390,6 +485,18 @@
         }
     }
 
+    private void waitForLauncherInitialized() {
+        for (int i = 0; i < 100; ++i) {
+            if (getTestInfo(
+                    TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
+                    getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
+                return;
+            }
+            SystemClock.sleep(100);
+        }
+        fail("Launcher didn't initialize");
+    }
+
     Parcelable executeAndWaitForEvent(Runnable command,
             UiAutomation.AccessibilityEventFilter eventFilter, String message) {
         try {
@@ -614,12 +721,12 @@
 
     @NonNull
     UiObject2 waitForLauncherObject(BySelector selector) {
-        return waitForObjectBySelector(selector.pkg(getLauncherPackageName()));
+        return waitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()));
     }
 
     @NonNull
     UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) {
-        return tryWaitForObjectBySelector(selector.pkg(getLauncherPackageName()), timeout);
+        return tryWaitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()), timeout);
     }
 
     @NonNull
@@ -649,6 +756,10 @@
         return mDevice.getLauncherPackageName();
     }
 
+    boolean isFallbackOverview() {
+        return !getOverviewPackageName().equals(getLauncherPackageName());
+    }
+
     @NonNull
     public UiDevice getDevice() {
         return mDevice;
@@ -684,7 +795,7 @@
                 startX = endX = rect.centerX();
                 final int vertCenter = rect.centerY();
                 final float halfGestureHeight = rect.height() * percent / 2.0f;
-                startY = (int) (vertCenter - halfGestureHeight);
+                startY = (int) (vertCenter - halfGestureHeight) + 1;
                 endY = (int) (vertCenter + halfGestureHeight);
             }
             break;
@@ -692,10 +803,26 @@
                 startX = endX = rect.centerX();
                 final int vertCenter = rect.centerY();
                 final float halfGestureHeight = rect.height() * percent / 2.0f;
-                startY = (int) (vertCenter + halfGestureHeight);
+                startY = (int) (vertCenter + halfGestureHeight) - 1;
                 endY = (int) (vertCenter - halfGestureHeight);
             }
             break;
+            case LEFT: {
+                startY = endY = rect.centerY();
+                final int horizCenter = rect.centerX();
+                final float halfGestureWidth = rect.width() * percent / 2.0f;
+                startX = (int) (horizCenter - halfGestureWidth) + 1;
+                endX = (int) (horizCenter + halfGestureWidth);
+            }
+            break;
+            case RIGHT: {
+                startY = endY = rect.centerY();
+                final int horizCenter = rect.centerX();
+                final float halfGestureWidth = rect.width() * percent / 2.0f;
+                startX = (int) (horizCenter + halfGestureWidth) - 1;
+                endX = (int) (horizCenter - halfGestureWidth);
+            }
+            break;
             default:
                 fail("Unsupported direction");
                 return;
@@ -711,6 +838,7 @@
     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
     // fixed interval each time.
     void linearGesture(int startX, int startY, int endX, int endY, int steps) {
+        log("linearGesture: " + startX + ", " + startY + " -> " + endX + ", " + endY);
         final long downTime = SystemClock.uptimeMillis();
         final Point start = new Point(startX, startY);
         final Point end = new Point(endX, endY);
@@ -812,7 +940,7 @@
     int getEdgeSensitivityWidth() {
         try {
             final Context context = mInstrumentation.getTargetContext().createPackageContext(
-                    "android", 0);
+                    getLauncherPackageName(), 0);
             return context.getResources().getDimensionPixelSize(
                     getSystemDimensionResId(context, "config_backGestureInset")) + 1;
         } catch (PackageManager.NameNotFoundException e) {
@@ -826,4 +954,12 @@
         getContext().getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(size);
         return size;
     }
+
+    public void enableDebugTracing() {
+        getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
+    }
+
+    public void disableDebugTracing() {
+        getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 058831f..da68da3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -19,9 +19,9 @@
 import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL;
 
 import androidx.annotation.NonNull;
-import androidx.test.uiautomator.UiObject2;
 
 import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
+import com.android.launcher3.testing.TestProtocol;
 
 /**
  * Overview pane.
@@ -51,11 +51,15 @@
 
             // Swipe from an app icon to the top.
             LauncherInstrumentation.log("Overview.switchToAllApps before swipe");
-            final UiObject2 allApps = mLauncher.waitForLauncherObject("apps_view");
-            mLauncher.swipeToState(mLauncher.getDevice().getDisplayWidth() / 2,
-                    allApps.getVisibleBounds().top,
+            mLauncher.swipeToState(
                     mLauncher.getDevice().getDisplayWidth() / 2,
-                    0, 50, ALL_APPS_STATE_ORDINAL);
+                    mLauncher.getTestInfo(
+                            TestProtocol.REQUEST_HOTSEAT_TOP).
+                            getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD),
+                    mLauncher.getDevice().getDisplayWidth() / 2,
+                    0,
+                    50,
+                    ALL_APPS_STATE_ORDINAL);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped all way up from overview")) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 641c413..6e33322 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -64,14 +64,12 @@
      */
     public Background open() {
         verifyActiveContainer();
-        mLauncher.getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "clicking an overview task")) {
             mLauncher.assertTrue("Launching task didn't open a new window: " +
                             mTask.getParent().getContentDescription(),
                     mTask.clickAndWait(Until.newWindow(), WAIT_TIME_MS));
         }
-        mLauncher.getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
         return new Background(mLauncher);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
index 93554d2..399c59d 100644
--- a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
+++ b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
@@ -26,7 +26,11 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
+import android.os.DropBoxManager;
 
+import org.junit.Assert;
+
+import java.util.Date;
 import java.util.List;
 
 public class TestHelpers {
@@ -81,4 +85,70 @@
         }
         return "com.android.systemui";
     }
+
+    private static String truncateCrash(String text, int maxLines) {
+        String[] lines = text.split("\\r?\\n");
+        StringBuilder ret = new StringBuilder();
+        for (int i = 0; i < maxLines && i < lines.length; i++) {
+            ret.append(lines[i]);
+            ret.append('\n');
+        }
+        if (lines.length > maxLines) {
+            ret.append("... ");
+            ret.append(lines.length - maxLines);
+            ret.append(" more lines truncated ...\n");
+        }
+        return ret.toString();
+    }
+
+    private static String checkCrash(Context context, String label) {
+        DropBoxManager dropbox = (DropBoxManager) context.getSystemService(Context.DROPBOX_SERVICE);
+        Assert.assertNotNull("Unable access the DropBoxManager service", dropbox);
+
+        long timestamp = System.currentTimeMillis() - 5 * 60000;
+        DropBoxManager.Entry entry;
+        StringBuilder errorDetails = new StringBuilder();
+        while (null != (entry = dropbox.getNextEntry(label, timestamp))) {
+            if (errorDetails.length() != 0) errorDetails.append("------------------------------");
+            timestamp = entry.getTimeMillis();
+            errorDetails.append(new Date(timestamp));
+            errorDetails.append(": ");
+            errorDetails.append(entry.getTag());
+            errorDetails.append(": ");
+            final String dropboxSnippet = entry.getText(4096);
+            if (dropboxSnippet != null) errorDetails.append(truncateCrash(dropboxSnippet, 40));
+            errorDetails.append("    ...\n");
+            entry.close();
+        }
+        return errorDetails.length() != 0 ? errorDetails.toString() : null;
+    }
+
+    public static String getSystemHealthMessage(Context context) {
+        try {
+            StringBuilder errors = new StringBuilder();
+
+            final String[] labels = {
+                    "system_app_anr",
+                    "system_app_crash",
+                    "system_app_native_crash",
+                    "system_app_wtf",
+                    "system_server_anr",
+                    "system_server_crash",
+                    "system_server_native_crash",
+                    "system_server_watchdog",
+            };
+
+            for (String label : labels) {
+                final String crash = checkCrash(context, label);
+                if (crash != null) errors.append(crash);
+            }
+
+            return errors.length() != 0
+                    ? "Current time: " + new Date(System.currentTimeMillis()) + "\n" + errors
+                    : null;
+        } catch (Exception e) {
+            return "Failed to get system health diags, maybe build your test via .bp instead of "
+                    + ".mk? " + android.util.Log.getStackTraceString(e);
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 128789d..1b6d8c4 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -18,7 +18,16 @@
 
 import androidx.test.uiautomator.UiObject2;
 
-public class Widget {
-    Widget(LauncherInstrumentation launcher, UiObject2 widget) {
+/**
+ * Widget in workspace or a widget list.
+ */
+public final class Widget extends Launchable {
+    Widget(LauncherInstrumentation launcher, UiObject2 icon) {
+        super(launcher, icon);
+    }
+
+    @Override
+    protected String getLongPressIndicator() {
+        return "drop_target_bar";
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 94003be..2495933 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -16,6 +16,12 @@
 
 package com.android.launcher3.tapl;
 
+import static org.junit.Assert.fail;
+
+import android.graphics.Point;
+
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
 
@@ -75,4 +81,27 @@
     protected LauncherInstrumentation.ContainerType getContainerType() {
         return LauncherInstrumentation.ContainerType.WIDGETS;
     }
+
+    public Widget getWidget(String label) {
+        final int margin = ResourceUtils.getNavbarSize(
+                ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1;
+        final UiObject2 widgetsContainer = verifyActiveContainer();
+        widgetsContainer.setGestureMargins(0, 0, 0, margin);
+
+        final Point displaySize = mLauncher.getRealDisplaySize();
+
+        int i = 0;
+        final BySelector selector = By.
+                clazz("com.android.launcher3.widget.WidgetCell").
+                hasDescendant(By.text(label));
+
+        for (; ; ) {
+            final UiObject2 widget = mLauncher.tryWaitForLauncherObject(selector, 300);
+            if (widget != null && widget.getVisibleBounds().bottom <= displaySize.y - margin) {
+                return new Widget(mLauncher, widget);
+            }
+            if (++i > 40) fail("Too many attempts");
+            widgetsContainer.scroll(Direction.DOWN, 1f);
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index b01b6f3..07f8b64 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -67,7 +67,6 @@
                     "switchToAllApps: swipeHeight = " + swipeHeight + ", slop = "
                             + mLauncher.getTouchSlop());
 
-            mLauncher.getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
             mLauncher.swipeToState(
                     start.x,
                     start.y,
@@ -75,7 +74,6 @@
                     start.y - swipeHeight - mLauncher.getTouchSlop(),
                     60,
                     ALL_APPS_STATE_ORDINAL);
-            mLauncher.getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "swiped to all apps")) {
@@ -157,7 +155,6 @@
     static void dragIconToWorkspace(
             LauncherInstrumentation launcher, Launchable launchable, Point dest,
             String longPressIndicator) {
-        launcher.getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING);
         LauncherInstrumentation.log("dragIconToWorkspace: begin");
         final Point launchableCenter = launchable.getObject().getVisibleCenter();
         final long downTime = SystemClock.uptimeMillis();
@@ -172,7 +169,6 @@
                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest);
         LauncherInstrumentation.log("dragIconToWorkspace: end");
         launcher.waitUntilGone("drop_target_bar");
-        launcher.getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING);
     }
 
     /**