Merge "Use bigger task size in app to overview carousel" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 9f6994a..8d83716 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -222,8 +222,7 @@
     public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
         // TODO(b/279514548) Cleans up bad state that can occur when user interacts with
         // taskbar on top of transparent activity.
-        if (!FeatureFlags.enableHomeTransitionListener()
-                && (finalState == LauncherState.NORMAL)
+        if ((finalState == LauncherState.NORMAL)
                 && mLauncher.hasBeenResumed()) {
             updateStateForFlag(FLAG_VISIBLE, true);
             applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index db7d0eb..7a69c55 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -21,6 +21,7 @@
 import static com.android.app.animation.Interpolators.FINAL_FRAME;
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.internal.jank.InteractionJankMonitor.Configuration;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
@@ -572,7 +573,8 @@
             mAnimator.cancel();
         }
         mAnimator = new AnimatorSet();
-        addJankMonitorListener(mAnimator, /* appearing= */ !mIsStashed);
+        addJankMonitorListener(
+                mAnimator, /* expanding= */ !mIsStashed, /* animationType= */ animationType);
         boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
         final float stashTranslation = mActivity.isPhoneMode() || isTransientTaskbar
                 ? 0
@@ -798,14 +800,20 @@
         as.play(a);
     }
 
-    private void addJankMonitorListener(AnimatorSet animator, boolean expanding) {
+    private void addJankMonitorListener(
+            AnimatorSet animator, boolean expanding, @StashAnimation int animationType) {
         View v = mControllers.taskbarActivityContext.getDragLayer();
         int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND :
                 InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE;
         animator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
-                InteractionJankMonitor.getInstance().begin(v, action);
+                final Configuration.Builder builder =
+                        Configuration.Builder.withView(action, v);
+                if (animationType == TRANSITION_HOME_TO_APP) {
+                    builder.setTag("HOME_TO_APP");
+                }
+                InteractionJankMonitor.getInstance().begin(builder);
             }
 
             @Override
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index d617828..cb0aa8f 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
 import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED;
@@ -54,15 +53,11 @@
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.provider.Settings;
-import android.util.Log;
-import android.view.ISystemGestureExclusionListener;
-import android.view.IWindowManager;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
-import android.view.WindowManagerGlobal;
 
-import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.config.FeatureFlags;
@@ -73,6 +68,8 @@
 import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.GestureExclusionManager;
+import com.android.quickstep.util.GestureExclusionManager.ExclusionListener;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -86,7 +83,7 @@
 /**
  * Manages the state of the system during a swipe up gesture.
  */
-public class RecentsAnimationDeviceState implements DisplayInfoChangeListener {
+public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener {
 
     private static final String TAG = "RecentsAnimationDeviceState";
 
@@ -99,25 +96,9 @@
     private final Context mContext;
     private final DisplayController mDisplayController;
 
-    private final IWindowManager mIWindowManager;
+    private final GestureExclusionManager mExclusionManager;
 
-    @VisibleForTesting
-    final ISystemGestureExclusionListener mGestureExclusionListener =
-            new ISystemGestureExclusionListener.Stub() {
-                @BinderThread
-                @Override
-                public void onSystemGestureExclusionChanged(int displayId,
-                        Region systemGestureExclusionRegion, Region unrestrictedOrNull) {
-                    if (displayId != DEFAULT_DISPLAY) {
-                        return;
-                    }
-                    // Assignments are atomic, it should be safe on binder thread. Also we don't
-                    // think systemGestureExclusionRegion can be null but just in case, don't
-                    // let mExclusionRegion be null.
-                    mExclusionRegion = systemGestureExclusionRegion != null
-                            ? systemGestureExclusionRegion : new Region();
-                }
-            };
+
     private final RotationTouchHelper mRotationTouchHelper;
     private final TaskStackChangeListener mPipListener;
     // Cache for better performance since it doesn't change at runtime.
@@ -140,20 +121,20 @@
     private boolean mPipIsActive;
 
     private int mGestureBlockingTaskId = -1;
-    private @NonNull Region mExclusionRegion = new Region();
+    private @NonNull Region mExclusionRegion = GestureExclusionManager.EMPTY_REGION;
     private boolean mExclusionListenerRegistered;
 
     public RecentsAnimationDeviceState(Context context) {
-        this(context, false, WindowManagerGlobal.getWindowManagerService());
+        this(context, false, GestureExclusionManager.INSTANCE);
     }
 
     public RecentsAnimationDeviceState(Context context, boolean isInstanceForTouches) {
-        this(context, isInstanceForTouches, WindowManagerGlobal.getWindowManagerService());
+        this(context, isInstanceForTouches, GestureExclusionManager.INSTANCE);
     }
 
     @VisibleForTesting
-    RecentsAnimationDeviceState(Context context, IWindowManager windowManager) {
-        this(context, false, windowManager);
+    RecentsAnimationDeviceState(Context context, GestureExclusionManager exclusionManager) {
+        this(context, false, exclusionManager);
     }
 
     /**
@@ -162,10 +143,10 @@
      */
     RecentsAnimationDeviceState(
             Context context, boolean isInstanceForTouches,
-            IWindowManager windowManager) {
+            GestureExclusionManager exclusionManager) {
         mContext = context;
         mDisplayController = DisplayController.INSTANCE.get(context);
-        mIWindowManager = windowManager;
+        mExclusionManager = exclusionManager;
         mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
         mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
         if (isInstanceForTouches) {
@@ -276,43 +257,33 @@
         }
     }
 
-    /**
-     * Registers {@link mGestureExclusionListener} for getting exclusion rect changes. Note that we
-     * make binder call on {@link UI_HELPER_EXECUTOR} to avoid jank.
-     */
-    public void registerExclusionListener() {
-        UI_HELPER_EXECUTOR.execute(() -> {
-            if (mExclusionListenerRegistered) {
-                return;
-            }
-            try {
-                mIWindowManager.registerSystemGestureExclusionListener(
-                        mGestureExclusionListener, DEFAULT_DISPLAY);
-                mExclusionListenerRegistered = true;
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to register window manager callbacks", e);
-            }
-        });
+    @Override
+    public void onGestureExclusionChanged(@Nullable Region exclusionRegion,
+            @Nullable Region unrestrictedOrNull) {
+        mExclusionRegion = exclusionRegion != null
+                ? exclusionRegion : GestureExclusionManager.EMPTY_REGION;
     }
 
     /**
-     * Unregisters {@link mGestureExclusionListener} if previously registered. We make binder call
-     * on same {@link UI_HELPER_EXECUTOR} as in {@link #registerExclusionListener()} so that
-     * read/write {@link mExclusionListenerRegistered} field is thread safe.
+     * Registers itself for getting exclusion rect changes.
+     */
+    public void registerExclusionListener() {
+        if (mExclusionListenerRegistered) {
+            return;
+        }
+        mExclusionManager.addListener(this);
+        mExclusionListenerRegistered = true;
+    }
+
+    /**
+     * Unregisters itself as gesture exclusion listener if previously registered.
      */
     public void unregisterExclusionListener() {
-        UI_HELPER_EXECUTOR.execute(() -> {
-            if (!mExclusionListenerRegistered) {
-                return;
-            }
-            try {
-                mIWindowManager.unregisterSystemGestureExclusionListener(
-                        mGestureExclusionListener, DEFAULT_DISPLAY);
-                mExclusionListenerRegistered = false;
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to unregister window manager callbacks", e);
-            }
-        });
+        if (!mExclusionListenerRegistered) {
+            return;
+        }
+        mExclusionManager.removeListener(this);
+        mExclusionListenerRegistered = false;
     }
 
     public void onOneHandedModeChanged(int newGesturalHeight) {
@@ -515,10 +486,8 @@
      *         This is only used for quickswitch, and not swipe up.
      */
     public boolean isInExclusionRegion(MotionEvent event) {
-        // mExclusionRegion can change on binder thread, use a local instance here.
-        Region exclusionRegion = mExclusionRegion;
         return mMode == NO_BUTTON
-                && exclusionRegion.contains((int) event.getX(), (int) event.getY());
+                && mExclusionRegion.contains((int) event.getX(), (int) event.getY());
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
index 24b48db..8648b56 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
@@ -602,7 +602,7 @@
                 : iconMenuParams.width / 2f);
         iconAppChipView.setPivotY(
                 isRtl ? (iconMenuParams.height / 2f) : iconMenuParams.width / 2f);
-        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setSplitTranslationY(0);
         iconAppChipView.setRotation(getDegreesRotated());
     }
 
@@ -641,12 +641,14 @@
         primaryIconView.setTranslationX(0);
         secondaryIconView.setTranslationX(0);
         if (enableOverviewIconMenu()) {
+            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
             if (primaryIconView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
-                secondaryIconView.setTranslationY(-primarySnapshotHeight);
-                primaryIconView.setTranslationY(0);
+                secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
+                primaryAppChipView.setSplitTranslationY(0);
             } else {
                 int secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight;
-                primaryIconView.setTranslationY(secondarySnapshotHeight);
+                primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight);
             }
         } else if (splitConfig.initiatedFromSeascape) {
             // if the split was initiated from seascape,
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 2f996e1..60e6a25 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -639,7 +639,7 @@
 
         iconAppChipView.setPivotX(0);
         iconAppChipView.setPivotY(0);
-        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setSplitTranslationY(0);
         iconAppChipView.setRotation(getDegreesRotated());
     }
 
@@ -655,6 +655,8 @@
                 : new FrameLayout.LayoutParams(primaryIconParams);
 
         if (enableOverviewIconMenu()) {
+            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
             primaryIconParams.gravity = TOP | START;
             secondaryIconParams.gravity = TOP | START;
             secondaryIconParams.topMargin = primaryIconParams.topMargin;
@@ -662,16 +664,16 @@
             if (deviceProfile.isLeftRightSplit) {
                 if (isRtl) {
                     int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth;
-                    primaryIconView.setTranslationX(-secondarySnapshotWidth);
+                    primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth);
                 } else {
-                    secondaryIconView.setTranslationX(primarySnapshotWidth);
+                    secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth);
                 }
             } else {
-                primaryIconView.setTranslationX(0);
-                secondaryIconView.setTranslationX(0);
+                primaryAppChipView.setSplitTranslationX(0);
+                secondaryAppChipView.setSplitTranslationX(0);
                 int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
                         splitConfig.visualDividerBounds.height());
-                secondaryIconView.setTranslationY(
+                secondaryAppChipView.setSplitTranslationY(
                         primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness));
             }
         } else if (deviceProfile.isLeftRightSplit) {
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
index 9090d14..a964639 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
@@ -244,7 +244,7 @@
         iconAppChipView.setPivotX(isRtl ? iconMenuParams.width / 2f : iconCenter);
         iconAppChipView.setPivotY(
                 isRtl ? iconMenuParams.width / 2f : iconCenter - iconMenuMargin);
-        iconAppChipView.setTranslationY(0);
+        iconAppChipView.setSplitTranslationY(0);
         iconAppChipView.setRotation(getDegreesRotated());
     }
 
@@ -281,12 +281,15 @@
         primaryIconView.setTranslationX(0);
         secondaryIconView.setTranslationX(0);
         if (enableOverviewIconMenu()) {
+            IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+            IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
             if (isRtl) {
-                primaryIconView.setTranslationY(groupedTaskViewHeight - primarySnapshotHeight);
-                secondaryIconView.setTranslationY(0);
+                primaryAppChipView.setSplitTranslationY(
+                        groupedTaskViewHeight - primarySnapshotHeight);
+                secondaryAppChipView.setSplitTranslationY(0);
             } else {
-                secondaryIconView.setTranslationY(-primarySnapshotHeight);
-                primaryIconView.setTranslationY(0);
+                secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
+                primaryAppChipView.setSplitTranslationY(0);
             }
         } else if (splitConfig.initiatedFromSeascape) {
             // if the split was initiated from seascape,
diff --git a/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt
new file mode 100644
index 0000000..24b0e3a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.graphics.Region
+import android.os.RemoteException
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.ISystemGestureExclusionListener
+import android.view.IWindowManager
+import android.view.WindowManagerGlobal
+import androidx.annotation.BinderThread
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.util.Executors
+
+/** Wrapper over system gesture exclusion listener to optimize for multiple RPCs */
+class GestureExclusionManager(private val windowManager: IWindowManager) {
+
+    private val listeners = mutableListOf<ExclusionListener>()
+
+    private var lastExclusionRegion: Region? = null
+    private var lastUnrestrictedOrNull: Region? = null
+
+    @VisibleForTesting
+    val exclusionListener =
+        object : ISystemGestureExclusionListener.Stub() {
+            @BinderThread
+            override fun onSystemGestureExclusionChanged(
+                displayId: Int,
+                exclusionRegion: Region?,
+                unrestrictedOrNull: Region?
+            ) {
+                if (displayId != DEFAULT_DISPLAY) {
+                    return
+                }
+                Executors.MAIN_EXECUTOR.execute {
+                    lastExclusionRegion = exclusionRegion
+                    lastUnrestrictedOrNull = unrestrictedOrNull
+                    listeners.forEach {
+                        it.onGestureExclusionChanged(exclusionRegion, unrestrictedOrNull)
+                    }
+                }
+            }
+        }
+
+    /** Adds a listener for receiving gesture exclusion regions */
+    fun addListener(listener: ExclusionListener) {
+        val wasEmpty = listeners.isEmpty()
+        listeners.add(listener)
+        if (wasEmpty) {
+            Executors.UI_HELPER_EXECUTOR.execute {
+                try {
+                    windowManager.registerSystemGestureExclusionListener(
+                        exclusionListener,
+                        DEFAULT_DISPLAY
+                    )
+                } catch (e: RemoteException) {
+                    Log.e(TAG, "Failed to register gesture exclusion listener", e)
+                }
+            }
+        } else {
+            // If we had already registered before, dispatch the last known value,
+            // otherwise registering the listener will initiate a dispatch
+            listener.onGestureExclusionChanged(lastExclusionRegion, lastUnrestrictedOrNull)
+        }
+    }
+
+    /** Removes a previously added exclusion listener */
+    fun removeListener(listener: ExclusionListener) {
+        if (listeners.remove(listener) && listeners.isEmpty()) {
+            Executors.UI_HELPER_EXECUTOR.execute {
+                try {
+                    windowManager.unregisterSystemGestureExclusionListener(
+                        exclusionListener,
+                        DEFAULT_DISPLAY
+                    )
+                } catch (e: RemoteException) {
+                    Log.e(TAG, "Failed to unregister gesture exclusion listener", e)
+                }
+            }
+        }
+    }
+
+    interface ExclusionListener {
+        fun onGestureExclusionChanged(exclusionRegion: Region?, unrestrictedOrNull: Region?)
+    }
+
+    companion object {
+
+        private const val TAG = "GestureExclusionManager"
+
+        @JvmField
+        val INSTANCE = GestureExclusionManager(WindowManagerGlobal.getWindowManagerService()!!)
+
+        @JvmField val EMPTY_REGION = Region()
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
index 958c28a..cb3566e 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -18,6 +18,8 @@
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -42,6 +44,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.RecentsOrientedState;
@@ -87,6 +90,67 @@
 
     private int mMaxWidth = Integer.MAX_VALUE;
 
+    private static final int INDEX_SPLIT_TRANSLATION = 0;
+    private static final int INDEX_MENU_TRANSLATION = 1;
+    private static final int INDEX_COUNT_TRANSLATION = 2;
+
+    private final MultiPropertyFactory<View> mViewTranslationX;
+    private final MultiPropertyFactory<View> mViewTranslationY;
+
+    /**
+     * Sets the view split x-axis translation
+     * @param translationX x-axis translation
+     */
+    public void setSplitTranslationX(float translationX) {
+        mViewTranslationX.get(INDEX_SPLIT_TRANSLATION).setValue(translationX);
+    }
+
+    /**
+     * Sets the view split y-axis translation
+     * @param translationY y-axis translation
+     */
+    public void setSplitTranslationY(float translationY) {
+        mViewTranslationY.get(INDEX_SPLIT_TRANSLATION).setValue(translationY);
+    }
+
+    /**
+     * Gets the menu x-axis translation for split task
+     */
+    public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() {
+        return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
+    }
+
+    /**
+     * Translate the View on the X-axis without overriding the raw translation.
+     * This function is used for the menu split animation. It allows external animations to
+     * translate this view without affecting the value of the original translation. Thus,
+     * it is possible to restore the initial translation value.
+     *
+     * @param translationX Animated translation to be aggregated to the raw translation.
+     */
+    public void setMenuTranslationX(float translationX) {
+        mViewTranslationX.get(INDEX_MENU_TRANSLATION).setValue(translationX);
+    }
+
+    /**
+     * Gets the menu y-axis translation for split task
+     */
+    public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
+        return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
+    }
+
+    /**
+     * Translate the View on the Y-axis without overriding the raw translation.
+     * This function is used for the menu split animation. It allows external animations to
+     * translate this view without affecting the value of the original translation. Thus,
+     * it is possible to restore the initial translation value.
+     *
+     * @param translationY Animated translation to be aggregated to the raw translation.
+     */
+    public void setMenuTranslationY(float translationY) {
+        mViewTranslationY.get(INDEX_MENU_TRANSLATION).setValue(translationY);
+    }
+
     public IconAppChipView(Context context) {
         this(context, null);
     }
@@ -136,6 +200,13 @@
                 R.dimen.task_thumbnail_icon_menu_arrow_size);
         mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
                 R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
+
+        mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
+                INDEX_COUNT_TRANSLATION,
+                Float::sum);
+        mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
+                INDEX_COUNT_TRANSLATION,
+                Float::sum);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 6404383..39f1e46 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -18,6 +18,7 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.Flags.enableOverviewIconMenu;
+import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
@@ -78,8 +79,6 @@
     private LinearLayout mOptionLayout;
     private float mMenuTranslationYBeforeOpen;
     private float mMenuTranslationXBeforeOpen;
