Merge "Add TAPL APIs for search web suggestions." into udc-qpr-dev
diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml
index d9e9691..a678b3f 100644
--- a/quickstep/res/values-am/strings.xml
+++ b/quickstep/res/values-am/strings.xml
@@ -92,7 +92,7 @@
     <string name="default_device_name" msgid="6660656727127422487">"መሣሪያ"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"የስርዓት አሰሳ ቅንብሮች"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"አጋራ"</string>
-    <string name="action_screenshot" msgid="8171125848358142917">"ቅጽበታዊ ገፅ እይታ"</string>
+    <string name="action_screenshot" msgid="8171125848358142917">"ቅጽበታዊ ገፅ ዕይታ"</string>
     <string name="action_split" msgid="2098009717623550676">"ክፈል"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"የተከፈለ ማያ ገጽን ለመጠቀም ሌላ መተግበሪያ መታ ያድርጉ"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ከተከፈለ ማያ ገፅ ምርጫ ይውጡ"</string>
diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml
index 2ec3cbf..e07d338 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -83,7 +83,7 @@
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"סיום"</string>
     <string name="gesture_tutorial_action_button_label_settings" msgid="2923621047916486604">"הגדרות"</string>
     <string name="gesture_tutorial_try_again" msgid="65962545858556697">"ניסיון חוזר"</string>
-    <string name="gesture_tutorial_nice" msgid="2936275692616928280">"איזה יופי!"</string>
+    <string name="gesture_tutorial_nice" msgid="2936275692616928280">"יפה!"</string>
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"מדריך <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"הכול מוכן!"</string>
     <string name="allset_hint" msgid="459504134589971527">"כדי לחזור לדף הבית, מחליקים כלפי מעלה"</string>
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index 619bef2..87a9ecb 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -27,6 +27,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.ComponentName;
+import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
 import android.view.ViewGroup;
