Extract cell size information to responsive grid structure

Bug: 287975993
Flag: ACONFIG com.android.launcher3.enable_responsive_workspace TEAMFOOD
Test: ResponsiveCellSpecsProviderTest
Test: DeviceProfileDumpTest
Test: DeviceProfileResponsiveDumpTest
Change-Id: I26a87d9b690fdfcff1599d862c09e97fe9f9f930
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 3682830..20e7089 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -220,6 +220,14 @@
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="hotseatSpecsId" format="reference" />
         <attr name="hotseatSpecsTwoPanelId" format="reference" />
+        <!-- File that contains the specs for workspace icon and text size.
+        Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+        <attr name="workspaceCellSpecsId" format="reference" />
+        <attr name="workspaceCellSpecsTwoPanelId" format="reference" />
+        <!-- File that contains the specs for all apps icon and text size.
+        Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
+        <attr name="allAppsCellSpecsId" format="reference" />
+        <attr name="allAppsCellSpecsTwoPanelId" format="reference" />
 
         <!-- By default all categories are enabled -->
         <attr name="deviceCategory" format="integer">
@@ -292,6 +300,11 @@
         <attr name="maxAvailableSize" />
     </declare-styleable>
 
+    <declare-styleable name="CellSpec">
+        <attr name="dimensionType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
     <declare-styleable name="SizeSpec">
         <attr name="fixedSize" format="dimension" />
         <attr name="ofAvailableSpace" format="float" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 4b15a6b..154312a 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -228,6 +228,10 @@
     <dimen name="iconSize60dp">66dp</dimen>
     <dimen name="iconSize66dp">72dp</dimen>
     <dimen name="iconSize72dp">79dp</dimen>
+    <dimen name="iconSize82dp">90dp</dimen>
+    <dimen name="iconSize110dp">121dp</dimen>
+    <dimen name="iconSize144dp">158dp</dimen>
+
 
     <!--  Icon size steps in dp  -->
     <integer-array name="icon_size_steps">
@@ -240,6 +244,9 @@
         <item>@dimen/iconSize60dp</item>
         <item>@dimen/iconSize66dp</item>
         <item>@dimen/iconSize72dp</item>
+        <item>@dimen/iconSize82dp</item>
+        <item>@dimen/iconSize110dp</item>
+        <item>@dimen/iconSize144dp</item>
     </integer-array>
 
     <dimen name="minimum_icon_label_size">8sp</dimen>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 8a63477..7ca8b82 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -54,9 +54,11 @@
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.responsive.CalculatedCellSpec;
 import com.android.launcher3.responsive.CalculatedHotseatSpec;
 import com.android.launcher3.responsive.CalculatedResponsiveSpec;
 import com.android.launcher3.responsive.HotseatSpecsProvider;
+import com.android.launcher3.responsive.ResponsiveCellSpecsProvider;
 import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType;
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType;
 import com.android.launcher3.responsive.ResponsiveSpecsProvider;
@@ -125,6 +127,8 @@
     private CalculatedResponsiveSpec mResponsiveFolderWidthSpec;
     private CalculatedResponsiveSpec mResponsiveFolderHeightSpec;
     private CalculatedHotseatSpec mResponsiveHotseatSpec;
