Merge "Revert "Adding some utility test rules"" into udc-qpr-dev
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index eec334a..a8e6849 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -306,7 +306,10 @@
         if (!isExpanded()) {
             for (int i = 0; i < viewOrder.size(); i++) {
                 View child = viewOrder.get(i);
-                if (child != null) {
+                // this child view may have already been removed so verify that it still exists
+                // before reordering it, otherwise it will be re-added.
+                int indexOfChild = indexOfChild(child);
+                if (child != null && indexOfChild >= 0) {
                     removeViewInLayout(child);
                     addViewInLayout(child, i, child.getLayoutParams());
                 }
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 538aba9..2d5b8db 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -38,6 +38,8 @@
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_DIVIDER_ANIM_DURATION;
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED;
 
@@ -431,6 +433,7 @@
             int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
             SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
         if (launchingTaskView != null) {
+            testLogD(LAUNCH_SPLIT_PAIR, "composeRecentsSplitLaunchAnimator taskView not-null");
             AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.addListener(new AnimatorListenerAdapter() {
                 @Override
@@ -463,7 +466,10 @@
         TransitionInfo.Change splitRoot2 = null;
         for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
             final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
-            if (change.getTaskInfo() == null) continue;
+            if (change.getTaskInfo() == null) {
+                testLogD(LAUNCH_SPLIT_PAIR, "changeTaskInfo null; change: " + change);
+                continue;
+            }
             final int taskId = change.getTaskInfo().taskId;
             final int mode = change.getMode();
 
@@ -499,6 +505,7 @@
 
     private static void animateSplitRoot(SurfaceControl.Transaction t,
             TransitionInfo.Change splitRoot) {
+        testLogD(LAUNCH_SPLIT_PAIR, "animateSplitRoot: " + splitRoot);
         if (splitRoot != null) {
             t.show(splitRoot.getLeash());
             t.setAlpha(splitRoot.getLeash(), 1.f);
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index bb12356..f1244ff 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -888,7 +888,8 @@
                             .append(SUBSTRING_PREFIX)
                             .append("TaskbarActivityContext != null, "
                                     + "using TaskbarUnstashInputConsumer");
-                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac);
+                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
+                            mOverviewCommandHelper);
                 }
             } else if (canStartSystemGesture && FeatureFlags.ENABLE_LONG_PRESS_NAV_HANDLE.get()) {
                 base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index e9a0761..172c9e9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static android.view.MotionEvent.ACTION_BUTTON_RELEASE;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -41,6 +42,7 @@
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.OverviewCommandHelper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
@@ -51,6 +53,7 @@
 public class TaskbarUnstashInputConsumer extends DelegateInputConsumer {
 
     private final TaskbarActivityContext mTaskbarActivityContext;
+    private final OverviewCommandHelper mOverviewCommandHelper;
     private final GestureDetector mLongPressDetector;
     private final float mSquaredTouchSlop;
 
@@ -80,9 +83,11 @@
     private final @Nullable TransitionCallback mTransitionCallback;
 
     public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
-            InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
+            InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext,
+            OverviewCommandHelper overviewCommandHelper) {
         super(delegate, inputMonitor);
         mTaskbarActivityContext = taskbarActivityContext;
+        mOverviewCommandHelper = overviewCommandHelper;
         // TODO(b/270395798): remove this when cleaning up old Persistent Taskbar code.
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
         mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
@@ -123,7 +128,11 @@
     public void onMotionEvent(MotionEvent ev) {
         mLongPressDetector.onTouchEvent(ev);
         if (mState != STATE_ACTIVE) {
-            mDelegate.onMotionEvent(ev);
+            boolean isStashedTaskbarHovered =
+                    isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY());
+            if (!isStashedTaskbarHovered) {
+                mDelegate.onMotionEvent(ev);
+            }
 
             // Only show the transient task bar if the touch events are on the screen.
             if (mTaskbarActivityContext != null && !isTrackpadMotionEvent(ev)) {
@@ -218,6 +227,11 @@
                         mHasPassedTaskbarNavThreshold = false;
                         mIsInBubbleBarArea = false;
                         break;
+                    case ACTION_BUTTON_RELEASE:
+                        if (isStashedTaskbarHovered) {
+                            mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME);
+                        }
+                        break;
                 }
             }
         }
@@ -274,19 +288,17 @@
 
     private void updateHoveredTaskbarState(int x, int y) {
         DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
-        mStashedTaskbarHandleBounds.set(
+        mBottomEdgeBounds.set(
                 (dp.widthPx - (int) mUnstashArea) / 2,
-                dp.heightPx - dp.stashedTaskbarHeight,
+                dp.heightPx - mStashedTaskbarBottomEdge,
                 (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
                 dp.heightPx);
-        mBottomEdgeBounds.set(mStashedTaskbarHandleBounds);
-        mBottomEdgeBounds.top = dp.heightPx - mStashedTaskbarBottomEdge;
 
         if (mBottomEdgeBounds.contains(x, y)) {
             // If hovering stashed taskbar and then hover screen bottom edge, unstash it.
             mTaskbarActivityContext.onSwipeToUnstashTaskbar();
             mIsStashedTaskbarHovered = false;
-        } else if (!mStashedTaskbarHandleBounds.contains(x, y)) {
+        } else if (!isStashedTaskbarHovered(x, y)) {
             // If exit hovering stashed taskbar, remove hint.
             startStashedTaskbarHover(/* isHovered = */ false);
         }
@@ -294,18 +306,13 @@
 
     private void updateUnhoveredTaskbarState(int x, int y) {
         DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
-        mStashedTaskbarHandleBounds.set(
-                (dp.widthPx - (int) mUnstashArea) / 2,
-                dp.heightPx - dp.stashedTaskbarHeight,
-                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
-                dp.heightPx);
         mBottomEdgeBounds.set(
                 0,
                 dp.heightPx - mBottomScreenEdge,
                 dp.widthPx,
                 dp.heightPx);
 
-        if (mStashedTaskbarHandleBounds.contains(x, y)) {
+        if (isStashedTaskbarHovered(x, y)) {
             // If enter hovering stashed taskbar, start hint.
             startStashedTaskbarHover(/* isHovered = */ true);
         } else if (mBottomEdgeBounds.contains(x, y)) {
@@ -318,4 +325,19 @@
         mTaskbarActivityContext.startTaskbarUnstashHint(isHovered, /* forceUnstash = */ true);
         mIsStashedTaskbarHovered = isHovered;
     }
+
+    private boolean isStashedTaskbarHovered(int x, int y) {
+        if (!mTaskbarActivityContext.isTaskbarStashed()
+                || mTaskbarActivityContext.isTaskbarAllAppsOpen()
+                || !ENABLE_CURSOR_HOVER_STATES.get()) {
+            return false;
+        }
+        DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+        mStashedTaskbarHandleBounds.set(
+                (dp.widthPx - (int) mUnstashArea) / 2,
+                dp.heightPx - dp.stashedTaskbarHeight,
+                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+                dp.heightPx);
+        return mStashedTaskbarHandleBounds.contains(x, y);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 51211df..7ba6d42 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -17,6 +17,8 @@
 package com.android.quickstep.util;
 
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT;
@@ -293,7 +295,7 @@
 
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback);
+                    secondTaskId, callback, "LaunchSplitPair");
             switch (launchData.getSplitLaunchType()) {
                 case SPLIT_TASK_TASK ->
                         mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
@@ -385,7 +387,7 @@
 
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback);
+                    secondTaskId, callback, "LaunchExistingPair");
             mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
                     null /* options2 */, stagePosition, splitRatio,
                     remoteTransition, null /*shellInstanceId*/);
@@ -455,11 +457,11 @@
     }
 
     private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
-            Consumer<Boolean> callback) {
+            Consumer<Boolean> callback, String transitionName) {
         final RemoteSplitLaunchTransitionRunner animationRunner =
                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
         return new RemoteTransition(animationRunner,
-                ActivityThread.currentActivityThread().getApplicationThread(), "LaunchSplitPair");
+                ActivityThread.currentActivityThread().getApplicationThread(), transitionName);
     }
 
     private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
@@ -522,6 +524,7 @@
         public void startAnimation(IBinder transition, TransitionInfo info,
                 SurfaceControl.Transaction t,
                 IRemoteTransitionFinishedCallback finishedCallback) {
+            testLogD(LAUNCH_SPLIT_PAIR, "Received split startAnimation");
             final Runnable finishAdapter = () ->  {
                 try {
                     finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 21da7cf..a21bbe1 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1369,9 +1369,15 @@
             return null;
         }
 
+        // We're looking for a taskView that matches these ids, regardless of order
+        int[] taskIdsCopy = Arrays.copyOf(taskIds, taskIds.length);
+        Arrays.sort(taskIdsCopy);
+
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
-            if (Arrays.equals(taskIds, taskView.getTaskIds())) {
+            int[] taskViewIdsCopy = taskView.getTaskIds();
+            Arrays.sort(taskViewIdsCopy);
+            if (Arrays.equals(taskIdsCopy, taskViewIdsCopy)) {
                 return taskView;
             }
         }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
index b58fe29..3869bf7 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
-
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -64,4 +63,15 @@
             throw new RuntimeException(e);
         }
     }
+
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testClickHoveredTaskbarToGoHome() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().clickStashedTaskbarToGoHome();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 733f527..9803779 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -252,7 +252,7 @@
     </declare-styleable>
 
     <!--  Responsive grids attributes  -->
-    <declare-styleable name="WorkspaceSpec">
+    <declare-styleable name="ResponsiveSpec">
         <attr name="specType" format="integer">
             <enum name="height" value="0" />
             <enum name="width" value="1" />
@@ -260,12 +260,9 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SizeSpec">
-        <attr name="fixedSize" format="dimension" />
-        <attr name="ofAvailableSpace" format="float" />
-        <attr name="ofRemainderSpace" format="float" />
-        <attr name="matchWorkspace" format="boolean" />
-        <attr name="maxSize" format="dimension" />
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
     </declare-styleable>
 
     <declare-styleable name="FolderSpec">
@@ -278,6 +275,14 @@
         <attr name="maxAvailableSize" />
     </declare-styleable>
 
+    <declare-styleable name="SizeSpec">
+        <attr name="fixedSize" format="dimension" />
+        <attr name="ofAvailableSpace" format="float" />
+        <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
+    </declare-styleable>
+
     <declare-styleable name="ProfileDisplayOption">
         <attr name="name" />
         <attr name="minWidthDps" format="float" />
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 2356bcc..360e060 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -97,7 +97,7 @@
     public static final int DISPLAY_ALL_APPS = 1;
     private static final int DISPLAY_FOLDER = 2;
     protected static final int DISPLAY_TASKBAR = 5;
-    private static final int DISPLAY_SEARCH_RESULT = 6;
+    public static final int DISPLAY_SEARCH_RESULT = 6;
     private static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
     public static final int DISPLAY_PREDICTION_ROW = 8;
 
@@ -632,6 +632,11 @@
         }
     }
 
+    @VisibleForTesting
+    public boolean getForceHideDot() {
+        return mForceHideDot;
+    }
+
     private boolean hasDot() {
         return mDotInfo != null;
     }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index f3b5155..88dcfcd 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -56,15 +56,15 @@
 import com.android.launcher3.responsive.AllAppsSpecs;
 import com.android.launcher3.responsive.CalculatedAllAppsSpec;
 import com.android.launcher3.responsive.CalculatedFolderSpec;
+import com.android.launcher3.responsive.CalculatedWorkspaceSpec;
 import com.android.launcher3.responsive.FolderSpecs;
+import com.android.launcher3.responsive.WorkspaceSpecs;
 import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.IconSizeSteps;
 import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.WindowBounds;
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec;
-import com.android.launcher3.workspace.WorkspaceSpecs;
 
 import java.io.PrintWriter;
 import java.util.Locale;
@@ -115,13 +115,10 @@
 
     // Responsive grid
     private final boolean mIsResponsiveGrid;
-    private WorkspaceSpecs mWorkspaceSpecs;
     private CalculatedWorkspaceSpec mResponsiveWidthSpec;
     private CalculatedWorkspaceSpec mResponsiveHeightSpec;
-    private AllAppsSpecs mAllAppsSpecs;
     private CalculatedAllAppsSpec mAllAppsResponsiveWidthSpec;
     private CalculatedAllAppsSpec mAllAppsResponsiveHeightSpec;
-    private FolderSpecs mFolderSpecs;
     private CalculatedFolderSpec mResponsiveFolderWidthSpec;
     private CalculatedFolderSpec mResponsiveFolderHeightSpec;
 
@@ -545,29 +542,31 @@
         // Needs to be calculated after hotseatBarSizePx is correct,
         // for the available height to be correct
         if (mIsResponsiveGrid) {
-            mWorkspaceSpecs = new WorkspaceSpecs(new ResourceHelper(context, inv.workspaceSpecsId));
+            WorkspaceSpecs workspaceSpecs = WorkspaceSpecs.create(
+                    new ResourceHelper(context, inv.workspaceSpecsId));
             int availableResponsiveWidth =
                     availableWidthPx - (isVerticalBarLayout() ? hotseatBarSizePx : 0);
             // don't use availableHeightPx because it subtracts bottom padding,
             // but the workspace go behind it
             int availableResponsiveHeight =
                     heightPx - mInsets.top - (isVerticalBarLayout() ? 0 : hotseatBarSizePx);
-            mResponsiveWidthSpec = mWorkspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
+            mResponsiveWidthSpec = workspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
                     availableResponsiveWidth);
-            mResponsiveHeightSpec = mWorkspaceSpecs.getCalculatedHeightSpec(inv.numRows,
+            mResponsiveHeightSpec = workspaceSpecs.getCalculatedHeightSpec(inv.numRows,
                     availableResponsiveHeight);
 
-            mAllAppsSpecs = new AllAppsSpecs(new ResourceHelper(context, inv.allAppsSpecsId));
-            mAllAppsResponsiveWidthSpec = mAllAppsSpecs.getCalculatedWidthSpec(inv.numColumns,
+            AllAppsSpecs allAppsSpecs = AllAppsSpecs.create(
+                    new ResourceHelper(context, inv.allAppsSpecsId));
+            mAllAppsResponsiveWidthSpec = allAppsSpecs.getCalculatedWidthSpec(inv.numColumns,
                     mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
-            mAllAppsResponsiveHeightSpec = mAllAppsSpecs.getCalculatedHeightSpec(inv.numRows,
-                    mResponsiveHeightSpec.getAvailableSpace(),
-                    mResponsiveHeightSpec);
+            mAllAppsResponsiveHeightSpec = allAppsSpecs.getCalculatedHeightSpec(inv.numRows,
+                    mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
 
-            mFolderSpecs = new FolderSpecs(new ResourceHelper(context, inv.folderSpecsId));
-            mResponsiveFolderWidthSpec = mFolderSpecs.getWidthSpec(inv.numFolderColumns,
+            FolderSpecs folderSpecs = FolderSpecs.create(
+                    new ResourceHelper(context, inv.folderSpecsId));
+            mResponsiveFolderWidthSpec = folderSpecs.getCalculatedWidthSpec(inv.numFolderColumns,
                     mResponsiveWidthSpec.getAvailableSpace(), mResponsiveWidthSpec);
-            mResponsiveFolderHeightSpec = mFolderSpecs.getHeightSpec(inv.numFolderRows,
+            mResponsiveFolderHeightSpec = folderSpecs.getCalculatedHeightSpec(inv.numFolderRows,
                     mResponsiveHeightSpec.getAvailableSpace(), mResponsiveHeightSpec);
         }
 
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 4f5de05..e8c6ff9 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -334,14 +334,12 @@
 
     public static void scaleRectAboutCenter(Rect r, float scale) {
         if (scale != 1.0f) {
-            int cx = r.centerX();
-            int cy = r.centerY();
-            r.offset(-cx, -cy);
-            r.left = (int) (r.left * scale + 0.5f);
-            r.top = (int) (r.top * scale + 0.5f);
-            r.right = (int) (r.right * scale + 0.5f);
-            r.bottom = (int) (r.bottom * scale + 0.5f);
-            r.offset(cx, cy);
+            float cx = r.exactCenterX();
+            float cy = r.exactCenterY();
+            r.left = Math.round(cx + (r.left - cx) * scale);
+            r.top = Math.round(cy + (r.top - cy) * scale);
+            r.right = Math.round(cx + (r.right - cx) * scale);
+            r.bottom = Math.round(cy + (r.bottom - cy) * scale);
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index eb4ecaf..4c86bec 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -391,7 +391,10 @@
                         rebindAdapters(false);
                         mRebindAdaptersAfterSearchAnimation = false;
                     }
-                    if (!goingToSearch) {
+
+                    if (goingToSearch) {
+                        mSearchUiDelegate.onAnimateToSearchStateCompleted();
+                    } else {
                         setSearchResults(null);
                         if (mViewPager != null) {
                             mViewPager.setCurrentPage(previousPage);
@@ -585,6 +588,10 @@
         } else {
             mAH.get(AdapterHolder.MAIN).setup(findViewById(R.id.apps_list_view), null);
             mAH.get(AdapterHolder.WORK).mRecyclerView = null;
+            if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+                mAH.get(AdapterHolder.MAIN).mRecyclerView
+                        .setRecycledViewPool(mAllAppsStore.getRecyclerViewPool());
+            }
         }
         setupHeader();
 
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
index 49cecca..2347bfd 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchUiDelegate.java
@@ -49,6 +49,11 @@
         // Do nothing.
     }
 
+    /** Invoked when transition animations to go to search is completed . */
+    public void onAnimateToSearchStateCompleted() {
+        // Do nothing
+    }
+
     /** Invoked when the search bar has been added to All Apps. */
     public void onInitializeSearchBar() {
         // Do nothing.
diff --git a/src/com/android/launcher3/responsive/AllAppsSpecs.kt b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
index 85e383e..8ed3ffc 100644
--- a/src/com/android/launcher3/responsive/AllAppsSpecs.kt
+++ b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
@@ -16,277 +16,89 @@
 
 package com.android.launcher3.responsive
 
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
 import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
 import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
 
-private const val LOG_TAG = "AllAppsSpecs"
+class AllAppsSpecs(widthSpecs: List<AllAppsSpec>, heightSpecs: List<AllAppsSpec>) :
+    ResponsiveSpecs<AllAppsSpec>(widthSpecs, heightSpecs) {
 
-class AllAppsSpecs(resourceHelper: ResourceHelper) {
-    object XmlTags {
-        const val ALL_APPS_SPECS = "allAppsSpecs"
-
-        const val ALL_APPS_SPEC = "allAppsSpec"
-        const val START_PADDING = "startPadding"
-        const val END_PADDING = "endPadding"
-        const val GUTTER = "gutter"
-        const val CELL_SIZE = "cellSize"
-    }
-
-    val allAppsHeightSpecList = mutableListOf<AllAppsSpec>()
-    val allAppsWidthSpecList = mutableListOf<AllAppsSpec>()
-
-    // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
-    init {
-        var parser: XmlResourceParser? = null
-        try {
-            parser = resourceHelper.getXml()
-            val depth = parser.depth
-            var type: Int
-            while (
-                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                    parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
-            ) {
-                if (type == XmlPullParser.START_TAG && XmlTags.ALL_APPS_SPECS == parser.name) {
-                    val displayDepth = parser.depth
-                    while (
-                        (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                            parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
-                    ) {
-                        if (
-                            type == XmlPullParser.START_TAG && XmlTags.ALL_APPS_SPEC == parser.name
-                        ) {
-                            val attrs =
-                                resourceHelper.obtainStyledAttributes(
-                                    Xml.asAttributeSet(parser),
-                                    R.styleable.AllAppsSpec
-                                )
-                            val maxAvailableSize =
-                                attrs.getDimensionPixelSize(
-                                    R.styleable.AllAppsSpec_maxAvailableSize,
-                                    0
-                                )
-                            val specType =
-                                AllAppsSpec.SpecType.values()[
-                                        attrs.getInt(
-                                            R.styleable.AllAppsSpec_specType,
-                                            AllAppsSpec.SpecType.HEIGHT.ordinal
-                                        )]
-                            attrs.recycle()
-
-                            var startPadding: SizeSpec? = null
-                            var endPadding: SizeSpec? = null
-                            var gutter: SizeSpec? = null
-                            var cellSize: SizeSpec? = null
-
-                            val limitDepth = parser.depth
-                            while (
-                                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                                    parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
-                            ) {
-                                val attr: AttributeSet = Xml.asAttributeSet(parser)
-                                if (type == XmlPullParser.START_TAG) {
-                                    when (parser.name) {
-                                        XmlTags.START_PADDING -> {
-                                            startPadding = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.END_PADDING -> {
-                                            endPadding = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.GUTTER -> {
-                                            gutter = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.CELL_SIZE -> {
-                                            cellSize = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                    }
-                                }
-                            }
-
-                            if (
-                                startPadding == null ||
-                                    endPadding == null ||
-                                    gutter == null ||
-                                    cellSize == null
-                            ) {
-                                throw IllegalStateException(
-                                    "All attributes in AllAppsSpec must be defined"
-                                )
-                            }
-
-                            val allAppsSpec =
-                                AllAppsSpec(
-                                    maxAvailableSize,
-                                    specType,
-                                    startPadding,
-                                    endPadding,
-                                    gutter,
-                                    cellSize
-                                )
-                            if (allAppsSpec.isValid()) {
-                                if (allAppsSpec.specType == AllAppsSpec.SpecType.HEIGHT)
-                                    allAppsHeightSpecList.add(allAppsSpec)
-                                else allAppsWidthSpecList.add(allAppsSpec)
-                            } else {
-                                throw IllegalStateException("Invalid AllAppsSpec found.")
-                            }
-                        }
-                    }
-
-                    if (allAppsWidthSpecList.isEmpty() || allAppsHeightSpecList.isEmpty()) {
-                        throw IllegalStateException(
-                            "AllAppsSpecs is incomplete - " +
-                                "height list size = ${allAppsHeightSpecList.size}; " +
-                                "width list size = ${allAppsWidthSpecList.size}."
-                        )
-                    }
-                }
-            }
-        } catch (e: Exception) {
-            when (e) {
-                is IOException,
-                is XmlPullParserException -> {
-                    throw RuntimeException("Failure parsing all apps specs file.", e)
-                }
-                else -> throw e
-            }
-        } finally {
-            parser?.close()
-        }
-    }
-
-    /**
-     * Returns the CalculatedAllAppsSpec for width, based on the available width, the AllAppsSpecs
-     * and the CalculatedWorkspaceSpec.
-     */
     fun getCalculatedWidthSpec(
         columns: Int,
         availableWidth: Int,
         calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedAllAppsSpec {
-        val widthSpec = allAppsWidthSpecList.first { availableWidth <= it.maxAvailableSize }
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.WIDTH} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
 
-        return CalculatedAllAppsSpec(availableWidth, columns, widthSpec, calculatedWorkspaceSpec)
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedAllAppsSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
     }
 
-    /**
-     * Returns the CalculatedAllAppsSpec for height, based on the available height, the AllAppsSpecs
-     * and the CalculatedWorkspaceSpec.
-     */
     fun getCalculatedHeightSpec(
         rows: Int,
         availableHeight: Int,
         calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedAllAppsSpec {
-        val heightSpec = allAppsHeightSpecList.first { availableHeight <= it.maxAvailableSize }
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${SpecType.HEIGHT} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
+        }
 
-        return CalculatedAllAppsSpec(availableHeight, rows, heightSpec, calculatedWorkspaceSpec)
-    }
-}
-
-class CalculatedAllAppsSpec(
-    val availableSpace: Int,
-    val cells: Int,
-    private val allAppsSpec: AllAppsSpec,
-    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
-) {
-    var startPaddingPx: Int = 0
-        private set
-    var endPaddingPx: Int = 0
-        private set
-    var gutterPx: Int = 0
-        private set
-    var cellSizePx: Int = 0
-        private set
-    init {
-        // Copy values from workspace
-        if (allAppsSpec.startPadding.matchWorkspace)
-            startPaddingPx = calculatedWorkspaceSpec.startPaddingPx
-        if (allAppsSpec.endPadding.matchWorkspace)
-            endPaddingPx = calculatedWorkspaceSpec.endPaddingPx
-        if (allAppsSpec.gutter.matchWorkspace) gutterPx = calculatedWorkspaceSpec.gutterPx
-        if (allAppsSpec.cellSize.matchWorkspace) cellSizePx = calculatedWorkspaceSpec.cellSizePx
-
-        // Calculate all fixed size first
-        if (allAppsSpec.startPadding.fixedSize > 0)
-            startPaddingPx = allAppsSpec.startPadding.fixedSize.roundToInt()
-        if (allAppsSpec.endPadding.fixedSize > 0)
-            endPaddingPx = allAppsSpec.endPadding.fixedSize.roundToInt()
-        if (allAppsSpec.gutter.fixedSize > 0) gutterPx = allAppsSpec.gutter.fixedSize.roundToInt()
-        if (allAppsSpec.cellSize.fixedSize > 0)
-            cellSizePx = allAppsSpec.cellSize.fixedSize.roundToInt()
-
-        // Calculate all available space next
-        if (allAppsSpec.startPadding.ofAvailableSpace > 0)
-            startPaddingPx =
-                (allAppsSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (allAppsSpec.endPadding.ofAvailableSpace > 0)
-            endPaddingPx = (allAppsSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (allAppsSpec.gutter.ofAvailableSpace > 0)
-            gutterPx = (allAppsSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
-        if (allAppsSpec.cellSize.ofAvailableSpace > 0)
-            cellSizePx = (allAppsSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
-        // Calculate remainder space last
-        val gutters = cells - 1
-        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
-        val remainderSpace = availableSpace - usedSpace
-        if (allAppsSpec.startPadding.ofRemainderSpace > 0)
-            startPaddingPx =
-                (allAppsSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (allAppsSpec.endPadding.ofRemainderSpace > 0)
-            endPaddingPx = (allAppsSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (allAppsSpec.gutter.ofRemainderSpace > 0)
-            gutterPx = (allAppsSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
-        if (allAppsSpec.cellSize.ofRemainderSpace > 0)
-            cellSizePx = (allAppsSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedAllAppsSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
     }
 
-    override fun toString(): String {
-        return "CalculatedAllAppsSpec(availableSpace=$availableSpace, " +
-            "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
-            "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
-            "AllAppsSpec.maxAvailableSize=${allAppsSpec.maxAvailableSize})"
+    companion object {
+        private const val XML_ALL_APPS_SPEC = "allAppsSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): AllAppsSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_ALL_APPS_SPEC, ::AllAppsSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return AllAppsSpecs(widthSpecs, heightSpecs)
+        }
     }
 }
 
 data class AllAppsSpec(
-    val maxAvailableSize: Int,
-    val specType: SpecType,
-    val startPadding: SizeSpec,
-    val endPadding: SizeSpec,
-    val gutter: SizeSpec,
-    val cellSize: SizeSpec
-) {
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
 
-    enum class SpecType {
-        HEIGHT,
-        WIDTH
+    init {
+        check(isValid()) { "Invalid AllAppsSpec found." }
     }
 
-    fun isValid(): Boolean {
-        if (maxAvailableSize <= 0) {
-            Log.e(LOG_TAG, "AllAppsSpec#isValid - maxAvailableSize <= 0")
-            return false
-        }
-
-        // All specs need to be individually valid
-        if (!allSpecsAreValid()) {
-            Log.e(LOG_TAG, "AllAppsSpec#isValid - !allSpecsAreValid()")
-            return false
-        }
-
-        return true
-    }
-
-    private fun allSpecsAreValid(): Boolean =
-        startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid()
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+    )
 }
+
+class CalculatedAllAppsSpec(
+    availableSpace: Int,
+    cells: Int,
+    spec: AllAppsSpec,
+    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/FolderSpecs.kt b/src/com/android/launcher3/responsive/FolderSpecs.kt
index f4446bc..bc2db28 100644
--- a/src/com/android/launcher3/responsive/FolderSpecs.kt
+++ b/src/com/android/launcher3/responsive/FolderSpecs.kt
@@ -1,280 +1,105 @@
+/*
+ * 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.responsive
 
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
+import android.content.res.TypedArray
 import com.android.launcher3.R
-import com.android.launcher3.responsive.FolderSpec.*
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
 import com.android.launcher3.util.ResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import com.android.launcher3.workspace.WorkspaceSpec
-import java.io.IOException
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
 
-private const val LOG_TAG = "FolderSpecs"
+class FolderSpecs(widthSpecs: List<FolderSpec>, heightSpecs: List<FolderSpec>) :
+    ResponsiveSpecs<FolderSpec>(widthSpecs, heightSpecs) {
 
-class FolderSpecs(resourceHelper: ResourceHelper) {
-
-    object XmlTags {
-        const val FOLDER_SPECS = "folderSpecs"
-
-        const val FOLDER_SPEC = "folderSpec"
-        const val START_PADDING = "startPadding"
-        const val END_PADDING = "endPadding"
-        const val GUTTER = "gutter"
-        const val CELL_SIZE = "cellSize"
-    }
-
-    private val _heightSpecs = mutableListOf<FolderSpec>()
-    val heightSpecs: List<FolderSpec>
-        get() = _heightSpecs
-
-    private val _widthSpecs = mutableListOf<FolderSpec>()
-    val widthSpecs: List<FolderSpec>
-        get() = _widthSpecs
-
-    // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
-    init {
-        var parser: XmlResourceParser? = null
-        try {
-            parser = resourceHelper.getXml()
-            val depth = parser.depth
-            var type: Int
-            while (
-                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                    parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
-            ) {
-                if (type == XmlPullParser.START_TAG && XmlTags.FOLDER_SPECS == parser.name) {
-                    val displayDepth = parser.depth
-                    while (
-                        (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                            parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
-                    ) {
-                        if (type == XmlPullParser.START_TAG && XmlTags.FOLDER_SPEC == parser.name) {
-                            val attrs =
-                                resourceHelper.obtainStyledAttributes(
-                                    Xml.asAttributeSet(parser),
-                                    R.styleable.FolderSpec
-                                )
-                            val maxAvailableSize =
-                                attrs.getDimensionPixelSize(
-                                    R.styleable.FolderSpec_maxAvailableSize,
-                                    0
-                                )
-                            val specType =
-                                SpecType.values()[
-                                        attrs.getInt(
-                                            R.styleable.FolderSpec_specType,
-                                            SpecType.HEIGHT.ordinal
-                                        )]
-                            attrs.recycle()
-
-                            var startPadding: SizeSpec? = null
-                            var endPadding: SizeSpec? = null
-                            var gutter: SizeSpec? = null
-                            var cellSize: SizeSpec? = null
-
-                            val limitDepth = parser.depth
-                            while (
-                                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                                    parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
-                            ) {
-                                val attr: AttributeSet = Xml.asAttributeSet(parser)
-                                if (type == XmlPullParser.START_TAG) {
-                                    val sizeSpec = SizeSpec.create(resourceHelper, attr)
-                                    when (parser.name) {
-                                        XmlTags.START_PADDING -> startPadding = sizeSpec
-                                        XmlTags.END_PADDING -> endPadding = sizeSpec
-                                        XmlTags.GUTTER -> gutter = sizeSpec
-                                        XmlTags.CELL_SIZE -> cellSize = sizeSpec
-                                    }
-                                }
-                            }
-
-                            checkNotNull(startPadding) {
-                                "Attr 'startPadding' in FolderSpec must be defined."
-                            }
-                            checkNotNull(endPadding) {
-                                "Attr 'endPadding' in FolderSpec must be defined."
-                            }
-                            checkNotNull(gutter) { "Attr 'gutter' in FolderSpec must be defined." }
-                            checkNotNull(cellSize) {
-                                "Attr 'cellSize' in FolderSpec must be defined."
-                            }
-
-                            val folderSpec =
-                                FolderSpec(
-                                    maxAvailableSize,
-                                    specType,
-                                    startPadding,
-                                    endPadding,
-                                    gutter,
-                                    cellSize
-                                )
-
-                            check(folderSpec.isValid()) { "Invalid FolderSpec found." }
-
-                            if (folderSpec.specType == SpecType.HEIGHT) {
-                                _heightSpecs += folderSpec
-                            } else {
-                                _widthSpecs += folderSpec
-                            }
-                        }
-                    }
-
-                    check(_widthSpecs.isNotEmpty() && _heightSpecs.isNotEmpty()) {
-                        "FolderSpecs is incomplete - " +
-                            "height list size = ${_heightSpecs.size}; " +
-                            "width list size = ${_widthSpecs.size}."
-                    }
-                }
-            }
-        } catch (e: Exception) {
-            when (e) {
-                is IOException,
-                is XmlPullParserException -> {
-                    throw RuntimeException("Failure parsing folder specs file.", e)
-                }
-                else -> throw e
-            }
-        } finally {
-            parser?.close()
-        }
-    }
-
-    /**
-     * Returns the [CalculatedFolderSpec] for width, based on the available width, FolderSpecs and
-     * WorkspaceSpecs.
-     */
-    fun getWidthSpec(
+    fun getCalculatedWidthSpec(
         columns: Int,
         availableWidth: Int,
-        workspaceSpec: CalculatedWorkspaceSpec
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedFolderSpec {
-        check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.WIDTH) {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.WIDTH) {
             "Invalid specType for CalculatedWorkspaceSpec. " +
-                "Expected: ${WorkspaceSpec.SpecType.WIDTH} - " +
-                "Found: ${workspaceSpec.workspaceSpec.specType}}"
+                "Expected: ${SpecType.WIDTH} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
         }
 
-        val widthSpec = _widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
-        check(widthSpec != null) { "No FolderSpec for width spec found with $availableWidth." }
-
-        return convertToCalculatedFolderSpec(widthSpec, availableWidth, columns, workspaceSpec)
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedFolderSpec(availableWidth, columns, spec, calculatedWorkspaceSpec)
     }
 
-    /**
-     * Returns the [CalculatedFolderSpec] for height, based on the available height, FolderSpecs and
-     * WorkspaceSpecs.
-     */
-    fun getHeightSpec(
+    fun getCalculatedHeightSpec(
         rows: Int,
         availableHeight: Int,
-        workspaceSpec: CalculatedWorkspaceSpec
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
     ): CalculatedFolderSpec {
-        check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) {
+        check(calculatedWorkspaceSpec.spec.specType == SpecType.HEIGHT) {
             "Invalid specType for CalculatedWorkspaceSpec. " +
-                "Expected: ${WorkspaceSpec.SpecType.HEIGHT} - " +
-                "Found: ${workspaceSpec.workspaceSpec.specType}}"
+                "Expected: ${SpecType.HEIGHT} - " +
+                "Found: ${calculatedWorkspaceSpec.spec.specType}}"
         }
 
-        val heightSpec = _heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
-        check(heightSpec != null) { "No FolderSpec for height spec found with $availableHeight." }
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedFolderSpec(availableHeight, rows, spec, calculatedWorkspaceSpec)
+    }
 
-        return convertToCalculatedFolderSpec(heightSpec, availableHeight, rows, workspaceSpec)
+    companion object {
+
+        private const val XML_FOLDER_SPEC = "folderSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): FolderSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_FOLDER_SPEC, ::FolderSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return FolderSpecs(widthSpecs, heightSpecs)
+        }
     }
 }
 
-data class CalculatedFolderSpec(
-    val availableSpace: Int,
-    val cells: Int,
-    val startPaddingPx: Int,
-    val endPaddingPx: Int,
-    val gutterPx: Int,
-    val cellSizePx: Int
-)
-
-/**
- * Responsive folder specs to be used to calculate the paddings, gutter and cell size for folders in
- * the workspace.
- *
- * @param maxAvailableSize indicates the breakpoint to use this specification.
- * @param specType indicates whether the paddings and gutters will be applied vertically or
- *   horizontally.
- * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
- * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
- * @param gutter the space between the cells vertically or horizontally depending on the [specType].
- * @param cellSize height or width of the cell depending on the [specType].
- */
 data class FolderSpec(
-    val maxAvailableSize: Int,
-    val specType: SpecType,
-    val startPadding: SizeSpec,
-    val endPadding: SizeSpec,
-    val gutter: SizeSpec,
-    val cellSize: SizeSpec
-) {
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
 
-    enum class SpecType {
-        HEIGHT,
-        WIDTH
+    init {
+        check(isValid()) { "Invalid FolderSpec found." }
     }
 
-    fun isValid(): Boolean {
-        if (maxAvailableSize <= 0) {
-            Log.e(LOG_TAG, "FolderSpec#isValid - maxAvailableSize <= 0")
-            return false
-        }
-
-        // All specs are valid
-        if (
-            !(startPadding.isValid() &&
-                endPadding.isValid() &&
-                gutter.isValid() &&
-                cellSize.isValid())
-        ) {
-            Log.e(LOG_TAG, "FolderSpec#isValid - !allSpecsAreValid()")
-            return false
-        }
-
-        return true
-    }
-}
-
-/** Helper function to convert [FolderSpec] to [CalculatedFolderSpec] */
-private fun convertToCalculatedFolderSpec(
-    folderSpec: FolderSpec,
-    availableSpace: Int,
-    cells: Int,
-    workspaceSpec: CalculatedWorkspaceSpec
-): CalculatedFolderSpec {
-    // Map if is fixedSize, ofAvailableSpace or matchWorkspace
-    var startPaddingPx =
-        folderSpec.startPadding.getCalculatedValue(availableSpace, workspaceSpec.startPaddingPx)
-    var endPaddingPx =
-        folderSpec.endPadding.getCalculatedValue(availableSpace, workspaceSpec.endPaddingPx)
-    var gutterPx = folderSpec.gutter.getCalculatedValue(availableSpace, workspaceSpec.gutterPx)
-    var cellSizePx =
-        folderSpec.cellSize.getCalculatedValue(availableSpace, workspaceSpec.cellSizePx)
-
-    // Remainder space
-    val gutters = cells - 1
-    val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
-    val remainderSpace = availableSpace - usedSpace
-
-    startPaddingPx = folderSpec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
-    endPaddingPx = folderSpec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
-    gutterPx = folderSpec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
-    cellSizePx = folderSpec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
-
-    return CalculatedFolderSpec(
-        availableSpace = availableSpace,
-        cells = cells,
-        startPaddingPx = startPaddingPx,
-        endPaddingPx = endPaddingPx,
-        gutterPx = gutterPx,
-        cellSizePx = cellSizePx
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
     )
 }
+
+class CalculatedFolderSpec(
+    availableSpace: Int,
+    cells: Int,
+    spec: FolderSpec,
+    calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+) : CalculatedResponsiveSpec(availableSpace, cells, spec, calculatedWorkspaceSpec)
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecs.kt b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
new file mode 100644
index 0000000..72a0ea4
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecs.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.responsive
+
+import android.util.Log
+
+/**
+ * Base class for responsive specs that holds a list of width and height specs.
+ *
+ * @param widthSpecs List of width responsive specifications
+ * @param heightSpecs List of height responsive specifications
+ */
+abstract class ResponsiveSpecs<T : ResponsiveSpec>(
+    val widthSpecs: List<T>,
+    val heightSpecs: List<T>
+) {
+
+    init {
+        check(widthSpecs.isNotEmpty() && heightSpecs.isNotEmpty()) {
+            "${this::class.simpleName} is incomplete - " +
+                "width list size = ${widthSpecs.size}; " +
+                "height list size = ${heightSpecs.size}."
+        }
+    }
+
+    /**
+     * Get a [ResponsiveSpec] for width within the breakpoint.
+     *
+     * @param availableWidth The width breakpoint for the spec
+     * @return A [ResponsiveSpec] for width.
+     */
+    fun getWidthSpec(availableWidth: Int): T {
+        val spec = widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize }
+        check(spec != null) { "No available width spec found within $availableWidth." }
+        return spec
+    }
+
+    /**
+     * Get a [ResponsiveSpec] for height within the breakpoint.
+     *
+     * @param availableHeight The height breakpoint for the spec
+     * @return A [ResponsiveSpec] for height.
+     */
+    fun getHeightSpec(availableHeight: Int): T {
+        val spec = heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
+        check(spec != null) { "No available height spec found within $availableHeight." }
+        return spec
+    }
+}
+
+/**
+ * Base class for a responsive specification that is used to calculate the paddings, gutter and cell
+ * size.
+ *
+ * @param maxAvailableSize indicates the breakpoint to use this specification.
+ * @param specType indicates whether the paddings and gutters will be applied vertically or
+ *   horizontally.
+ * @param startPadding padding used at the top or left (right in RTL) in the workspace folder.
+ * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder.
+ * @param gutter the space between the cells vertically or horizontally depending on the [specType].
+ * @param cellSize height or width of the cell depending on the [specType].
+ */
+abstract class ResponsiveSpec(
+    open val maxAvailableSize: Int,
+    open val specType: SpecType,
+    open val startPadding: SizeSpec,
+    open val endPadding: SizeSpec,
+    open val gutter: SizeSpec,
+    open val cellSize: SizeSpec
+) {
+    open fun isValid(): Boolean {
+        if (maxAvailableSize <= 0) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
+            return false
+        }
+
+        // All specs need to be individually valid
+        if (!allSpecsAreValid()) {
+            Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
+            return false
+        }
+
+        return true
+    }
+
+    private fun allSpecsAreValid(): Boolean {
+        return startPadding.isValid() &&
+            endPadding.isValid() &&
+            gutter.isValid() &&
+            cellSize.isValid()
+    }
+
+    enum class SpecType {
+        HEIGHT,
+        WIDTH
+    }
+
+    companion object {
+        private const val LOG_TAG = "ResponsiveSpec"
+    }
+}
+
+/**
+ * Calculated responsive specs contains the final paddings, gutter and cell size in pixels after
+ * they are calculated from the available space, cells and workspace specs.
+ */
+sealed class CalculatedResponsiveSpec {
+    var availableSpace: Int = 0
+        private set
+
+    var cells: Int = 0
+        private set
+
+    var startPaddingPx: Int = 0
+        private set
+
+    var endPaddingPx: Int = 0
+        private set
+
+    var gutterPx: Int = 0
+        private set
+
+    var cellSizePx: Int = 0
+        private set
+
+    var spec: ResponsiveSpec
+        private set
+
+    constructor(
+        availableSpace: Int,
+        cells: Int,
+        spec: ResponsiveSpec,
+        calculatedWorkspaceSpec: CalculatedWorkspaceSpec
+    ) {
+        this.availableSpace = availableSpace
+        this.cells = cells
+        this.spec = spec
+
+        // Map if is fixedSize, ofAvailableSpace or matchWorkspace
+        startPaddingPx =
+            spec.startPadding.getCalculatedValue(
+                availableSpace,
+                calculatedWorkspaceSpec.startPaddingPx
+            )
+        endPaddingPx =
+            spec.endPadding.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.endPaddingPx)
+        gutterPx = spec.gutter.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.gutterPx)
+        cellSizePx =
+            spec.cellSize.getCalculatedValue(availableSpace, calculatedWorkspaceSpec.cellSizePx)
+
+        updateRemainderSpaces(availableSpace, cells, spec)
+    }
+
+    constructor(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+        this.availableSpace = availableSpace
+        this.cells = cells
+        this.spec = spec
+
+        // Map if is fixedSize or ofAvailableSpace
+        startPaddingPx = spec.startPadding.getCalculatedValue(availableSpace)
+        endPaddingPx = spec.endPadding.getCalculatedValue(availableSpace)
+        gutterPx = spec.gutter.getCalculatedValue(availableSpace)
+        cellSizePx = spec.cellSize.getCalculatedValue(availableSpace)
+
+        updateRemainderSpaces(availableSpace, cells, spec)
+    }
+
+    private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
+        val gutters = cells - 1
+        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
+        val remainderSpace = availableSpace - usedSpace
+
+        startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
+        endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
+        gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
+        cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
+    }
+
+    override fun hashCode(): Int {
+        var result = availableSpace.hashCode()
+        result = 31 * result + cells.hashCode()
+        result = 31 * result + startPaddingPx.hashCode()
+        result = 31 * result + endPaddingPx.hashCode()
+        result = 31 * result + gutterPx.hashCode()
+        result = 31 * result + cellSizePx.hashCode()
+        result = 31 * result + spec.hashCode()
+        return result
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is CalculatedResponsiveSpec &&
+            availableSpace == other.availableSpace &&
+            cells == other.cells &&
+            startPaddingPx == other.startPaddingPx &&
+            endPaddingPx == other.endPaddingPx &&
+            gutterPx == other.gutterPx &&
+            cellSizePx == other.cellSizePx &&
+            spec == other.spec
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName}(" +
+            "availableSpace=$availableSpace, cells=$cells, startPaddingPx=$startPaddingPx, " +
+            "endPaddingPx=$endPaddingPx, gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
+            "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+            ")"
+    }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
new file mode 100644
index 0000000..a89b619
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecsParser.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.responsive
+
+import android.content.res.TypedArray
+import android.content.res.XmlResourceParser
+import android.util.Xml
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+import java.io.IOException
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+class ResponsiveSpecsParser(private val resourceHelper: ResourceHelper) {
+
+    private fun parseSizeSpecs(parser: XmlResourceParser): Map<String, SizeSpec> {
+        val parentName = parser.name
+        parser.next()
+
+        val result = mutableMapOf<String, SizeSpec>()
+        while (parser.eventType != XmlPullParser.END_DOCUMENT && parser.name != parentName) {
+            if (parser.eventType == XmlResourceParser.START_TAG) {
+                result[parser.name] = SizeSpec.create(resourceHelper, Xml.asAttributeSet(parser))
+            }
+            parser.next()
+        }
+
+        return result
+    }
+
+    fun <T> parseXML(
+        tagName: String,
+        map: (attributes: TypedArray, sizeSpecs: Map<String, SizeSpec>) -> T
+    ): List<T> {
+        val parser: XmlResourceParser = resourceHelper.getXml()
+
+        try {
+            val list = mutableListOf<T>()
+
+            var eventType = parser.eventType
+            while (eventType != XmlPullParser.END_DOCUMENT) {
+                if (eventType == XmlResourceParser.START_TAG && parser.name == tagName) {
+                    val attrs =
+                        resourceHelper.obtainStyledAttributes(
+                            Xml.asAttributeSet(parser),
+                            R.styleable.ResponsiveSpec
+                        )
+
+                    val sizeSpecs = parseSizeSpecs(parser)
+                    list += map(attrs, sizeSpecs)
+                    attrs.recycle()
+                }
+
+                eventType = parser.next()
+            }
+
+            parser.close()
+
+            return list
+        } catch (e: Exception) {
+            when (e) {
+                is NoSuchFieldException,
+                is IOException,
+                is XmlPullParserException ->
+                    throw RuntimeException("Failure parsing specs file.", e)
+                else -> throw e
+            }
+        } finally {
+            parser.close()
+        }
+    }
+}
+
+fun Map<String, SizeSpec>.getOrError(key: String): SizeSpec {
+    return this.getOrElse(key) { error("Attr '$key' must be defined.") }
+}
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index 3d618f9..d3868f0 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.responsive
 
 import android.content.res.TypedArray
@@ -26,7 +42,7 @@
 ) {
 
     /** Retrieves the correct value for [SizeSpec]. */
-    fun getCalculatedValue(availableSpace: Int, workspaceValue: Int): Int {
+    fun getCalculatedValue(availableSpace: Int, workspaceValue: Int = 0): Int {
         val calculatedValue =
             when {
                 fixedSize > 0 -> fixedSize.roundToInt()
@@ -91,6 +107,13 @@
         return true
     }
 
+    object XmlTags {
+        const val START_PADDING = "startPadding"
+        const val END_PADDING = "endPadding"
+        const val GUTTER = "gutter"
+        const val CELL_SIZE = "cellSize"
+    }
+
     companion object {
         private const val TAG = "SizeSpec"
 
diff --git a/src/com/android/launcher3/responsive/WorkspaceSpecs.kt b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
new file mode 100644
index 0000000..0da7026
--- /dev/null
+++ b/src/com/android/launcher3/responsive/WorkspaceSpecs.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.responsive
+
+import android.content.res.TypedArray
+import android.util.Log
+import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
+import com.android.launcher3.util.ResourceHelper
+
+private const val TAG = "WorkspaceSpecs"
+
+class WorkspaceSpecs(widthSpecs: List<WorkspaceSpec>, heightSpecs: List<WorkspaceSpec>) :
+    ResponsiveSpecs<WorkspaceSpec>(widthSpecs, heightSpecs) {
+
+    fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
+        val spec = getWidthSpec(availableWidth)
+        return CalculatedWorkspaceSpec(availableWidth, columns, spec)
+    }
+
+    fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
+        val spec = getHeightSpec(availableHeight)
+        return CalculatedWorkspaceSpec(availableHeight, rows, spec)
+    }
+
+    companion object {
+        private const val XML_WORKSPACE_SPEC = "workspaceSpec"
+
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): WorkspaceSpecs {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(XML_WORKSPACE_SPEC, ::WorkspaceSpec)
+            val (widthSpecs, heightSpecs) = specs.partition { it.specType == SpecType.WIDTH }
+            return WorkspaceSpecs(widthSpecs, heightSpecs)
+        }
+    }
+}
+
+data class WorkspaceSpec(
+    override val maxAvailableSize: Int,
+    override val specType: SpecType,
+    override val startPadding: SizeSpec,
+    override val endPadding: SizeSpec,
+    override val gutter: SizeSpec,
+    override val cellSize: SizeSpec
+) : ResponsiveSpec(maxAvailableSize, specType, startPadding, endPadding, gutter, cellSize) {
+
+    init {
+        check(isValid()) { "Invalid WorkspaceSpec found." }
+    }
+
+    constructor(
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        specType =
+            SpecType.values()[
+                    attrs.getInt(R.styleable.ResponsiveSpec_specType, SpecType.HEIGHT.ordinal)],
+        startPadding = specs.getOrError(SizeSpec.XmlTags.START_PADDING),
+        endPadding = specs.getOrError(SizeSpec.XmlTags.END_PADDING),
+        gutter = specs.getOrError(SizeSpec.XmlTags.GUTTER),
+        cellSize = specs.getOrError(SizeSpec.XmlTags.CELL_SIZE)
+    )
+
+    override fun isValid(): Boolean {
+        // Workspace spec should not match workspace
+        if (
+            startPadding.matchWorkspace ||
+                endPadding.matchWorkspace ||
+                gutter.matchWorkspace ||
+                cellSize.matchWorkspace
+        ) {
+            Log.e(TAG, "WorkspaceSpec#isValid - workspace shouldn't contain matchWorkspace!")
+            return false
+        }
+
+        return super.isValid()
+    }
+}
+
+class CalculatedWorkspaceSpec(availableSpace: Int, cells: Int, spec: WorkspaceSpec) :
+    CalculatedResponsiveSpec(availableSpace, cells, spec)
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
deleted file mode 100644
index 8cc0c59..0000000
--- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * 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.workspace
-
-import android.content.res.XmlResourceParser
-import android.util.AttributeSet
-import android.util.Log
-import android.util.Xml
-import com.android.launcher3.R
-import com.android.launcher3.responsive.SizeSpec
-import com.android.launcher3.util.ResourceHelper
-import java.io.IOException
-import kotlin.math.roundToInt
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-
-private const val TAG = "WorkspaceSpecs"
-
-class WorkspaceSpecs(resourceHelper: ResourceHelper) {
-    object XmlTags {
-        const val WORKSPACE_SPECS = "workspaceSpecs"
-
-        const val WORKSPACE_SPEC = "workspaceSpec"
-        const val START_PADDING = "startPadding"
-        const val END_PADDING = "endPadding"
-        const val GUTTER = "gutter"
-        const val CELL_SIZE = "cellSize"
-    }
-
-    val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>()
-    val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>()
-
-    // TODO(b/286538013) Remove this init after a more generic or reusable parser is created
-    init {
-        try {
-            val parser: XmlResourceParser = resourceHelper.getXml()
-            val depth = parser.depth
-            var type: Int
-            while (
-                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                    parser.depth > depth) && type != XmlPullParser.END_DOCUMENT
-            ) {
-                if (type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPECS == parser.name) {
-                    val displayDepth = parser.depth
-                    while (
-                        (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                            parser.depth > displayDepth) && type != XmlPullParser.END_DOCUMENT
-                    ) {
-                        if (
-                            type == XmlPullParser.START_TAG && XmlTags.WORKSPACE_SPEC == parser.name
-                        ) {
-                            val attrs =
-                                resourceHelper.obtainStyledAttributes(
-                                    Xml.asAttributeSet(parser),
-                                    R.styleable.WorkspaceSpec
-                                )
-                            val maxAvailableSize =
-                                attrs.getDimensionPixelSize(
-                                    R.styleable.WorkspaceSpec_maxAvailableSize,
-                                    0
-                                )
-                            val specType =
-                                WorkspaceSpec.SpecType.values()[
-                                        attrs.getInt(
-                                            R.styleable.WorkspaceSpec_specType,
-                                            WorkspaceSpec.SpecType.HEIGHT.ordinal
-                                        )]
-                            attrs.recycle()
-
-                            var startPadding: SizeSpec? = null
-                            var endPadding: SizeSpec? = null
-                            var gutter: SizeSpec? = null
-                            var cellSize: SizeSpec? = null
-
-                            val limitDepth = parser.depth
-                            while (
-                                (parser.next().also { type = it } != XmlPullParser.END_TAG ||
-                                    parser.depth > limitDepth) && type != XmlPullParser.END_DOCUMENT
-                            ) {
-                                val attr: AttributeSet = Xml.asAttributeSet(parser)
-                                if (type == XmlPullParser.START_TAG) {
-                                    when (parser.name) {
-                                        XmlTags.START_PADDING -> {
-                                            startPadding = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.END_PADDING -> {
-                                            endPadding = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.GUTTER -> {
-                                            gutter = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                        XmlTags.CELL_SIZE -> {
-                                            cellSize = SizeSpec.create(resourceHelper, attr)
-                                        }
-                                    }
-                                }
-                            }
-
-                            if (
-                                startPadding == null ||
-                                    endPadding == null ||
-                                    gutter == null ||
-                                    cellSize == null
-                            ) {
-                                throw IllegalStateException(
-                                    "All attributes in workspaceSpec must be defined"
-                                )
-                            }
-
-                            val workspaceSpec =
-                                WorkspaceSpec(
-                                    maxAvailableSize,
-                                    specType,
-                                    startPadding,
-                                    endPadding,
-                                    gutter,
-                                    cellSize
-                                )
-                            if (workspaceSpec.isValid()) {
-                                if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT)
-                                    workspaceHeightSpecList.add(workspaceSpec)
-                                else workspaceWidthSpecList.add(workspaceSpec)
-                            } else {
-                                throw IllegalStateException("Invalid workspaceSpec found.")
-                            }
-                        }
-                    }
-
-                    if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) {
-                        throw IllegalStateException(
-                            "WorkspaceSpecs is incomplete - " +
-                                "height list size = ${workspaceHeightSpecList.size}; " +
-                                "width list size = ${workspaceWidthSpecList.size}."
-                        )
-                    }
-                }
-            }
-            parser.close()
-        } catch (e: Exception) {
-            when (e) {
-                is IOException,
-                is XmlPullParserException -> {
-                    throw RuntimeException("Failure parsing workspaces specs file.", e)
-                }
-                else -> throw e
-            }
-        }
-    }
-
-    /**
-     * Returns the CalculatedWorkspaceSpec for width, based on the available width and the
-     * WorkspaceSpecs.
-     */
-    fun getCalculatedWidthSpec(columns: Int, availableWidth: Int): CalculatedWorkspaceSpec {
-        val widthSpec = workspaceWidthSpecList.first { availableWidth <= it.maxAvailableSize }
-
-        return CalculatedWorkspaceSpec(availableWidth, columns, widthSpec)
-    }
-
-    /**
-     * Returns the CalculatedWorkspaceSpec for height, based on the available height and the
-     * WorkspaceSpecs.
-     */
-    fun getCalculatedHeightSpec(rows: Int, availableHeight: Int): CalculatedWorkspaceSpec {
-        val heightSpec = workspaceHeightSpecList.first { availableHeight <= it.maxAvailableSize }
-
-        return CalculatedWorkspaceSpec(availableHeight, rows, heightSpec)
-    }
-}
-
-class CalculatedWorkspaceSpec(
-    val availableSpace: Int,
-    val cells: Int,
-    val workspaceSpec: WorkspaceSpec
-) {
-    var startPaddingPx: Int = 0
-        private set
-    var endPaddingPx: Int = 0
-        private set
-    var gutterPx: Int = 0
-        private set
-    var cellSizePx: Int = 0
-        private set
-    init {
-        // Calculate all fixed size first
-        if (workspaceSpec.startPadding.fixedSize > 0)
-            startPaddingPx = workspaceSpec.startPadding.fixedSize.roundToInt()
-        if (workspaceSpec.endPadding.fixedSize > 0)
-            endPaddingPx = workspaceSpec.endPadding.fixedSize.roundToInt()
-        if (workspaceSpec.gutter.fixedSize > 0)
-            gutterPx = workspaceSpec.gutter.fixedSize.roundToInt()
-        if (workspaceSpec.cellSize.fixedSize > 0)
-            cellSizePx = workspaceSpec.cellSize.fixedSize.roundToInt()
-
-        // Calculate all available space next
-        if (workspaceSpec.startPadding.ofAvailableSpace > 0)
-            startPaddingPx =
-                (workspaceSpec.startPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.endPadding.ofAvailableSpace > 0)
-            endPaddingPx = (workspaceSpec.endPadding.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.gutter.ofAvailableSpace > 0)
-            gutterPx = (workspaceSpec.gutter.ofAvailableSpace * availableSpace).roundToInt()
-        if (workspaceSpec.cellSize.ofAvailableSpace > 0)
-            cellSizePx = (workspaceSpec.cellSize.ofAvailableSpace * availableSpace).roundToInt()
-
-        // Calculate remainder space last
-        val gutters = cells - 1
-        val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
-        val remainderSpace = availableSpace - usedSpace
-        if (workspaceSpec.startPadding.ofRemainderSpace > 0)
-            startPaddingPx =
-                (workspaceSpec.startPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.endPadding.ofRemainderSpace > 0)
-            endPaddingPx = (workspaceSpec.endPadding.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.gutter.ofRemainderSpace > 0)
-            gutterPx = (workspaceSpec.gutter.ofRemainderSpace * remainderSpace).roundToInt()
-        if (workspaceSpec.cellSize.ofRemainderSpace > 0)
-            cellSizePx = (workspaceSpec.cellSize.ofRemainderSpace * remainderSpace).roundToInt()
-    }
-
-    override fun toString(): String {
-        return "CalculatedWorkspaceSpec(availableSpace=$availableSpace, " +
-            "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
-            "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
-            "workspaceSpec.maxAvailableSize=${workspaceSpec.maxAvailableSize})"
-    }
-}
-
-data class WorkspaceSpec(
-    val maxAvailableSize: Int,
-    val specType: SpecType,
-    val startPadding: SizeSpec,
-    val endPadding: SizeSpec,
-    val gutter: SizeSpec,
-    val cellSize: SizeSpec
-) {
-
-    enum class SpecType {
-        HEIGHT,
-        WIDTH
-    }
-
-    fun isValid(): Boolean {
-        if (maxAvailableSize <= 0) {
-            Log.e(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0")
-            return false
-        }
-
-        // All specs need to be individually valid
-        if (!allSpecsAreValid()) {
-            Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()")
-            return false
-        }
-
-        return true
-    }
-
-    private fun allSpecsAreValid(): Boolean =
-        startPadding.isValid() &&
-            endPadding.isValid() &&
-            gutter.isValid() &&
-            cellSize.isValid() &&
-            !startPadding.matchWorkspace &&
-            !endPadding.matchWorkspace &&
-            !gutter.matchWorkspace &&
-            !cellSize.matchWorkspace
-}
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index 0d586c2..e5ee064 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -18,7 +18,7 @@
 <!-- Attributes have to be copied to test for correct parsing of files -->
 <resources>
     <!--  Responsive grids attributes  -->
-    <declare-styleable name="WorkspaceSpec">
+    <declare-styleable name="ResponsiveSpec">
         <attr name="specType" format="integer">
             <enum name="height" value="0" />
             <enum name="width" value="1" />
@@ -26,12 +26,9 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SizeSpec">
-        <attr name="fixedSize" format="dimension" />
-        <attr name="ofAvailableSpace" format="float" />
-        <attr name="ofRemainderSpace" format="float" />
-        <attr name="matchWorkspace" format="boolean" />
-        <attr name="maxSize" format="dimension" />
+    <declare-styleable name="WorkspaceSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
     </declare-styleable>
 
     <declare-styleable name="FolderSpec">
@@ -43,4 +40,12 @@
         <attr name="specType" />
         <attr name="maxAvailableSize" />
     </declare-styleable>
+
+    <declare-styleable name="SizeSpec">
+        <attr name="fixedSize" format="dimension" />
+        <attr name="ofAvailableSpace" format="float" />
+        <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
+    </declare-styleable>
 </resources>
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 4073517..1e2447a 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -157,6 +157,7 @@
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
     public static final String FLAKY_ACTIVITY_COUNT = "b/260260325";
     public static final String ICON_MISSING = "b/282963545";
+    public static final String LAUNCH_SPLIT_PAIR = "b/288939273";
 
     public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
diff --git a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
index 77ea5ba..cd95e99 100644
--- a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
@@ -41,9 +41,9 @@
     @Test
     fun parseValidFile() {
         val allAppsSpecs =
-            AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
-        assertThat(allAppsSpecs.allAppsHeightSpecList.size).isEqualTo(1)
-        assertThat(allAppsSpecs.allAppsHeightSpecList[0].toString())
+            AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+        assertThat(allAppsSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(allAppsSpecs.heightSpecs[0].toString())
             .isEqualTo(
                 "AllAppsSpec(" +
                     "maxAvailableSize=26247, " +
@@ -71,8 +71,8 @@
                     ")"
             )
 
-        assertThat(allAppsSpecs.allAppsWidthSpecList.size).isEqualTo(1)
-        assertThat(allAppsSpecs.allAppsWidthSpecList[0].toString())
+        assertThat(allAppsSpecs.widthSpecs.size).isEqualTo(1)
+        assertThat(allAppsSpecs.widthSpecs[0].toString())
             .isEqualTo(
                 "AllAppsSpec(" +
                     "maxAvailableSize=26247, " +
@@ -103,16 +103,16 @@
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
-        AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_1))
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_1))
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
-        AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_2))
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_2))
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
-        AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_3))
+        AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_all_apps_file_case_3))
     }
 }
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
index 9f981fa..0f12e58 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
@@ -23,7 +23,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.tests.R as TestR
 import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.WorkspaceSpecs
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,12 +48,12 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
         val allAppsSpecs =
-            AllAppsSpecs(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
+            AllAppsSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_all_apps_file))
 
         with(allAppsSpecs.getCalculatedWidthSpec(4, availableWidth, widthSpec)) {
             assertThat(availableSpace).isEqualTo(availableWidth)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
index c14722c..863cf76 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
@@ -24,7 +24,6 @@
 import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.tests.R
 import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.WorkspaceSpecs
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -48,11 +47,11 @@
 
         // Loading workspace specs
         val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
-        val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+        val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
 
         // Loading folders specs
         val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelperFolder)
+        val folderSpecs = FolderSpecs.create(resourceHelperFolder)
 
         assertThat(folderSpecs.widthSpecs.size).isEqualTo(2)
         assertThat(folderSpecs.widthSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
@@ -62,7 +61,7 @@
         var availableWidth = deviceSpec.naturalSize.first
         var calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
         var calculatedWidthFolderSpec =
-            folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+            folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
         with(calculatedWidthFolderSpec) {
             assertThat(availableSpace).isEqualTo(availableWidth)
             assertThat(cells).isEqualTo(columns)
@@ -76,7 +75,7 @@
         availableWidth = 2000.dpToPx()
         calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
         calculatedWidthFolderSpec =
-            folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+            folderSpecs.getCalculatedWidthSpec(columns, availableWidth, calculatedWorkspace)
         with(calculatedWidthFolderSpec) {
             assertThat(availableSpace).isEqualTo(availableWidth)
             assertThat(cells).isEqualTo(columns)
@@ -97,11 +96,11 @@
 
         // Loading workspace specs
         val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
-        val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+        val workspaceSpecs = WorkspaceSpecs.create(resourceHelperWorkspace)
 
         // Loading folders specs
         val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelperFolder)
+        val folderSpecs = FolderSpecs.create(resourceHelperFolder)
 
         assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
         assertThat(folderSpecs.heightSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
@@ -109,7 +108,7 @@
         // Validate height spec
         val calculatedWorkspace = workspaceSpecs.getCalculatedHeightSpec(rows, availableHeight)
         val calculatedFolderSpec =
-            folderSpecs.getHeightSpec(rows, availableHeight, calculatedWorkspace)
+            folderSpecs.getCalculatedHeightSpec(rows, availableHeight, calculatedWorkspace)
         with(calculatedFolderSpec) {
             assertThat(availableSpace).isEqualTo(availableHeight)
             assertThat(cells).isEqualTo(rows)
diff --git a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
rename to tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
index 7f03ba2..0af694e 100644
--- a/tests/src/com/android/launcher3/workspace/CalculatedWorkspaceSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.workspace
+package com.android.launcher3.responsive
 
 import android.content.Context
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -49,7 +49,7 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
@@ -86,7 +86,7 @@
         val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 640
 
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
         val widthSpec = workspaceSpecs.getCalculatedWidthSpec(4, availableWidth)
         val heightSpec = workspaceSpecs.getCalculatedHeightSpec(5, availableHeight)
 
diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
index 796bf9a..e21af57 100644
--- a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
@@ -21,11 +21,10 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.responsive.ResponsiveSpec.SpecType
 import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.tests.R
 import com.android.launcher3.util.TestResourceHelper
-import com.android.launcher3.workspace.CalculatedWorkspaceSpec
-import com.android.launcher3.workspace.WorkspaceSpec
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -44,14 +43,14 @@
     @Test
     fun parseValidFile() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
 
         val sizeSpec16 = SizeSpec(16f.dpToPx())
         val widthSpecsExpected =
             listOf(
                 FolderSpec(
                     maxAvailableSize = 800.dpToPx(),
-                    specType = FolderSpec.SpecType.WIDTH,
+                    specType = SpecType.WIDTH,
                     startPadding = sizeSpec16,
                     endPadding = sizeSpec16,
                     gutter = sizeSpec16,
@@ -59,7 +58,7 @@
                 ),
                 FolderSpec(
                     maxAvailableSize = 9999.dpToPx(),
-                    specType = FolderSpec.SpecType.WIDTH,
+                    specType = SpecType.WIDTH,
                     startPadding = sizeSpec16,
                     endPadding = sizeSpec16,
                     gutter = sizeSpec16,
@@ -70,7 +69,7 @@
         val heightSpecsExpected =
             FolderSpec(
                 maxAvailableSize = 9999.dpToPx(),
-                specType = FolderSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(24f.dpToPx()),
                 endPadding = SizeSpec(64f.dpToPx()),
                 gutter = sizeSpec16,
@@ -88,25 +87,25 @@
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_1)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_2)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_3)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingSpecs_throwsError() {
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_4)
-        FolderSpecs(resourceHelper)
+        FolderSpecs.create(resourceHelper)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -117,7 +116,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.WIDTH,
+                specType = SpecType.WIDTH,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -126,8 +125,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -138,7 +137,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -147,8 +146,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     @Test
@@ -159,7 +158,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.WIDTH,
+                specType = SpecType.WIDTH,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -167,21 +166,17 @@
             )
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
-        val expectedResult =
-            CalculatedFolderSpec(
-                startPaddingPx = 16.dpToPx(),
-                endPaddingPx = 16.dpToPx(),
-                gutterPx = 16.dpToPx(),
-                cellSizePx = calculatedWorkspaceSpec.cellSizePx,
-                availableSpace = availableSpace,
-                cells = cells
-            )
-
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
         val calculatedWidthSpec =
-            folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
-        assertThat(calculatedWidthSpec).isEqualTo(expectedResult)
+            folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+        assertThat(calculatedWidthSpec.cells).isEqualTo(cells)
+        assertThat(calculatedWidthSpec.availableSpace).isEqualTo(availableSpace)
+        assertThat(calculatedWidthSpec.startPaddingPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.endPaddingPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.gutterPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedWidthSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -192,7 +187,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -201,8 +196,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     @Test
@@ -213,7 +208,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.HEIGHT,
+                specType = SpecType.HEIGHT,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -221,21 +216,17 @@
             )
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
-        val expectedResult =
-            CalculatedFolderSpec(
-                startPaddingPx = 24.dpToPx(),
-                endPaddingPx = 64.dpToPx(),
-                gutterPx = 16.dpToPx(),
-                cellSizePx = calculatedWorkspaceSpec.cellSizePx,
-                availableSpace = availableSpace,
-                cells = cells
-            )
-
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
         val calculatedHeightSpec =
-            folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
-        assertThat(calculatedHeightSpec).isEqualTo(expectedResult)
+            folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+
+        assertThat(calculatedHeightSpec.cells).isEqualTo(cells)
+        assertThat(calculatedHeightSpec.availableSpace).isEqualTo(availableSpace)
+        assertThat(calculatedHeightSpec.startPaddingPx).isEqualTo(24.dpToPx())
+        assertThat(calculatedHeightSpec.endPaddingPx).isEqualTo(64.dpToPx())
+        assertThat(calculatedHeightSpec.gutterPx).isEqualTo(16.dpToPx())
+        assertThat(calculatedHeightSpec.cellSizePx).isEqualTo(calculatedWorkspaceSpec.cellSizePx)
     }
 
     @Test(expected = IllegalStateException::class)
@@ -246,7 +237,7 @@
         val workspaceSpec =
             WorkspaceSpec(
                 maxAvailableSize = availableSpace,
-                specType = WorkspaceSpec.SpecType.WIDTH,
+                specType = SpecType.WIDTH,
                 startPadding = SizeSpec(fixedSize = 10f),
                 endPadding = SizeSpec(fixedSize = 10f),
                 gutter = SizeSpec(fixedSize = 10f),
@@ -255,8 +246,8 @@
         val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
 
         val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
-        val folderSpecs = FolderSpecs(resourceHelper)
-        folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        val folderSpecs = FolderSpecs.create(resourceHelper)
+        folderSpecs.getCalculatedHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
     }
 
     private fun Float.dpToPx(): Float {
diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
similarity index 86%
rename from tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
rename to tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
index 8b99a3a..0364069 100644
--- a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.workspace
+package com.android.launcher3.responsive
 
 import android.content.Context
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -41,9 +41,9 @@
     @Test
     fun parseValidFile() {
         val workspaceSpecs =
-            WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
-        assertThat(workspaceSpecs.workspaceHeightSpecList.size).isEqualTo(3)
-        assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString())
+            WorkspaceSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+        assertThat(workspaceSpecs.heightSpecs.size).isEqualTo(3)
+        assertThat(workspaceSpecs.heightSpecs[0].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=1533, " +
@@ -70,7 +70,7 @@
                     "maxSize=2147483647)" +
                     ")"
             )
-        assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
+        assertThat(workspaceSpecs.heightSpecs[1].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=1607, " +
@@ -97,7 +97,7 @@
                     "maxSize=2147483647)" +
                     ")"
             )
-        assertThat(workspaceSpecs.workspaceHeightSpecList[2].toString())
+        assertThat(workspaceSpecs.heightSpecs[2].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=26247, " +
@@ -124,8 +124,8 @@
                     "maxSize=2147483647)" +
                     ")"
             )
-        assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
-        assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString())
+        assertThat(workspaceSpecs.widthSpecs.size).isEqualTo(1)
+        assertThat(workspaceSpecs.widthSpecs[0].toString())
             .isEqualTo(
                 "WorkspaceSpec(" +
                     "maxAvailableSize=26247, " +
@@ -156,21 +156,29 @@
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_missingTag_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1))
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1)
+        )
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2))
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2)
+        )
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3))
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3)
+        )
     }
 
     @Test(expected = IllegalStateException::class)
     fun parseInvalidFile_matchWorkspace_true_throwsError() {
-        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4))
+        WorkspaceSpecs.create(
+            TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4)
+        )
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
index 7237387..5b9adcd 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -85,7 +85,7 @@
         waitForStateTransitionToEnd("Launcher internal state didn't switch to Normal",
                 () -> NORMAL);
         waitForResumed("Launcher internal state is still Background");