@@ -74,6 +75,7 @@
         SystemShortcut.Factory<QuickstepLauncher>, DeviceProfile.OnDeviceProfileChangeListener,
         DragSource, ViewGroup.OnHierarchyChangeListener {
 
+    private static final String TAG = "HotseatPredictionController";
     private static final int FLAG_UPDATE_PAUSED = 1 << 0;
     private static final int FLAG_DRAG_IN_PROGRESS = 1 << 1;
     private static final int FLAG_FILL_IN_PROGRESS = 1 << 2;
@@ -183,6 +185,7 @@
     }
 
     private void fillGapsWithPrediction(boolean animate) {
+        Log.d(TAG, "fillGapsWithPrediction");
         if (mPauseFlags != 0) {
             return;
         }
@@ -207,12 +210,16 @@
             View child = mHotseat.getChildAt(
                     mHotseat.getCellXFromOrder(rank),
                     mHotseat.getCellYFromOrder(rank));
+            Log.d(TAG, "Hotseat app child is: " + child + " and isPredictedIcon() evaluates to"
+                    + ": " + isPredictedIcon(child));
 
             if (child != null && !isPredictedIcon(child)) {
                 continue;
             }
             if (mPredictedItems.size() <= predictionIndex) {
                 // Remove predicted apps from the past
+                Log.d(TAG, "Remove predicted apps from the past\nPrediction Index: "
+                        + predictionIndex);
                 if (isPredictedIcon(child)) {
                     mHotseat.removeView(child);
                 }
@@ -220,6 +227,11 @@
             }
             WorkspaceItemInfo predictedItem =
                     (WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
+            Log.d(TAG, "Predicted item is: " + predictedItem);
+            if (child != null) {
+                Log.d(TAG, "Predicted item is enabled: " + child.isEnabled());
+            }
+
             if (isPredictedIcon(child) && child.isEnabled()) {
                 PredictedAppIcon icon = (PredictedAppIcon) child;
                 boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem);
@@ -239,6 +251,7 @@
     }
 
     private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate) {
+        Log.d(TAG, "bindItems to hotseat: " + itemsToAdd);
         AnimatorSet animationSet = new AnimatorSet();
         for (WorkspaceItemInfo item : itemsToAdd) {
             PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
@@ -292,8 +305,10 @@
     public void setPredictedItems(FixedContainerItems items) {
         mPredictedItems = new ArrayList(items.items);
         if (mPredictedItems.isEmpty()) {
+            Log.d(TAG, "Predicted items is initially empty");
             HotseatRestoreHelper.restoreBackup(mLauncher);
         }
+        Log.d(TAG, "Predicted items: " + mPredictedItems);
         fillGapsWithPrediction();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 993f13e..fe8400f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -51,7 +51,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TouchInteractionService;
-import com.android.quickstep.util.AssistUtilsBase;
+import com.android.quickstep.util.AssistUtils;
 import com.android.quickstep.views.DesktopTaskView;
 
 import java.io.PrintWriter;
@@ -313,7 +313,7 @@
             return;
         }
         // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation.
-        if (!AssistUtilsBase.newInstance(mService.getApplicationContext()).tryStartAssistOverride(
+        if (!AssistUtils.newInstance(mService.getApplicationContext()).tryStartAssistOverride(
                 INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
             Bundle args = new Bundle();
             args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index ffe077b..c482911 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -280,7 +280,7 @@
             // the position of the bubble when the bar is fully expanded
             final float expandedX = i * (mIconSize + mIconSpacing);
             // the position of the bubble when the bar is fully collapsed
-            final float collapsedX = i * mIconOverlapAmount;
+            final float collapsedX = i == 0 ? 0 : mIconOverlapAmount;
 
             if (mIsBarExpanded) {
                 // where the bubble will end up when the animation ends
@@ -292,12 +292,22 @@
                 }
                 // When we're expanded, we're not stacked so we're not behind the stack
                 bv.setBehindStack(false, animate);
+                bv.setAlpha(1);
             } else {
                 final float targetX = currentWidth - collapsedWidth + collapsedX;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                 bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
                 // If we're not the first bubble we're behind the stack
                 bv.setBehindStack(i > 0, animate);
+                // If we're fully collapsed, hide all bubbles except for the first 2. If there are
+                // only 2 bubbles, hide the second bubble as well because it's the overflow.
+                if (widthState == 0) {
+                    if (i > 1) {
+                        bv.setAlpha(0);
+                    } else if (i == 1 && bubbleCount == 2) {
+                        bv.setAlpha(0);
+                    }
+                }
             }
         }
 
@@ -458,7 +468,11 @@
     private float collapsedWidth() {
         final int childCount = getChildCount();
         final int horizontalPadding = getPaddingStart() + getPaddingEnd();
-        return mIconSize + ((childCount - 1) * mIconOverlapAmount) + horizontalPadding;
+        // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
+        // Otherwise just the first bubble should be visible because we don't show the overflow.
+        return childCount > 2
+                ? mIconSize + mIconOverlapAmount + horizontalPadding
+                : mIconSize + horizontalPadding;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b444b49..24bc58f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -73,6 +73,7 @@
 import android.os.IBinder;
 import android.os.SystemProperties;
 import android.os.Trace;
+import android.util.Log;
 import android.view.Display;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
@@ -190,6 +191,8 @@
     private static final String TRACE_RELAYOUT_CLASS =
             SystemProperties.get("persist.debug.trace_request_layout_class", null);
 
+    private static final String TAG = "QuickstepLauncher";
+
     public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
 
     protected static final String RING_APPEAR_ANIMATION_PREFIX = "RingAppearAnimation\t";
@@ -434,6 +437,7 @@
 
     @Override
     public void bindExtraContainerItems(FixedContainerItems item) {
+        Log.d(TAG, "Bind extra container items");
         if (item.containerId == Favorites.CONTAINER_PREDICTION) {
             mAllAppsPredictions = item;
             PredictionRowView<?> predictionRowView =
@@ -441,6 +445,7 @@
                             PredictionRowView.class);
             predictionRowView.setPredictedApps(item.items);
         } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            Log.d(TAG, "Bind extra container item is hotseat prediction");
             mHotseatPredictionController.setPredictedItems(item);
         } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
             getPopupDataProvider().setRecommendedWidgets(item.items);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 823e137..fae929a 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -62,7 +62,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SplitConfigurationOptions;
-import com.android.quickstep.util.AssistUtilsBase;
+import com.android.quickstep.util.AssistUtils;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -252,7 +252,7 @@
         setUnfoldAnimationListener(mUnfoldAnimationListener);
         setDesktopTaskListener(mDesktopTaskListener);
         setAssistantOverridesRequested(
-                AssistUtilsBase.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
+                AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 06f1f9a..076f4b1 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -122,6 +122,12 @@
     public void removeListeners() {
     }
 
+    /**
+     * Clears any active state outside of the TaskOverlay lifecycle which might have built
+     * up over time
+     */
+    public void clearAllActiveState() { }
+
     /** Note that these will be shown in order from top to bottom, if available for the task. */
     private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
             TaskShortcutFactory.APP_INFO,
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index cd88894..c1680de 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -117,7 +117,7 @@
 import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
-import com.android.quickstep.util.AssistUtilsBase;
+import com.android.quickstep.util.AssistUtils;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -289,7 +289,7 @@
         @Override
         public void onAssistantOverrideInvoked(int invocationType) {
             executeForTouchInteractionService(tis -> {
-                if (!AssistUtilsBase.newInstance(tis).tryStartAssistOverride(invocationType)) {
+                if (!AssistUtils.newInstance(tis).tryStartAssistOverride(invocationType)) {
                     Log.w(TAG, "Failed to invoke Assist override");
                 }
             });
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 2816228..4c66504 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -278,7 +278,8 @@
                     if (!mIsDeferredDownTarget) {
                         // Normal gesture, ensure we pass the drag slop before we start tracking
                         // the gesture
-                        if (Math.abs(displacement) > mTouchSlop) {
+                        if (mGestureState.isTrackpadGesture() || Math.abs(displacement)
+                                > mTouchSlop) {
                             mPassedWindowMoveSlop = true;
                             mStartDisplacement = Math.min(displacement, -mTouchSlop);
                         }
@@ -287,8 +288,8 @@
 
                 float horizontalDist = Math.abs(displacementX);
                 float upDist = -displacement;
-                boolean passedSlop = squaredHypot(displacementX, displacementY)
-                        >= mSquaredTouchSlop;
+                boolean passedSlop = mGestureState.isTrackpadGesture() || squaredHypot(
+                        displacementX, displacementY) >= mSquaredTouchSlop;
 
                 if (!mPassedSlopOnThisGesture && passedSlop) {
                     mPassedSlopOnThisGesture = true;
diff --git a/quickstep/src/com/android/quickstep/util/AssistUtilsBase.java b/quickstep/src/com/android/quickstep/util/AssistUtils.java
similarity index 84%
rename from quickstep/src/com/android/quickstep/util/AssistUtilsBase.java
rename to quickstep/src/com/android/quickstep/util/AssistUtils.java
index 7b27020..11b6ea7 100644
--- a/quickstep/src/com/android/quickstep/util/AssistUtilsBase.java
+++ b/quickstep/src/com/android/quickstep/util/AssistUtils.java
@@ -21,13 +21,13 @@
 import com.android.launcher3.util.ResourceBasedOverride;
 
 /** Utilities to work with Assistant functionality. */
-public class AssistUtilsBase implements ResourceBasedOverride {
+public class AssistUtils implements ResourceBasedOverride {
 
-    public AssistUtilsBase() {}
+    public AssistUtils() {}
 
     /** Creates AssistUtils as specified by overrides */
-    public static AssistUtilsBase newInstance(Context context) {
-        return Overrides.getObject(AssistUtilsBase.class, context, R.string.assist_utils_class);
+    public static AssistUtils newInstance(Context context) {
+        return Overrides.getObject(AssistUtils.class, context, R.string.assist_utils_class);
     }
 
     /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 6d5aa16..d4ddf76 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
 import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
 import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
@@ -508,10 +509,6 @@
         mSplitFromDesktopController = new SplitFromDesktopController(launcher);
     }
 
-    public void enterSplitFromDesktop(ActivityManager.RunningTaskInfo taskInfo) {
-        mSplitFromDesktopController.enterSplitSelect(taskInfo);
-    }
-
     private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
             @Nullable Consumer<Boolean> callback, String transitionName) {
         final RemoteSplitLaunchTransitionRunner animationRunner =
@@ -745,9 +742,11 @@
                     R.dimen.split_placeholder_inset);
             mSplitSelectListener = new ISplitSelectListener.Stub() {
                 @Override
-                public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo) {
+                public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+                        int splitPosition, Rect taskBounds) {
                     if (!ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE.get()) return false;
-                    MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo));
+                    MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition,
+                            taskBounds));
                     return true;
                 }
             };
@@ -757,8 +756,11 @@
         /**
          * Enter split select from desktop mode.
          * @param taskInfo the desktop task to move to split stage
+         * @param splitPosition the stage position used for this transition
+         * @param taskBounds the bounds of the task, used for {@link FloatingTaskView} animation
          */
-        public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo) {
+        public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+                int splitPosition, Rect taskBounds) {
             mTaskInfo = taskInfo;
             String packageName = mTaskInfo.realActivity.getPackageName();
             PackageManager pm = mLauncher.getApplicationContext().getPackageManager();
@@ -774,7 +776,7 @@
                     false /* allowMinimizeSplitScreen */);
 
             DesktopSplitRecentsAnimationListener listener =
-                    new DesktopSplitRecentsAnimationListener();
+                    new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds);
 
             MAIN_EXECUTOR.execute(() -> {
                 callbacks.addListener(listener);
@@ -790,12 +792,23 @@
         private class DesktopSplitRecentsAnimationListener implements
                 RecentsAnimationCallbacks.RecentsAnimationListener {
             private final Rect mTempRect = new Rect();
+            private final RectF mTaskBounds = new RectF();
+            private final int mSplitPosition;
+
+            DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds) {
+                mSplitPosition = splitPosition;
+                mTaskBounds.set(taskBounds);
+            }
 
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
-                setInitialTaskSelect(mTaskInfo, STAGE_POSITION_BOTTOM_OR_RIGHT,
-                        null, LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM);
+                StatsLogManager.LauncherEvent launcherDesktopSplitEvent =
+                        mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ?
+                        LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM :
+                        LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
+                setInitialTaskSelect(mTaskInfo, mSplitPosition,
+                        null, launcherDesktopSplitEvent);
 
                 RecentsView recentsView = mLauncher.getOverviewPanel();
                 recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
@@ -804,14 +817,12 @@
 
                 PendingAnimation anim = new PendingAnimation(
                         SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration());
-                RectF startingTaskRect = new RectF(mTaskInfo.configuration.windowConfiguration
-                        .getBounds());
                 final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
                         mLauncher, mLauncher.getDragLayer(),
                         null /* thumbnail */,
                         mAppIcon, new RectF());
                 floatingTaskView.setAlpha(1);
-                floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
+                floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect,
                         false /* fadeWithThumbnail */, true /* isStagedTask */);
                 setFirstFloatingTaskView(floatingTaskView);
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index cb5b457..76f9c2c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1396,6 +1396,7 @@
             // its thumbnail
             mTmpRunningTasks = null;
             mSplitBoundsConfig = null;
+            mTaskOverlayFactory.clearAllActiveState();
         }
         updateLocusId();
     }
@@ -4793,8 +4794,9 @@
                         } else {
                             resetFromSplitSelectionState();
                         }
+                        InteractionJankMonitorWrapper.end(
+                                InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
                     });
