Merge "Move AppWidgetsRestoredReceiver methods to enable adding tests for restoring widget Ids" into main
diff --git a/Android.bp b/Android.bp
index b68e1e5..af0e3be 100644
--- a/Android.bp
+++ b/Android.bp
@@ -167,8 +167,8 @@
 //
 // Build rule for Launcher3 dependencies lib.
 //
-android_library {
-    name: "Launcher3CommonDepsLib",
+java_defaults {
+    name: "Launcher3CommonDepsDefault",
     srcs: ["src_build_config/**/*.java"],
     static_libs: [
         "Launcher3ResLib",
@@ -184,13 +184,35 @@
 }
 
 //
+// Build rule for Launcher3 dependencies lib.
+//
+android_library {
+    name: "Launcher3CommonDepsLib",
+    defaults: ["Launcher3CommonDepsDefault"],
+    static_libs: [
+        "com_android_launcher3_flags_lib",
+    ],
+}
+
+//
+// Build rule for Launcher3 dependencies lib for test and debug.
+//
+android_library {
+    name: "Launcher3CommonDepsLibDebug",
+    defaults: ["Launcher3CommonDepsDefault"],
+    static_libs: [
+        "com_android_launcher3_flags_lib_debug",
+    ],
+}
+
+//
 // Build rule for Launcher3 app.
 //
 android_app {
     name: "Launcher3",
 
     static_libs: [
-        "Launcher3CommonDepsLib",
+        "Launcher3CommonDepsLibDebug",
     ],
     srcs: [
         ":launcher-src",
diff --git a/aconfig/Android.bp b/aconfig/Android.bp
new file mode 100644
index 0000000..4ecc282
--- /dev/null
+++ b/aconfig/Android.bp
@@ -0,0 +1,35 @@
+// 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aconfig_declarations {
+    name: "com_android_launcher3_flags",
+    package: "com.android.launcher3",
+    srcs: ["launcher.aconfig"],
+}
+
+java_aconfig_library {
+    name: "com_android_launcher3_flags_lib",
+    aconfig_declarations: "com_android_launcher3_flags",
+}
+
+java_aconfig_library {
+    name: "com_android_launcher3_flags_lib_debug",
+    aconfig_declarations: "com_android_launcher3_flags",
+    test: true
+}
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
new file mode 100644
index 0000000..dd39249
--- /dev/null
+++ b/aconfig/launcher.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.launcher3"
+
+flag {
+    name: "enable_expanding_pause_work_button"
+    namespace: "launcher"
+    description: "Expand and collapse pause work button while scrolling."
+    bug: "270390779"
+}
diff --git a/build.gradle b/build.gradle
index 090bafe..f4d7261 100644
--- a/build.gradle
+++ b/build.gradle
@@ -157,6 +157,8 @@
     withoutQuickstepImplementation fileTree(dir: "${FRAMEWORK_PREBUILTS_DIR}/libs", include: 'plugin_core.jar')
 
     testImplementation 'junit:junit:4.12'
+    testImplementation libs.mockitoInlineExtended
+    androidTestImplementation libs.mockitoInlineExtended
     androidTestImplementation "org.mockito:mockito-core:1.9.5"
     androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
     androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
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/res/values/override.xml b/quickstep/res/values/override.xml
index 67be0dd..860abc1 100644
--- a/quickstep/res/values/override.xml
+++ b/quickstep/res/values/override.xml
@@ -27,6 +27,8 @@
 
   <string name="nav_handle_long_press_handler_class" translatable="false"></string>
 
+  <string name="assist_utils_class" translatable="false"></string>
+
   <string name="secondary_display_predictions_class" translatable="false">com.android.launcher3.secondarydisplay.SecondaryDisplayPredictionsImpl</string>
 
   <string name="taskbar_model_callbacks_factory_class" translatable="false">com.android.launcher3.taskbar.TaskbarModelCallbacksFactory</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/DesktopNavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
index 885afff..29c5204 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_NOTIFICATIONS;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_QUICK_SETTINGS;
 
+import android.content.pm.ActivityInfo.Config;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -56,6 +57,11 @@
     @Override
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
+        super.init(controllers);
+    }
+
+    @Override
+    protected void setupController() {
         mNavButtonsView.getLayoutParams().height = mContext.getDeviceProfile().taskbarHeight;
 
         // Quick settings and notifications buttons
@@ -72,4 +78,7 @@
     /** Cleans up on destroy */
     @Override
     public void onDestroy() { }
+
+    @Override
+    public void onConfigurationChanged(@Config int configChanges) { }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index fcd8c80..fa16b61 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -219,11 +219,15 @@
      */
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
+        setupController();
+    }
+
+    protected void setupController() {
         boolean isThreeButtonNav = mContext.isThreeButtonNav();
         DeviceProfile deviceProfile = mContext.getDeviceProfile();
         Resources resources = mContext.getResources();
         Point p = !mContext.isUserSetupComplete()
-                ? new Point(0, controllers.taskbarActivityContext.getSetupWindowHeight())
+                ? new Point(0, mControllers.taskbarActivityContext.getSetupWindowHeight())
                 : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
                         TaskbarManager.isPhoneMode(deviceProfile));
         mNavButtonsView.getLayoutParams().height = p.y;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 0f8de34..fe8400f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -51,6 +51,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.util.AssistUtils;
 import com.android.quickstep.views.DesktopTaskView;
 
 import java.io.PrintWriter;
@@ -158,7 +159,7 @@
         switch (buttonType) {
             case BUTTON_HOME:
                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS);
-                startAssistant();
+                onLongPressHome();
                 return true;
             case BUTTON_A11Y:
                 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS);
@@ -307,13 +308,17 @@
         }
     }
 