-    private float mIconViewTranslationYBeforeOpen;
-    private float mIconViewTranslationXBeforeOpen;
 
     public TaskMenuView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -277,8 +276,6 @@
     private void animateOpen() {
         mMenuTranslationYBeforeOpen = getTranslationY();
         mMenuTranslationXBeforeOpen = getTranslationX();
-        mIconViewTranslationYBeforeOpen = getIconView().getTranslationY();
-        mIconViewTranslationXBeforeOpen = getIconView().getTranslationX();
         animateOpenOrClosed(false);
         mIsOpen = true;
     }
@@ -323,10 +320,10 @@
                             : mMenuTranslationYBeforeOpen + additionalTranslationY);
             translationYAnim.setInterpolator(EMPHASIZED);
 
+            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
             ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
-                    getIconView(), TRANSLATION_Y,
-                    closing ? mIconViewTranslationYBeforeOpen
-                            : mIconViewTranslationYBeforeOpen + additionalTranslationY);
+                    iconAppChip.getMenuTranslationY(),
+                    MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY);
             menuTranslationYAnim.setInterpolator(EMPHASIZED);
 
             mOpenCloseAnimator.playTogether(translationYAnim, menuTranslationYAnim);
@@ -344,10 +341,10 @@
                             : mMenuTranslationXBeforeOpen - additionalTranslationX);
             translationXAnim.setInterpolator(EMPHASIZED);
 