-            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
         });
 
         mSecondSplitHiddenView = containerTaskView;
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index 4768773..c6c38fc 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -55,6 +55,7 @@
         int y = presenterPos.cellY;
         if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
                 || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            Log.d(TAG, "add predicted icon " + child.getTag().toString() + " to home screen");
             int screenId = presenterPos.screenId;
             x = getHotseat().getCellXFromOrder(screenId);
             y = getHotseat().getCellYFromOrder(screenId);
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index d78bfba..53d0efb 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.folder;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
@@ -627,7 +628,7 @@
             Utilities.scaleRectAboutCenter(iconBounds, iconScale);
 
             // If we are animating to the accepting state, animate the dot out.
-            mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress());
+            mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress());
             mDotParams.dotColor = mBackground.getDotColor();
             mDotRenderer.draw(canvas, mDotParams);
         }
@@ -801,6 +802,14 @@
         }
     }
 
+    @Override
+    public void onHoverChanged(boolean hovered) {
+        super.onHoverChanged(hovered);
+        if (ENABLE_CURSOR_HOVER_STATES.get()) {
+            mBackground.setHovered(hovered);
+        }
+    }
+
     /**
      * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
      */
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 406955c..b320ceb 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.folder;
 
+import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.graphics.IconShape.getShape;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -39,6 +41,9 @@
 import android.graphics.Shader;
 import android.util.Property;
 import android.view.View;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