-    private void startAssistant() {
+    private void onLongPressHome() {
         if (mScreenPinned || !mAssistantLongPressEnabled) {
             return;
         }
-        Bundle args = new Bundle();
-        args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
-        mSystemUiProxy.startAssistant(args);
+        // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation.
+        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);
+            mSystemUiProxy.startAssistant(args);
+        }
     }
 
     private void showQuickSettings() {
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/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
index 4b235a9..6c3f0d8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt
@@ -30,6 +30,7 @@
 fun DismissView.setup() {
     setup(
         DismissView.Config(
+            dismissViewResId = R.id.dismiss_view,
             targetSizeResId = R.dimen.bubblebar_dismiss_target_size,
             iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size,
             bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b444b49..b50ab97 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;
@@ -132,6 +133,7 @@
 import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController;
 import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.BackPressHandler;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.IntSet;
@@ -190,6 +192,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";
@@ -229,7 +233,8 @@
         mSplitSelectStateController =
                 new SplitSelectStateController(this, mHandler, getStateManager(),
                         getDepthController(), getStatsLogManager(),
-                        SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this));
+                        SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this),
+                        () -> onStateBack());
         overviewPanel.init(mActionsView, mSplitSelectStateController);
         mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
                 mSplitSelectStateController);
@@ -254,6 +259,7 @@
         mEnableWidgetDepth = SystemProperties.getBoolean("ro.launcher.depth.widget", true);
         getWorkspace().addOverlayCallback(progress ->
                 onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX));
+        addBackAnimationCallback(mSplitSelectStateController.getSplitBackHandler());
     }
 
     @Override
@@ -434,6 +440,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 +448,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);
@@ -479,6 +487,7 @@
         mHotseatPredictionController.destroy();
         mSplitWithKeyboardShortcutController.onDestroy();
         if (mViewCapture != null) mViewCapture.close();
+        removeBackAnimationCallback(mSplitSelectStateController.getSplitBackHandler());
     }
 
     @Override
@@ -663,6 +672,10 @@
         anim.buildAnim().start();
     }
 