-        executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        mLauncher.getWorkspace().switchToAllApps();
         waitForStateTransitionToEnd("Launcher internal state didn't switch to All Apps",
                 () -> ALL_APPS);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index a59eff7..9b4d273 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -37,6 +37,7 @@
 import androidx.test.uiautomator.Condition;
 import androidx.test.uiautomator.UiDevice;
 
+import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.testing.shared.TestProtocol;
 
 /**
@@ -50,6 +51,8 @@
     // UNSTASHED_TASKBAR_HANDLE_HINT_SCALE value from TaskbarStashController.
     private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
 
+    private static final int STASHED_TASKBAR_BOTTOM_EDGE_DP = 1;
+
     private final Condition<UiDevice, Boolean> mStashedTaskbarHintScaleCondition =
             device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
                     TestProtocol.TEST_INFO_RESPONSE_FIELD) - UNSTASHED_TASKBAR_HANDLE_HINT_SCALE
@@ -209,7 +212,7 @@
      *
      * <p>This unstashing occurs when not actively hovering the taskbar.
      */
-    public void hoverScreenBottomEdgeToUnstashTaskbar() {
+    public Taskbar hoverScreenBottomEdgeToUnstashTaskbar() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "cursor hover entering screen edge to unstash taskbar")) {
@@ -226,13 +229,15 @@
 
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
                     new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+
+            return new Taskbar(mLauncher);
         }
     }
 
     /**
      * Emulate the cursor hovering the taskbar to get unstash hint, then hovering below to unstash.
      */