@@ -55,7 +60,10 @@
     private static final boolean DRAW_SHADOW = false;
     private static final boolean DRAW_STROKE = false;
 
-    private static final int CONSUMPTION_ANIMATION_DURATION = 100;
+    @VisibleForTesting protected static final int CONSUMPTION_ANIMATION_DURATION = 100;
+
+    @VisibleForTesting protected static final float HOVER_SCALE = 1.1f;
+    @VisibleForTesting protected static final int HOVER_ANIMATION_DURATION = 300;
 
     private final PorterDuffXfermode mShadowPorterDuffXfermode
             = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
@@ -86,17 +94,21 @@
     public boolean isClipping = true;
 
     // Drawing / animation configurations
-    private static final float ACCEPT_SCALE_FACTOR = 1.20f;
+    @VisibleForTesting protected static final float ACCEPT_SCALE_FACTOR = 1.20f;
 
     // Expressed on a scale from 0 to 255.
     private static final int BG_OPACITY = 255;
     private static final int MAX_BG_OPACITY = 255;
     private static final int SHADOW_OPACITY = 40;
 
-    private ValueAnimator mScaleAnimator;
+    @VisibleForTesting protected ValueAnimator mScaleAnimator;
     private ObjectAnimator mStrokeAlphaAnimator;
     private ObjectAnimator mShadowAnimator;
 
+    @VisibleForTesting protected boolean mIsAccepting;
+    @VisibleForTesting protected boolean mIsHovered;
+    @VisibleForTesting protected boolean mIsHoveredOrAnimating;
+
     private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
             new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
                 @Override
@@ -203,11 +215,11 @@
     }
 
     /**
-     * Returns the progress of the scale animation, where 0 means the scale is at 1f
-     * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
+     * Returns the progress of the scale animation to accept state, where 0 means the scale is at
+     * 1f and 1 means the scale is at ACCEPT_SCALE_FACTOR. Returns 0 when scaled due to hover.
      */