+            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
             ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat(
-                    getIconView(), TRANSLATION_X,
-                    closing ? mIconViewTranslationXBeforeOpen
-                            : mIconViewTranslationXBeforeOpen - additionalTranslationX);
+                    iconAppChip.getMenuTranslationX(),
+                    MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX);
             menuTranslationXAnim.setInterpolator(EMPHASIZED);
 
             mOpenCloseAnimator.playTogether(translationXAnim, menuTranslationXAnim);
@@ -387,9 +384,11 @@
 
     private void resetOverviewIconMenu() {
         if (enableOverviewIconMenu()) {
-            ((IconAppChipView) mTaskContainer.getIconView()).reset();
+            IconAppChipView iconAppChipView = (IconAppChipView) mTaskContainer.getIconView();
+            iconAppChipView.reset();
+            iconAppChipView.setMenuTranslationX(0);
+            iconAppChipView.setMenuTranslationY(0);
             setTranslationY(mMenuTranslationYBeforeOpen);
-            getIconView().setTranslationY(mIconViewTranslationYBeforeOpen);
         }
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index 53bc2a2..2916952 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -2,17 +2,15 @@
 
 import android.content.Context
 import android.testing.AndroidTestingRunner
-import android.view.Display
-import android.view.IWindowManager
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.SmallTest
 import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
 import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE
 import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
 import com.android.launcher3.util.DisplayController.Info
-import com.android.launcher3.util.Executors
 import com.android.launcher3.util.NavigationMode
 import com.android.launcher3.util.window.WindowManagerProxy
+import com.android.quickstep.util.GestureExclusionManager
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,7 +26,7 @@
 @RunWith(AndroidTestingRunner::class)
 class RecentsAnimationDeviceStateTest {
 
-    @Mock private lateinit var windowManager: IWindowManager
+    @Mock private lateinit var exclusionManager: GestureExclusionManager
     @Mock private lateinit var windowManagerProxy: WindowManagerProxy
     @Mock private lateinit var info: Info
 
@@ -38,60 +36,45 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest = RecentsAnimationDeviceState(context, windowManager)
+        underTest = RecentsAnimationDeviceState(context, exclusionManager)
     }
 
     @Test
     fun registerExclusionListener_success() {
         underTest.registerExclusionListener()
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .registerSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).addListener(underTest)
     }
 
     @Test
     fun registerExclusionListener_again_fail() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.registerExclusionListener()
 