-    public void hoverBelowHintedTaskbarToUnstash() {
+    public Taskbar hoverBelowHintedTaskbarToUnstash() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "cursor hover entering stashed taskbar")) {
@@ -254,6 +259,7 @@
                         new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
 
                 mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+                return new Taskbar(mLauncher);
             }
         }
     }
@@ -288,4 +294,45 @@
             }
         }
     }
+
+    /**
+     * Emulate the cursor clicking the stashed taskbar to go home.
+     */
+    public Workspace clickStashedTaskbarToGoHome() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering stashed taskbar")) {
+            long downTime = SystemClock.uptimeMillis();
+            int stashedTaskbarBottomEdge = ResourceUtils.pxFromDp(STASHED_TASKBAR_BOTTOM_EDGE_DP,
+                    mLauncher.getResources().getDisplayMetrics());
+            Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                    mLauncher.getRealDisplaySize().y - stashedTaskbarBottomEdge - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+
+            mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+                    LauncherInstrumentation.WAIT_TIME_MS);
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "cursor clicking stashed taskbar to go home")) {
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_PRESS,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_BUTTON_RELEASE,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        null);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP,
+                        new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y),
+                        LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER);
+
+                return mLauncher.getWorkspace();
+            }
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 89f141f..3395bbf 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -110,6 +110,9 @@
     static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPatternTIS("ACTION_CANCEL");
     static final Pattern EVENT_HOVER_ENTER_TIS = getTouchEventPatternTIS("ACTION_HOVER_ENTER");
     static final Pattern EVENT_HOVER_EXIT_TIS = getTouchEventPatternTIS("ACTION_HOVER_EXIT");