+    @Override
+    protected boolean isSplitSelectionEnabled() {
+        return mSplitSelectStateController.isSplitSelectActive();
+    }
 
     @Override
     protected void onResume() {
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 72439de..d66421f 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -136,7 +136,8 @@
         mSplitSelectStateController =
                 new SplitSelectStateController(this, mHandler, getStateManager(),
                         null /* depthController */, getStatsLogManager(),
-                        SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this));
+                        SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this),
+                        null /*activityBackCallback*/);
         mDragLayer.recreateControllers();
         mFallbackRecentsView.init(mActionsView, mSplitSelectStateController);
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index ef2d20a..99dd634 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -62,6 +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.AssistUtils;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -250,6 +251,8 @@
         setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner);
         setUnfoldAnimationListener(mUnfoldAnimationListener);
         setDesktopTaskListener(mDesktopTaskListener);
+        setAssistantOverridesRequested(
+                AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
     }
 
     /**
@@ -374,6 +377,17 @@
     }
 
     @Override
+    public void setAssistantOverridesRequested(int[] invocationTypes) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setAssistantOverridesRequested(invocationTypes);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setAssistantOverridesRequested", e);
+            }
+        }
+    }
+
+    @Override
     public void notifyAccessibilityButtonClicked(int displayId) {
         if (mSystemUiProxy != null) {
             try {
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 22aca25..c1680de 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -117,6 +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.AssistUtils;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -280,6 +281,20 @@
             }));
         }
 
+        /**
+         * Sent when the assistant has been invoked with the given type (defined in AssistManager)
+         * and should be shown. This method is used if SystemUiProxy#setAssistantOverridesRequested
+         * was previously called including this invocation type.
+         */
+        @Override
+        public void onAssistantOverrideInvoked(int invocationType) {
+            executeForTouchInteractionService(tis -> {
+                if (!AssistUtils.newInstance(tis).tryStartAssistOverride(invocationType)) {
+                    Log.w(TAG, "Failed to invoke Assist override");
+                }
+            });
+        }
+
         @Override
         public void onNavigationBarSurface(SurfaceControl surface) {
             // TODO: implement
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/AssistUtils.java b/quickstep/src/com/android/quickstep/util/AssistUtils.java
new file mode 100644
index 0000000..11b6ea7
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistUtils.java
@@ -0,0 +1,45 @@
+/*
+ * 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.quickstep.util;
+
+import android.content.Context;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+/** Utilities to work with Assistant functionality. */
+public class AssistUtils implements ResourceBasedOverride {
+
+    public AssistUtils() {}
+
+    /** Creates AssistUtils as specified by overrides */
+    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. */
+    public int[] getSysUiAssistOverrideInvocationTypes() {
+        return new int[0];
+    }
+
+    /**
+     * @return {@code true} if the override was handled, i.e. an assist surface was shown or the
+     * request should be ignored. {@code false} means the caller should start assist another way.
+     */
+    public boolean tryStartAssistOverride(int invocationType) {
+        return false;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 6d5aa16..8ff3537 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;
@@ -42,7 +43,6 @@
 import android.app.ActivityOptions;
 import android.app.ActivityThread;
 import android.app.PendingIntent;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
@@ -77,8 +77,10 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.BackPressHandler;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
 import com.android.quickstep.OverviewComponentObserver;
@@ -113,9 +115,11 @@
 public class SplitSelectStateController {
     private static final String TAG = "SplitSelectStateCtor";
 
-    private Context mContext;
+    private StatefulActivity mContext;
     private final Handler mHandler;
     private final RecentsModel mRecentTasksModel;
+    @Nullable
+    private final Runnable mActivityBackCallback;
     private final SplitAnimationController mSplitAnimationController;
     private final AppPairsController mAppPairsController;
     private final SplitSelectDataHolder mSplitSelectDataHolder;
@@ -142,9 +146,28 @@
 
     private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>();
 
-    public SplitSelectStateController(Context context, Handler handler, StateManager stateManager,
-            DepthController depthController, StatsLogManager statsLogManager,
-            SystemUiProxy systemUiProxy, RecentsModel recentsModel) {
+    private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
+        @Override
+        public boolean canHandleBack() {
+            return FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get() &&
+                    isSplitSelectActive();
+        }
+
+        @Override
+        public void onBackInvoked() {
+            // When exiting from split selection, leave current context to go to
+            // homescreen as well
+            getSplitAnimationController().playPlaceholderDismissAnim(mContext);
+            if (mActivityBackCallback != null) {
+                mActivityBackCallback.run();
+            }
+        }
+    };
+
+    public SplitSelectStateController(StatefulActivity context, Handler handler,
+            StateManager stateManager, DepthController depthController,
+            StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel,
+            Runnable activityBackCallback) {
         mContext = context;
         mHandler = handler;
         mStatsLogManager = statsLogManager;
@@ -152,6 +175,7 @@
         mStateManager = stateManager;
         mDepthController = depthController;
         mRecentTasksModel = recentsModel;
+        mActivityBackCallback = activityBackCallback;
         mSplitAnimationController = new SplitAnimationController(this);
         mAppPairsController = new AppPairsController(context, this, statsLogManager);
         mSplitSelectDataHolder = new SplitSelectDataHolder(mContext);
@@ -508,10 +532,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 =
@@ -716,6 +736,10 @@
         return mAppPairsController;
     }
 
+    public BackPressHandler getSplitBackHandler() {
+        return mSplitBackHandler;
+    }
+
     public void dump(String prefix, PrintWriter writer) {
         if (mSplitSelectDataHolder != null) {
             mSplitSelectDataHolder.dump(prefix, writer);
@@ -745,9 +769,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 +783,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 +803,7 @@
                     false /* allowMinimizeSplitScreen */);
 
             DesktopSplitRecentsAnimationListener listener =
-                    new DesktopSplitRecentsAnimationListener();
+                    new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds);
 
             MAIN_EXECUTOR.execute(() -> {
                 callbacks.addListener(listener);
@@ -790,12 +819,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 +844,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/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index f250b8c..92ffcd0 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -1,3 +1,18 @@
+/*
+ * 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.quickstep.views;
 
 import static com.android.app.animation.Interpolators.LINEAR;
@@ -47,8 +62,6 @@
  * {@link #addConfirmAnimation(PendingAnimation, RectF, Rect, boolean, boolean)}
  * giving a starting and ending bounds. Currently this is set to use the split placeholder view,
  * but it could be generified.
- *
- * TODO: Figure out how to copy thumbnail data from existing TaskView to this view.
  */
 public class FloatingTaskView extends FrameLayout {
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index cb5b457..be9da34 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();
     }
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 9622619..b3d04c6 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -26,6 +26,7 @@
 import android.os.Handler;
 import android.view.View;
 
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.logging.StatsLogManager;
@@ -70,6 +71,9 @@
         MockitoAnnotations.initMocks(this);
         when(mockService.getDisplayId()).thenReturn(DISPLAY_ID);
         when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper);
+        when(mockService.getApplicationContext())
+                .thenReturn(InstrumentationRegistry.getInstrumentation().getTargetContext()
+                        .getApplicationContext());
         when(mockStatsLogManager.logger()).thenReturn(mockStatsLogger);
         when(mockTaskbarControllers.getTaskbarActivityContext())
                 .thenReturn(mockTaskbarActivityContext);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index 1aa7ab6..92b598b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -25,6 +25,8 @@
 
 import android.content.Intent;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.launcher3.config.FeatureFlags;
@@ -36,7 +38,10 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
+@LargeTest
+@RunWith(AndroidJUnit4.class)
 public class TaplTestsSplitscreen extends AbstractQuickStepTest {
     private static final String CALCULATOR_APP_NAME = "Calculator";
     private static final String CALCULATOR_APP_PACKAGE =
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 69109c2..4e7fcf0 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -31,6 +31,7 @@
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
+import com.android.launcher3.statemanager.StatefulActivity
 import com.android.launcher3.util.ComponentKey
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.withArgCaptor
@@ -58,7 +59,7 @@
     @Mock lateinit var statsLogManager: StatsLogManager
     @Mock lateinit var stateManager: StateManager<LauncherState>
     @Mock lateinit var handler: Handler
-    @Mock lateinit var context: Context
+    @Mock lateinit var context: StatefulActivity<*>
     @Mock lateinit var recentsModel: RecentsModel
     @Mock lateinit var pendingIntent: PendingIntent
 
@@ -81,7 +82,8 @@
                 depthController,
                 statsLogManager,
                 systemUiProxy,
-                recentsModel
+                recentsModel,
+                null /*activityBackCallback*/
             )
     }
 
diff --git a/res/values/id.xml b/res/values/id.xml
index 7b812de..872ae2f 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -43,4 +43,6 @@
     <item type="id" name="saved_floating_widget_foreground" />
     <item type="id" name="saved_floating_widget_background" />
 
+    <item type="id" name="dismiss_view" />
+
 </resources>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index ffb8b82..439cc00 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -199,6 +199,7 @@
 import com.android.launcher3.util.ActivityResultInfo;
 import com.android.launcher3.util.ActivityTracker;
 import com.android.launcher3.util.CannedAnimationCoordinator;
+import com.android.launcher3.util.BackPressHandler;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -425,6 +426,8 @@
     private final CannedAnimationCoordinator mAnimationCoordinator =
             new CannedAnimationCoordinator(this);
 
+    private final List<BackPressHandler> mBackPressedHandlers = new ArrayList<>();
+
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
@@ -604,6 +607,7 @@
      *  <li> auto cancel action mode handler
      *  <li> drag handler
      *  <li> view handler
+     *  <li> registered {@link BackPressHandler}
      *  <li> state handler
      * </ol>
      *
@@ -633,7 +637,14 @@
             return topView;
         }
 
-        // #4 state handler
+        // #4 Custom back handlers
+        for (BackPressHandler handler : mBackPressedHandlers) {
+            if (handler.canHandleBack()) {
+                return handler;
+            }
+        }
+
+        // #5 state handler
         return new OnBackAnimationCallback() {
             @Override
             public void onBackInvoked() {
@@ -3281,6 +3292,14 @@
         updateDisallowBack();
     }
 
+    protected void addBackAnimationCallback(BackPressHandler callback) {
+        mBackPressedHandlers.add(callback);
+    }
+
+    protected void removeBackAnimationCallback(BackPressHandler callback) {
+        mBackPressedHandlers.remove(callback);
+    }
+
     private void updateDisallowBack() {
         if (DESKTOP_MODE_1_SUPPORTED || DESKTOP_MODE_2_SUPPORTED) {
             // Do not disable back in launcher when prototype behavior is enabled
@@ -3288,12 +3307,20 @@
         }
         LauncherRootView rv = getRootView();
         if (rv != null) {
+            boolean isSplitSelectionEnabled = isSplitSelectionEnabled();
             boolean disableBack = getStateManager().getState() == NORMAL
-                    && AbstractFloatingView.getTopOpenView(this) == null;
+                    && AbstractFloatingView.getTopOpenView(this) == null
+                    && !isSplitSelectionEnabled;
             rv.setDisallowBackGesture(disableBack);
         }
     }
 
+    /** To be overridden by subclasses */
+    protected boolean isSplitSelectionEnabled() {
+        // Overridden
+        return false;
+    }
+
     @Override
     public void returnToHomescreen() {
         super.returnToHomescreen();
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/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 40382b2..9c4ce46 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.Flags.enableExpandingPauseWorkButton;
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD;
@@ -561,7 +562,8 @@
             mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher);
             mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher());
             workRecyclerView.setId(R.id.apps_list_view_work);
-            if (FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
+            if (enableExpandingPauseWorkButton()
+                    || FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
                 mAH.get(AdapterHolder.WORK).mRecyclerView.addOnScrollListener(
                         mWorkManager.newScrollListener());
             }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 9bc2a0a..21520bf 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -230,6 +230,7 @@
     public static final BooleanFlag ENABLE_HIDE_HEADER = getReleaseFlag(270390930,
             "ENABLE_HIDE_HEADER", ENABLED, "Hide header on keyboard before typing in all apps");
 
+    // Aconfig migration complete for ENABLE_EXPANDING_PAUSE_WORK_BUTTON.
     public static final BooleanFlag ENABLE_EXPANDING_PAUSE_WORK_BUTTON = getDebugFlag(270390779,
             "ENABLE_EXPANDING_PAUSE_WORK_BUTTON", DISABLED,
             "Expand and collapse pause work button while scrolling");
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/src/com/android/launcher3/util/BackPressHandler.java b/src/com/android/launcher3/util/BackPressHandler.java
new file mode 100644
index 0000000..b63f648
--- /dev/null
+++ b/src/com/android/launcher3/util/BackPressHandler.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+import android.os.Build;
+import android.window.OnBackAnimationCallback;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Extension of {@link OnBackAnimationCallback} that allows a check to determine
+ * if this callback supports handling back or not
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public interface BackPressHandler extends OnBackAnimationCallback {
+    boolean canHandleBack();
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 5a52440..5effa5c 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -73,6 +73,7 @@
     asset_dirs: ["assets"],
     resource_dirs: ["res"],
     static_libs: [
+        "flag-junit-base",
         "launcher-aosp-tapl",
         "androidx.test.core",
         "androidx.test.runner",
@@ -82,11 +83,12 @@
         "androidx.test.espresso.contrib",
         "androidx.test.espresso.intents",
         "androidx.test.uiautomator_uiautomator",
-        "mockito-target-inline-minus-junit4",
+        "mockito-target-extended-minus-junit4",
         "launcher_log_protos_lite",
         "truth-prebuilt",
         "platform-test-rules",
         "testables",
+        "com_android_launcher3_flags_lib_debug",
     ],
     manifest: "AndroidManifest-common.xml",
     platform_apis: true,
@@ -103,12 +105,16 @@
         ":launcher-tests-src",
         ":launcher-non-quickstep-tests-src",
     ],
-    static_libs: ["Launcher3TestLib"],
+    static_libs: [
+        "Launcher3TestLib",
+        "com_android_launcher3_flags_lib_debug",
+    ],
     libs: [
         "android.test.base",
         "android.test.runner",
         "android.test.mock",
     ],
+    // Libraries used by mockito inline extended
     jni_libs: [
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
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/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
index 5b9adcd..ec62058 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.ui;
 
+import static com.android.launcher3.Flags.FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
@@ -26,6 +27,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.Log;
 import android.view.View;
 
@@ -44,6 +46,7 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.Objects;
@@ -52,6 +55,7 @@
 public class WorkProfileTest extends AbstractLauncherUiTest {
 
     private static final int WORK_PAGE = ActivityAllAppsContainerView.AdapterHolder.WORK;
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private int mProfileUserId;
     private boolean mWorkProfileSetupSuccessful;
@@ -60,6 +64,7 @@
     @Before
     @Override
     public void setUp() throws Exception {
+        mSetFlagsRule.disableFlags(FLAG_ENABLE_EXPANDING_PAUSE_WORK_BUTTON);
         super.setUp();
         String output =
                 mDevice.executeShellCommand(
diff --git a/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java b/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java
new file mode 100644
index 0000000..6b91474
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util.rule;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+
+import org.junit.rules.MethodRule;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+/**
+ * Similar to {@link MockitoRule}, but uses {@link StaticMockitoSession}, which allows mocking
+ * static methods.
+ */
+public class StaticMockitoRule implements MethodRule {
+    private Class<?>[] mClasses;
+
+    public StaticMockitoRule(Class<?>... classes) {
+        mClasses = classes;
+    }
+
+    @Override
+    public Statement apply(Statement base, FrameworkMethod method, Object target) {
+        return new Statement() {
+            public void evaluate() throws Throwable {
+                StaticMockitoSessionBuilder builder =
+                        mockitoSession()
+                                .name(target.getClass().getSimpleName() + "." + method.getName())
+                                .initMocks(target)
+                                .strictness(Strictness.STRICT_STUBS);
+
+                for (Class<?> clazz : mClasses) {
+                    builder.mockStatic(clazz);
+                }
+
+                StaticMockitoSession session = builder.startMocking();
+                Throwable testFailure = evaluateSafely(base);
+                session.finishMocking(testFailure);
+                if (testFailure != null) {
+                    throw testFailure;
+                }
+            }
+
+            private Throwable evaluateSafely(Statement base) {
+                try {
+                    base.evaluate();
+                    return null;
+                } catch (Throwable throwable) {
+                    return throwable;
+                }
+            }
+        };
+    }
+}
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);