Merge "Adding gesture error detection to RecentsAnimationCallback" into main
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 16abdee..5af8d51 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -17,6 +17,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/keyboard_quick_switch_view"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top"
diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml
index 860abc1..df32626 100644
--- a/quickstep/res/values/override.xml
+++ b/quickstep/res/values/override.xml
@@ -33,4 +33,6 @@
 
   <string name="taskbar_model_callbacks_factory_class" translatable="false">com.android.launcher3.taskbar.TaskbarModelCallbacksFactory</string>
 
+  <string name="assist_state_manager_class" translatable="false"></string>
+
 </resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 4e9e301..42c423c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -46,6 +46,8 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.util.GroupTask;
 
 import java.util.HashMap;
@@ -360,11 +362,8 @@
                                         OPEN_OUTLINE_INTERPOLATOR));
                     }
                 });
-                if (currentFocusIndexOverride == -1) {
-                    initializeScroll(/* index= */ 0, /* shouldTruncateTarget= */ false);
-                } else {
-                    animateFocusMove(-1, currentFocusIndexOverride);
-                }
+                animateFocusMove(-1, currentFocusIndexOverride == -1
+                        ? Math.min(mContent.getChildCount(), 1) : currentFocusIndexOverride);
                 displayedContent.setVisibility(VISIBLE);
                 setVisibility(VISIBLE);
                 requestFocus();
@@ -413,7 +412,8 @@
                     // there are more tasks
                     initializeScroll(
                             firstVisibleTaskIndex,
-                            /* shouldTruncateTarget= */ firstVisibleTaskIndex != toIndex);
+                            /* shouldTruncateTarget= */ firstVisibleTaskIndex != 0
+                                    && firstVisibleTaskIndex != toIndex);
                 } else if (toIndex > fromIndex || toIndex == 0) {
                     // Scrolling to next task view
                     if (mIsRtl) {
@@ -439,6 +439,13 @@
     }
 
     @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        TestLogging.recordKeyEvent(
+                TestProtocol.SEQUENCE_MAIN, "KeyboardQuickSwitchView key event", event);
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
     public boolean onKeyUp(int keyCode, KeyEvent event) {
         return (mViewCallbacks != null
                 && mViewCallbacks.onKeyUp(keyCode, event, mIsRtl, mDisplayingRecentTasks))
@@ -454,56 +461,80 @@
             return;
         }
         if (mIsRtl) {
-            scrollRightTo(
-                    task, shouldTruncateTarget, /* smoothScroll= */ false);
-        } else {
             scrollLeftTo(
-                    task, shouldTruncateTarget, /* smoothScroll= */ false);
+                    task,
+                    shouldTruncateTarget,
+                    /* smoothScroll= */ false,
+                    /* waitForLayout= */ true);
+        } else {
+            scrollRightTo(
+                    task,
+                    shouldTruncateTarget,
+                    /* smoothScroll= */ false,
+                    /* waitForLayout= */ true);
         }
     }
 
     private void scrollRightTo(@NonNull View targetTask) {
-        scrollRightTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true);
+        scrollRightTo(
+                targetTask,
+                /* shouldTruncateTarget= */ false,
+                /* smoothScroll= */ true,
+                /* waitForLayout= */ false);
     }
 
     private void scrollRightTo(
-            @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) {
+            @NonNull View targetTask,
+            boolean shouldTruncateTarget,
+            boolean smoothScroll,
+            boolean waitForLayout) {
         if (!mDisplayingRecentTasks) {
             return;
         }
         if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) {
             return;
         }
-        int scrollTo = targetTask.getLeft() - mSpacing
-                + (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
-        // Scroll so that the focused task is to the left of the list
-        if (smoothScroll) {
-            mScrollView.smoothScrollTo(scrollTo, 0);
-        } else {
-            mScrollView.scrollTo(scrollTo, 0);
-        }
+        runScrollCommand(waitForLayout, () -> {
+            int scrollTo = targetTask.getLeft() - mSpacing
+                    + (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
+            // Scroll so that the focused task is to the left of the list
+            if (smoothScroll) {
+                mScrollView.smoothScrollTo(scrollTo, 0);
+            } else {
+                mScrollView.scrollTo(scrollTo, 0);
+            }
+        });
     }
 
     private void scrollLeftTo(@NonNull View targetTask) {
-        scrollLeftTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true);
+        scrollLeftTo(
+                targetTask,
+                /* shouldTruncateTarget= */ false,
+                /* smoothScroll= */ true,
+                /* waitForLayout= */ false);
     }
 
     private void scrollLeftTo(
-            @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) {
+            @NonNull View targetTask,
+            boolean shouldTruncateTarget,
+            boolean smoothScroll,
+            boolean waitForLayout) {
         if (!mDisplayingRecentTasks) {
             return;
         }
         if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) {
             return;
         }
