Merge "Update nav button color when nav button layout changes" into udc-qpr-dev
diff --git a/Android.bp b/Android.bp
index 9b696a2..316f9c0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -153,7 +153,8 @@
         "androidx.cardview_cardview",
         "com.google.android.material_material",
         "iconloader_base",
-        "view_capture"
+        "view_capture",
+        "animationlib"
     ],
     manifest: "AndroidManifest-common.xml",
     sdk_version: "current",
diff --git a/res/interpolator/standard_decelerate.xml b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
similarity index 65%
rename from res/interpolator/standard_decelerate.xml
rename to quickstep/res/drawable/bg_bubble_dismiss_circle.xml
index 579f4f5..b793eec 100644
--- a/res/interpolator/standard_decelerate.xml
+++ b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ Copyright (C) 2023 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.
@@ -15,8 +15,13 @@
   ~ limitations under the License.
   -->
 
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0"
-    android:controlY1="0"
-    android:controlX2="0"
-    android:controlY2="1"/>
\ No newline at end of file
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+
+    <stroke
+        android:width="2dp"
+        android:color="@android:color/system_accent1_600" />
+
+    <solid android:color="@android:color/system_accent1_600" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_bubble_dismiss_white.xml b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
new file mode 100644
index 0000000..b15111b
--- /dev/null
+++ b/quickstep/res/drawable/ic_bubble_dismiss_white.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2023 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+        android:fillColor="@android:color/system_neutral1_50"/>
+</vector>
diff --git a/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml b/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml
deleted file mode 100644
index 70c4231..0000000
--- a/quickstep/res/interpolator/three_point_fast_out_extra_slow_in.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2021, 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.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
diff --git a/quickstep/res/layout/transient_taskbar.xml b/quickstep/res/layout/transient_taskbar.xml
index 7a6d16a..0890a4e 100644
--- a/quickstep/res/layout/transient_taskbar.xml
+++ b/quickstep/res/layout/transient_taskbar.xml
@@ -49,6 +49,7 @@
         android:visibility="gone"
         android:gravity="center"
         android:clipChildren="false"
+        android:elevation="@dimen/bubblebar_elevation"
         />
 
     <FrameLayout
diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml
index d0fed79..1603fac 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -72,7 +72,7 @@
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Prøv at holde fingeren nede på vinduet i længere tid, inden du løfter den"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Stryg lige opad, og hold derefter fingeren stille"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Du har lært, hvordan du bruger bevægelser. Du kan aktivere bevægelser i Indstillinger."</string>
-    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har fuldført bevægelsen for Skift app"</string>
+    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har fuldført bevægelsen for at skifte mellem apps"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Stryg for at skifte app"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Skift mellem apps ved at stryge opad fra bunden af skærmen, holde fingeren stille og løfte den."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Skift mellem apps ved at stryge opad fra bunden af skærmen med 2 fingre, holde dem nede og slippe."</string>
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index dde0177..878c062 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -70,7 +70,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index dde0177..878c062 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -70,7 +70,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index dde0177..878c062 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -70,7 +70,7 @@
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
-    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+    <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
     <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 88f1478..902e0c3 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -363,6 +363,7 @@
     <dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
     <dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
     <dimen name="bubblebar_pointer_size">8dp</dimen>
+    <dimen name="bubblebar_elevation">1dp</dimen>
 
     <dimen name="bubblebar_icon_size">50dp</dimen>
     <dimen name="bubblebar_badge_size">24dp</dimen>
@@ -371,6 +372,13 @@
     <dimen name="bubblebar_icon_spacing">3dp</dimen>
     <dimen name="bubblebar_icon_elevation">1dp</dimen>
 
+    <!-- Bubble bar dismiss view -->
+    <dimen name="bubblebar_dismiss_target_size">96dp</dimen>
+    <dimen name="bubblebar_dismiss_target_small_size">60dp</dimen>
+    <dimen name="bubblebar_dismiss_target_icon_size">24dp</dimen>
+    <dimen name="bubblebar_dismiss_target_bottom_margin">50dp</dimen>
+    <dimen name="bubblebar_dismiss_floating_gradient_height">548dp</dimen>
+
     <!-- Launcher splash screen -->
     <!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
     <!--     starting_surface_exit_animation_window_shift_length -->
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 21f4e9b..110d275 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -183,7 +183,7 @@
     private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
             "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
 
-    private static final long APP_LAUNCH_DURATION = 500;
+    public static final long APP_LAUNCH_DURATION = 500;
 
     private static final long APP_LAUNCH_ALPHA_DURATION = 50;
     private static final long APP_LAUNCH_ALPHA_START_DELAY = 25;
@@ -286,7 +286,7 @@
 
         mOpeningXInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.app_open_x);
         mOpeningInterpolator = AnimationUtils.loadInterpolator(context,
-                R.interpolator.three_point_fast_out_extra_slow_in);
+                R.interpolator.emphasized_interpolator);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
index bd47923..5691ecf 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -19,7 +19,6 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_DENY;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_EDU_SEEN;
 
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -30,7 +29,6 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
-import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
@@ -164,10 +162,7 @@
             return;
         }
         mIsOpen = true;
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimator().start();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index d379d6d..882682d 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -193,5 +193,6 @@
         writer.println(prefix + "\tmIgnoreStateChangesDuringMultiWindowAnimation="
                 + mIgnoreStateChangesDuringMultiWindowAnimation);
         writer.println(prefix + "\tmPauseBlurs=" + mPauseBlurs);
+        writer.println(prefix + "\tmWaitingOnSurfaceValidity=" + mWaitingOnSurfaceValidity);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 7283a18..ecf483c 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.quickstep.GestureState;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.views.DesktopAppSelectView;
 import com.android.wm.shell.desktopmode.IDesktopTaskListener;
@@ -39,7 +40,8 @@
 
     private static final String TAG = "DesktopVisController";
     private static final boolean DEBUG = false;
-
+    private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_stashing", false);
     private final Launcher mLauncher;
 
     private boolean mFreeformTasksVisible;
@@ -73,6 +75,9 @@
 
             @Override
             public void onStashedChanged(int displayId, boolean stashed) {
+                if (!IS_STASHING_ENABLED) {
+                    return;
+                }
                 MAIN_EXECUTOR.execute(() -> {
                     if (displayId == mLauncher.getDisplayId()) {
                         if (DEBUG) {
@@ -166,20 +171,40 @@
     /**
      * Whether recents gesture is currently in progress.
      */
-    public boolean isGestureInProgress() {
+    public boolean isRecentsGestureInProgress() {
         return mGestureInProgress;
     }
 
     /**
-     * Sets whether recents gesture is in progress.
+     * Notify controller that recents gesture has started.
      */
-    public void setGestureInProgress(boolean gestureInProgress) {
-        if (DEBUG) {
-            Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress);
-        }
+    public void setRecentsGestureStart() {
         if (!isDesktopModeSupported()) {
             return;
         }
+        setRecentsGestureInProgress(true);
+    }
+
+    /**
+     * Notify controller that recents gesture finished with the given
+     * {@link com.android.quickstep.GestureState.GestureEndTarget}
+     */
+    public void setRecentsGestureEnd(@Nullable GestureState.GestureEndTarget endTarget) {
+        if (!isDesktopModeSupported()) {
+            return;
+        }
+        setRecentsGestureInProgress(false);
+
+        if (endTarget == null) {
+            // Gesture did not result in a new end target. Ensure launchers gets paused again.
+            markLauncherPaused();
+        }
+    }
+
+    private void setRecentsGestureInProgress(boolean gestureInProgress) {
+        if (DEBUG) {
+            Log.d(TAG, "setGestureInProgress: inProgress=" + gestureInProgress);
+        }
         if (gestureInProgress != mGestureInProgress) {
             mGestureInProgress = gestureInProgress;
         }
@@ -189,7 +214,7 @@
      * Handle launcher moving to home due to home gesture or home button press.
      */
     public void onHomeActionTriggered() {
-        if (areFreeformTasksVisible()) {
+        if (IS_STASHING_ENABLED && areFreeformTasksVisible()) {
             SystemUiProxy.INSTANCE.get(mLauncher).stashDesktopApps(mLauncher.getDisplayId());
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 42cb290..f7f3bfd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -90,6 +90,8 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
+import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
@@ -144,7 +146,7 @@
     private int mLastRequestedNonFullscreenHeight;
 
     private NavigationMode mNavMode;
-    private final boolean mImeDrawsImeNavBar;
+    private boolean mImeDrawsImeNavBar;
     private final ViewCache mViewCache = new ViewCache();
 
     private final boolean mIsSafeModeEnabled;
@@ -216,7 +218,9 @@
                     new BubbleBarController(this, bubbleBarView),
                     new BubbleBarViewController(this, bubbleBarView),
                     new BubbleStashController(this),
-                    new BubbleStashedHandleViewController(this, bubbleHandleView)));
+                    new BubbleStashedHandleViewController(this, bubbleHandleView),
+                    new BubbleDragController(this),
+                    new BubbleDismissController(this, mDragLayer)));
         }
 
         // Construct controllers.
@@ -295,6 +299,7 @@
 
 
     public void init(@NonNull TaskbarSharedState sharedState) {
+        mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false);
         mLastRequestedNonFullscreenHeight = getDefaultTaskbarWindowHeight();
         mWindowLayoutParams = createAllWindowParams();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index 363f915..c3ec1e5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -35,6 +35,8 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
@@ -49,7 +51,7 @@
  */
 public class TaskbarHoverToolTipController implements View.OnHoverListener {
 
-    private static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400;
+    @VisibleForTesting protected static final int HOVER_TOOL_TIP_REVEAL_START_DELAY = 400;
     private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 300;
     private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150;
 
@@ -145,7 +147,6 @@
     }
 
     private void startRevealHoverToolTip() {
-        mActivity.setTaskbarWindowFullscreen(true);
         mHoverToolTipHandler.postDelayed(mRevealHoverToolTipRunnable,
                 HOVER_TOOL_TIP_REVEAL_START_DELAY);
     }
@@ -157,6 +158,7 @@
         if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
             return;
         }
+        mActivity.setTaskbarWindowFullscreen(true);
         Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
         mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
                 mTaskbarView.getTop(), /* shouldAutoClose= */ false);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 68ea475..a935bac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -94,26 +94,12 @@
             } else {
                 0
             }
-        if (context.isGestureNav) {
-            windowLayoutParams.providedInsets =
-                arrayOf(
-                    InsetsFrameProvider(insetsOwner, 0, navigationBars())
-                        .setFlags(
-                            FLAG_SUPPRESS_SCRIM or insetsRoundedCornerFlag,
-                            FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER
-                        ),
-                    InsetsFrameProvider(insetsOwner, 0, tappableElement()),
-                    InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
-                    InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
-                        .setSource(SOURCE_DISPLAY),
-                    InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
-                        .setSource(SOURCE_DISPLAY)
-                )
-        } else {
-            windowLayoutParams.providedInsets = getButtonNavInsets(insetsRoundedCornerFlag)
+
+        windowLayoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag)
+        if (!context.isGestureNav) {
             if (windowLayoutParams.paramsForRotation != null) {
                 for (layoutParams in windowLayoutParams.paramsForRotation) {
-                    layoutParams.providedInsets = getButtonNavInsets(insetsRoundedCornerFlag)
+                    layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag)
                 }
             }
         }
@@ -165,15 +151,28 @@
         context.notifyUpdateLayoutParams()
     }
 
