Add FolderSpec for responsive grid support

Adds responsive grid implementation for folders. It follows the same concept as WorkspaceSpecs, so we have a parser similar to WorkspaceSpecs for FolderSpecs.

Bug: 284155638
Flag: ENABLE_RESPONSIVE_WORKSPACE
Test: FolderSpecsTest
Test: CalculatedFolderSpecsTest
Change-Id: Iea6d7d88ef42d1273aed7cf2ed5b397035518a52
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 80df78a..1be1a1a 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -259,6 +259,11 @@
         <attr name="matchWorkspace" format="boolean" />
     </declare-styleable>
 
+    <declare-styleable name="FolderSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
     <declare-styleable name="ProfileDisplayOption">
         <attr name="name" />
         <attr name="minWidthDps" format="float" />
diff --git a/src/com/android/launcher3/responsive/FolderSpecs.kt b/src/com/android/launcher3/responsive/FolderSpecs.kt
new file mode 100644
index 0000000..be97cf8
--- /dev/null
+++ b/src/com/android/launcher3/responsive/FolderSpecs.kt
@@ -0,0 +1,280 @@
+package com.android.launcher3.responsive
+
+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.FolderSpec.*
+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(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(
+        columns: Int,
+        availableWidth: Int,
+        workspaceSpec: CalculatedWorkspaceSpec
+    ): CalculatedFolderSpec {
+        check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.WIDTH) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${WorkspaceSpec.SpecType.WIDTH} - " +
+                "Found: ${workspaceSpec.workspaceSpec.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)
+    }
+
+    /**
+     * Returns the [CalculatedFolderSpec] for height, based on the available height, FolderSpecs and
+     * WorkspaceSpecs.
+     */
+    fun getHeightSpec(
+        rows: Int,
+        availableHeight: Int,
+        workspaceSpec: CalculatedWorkspaceSpec
+    ): CalculatedFolderSpec {
+        check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) {
+            "Invalid specType for CalculatedWorkspaceSpec. " +
+                "Expected: ${WorkspaceSpec.SpecType.HEIGHT} - " +
+                "Found: ${workspaceSpec.workspaceSpec.specType}}"
+        }
+
+        val heightSpec = _heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize }
+        check(heightSpec != null) { "No FolderSpec for height spec found with $availableHeight." }
+
+        return convertToCalculatedFolderSpec(heightSpec, availableHeight, rows, workspaceSpec)
+    }
+}
+
+data class CalculatedFolderSpec(
+    val startPaddingPx: Int,
+    val endPaddingPx: Int,
+    val gutterPx: Int,
+    val cellSizePx: Int,
+    val availableSpace: Int,
+    val cells: 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
+) {
+
+    enum class SpecType {
+        HEIGHT,
+        WIDTH
+    }
+
+    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(
+        startPaddingPx = startPaddingPx,
+        endPaddingPx = endPaddingPx,
+        gutterPx = gutterPx,
+        cellSizePx = cellSizePx,
+        availableSpace = availableSpace,
+        cells = cells
+    )
+}
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index bf5ca1c..407a212 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -6,14 +6,45 @@
 import android.util.TypedValue
 import com.android.launcher3.R
 import com.android.launcher3.util.ResourceHelper
+import kotlin.math.roundToInt
 