-        int scrollTo = targetTask.getRight() + mSpacing - mScrollView.getWidth()
-                - (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
-        // Scroll so that the focused task is to the right of the list
-        if (smoothScroll) {
-            mScrollView.smoothScrollTo(scrollTo, 0);
-        } else {
-            mScrollView.scrollTo(scrollTo, 0);
-        }
+        runScrollCommand(waitForLayout, () -> {
+            int scrollTo = targetTask.getRight() + mSpacing - mScrollView.getWidth()
+                    - (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
+            // Scroll so that the focused task is to the right of the list
+            if (smoothScroll) {
+                mScrollView.smoothScrollTo(scrollTo, 0);
+            } else {
+                mScrollView.scrollTo(scrollTo, 0);
+            }
+        });
     }
 
     private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) {
@@ -514,6 +545,21 @@
         return isTargetTruncated && !shouldTruncateTarget;
     }
 
+    private void runScrollCommand(boolean waitForLayout, @NonNull Runnable scrollCommand) {
+        if (!waitForLayout) {
+            scrollCommand.run();
+            return;
+        }
+        mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        scrollCommand.run();
+                        mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                    }
+                });
+    }
+
     @Nullable
     protected KeyboardQuickSwitchTaskView getTaskAt(int index) {
         return !mDisplayingRecentTasks || index < 0 || index >= mContent.getChildCount()
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index a293f74..cbb991d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -24,7 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
 import com.android.quickstep.SystemUiProxy;
@@ -92,27 +92,19 @@
 
     protected void closeQuickSwitchView(boolean animate) {
         if (mCloseAnimation != null) {
-            if (animate) {
-                // Let currently-running animation finish.
-                return;
-            } else {
-                mCloseAnimation.cancel();
+            // Let currently-running animation finish.
+            if (!animate) {
+                mCloseAnimation.end();
             }
+            return;
         }
         if (!animate) {
-            mCloseAnimation = null;
             onCloseComplete();
             return;
         }
         mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation();
 
-        mCloseAnimation.addListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                mCloseAnimation = null;
-                onCloseComplete();
-            }
-        });
+        mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
         mCloseAnimation.start();
     }
 
@@ -160,6 +152,7 @@
     }
 
     private void onCloseComplete() {
+        mCloseAnimation = null;
         mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
         mControllerCallbacks.onCloseComplete();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index eb15cef..881f5c4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -226,24 +226,24 @@
         }
 
 
-        val imeInsetsSize = getInsetsForGravity(taskbarHeightForIme, gravity)
+        // When in gesture nav, report the stashed height to the IME, to allow hiding the
+        // IME navigation bar.
+        val imeInsetsSize = if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
+            getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity);
+        } else {
+            getInsetsForGravity(taskbarHeightForIme, gravity)
+        }
         val imeInsetsSizeOverride =
-                if (!ENABLE_HIDE_IME_CAPTION_BAR) {
-                    arrayOf(
-                            InsetsFrameProvider.InsetsSizeOverride(
-                                    TYPE_INPUT_METHOD,
-                                    imeInsetsSize
-                            ),
-                    )
-                } else {
-                    arrayOf()
-                }
+                arrayOf(
+                        InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
+                )
         // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
         val visInsetsSizeForTappableElement =
                 if (context.isGestureNav) getInsetsForGravity(0, gravity)
                 else getInsetsForGravity(tappableHeight, gravity)
         val insetsSizeOverrideForTappableElement =