+    private CalculatedCellSpec mResponsiveWorkspaceCellSpec;
+    private CalculatedCellSpec mResponsiveAllAppsCellSpec;
 
     /**
      * The maximum amount of left/right workspace padding as a percentage of the screen width.
@@ -165,7 +169,7 @@
     public int iconSizePx;
     public int iconTextSizePx;
     public int iconDrawablePaddingPx;
-    private final int mIconDrawablePaddingOriginalPx;
+    private int mIconDrawablePaddingOriginalPx;
     public boolean iconCenterVertically;
 
     public float cellScaleToFit;
@@ -178,7 +182,7 @@
     // Folder
     public final int numFolderRows;
     public final int numFolderColumns;
-    public float folderLabelTextScale;
+    public final float folderLabelTextScale;
     public int folderLabelTextSizePx;
     public int folderFooterHeightPx;
     public int folderIconSizePx;
@@ -330,7 +334,9 @@
         mIsResponsiveGrid = inv.workspaceSpecsId != INVALID_RESOURCE_HANDLE
                 && inv.allAppsSpecsId != INVALID_RESOURCE_HANDLE
                 && inv.folderSpecsId != INVALID_RESOURCE_HANDLE
-                && inv.hotseatSpecsId != INVALID_RESOURCE_HANDLE;
+                && inv.hotseatSpecsId != INVALID_RESOURCE_HANDLE
+                && inv.workspaceCellSpecsId != INVALID_RESOURCE_HANDLE
+                && inv.allAppsCellSpecsId != INVALID_RESOURCE_HANDLE;
 
         mIsScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
         // Determine device posture.
@@ -470,17 +476,19 @@
         mWorkspacePageIndicatorOverlapWorkspace =
                 res.getDimensionPixelSize(R.dimen.workspace_page_indicator_overlap_workspace);
 
-        TypedArray cellStyle;
-        if (inv.cellStyle != INVALID_RESOURCE_HANDLE) {
-            cellStyle = context.obtainStyledAttributes(inv.cellStyle,
-                    R.styleable.CellStyle);
-        } else {
-            cellStyle = context.obtainStyledAttributes(R.style.CellStyleDefault,
-                    R.styleable.CellStyle);
+        if (!mIsResponsiveGrid) {
+            TypedArray cellStyle;
+            if (inv.cellStyle != INVALID_RESOURCE_HANDLE) {
+                cellStyle = context.obtainStyledAttributes(inv.cellStyle,
+                        R.styleable.CellStyle);
+            } else {
+                cellStyle = context.obtainStyledAttributes(R.style.CellStyleDefault,
+                        R.styleable.CellStyle);
+            }
+            mIconDrawablePaddingOriginalPx = cellStyle.getDimensionPixelSize(
+                    R.styleable.CellStyle_iconDrawablePadding, 0);
+            cellStyle.recycle();
         }
-        mIconDrawablePaddingOriginalPx = cellStyle.getDimensionPixelSize(
-                R.styleable.CellStyle_iconDrawablePadding, 0);
-        cellStyle.recycle();
 
         dropTargetBarSizePx = res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size);
         dropTargetBarTopMarginPx = res.getDimensionPixelSize(R.dimen.drop_target_top_margin);
@@ -537,6 +545,13 @@
             mHotseatBarEdgePaddingPx =
                     isVerticalBarLayout() ? mResponsiveHotseatSpec.getEdgePadding() : 0;
             mHotseatBarWorkspaceSpacePx = 0;
+
+            ResponsiveCellSpecsProvider workspaceCellSpecs = ResponsiveCellSpecsProvider.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.workspaceCellSpecsTwoPanelId
+                                    : inv.workspaceCellSpecsId));
+            mResponsiveWorkspaceCellSpec = workspaceCellSpecs.getCalculatedSpec(
+                    responsiveAspectRatio, heightPx);
         } else {
             hotseatQsbSpace = pxFromDp(inv.hotseatQsbSpace[mTypeIndex], mMetrics);
             hotseatBarBottomSpace = pxFromDp(inv.hotseatBarBottomSpace[mTypeIndex], mMetrics);
@@ -570,7 +585,13 @@
 
         springLoadedHotseatBarTopMarginPx = res.getDimensionPixelSize(
                 R.dimen.spring_loaded_hotseat_top_margin);
-        updateHotseatSizes(pxFromDp(inv.iconSize[mTypeIndex], mMetrics));
+
+        if (mIsResponsiveGrid) {
+            updateHotseatSizes(mResponsiveWorkspaceCellSpec.getIconSize());
+        } else {
+            updateHotseatSizes(pxFromDp(inv.iconSize[mTypeIndex], mMetrics));
+        }
+
         if (areNavButtonsInline && !isPhone) {
             inlineNavButtonsEndSpacingPx =
                     res.getDimensionPixelSize(inv.inlineNavButtonsEndSpacing);
@@ -633,6 +654,15 @@
                     DimensionType.HEIGHT, numFolderRows,
                     mResponsiveWorkspaceHeightSpec.getAvailableSpace(),
                     mResponsiveWorkspaceHeightSpec);
+
+            ResponsiveCellSpecsProvider allAppsCellSpecs = ResponsiveCellSpecsProvider.create(
+                    new ResourceHelper(context,
+                            isTwoPanels ? inv.allAppsCellSpecsTwoPanelId
+                                    : inv.allAppsCellSpecsId));
+            mResponsiveAllAppsCellSpec = allAppsCellSpecs.getCalculatedSpec(
+                    responsiveAspectRatio,
+                    mResponsiveAllAppsHeightSpec.getAvailableSpace(),
+                    mResponsiveWorkspaceCellSpec);
         }
 
         desiredWorkspaceHorizontalMarginPx = getHorizontalMarginPx(inv, res);
@@ -969,19 +999,22 @@
      * Returns the amount of extra (or unused) vertical space.
      */
     private int updateAvailableDimensions(Resources res) {
+        iconCenterVertically = mIsScalableGrid || mIsResponsiveGrid;
+
+        if (mIsResponsiveGrid) {
+            updateIconSize(1f, res);
+            updateWorkspacePadding();
+            return 0;
+        }
+
         float invIconSizeDp = inv.iconSize[mTypeIndex];
         float invIconTextSizeSp = inv.iconTextSize[mTypeIndex];
         iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics));
         iconTextSizePx = pxFromSp(invIconTextSizeSp, mMetrics);