-    float getScaleProgress() {
-        return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
+    float getAcceptScaleProgress() {
+        return mIsHoveredOrAnimating ? 0 : (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
     }
 
     void invalidate() {
@@ -385,60 +397,70 @@
         return mDrawingDelegate != null;
     }
 
-    private void animateScale(float finalScale, final Runnable onStart, final Runnable onEnd) {
-        final float scale0 = mScale;
-        final float scale1 = finalScale;
-
+    protected void animateScale(boolean isAccepting, boolean isHovered) {
         if (mScaleAnimator != null) {
             mScaleAnimator.cancel();
         }
 
-        mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
-
-        mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
-                float prog = animation.getAnimatedFraction();
-                mScale = prog * scale1 + (1 - prog) * scale0;
-                invalidate();
+        final float startScale = mScale;
+        final float endScale = isAccepting ? ACCEPT_SCALE_FACTOR : (isHovered ? HOVER_SCALE : 1f);
+        Interpolator interpolator =
+                isAccepting != mIsAccepting ? ACCELERATE_DECELERATE : EMPHASIZED_DECELERATE;
+        int duration = isAccepting != mIsAccepting ? CONSUMPTION_ANIMATION_DURATION
+                : HOVER_ANIMATION_DURATION;
+        mIsAccepting = isAccepting;
+        mIsHovered = isHovered;
+        if (startScale == endScale) {
+            if (!mIsAccepting) {
+                clearDrawingDelegate();
             }
+            mIsHoveredOrAnimating = mIsHovered;
+            return;
+        }
+
+
+        mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
+        mScaleAnimator.addUpdateListener(animation -> {
+            float prog = animation.getAnimatedFraction();
+            mScale = prog * endScale + (1 - prog) * startScale;
+            invalidate();
         });
         mScaleAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
-                if (onStart != null) {
-                    onStart.run();
+                if (mIsHovered) {
+                    mIsHoveredOrAnimating = true;
                 }
             }
 
             @Override
             public void onAnimationEnd(Animator animation) {
-                if (onEnd != null) {
-                    onEnd.run();
+                if (!mIsAccepting) {
+                    clearDrawingDelegate();
                 }
+                mIsHoveredOrAnimating = mIsHovered;
                 mScaleAnimator = null;
             }
         });
-
-        mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
+        mScaleAnimator.setInterpolator(interpolator);
+        mScaleAnimator.setDuration(duration);
         mScaleAnimator.start();
     }
 
     public void animateToAccept(CellLayout cl, int cellX, int cellY) {
-        animateScale(ACCEPT_SCALE_FACTOR, () -> delegateDrawing(cl, cellX, cellY), null);
+        delegateDrawing(cl, cellX, cellY);
+        animateScale(/* isAccepting= */ true, mIsHovered);
     }
 
     public void animateToRest() {
-        // This can be called multiple times -- we need to make sure the drawing delegate
-        // is saved and restored at the beginning of the animation, since cancelling the
-        // existing animation can clear the delgate.
-        CellLayout cl = mDrawingDelegate;
-        int cellX = mDelegateCellX;
-        int cellY = mDelegateCellY;
-        animateScale(1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
+        animateScale(/* isAccepting= */ false, mIsHovered);
     }
 
     public float getStrokeWidth() {
         return mStrokeWidth;
     }
+
+    protected void setHovered(boolean hovered) {
+        animateScale(mIsAccepting, /* isHovered= */ hovered);
+    }
 }
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 780cb5e..265378c 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -621,9 +621,12 @@
         @UiEvent(doc = "User has invoked split to left half with a keyboard shortcut.")
         LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP(1233),
 
-        @UiEvent(doc = "User has invoked split to right half with desktop mode app icon")
+        @UiEvent(doc = "User has invoked split to right half from desktop mode.")
         LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM(1412),
 
+        @UiEvent(doc = "User has invoked split to left half from desktop mode.")
+        LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP(1464),
+
         @UiEvent(doc = "User has collapsed the work FAB button by scrolling down in the all apps"
                 + " work A-Z list.")
         LAUNCHER_WORK_FAB_BUTTON_COLLAPSE(1276),
diff --git a/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java b/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
new file mode 100644
index 0000000..715a1f8
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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.folder;
+
+import static com.android.launcher3.folder.PreviewBackground.ACCEPT_SCALE_FACTOR;
+import static com.android.launcher3.folder.PreviewBackground.CONSUMPTION_ANIMATION_DURATION;
+import static com.android.launcher3.folder.PreviewBackground.HOVER_ANIMATION_DURATION;
+import static com.android.launcher3.folder.PreviewBackground.HOVER_SCALE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.CellLayout;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class PreviewBackgroundTest {
+
+    private static final float REST_SCALE = 1f;
+    private static final float EPSILON = 0.00001f;
+
+    @Mock
+    CellLayout mCellLayout;
+
+    private final PreviewBackground mPreviewBackground = new PreviewBackground();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mPreviewBackground.mScale = REST_SCALE;
+        mPreviewBackground.mIsAccepting = false;
+        mPreviewBackground.mIsHovered = false;
+        mPreviewBackground.mIsHoveredOrAnimating = false;
+        mPreviewBackground.invalidate();
+    }
+
+    @Test
+    public void testAnimateScale_restToHovered() {
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_restToNotHovered() {
+        mPreviewBackground.setHovered(false);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_hoveredToHovered() {
+        mPreviewBackground.mScale = HOVER_SCALE;
+        mPreviewBackground.mIsHovered = true;
+        mPreviewBackground.mIsHoveredOrAnimating = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.setHovered(true);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_hoveredToRest() {
+        mPreviewBackground.mScale = HOVER_SCALE;
+        mPreviewBackground.mIsHovered = true;
+        mPreviewBackground.mIsHoveredOrAnimating = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.setHovered(false);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_restToAccept() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_restToRest() {
+        mPreviewBackground.animateToRest();
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_acceptToRest() {
+        mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR;
+        mPreviewBackground.mIsAccepting = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.animateToRest();
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_acceptToHover() {
+        mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR;
+        mPreviewBackground.mIsAccepting = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.mIsAccepting = false;
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_hoverToAccept() {
+        mPreviewBackground.mScale = HOVER_SCALE;
+        mPreviewBackground.mIsHovered = true;
+        mPreviewBackground.mIsHoveredOrAnimating = true;
+        mPreviewBackground.invalidate();
+
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        mPreviewBackground.mIsHovered = false;
+        endAnimation();
+        assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToHoverToAccept() {
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(0.5f);
+        assertTrue("Scale not changed.",
+                mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        mPreviewBackground.mIsHovered = false;
+        endAnimation();
+        assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1,
+                EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+    }
+
+    @Test
+    public void testAnimateScale_partWayToAcceptToHover() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(0.25f);
+        assertTrue("Scale not changed part way.", mPreviewBackground.mScale > REST_SCALE
+                && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR);
+
+        mPreviewBackground.mIsAccepting = false;
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToAcceptEqualsHover() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(0.5f);
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        mPreviewBackground.mIsAccepting = false;
+
+        mPreviewBackground.setHovered(true);
+
+        assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON);
+        assertNull("Animator not null.", mPreviewBackground.mScaleAnimator);
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToHoverToRest() {
+        mPreviewBackground.setHovered(true);
+        runAnimationToFraction(0.5f);
+        assertTrue("Scale not changed midway.",
+                mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE);
+
+        mPreviewBackground.mIsHovered = false;
+        mPreviewBackground.animateToRest();
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                HOVER_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    @Test
+    public void testAnimateScale_midwayToAcceptToRest() {
+        mPreviewBackground.animateToAccept(mCellLayout, 0, 0);
+        runAnimationToFraction(0.5f);
+        assertTrue("Scale not changed.", mPreviewBackground.mScale > REST_SCALE
+                && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR);
+
+        mPreviewBackground.animateToRest();
+        runAnimationToFraction(1f);
+
+        assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON);
+        assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(),
+                CONSUMPTION_ANIMATION_DURATION);
+        assertTrue("Wrong interpolator used.",
+                mPreviewBackground.mScaleAnimator.getInterpolator()
+                        instanceof AccelerateDecelerateInterpolator);
+        endAnimation();
+        assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0,
+                EPSILON);
+    }
+
+    private void runAnimationToFraction(float animationFraction) {
+        mPreviewBackground.mScaleAnimator.setCurrentFraction(animationFraction);
+    }
+
+    private void endAnimation() {
+        mPreviewBackground.mScaleAnimator.end();
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index e12cf2d..f395e80 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -643,6 +643,10 @@
             mLauncher.getWorkspace().verifyWorkspaceAppIconIsGone(
                     DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
 
+            // Debug for b/288944469 I want to test if we are not waiting enough after removing
+            // the icon to request the list of icons again, since the items are not removed
+            // immediately. This should reduce the flake rate
+            SystemClock.sleep(500);
             Map<String, Point> finalPositions =
                     mLauncher.getWorkspace().getWorkspaceIconsPositions();
             assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
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 49abad4..4b65439 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
@@ -180,7 +180,8 @@
     }
 
     @Override
-    String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long timestamp) {
+    String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN, long timestamp,
+            int windowSizePx) {
         // 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 null;
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
index 09e2f65..786791c 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AnomalyDetector.java
@@ -68,17 +68,18 @@
      * null value means that the view. 'oldInfo' and 'newInfo' cannot be both null.
      * If an anomaly is detected, an exception will be thrown.
      *
-     * @param oldInfo the view, as seen in the last frame that contained it in the view
-     *                hierarchy before 'currentFrame'. 'null' means that the view is first seen
-     *                in the 'currentFrame'.
-     * @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that
-     *                the view is not present in the 'currentFrame', but was present in the previous
-     *                frame.
-     * @param frameN  number of the current frame.
+     * @param oldInfo      the view, as seen in the last frame that contained it in the view
+     *                     hierarchy before 'currentFrame'. 'null' means that the view is first seen
+     *                     in the 'currentFrame'.
+     * @param newInfo      the view in the view hierarchy of the 'currentFrame'. 'null' means that
+     *                     the view is not present in the 'currentFrame', but was present in the
+     *                     previous frame.
+     * @param frameN       number of the current frame.
+     * @param windowSizePx maximum of the window width and height, in pixels.
      * @return Anomaly diagnostic message if an anomaly has been detected; null otherwise.
      */
     abstract String detectAnomalies(
             @Nullable ViewCaptureAnalyzer.AnalysisNode oldInfo,
             @Nullable ViewCaptureAnalyzer.AnalysisNode newInfo, int frameN,
-            long frameTimeNs);
+            long frameTimeNs, int windowSizePx);
 }
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
index 388a59a..8b88ace 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
@@ -59,6 +59,11 @@
             DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail",
             DRAG_LAYER
                     + "WidgetsFullSheet|SpringRelativeLayout:id/container|WidgetsRecyclerView:id"
+                    + "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header",
+            DRAG_LAYER
+                    + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container|LinearLayout:id"
+                    + "/linear_layout_container|FrameLayout:id/recycler_view_container"
+                    + "|FrameLayout:id/widgets_two_pane_sheet_recyclerview|WidgetsRecyclerView:id"
                     + "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header"
     ));
 
@@ -105,7 +110,7 @@
 
     @Override
     String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
-            long frameTimeNs) {
+            long frameTimeNs, int windowSizePx) {
         // Should we check when a view was visible for a short period, then its alpha became 0?
         // Then 'lastVisible' time should be the last one still visible?
         // Check only transitions of alpha between 0 and 1?
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
new file mode 100644
index 0000000..a1ddcb0
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
@@ -0,0 +1,126 @@
+/*
+ * 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.util.viewcapture_analysis;
+
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
+
+import java.util.List;
+
+/**
+ * Anomaly detector that triggers an error when a view position jumps.
+ */
+final class PositionJumpDetector extends AnomalyDetector {
+    // Maximum allowed jump in "milliwindows", i.e. a 1/1000's of the maximum of the window
+    // dimensions.
+    private static final float JUMP_MIW = 250;
+
+    private static final String[] BORDER_NAMES = {"left", "top", "right", "bottom"};
+
+    // Commonly used parts of the paths to ignore.
+    private static final String CONTENT = "DecorView|LinearLayout|FrameLayout:id/content|";
+    private static final String DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|DragLayer:id/drag_layer|";
+    private static final String RECENTS_DRAG_LAYER =
+            CONTENT + "LauncherRootView:id/launcher|RecentsDragLayer:id/drag_layer|";
+
+    private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
+            DRAG_LAYER + "SearchContainerView:id/apps_view",
+            DRAG_LAYER + "AppWidgetResizeFrame",
+            DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
+            CONTENT
+                    + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+                    + "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content",
+            DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
+            DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
+            DRAG_LAYER + "LauncherDragView",
+            RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView",
+            CONTENT + "LauncherRootView:id/launcher|FloatingIconView",
+            DRAG_LAYER + "FloatingTaskView",
+            DRAG_LAYER + "LauncherRecentsView:id/overview_panel"
+    ));
+
+    // Per-AnalysisNode data that's specific to this detector.
+    private static class NodeData {
+        public boolean ignoreJumps;
+
+        // If ignoreNode is null, then this AnalysisNode node will be ignored if its parent is
+        // ignored.
+        // Otherwise, this AnalysisNode will be ignored if ignoreNode is a leaf i.e. has no
+        // children.
+        public IgnoreNode ignoreNode;
+    }
+
+    private NodeData getNodeData(AnalysisNode info) {
+        return (NodeData) info.detectorsData[detectorOrdinal];
+    }
+
+    @Override
+    void initializeNode(AnalysisNode info) {
+        final NodeData nodeData = new NodeData();
+        info.detectorsData[detectorOrdinal] = nodeData;
+
+        // If the parent view ignores jumps, its descendants will too.
+        final boolean parentIgnoresJumps = info.parent != null && getNodeData(
+                info.parent).ignoreJumps;
+        if (parentIgnoresJumps) {
+            nodeData.ignoreJumps = true;
+            return;
+        }
+
+        // Parent view doesn't ignore jumps.
+        // Initialize this AnalysisNode's ignore-node with the corresponding child of the
+        // ignore-node of the parent, if present.
+        final IgnoreNode parentIgnoreNode = info.parent != null
+                ? getNodeData(info.parent).ignoreNode
+                : IGNORED_NODES_ROOT;
+        nodeData.ignoreNode = parentIgnoreNode != null
+                ? parentIgnoreNode.children.get(info.nodeIdentity) : null;
+        // AnalysisNode will be ignored if the corresponding ignore-node is a leaf.
+        nodeData.ignoreJumps =
+                nodeData.ignoreNode != null && nodeData.ignoreNode.children.isEmpty();
+    }
+
+    @Override
+    String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN,
+            long frameTimeNs, int windowSizePx) {
+        // If the view is not present in the current frame, there can't be a jump detected in the
+        // current frame.
+        if (newInfo == null) return null;
+
+        // We only detect position jumps if the view was visible in the previous frame.
+        if (oldInfo == null || frameN != oldInfo.frameN + 1) return null;
+
+        final NodeData newNodeData = getNodeData(newInfo);
+        if (newNodeData.ignoreJumps) return null;
+
+        final float[] positionDiffs = {
+                newInfo.left - oldInfo.left,
+                newInfo.top - oldInfo.top,
+                newInfo.right - oldInfo.right,
+                newInfo.bottom - oldInfo.bottom
+        };
+
+        for (int i = 0; i < 4; ++i) {
+            final float positionDiffAbs = Math.abs(positionDiffs[i]);
+            if (positionDiffAbs * 1000 > JUMP_MIW * windowSizePx) {
+                newNodeData.ignoreJumps = true;
+                return String.format("Position jump: %s jumped by %s",
+                        BORDER_NAMES[i], positionDiffAbs);
+            }
+        }
+        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 ccb4a1e..9459cc2 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
@@ -36,7 +36,8 @@
     // All detectors. They will be invoked in the order listed here.
     private static final AnomalyDetector[] ANOMALY_DETECTORS = {
             new AlphaJumpDetector(),
-            new FlashDetector()
+            new FlashDetector(),
+            new PositionJumpDetector()
     };
 
     static {
@@ -52,6 +53,8 @@
         // Window coordinates of the view.
         public float left;
         public float top;
+        public float right;
+        public float bottom;
 
         // Visible scale and alpha, build recursively from the ancestor list.
         public float scaleX;
@@ -81,7 +84,8 @@
 
         @Override
         public String toString() {
-            return String.format("view window coordinates: (%s, %s)", left, top);
+            return String.format("view window coordinates: (%s, %s, %s, %s)",
+                    left, top, right, bottom);
         }
     }
 
@@ -112,15 +116,33 @@
         // As we go though frames, if a view becomes invisible, it stays in the map.
         final Map<Integer, AnalysisNode> lastSeenNodes = new HashMap<>();
 
+        int windowWidthPx = -1;
+        int windowHeightPx = -1;
+
         for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
-            analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes,
-                    scrimClassIndex, anomalies);
+            final FrameData frame = windowData.getFrameData(frameN);
+            final ViewNode rootNode = frame.getNode();
+
+            // If the rotation or window size has changed, reset the analyzer state.
+            final boolean isFirstFrame = windowWidthPx != rootNode.getWidth()
+                    || windowHeightPx != rootNode.getHeight();
+            if (isFirstFrame) {
+                windowWidthPx = rootNode.getWidth();
+                windowHeightPx = rootNode.getHeight();
+                lastSeenNodes.clear();
+            }
+
+            final int windowSizePx = Math.max(rootNode.getWidth(), rootNode.getHeight());
+
+            analyzeFrame(frameN, isFirstFrame, frame, viewCaptureData, lastSeenNodes,
+                    scrimClassIndex, anomalies, windowSizePx);
         }
     }
 
-    private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData,
+    private static void analyzeFrame(int frameN, boolean isFirstFrame, FrameData frame,
+            ExportedData viewCaptureData,
             Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
-            Map<String, String> anomalies) {
+            Map<String, String> anomalies, int windowSizePx) {
         // Analyze the node tree starting from the root.
         long frameTimeNs = frame.getTimestamp();
         analyzeView(
@@ -128,12 +150,14 @@
                 frame.getNode(),
                 /* parent = */ null,
                 frameN,
+                isFirstFrame,
                 /* leftShift = */ 0,
                 /* topShift = */ 0,
                 viewCaptureData,
                 lastSeenNodes,
                 scrimClassIndex,
-                anomalies);
+                anomalies,
+                windowSizePx);
 
         // Analyze transitions when a view visible in the previous frame became invisible in the
         // current one.
@@ -148,7 +172,8 @@
                                             /* oldInfo = */ info,
                                             /* newInfo = */ null,
                                             anomalies,
-                                            frameTimeNs)
+                                            frameTimeNs,
+                                            windowSizePx)
                     );
                 }
                 info.timeBecameInvisibleNs = info.alpha == 1 ? frameTimeNs : -1;