-                imeInsetsSizeOverride + arrayOf(
+                arrayOf(
+                        InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
                         InsetsFrameProvider.InsetsSizeOverride(
                                 TYPE_VOICE_INTERACTION,
                                 visInsetsSizeForTappableElement
@@ -252,7 +252,7 @@
         if ((context.isGestureNav || TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW)
                 && provider.type == tappableElement()) {
             provider.insetsSizeOverrides = insetsSizeOverrideForTappableElement
-        } else if (provider.type != systemGestures() && imeInsetsSizeOverride.isNotEmpty()) {
+        } else if (provider.type != systemGestures()) {
             // We only override insets at the bottom of the screen
             provider.insetsSizeOverrides = imeInsetsSizeOverride
         }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index c615d5f..4c3f738 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -120,6 +120,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.AssistStateManager;
 import com.android.quickstep.util.AssistUtils;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -1352,6 +1353,8 @@
             createdOverviewActivity.getDeviceProfile().dump(this, "", pw);
         }
         mTaskbarManager.dumpLogs("", pw);
+        pw.println("AssistStateManager:");
+        AssistStateManager.INSTANCE.get(this).dump("  ", pw);
     }
 
     private AbsSwipeUpHandler createLauncherSwipeHandler(
diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
new file mode 100644
index 0000000..d4923b8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+import java.io.PrintWriter;
+
+/** Class to manage Assistant states. */
+public class AssistStateManager implements ResourceBasedOverride {
+
+    public static final MainThreadInitializedObject<AssistStateManager> INSTANCE =
+            forOverride(AssistStateManager.class, R.string.assist_state_manager_class);
+
+    public AssistStateManager() {}
+
+    /** Whether search is available. */
+    public boolean isSearchAvailable() {
+        return false;
+    }
+
+    /** Dump states. */
+    public void dump(String prefix, PrintWriter writer) {}
+}
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 23b5d30..a10b24d 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -177,7 +177,6 @@
 
     // b/143488140
     //@NavigationModeSwitch
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/266606727
     @Test
     public void goToOverviewFromHome() {
         mDevice.pressHome();
@@ -225,7 +224,6 @@
 
     // b/143488140
     //@NavigationModeSwitch
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/266606727
     @Test
     public void testOverview() {
         startAppFast(getAppPackageName());
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
index 1b5313b..9a2826d 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
@@ -22,6 +22,7 @@
 
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -38,6 +39,7 @@
 
     @Test
     @TaskbarModeSwitch(mode = PERSISTENT)
+    @Ignore // b/301575789
     public void testHideTaskbarPersistsOnRecreate() {
         getTaskbar().hide();
         mLauncher.recreateTaskbar();
diff --git a/src/com/android/launcher3/responsive/HotseatSpecs.kt b/src/com/android/launcher3/responsive/HotseatSpecs.kt
index d578b08..37a682f 100644
--- a/src/com/android/launcher3/responsive/HotseatSpecs.kt
+++ b/src/com/android/launcher3/responsive/HotseatSpecs.kt
@@ -21,7 +21,15 @@
 import com.android.launcher3.R
 import com.android.launcher3.util.ResourceHelper
 
-class HotseatSpecs(val widthSpecs: List<HotseatSpec>, val heightSpecs: List<HotseatSpec>) {
+class HotseatSpecs(widthSpecs: List<HotseatSpec>, heightSpecs: List<HotseatSpec>) {
+
+    val widthSpecs: List<HotseatSpec>
+    val heightSpecs: List<HotseatSpec>
+
+    init {
+        this.widthSpecs = widthSpecs.sortedBy { it.maxAvailableSize }
+        this.heightSpecs = heightSpecs.sortedBy { it.maxAvailableSize }
+    }
 
     fun getCalculatedHeightSpec(availableHeight: Int): CalculatedHotseatSpec {
         val spec = heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecs.kt b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
index 72a0ea4..a43c44a 100644
--- a/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
@@ -24,10 +24,9 @@
  * @param widthSpecs List of width responsive specifications
  * @param heightSpecs List of height responsive specifications
  */
-abstract class ResponsiveSpecs<T : ResponsiveSpec>(
-    val widthSpecs: List<T>,
+abstract class ResponsiveSpecs<T : ResponsiveSpec>(widthSpecs: List<T>, heightSpecs: List<T>) {
+    val widthSpecs: List<T>
     val heightSpecs: List<T>
-) {
 
     init {
         check(widthSpecs.isNotEmpty() && heightSpecs.isNotEmpty()) {
@@ -35,6 +34,9 @@
                 "width list size = ${widthSpecs.size}; " +
                 "height list size = ${heightSpecs.size}."
         }
+
+        this.widthSpecs = widthSpecs.sortedBy { it.maxAvailableSize }
+        this.heightSpecs = heightSpecs.sortedBy { it.maxAvailableSize }
     }
 
     /**
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 7b82195..04f2ffa 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -273,13 +273,19 @@
             final WindowInsetsController wic = root.getWindowInsetsController();
             WindowInsets insets = root.getRootWindowInsets();
             boolean isImeShown = insets != null && insets.isVisible(WindowInsets.Type.ime());
-            if (wic != null && isImeShown) {
-                StatsLogManager slm  = getStatsLogManager();
-                slm.keyboardStateManager().setKeyboardState(HIDE);
+            if (wic != null) {
+                // Only hide the keyboard if it is actually showing.
+                if (isImeShown) {
+                    StatsLogManager slm = getStatsLogManager();
+                    slm.keyboardStateManager().setKeyboardState(HIDE);
 
-                // this method cannot be called cross threads
-                wic.hide(WindowInsets.Type.ime());
-                slm.logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
+                    // this method cannot be called cross threads
+                    wic.hide(WindowInsets.Type.ime());
+                    slm.logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
+                }
+
+                // If the WindowInsetsController is not null, we end here regardless of whether we
+                // hid the keyboard or not.
                 return;
             }
         }
diff --git a/tests/Android.bp b/tests/Android.bp
index fe04527..1471c08 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -44,6 +44,7 @@
     srcs: [
       "src/com/android/launcher3/allapps/OopTaplOpenCloseAllApps.java",
       "src/com/android/launcher3/dragging/TaplDragTest.java",
+      "src/com/android/launcher3/dragging/TaplUninstallRemove.java",
       "src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
       "src/com/android/launcher3/ui/PortraitLandscapeRunner.java",
       "src/com/android/launcher3/ui/TaplTestsLauncher3.java",
diff --git a/tests/res/xml/valid_workspace_unsorted_file.xml b/tests/res/xml/valid_workspace_unsorted_file.xml
new file mode 100644
index 0000000..1216c81
--- /dev/null
+++ b/tests/res/xml/valid_workspace_unsorted_file.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="8dp" />
+        <endPadding launcher:ofRemainderSpace="1" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </workspaceSpec>
+
+    <!-- 584 grid height -->
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="584dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:fixedSize="32dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:ofAvailableSpace="0.15808" />
+    </workspaceSpec>
+
+    <!-- 584 grid height + 28 remainder space -->
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="612dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:ofRemainderSpace="1" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </workspaceSpec>
+
+    <!-- Width spec is always the same -->
+    <workspaceSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="22dp" />
+        <endPadding launcher:fixedSize="22dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:ofRemainderSpace="0.25" />
+    </workspaceSpec>
+
+</workspaceSpecs>
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java
new file mode 100644
index 0000000..712806c
--- /dev/null
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java
@@ -0,0 +1,167 @@
+/*
+ * 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.dragging;
+
+import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
+import static com.android.launcher3.ui.TaplTestsLauncher3.APP_NAME;
+import static com.android.launcher3.ui.TaplTestsLauncher3.DUMMY_APP_NAME;
+import static com.android.launcher3.ui.TaplTestsLauncher3.MAPS_APP_NAME;
+import static com.android.launcher3.ui.TaplTestsLauncher3.STORE_APP_NAME;
+import static com.android.launcher3.ui.TaplTestsLauncher3.initialize;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Point;
+import android.platform.test.annotations.PlatinumTest;
+import android.util.Log;
+
+import com.android.launcher3.tapl.HomeAllApps;
+import com.android.launcher3.tapl.HomeAppIcon;
+import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.ScreenRecordRule;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Test runs in Out of process (Oop) and In process (Ipc)
+ * Test the behaviour of uninstalling and removing apps both from AllApps and from the Workspace.
+ */
+public class TaplUninstallRemove extends AbstractLauncherUiTest {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        initialize(this);
+    }
+
+    /**
+     * Deletes app both built-in and user-installed from the Workspace and makes sure it's no longer
+     * in the Workspace.
+     */
+    @Test
+    @PortraitLandscape
+    public void testDeleteFromWorkspace() {
+        for (String appName : new String[]{"Gmail", "Play Store", APP_NAME}) {
+            final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(appName);
+            Workspace workspace = mLauncher.getWorkspace().deleteAppIcon(homeAppIcon);
+            workspace.verifyWorkspaceAppIconIsGone(
+                    appName + " app was found after being deleted from workspace",
+                    appName);
+        }
+    }
+
+    private void verifyAppUninstalledFromAllApps(Workspace workspace, String appName) {
+        final HomeAllApps allApps = workspace.switchToAllApps();
+        Wait.atMost(appName + " app was found on all apps after being uninstalled",
+                () -> allApps.tryGetAppIcon(appName) == null,
+                DEFAULT_UI_TIMEOUT, mLauncher);
+    }
+
+    private void installDummyAppAndWaitForUIUpdate() throws IOException {
+        TestUtil.installDummyApp();
+        waitForLauncherUIUpdate();
+    }
+
+    private void waitForLauncherUIUpdate() {
+        // Wait for model thread completion as it may be processing
+        // the install event from the SystemService
+        mLauncher.waitForModelQueueCleared();
+        // Wait for Launcher UI thread completion, as it may be processing updating the UI in
+        // response to the model update. Not that `waitForLauncherInitialized` is just a proxy
+        // method, we can use any method which touches Launcher UI thread,
+        mLauncher.waitForLauncherInitialized();
+    }
+
+    /**
+     * Makes sure you can uninstall an app from the Workspace.
+     * @throws Exception
+     */
+    @Test
+    @PortraitLandscape
+    // TODO(b/293944634): Remove Screenrecord after flaky debug, and add
+    // @PlatinumTest(focusArea = "launcher") back
+    @ScreenRecordRule.ScreenRecord
+    public void testUninstallFromWorkspace() throws Exception {
+        installDummyAppAndWaitForUIUpdate();
+        try {
+            verifyAppUninstalledFromAllApps(
+                    createShortcutInCenterIfNotExist(DUMMY_APP_NAME).uninstall(), DUMMY_APP_NAME);
+        } finally {
+            TestUtil.uninstallDummyApp();
+        }
+    }
+
+    /**
+     * Makes sure you can uninstall an app from AllApps.
+     * @throws Exception
+     */
+    @Test
+    @PortraitLandscape
+    @PlatinumTest(focusArea = "launcher")
+    public void testUninstallFromAllApps() throws Exception {
+        installDummyAppAndWaitForUIUpdate();
+        try {
+            Workspace workspace = mLauncher.getWorkspace();
+            final HomeAllApps allApps = workspace.switchToAllApps();
+            workspace = allApps.getAppIcon(DUMMY_APP_NAME).uninstall();
+            verifyAppUninstalledFromAllApps(workspace, DUMMY_APP_NAME);
+        } finally {
+            TestUtil.uninstallDummyApp();
+        }
+    }
+
+    /**
+     * Adds three icons to the workspace and removes one of them by dragging to uninstall.
+     */
+    @Test
+    @PlatinumTest(focusArea = "launcher")
+    public void uninstallWorkspaceIcon() throws IOException {
+        Point[] gridPositions = TestUtil.getCornersAndCenterPositions(mLauncher);
+        StringBuilder sb = new StringBuilder();
+        for (Point p : gridPositions) {
+            sb.append(p).append(", ");
+        }
+        Log.d(ICON_MISSING, "allGridPositions: " + sb);
+        createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]);
+        createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]);
+        installDummyAppAndWaitForUIUpdate();
+        try {
+            createShortcutIfNotExist(DUMMY_APP_NAME, gridPositions[2]);
+            Map<String, Point> initialPositions =
+                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
+            assertThat(initialPositions.keySet())
+                    .containsAtLeast(DUMMY_APP_NAME, MAPS_APP_NAME, STORE_APP_NAME);
+
+            mLauncher.getWorkspace().getWorkspaceAppIcon(DUMMY_APP_NAME).uninstall();
+            mLauncher.getWorkspace().verifyWorkspaceAppIconIsGone(
+                    DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
+
+            Map<String, Point> finalPositions =
+                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
+            assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
+        } finally {
+            TestUtil.uninstallDummyApp();
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
index 0af694e..8f56c5f 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
@@ -104,4 +104,43 @@
         assertThat(heightSpec.gutterPx).isEqualTo(54)
         assertThat(heightSpec.cellSizePx).isEqualTo(260)
     }
+
+    /**
+     * This test tests:
+     * - (height spec) gets the correct breakpoint from the XML - use the first breakpoint
+     * - (height spec) do the correct calculations for remainder space and fixed size
+     * - (width spec) do the correct calculations for remainder space and fixed size
+     */
+    @Test
+    fun smallPhone_returnsFirstBreakpointSpec_unsortedFile() {
+        val deviceSpec = deviceSpecs["phone"]!!
+        deviceSpec.densityDpi = 540 // larger display size
+        initializeVarsForPhone(deviceSpec)
+
+        val availableWidth = deviceSpec.naturalSize.first
+        // Hotseat size is roughly 640px on a real device,
+        // it doesn't need to be precise on unit tests
+        val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 640
+
+        val workspaceSpecs =
+            WorkspaceSpecs.create(
+                TestResourceHelper(context!!, TestR.xml.valid_workspace_unsorted_file)
+            )
+        val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
+        val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
+
+        assertThat(widthSpec.availableSpace).isEqualTo(availableWidth)
+        assertThat(widthSpec.cells).isEqualTo(4)
+        assertThat(widthSpec.startPaddingPx).isEqualTo(74)
+        assertThat(widthSpec.endPaddingPx).isEqualTo(74)
+        assertThat(widthSpec.gutterPx).isEqualTo(54)
+        assertThat(widthSpec.cellSizePx).isEqualTo(193)
+
+        assertThat(heightSpec.availableSpace).isEqualTo(availableHeight)
+        assertThat(heightSpec.cells).isEqualTo(5)
+        assertThat(heightSpec.startPaddingPx).isEqualTo(0)
+        assertThat(heightSpec.endPaddingPx).isEqualTo(108)
+        assertThat(heightSpec.gutterPx).isEqualTo(54)
+        assertThat(heightSpec.cellSizePx).isEqualTo(260)
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 69572df..8f2ac98 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -16,12 +16,9 @@
 
 package com.android.launcher3.ui;
 
-import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
 import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -29,9 +26,7 @@
 import static org.junit.Assume.assumeFalse;
 
 import android.content.Intent;
-import android.graphics.Point;
 import android.platform.test.annotations.PlatinumTest;
-import android.util.Log;
 
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
@@ -53,7 +48,6 @@
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.TISBindRule;
 import com.android.launcher3.util.rule.TestStabilityRule.Stability;
@@ -66,9 +60,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.IOException;
-import java.util.Map;
-
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
@@ -324,90 +315,6 @@
 
     @Test
     @PortraitLandscape
-    public void testDeleteFromWorkspace() throws Exception {
-        // test delete both built-in apps and user-installed app from workspace
-        for (String appName : new String[]{"Gmail", "Play Store", APP_NAME}) {
-            final HomeAppIcon homeAppIcon = createShortcutInCenterIfNotExist(appName);
-            Workspace workspace = mLauncher.getWorkspace().deleteAppIcon(homeAppIcon);
-            workspace.verifyWorkspaceAppIconIsGone(
-                    appName + " app was found after being deleted from workspace",
-                    appName);
-        }
-    }
-
-    private void verifyAppUninstalledFromAllApps(Workspace workspace, String appName) {
-        final HomeAllApps allApps = workspace.switchToAllApps();
-        Wait.atMost(appName + " app was found on all apps after being uninstalled",
-                () -> allApps.tryGetAppIcon(appName) == null,
-                DEFAULT_UI_TIMEOUT, mLauncher);
-    }
-
-    @Test
-    @PortraitLandscape
-    // TODO(b/293944634): Remove Screenrecord after flaky debug, and add
-    // @PlatinumTest(focusArea = "launcher") back
-    @ScreenRecord
-    public void testUninstallFromWorkspace() throws Exception {
-        installDummyAppAndWaitForUIUpdate();
-        try {
-            verifyAppUninstalledFromAllApps(
-                    createShortcutInCenterIfNotExist(DUMMY_APP_NAME).uninstall(), DUMMY_APP_NAME);
-        } finally {
-            TestUtil.uninstallDummyApp();
-        }
-    }
-
-    @Test
-    @PortraitLandscape
-    @PlatinumTest(focusArea = "launcher")
-    public void testUninstallFromAllApps() throws Exception {
-        installDummyAppAndWaitForUIUpdate();
-        try {
-            Workspace workspace = mLauncher.getWorkspace();
-            final HomeAllApps allApps = workspace.switchToAllApps();
-            workspace = allApps.getAppIcon(DUMMY_APP_NAME).uninstall();
-            verifyAppUninstalledFromAllApps(workspace, DUMMY_APP_NAME);
-        } finally {
-            TestUtil.uninstallDummyApp();
-        }
-    }
-    /**
-     * Adds three icons to the workspace and removes one of them by dragging to uninstall.
-     */
-    @Test
-    @ScreenRecord // b/241821721
-    @PlatinumTest(focusArea = "launcher")
-    public void uninstallWorkspaceIcon() throws IOException {
-        Point[] gridPositions = TestUtil.getCornersAndCenterPositions(mLauncher);
-        StringBuilder sb = new StringBuilder();
-        for (Point p : gridPositions) {
-            sb.append(p).append(", ");
-        }
-        Log.d(ICON_MISSING, "allGridPositions: " + sb);
-        createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]);
-        createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]);
-        installDummyAppAndWaitForUIUpdate();
-        try {
-            createShortcutIfNotExist(DUMMY_APP_NAME, gridPositions[2]);
-            Map<String, Point> initialPositions =
-                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
-            assertThat(initialPositions.keySet())
-                    .containsAtLeast(DUMMY_APP_NAME, MAPS_APP_NAME, STORE_APP_NAME);
-
-            mLauncher.getWorkspace().getWorkspaceAppIcon(DUMMY_APP_NAME).uninstall();
-            mLauncher.getWorkspace().verifyWorkspaceAppIconIsGone(
-                    DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
-
-            Map<String, Point> finalPositions =
-                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
-            assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
-        } finally {
-            TestUtil.uninstallDummyApp();
-        }
-    }
-
-    @Test
-    @PortraitLandscape
     public void testAddDeleteShortcutOnHotseat() {
         mLauncher.getWorkspace()
                 .deleteAppIcon(mLauncher.getWorkspace().getHotseatAppIcon(0))
@@ -418,21 +325,6 @@
                 mLauncher.getWorkspace().getHotseatAppIcon(APP_NAME));
     }
 
-    private void installDummyAppAndWaitForUIUpdate() throws IOException {
-        TestUtil.installDummyApp();
-        waitForLauncherUIUpdate();
-    }
-
-    private void waitForLauncherUIUpdate() {
-        // Wait for model thread completion as it may be processing
-        // the install event from the SystemService
-        mLauncher.waitForModelQueueCleared();
-        // Wait for Launcher UI thread completion, as it may be processing updating the UI in
-        // response to the model update. Not that `waitForLauncherInitialized` is just a proxy
-        // method, we can use any method which touches Launcher UI thread,
-        mLauncher.waitForLauncherInitialized();
-    }
-
     @Test
     public void testGetAppIconName() {
         HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
diff --git a/tests/tapl/com/android/launcher3/tapl/Qsb.java b/tests/tapl/com/android/launcher3/tapl/Qsb.java
index 7f3f61d..0f2aff8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Qsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/Qsb.java
@@ -29,6 +29,8 @@
 
     private static final String ASSISTANT_APP_PACKAGE = "com.google.android.googlequicksearchbox";
     private static final String ASSISTANT_ICON_RES_ID = "mic_icon";
+    private static final String LENS_ICON_RES_ID = "lens_icon";
+    private static final String LENS_APP_TEXT_RES_ID = "lens_camera_cutout_text";
     protected final LauncherInstrumentation mLauncher;
     private final UiObject2 mContainer;
     private final String mQsbResName;
@@ -76,6 +78,37 @@
     }
 
     /**
+     * Launches lens app by tapping lens icon on qsb.
+     */
+    @NonNull
+    public LaunchedAppState launchLens() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to click lens icon button");
+             LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            UiObject2 lensIcon = mLauncher.waitForLauncherObject(LENS_ICON_RES_ID);
+
+            LauncherInstrumentation.log("Qsb.launchLens before click "
+                    + lensIcon.getVisibleCenter() + " in "
+                    + mLauncher.getVisibleBounds(lensIcon));
+
+            mLauncher.clickLauncherObject(lensIcon);
+
+            try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) {
+                // Package name is not enough to check if the app is launched, because many
+                // elements are having googlequicksearchbox as package name. So it checks if the
+                // corresponding text resource is displayed
+                BySelector selector = By.res(ASSISTANT_APP_PACKAGE, LENS_APP_TEXT_RES_ID);
+                mLauncher.assertTrue(
+                        "Lens app didn't start: (" + selector + ")",
+                        mLauncher.getDevice().wait(Until.hasObject(selector),
+                                LauncherInstrumentation.WAIT_TIME_MS)
+                );
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
+    /**
      * Show search result page from tapping qsb.
      */
     public SearchResultFromQsb showSearchResult() {