-        iconCenterVertically = mIsScalableGrid || mIsResponsiveGrid;
 
         updateIconSize(1f, res);
         updateWorkspacePadding();
 
-        if (mIsResponsiveGrid) {
-            return 0;
-        }
-
         // Check to see if the icons fit within the available height.
         float usedHeight = getCellLayoutHeightSpecification();
         final int maxHeight = getCellLayoutHeight();
@@ -1025,7 +1058,7 @@
         // TODO(b/235886078): workaround needed because of this bug
         // Icons are 10% larger on XML than their visual size,
         // so remove that extra space to get labels closer to the correct padding
-        int iconVisibleSizePx = (int) Math.round(ICON_VISIBLE_AREA_FACTOR * iconSizePx);
+        int iconVisibleSizePx = Math.round(ICON_VISIBLE_AREA_FACTOR * iconSizePx);
         return Math.max(0, iconDrawablePadding - ((iconSizePx - iconVisibleSizePx) / 2));
     }
 
@@ -1063,6 +1096,10 @@
         cellLayoutBorderSpacePx = getCellLayoutBorderSpace(inv, scale);
 
         if (mIsResponsiveGrid) {
+            iconSizePx = mResponsiveWorkspaceCellSpec.getIconSize();
+            iconTextSizePx = mResponsiveWorkspaceCellSpec.getIconTextSize();
+            mIconDrawablePaddingOriginalPx = mResponsiveWorkspaceCellSpec.getIconDrawablePadding();
+
             cellWidthPx = mResponsiveWorkspaceWidthSpec.getCellSizePx();
             cellHeightPx = mResponsiveWorkspaceHeightSpec.getCellSizePx();
 
@@ -1168,7 +1205,7 @@
 
         // All apps
         if (mIsResponsiveGrid) {
-            updateAllAppsWithResponsiveMeasures(res);
+            updateAllAppsWithResponsiveMeasures();
         } else {
             updateAllAppsIconSize(scale, res);
         }
@@ -1267,13 +1304,16 @@
         }
     }
 