-        awaitTasksCompleted()
-        verifyZeroInteractions(windowManager)
+        verifyZeroInteractions(exclusionManager)
     }
 
     @Test
     fun unregisterExclusionListener_success() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.unregisterExclusionListener()
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .unregisterSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).removeListener(underTest)
     }
 
     @Test
     fun unregisterExclusionListener_again_fail() {
         underTest.registerExclusionListener()
         underTest.unregisterExclusionListener()
-        awaitTasksCompleted()
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.unregisterExclusionListener()
 
-        awaitTasksCompleted()
-        verifyZeroInteractions(windowManager)
+        verifyZeroInteractions(exclusionManager)
     }
 
     @Test
@@ -100,45 +83,28 @@
 
         underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .registerSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).addListener(underTest)
     }
 
     @Test
     fun onDisplayInfoChanged_twoButton_unregisterExclusionListener() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
         whenever(info.getNavigationMode()).thenReturn(NavigationMode.TWO_BUTTONS)
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
 
-        awaitTasksCompleted()
-        verify(windowManager)
-            .unregisterSystemGestureExclusionListener(
-                underTest.mGestureExclusionListener,
-                Display.DEFAULT_DISPLAY
-            )
+        verify(exclusionManager).removeListener(underTest)
     }
 
     @Test
     fun onDisplayInfoChanged_changeDensity_noOp() {
         underTest.registerExclusionListener()
-        awaitTasksCompleted()
         whenever(info.getNavigationMode()).thenReturn(NavigationMode.NO_BUTTON)
-        reset(windowManager)
+        reset(exclusionManager)
 
         underTest.onDisplayInfoChanged(context, info, CHANGE_DENSITY)
 
-        awaitTasksCompleted()
-        verifyZeroInteractions(windowManager)
-    }
-
-    private fun awaitTasksCompleted() {
-        Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+        verifyZeroInteractions(exclusionManager)
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt
new file mode 100644
index 0000000..c190cfe
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.graphics.Rect
+import android.graphics.Region
+import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.IWindowManager
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.quickstep.util.GestureExclusionManager.ExclusionListener
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+
+/** Unit test for [GestureExclusionManager]. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class GestureExclusionManagerTest {
+
+    @Mock private lateinit var windowManager: IWindowManager
+
+    @Mock private lateinit var listener1: ExclusionListener
+    @Mock private lateinit var listener2: ExclusionListener
+
+    private val r1 = Region().apply { union(Rect(0, 0, 100, 200)) }
+    private val r2 = Region().apply { union(Rect(200, 200, 500, 800)) }
+
+    private lateinit var underTest: GestureExclusionManager
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = GestureExclusionManager(windowManager)
+    }
+
+    @Test
+    fun addListener_registers() {
+        underTest.addListener(listener1)
+
+        awaitTasksCompleted()
+        verify(windowManager)
+            .registerSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
+    }
+
+    @Test
+    fun addListener_again_skips_register() {
+        underTest.addListener(listener1)
+        awaitTasksCompleted()
+        reset(windowManager)
+
+        underTest.addListener(listener2)
+
+        awaitTasksCompleted()
+        verifyZeroInteractions(windowManager)
+    }
+
+    @Test
+    fun removeListener_unregisters() {
+        underTest.addListener(listener1)
+        awaitTasksCompleted()
+        reset(windowManager)
+
+        underTest.removeListener(listener1)
+
+        awaitTasksCompleted()
+        verify(windowManager)
+            .unregisterSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
+    }
+
+    @Test
+    fun removeListener_again_skips_unregister() {
+        underTest.addListener(listener1)
+        underTest.addListener(listener2)
+        awaitTasksCompleted()
+        reset(windowManager)
+
+        underTest.removeListener(listener1)
+
+        awaitTasksCompleted()
+        verifyZeroInteractions(windowManager)
+    }
+
+    @Test
+    fun onSystemGestureExclusionChanged_dispatches_to_listeners() {
+        underTest.addListener(listener1)
+        underTest.addListener(listener2)
+        awaitTasksCompleted()
+
+        underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
+        awaitTasksCompleted()
+        verify(listener1).onGestureExclusionChanged(r1, r2)
+        verify(listener2).onGestureExclusionChanged(r1, r2)
+    }
+
+    @Test
+    fun addLister_dispatches_second_time() {
+        underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
+        awaitTasksCompleted()
+        underTest.addListener(listener1)
+        awaitTasksCompleted()
+        verifyZeroInteractions(listener1)
+
+        underTest.addListener(listener2)
+        awaitTasksCompleted()
+
+        verifyZeroInteractions(listener1)
+        verify(listener2).onGestureExclusionChanged(r1, r2)
+    }
+
+    private fun awaitTasksCompleted() {
+        Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+        Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+    }
+}
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 00082ae..c187000 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -485,4 +485,7 @@
     <dimen name="ps_button_width">36dp</dimen>
     <dimen name="ps_lock_button_width">89dp</dimen>
     <dimen name="ps_app_divider_padding">16dp</dimen>
+
+    <!-- WindowManagerProxy -->
+    <dimen name="max_width_and_height_of_small_display_cutout">136px</dimen>
 </resources>
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 984a9ae..ac0d7ce 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1636,9 +1636,15 @@
             mDragController.addDragListener(
                     new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
                         @Override
-                        protected void enableAccessibleDrag(boolean enable) {
-                            super.enableAccessibleDrag(enable);
+                        protected void enableAccessibleDrag(boolean enable,
+                                @Nullable DragObject dragObject) {
+                            super.enableAccessibleDrag(enable, dragObject);
                             setEnableForLayout(mLauncher.getHotseat(), enable);
+                            if (enable && dragObject != null
+                                    && dragObject.dragInfo instanceof LauncherAppWidgetInfo) {
+                                mLauncher.getHotseat().setImportantForAccessibility(
+                                        IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+                            }
                         }
                     });
         }
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
index 0d7df2b..79b8187 100644
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
@@ -20,6 +20,8 @@
 import android.view.ViewGroup;
 import android.view.ViewGroup.OnHierarchyChangeListener;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
@@ -50,13 +52,13 @@
     @Override
     public void onDragStart(DragObject dragObject, DragOptions options) {
         mViewGroup.setOnHierarchyChangeListener(this);
-        enableAccessibleDrag(true);
+        enableAccessibleDrag(true, dragObject);
     }
 
     @Override
     public void onDragEnd() {
         mViewGroup.setOnHierarchyChangeListener(null);
-        enableAccessibleDrag(false);
+        enableAccessibleDrag(false, null);
         Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
     }
 
@@ -75,7 +77,7 @@
         }
     }
 
-    protected void enableAccessibleDrag(boolean enable) {
+    protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) {
         for (int i = 0; i < mViewGroup.getChildCount(); i++) {
             setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
         }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 2f3f029..f013126 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -320,8 +320,9 @@
                 mDragController.addDragListener(new AccessibleDragListenerAdapter(
                         mContent, FolderAccessibilityHelper::new) {
                     @Override
-                    protected void enableAccessibleDrag(boolean enable) {
-                        super.enableAccessibleDrag(enable);
+                    protected void enableAccessibleDrag(boolean enable,
+                            @Nullable DragObject dragObject) {
+                        super.enableAccessibleDrag(enable, dragObject);
                         mFooter.setImportantForAccessibility(enable
                                 ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
                                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 31ae7c2..88b98aa 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -433,7 +433,9 @@
                     !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) &&
                         !isSafeMode &&
                         (si == null) &&
-                        (lapi == null)
+                        (lapi == null) &&
+                        !(Utilities.enableSupportForArchiving() &&
+                            pmHelper.isAppArchived(component.packageName))
                 ) {
                     // Restore never started
                     c.markDeleted(
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 50d8886..92288e1 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -107,6 +107,23 @@
     }
 
     /**
+     * Returns whether the target app is in archived state
+     */
+    @SuppressWarnings("NewApi")
+    public boolean isAppArchived(@NonNull final String packageName) {
+        final ApplicationInfo info;
+        try {
+            info = mPm.getPackageInfo(packageName,
+                    PackageManager.PackageInfoFlags.of(
+                            PackageManager.MATCH_ARCHIVED_PACKAGES)).applicationInfo;
+            return info.isArchived;
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Failed to get applicationInfo for package: " + packageName, e);
+            return false;
+        }
+    }
+
+    /**
      * Returns the application info for the provided package or null
      */
     @Nullable
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 32f1736..6a0090c 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -17,6 +17,7 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.launcher3.Utilities.dpToPx;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT;
@@ -49,6 +50,9 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.shared.ResourceUtils;
@@ -130,11 +134,11 @@
         Resources systemRes = context.getResources();
         Configuration config = systemRes.getConfiguration();
 
-        boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
+        boolean isLargeScreen = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
         boolean isGesture = isGestureNav(context);
         boolean isPortrait = config.screenHeightDp > config.screenWidthDp;
 
-        int bottomNav = isTablet
+        int bottomNav = isLargeScreen
                 ? 0
                 : (isPortrait
                         ? getDimenByName(systemRes, NAVBAR_HEIGHT)
@@ -165,6 +169,9 @@
             insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets);
         }
 