-    private fun getButtonNavInsets(insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> {
+    /**
+     * The inset types and number of insets provided have to match for both gesture nav and button
+     * nav. The values and the order of the elements in array are allowed to differ.
+     * Reason being WM does not allow types and number of insets changing for a given window once it
+     * is added into the hierarchy for performance reasons.
+     */
+    private fun getProvidedInsets(insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> {
+        val navBarsFlag =
+                (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
         return arrayOf(
-                    InsetsFrameProvider(insetsOwner, 0, navigationBars())
+                InsetsFrameProvider(insetsOwner, 0, navigationBars())
                         .setFlags(
-                            insetsRoundedCornerFlag,
-                            (FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER)
+                                navBarsFlag,
+                                FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER
                         ),
-                    InsetsFrameProvider(insetsOwner, 0, tappableElement()),
-                    InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()))
+                InsetsFrameProvider(insetsOwner, 0, tappableElement()),
+                InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
+                InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
+                        .setSource(SOURCE_DISPLAY),
+                InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
+                        .setSource(SOURCE_DISPLAY)
+        )
     }
 
     private fun setProviderInsets(provider: InsetsFrameProvider, gravity: Int) {
@@ -181,47 +180,45 @@
         val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
         val res = context.resources
         if (provider.type == navigationBars() || provider.type == mandatorySystemGestures()) {
-            provider.insetsSize = getInsetsByNavMode(contentHeight, gravity)
+            provider.insetsSize = getInsetsForGravity(contentHeight, gravity)
         } else if (provider.type == tappableElement()) {
-            provider.insetsSize = getInsetsByNavMode(tappableHeight, gravity)
+            provider.insetsSize = getInsetsForGravity(tappableHeight, gravity)
         } else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) {
-            provider.insetsSize =
-                    Insets.of(
-                            gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res),
-                            0,
-                            0,
-                            0
-                    )
+            val leftIndexInset =
+                    if (context.isThreeButtonNav) 0
+                    else gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res)
+            provider.insetsSize = Insets.of(leftIndexInset, 0, 0, 0)
         } else if (provider.type == systemGestures() && provider.index == INDEX_RIGHT) {
-            provider.insetsSize =
-                    Insets.of(
-                            0,
-                            0,
-                            gestureNavSettingsObserver.getRightSensitivityForCallingUser(res),
-                            0
-                    )
+            val rightIndexInset =
+                    if (context.isThreeButtonNav) 0
+                    else gestureNavSettingsObserver.getRightSensitivityForCallingUser(res)
+            provider.insetsSize = Insets.of(0, 0, rightIndexInset, 0)
         }
 
-        val imeInsetsSize = getInsetsByNavMode(taskbarHeightForIme, gravity)
-        val insetsSizeOverride =
+
+        val imeInsetsSize = getInsetsForGravity(taskbarHeightForIme, gravity)
+        val imeInsetsSizeOverride =
                 arrayOf(
                         InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
                 )
         // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
-        val visInsetsSizeForGestureNavTappableElement = getInsetsByNavMode(0, gravity)
-        val insetsSizeOverrideForGestureNavTappableElement =
+        val visInsetsSizeForTappableElement =
+                if (context.isGestureNav) getInsetsForGravity(0, gravity)
+                else getInsetsForGravity(tappableHeight, gravity)
+        val insetsSizeOverrideForTappableElement =
                 arrayOf(
                         InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
                         InsetsFrameProvider.InsetsSizeOverride(
                                 TYPE_VOICE_INTERACTION,
-                                visInsetsSizeForGestureNavTappableElement
+                                visInsetsSizeForTappableElement
                         ),
                 )
-        if (context.isGestureNav && provider.type == tappableElement()) {
-            provider.insetsSizeOverrides = insetsSizeOverrideForGestureNavTappableElement
+        if ((context.isGestureNav || TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW)
+                && provider.type == tappableElement()) {
+            provider.insetsSizeOverrides = insetsSizeOverrideForTappableElement
         } else if (provider.type != systemGestures()) {
             // We only override insets at the bottom of the screen
-            provider.insetsSizeOverrides = insetsSizeOverride
+            provider.insetsSizeOverrides = imeInsetsSizeOverride
         }
     }
 
@@ -229,14 +226,14 @@
      * @return [Insets] where the [inset] is either used as a bottom inset or
      * right/left inset if using 3 button nav
      */
-    private fun getInsetsByNavMode(inset: Int, gravity: Int): Insets {
-        if ((gravity and Gravity.BOTTOM) != 0) {
+    private fun getInsetsForGravity(inset: Int, gravity: Int): Insets {
+        if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
             // Taskbar or portrait phone mode
             return Insets.of(0, 0, 0, inset)
         }
 
         // TODO(b/230394142): seascape
-        val isSeascape = (gravity and Gravity.START) != 0
+        val isSeascape = (gravity and Gravity.START) == Gravity.START
         val leftInset = if (isSeascape) inset else 0
         val rightInset = if (isSeascape) 0 else inset
         return Insets.of(leftInset , 0, rightInset, 0)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 5e37cf4..b5b453b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -338,7 +338,6 @@
         // us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
         updateStateForFlag(FLAG_IN_APP, true);
         applyState(/* duration = */ 0);
-
         notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp());
     }
 
@@ -675,7 +674,10 @@
                     .setDuration(duration));
             mAnimator.play(mTaskbarImeBgAlpha.animateToValue(
                     hasAnyFlag(FLAG_STASHED_IN_APP_IME) ? 0 : 1).setDuration(duration));
-            mAnimator.addListener(AnimatorListeners.forEndCallback(() -> mAnimator = null));
+            mAnimator.addListener(AnimatorListeners.forEndCallback(() -> {
+                mAnimator = null;
+                mIsStashed = isStashed;
+            }));
             return;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 4f75ef5..3fe7359 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -17,9 +17,6 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Rect;
@@ -31,6 +28,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
@@ -68,16 +66,9 @@
         mAllAppsCallbacks.onAllAppsTransitionStart(true);
 
         if (animate) {
-            mOpenCloseAnimator.setValues(
-                    PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator.setInterpolator(EMPHASIZED);
-            mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mOpenCloseAnimator.removeListener(this);
-                    mAllAppsCallbacks.onAllAppsTransitionEnd(true);
-                }
-            });
+            setUpOpenCloseAnimator(TRANSLATION_SHIFT_OPENED, EMPHASIZED);
+            mOpenCloseAnimator.addListener(AnimatorListeners.forEndCallback(
+                    () -> mAllAppsCallbacks.onAllAppsTransitionEnd(true)));
             mOpenCloseAnimator.setDuration(mAllAppsCallbacks.getOpenDuration()).start();
         } else {
             mTranslationShift = TRANSLATION_SHIFT_OPENED;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index a8e6849..ffe077b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -95,6 +95,8 @@
     private View.OnClickListener mOnClickListener;
 
     private final Rect mTempRect = new Rect();
+    private float mRelativePivotX = 1f;
+    private float mRelativePivotY = 1f;
 
     // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
     // collapsed state and 1 to the fully expanded state.
@@ -109,6 +111,9 @@
     @Nullable
     private Consumer<String> mUpdateSelectedBubbleAfterCollapse;
 
+    @Nullable
+    private BubbleView mDraggedBubbleView;
+
     public BubbleBarView(Context context) {
         this(context, null);
     }
@@ -181,9 +186,10 @@
         mBubbleBarBounds.right = right;
         mBubbleBarBounds.bottom = bottom;
 
-        // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
-        setPivotX(getWidth());
-        setPivotY(getHeight());
+        // The bubble bar handle is aligned according to the relative pivot,
+        // by default it's aligned to the bottom edge of the screen so scale towards that
+        setPivotX(mRelativePivotX * getWidth());
+        setPivotY(mRelativePivotY * getHeight());
 
         // Position the views
         updateChildrenRenderNodeProperties();
@@ -198,6 +204,32 @@
         return mBubbleBarBounds;
     }
 
+    /**
+     * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
+     * respectively. If the value is not in range of 0 to 1 it will be normalized.
+     * @param x relative X pivot value in range 0..1
+     * @param y relative Y pivot value in range 0..1
+     */
+    public void setRelativePivot(float x, float y) {
+        mRelativePivotX = Float.max(Float.min(x, 1), 0);
+        mRelativePivotY = Float.max(Float.min(y, 1), 0);
+        requestLayout();
+    }
+
+    /**
+     * Get current relative pivot for X axis
+     */
+    public float getRelativePivotX() {
+        return mRelativePivotX;
+    }
+
+    /**
+     * Get current relative pivot for Y axis
+     */
+    public float getRelativePivotY() {
+        return mRelativePivotY;
+    }
+
     // TODO: (b/280605790) animate it
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -254,9 +286,9 @@
                 // where the bubble will end up when the animation ends
                 final float targetX = currentWidth - expandedWidth + expandedX;
                 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
-                // if we're fully expanded, set the z level to 0
+                // if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
                 if (widthState == 1f) {
-                    bv.setZ(0);
+                    bv.setZ(bv == mDraggedBubbleView ? mBubbleElevation : 0);
                 }
                 // When we're expanded, we're not stacked so we're not behind the stack
                 bv.setBehindStack(false, animate);
@@ -332,6 +364,14 @@
     }
 
     /**
+     * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top
+     */
+    public void setDraggedBubble(@Nullable BubbleView view) {
+        mDraggedBubbleView = view;
+        requestLayout();
+    }
+
+    /**
      * Update the arrow position to match the selected bubble.
      *
      * @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 6da1a2a..20b8e3b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -24,6 +24,8 @@
 import android.view.View;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -54,6 +56,7 @@
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
     private BubbleBarController mBubbleBarController;
+    private BubbleDragController mBubbleDragController;
     private TaskbarStashController mTaskbarStashController;
     private TaskbarInsetsController mTaskbarInsetsController;
     private View.OnClickListener mBubbleClickListener;
@@ -85,6 +88,7 @@
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
+        mBubbleDragController = bubbleControllers.bubbleDragController;
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
 
@@ -95,6 +99,7 @@
         mBubbleBarScale.updateValue(1f);
         mBubbleClickListener = v -> onBubbleClicked(v);
         mBubbleBarClickListener = v -> setExpanded(true);
+        mBubbleDragController.setupBubbleBarView(mBarView);
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
                 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -268,6 +273,7 @@
         if (b != null) {
             mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
             b.getView().setOnClickListener(mBubbleClickListener);
+            mBubbleDragController.setupBubbleView(b.getView());
         } else {
             Log.w(TAG, "addBubble, bubble was null!");
         }
@@ -319,4 +325,46 @@
             mBubbleStashController.showBubbleBar(true /* expand the bubbles */);
         }
     }
+
+    /**
+     * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI
+     * that a bubble is being dragged to dismiss.
+     * @param bubbleView dragged bubble view
+     */
+    public void onDragStart(@NonNull BubbleView bubbleView) {
+        if (bubbleView.getBubble() == null) return;
+        mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ true);
+        mBarView.setDraggedBubble(bubbleView);
+    }
+
+    /**
+     * Notifies SystemUI to expand the selected bubble when the bubble is released.
+     * @param bubbleView dragged bubble view
+     */
+    public void onDragRelease(@NonNull BubbleView bubbleView) {
+        if (bubbleView.getBubble() == null) return;
+        mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ false);
+    }
+
+    /**
+     * Removes the dragged bubble view in the bubble bar view
+     */
+    public void onDragEnd() {
+        mBarView.setDraggedBubble(null);
+    }
+
+    /**
+     * Called when bubble was dragged into the dismiss target. Notifies System
+     * @param bubble dismissed bubble item
+     */
+    public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+        mSystemUiProxy.removeBubble(bubble.getKey());
+    }
+
+    /**
+     * Called when bubble stack was dragged into the dismiss target
+     */
+    public void onDismissAllBubblesWhileDragging() {
+        mSystemUiProxy.removeAllBubbles();
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 6417f3c..c47427d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -27,6 +27,8 @@
     public final BubbleBarViewController bubbleBarViewController;
     public final BubbleStashController bubbleStashController;
     public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
+    public final BubbleDragController bubbleDragController;
+    public final BubbleDismissController bubbleDismissController;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
 
@@ -39,11 +41,15 @@
             BubbleBarController bubbleBarController,
             BubbleBarViewController bubbleBarViewController,
             BubbleStashController bubbleStashController,
-            BubbleStashedHandleViewController bubbleStashedHandleViewController) {
+            BubbleStashedHandleViewController bubbleStashedHandleViewController,
+            BubbleDragController bubbleDragController,
+            BubbleDismissController bubbleDismissController) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
         this.bubbleStashController = bubbleStashController;
         this.bubbleStashedHandleViewController = bubbleStashedHandleViewController;
+        this.bubbleDragController = bubbleDragController;
+        this.bubbleDismissController = bubbleDismissController;
     }
 
     /**
@@ -56,6 +62,8 @@
         bubbleBarViewController.init(taskbarControllers, this);
         bubbleStashedHandleViewController.init(taskbarControllers, this);
         bubbleStashController.init(taskbarControllers, this);
+        bubbleDragController.init(/* bubbleControllers = */ this);
+        bubbleDismissController.init(/* bubbleControllers = */ this);
 
         mPostInitRunnables.executeAllAndDestroy();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