+    static final Pattern EVENT_BUTTON_PRESS_TIS = getTouchEventPatternTIS("ACTION_BUTTON_PRESS");
+    static final Pattern EVENT_BUTTON_RELEASE_TIS =
+            getTouchEventPatternTIS("ACTION_BUTTON_RELEASE");
 
     private static final Pattern EVENT_KEY_BACK_DOWN =
             getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
@@ -173,7 +176,7 @@
         void close();
     }
 
-    private static final String WORKSPACE_RES_ID = "workspace";
+    static final String WORKSPACE_RES_ID = "workspace";
     private static final String APPS_RES_ID = "apps_view";
     private static final String OVERVIEW_RES_ID = "overview_panel";
     private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
@@ -1834,6 +1837,12 @@
                 }
                 mPointerCount--;
                 break;
+            case MotionEvent.ACTION_BUTTON_PRESS:
+                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_BUTTON_PRESS_TIS);
+                break;
+            case MotionEvent.ACTION_BUTTON_RELEASE:
+                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_BUTTON_RELEASE_TIS);
+                break;
         }
 
         final MotionEvent event = isTrackpadGesture
@@ -1841,6 +1850,10 @@
                         downTime, currentTime, action, point.x, point.y, pointerCount,
                         mTrackpadGestureType)
                 : getMotionEvent(downTime, currentTime, action, point.x, point.y);
+        if (action == MotionEvent.ACTION_BUTTON_PRESS
+                || action == MotionEvent.ACTION_BUTTON_RELEASE) {
+            event.setActionButton(MotionEvent.BUTTON_PRIMARY);
+        }
         assertTrue("injectInputEvent failed",
                 mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
         event.recycle();