+        applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+                context, isLargeScreen, dpToPx(config.screenWidthDp), oldInsets, insetsBuilder);
+
         WindowInsets result = insetsBuilder.build();
         Insets systemWindowInsets = result.getInsetsIgnoringVisibility(
                 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
@@ -173,6 +180,71 @@
         return result;
     }
 
+    /**
+     * For large screen, when display cutout is at bottom left/right corner of screen, override
+     * display cutout's bottom inset to 0, because launcher allows drawing content over that area.
+     */
+    private static void applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+            @NonNull Context context,
+            boolean isLargeScreen,
+            int screenWidthPx,
+            @NonNull WindowInsets windowInsets,
+            @NonNull WindowInsets.Builder insetsBuilder) {
+        if (!isLargeScreen || !Utilities.ATLEAST_S) {
+            return;
+        }
+
+        final DisplayCutout displayCutout = windowInsets.getDisplayCutout();
+        if (displayCutout == null) {
+            return;
+        }
+
+        if (!areBottomDisplayCutoutsSmallAndAtCorners(
+                displayCutout.getBoundingRectBottom(), screenWidthPx, context.getResources())) {
+            return;
+        }
+
+        Insets oldDisplayCutoutInset = windowInsets.getInsets(WindowInsets.Type.displayCutout());
+        Insets newDisplayCutoutInset = Insets.of(
+                oldDisplayCutoutInset.left,
+                oldDisplayCutoutInset.top,
+                oldDisplayCutoutInset.right,
+                0);
+        insetsBuilder.setInsetsIgnoringVisibility(
+                WindowInsets.Type.displayCutout(), newDisplayCutoutInset);
+    }
+
+    /**
+     * @see doc at {@link #areBottomDisplayCutoutsSmallAndAtCorners(Rect, int, int)}
+     */
+    private static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+            @NonNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res) {
+        return areBottomDisplayCutoutsSmallAndAtCorners(cutoutRectBottom, screenWidthPx,
+                res.getDimensionPixelSize(R.dimen.max_width_and_height_of_small_display_cutout));
+    }
+
+    /**
+     * Return true if bottom display cutouts are at bottom left/right corners, AND has width or
+     * height <= maxWidthAndHeightOfSmallCutoutPx. Note that display cutout rect and screenWidthPx
+     * passed to this method should be in the SAME screen rotation.
+     *
+     * @param cutoutRectBottom bottom display cutout rect, this is based on current screen rotation
+     * @param screenWidthPx screen width in px based on current screen rotation
+     * @param maxWidthAndHeightOfSmallCutoutPx maximum width and height pixels of cutout.
+     */
+    @VisibleForTesting
+    static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+            @NonNull Rect cutoutRectBottom, int screenWidthPx,
+            int maxWidthAndHeightOfSmallCutoutPx) {
+        // Empty cutoutRectBottom means there is no display cutout at the bottom. We should ignore
+        // it by returning false.
+        if (cutoutRectBottom.isEmpty()) {
+            return false;
+        }
+        return (cutoutRectBottom.right <= maxWidthAndHeightOfSmallCutoutPx)
+                || cutoutRectBottom.left >= (screenWidthPx - maxWidthAndHeightOfSmallCutoutPx);
+    }
+
     protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) {
         Resources systemRes = context.getResources();
         int statusBarHeight = getDimenByName(systemRes,
@@ -249,6 +321,12 @@
             DisplayCutout rotatedCutout = rotateCutout(
                     displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i);
             Rect insets = getSafeInsets(rotatedCutout);
+            if (areBottomDisplayCutoutsSmallAndAtCorners(
+                    rotatedCutout.getBoundingRectBottom(),
+                    bounds.width(),
+                    context.getResources())) {
+                insets.bottom = 0;
+            }
             insets.top = Math.max(insets.top, statusBarHeight);
             insets.bottom = Math.max(insets.bottom, navBarHeight);
 
diff --git a/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
new file mode 100644
index 0000000..4819388
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util.window
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.window.WindowManagerProxy.areBottomDisplayCutoutsSmallAndAtCorners
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit test for [WindowManagerProxy] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WindowManagerProxyTest {
+
+    private val windowWidthPx = 2000
+
+    private val bottomLeftCutout = Rect(0, 2364, 136, 2500)
+    private val bottomRightCutout = Rect(1864, 2364, 2000, 2500)
+
+    private val bottomLeftCutoutWithOffset = Rect(10, 2364, 136, 2500)
+    private val bottomRightCutoutWithOffset = Rect(1864, 2364, 1990, 2500)
+
+    private val maxWidthAndHeightOfSmallCutoutPx = 136
+
+    @Test
+    fun cutout_at_bottom_right_corner() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomRightCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_left_corner_with_offset() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomLeftCutoutWithOffset,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_right_corner_with_offset() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomRightCutoutWithOffset,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_left_corner() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomLeftCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_at_bottom_edge_at_bottom_corners() {
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bottomLeftCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_too_big_not_at_bottom_corners() {
+        // Rect in size of 200px
+        val bigBottomLeftCutout = Rect(0, 2300, 200, 2500)
+
+        assertFalse(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                bigBottomLeftCutout,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_too_small_at_bottom_corners() {
+        // Rect in size of 100px
+        val smallBottomLeft = Rect(0, 2400, 100, 2500)
+
+        assertTrue(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                smallBottomLeft,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+
+    @Test
+    fun cutout_empty_not_at_bottom_corners() {
+        val emptyRect = Rect(0, 0, 0, 0)
+
+        assertFalse(
+            areBottomDisplayCutoutsSmallAndAtCorners(
+                emptyRect,
+                windowWidthPx,
+                maxWidthAndHeightOfSmallCutoutPx
+            )
+        )
+    }
+}