new file mode 100644
index 0000000..0ff0469
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 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.taskbar.bubbles;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarDragLayer;
+import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
+/**
+ * Controls dismiss view presentation for the bubble bar dismiss functionality.
+ * Provides the dragged view snapping to the target dismiss area and animates it.
+ * When the dragged bubble/bubble stack is released inside of the target area, it gets dismissed.
+ *
+ * @see BubbleDragController
+ */
+public class BubbleDismissController {
+    private static final String TAG = BubbleDismissController.class.getSimpleName();
+    private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
+    // LINT.IfChange
+    private static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
+            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
+    // LINT.ThenChange(com/android/wm/shell/bubbles/BubbleStackView.java)
+    private final TaskbarActivityContext mActivity;
+    private final TaskbarDragLayer mDragLayer;
+    @Nullable
+    private BubbleBarViewController mBubbleBarViewController;
+
+    // Dismiss view that's attached to drag layer. It consists of the scrim view and the circular
+    // dismiss view used as a dismiss target.
+    @Nullable
+    private DismissView mDismissView;
+
+    // The currently magnetized object, which is being dragged and will be attracted to the magnetic
+    // dismiss target. This is either the stack itself, or an individual bubble.
+    @Nullable
+    private MagnetizedObject<View> mMagnetizedObject;
+
+    // The MagneticTarget instance for our circular dismiss view. This is added to the
+    // MagnetizedObject instances for the stack and any dragged-out bubbles.
+    @Nullable
+    private MagnetizedObject.MagneticTarget mMagneticTarget;
+
+    // The bubble drag animator that synchronizes bubble drag and dismiss view animations
+    // A new instance is provided when the dismiss view is setup
+    @Nullable
+    private BubbleDragAnimator mAnimator;
+
+    public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
+        mActivity = activity;
+        mDragLayer = dragLayer;
+    }
+
+    /**
+     * Initializes dependencies when bubble controllers are created.
+     * Should be careful to only access things that were created in constructors for now, as some
+     * controllers may still be waiting for init().
+     */
+    public void init(@NonNull BubbleControllers bubbleControllers) {
+        mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+    }
+
+    /**
+     * Setup the dismiss view and magnetized object that will be attracted to magnetic target.
+     * Should be called before handling events or showing/hiding dismiss view.
+     *
+     * @param magnetizedView the view to be pulled into target dismiss area
+     * @param animator       the bubble animator to be used for the magnetized view, it syncs bubble
+     *                       dragging and dismiss animations with the dismiss view provided.
+     */
+    public void setupDismissView(@NonNull View magnetizedView,
+            @NonNull BubbleDragAnimator animator) {
+        setupDismissView();
+        setupMagnetizedObject(magnetizedView);
+        if (mDismissView != null) {
+            animator.setDismissView(mDismissView);
+            mAnimator = animator;
+        }
+    }
+
+    /**
+     * Handle the touch event and pass it to the magnetized object.
+     * It should be called after {@code setupDismissView}
+     */
+    public boolean handleTouchEvent(@NonNull MotionEvent event) {
+        return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+    }
+
+    /**
+     * Show dismiss view with animation
+     * It should be called after {@code setupDismissView}
+     */
+    public void showDismissView() {
+        if (mDismissView == null) return;
+        mDismissView.show();
+    }
+
+    /**
+     * Hide dismiss view with animation
+     * It should be called after {@code setupDismissView}
+     */
+    public void hideDismissView() {
+        if (mDismissView == null) return;
+        mDismissView.hide();
+    }
+
+    /**
+     * Dismiss magnetized object when it's released in the dismiss target area
+     */
+    private void dismissMagnetizedObject() {
+        if (mMagnetizedObject == null || mBubbleBarViewController == null) return;
+        if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
+            BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
+            if (bubbleView.getBubble() != null) {
+                mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+            }
+        } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
+            mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+        }
+    }
+
+    private void setupDismissView() {
+        if (mDismissView != null) return;
+        mDismissView = new DismissView(mActivity.getApplicationContext());
+        BubbleDismissViewUtils.setup(mDismissView);
+        mDragLayer.addView(mDismissView, /* index = */ 0,
+                new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+        mDismissView.setElevation(mDismissView.getResources().getDimensionPixelSize(
+                R.dimen.bubblebar_elevation));
+        setupMagneticTarget(mDismissView.getCircle());
+    }
+
+    private void setupMagneticTarget(@NonNull View view) {
+        int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_size);
+        mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius);
+    }
+
+    private void setupMagnetizedObject(@NonNull View magnetizedView) {
+        mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(),
+                magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
+            @Override
+            public float getWidth(@NonNull View underlyingObject) {
+                return underlyingObject.getWidth() * underlyingObject.getScaleX();
+            }
+
+            @Override
+            public float getHeight(@NonNull View underlyingObject) {
+                return underlyingObject.getHeight() * underlyingObject.getScaleY();
+            }
+
+            @Override
+            public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
+                underlyingObject.getLocationOnScreen(loc);
+            }
+        };
+
+        mMagnetizedObject.setHapticsEnabled(true);
+        mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
+        mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+        if (mMagneticTarget != null) {
+            mMagnetizedObject.addTarget(mMagneticTarget);
+        } else {
+            Log.e(TAG,"Requires MagneticTarget to add target to MagnetizedObject!");
+        }
+        mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() {
+            @Override
+            public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                if (mAnimator == null) return;
+                mAnimator.animateDismissCaptured();
+            }
+
+            @Override
+            public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+                    float velX, float velY, boolean wasFlungOut) {
+                if (mAnimator == null) return;
+                mAnimator.animateDismissReleased();
+            }
+
+            @Override
+            public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                dismissMagnetizedObject();
+            }
+        });
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
new file mode 100644
index 0000000..4b235a9
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+@file:JvmName("BubbleDismissViewUtils")
+
+package com.android.launcher3.taskbar.bubbles
+
+import com.android.launcher3.R
+import com.android.wm.shell.common.bubbles.DismissView
+
+/**
+ * Dismiss view is shared from WMShell. It requires setup with local resources.
+ *
+ * Usage:
+ * - Kotlin `dismissView.setup()`
+ * - Java `BubbleDismissViewUtils.setup(dismissView)`
+ */
+fun DismissView.setup() {
+    setup(
+        DismissView.Config(
+            targetSizeResId = R.dimen.bubblebar_dismiss_target_size,
+            iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size,
+            bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin,
+            floatingGradientHeightResId = R.dimen.bubblebar_dismiss_floating_gradient_height,
+            floatingGradientColorResId = android.R.color.system_neutral1_900,
+            backgroundResId = R.drawable.bg_bubble_dismiss_circle,
+            iconResId = R.drawable.ic_bubble_dismiss_white
+        )
+    )
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
new file mode 100644
index 0000000..24dca5e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragAnimator.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2023 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.taskbar.bubbles;
+
+import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
+
+import android.content.res.Resources;
+import android.graphics.PointF;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+
+import com.android.launcher3.R;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.common.bubbles.DismissCircleView;
+import com.android.wm.shell.common.bubbles.DismissView;
+
+/**
+ * The animator performs the bubble animations while dragging and coordinates bubble and dismiss
+ * view animations when it gets magnetized, released or dismissed.
+ */
+public class BubbleDragAnimator {
+    private static final float SCALE_BUBBLE_FOCUSED = 1.2f;
+    private static final float SCALE_BUBBLE_CAPTURED = 0.9f;
+    private static final float SCALE_BUBBLE_BAR_FOCUSED = 1.1f;
+
+    private final PhysicsAnimator.SpringConfig mDefaultConfig =
+            new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY);
+    private final PhysicsAnimator.SpringConfig mTranslationConfig =
+            new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_LOW_BOUNCY);
+    @NonNull
+    private final View mView;
+    @NonNull
+    private final PhysicsAnimator<View> mBubbleAnimator;
+    @Nullable
+    private DismissView mDismissView;
+    @Nullable
+    private PhysicsAnimator<DismissCircleView> mDismissAnimator;
+    private final float mBubbleFocusedScale;
+    private final float mBubbleCapturedScale;
+    private final float mDismissCapturedScale;
+
+    /**
+     * Should be initialised for each dragged view
+     *
+     * @param view the dragged view to animate
+     */
+    public BubbleDragAnimator(@NonNull View view) {
+        mView = view;
+        mBubbleAnimator = PhysicsAnimator.getInstance(view);
+        mBubbleAnimator.setDefaultSpringConfig(mDefaultConfig);
+
+        Resources resources = view.getResources();
+        final int collapsedSize = resources.getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_small_size);
+        final int expandedSize = resources.getDimensionPixelSize(
+                R.dimen.bubblebar_dismiss_target_size);
+        mDismissCapturedScale = (float) collapsedSize / expandedSize;
+
+        if (view instanceof BubbleBarView) {
+            mBubbleFocusedScale = SCALE_BUBBLE_BAR_FOCUSED;
+            mBubbleCapturedScale = mDismissCapturedScale;
+        } else {
+            mBubbleFocusedScale = SCALE_BUBBLE_FOCUSED;
+            mBubbleCapturedScale = SCALE_BUBBLE_CAPTURED;
+        }
+    }
+
+    /**
+     * Sets dismiss view to be animated alongside the dragged bubble
+     */
+    public void setDismissView(@NonNull DismissView dismissView) {
+        mDismissView = dismissView;
+        mDismissAnimator = PhysicsAnimator.getInstance(dismissView.getCircle());
+        mDismissAnimator.setDefaultSpringConfig(mDefaultConfig);
+    }
+
+    /**
+     * Animates the focused state of the bubble when the dragging starts
+     */
+    public void animateFocused() {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
+                .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
+                .start();
+    }
+
+    /**
+     * Animates the dragged bubble movement back to the initial position.
+     *
+     * @param initialPosition the position to animate to
+     * @param velocity        the initial velocity to use for the spring animation
+     * @param endActions      gets called when the animation completes or gets cancelled
+     */
+    public void animateToInitialState(@NonNull PointF initialPosition, @NonNull PointF velocity,
+            @Nullable Runnable endActions) {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, 1f)
+                .spring(DynamicAnimation.SCALE_Y, 1f)
+                .spring(DynamicAnimation.TRANSLATION_X, initialPosition.x, velocity.x,
+                        mTranslationConfig)
+                .spring(DynamicAnimation.TRANSLATION_Y, initialPosition.y, velocity.y,
+                        mTranslationConfig)
+                .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
+                        boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
+                        boolean allRelevantPropertyAnimationsEnded) -> {
+                    if (canceled || allRelevantPropertyAnimationsEnded) {
+                        resetAnimatedViews(initialPosition);
+                        if (endActions != null) {
+                            endActions.run();
+                        }
+                    }
+                })
+                .start();
+    }
+
+    /**
+     * Animates the dragged view alongside the dismiss view when it gets captured in the dismiss
+     * target area.
+     */
+    public void animateDismissCaptured() {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, mBubbleCapturedScale)
+                .spring(DynamicAnimation.SCALE_Y, mBubbleCapturedScale)
+                .spring(DynamicAnimation.ALPHA, mDismissCapturedScale)
+                .start();
+
+        if (mDismissAnimator != null) {
+            mDismissAnimator.cancel();
+            mDismissAnimator
+                    .spring(DynamicAnimation.SCALE_X, mDismissCapturedScale)
+                    .spring(DynamicAnimation.SCALE_Y, mDismissCapturedScale)
+                    .start();
+        }
+    }
+
+    /**
+     * Animates the dragged view alongside the dismiss view when it gets released from the dismiss
+     * target area.
+     */
+    public void animateDismissReleased() {
+        mBubbleAnimator.cancel();
+        mBubbleAnimator
+                .spring(DynamicAnimation.SCALE_X, mBubbleFocusedScale)
+                .spring(DynamicAnimation.SCALE_Y, mBubbleFocusedScale)
+                .spring(DynamicAnimation.ALPHA, 1f)
+                .start();
+
+        if (mDismissAnimator != null) {
+            mDismissAnimator.cancel();
+            mDismissAnimator
+                    .spring(DynamicAnimation.SCALE_X, 1f)
+                    .spring(DynamicAnimation.SCALE_Y, 1f)
+                    .start();
+        }
+    }
+
+    /**
+     * Animates the dragged bubble dismiss when it's released in the dismiss target area.
+     *
+     * @param initialPosition the initial position to move the bubble too after animation finishes
+     * @param endActions      gets called when the animation completes or gets cancelled
+     */
+    public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) {
+        float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f;
+        float translationY = mView.getTranslationY() + dismissHeight;
+        mBubbleAnimator
+                .spring(DynamicAnimation.TRANSLATION_Y, translationY)
+                .spring(DynamicAnimation.SCALE_X, 0f)
+                .spring(DynamicAnimation.SCALE_Y, 0f)
+                .spring(DynamicAnimation.ALPHA, 0f)
+                .addEndListener((View target, @NonNull FloatPropertyCompat<? super View> property,
+                        boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
+                        boolean allRelevantPropertyAnimationsEnded) -> {
+                    if (canceled || allRelevantPropertyAnimationsEnded) {
+                        resetAnimatedViews(initialPosition);
+                        if (endActions != null) endActions.run();
+                    }
+                })
+                .start();
+    }
+
+    /**
+     * Reset the animated views to the initial state
+     *
+     * @param initialPosition position of the bubble
+     */
+    private void resetAnimatedViews(@NonNull PointF initialPosition) {
+        mView.setScaleX(1f);
+        mView.setScaleY(1f);
+        mView.setAlpha(1f);
+        mView.setTranslationX(initialPosition.x);
+        mView.setTranslationY(initialPosition.y);
+
+        if (mDismissView != null) {
+            mDismissView.getCircle().setScaleX(1f);
+            mDismissView.getCircle().setScaleY(1f);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
new file mode 100644
index 0000000..08fd681
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2023 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.taskbar.bubbles;
+
+import android.annotation.SuppressLint;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+
+/**
+ * Controls bubble bar drag to dismiss interaction.
+ * Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}.
+ * Supported interactions:
+ * - Drag a single bubble view into dismiss target to remove it.
+ * - Drag the bubble stack into dismiss target to remove all.
+ * Restores initial position of dragged view if released outside of the dismiss target.
+ */
+public class BubbleDragController {
+    private final TaskbarActivityContext mActivity;
+    private BubbleBarViewController mBubbleBarViewController;
+    private BubbleDismissController mBubbleDismissController;
+
+    public BubbleDragController(TaskbarActivityContext activity) {
+        mActivity = activity;
+    }
+
+    /**
+     * Initializes dependencies when bubble controllers are created.
+     * Should be careful to only access things that were created in constructors for now, as some
+     * controllers may still be waiting for init().
+     */
+    public void init(@NonNull BubbleControllers bubbleControllers) {
+        mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
+        mBubbleDismissController = bubbleControllers.bubbleDismissController;
+    }
+
+    /**
+     * Setup the bubble view for dragging and attach touch listener to it
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    public void setupBubbleView(@NonNull BubbleView bubbleView) {
+        if (!(bubbleView.getBubble() instanceof BubbleBarBubble)) {
+            // Don't setup dragging for overflow bubble view
+            return;
+        }
+
+        bubbleView.setOnTouchListener(new BubbleTouchListener() {
+            @Override
+            void onDragStart() {
+                mBubbleBarViewController.onDragStart(bubbleView);
+            }
+
+            @Override
+            void onDragEnd() {
+                mBubbleBarViewController.onDragEnd();
+            }
+
+            @Override
+            protected void onDragRelease() {
+                mBubbleBarViewController.onDragRelease(bubbleView);
+            }
+        });
+    }
+
+    /**
+     * Setup the bubble bar view for dragging and attach touch listener to it
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) {
+        PointF initialRelativePivot = new PointF();
+        bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
+            @Override
+            protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
+                if (bubbleBarView.isExpanded()) return false;
+                return super.onTouchDown(view, event);
+            }
+
+            @Override
+            void onDragStart() {
+                initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
+                        bubbleBarView.getRelativePivotY());
+                // By default the bubble bar view pivot is in bottom right corner, while dragging
+                // it should be centered in order to align it with the dismiss target view
+                bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f);
+            }
+
+            @Override
+            void onDragEnd() {
+                // Restoring the initial pivot for the bubble bar view
+                bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
+            }
+        });
+    }
+
+    /**
+     * Bubble touch listener for handling a single bubble view or bubble bar view while dragging.
+     * The dragging starts after "shorter" long click (the long click duration might change):
+     * - When the touch gesture moves out of the {@code ACTION_DOWN} location the dragging
+     * interaction is cancelled.
+     * - When {@code ACTION_UP} happens before long click is registered and there was no significant
+     * movement the view will perform click.
+     * - When the listener registers long click it starts dragging interaction, all the subsequent
+     * {@code ACTION_MOVE} events will drag the view, and the interaction finishes when
+     * {@code ACTION_UP} or {@code ACTION_CANCEL} are received.
+     * Lifecycle methods can be overridden do add extra setup/clean up steps.
+     */
+    private abstract class BubbleTouchListener implements View.OnTouchListener {
+        /**
+         * The internal state of the touch listener
+         */
+        private enum State {
+            // Idle and ready for the touch events.
+            // Changes to:
+            // - TOUCHED, when the {@code ACTION_DOWN} is handled
+            IDLE,
+
+            // Touch down was handled and the lister is recognising the gestures.
+            // Changes to:
+            // - IDLE, when performs the click
+            // - DRAGGING, when registers the long click and starts dragging interaction
+            // - CANCELLED, when the touch events move out of the initial location before the long
+            // click is recognised
+
+            TOUCHED,
+
+            // The long click was registered and the view is being dragged.
+            // Changes to:
+            // - IDLE, when the gesture ends with the {@code ACTION_UP} or {@code ACTION_CANCEL}
+            DRAGGING,
+
+            // The dragging was cancelled.
+            // Changes to:
+            // - IDLE, when the current gesture completes
+            CANCELLED
+        }
+
+        private final PointF mTouchDownLocation = new PointF();
+        private final PointF mViewInitialPosition = new PointF();
+        private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+        private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
+        private State mState = State.IDLE;
+        private int mTouchSlop = -1;
+        private BubbleDragAnimator mAnimator;
+        @Nullable
+        private Runnable mLongClickRunnable;
+
+        /**
+         * Called when the dragging interaction has started
+         */
+        abstract void onDragStart();
+
+        /**
+         * Called when the dragging interaction has ended and all the animations have completed
+         */
+        abstract void onDragEnd();
+
+        /**
+         * Called when the dragged bubble is released outside of the dismiss target area and will
+         * move back to its initial position
+         */
+        protected void onDragRelease() {
+        }
+
+        /**
+         * Called when the dragged bubble is released inside of the dismiss target area and will get
+         * dismissed with animation
+         */
+        protected void onDragDismiss() {
+        }
+
+        @Override
+        @SuppressLint("ClickableViewAccessibility")
+        public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
+            updateVelocity(event);
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    return onTouchDown(view, event);
+                case MotionEvent.ACTION_MOVE:
+                    onTouchMove(view, event);
+                    break;
+                case MotionEvent.ACTION_UP:
+                    onTouchUp(view, event);
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    onTouchCancel(view, event);
+                    break;
+            }
+            return true;
+        }
+
+        /**
+         * The touch down starts the interaction and schedules the long click handler.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         * @return true if the gesture should be intercepted and handled, false otherwise. Note if
+         * the false is returned subsequent events in the gesture won't get reported.
+         */
+        protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
+            mState = State.TOUCHED;
+            mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
+            mTouchDownLocation.set(event.getRawX(), event.getRawY());
+            mViewInitialPosition.set(view.getTranslationX(), view.getTranslationY());
+            setupLongClickHandler(view);
+            return true;
+        }
+
+        /**
+         * The move event drags the view or cancels the interaction if hasn't long clicked yet.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         */
+        protected void onTouchMove(@NonNull View view, @NonNull MotionEvent event) {
+            final float dx = event.getRawX() - mTouchDownLocation.x;
+            final float dy = event.getRawY() - mTouchDownLocation.y;
+            switch (mState) {
+                case TOUCHED:
+                    final boolean movedOut = Math.hypot(dx, dy) > mTouchSlop;
+                    if (movedOut) {
+                        // Moved out of the initial location before the long click was registered
+                        mState = State.CANCELLED;
+                        cleanUpLongClickHandler(view);
+                    }
+                    break;
+                case DRAGGING:
+                    drag(view, event, dx, dy);
+                    break;
+            }
+        }
+
+        /**
+         * On touch up performs click or finishes the dragging depending on the state.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         */
+        protected void onTouchUp(@NonNull View view, @NonNull MotionEvent event) {
+            switch (mState) {
+                case TOUCHED:
+                    view.performClick();
+                    cleanUp(view);
+                    break;
+                case DRAGGING:
+                    stopDragging(view, event);
+                    break;
+                default:
+                    cleanUp(view);
+                    break;
+            }
+        }
+
+        /**
+         * The gesture is cancelled and the interaction should clean up and complete.
+         *
+         * @param view  the view that received the event
+         * @param event the motion event
+         */
+        protected void onTouchCancel(@NonNull View view, @NonNull MotionEvent event) {
+            if (mState == State.DRAGGING) {
+                stopDragging(view, event);
+            } else {
+                cleanUp(view);
+            }
+        }
+
+        private void startDragging(@NonNull View view) {
+            onDragStart();
+            mActivity.setTaskbarWindowFullscreen(true);
+            mAnimator = new BubbleDragAnimator(view);
+            mAnimator.animateFocused();
+            mBubbleDismissController.setupDismissView(view, mAnimator);
+            mBubbleDismissController.showDismissView();
+        }
+
+        private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy) {
+            if (mBubbleDismissController.handleTouchEvent(event)) return;
+            view.setTranslationX(mViewInitialPosition.x + dx);
+            view.setTranslationY(mViewInitialPosition.y + dy);
+        }
+
+        private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
+            Runnable onComplete = () -> {
+                mActivity.setTaskbarWindowFullscreen(false);
+                cleanUp(view);
+                onDragEnd();
+            };
+
+            if (mBubbleDismissController.handleTouchEvent(event)) {
+                onDragDismiss();
+                mAnimator.animateDismiss(mViewInitialPosition, onComplete);
+            } else {
+                onDragRelease();
+                mAnimator.animateToInitialState(mViewInitialPosition, getCurrentVelocity(),
+                        onComplete);
+            }
+            mBubbleDismissController.hideDismissView();
+        }
+
+        private void setupLongClickHandler(@NonNull View view) {
+            cleanUpLongClickHandler(view);
+            mLongClickRunnable = () -> {
+                // Register long click and start dragging interaction
+                mState = State.DRAGGING;
+                startDragging(view);
+            };
+            view.getHandler().postDelayed(mLongClickRunnable, mPressToDragTimeout);
+        }
+
+        private void cleanUpLongClickHandler(@NonNull View view) {
+            if (mLongClickRunnable == null || view.getHandler() == null) return;
+            view.getHandler().removeCallbacks(mLongClickRunnable);
+            mLongClickRunnable = null;
+        }
+
+        private void cleanUp(@NonNull View view) {
+            cleanUpLongClickHandler(view);
+            mVelocityTracker.clear();
+            mState = State.IDLE;
+        }
+
+        private void updateVelocity(MotionEvent event) {
+            final float deltaX = event.getRawX() - event.getX();
+            final float deltaY = event.getRawY() - event.getY();
+            event.offsetLocation(deltaX, deltaY);
+            mVelocityTracker.addMovement(event);
+            event.offsetLocation(-deltaX, -deltaY);
+        }
+
+        private PointF getCurrentVelocity() {
+            mVelocityTracker.computeCurrentVelocity(/* units = */ 1000);
+            return new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 7db2320..5c7f2be 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -67,6 +67,7 @@
             val startContextualContainer =
                 navButtonsView.findViewById<ViewGroup>(ID_START_CONTEXTUAL_BUTTONS)
             val isPhoneNavMode = phoneMode && isThreeButtonNav
+            val isPhoneGestureMode = phoneMode && !isThreeButtonNav
             return when {
                 isPhoneNavMode -> {
                     if (!deviceProfile.isLandscape) {
@@ -92,6 +93,14 @@
                         )
                     }
                 }
+                isPhoneGestureMode ->{
+                    PhoneGestureLayoutter(
+                            resources,
+                            navButtonContainer,
+                            endContextualContainer,
+                            startContextualContainer
+                    )
+                }
                 deviceProfile.isTaskbarPresent -> {
                     return when {
                         isInSetup -> {
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
new file mode 100644
index 0000000..8525c6c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.taskbar.navbutton
+
+import android.content.res.Resources
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import com.android.launcher3.DeviceProfile
+
+/** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */
+class PhoneGestureLayoutter(
+        resources: Resources,
+        navBarContainer: LinearLayout,
+        endContextualContainer: ViewGroup,
+        startContextualContainer: ViewGroup
+) :
+        AbstractNavButtonLayoutter(
+                resources,
+                navBarContainer,
+                endContextualContainer,
+                startContextualContainer
+        ) {
+
+    override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
+        // no-op
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index ffd22b8..20383f4 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -664,6 +664,8 @@
             @Override
             public void onAnimationCancel(Animator animation) {
                 getDragLayer().removeView(floatingTaskView);
+                mSplitSelectStateController.getSplitAnimationController()
+                        .removeSplitInstructionsView(QuickstepLauncher.this);
                 mSplitSelectStateController.resetState();
             }
         });
@@ -865,7 +867,7 @@
         if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
             DesktopVisibilityController controller = mDesktopVisibilityController;
             if (controller != null && controller.areFreeformTasksVisible()
-                    && !controller.isGestureInProgress()) {
+                    && !controller.isRecentsGestureInProgress()) {
                 // Return early to skip setting activity to appear as resumed
                 // TODO(b/255649902): shouldn't be needed when we have a separate launcher state
                 //  for desktop that we can use to control other parts of launcher
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index c18ad5a..f1660ee 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -129,7 +129,7 @@
         mWindowMaxDeltaY = mLauncher.getResources().getDimensionPixelSize(
                 R.dimen.swipe_back_window_max_delta_y);
         mCancelInterpolator =
-                AnimationUtils.loadInterpolator(mLauncher, R.interpolator.back_cancel);
+                AnimationUtils.loadInterpolator(mLauncher, R.interpolator.standard_interpolator);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 60784f5..c9b7d5e 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -662,6 +662,31 @@
     }
 
     /**
+     * Tells SysUI to remove the bubble with the provided key.
+     * @param key the key of the bubble to show.
+     */
+    public void removeBubble(String key) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.removeBubble(key);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call removeBubble");
+        }
+    }
+
+    /**
+     * Tells SysUI to remove all bubbles.
+     */
+    public void removeAllBubbles() {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.removeAllBubbles();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call removeAllBubbles");
+        }
+    }
+
+    /**
      * Tells SysUI to collapse the bubbles.
      */
     public void collapseBubbles() {
@@ -674,6 +699,21 @@
         }
     }
 