@@ -159,9 +184,9 @@
 
     private static void analyzeView(long frameTimeNs, ViewNode viewCaptureNode, AnalysisNode parent,
             int frameN,
-            float leftShift, float topShift, ExportedData viewCaptureData,
+            boolean isFirstFrame, float leftShift, float topShift, ExportedData viewCaptureData,
             Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex,
-            Map<String, String> anomalies) {
+            Map<String, String> anomalies, int windowSizePx) {
         // Skip analysis of invisible views
         final float parentAlpha = parent != null ? parent.alpha : 1;
         final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
@@ -182,6 +207,8 @@
         final float top = topShift
                 + (viewCaptureNode.getTop() + viewCaptureNode.getTranslationY()) * parentScaleY
                 + viewCaptureNode.getHeight() * (parentScaleY - scaleY) / 2;
+        final float width = viewCaptureNode.getWidth() * scaleX;
+        final float height = viewCaptureNode.getHeight() * scaleY;
 
         // Initialize new analysis node
         final AnalysisNode newAnalysisNode = new AnalysisNode();
@@ -192,6 +219,8 @@
         newAnalysisNode.parent = parent;
         newAnalysisNode.left = left;
         newAnalysisNode.top = top;
+        newAnalysisNode.right = left + width;
+        newAnalysisNode.bottom = top + height;
         newAnalysisNode.scaleX = scaleX;
         newAnalysisNode.scaleY = scaleY;
         newAnalysisNode.alpha = alpha;
@@ -216,11 +245,11 @@
         }
 
         // Detect anomalies for the view.