+/**
+ * [SizeSpec] is an attribute used to represent a property in the responsive grid specs.
+ *
+ * @param fixedSize a fixed size in dp to be used
+ * @param ofAvailableSpace a percentage of the available space
+ * @param ofRemainderSpace a percentage of the remaining space (available space minus used space)
+ * @param matchWorkspace indicates whether the workspace value will be used or not.
+ */
 data class SizeSpec(
-    val fixedSize: Float,
-    val ofAvailableSpace: Float,
-    val ofRemainderSpace: Float,
-    val matchWorkspace: Boolean
+    val fixedSize: Float = 0f,
+    val ofAvailableSpace: Float = 0f,
+    val ofRemainderSpace: Float = 0f,
+    val matchWorkspace: Boolean = false
 ) {
 
+    /** Retrieves the correct value for [SizeSpec]. */
+    fun getCalculatedValue(availableSpace: Int, workspaceValue: Int): Int {
+        return when {
+            fixedSize > 0 -> fixedSize.roundToInt()
+            ofAvailableSpace > 0 -> (ofAvailableSpace * availableSpace).roundToInt()
+            matchWorkspace -> workspaceValue
+            else -> 0
+        }
+    }
+
+    /**
+     * Calculates the [SizeSpec] value when remainder space value is defined. If no remainderSpace
+     * is 0, returns a default value.
+     */
+    fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int): Int {
+        return if (ofRemainderSpace > 0) {
+            (ofRemainderSpace * remainderSpace).roundToInt()
+        } else {
+            defaultValue
+        }
+    }
+
     fun isValid(): Boolean {
         // All attributes are empty
         if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f && !matchWorkspace) {
@@ -48,7 +79,8 @@
     }
 
     companion object {
-        private const val TAG = "WorkspaceSpecs::SizeSpec"
+        private const val TAG = "SizeSpec"
+
         private fun getValue(a: TypedArray, index: Int): Float {
             return when (a.getType(index)) {
                 TypedValue.TYPE_DIMENSION -> a.getDimensionPixelSize(index, 0).toFloat()
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
index 4815924..8cc0c59 100644
--- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
+++ b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
@@ -44,6 +44,7 @@
     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()
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index cb6da3b..54f0381 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -32,4 +32,10 @@
         <attr name="ofRemainderSpace" format="float" />
         <attr name="matchWorkspace" format="boolean" />
     </declare-styleable>
+
+    <declare-styleable name="FolderSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
+
 </resources>
diff --git a/tests/res/xml/invalid_folders_specs_1.xml b/tests/res/xml/invalid_folders_specs_1.xml
new file mode 100644
index 0000000..0864249
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_1.xml
@@ -0,0 +1,40 @@
+<?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.
+  -->
+
+<!-- Tablet - 6x5 portrait -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <!--  missing startPadding  -->
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_2.xml b/tests/res/xml/invalid_folders_specs_2.xml
new file mode 100644
index 0000000..0b7dd62
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_2.xml
@@ -0,0 +1,43 @@
+<?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.
+  -->
+
+<!-- Tablet - 6x5 portrait -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <!--  more than 1 value in one tag -->
+        <gutter
+            launcher:ofAvailableSpace="0.0125"
+            launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_3.xml b/tests/res/xml/invalid_folders_specs_3.xml
new file mode 100644
index 0000000..83fd3e1
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_3.xml
@@ -0,0 +1,41 @@
+<?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.
+  -->
+
+<!-- Tablet - 6x5 portrait - More the one value first gutter -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <!--  value bigger than 1 -->
+        <cellSize launcher:ofRemainderSpace="1.001" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_4.xml b/tests/res/xml/invalid_folders_specs_4.xml
new file mode 100644
index 0000000..2d8c730
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_4.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+<!-- missing height spec -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/invalid_folders_specs_5.xml b/tests/res/xml/invalid_folders_specs_5.xml
new file mode 100644
index 0000000..b4f1f4d
--- /dev/null
+++ b/tests/res/xml/invalid_folders_specs_5.xml
@@ -0,0 +1,33 @@
+<?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.
+  -->
+<!-- missing breakpoints > 800dp -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="104dp" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/res/xml/valid_folders_specs.xml b/tests/res/xml/valid_folders_specs.xml
new file mode 100644
index 0000000..0c45544
--- /dev/null
+++ b/tests/res/xml/valid_folders_specs.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.
+  -->
+<folderSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="800dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+    <folderSpec launcher:specType="width" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="16dp" />
+        <endPadding launcher:fixedSize="16dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:fixedSize="102dp" />
+    </folderSpec>
+
+    <!-- Height spec is fixed -->
+    <folderSpec launcher:specType="height" launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="24dp" />
+        <!-- mapped to footer height size -->
+        <endPadding launcher:fixedSize="64dp" />
+        <gutter launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </folderSpec>
+</folderSpecs>
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
new file mode 100644
index 0000000..c14722c
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.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.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
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CalculatedFolderSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    private val deviceSpec = deviceSpecs["phone"]!!
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpec)
+    }
+
+    @Test
+    fun validate_matchWidthWorkspace() {
+        val columns = 6
+
+        // Loading workspace specs
+        val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
+        val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+
+        // Loading folders specs
+        val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs(resourceHelperFolder)
+
+        assertThat(folderSpecs.widthSpecs.size).isEqualTo(2)
+        assertThat(folderSpecs.widthSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
+        assertThat(folderSpecs.widthSpecs[1].cellSize.matchWorkspace).isEqualTo(false)
+
+        // Validate width spec <= 800
+        var availableWidth = deviceSpec.naturalSize.first
+        var calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
+        var calculatedWidthFolderSpec =
+            folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+        with(calculatedWidthFolderSpec) {
+            assertThat(availableSpace).isEqualTo(availableWidth)
+            assertThat(cells).isEqualTo(columns)
+            assertThat(startPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(endPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(gutterPx).isEqualTo(16.dpToPx())
+            assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
+        }
+
+        // Validate width spec > 800
+        availableWidth = 2000.dpToPx()
+        calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth)
+        calculatedWidthFolderSpec =
+            folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace)
+        with(calculatedWidthFolderSpec) {
+            assertThat(availableSpace).isEqualTo(availableWidth)
+            assertThat(cells).isEqualTo(columns)
+            assertThat(startPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(endPaddingPx).isEqualTo(16.dpToPx())
+            assertThat(gutterPx).isEqualTo(16.dpToPx())
+            assertThat(cellSizePx).isEqualTo(102.dpToPx())
+        }
+    }
+
+    @Test
+    fun validate_matchHeightWorkspace() {
+        // Hotseat is roughly 495px on a real device, it doesn't need to be precise on unit tests
+        val hotseatSize = 495
+        val statusBarHeight = deviceSpec.statusBarNaturalPx
+        val availableHeight = deviceSpec.naturalSize.second - statusBarHeight - hotseatSize
+        val rows = 5
+
+        // Loading workspace specs
+        val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file)
+        val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace)
+
+        // Loading folders specs
+        val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs(resourceHelperFolder)
+
+        assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(folderSpecs.heightSpecs[0].cellSize.matchWorkspace).isEqualTo(true)
+
+        // Validate height spec
+        val calculatedWorkspace = workspaceSpecs.getCalculatedHeightSpec(rows, availableHeight)
+        val calculatedFolderSpec =
+            folderSpecs.getHeightSpec(rows, availableHeight, calculatedWorkspace)
+        with(calculatedFolderSpec) {
+            assertThat(availableSpace).isEqualTo(availableHeight)
+            assertThat(cells).isEqualTo(rows)
+            assertThat(startPaddingPx).isEqualTo(24.dpToPx())
+            assertThat(endPaddingPx).isEqualTo(64.dpToPx())
+            assertThat(gutterPx).isEqualTo(16.dpToPx())
+            assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
+        }
+    }
+
+    private fun Int.dpToPx(): Int {
+        return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
new file mode 100644
index 0000000..796bf9a
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.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
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["tablet"]!!)
+    }
+
+    @Test
+    fun parseValidFile() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs(resourceHelper)
+
+        val sizeSpec16 = SizeSpec(16f.dpToPx())
+        val widthSpecsExpected =
+            listOf(
+                FolderSpec(
+                    maxAvailableSize = 800.dpToPx(),
+                    specType = FolderSpec.SpecType.WIDTH,
+                    startPadding = sizeSpec16,
+                    endPadding = sizeSpec16,
+                    gutter = sizeSpec16,
+                    cellSize = SizeSpec(matchWorkspace = true)
+                ),
+                FolderSpec(
+                    maxAvailableSize = 9999.dpToPx(),
+                    specType = FolderSpec.SpecType.WIDTH,
+                    startPadding = sizeSpec16,
+                    endPadding = sizeSpec16,
+                    gutter = sizeSpec16,
+                    cellSize = SizeSpec(102f.dpToPx())
+                )
+            )
+
+        val heightSpecsExpected =
+            FolderSpec(
+                maxAvailableSize = 9999.dpToPx(),
+                specType = FolderSpec.SpecType.HEIGHT,
+                startPadding = SizeSpec(24f.dpToPx()),
+                endPadding = SizeSpec(64f.dpToPx()),
+                gutter = sizeSpec16,
+                cellSize = SizeSpec(matchWorkspace = true)
+            )
+
+        assertThat(folderSpecs.widthSpecs.size).isEqualTo(widthSpecsExpected.size)
+        assertThat(folderSpecs.widthSpecs[0]).isEqualTo(widthSpecsExpected[0])
+        assertThat(folderSpecs.widthSpecs[1]).isEqualTo(widthSpecsExpected[1])
+
+        assertThat(folderSpecs.heightSpecs.size).isEqualTo(1)
+        assertThat(folderSpecs.heightSpecs[0]).isEqualTo(heightSpecsExpected)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingTag_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_1)
+        FolderSpecs(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_2)
+        FolderSpecs(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_valueBiggerThan1_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_3)
+        FolderSpecs(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingSpecs_throwsError() {
+        val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_4)
+        FolderSpecs(resourceHelper)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingWidthBreakpoint_throwsError() {
+        val availableSpace = 900.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = WorkspaceSpec.SpecType.WIDTH,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        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)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingHeightBreakpoint_throwsError() {
+        val availableSpace = 900.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = WorkspaceSpec.SpecType.HEIGHT,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        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)
+    }
+
+    @Test
+    fun retrievesCalculatedWidthSpec() {
+        val availableSpace = 800.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = WorkspaceSpec.SpecType.WIDTH,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        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 calculatedWidthSpec =
+            folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        assertThat(calculatedWidthSpec).isEqualTo(expectedResult)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun retrievesCalculatedWidthSpec_invalidCalculatedWorkspaceSpecType_throwsError() {
+        val availableSpace = 10.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = WorkspaceSpec.SpecType.HEIGHT,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs(resourceHelper)
+        folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec)
+    }
+
+    @Test
+    fun retrievesCalculatedHeightSpec() {
+        val availableSpace = 700.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = WorkspaceSpec.SpecType.HEIGHT,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        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 calculatedHeightSpec =
+            folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+        assertThat(calculatedHeightSpec).isEqualTo(expectedResult)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun retrievesCalculatedHeightSpec_invalidCalculatedWorkspaceSpecType_throwsError() {
+        val availableSpace = 10.dpToPx()
+        val cells = 3
+
+        val workspaceSpec =
+            WorkspaceSpec(
+                maxAvailableSize = availableSpace,
+                specType = WorkspaceSpec.SpecType.WIDTH,
+                startPadding = SizeSpec(fixedSize = 10f),
+                endPadding = SizeSpec(fixedSize = 10f),
+                gutter = SizeSpec(fixedSize = 10f),
+                cellSize = SizeSpec(fixedSize = 10f)
+            )
+        val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec)
+
+        val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs)
+        val folderSpecs = FolderSpecs(resourceHelper)
+        folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec)
+    }
+
+    private fun Float.dpToPx(): Float {
+        return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat()
+    }
+
+    private fun Int.dpToPx(): Int {
+        return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
+    }
+}
diff --git a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
index 426777d..5db86ff 100644
--- a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,6 +54,42 @@
     }
 
     @Test