+    /**
+     * Tells SysUI when the bubble is being dragged.
+     * Should be called only when the bubble bar is expanded.
+     * @param bubbleKey the key of the bubble to collapse/expand
+     * @param isBeingDragged whether the bubble is being dragged
+     */
+    public void onBubbleDrag(@Nullable String bubbleKey, boolean isBeingDragged) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.onBubbleDrag(bubbleKey, isBeingDragged);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call onBubbleDrag");
+        }
+    }
+
     //
     // Splitscreen
     //
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 1744b08..06f1f9a 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -159,6 +159,10 @@
             return mActionsView;
         }
 
+        public TaskThumbnailView getThumbnailView() {
+            return mThumbnailView;
+        }
+
         /**
          * Called when the current task is interactive for the user
          */
diff --git a/quickstep/src/com/android/quickstep/util/BaseDepthController.java b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
index 931e468..99f564c 100644
--- a/quickstep/src/com/android/quickstep/util/BaseDepthController.java
+++ b/quickstep/src/com/android/quickstep/util/BaseDepthController.java
@@ -18,6 +18,7 @@
 import android.app.WallpaperManager;
 import android.os.IBinder;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.view.AttachedSurfaceControl;
 import android.view.SurfaceControl;
 
@@ -50,6 +51,9 @@
     private static final int DEPTH_INDEX_WIDGET = 1;
     private static final int DEPTH_INDEX_COUNT = 2;
 