-        if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) {
+        if (!isFirstFrame && !viewCaptureNode.getWillNotDraw()) {
             Arrays.stream(ANOMALY_DETECTORS).forEach(
                     detector ->
                             detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode,
-                                    anomalies, frameTimeNs)
+                                    anomalies, frameTimeNs, windowSizePx)
             );
         }
         lastSeenNodes.put(hashcode, newAnalysisNode);
@@ -235,17 +264,19 @@
             // transparent.
             if (child.getClassnameIndex() == scrimClassIndex) break;
 
-            analyzeView(frameTimeNs, child, newAnalysisNode, frameN, leftShiftForChildren,
+            analyzeView(frameTimeNs, child, newAnalysisNode, frameN, isFirstFrame,
+                    leftShiftForChildren,
                     topShiftForChildren,
-                    viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies);
+                    viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies, windowSizePx);
         }
     }
 
     private static void detectAnomaly(AnomalyDetector detector, int frameN,
             AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode,
-            Map<String, String> anomalies, long frameTimeNs) {
+            Map<String, String> anomalies, long frameTimeNs, int windowSizePx) {
         final String maybeAnomaly =
-                detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN, frameTimeNs);
+                detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN, frameTimeNs,
+                        windowSizePx);
         if (maybeAnomaly != null) {
             AnalysisNode latestInfo = newAnalysisNode != null ? newAnalysisNode : oldAnalysisNode;
             final String viewDiagPath = diagPathFromRoot(latestInfo);