+    fun validate_getCalculatedValue() {
+        val availableSpace = 100
+        val matchWorkspaceValue = 101
+        val combinations =
+            listOf(
+                SizeSpec(100f) to 100,
+                SizeSpec(ofAvailableSpace = .5f) to (availableSpace * .5f).roundToInt(),
+                SizeSpec(ofRemainderSpace = .5f) to 0,
+                SizeSpec(matchWorkspace = true) to matchWorkspaceValue
+            )
+
+        for ((sizeSpec, expectedValue) in combinations) {
+            val value = sizeSpec.getCalculatedValue(availableSpace, matchWorkspaceValue)
+            assertThat(value).isEqualTo(expectedValue)
+        }
+    }
+
+    @Test
+    fun validate_getRemainderSpaceValue() {
+        val remainderSpace = 100
+        val defaultValue = 10
+        val combinations =
+            listOf(
+                SizeSpec(100f) to defaultValue,
+                SizeSpec(ofAvailableSpace = .5f) to defaultValue,
+                SizeSpec(ofRemainderSpace = .5f) to (remainderSpace * .5f).roundToInt(),
+                SizeSpec(matchWorkspace = true) to defaultValue
+            )
+
+        for ((sizeSpec, expectedValue) in combinations) {
+            val value = sizeSpec.getRemainderSpaceValue(remainderSpace, defaultValue)
+            assertThat(value).isEqualTo(expectedValue)
+        }
+    }
+
+    @Test
     fun multiple_values_assigned() {
         val combinations =
             listOf(
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
index 3f0054e..691a069 100644
--- a/tests/src/com/android/launcher3/util/TestResourceHelper.kt
+++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
@@ -23,12 +23,17 @@
 import com.android.launcher3.tests.R as TestR
 import kotlin.IntArray
 
-class TestResourceHelper(private val context: Context, private val specsFileId: Int) :
+class TestResourceHelper(private val context: Context, specsFileId: Int) :
     ResourceHelper(context, specsFileId) {
     override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
-        var clone = styleId.clone()
-        if (styleId == R.styleable.SizeSpec) clone = TestR.styleable.SizeSpec
-        else if (styleId == R.styleable.WorkspaceSpec) clone = TestR.styleable.WorkspaceSpec
+        val clone =
+            when {
+                styleId.contentEquals(R.styleable.SizeSpec) -> TestR.styleable.SizeSpec
+                styleId.contentEquals(R.styleable.WorkspaceSpec) -> TestR.styleable.WorkspaceSpec
+                styleId.contentEquals(R.styleable.FolderSpec) -> TestR.styleable.FolderSpec
+                else -> styleId.clone()
+            }
+
         return context.obtainStyledAttributes(attrs, clone)
     }
 }