+    // b/291401432
+    private static final String TAG = "BaseDepthController";
+
     protected final Launcher mLauncher;
     /** Property to set the depth for state transition. */
     public final MultiProperty stateDepth;
@@ -88,7 +92,7 @@
      */
     protected boolean mInEarlyWakeUp;
 
-    private boolean mWaitingOnSurfaceValidity;
+    protected boolean mWaitingOnSurfaceValidity;
 
     public BaseDepthController(Launcher activity) {
         mLauncher = activity;
@@ -133,9 +137,11 @@
             return;
         }
         if (mSurface == null) {
+            Log.d(TAG, "mSurface is null and mCurrentBlur is: " + mCurrentBlur);
             return;
         }
         if (!mSurface.isValid()) {
+            Log.d(TAG, "mSurface is not valid");
             mWaitingOnSurfaceValidity = true;
             onInvalidSurface();
             return;
@@ -186,6 +192,8 @@
     protected void setSurface(SurfaceControl surface) {
         if (mSurface != surface || mWaitingOnSurfaceValidity) {
             mSurface = surface;
+            Log.d(TAG, "setSurface:\n\tmWaitingOnSurfaceValidity: " + mWaitingOnSurfaceValidity
+                    + "\n\tmSurface: " + mSurface);
             applyDepthAndBlur();
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 56d6857..bcb9cec 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -265,6 +265,11 @@
         return anim
     }
 
+    /** Removes the split instructions view from [launcher] drag layer. */
+    fun removeSplitInstructionsView(launcher: StatefulActivity<*>) {
+        safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+    }
+
     private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
         if (view != null) {
             launcher.dragLayer.removeView(view)
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 148a45a..b36cf5f 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -169,6 +169,7 @@
             private void cleanUp() {
                 mLauncher.getDragLayer().removeView(firstFloatingTaskView);
                 mLauncher.getDragLayer().removeView(secondFloatingTaskView);
+                mController.getSplitAnimationController().removeSplitInstructionsView(mLauncher);
                 mController.resetState();
             }
         });
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 1cfaf14..83e9945 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -51,6 +51,7 @@
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.QuickStepContract;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -79,7 +80,7 @@
 
     private static final String TAG = DesktopTaskView.class.getSimpleName();
 
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     @NonNull
     private List<Task> mTasks = new ArrayList<>();
@@ -91,6 +92,8 @@
 
     private final ArrayList<CancellableTask<?>> mPendingThumbnailRequests = new ArrayList<>();
 
+    private final TaskView.FullscreenDrawParams mSnapshotDrawParams;
+
     private View mBackgroundView;
 
     public DesktopTaskView(Context context) {
@@ -103,6 +106,10 @@
 
     public DesktopTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+
+        mSnapshotDrawParams = new FullscreenDrawParams(
+                QuickStepContract.getWindowCornerRadius(context),
+                QuickStepContract.getWindowCornerRadius(context));
     }
 
     @Override
@@ -465,14 +472,20 @@
         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
             TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i);
             thumbnailView.getTaskOverlay().setFullscreenProgress(progress);
-            updateSnapshotRadius();
         }
+        updateSnapshotRadius();
     }
 
     @Override
     protected void updateSnapshotRadius() {
+        super.updateSnapshotRadius();
         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
-            mSnapshotViewMap.valueAt(i).setFullscreenParams(mCurrentFullscreenParams);
+            if (i == 0) {
+                // All snapshots share the same params. Only update it with the first snapshot.
+                updateFullscreenParams(mSnapshotDrawParams,
+                        mSnapshotView.getPreviewPositionHelper());
+            }
+            mSnapshotViewMap.valueAt(i).setFullscreenParams(mSnapshotDrawParams);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 7bbe36a..e5a0e10 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -145,23 +145,29 @@
         mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1;
         mTask = task;
         THREAD_POOL_EXECUTOR.execute(() -> {
-            final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
-                    mTask.getTopComponent().getPackageName(),
-                    UserHandle.of(mTask.key.userId));
+                    AppUsageLimit usageLimit = null;
+                    try {
+                        usageLimit = mLauncherApps.getAppUsageLimit(
+                                mTask.getTopComponent().getPackageName(),
+                                UserHandle.of(mTask.key.userId));
+                    } catch (Exception e) {
+                        Log.e(TAG, "Error initializing digital well being toast", e);
+                    }
+                    final long appUsageLimitTimeMs =
+                            usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
+                    final long appRemainingTimeMs =
+                            usageLimit != null ? usageLimit.getUsageRemaining() : -1;
 
-            final long appUsageLimitTimeMs =
-                    usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
-            final long appRemainingTimeMs =
-                    usageLimit != null ? usageLimit.getUsageRemaining() : -1;
+                    mTaskView.post(() -> {
+                        if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+                            setNoLimit();
+                        } else {
+                            setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
+                        }
+                    });
 
-            mTaskView.post(() -> {
-                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
-                    setNoLimit();
-                } else {
-                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
                 }
-            });
-        });
+        );
     }
 
     public void setSplitConfiguration(SplitBounds splitBounds) {
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 5e488cc..4f119c0 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -249,7 +249,7 @@
         DesktopVisibilityController desktopVisibilityController =
                 mActivity.getDesktopVisibilityController();
         if (desktopVisibilityController != null) {
-            desktopVisibilityController.setGestureInProgress(true);
+            desktopVisibilityController.setRecentsGestureStart();
         }
     }
 
@@ -257,9 +257,11 @@
     public void onGestureAnimationEnd() {
         DesktopVisibilityController desktopVisibilityController = null;
         boolean showDesktopApps = false;
+        GestureState.GestureEndTarget endTarget = null;
         if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
             desktopVisibilityController = mActivity.getDesktopVisibilityController();
-            if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.LAST_TASK
+            endTarget = mCurrentGestureEndTarget;
+            if (endTarget == GestureState.GestureEndTarget.LAST_TASK
                     && desktopVisibilityController.areFreeformTasksVisible()) {
                 // Recents gesture was cancelled and we are returning to the previous task.
                 // After super class has handled clean up, show desktop apps on top again
@@ -268,7 +270,7 @@
         }
         super.onGestureAnimationEnd();
         if (desktopVisibilityController != null) {
-            desktopVisibilityController.setGestureInProgress(false);
+            desktopVisibilityController.setRecentsGestureEnd(endTarget);
         }
         if (showDesktopApps) {
             SystemUiProxy.INSTANCE.get(mActivity).showDesktopApps(mActivity.getDisplayId());
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 754d1ff..bfb578b 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -263,6 +263,13 @@
     }
 
     /**
+     * Returns the visibility of the overview actions buttons.
+     */
+    public @Visibility int getActionsButtonVisibility() {
+        return findViewById(R.id.action_buttons).getVisibility();
+    }
+
+    /**
      * Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar.
      */
     private void updatePadding() {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 0071927..29d9fa6 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -4823,6 +4823,8 @@
             mSecondFloatingTaskView = null;
             mSplitInstructionsView = null;
             mSplitSelectSource = null;
+            mSplitSelectStateController.getSplitAnimationController()
+                    .removeSplitInstructionsView(mActivity);
         }
 
         if (mSecondSplitHiddenView != null) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 6b0843c..854c3c7 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -1707,10 +1707,15 @@
     }
 
     void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
+        updateFullscreenParams(mCurrentFullscreenParams, previewPositionHelper);
+    }
+
+    protected void updateFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams,
+            PreviewPositionHelper previewPositionHelper) {
         if (getRecentsView() == null) {
             return;
         }
-        mCurrentFullscreenParams.setProgress(mFullscreenProgress, getRecentsView().getScaleX(),
+        fullscreenParams.setProgress(mFullscreenProgress, getRecentsView().getScaleX(),
                 getScaleX(), getWidth(), mActivity.getDeviceProfile(), previewPositionHelper);
     }
 
@@ -1860,9 +1865,12 @@
         public float mCurrentDrawnCornerRadius;
 
         public FullscreenDrawParams(Context context) {
-            mCornerRadius = TaskCornerRadius.get(context);
-            mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context);
+            this(TaskCornerRadius.get(context), QuickStepContract.getWindowCornerRadius(context));
+        }
 
+        FullscreenDrawParams(float cornerRadius, float windowCornerRadius) {
+            mCornerRadius = cornerRadius;
+            mWindowCornerRadius = windowCornerRadius;
             mCurrentDrawnCornerRadius = mCornerRadius;
         }
 
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
index 82849be..6c0d44d 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -18,6 +18,7 @@
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
+import static com.android.launcher3.taskbar.TaskbarHoverToolTipController.HOVER_TOOL_TIP_REVEAL_START_DELAY;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -26,6 +27,7 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -56,7 +58,7 @@
  */
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase {
 
     private TaskbarHoverToolTipController mTaskbarHoverToolTipController;
@@ -126,8 +128,10 @@
 
         boolean hoverHandled =
                 mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
-        waitForIdleSync();
 
+        // Verify fullscreen is not set until the delayed runnable to reveal the tooltip has run
+        verify(taskbarActivityContext, never()).setTaskbarWindowFullscreen(true);
+        waitForIdleSync();
         assertThat(hoverHandled).isTrue();
         verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
                 true);