-    private void updateAllAppsWithResponsiveMeasures(Resources res) {
+    private void updateAllAppsWithResponsiveMeasures() {
+        allAppsIconSizePx = mResponsiveAllAppsCellSpec.getIconSize();
+        allAppsIconTextSizePx = mResponsiveAllAppsCellSpec.getIconTextSize();
+        allAppsIconDrawablePaddingPx = getNormalizedIconDrawablePadding(allAppsIconSizePx,
+                mResponsiveAllAppsCellSpec.getIconDrawablePadding());
         allAppsBorderSpacePx = new Point(
                 mResponsiveAllAppsWidthSpec.getGutterPx(),
                 mResponsiveAllAppsHeightSpec.getGutterPx()
         );
-        allAppsCellHeightPx = mResponsiveAllAppsHeightSpec.getCellSizePx()
-                + mResponsiveAllAppsHeightSpec.getGutterPx();
+        allAppsCellHeightPx = mResponsiveAllAppsHeightSpec.getCellSizePx();
         allAppsCellWidthPx = mResponsiveAllAppsWidthSpec.getCellSizePx();
 
         // This workaround is needed to align AllApps icons with Workspace icons
@@ -1282,22 +1322,6 @@
         allAppsPadding.left = mResponsiveAllAppsWidthSpec.getStartPaddingPx() - halfBorder;
         allAppsPadding.right = mResponsiveAllAppsWidthSpec.getEndPaddingPx() - halfBorder;
 
-        // TODO(b/287975993): Remove this after icon size is extracted to responsive grid
-        // Copy icon size from the workspace when spec is matchWorkspace or
-        // use the default all apps icon size
-        if (mResponsiveAllAppsHeightSpec.isCellSizeMatchWorkspace()
-                || mResponsiveAllAppsWidthSpec.isCellSizeMatchWorkspace()) {
-            allAppsIconSizePx = iconSizePx;
-            allAppsIconTextSizePx = iconTextSizePx;
-            allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
-        } else {
-            allAppsIconSizePx = pxFromDp(inv.allAppsIconSize[mTypeIndex], mMetrics);
-            allAppsIconTextSizePx = pxFromSp(inv.allAppsIconTextSize[mTypeIndex], mMetrics);
-            allAppsIconDrawablePaddingPx = res.getDimensionPixelSize(
-                    R.dimen.all_apps_icon_drawable_padding);
-            allAppsIconDrawablePaddingPx = getNormalizedIconDrawablePadding(allAppsIconSizePx,
-                    allAppsIconDrawablePaddingPx);
-        }
 
         // Reduce the size of the app icon if it doesn't fit
         if (allAppsCellWidthPx < allAppsIconSizePx) {
@@ -1322,6 +1346,8 @@
             allAppsIconDrawablePaddingPx = cellContentDimensions.getIconDrawablePaddingPx();
             allAppsIconTextSizePx = cellContentDimensions.getIconTextSizePx();
         }
+
+        allAppsCellHeightPx += mResponsiveAllAppsHeightSpec.getGutterPx();
     }
 
     /**
@@ -1390,15 +1416,14 @@
     }
 
     private void updateFolderCellSize(float scale, Resources res) {
-        float invIconSizeDp = inv.iconSize[mTypeIndex];
-        folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, scale));
-        folderChildTextSizePx = pxFromSp(inv.iconTextSize[mTypeIndex], mMetrics, scale);
-        folderLabelTextSizePx = Math.max(pxFromSp(MIN_FOLDER_TEXT_SIZE_SP, mMetrics, scale),
-                (int) (folderChildTextSizePx * folderLabelTextScale));
-
-        int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
-
+        int minLabelTextSize = pxFromSp(MIN_FOLDER_TEXT_SIZE_SP, mMetrics, scale);
         if (mIsResponsiveGrid) {
+            folderChildIconSizePx = mResponsiveWorkspaceCellSpec.getIconSize();
+            folderChildTextSizePx = mResponsiveWorkspaceCellSpec.getIconTextSize();
+            folderLabelTextSizePx = Math.max(minLabelTextSize,
+                    (int) (folderChildTextSizePx * folderLabelTextScale));
+            int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
+
             folderCellWidthPx = mResponsiveFolderWidthSpec.getCellSizePx();
             folderCellHeightPx = mResponsiveFolderHeightSpec.getCellSizePx();
             folderContentPaddingTop = mResponsiveFolderHeightSpec.getStartPaddingPx();
@@ -1425,9 +1450,20 @@
             folderChildIconSizePx = cellContentDimensions.getIconSizePx();
             folderChildDrawablePaddingPx = cellContentDimensions.getIconDrawablePaddingPx();
             folderChildTextSizePx = cellContentDimensions.getIconTextSizePx();
-            folderLabelTextSizePx = Math.max(pxFromSp(MIN_FOLDER_TEXT_SIZE_SP, mMetrics, scale),
+            folderLabelTextSizePx = Math.max(minLabelTextSize,
                     (int) (folderChildTextSizePx * folderLabelTextScale));
-        } else if (mIsScalableGrid) {
+            return;
+        }
+
+        float invIconSizeDp = inv.iconSize[mTypeIndex];
+        float invIconTextSizeDp = inv.iconTextSize[mTypeIndex];
+        folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, scale));
+        folderChildTextSizePx = pxFromSp(invIconTextSizeDp, mMetrics, scale);
+        folderLabelTextSizePx = Math.max(minLabelTextSize,
+                (int) (folderChildTextSizePx * folderLabelTextScale));
+        int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
+
+        if (mIsScalableGrid) {
             if (inv.folderStyle == INVALID_RESOURCE_HANDLE) {
                 folderCellWidthPx = roundPxValueFromFloat(getCellSize().x * scale);
                 folderCellHeightPx = roundPxValueFromFloat(getCellSize().y * scale);
@@ -2136,6 +2172,9 @@
             writer.println(prefix + "\tmResponsiveFolderHeightSpec:" + mResponsiveFolderHeightSpec);
             writer.println(prefix + "\tmResponsiveFolderWidthSpec:" + mResponsiveFolderWidthSpec);
             writer.println(prefix + "\tmResponsiveHotseatSpec:" + mResponsiveHotseatSpec);
+            writer.println(prefix + "\tmResponsiveWorkspaceCellSpec:"
+                    + mResponsiveWorkspaceCellSpec);
+            writer.println(prefix + "\tmResponsiveAllAppsCellSpec:" + mResponsiveAllAppsCellSpec);
         }
     }
 
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index e5a6b2b..567d0c5 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -191,8 +191,18 @@
     public int folderSpecsId = INVALID_RESOURCE_HANDLE;
     @XmlRes
     public int folderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
     public int hotseatSpecsId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
     public int hotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int workspaceCellSpecsId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int workspaceCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int allAppsCellSpecsId = INVALID_RESOURCE_HANDLE;
+    @XmlRes
+    public int allAppsCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
 
     public String dbFile;
     public int defaultLayoutId;
@@ -375,6 +385,10 @@
         folderSpecsTwoPanelId = closestProfile.mFolderSpecsTwoPanelId;
         hotseatSpecsId = closestProfile.mHotseatSpecsId;
         hotseatSpecsTwoPanelId = closestProfile.mHotseatSpecsTwoPanelId;
+        workspaceCellSpecsId = closestProfile.mWorkspaceCellSpecsId;
+        workspaceCellSpecsTwoPanelId = closestProfile.mWorkspaceCellSpecsTwoPanelId;
+        allAppsCellSpecsId = closestProfile.mAllAppsCellSpecsId;
+        allAppsCellSpecsTwoPanelId = closestProfile.mAllAppsCellSpecsTwoPanelId;
         this.deviceType = deviceType;
 
         inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
@@ -827,6 +841,10 @@
         private final int mFolderSpecsTwoPanelId;
         private final int mHotseatSpecsId;
         private final int mHotseatSpecsTwoPanelId;
+        private final int mWorkspaceCellSpecsId;
+        private final int mWorkspaceCellSpecsTwoPanelId;
+        private final int mAllAppsCellSpecsId;
+        private final int mAllAppsCellSpecsTwoPanelId;
 
         public GridOption(Context context, AttributeSet attrs) {
             TypedArray a = context.obtainStyledAttributes(
@@ -909,6 +927,18 @@
                 mHotseatSpecsTwoPanelId = a.getResourceId(
                         R.styleable.GridDisplayOption_hotseatSpecsTwoPanelId,
                         INVALID_RESOURCE_HANDLE);
+                mWorkspaceCellSpecsId = a.getResourceId(
+                        R.styleable.GridDisplayOption_workspaceCellSpecsId,
+                        INVALID_RESOURCE_HANDLE);
+                mWorkspaceCellSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_workspaceCellSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
+                mAllAppsCellSpecsId = a.getResourceId(
+                        R.styleable.GridDisplayOption_allAppsCellSpecsId,
+                        INVALID_RESOURCE_HANDLE);
+                mAllAppsCellSpecsTwoPanelId = a.getResourceId(
+                        R.styleable.GridDisplayOption_allAppsCellSpecsTwoPanelId,
+                        INVALID_RESOURCE_HANDLE);
             } else {
                 mWorkspaceSpecsId = INVALID_RESOURCE_HANDLE;
                 mWorkspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
@@ -918,6 +948,10 @@
                 mFolderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
                 mHotseatSpecsId = INVALID_RESOURCE_HANDLE;
                 mHotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+                mWorkspaceCellSpecsId = INVALID_RESOURCE_HANDLE;
+                mWorkspaceCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+                mAllAppsCellSpecsId = INVALID_RESOURCE_HANDLE;
+                mAllAppsCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
             }
 
             int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
diff --git a/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt b/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
index 8710303..8e3a5b4 100644
--- a/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
+++ b/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
@@ -23,7 +23,13 @@
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
 import com.android.launcher3.util.ResourceHelper
 
-class HotseatSpecsProvider(private val groupOfSpecs: List<ResponsiveSpecGroup<HotseatSpec>>) {
+class HotseatSpecsProvider(groupOfSpecs: List<ResponsiveSpecGroup<HotseatSpec>>) {
+
+    private val groupOfSpecs: List<ResponsiveSpecGroup<HotseatSpec>>
+    init {
+        this.groupOfSpecs = groupOfSpecs.sortedBy { it.aspectRatio }
+    }
+
     fun getSpecsByAspectRatio(aspectRatio: Float): ResponsiveSpecGroup<HotseatSpec> {
         check(aspectRatio > 0f) { "Invalid aspect ratio! The value should be bigger than 0." }
 
diff --git a/src/com/android/launcher3/responsive/ResponsiveCellSpecsProvider.kt b/src/com/android/launcher3/responsive/ResponsiveCellSpecsProvider.kt
new file mode 100644
index 0000000..affca60
--- /dev/null
+++ b/src/com/android/launcher3/responsive/ResponsiveCellSpecsProvider.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.Companion.ResponsiveSpecType
+import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
+import com.android.launcher3.util.ResourceHelper
+
+class ResponsiveCellSpecsProvider(groupOfSpecs: List<ResponsiveSpecGroup<CellSpec>>) {
+    private val groupOfSpecs: List<ResponsiveSpecGroup<CellSpec>>
+
+    init {
+        this.groupOfSpecs =
+            groupOfSpecs
+                .onEach { group ->
+                    check(group.widthSpecs.isEmpty() && group.heightSpecs.isNotEmpty()) {
+                        "${this::class.simpleName} is invalid, only heightSpecs are allowed - " +
+                            "width list size = ${group.widthSpecs.size}; " +
+                            "height list size = ${group.heightSpecs.size}."
+                    }
+                }
+                .sortedBy { it.aspectRatio }
+    }
+
+    fun getSpecsByAspectRatio(aspectRatio: Float): ResponsiveSpecGroup<CellSpec> {
+        check(aspectRatio > 0f) { "Invalid aspect ratio! The value should be bigger than 0." }
+
+        val specsGroup = groupOfSpecs.firstOrNull { aspectRatio <= it.aspectRatio }
+        check(specsGroup != null) { "No available spec with aspectRatio within $aspectRatio." }
+
+        return specsGroup
+    }
+
+    fun getCalculatedSpec(aspectRatio: Float, availableHeightSpace: Int): CalculatedCellSpec {
+        val specsGroup = getSpecsByAspectRatio(aspectRatio)
+        val spec = specsGroup.getSpec(DimensionType.HEIGHT, availableHeightSpace)
+        return CalculatedCellSpec(availableHeightSpace, spec)
+    }
+
+    fun getCalculatedSpec(
+        aspectRatio: Float,
+        availableHeightSpace: Int,
+        workspaceCellSpec: CalculatedCellSpec
+    ): CalculatedCellSpec {
+        val specsGroup = getSpecsByAspectRatio(aspectRatio)
+        val spec = specsGroup.getSpec(DimensionType.HEIGHT, availableHeightSpace)
+        return CalculatedCellSpec(availableHeightSpace, spec, workspaceCellSpec)
+    }
+
+    companion object {
+        @JvmStatic
+        fun create(resourceHelper: ResourceHelper): ResponsiveCellSpecsProvider {
+            val parser = ResponsiveSpecsParser(resourceHelper)
+            val specs = parser.parseXML(ResponsiveSpecType.Cell, ::CellSpec)
+            return ResponsiveCellSpecsProvider(specs)
+        }
+    }
+}
+
+data class CellSpec(
+    override val maxAvailableSize: Int,
+    override val dimensionType: DimensionType,
+    override val specType: ResponsiveSpecType,
+    val iconSize: SizeSpec,
+    val iconTextSize: SizeSpec,
+    val iconDrawablePadding: SizeSpec
+) : IResponsiveSpec {
+    init {
+        check(isValid()) { "Invalid CellSpec found." }
+    }
+
+    constructor(
+        responsiveSpecType: ResponsiveSpecType,
+        attrs: TypedArray,
+        specs: Map<String, SizeSpec>
+    ) : this(
+        maxAvailableSize =
+            attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
+        dimensionType =
+            DimensionType.entries[
+                    attrs.getInt(
+                        R.styleable.ResponsiveSpec_dimensionType,
+                        DimensionType.HEIGHT.ordinal
+                    )],
+        specType = responsiveSpecType,
+        iconSize = specs.getOrError(SizeSpec.XmlTags.ICON_SIZE),
+        iconTextSize = specs.getOrError(SizeSpec.XmlTags.ICON_TEXT_SIZE),
+        iconDrawablePadding = specs.getOrError(SizeSpec.XmlTags.ICON_DRAWABLE_PADDING)
+    )
+
+    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 (iconSize.fixedSize > 0f || iconSize.matchWorkspace) &&
+            (iconTextSize.fixedSize >= 0f || iconTextSize.matchWorkspace) &&
+            (iconDrawablePadding.fixedSize >= 0f || iconDrawablePadding.matchWorkspace)
+    }
+
+    companion object {
+        private const val LOG_TAG = "CellSpec"
+    }
+}
+
+data class CalculatedCellSpec(
+    val availableSpace: Int,
+    val spec: CellSpec,
+    val iconSize: Int,
+    val iconTextSize: Int,
+    val iconDrawablePadding: Int
+) {
+    constructor(
+        availableSpace: Int,
+        spec: CellSpec
+    ) : this(
+        availableSpace = availableSpace,
+        spec = spec,
+        iconSize = spec.iconSize.getCalculatedValue(availableSpace),
+        iconTextSize = spec.iconTextSize.getCalculatedValue(availableSpace),
+        iconDrawablePadding = spec.iconDrawablePadding.getCalculatedValue(availableSpace)
+    )
+
+    constructor(
+        availableSpace: Int,
+        spec: CellSpec,
+        workspaceCellSpec: CalculatedCellSpec
+    ) : this(
+        availableSpace = availableSpace,
+        spec = spec,
+        iconSize = getCalculatedValue(availableSpace, spec.iconSize, workspaceCellSpec.iconSize),
+        iconTextSize =
+            getCalculatedValue(availableSpace, spec.iconTextSize, workspaceCellSpec.iconTextSize),
+        iconDrawablePadding =
+            getCalculatedValue(
+                availableSpace,
+                spec.iconDrawablePadding,
+                workspaceCellSpec.iconDrawablePadding
+            )
+    )
+
+    companion object {
+        private fun getCalculatedValue(
+            availableSpace: Int,
+            spec: SizeSpec,
+            workspaceValue: Int
+        ): Int =
+            if (spec.matchWorkspace) workspaceValue else spec.getCalculatedValue(availableSpace)
+    }
+
+    override fun toString(): String {
+        return "${this::class.simpleName}(" +
+            "availableSpace=$availableSpace, iconSize=$iconSize, " +
+            "iconTextSize=$iconTextSize, iconDrawablePadding=$iconDrawablePadding, " +
+            "${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
+            ")"
+    }
+}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpec.kt b/src/com/android/launcher3/responsive/ResponsiveSpec.kt
index 413e2dc..32dedfb 100644
--- a/src/com/android/launcher3/responsive/ResponsiveSpec.kt
+++ b/src/com/android/launcher3/responsive/ResponsiveSpec.kt
@@ -127,7 +127,8 @@
             AllApps("allAppsSpec"),
             Folder("folderSpec"),
             Workspace("workspaceSpec"),
-            Hotseat("hotseatSpec")
+            Hotseat("hotseatSpec"),
+            Cell("cellSpec")
         }
     }
 }
@@ -205,9 +206,6 @@
 
     fun isResponsiveSpecType(type: ResponsiveSpecType) = spec.specType == type
 
-    // TODO(b/287975993): Remove this after icon size is extracted to responsive grid
-    fun isCellSizeMatchWorkspace(): Boolean = spec.cellSize.matchWorkspace
-
     private fun updateRemainderSpaces(availableSpace: Int, cells: Int, spec: ResponsiveSpec) {
         val gutters = cells - 1
         val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells)
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index 2db843b..d146898 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -122,6 +122,9 @@
         const val CELL_SIZE = "cellSize"
         const val HOTSEAT_QSB_SPACE = "hotseatQsbSpace"
         const val EDGE_PADDING = "edgePadding"
+        const val ICON_SIZE = "iconSize"
+        const val ICON_TEXT_SIZE = "iconTextSize"
+        const val ICON_DRAWABLE_PADDING = "iconDrawablePadding"
     }
 
     companion object {
diff --git a/tests/res/xml/invalid_cell_specs_1.xml b/tests/res/xml/invalid_cell_specs_1.xml
new file mode 100644
index 0000000..1e54771
--- /dev/null
+++ b/tests/res/xml/invalid_cell_specs_1.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<!-- invalid: only fixedSize attribute is allowed -->
+<cellSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <!-- portrait -->
+    <specs launcher:maxAspectRatio="1.0">
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:ofAvailableSpace="0.0125" />
+            <iconSize launcher:fixedSize="48dp" />
+            <iconTextSize launcher:fixedSize="12sp" />
+        </cellSpec>
+    </specs>
+    <!-- landscape -->
+    <specs launcher:maxAspectRatio="10">
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="0dp" />
+            <iconSize launcher:ofAvailableSpace="0.0125" />
+            <iconTextSize launcher:fixedSize="0sp" />
+        </cellSpec>
+    </specs>
+</cellSpecs>
\ No newline at end of file
diff --git a/tests/res/xml/invalid_cell_specs_2.xml b/tests/res/xml/invalid_cell_specs_2.xml
new file mode 100644
index 0000000..6edfda0
--- /dev/null
+++ b/tests/res/xml/invalid_cell_specs_2.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<!-- invalid: dimension type should be height -->
+<cellSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <!-- portrait -->
+    <specs launcher:maxAspectRatio="1.0">
+        <cellSpec
+            launcher:dimensionType="width"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="8dp" />
+            <iconSize launcher:fixedSize="48dp" />
+            <iconTextSize launcher:fixedSize="12sp" />
+        </cellSpec>
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="8dp" />
+            <iconSize launcher:fixedSize="48dp" />
+            <iconTextSize launcher:fixedSize="12sp" />
+        </cellSpec>
+    </specs>
+    <!-- landscape -->
+    <specs launcher:maxAspectRatio="10">
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="0dp" />
+            <iconSize launcher:fixedSize="52dp" />
+            <iconTextSize launcher:fixedSize="0sp" />
+        </cellSpec>
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="0dp" />
+            <iconSize launcher:fixedSize="52dp" />
+            <iconTextSize launcher:fixedSize="0sp" />
+        </cellSpec>
+        <cellSpec
+            launcher:dimensionType="width"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="0dp" />
+            <iconSize launcher:fixedSize="52dp" />
+            <iconTextSize launcher:fixedSize="0sp" />
+        </cellSpec>
+    </specs>
+</cellSpecs>
\ No newline at end of file
diff --git a/tests/res/xml/valid_cell_specs_file.xml b/tests/res/xml/valid_cell_specs_file.xml
new file mode 100644
index 0000000..7a5f03f
--- /dev/null
+++ b/tests/res/xml/valid_cell_specs_file.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<cellSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <!-- portrait -->
+    <specs launcher:maxAspectRatio="1.0">
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="606dp">
+            <iconDrawablePadding launcher:fixedSize="8dp" />
+            <iconSize launcher:fixedSize="48dp" />
+            <iconTextSize launcher:fixedSize="12sp" />
+        </cellSpec>
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="11dp" />
+            <iconSize launcher:fixedSize="52dp" />
+            <iconTextSize launcher:fixedSize="12sp" />
+        </cellSpec>
+    </specs>
+    <!-- landscape -->
+    <specs launcher:maxAspectRatio="10">
+        <cellSpec
+            launcher:dimensionType="height"
+            launcher:maxAvailableSize="9999dp">
+            <iconDrawablePadding launcher:fixedSize="0dp" />
+            <iconSize launcher:fixedSize="52dp" />
+            <iconTextSize launcher:fixedSize="0sp" />
+        </cellSpec>
+    </specs>
+</cellSpecs>
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt b/tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
new file mode 100644
index 0000000..295f2e4
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractDeviceProfileTest
+import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
+import com.android.launcher3.tests.R as TestR
+import com.android.launcher3.util.TestResourceHelper
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ResponsiveCellSpecsProviderTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+    val deviceSpec = deviceSpecs["phone"]!!
+    val aspectRatio = deviceSpec.naturalSize.first.toFloat() / deviceSpec.naturalSize.second
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpec)
+    }
+
+    @Test
+    fun parseValidFile() {
+        val testResourceHelper = TestResourceHelper(context, TestR.xml.valid_cell_specs_file)
+        val provider = ResponsiveCellSpecsProvider.create(testResourceHelper)
+
+        // Validate Portrait
+        val aspectRatioPortrait = 1.0f
+        val expectedPortraitSpecs =
+            listOf(
+                CellSpec(
+                    maxAvailableSize = 606.dpToPx(),
+                    dimensionType = ResponsiveSpec.DimensionType.HEIGHT,
+                    specType = ResponsiveSpecType.Cell,
+                    iconSize = SizeSpec(48f.dpToPx()),
+                    iconTextSize = SizeSpec(12f.dpToPx()),
+                    iconDrawablePadding = SizeSpec(8f.dpToPx())
+                ),
+                CellSpec(
+                    maxAvailableSize = 9999.dpToPx(),
+                    dimensionType = ResponsiveSpec.DimensionType.HEIGHT,
+                    specType = ResponsiveSpecType.Cell,
+                    iconSize = SizeSpec(52f.dpToPx()),
+                    iconTextSize = SizeSpec(12f.dpToPx()),
+                    iconDrawablePadding = SizeSpec(11f.dpToPx())
+                )
+            )
+
+        val portraitSpecs = provider.getSpecsByAspectRatio(aspectRatioPortrait)
+
+        assertThat(portraitSpecs.aspectRatio).isAtLeast(aspectRatioPortrait)
+        assertThat(portraitSpecs.widthSpecs.size).isEqualTo(0)
+        assertThat(portraitSpecs.heightSpecs.size).isEqualTo(2)
+        assertThat(portraitSpecs.heightSpecs[0]).isEqualTo(expectedPortraitSpecs[0])
+        assertThat(portraitSpecs.heightSpecs[1]).isEqualTo(expectedPortraitSpecs[1])
+
+        // Validate Landscape
+        val aspectRatioLandscape = 1.051f
+        val expectedLandscapeSpec =
+            CellSpec(
+                maxAvailableSize = 9999.dpToPx(),
+                dimensionType = ResponsiveSpec.DimensionType.HEIGHT,
+                specType = ResponsiveSpecType.Cell,
+                iconSize = SizeSpec(52f.dpToPx()),
+                iconTextSize = SizeSpec(0f),
+                iconDrawablePadding = SizeSpec(0f)
+            )
+        val landscapeSpecs = provider.getSpecsByAspectRatio(aspectRatioLandscape)
+
+        assertThat(landscapeSpecs.aspectRatio).isAtLeast(aspectRatioLandscape)
+        assertThat(landscapeSpecs.widthSpecs.size).isEqualTo(0)
+        assertThat(landscapeSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(landscapeSpecs.heightSpecs[0]).isEqualTo(expectedLandscapeSpec)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_IsNotFixedSizeOrMatchWorkspace_throwsError() {
+        ResponsiveCellSpecsProvider.create(
+            TestResourceHelper(context, TestR.xml.invalid_cell_specs_1)
+        )
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_dimensionTypeIsNotHeight_throwsError() {
+        ResponsiveCellSpecsProvider.create(
+            TestResourceHelper(context, TestR.xml.invalid_cell_specs_2)
+        )
+    }
+}