@@ -155,8 +159,10 @@
 
         boolean hoverHandled =
                 mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
-        waitForIdleSync();
 
+        // Verify fullscreen is not set until the delayed runnable to reveal the tooltip has run
+        verify(taskbarActivityContext, never()).setTaskbarWindowFullscreen(true);
+        waitForIdleSync();
         assertThat(hoverHandled).isTrue();
         verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
                 true);
@@ -216,6 +222,7 @@
     }
 
     private void waitForIdleSync() {
+        mTestableLooper.moveTimeForward(HOVER_TOOL_TIP_REVEAL_START_DELAY + 1);
         mTestableLooper.processAllMessages();
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 06ce0e5..9d188ed 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
+import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.util.DisplayController;
@@ -196,6 +197,19 @@
         actionsView.clickAndDismissScreenshot();
     }
 
+
+    @Test
+    public void testOverviewActionsMenu() throws Exception {
+        startTestAppsWithCheck();
+
+        OverviewTaskMenu menu = mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
+
+        assertNotNull("Tapping App info menu item returned null", menu.tapAppInfoMenuItem());
+        executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top",
+                isInLaunchedApp(launcher)));
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml
index 94ef06c..3d7ad2b 100644
--- a/res/anim-v33/shared_x_axis_activity_close_enter.xml
+++ b/res/anim-v33/shared_x_axis_activity_close_enter.xml
@@ -25,7 +25,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_decelerate"
+        android:interpolator="@interpolator/standard_decelerate_interpolator"
         android:startOffset="100"
         android:duration="350" />
 
@@ -35,7 +35,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml
index 19eb09e..fb63602 100644
--- a/res/anim-v33/shared_x_axis_activity_close_exit.xml
+++ b/res/anim-v33/shared_x_axis_activity_close_exit.xml
@@ -24,7 +24,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_accelerate"
+        android:interpolator="@interpolator/standard_accelerate_interpolator"
         android:startOffset="0"
         android:duration="100" />
 
@@ -34,7 +34,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml
index f699cec..cba74ba 100644
--- a/res/anim-v33/shared_x_axis_activity_open_enter.xml
+++ b/res/anim-v33/shared_x_axis_activity_open_enter.xml
@@ -25,7 +25,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_decelerate"
+        android:interpolator="@interpolator/standard_decelerate_interpolator"
         android:startOffset="100"
         android:duration="350" />
 
@@ -35,7 +35,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml
index 85988ec..22e878d 100644
--- a/res/anim-v33/shared_x_axis_activity_open_exit.xml
+++ b/res/anim-v33/shared_x_axis_activity_open_exit.xml
@@ -24,7 +24,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/standard_accelerate"
+        android:interpolator="@interpolator/standard_accelerate_interpolator"
         android:startOffset="0"
         android:duration="100" />
 
@@ -34,7 +34,7 @@
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
-        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:interpolator="@interpolator/emphasized_interpolator"
         android:startOffset="0"
         android:duration="450" />
 
diff --git a/res/interpolator/back_cancel.xml b/res/interpolator/back_cancel.xml
deleted file mode 100644
index 2165457..0000000
--- a/res/interpolator/back_cancel.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2022, 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.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.2"
-    android:controlY1="0"
-    android:controlX2="0"
-    android:controlY2="1"/>
\ No newline at end of file
diff --git a/res/interpolator/fast_out_extra_slow_in.xml b/res/interpolator/fast_out_extra_slow_in.xml
deleted file mode 100644
index f296a82..0000000
--- a/res/interpolator/fast_out_extra_slow_in.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 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
-  -->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/>
\ No newline at end of file
diff --git a/res/interpolator/folder_interpolator.xml b/res/interpolator/folder_interpolator.xml
deleted file mode 100644
index b95d454..0000000
--- a/res/interpolator/folder_interpolator.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2017, 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.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.2"
-    android:controlY1="0"
-    android:controlX2="0"
-    android:controlY2="1"/>
diff --git a/res/interpolator/large_folder_preview_item_close_interpolator.xml b/res/interpolator/large_folder_preview_item_close_interpolator.xml
deleted file mode 100644
index d28af63..0000000
--- a/res/interpolator/large_folder_preview_item_close_interpolator.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2017, 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.
-*/
--->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.3"
-    android:controlY1="0"
-    android:controlX2="1"
-    android:controlY2="1"/>
diff --git a/res/interpolator/standard_accelerate.xml b/res/interpolator/standard_accelerate.xml
deleted file mode 100644
index 394393d..0000000
--- a/res/interpolator/standard_accelerate.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 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.
-  -->
-
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:controlX1="0.3"
-    android:controlY1="0"
-    android:controlX2="1"
-    android:controlY2="1"/>
\ No newline at end of file
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 28e0cf2..9a3480b 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -886,6 +886,10 @@
         updateIconSize(1f, res);
         updateWorkspacePadding();
 
+        if (mIsResponsiveGrid) {
+            return 0;
+        }
+
         // Check to see if the icons fit within the available height.
         float usedHeight = getCellLayoutHeightSpecification();
         final int maxHeight = getCellLayoutHeight();
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index bfbca65..d2ea7cc 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -386,8 +386,7 @@
      * Gets the translation provider for workspace pages.
      */
     public PageTranslationProvider getWorkspacePageTranslationProvider(Launcher launcher) {
-        if (this != SPRING_LOADED
-                || this != EDIT_MODE
+        if (!(this == SPRING_LOADED || this == EDIT_MODE)
                 || !launcher.getDeviceProfile().isTwoPanels) {
             return DEFAULT_PAGE_TRANSLATION_PROVIDER;
         }
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 6ad5221..40382b2 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
+import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
@@ -552,17 +553,14 @@
         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
 
+        final AllAppsRecyclerView mainRecyclerView;
+        final AllAppsRecyclerView workRecyclerView;
         if (mUsingTabs) {
-            mAH.get(AdapterHolder.MAIN).setup(mViewPager.getChildAt(0), mPersonalMatcher);
-            mAH.get(AdapterHolder.WORK).setup(mViewPager.getChildAt(1), mWorkManager.getMatcher());
-            mAH.get(AdapterHolder.WORK).mRecyclerView.setId(R.id.apps_list_view_work);
-            if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
-                // Let main and work rv share same view pool.
-                ((RecyclerView) mViewPager.getChildAt(0))
-                        .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
-                ((RecyclerView) mViewPager.getChildAt(1))
-                        .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
-            }
+            mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0);
+            workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1);
+            mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher);
+            mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher());
+            workRecyclerView.setId(R.id.apps_list_view_work);
             if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
                 mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener(
                         mWorkManager.newScrollListener());
@@ -587,13 +585,15 @@
                 onActivePageChanged(mViewPager.getNextPage());
             }
         } else {
-            mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null);
+            mainRecyclerView = findViewById(R.id.apps_list_view);
+            workRecyclerView = null;
+            mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, null);
             mAH.get(AdapterHolder.WORK).mRecyclerView = null;
-            if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
-                mAH.get(AdapterHolder.MAIN).mRecyclerView
-                        .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
-            }
         }
+        setUpCustomRecyclerViewPool(
+                mainRecyclerView,
+                workRecyclerView,
+                mAllAppsStore.getRecyclerViewPool());
         setupHeader();
 
         if (isSearchBarFloating()) {
@@ -610,6 +610,30 @@
         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.SEARCH).mRecyclerView);
     }
 
+    /**
+     * If {@link ENABLE_ALL_APPS_RV_PREINFLATION} is enabled, wire custom
+     * {@link RecyclerView.RecycledViewPool} to main and work {@link AllAppsRecyclerView}.
+     *
+     * Then if {@link ALL_APPS_GONE_VISIBILITY} is enabled, update max pool size. This is because
+     * all apps rv's hidden visibility is changed to {@link View#GONE} from {@link View#INVISIBLE),
+     * thus we cannot rely on layout pass to update pool size.
+     */
+    private static void setUpCustomRecyclerViewPool(
+            @NonNull AllAppsRecyclerView mainRecyclerView,
+            @Nullable AllAppsRecyclerView workRecyclerView,
+            @NonNull RecyclerView.RecycledViewPool recycledViewPool) {
+        if (!ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+            return;
+        }
+        mainRecyclerView.setRecycledViewPool(recycledViewPool);
+        if (workRecyclerView != null) {
+            workRecyclerView.setRecycledViewPool(recycledViewPool);
+        }
+        if (ALL_APPS_GONE_VISIBILITY.get()) {
+            mainRecyclerView.updatePoolSize();
+        }
+    }
+
     private void replaceAppsRVContainer(boolean showTabs) {
         for (int i = AdapterHolder.MAIN; i <= AdapterHolder.WORK; i++) {
             AdapterHolder adapterHolder = mAH.get(i);
@@ -910,6 +934,7 @@
     public void onDeviceProfileChanged(DeviceProfile dp) {
         for (AdapterHolder holder : mAH) {
             holder.mAdapter.setAppsPerRow(dp.numShownAllAppsColumns);
+            holder.mAppsList.setNumAppsPerRowAllApps(dp.numShownAllAppsColumns);
             if (holder.mRecyclerView != null) {
                 // Remove all views and clear the pool, while keeping the data same. After this
                 // call, all the viewHolders will be recreated.
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 602d1a3..7edbeac 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN;
@@ -26,6 +27,8 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND;
+import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.EXTRA_ICONS_COUNT;
+import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.PREINFLATE_ICONS_ROW_COUNT;
 import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING;
 
 import android.content.Context;
@@ -96,8 +99,18 @@
         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
+
+        // If all apps' hidden visibility is INVISIBLE, we will need to preinflate one page of
+        // all apps icons for smooth scrolling.
+        int maxPoolSizeForAppIcons = (approxRows + 1) * grid.numShownAllAppsColumns;
+        if (ALL_APPS_GONE_VISIBILITY.get()) {
+            // If all apps' hidden visibility is GONE, we need to increase prefinated icons number
+            // by [PREINFLATE_ICONS_ROW_COUNT] rows + [EXTRA_ICONS_COUNT] for fast opening all apps.
+            maxPoolSizeForAppIcons +=
+                    PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT;
+        }
         pool.setMaxRecycledViews(
-                AllAppsGridAdapter.VIEW_TYPE_ICON, (approxRows + 1) * grid.numShownAllAppsColumns);
+                AllAppsGridAdapter.VIEW_TYPE_ICON, maxPoolSizeForAppIcons);
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 0d7b736..c09a5b9 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -438,7 +438,8 @@
         mAppsView = appsView;
         mAppsView.setScrimView(scrimView);
 
-        mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT);
+        mAppsViewAlpha = new MultiValueAlpha(mAppsView, APPS_VIEW_INDEX_COUNT,
+                FeatureFlags.ALL_APPS_GONE_VISIBILITY.get() ? View.GONE : View.INVISIBLE);
         mAppsViewAlpha.setUpdateVisibility(true);
         mAppsViewTranslationY = new MultiPropertyFactory<>(
                 mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum);
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 0657178..ee4b5bc 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -82,7 +82,7 @@
     private final ArrayList<AdapterItem> mSearchResults = new ArrayList<>();
     private BaseAllAppsAdapter<T> mAdapter;
     private AppInfoComparator mAppNameComparator;
-    private final int mNumAppsPerRowAllApps;
+    private int mNumAppsPerRowAllApps;
     private int mNumAppRowsInAdapter;
     private Predicate<ItemInfo> mItemFilter;
 
@@ -92,12 +92,17 @@
         mActivityContext = ActivityContext.lookupContext(context);
         mAppNameComparator = new AppInfoComparator(context);
         mWorkProviderManager = workProfileManager;
-        mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().inv.numAllAppsColumns;
+        mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
         if (mAllAppsStore != null) {
             mAllAppsStore.addUpdateListener(this);
         }
     }
 
+    /** Set the number of apps per row when device profile changes. */
+    public void setNumAppsPerRowAllApps(int numAppsPerRow) {
+        mNumAppsPerRowAllApps = numAppsPerRow;
+    }
+
     public void updateItemFilter(Predicate<ItemInfo> itemFilter) {
         this.mItemFilter = itemFilter;
         onAppsUpdated();
diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java
index 8dad1b4..4382174 100644
--- a/src/com/android/launcher3/anim/AlphaUpdateListener.java
+++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java
@@ -53,8 +53,18 @@
     }
 
     public static void updateVisibility(View view) {
-        if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != View.INVISIBLE) {
-            view.setVisibility(View.INVISIBLE);
+        updateVisibility(view, View.INVISIBLE);
+    }
+
+    /**
+     * Update view's visibility.
+     *
+     * @param view View that needs to update visibility.
+     * @param hiddenVisibility {@link View#GONE} or {@link View#INVISIBLE}
+     */
+    public static void updateVisibility(View view, int hiddenVisibility) {
+        if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != hiddenVisibility) {
+            view.setVisibility(hiddenVisibility);
         } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD
                 && view.getVisibility() != View.VISIBLE) {
             if (view instanceof ViewGroup) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index d2f37fb..1f68e11 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -83,11 +83,11 @@
      */
     // TODO(Block 1): Clean up flags
     public static final BooleanFlag ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES = getReleaseFlag(
-            270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", DISABLED,
+            270394041, "ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES", TEAMFOOD,
             "Enable option to replace decorator-based search result backgrounds with drawables");
 
     public static final BooleanFlag ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION = getReleaseFlag(
-            270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", DISABLED,
+            270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", TEAMFOOD,
             "Enable option to launch search results using the new view container transitions");
 
     // TODO(Block 2): Clean up flags
@@ -398,7 +398,12 @@
             "ENABLE_ALL_APPS_RV_PREINFLATION", DISABLED,
             "Enables preinflating all apps icons to avoid scrolling jank.");
 
-    // TODO(Block 34): Empty block
+    // TODO(Block 34): Clean up flags
+    public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514,
+            "ALL_APPS_GONE_VISIBILITY", DISABLED,
+            "Set all apps container view's hidden visibility to GONE instead of INVISIBLE.");
+
+    // TODO(Block 35): Empty block
 
     public static class BooleanFlag {
 
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index b09985c..9e2e2bf 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -105,11 +105,11 @@
         mDelay = res.getInteger(R.integer.config_folderDelay);
 
         mFolderInterpolator = AnimationUtils.loadInterpolator(mContext,
-                R.interpolator.folder_interpolator);
+                R.interpolator.standard_interpolator);
         mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext,
                 R.interpolator.large_folder_preview_item_open_interpolator);
         mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext,
-                R.interpolator.large_folder_preview_item_close_interpolator);
+                R.interpolator.standard_accelerate_interpolator);
     }
 
     /**
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index ba1547f..9afa459 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -49,7 +49,6 @@
 import com.android.launcher3.LauncherSettings.Animation;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.AllAppsContainer;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
@@ -323,9 +322,7 @@
      * Returns whether this item should use the background animation.
      */
     public boolean shouldUseBackgroundAnimation() {
-        return animationType == LauncherSettings.Animation.VIEW_BACKGROUND
-                && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()
-                && FeatureFlags.ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION.get();
+        return animationType == LauncherSettings.Animation.VIEW_BACKGROUND;
     }
 
     /**
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 26dde29..3c59c1d 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -22,13 +22,14 @@
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.android.launcher3.BubbleTextView
 import com.android.launcher3.allapps.BaseAllAppsAdapter
+import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
 import com.android.launcher3.views.ActivityContext
 import java.util.concurrent.Future
 
-private const val PREINFLATE_ICONS_ROW_COUNT = 4
-private const val EXTRA_ICONS_COUNT = 2
+const val PREINFLATE_ICONS_ROW_COUNT = 4
+const val EXTRA_ICONS_COUNT = 2
 
 /**
  * An [RecycledViewPool] that preinflates app icons ([ViewHolder] of [BubbleTextView]) of all apps
@@ -81,11 +82,21 @@
      * After testing on phone, foldable and tablet, we found [PREINFLATE_ICONS_ROW_COUNT] rows of
      * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
      * suffice fast scrolling.
+     *
+     * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
+     * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
+     * icons.
      */
     fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
-        val targetPreinflateCount =
+        var targetPreinflateCount =
             PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
                 EXTRA_ICONS_COUNT
+        if (FeatureFlags.ALL_APPS_GONE_VISIBILITY.get()) {
+            val grid = ActivityContext.lookupContext<T>(context).deviceProfile
+            val approxRows =
+                Math.ceil((grid.availableHeightPx / grid.allAppsIconSizePx).toDouble()).toInt()
+            targetPreinflateCount += (approxRows + 1) * grid.numShownAllAppsColumns
+        }
         val existingPreinflateCount = getRecycledViewCount(BaseAllAppsAdapter.VIEW_TYPE_ICON)
         return targetPreinflateCount - existingPreinflateCount
     }
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index ac016a8..a66a9d2 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -32,8 +32,15 @@
     // Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
     private boolean mUpdateVisibility;
 
+    private final int mHiddenVisibility;
+
     public MultiValueAlpha(View view, int size) {
+        this(view, size, View.INVISIBLE);
+    }
+
+    public MultiValueAlpha(View view, int size, int hiddenVisibility) {
         super(view, VIEW_ALPHA, size, ALPHA_AGGREGATOR, 1f);
+        this.mHiddenVisibility = hiddenVisibility;
     }
 
     /** Sets whether we should update between INVISIBLE and VISIBLE based on alpha. */
@@ -45,7 +52,7 @@
     protected void apply(float value) {
         super.apply(value);
         if (mUpdateVisibility) {
-            AlphaUpdateListener.updateVisibility(mTarget);
+            AlphaUpdateListener.updateVisibility(mTarget, mHiddenVisibility);
         }
     }
 }
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index de10fc5..f69d299 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -24,8 +24,7 @@
 import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS;
 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
 import android.content.Context;
@@ -51,6 +50,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 
@@ -82,15 +82,19 @@
     protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
     private static final float VIEW_NO_SCALE = 1f;
+    private static final int NO_DURATION = -1;
 
     protected final T mActivityContext;
 
     protected final SingleAxisSwipeDetector mSwipeDetector;
-    protected final ObjectAnimator mOpenCloseAnimator;
+    protected @NonNull AnimatorSet mOpenCloseAnimator;
+    private final ObjectAnimator mTranslationShiftAnimator;
 
     protected ViewGroup mContent;
     protected final View mColorScrim;
-    protected Interpolator mScrollInterpolator;
+
+    private Interpolator mScrollInterpolator;
+    private long mScrollDuration;
 
     // range [0, 1], 0=> completely open, 1=> completely closed
     protected float mTranslationShift = TRANSLATION_SHIFT_CLOSED;
@@ -104,8 +108,8 @@
     protected final AnimatedFloat mSlideInViewScale =
             new AnimatedFloat(this::onScaleProgressChanged, VIEW_NO_SCALE);
     protected boolean mIsBackProgressing;
-    @Nullable private Drawable mContentBackground;
-    @Nullable private View mContentBackgroundParentView;
+    private @Nullable Drawable mContentBackground;
+    private @Nullable View mContentBackgroundParentView;
 
     protected final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
         @Override
@@ -124,21 +128,52 @@
         mActivityContext = ActivityContext.lookupContext(context);
 
         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
+        mScrollDuration = NO_DURATION;
         mSwipeDetector = new SingleAxisSwipeDetector(context, this,
                 SingleAxisSwipeDetector.VERTICAL);
 
-        mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
-        mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mSwipeDetector.finishedScrolling();
-                announceAccessibilityChanges();
-            }
-        });
+        mOpenCloseAnimator = new AnimatorSet();
+        mTranslationShiftAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
+
         int scrimColor = getScrimColor(context);
         mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
     }
 
+    /**
+     * Sets up a {@link #mOpenCloseAnimator} for opening with default parameters.
+     *
+     * @see #setUpOpenCloseAnimator(float, Interpolator)
+     */
+    protected final AnimatorSet setUpDefaultOpenAnimator() {
+        return setUpOpenCloseAnimator(TRANSLATION_SHIFT_OPENED, Interpolators.FAST_OUT_SLOW_IN);
+    }
+
+    /**
+     * Initializes a new {@link #mOpenCloseAnimator}.
+     * <p>
+     * Subclasses should override this method if they want to add more {@code Animator} instances
+     * to the set.
+     *
+     * @param translationShift             translation shift to animate to.
+     * @param translationShiftInterpolator interpolator for {@link #mTranslationShiftAnimator}.
+     * @return {@link #mOpenCloseAnimator}
+     */
+    protected AnimatorSet setUpOpenCloseAnimator(
+            float translationShift, Interpolator translationShiftInterpolator) {
+        mOpenCloseAnimator = new AnimatorSet();
+        mOpenCloseAnimator.addListener(AnimatorListeners.forEndCallback(() -> {
+            mSwipeDetector.finishedScrolling();
+            announceAccessibilityChanges();
+        }));
+
+        mTranslationShiftAnimator.setValues(PropertyValuesHolder.ofFloat(
+                TRANSLATION_SHIFT, translationShift));
+        mTranslationShiftAnimator.setInterpolator(translationShiftInterpolator);
+        mOpenCloseAnimator.play(mTranslationShiftAnimator);
+
+        return mOpenCloseAnimator;
+    }
+
     protected void attachToContainer() {
         if (mColorScrim != null) {
             getPopupContainer().addView(mColorScrim);
@@ -309,16 +344,13 @@
         if ((mSwipeDetector.isFling(velocity) && velocity > 0)
                 || mTranslationShift > successfulShiftThreshold) {
             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
-            mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
-                    velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
+            mScrollDuration = BaseSwipeDetector.calculateDuration(
+                    velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift);
             close(true);
         } else {
-            mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
-                    TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator.setDuration(
-                    BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
-                    .setInterpolator(Interpolators.DECELERATE);
-            mOpenCloseAnimator.start();
+            setUpOpenCloseAnimator(TRANSLATION_SHIFT_OPENED, Interpolators.DECELERATE)
+                    .setDuration(BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
+                    .start();
         }
     }
 
@@ -344,23 +376,19 @@
             onCloseComplete();
             return;
         }
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
-        mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator.removeListener(this);
-                onCloseComplete();
-            }
-        });
+
+        final Interpolator interpolator;
+        final long duration;
         if (mSwipeDetector.isIdleState()) {
-            mOpenCloseAnimator
-                    .setDuration(defaultDuration)
-                    .setInterpolator(getIdleInterpolator());
+            interpolator = getIdleInterpolator();
+            duration = defaultDuration;
         } else {
-            mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
+            interpolator = mScrollInterpolator;
+            duration = mScrollDuration > NO_DURATION ? mScrollDuration : defaultDuration;
         }
-        mOpenCloseAnimator.start();
+        setUpOpenCloseAnimator(TRANSLATION_SHIFT_CLOSED, interpolator)
+                .addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
+        mOpenCloseAnimator.setDuration(duration).start();
     }
 
     protected Interpolator getIdleInterpolator() {
diff --git a/src/com/android/launcher3/views/WidgetsEduView.java b/src/com/android/launcher3/views/WidgetsEduView.java
index 9180781..92e048b 100644
--- a/src/com/android/launcher3/views/WidgetsEduView.java
+++ b/src/com/android/launcher3/views/WidgetsEduView.java
@@ -15,9 +15,6 @@
  */
 package com.android.launcher3.views;
 
-import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
-
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -123,10 +120,7 @@
             return;
         }
         mIsOpen = true;
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimator().start();
     }
 
     /** Shows widget education dialog. */
diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
index 473abf1..c6fc5fe 100644
--- a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
@@ -16,10 +16,8 @@
 
 package com.android.launcher3.widget;
 
-import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.Utilities.ATLEAST_R;
 
-import android.animation.PropertyValuesHolder;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Insets;
@@ -134,10 +132,7 @@
             return;
         }
         mIsOpen = true;
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimator().start();
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 93f7cb3..82394f1 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -16,10 +16,8 @@
 
 package com.android.launcher3.widget;
 
-import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
 
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -231,10 +229,7 @@
         }
         mIsOpen = true;
         setupNavBarColor();
-        mOpenCloseAnimator.setValues(
-                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-        mOpenCloseAnimator.setInterpolator(FAST_OUT_SLOW_IN);
-        mOpenCloseAnimator.start();
+        setUpDefaultOpenAnimator().start();
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index c9cd4b6..43f1846 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -22,9 +22,6 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.content.res.Configuration;
@@ -627,20 +624,14 @@
                 mContent.setAlpha(0);
                 setTranslationShift(VERTICAL_START_POSITION);
             }
-            mOpenCloseAnimator.setValues(
-                    PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator
-                    .setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
-                    .setInterpolator(AnimationUtils.loadInterpolator(
+            setUpOpenCloseAnimator(
+                    TRANSLATION_SHIFT_OPENED,
+                    AnimationUtils.loadInterpolator(
                             getContext(), android.R.interpolator.linear_out_slow_in));
-            mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mOpenCloseAnimator.removeListener(this);
-                }
-            });
             post(() -> {
-                mOpenCloseAnimator.start();
+                mOpenCloseAnimator
+                        .setDuration(mActivityContext.getDeviceProfile().bottomSheetOpenDuration)
+                        .start();
                 mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
             });
         } else {
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 28688fd..bb61fbe 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -32,6 +32,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
             android:exported="true"
+            android:icon="@drawable/test_widget_no_config_icon"
             android:label="No Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -65,6 +66,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
             android:exported="true"
+            android:icon="@drawable/test_widget_with_config_icon"
             android:label="With Config">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -76,6 +78,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetWithDialog"
             android:exported="true"
+            android:icon="@drawable/test_widget_with_dialog_icon"
             android:label="With Dialog">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
@@ -87,6 +90,7 @@
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWidgetDynamicColors"
             android:exported="true"
+            android:icon="@drawable/test_widget_dynamic_colors_icon"
             android:label="Dynamic Colors">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
diff --git a/tests/res/drawable/test_widget_dynamic_colors_icon.xml b/tests/res/drawable/test_widget_dynamic_colors_icon.xml
new file mode 100644
index 0000000..69f6675
--- /dev/null
+++ b/tests/res/drawable/test_widget_dynamic_colors_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#964B00"/>
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_no_config_icon.xml b/tests/res/drawable/test_widget_no_config_icon.xml
new file mode 100644
index 0000000..e3d0125
--- /dev/null
+++ b/tests/res/drawable/test_widget_no_config_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#00FFFF"/>
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_with_config_icon.xml b/tests/res/drawable/test_widget_with_config_icon.xml
new file mode 100644
index 0000000..98b797b
--- /dev/null
+++ b/tests/res/drawable/test_widget_with_config_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#008000" />
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_widget_with_dialog_icon.xml b/tests/res/drawable/test_widget_with_dialog_icon.xml
new file mode 100644
index 0000000..d2879d2
--- /dev/null
+++ b/tests/res/drawable/test_widget_with_dialog_icon.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2023 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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#800080"/>
+    </foreground>
+    <monochrome>
+        <vector android:width="48dp" android:height="48dp" android:viewportWidth="48.0" android:viewportHeight="48.0">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M0,24L48,24 48,48, 0,48 Z"/>
+        </vector>
+    </monochrome>
+</adaptive-icon>
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index b67478f..753d89d 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -318,6 +318,7 @@
     }
 
     @Test
+    @Ignore // b/293191790
     @PortraitLandscape
     public void testWidgets() throws Exception {
         // Test opening widgets.
diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
index b4ad1f3..f3f9b89 100644
--- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
+++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt
@@ -26,8 +26,13 @@
 import com.android.launcher3.tapl.TestHelpers
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
 import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer
-import org.junit.Assert.assertTrue
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStreamWriter
 import java.util.function.Supplier
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -81,7 +86,7 @@
                     MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) }
                 }
 
-                analyzeViewCapture()
+                analyzeViewCapture(description)
             }
 
             private fun startCapturingExistingActivity(
@@ -107,16 +112,39 @@
         }
     }
 
-    private fun analyzeViewCapture() {
+    private fun analyzeViewCapture(description: Description) {
         // OOP tests don't produce ViewCapture data
         if (!TestHelpers.isInLauncherProcess()) return
 
-        ViewCaptureAnalyzer.assertNoAnomalies(viewCaptureData)
-
         var frameCount = 0
         for (i in 0 until viewCaptureData!!.windowDataCount) {
             frameCount += viewCaptureData!!.getWindowData(i).frameDataCount
         }
         assertTrue("Empty ViewCapture data", frameCount > 0)
+
+        val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData)
+        if (!anomalies.isEmpty()) {
+            val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt")
+            try {
+                OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer ->
+                    writer.write("View animation anomalies detected.\r\n")
+                    writer.write(
+                        "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n"
+                    )
+                    writer.write("List of views with animation anomalies:\r\n")
+
+                    for ((viewPath, message) in anomalies) {
+                        writer.write("View: $viewPath\r\n        $message\r\n")
+                    }
+                }
+            } catch (ex: IOException) {
+                throw RuntimeException(ex)
+            }
+
+            val (viewPath, message) = anomalies.entries.first()
+            fail(
+                "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details."
+            )
+        }
     }
 }
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
index cb404b9..a147350 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
@@ -113,7 +113,7 @@
             DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
             DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
             CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
-            RECENTS_DRAG_LAYER + "ArrowTipView|View:id/arrow",
+            RECENTS_DRAG_LAYER + "ArrowTipView",
             DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
             RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel",
             DRAG_LAYER
@@ -133,7 +133,8 @@
                     + "|LinearLayout:id/action_buttons|ImageButton:id/action_split",
             RECENTS_DRAG_LAYER
                     + "NexusOverviewActionsView:id/overview_actions_view"
-                    + "|LinearLayout:id/action_buttons|ImageButton:id/action_split"
+                    + "|LinearLayout:id/action_buttons|ImageButton:id/action_split",
+            DRAG_LAYER + "IconView"
     );
 
     /**
@@ -212,24 +213,26 @@
     }
 
     @Override
-    void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) {
+    String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) {
         // If the view was previously seen, proceed with analysis only if it was present in the
         // view hierarchy in the previous frame.
-        if (oldInfo != null && oldInfo.frameN != frameN) return;
+        if (oldInfo != null && oldInfo.frameN != frameN) return null;
 
         final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
-        if (getNodeData(latestInfo).ignoreAlphaJumps) return;
+        final NodeData nodeData = getNodeData(latestInfo);
+        if (nodeData.ignoreAlphaJumps) return null;
 
         final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0;
         final float newAlpha = newInfo != null ? newInfo.alpha : 0;
         final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha);
 
         if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) {
-            throw new AssertionError(
-                    String.format(
-                            "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)"
-                                    + ", threshold: %s, view: %s",
-                            alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo));
+            nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children.
+            return String.format(
+                    "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)"
+                            + ", threshold: %s, %s", // ----------- no need to include view?
+                    alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo);
         }
+        return null;
     }
 }
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
index 2cf3843..949c536 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
@@ -61,8 +61,9 @@
          *                the view is not present in the 'currentFrame', but was present in earlier
          *                frames.
          * @param frameN  number of the current frame.
+         * @return Anomaly diagnostic message if an anomaly has been detected; null otherwise.
          */
-        abstract void detectAnomalies(
+        abstract String detectAnomalies(
                 @Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN);
     }
 
@@ -101,25 +102,31 @@
 
         @Override
         public String toString() {
-            return String.format("window coordinates: (%s, %s), class path from the root: %s",
-                    left, top, diagPathFromRoot(this));
+            return String.format("view window coordinates: (%s, %s)", left, top);
         }
     }
 
     /**
-     * Scans a view capture record and throws an error if an anomaly is found.
+     * Scans a view capture record and searches for view animation anomalies. Can find anomalies for
+     * multiple views.
+     * Returns a map from the view path to the anomaly message for the view. Non-empty map means
+     * that anomalies were detected.
      */
-    public static void assertNoAnomalies(ExportedData viewCaptureData) {
+    public static Map<String, String> getAnomalies(ExportedData viewCaptureData) {
+        final Map<String, String> anomalies = new HashMap<>();
+
         final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS);
 
         final int windowDataCount = viewCaptureData.getWindowDataCount();
         for (int i = 0; i < windowDataCount; ++i) {
-            analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex);
+            analyzeWindowData(
+                    viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies);
         }
+        return anomalies;
     }
 
     private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData,
-            int scrimClassIndex) {
+            int scrimClassIndex, Map<String, String> anomalies) {
         // View hash code => Last seen node with this hash code.
         // The view is added when we analyze the first frame where it's visible.
         // After that, it gets updated for every frame where it's visible.
@@ -128,12 +135,13 @@
 
         for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
             analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes,
-                    scrimClassIndex);
+                    scrimClassIndex, anomalies);
         }
     }
 
     private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData,
-            Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
+            Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
+            Map<String, String> anomalies) {
         // Analyze the node tree starting from the root.
         analyzeView(
                 frame.getNode(),
@@ -143,7 +151,8 @@
                 /* topShift = */ 0,
                 viewCaptureData,
                 lastSeenNodes,
-                scrimClassIndex);
+                scrimClassIndex,
+                anomalies);
 
         // Analyze transitions when a view visible in the last frame become invisible in the
         // current one.
@@ -151,10 +160,14 @@
             if (info.frameN == frameN - 1) {
                 if (!info.viewCaptureNode.getWillNotDraw()) {
                     Arrays.stream(ANOMALY_DETECTORS).forEach(
-                            detector -> detector.detectAnomalies(
-                                    /* oldInfo = */ info,
-                                    /* newInfo = */ null,
-                                    frameN));
+                            detector ->
+                                    detectAnomaly(
+                                            detector,
+                                            frameN,
+                                            /* oldInfo = */ info,
+                                            /* newInfo = */ null,
+                                            anomalies)
+                    );
                 }
             }
         }
@@ -162,7 +175,8 @@
 
     private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN,
             float leftShift, float topShift, ExportedData viewCaptureData,
-            Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
+            Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
+            Map<String, String> anomalies) {
         // Skip analysis of invisible views
         final float parentAlpha = parent != null ? parent.alpha : 1;
         final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
@@ -205,7 +219,10 @@
         final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null
         if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) {
             Arrays.stream(ANOMALY_DETECTORS).forEach(
-                    detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN));
+                    detector ->
+                            detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode,
+                                    anomalies)
+            );
         }
         lastSeenNodes.put(hashcode, newAnalysisNode);
 
@@ -220,8 +237,20 @@
             if (child.getClassnameIndex() == scrimClassIndex) break;
 
             analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren,
-                    viewCaptureData, lastSeenNodes,
-                    scrimClassIndex);
+                    viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies);
+        }
+    }
+
+    private static void detectAnomaly(AnomalyDetector detector, int frameN,
+            AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode,
+            Map<String, String> anomalies) {
+        final String maybeAnomaly =
+                detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN);
+        if (maybeAnomaly != null) {
+            final String viewDiagPath = diagPathFromRoot(newAnalysisNode);
+            if (!anomalies.containsKey(viewDiagPath)) {
+                anomalies.put(viewDiagPath, maybeAnomaly);
+            }
         }
     }
 
@@ -235,9 +264,11 @@
         return className.substring(className.lastIndexOf(".") + 1);
     }
 
-    private static String diagPathFromRoot(AnalysisNode nodeBox) {
-        final StringBuilder path = new StringBuilder(nodeBox.nodeIdentity);
-        for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) {
+    private static String diagPathFromRoot(AnalysisNode analysisNode) {
+        final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity);
+        for (AnalysisNode ancestor = analysisNode.parent;
+                ancestor != null;
+                ancestor = ancestor.parent) {
             path.insert(0, ancestor.nodeIdentity + "|");
         }
         return path.toString();
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index e349620..7c29a6c 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -50,6 +50,24 @@
         }
     }
 
+    /** Taps the app info item from the overview task menu and returns the LaunchedAppState
+     * representing the App info settings page. */
+    @NonNull
+    public LaunchedAppState tapAppInfoMenuItem() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "before tapping the app info menu item")) {
+            mLauncher.clickLauncherObject(
+                    mLauncher.findObjectInContainer(mMenu, By.text("App info")));
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "tapped app info menu item")) {
+                mLauncher.waitUntilSystemLauncherObjectGone("overview_panel");
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
     /** Returns true if an item matching the given string is present in the menu. */
     public boolean hasMenuItem(String expectedMenuItemText) {
         UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));