Create AllApps responsive spec

Copy the parser from WorkspaceSpec and modify to use AllApps attributes.

Bug: 284152932
Test: AllAppsSpecsTest
Test: CalculatedAllAppsSpecTest
Flag: ENABLE_RESPONSIVE_WORKSPACE
Change-Id: I9362e126c64cb1a1abdef61894b003f14701b8e3
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 0ffe37b..bf0c4a3 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -265,6 +265,11 @@
         <attr name="maxAvailableSize" />
     </declare-styleable>
 
+    <declare-styleable name="AllAppsSpec">
+        <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/AllAppsSpecs.kt b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
new file mode 100644
index 0000000..85e383e
--- /dev/null
+++ b/src/com/android/launcher3/responsive/AllAppsSpecs.kt
@@ -0,0 +1,292 @@
+/*
+ * 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 com.android.launcher3.R
+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(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 }
+
+        return CalculatedAllAppsSpec(availableWidth, columns, widthSpec, 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 }
+
+        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()
+    }
+
+    override fun toString(): String {
+        return "CalculatedAllAppsSpec(availableSpace=$availableSpace, " +
+            "cells=$cells, startPaddingPx=$startPaddingPx, endPaddingPx=$endPaddingPx, " +
+            "gutterPx=$gutterPx, cellSizePx=$cellSizePx, " +
+            "AllAppsSpec.maxAvailableSize=${allAppsSpec.maxAvailableSize})"
+    }
+}
+
+data class AllAppsSpec(
+    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, "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()
+}
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index 32bc550..0d586c2 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -39,4 +39,8 @@
         <attr name="maxAvailableSize" />
     </declare-styleable>
 
+    <declare-styleable name="AllAppsSpec">
+        <attr name="specType" />
+        <attr name="maxAvailableSize" />
+    </declare-styleable>
 </resources>
diff --git a/tests/res/xml/invalid_all_apps_file_case_1.xml b/tests/res/xml/invalid_all_apps_file_case_1.xml
new file mode 100644
index 0000000..6fd35b1
--- /dev/null
+++ b/tests/res/xml/invalid_all_apps_file_case_1.xml
@@ -0,0 +1,36 @@
+<?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.
+  -->
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <!--  missing startPadding  -->
+        <endPadding launcher:fixedSize="0dp" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
+
diff --git a/tests/res/xml/invalid_all_apps_file_case_2.xml b/tests/res/xml/invalid_all_apps_file_case_2.xml
new file mode 100644
index 0000000..de9c1ac
--- /dev/null
+++ b/tests/res/xml/invalid_all_apps_file_case_2.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.
+  -->
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:fixedSize="0dp" />
+        <!--  more than 1 value in one tag -->
+        <gutter
+            launcher:matchWorkspace="true"
+            launcher:fixedSize="16dp" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
\ No newline at end of file
diff --git a/tests/res/xml/invalid_all_apps_file_case_3.xml b/tests/res/xml/invalid_all_apps_file_case_3.xml
new file mode 100644
index 0000000..7af0af4
--- /dev/null
+++ b/tests/res/xml/invalid_all_apps_file_case_3.xml
@@ -0,0 +1,37 @@
+<?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.
+  -->
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:fixedSize="0dp" />
+        <gutter launcher:matchWorkspace="true" />
+        <!--  value bigger than 1 -->
+        <cellSize launcher:ofRemainderSpace="1.001" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
+
diff --git a/tests/res/xml/valid_all_apps_file.xml b/tests/res/xml/valid_all_apps_file.xml
new file mode 100644
index 0000000..0be55d1
--- /dev/null
+++ b/tests/res/xml/valid_all_apps_file.xml
@@ -0,0 +1,36 @@
+<?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.
+  -->
+
+<allAppsSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <allAppsSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:fixedSize="0dp" />
+        <endPadding launcher:fixedSize="0dp" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+    <allAppsSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding launcher:matchWorkspace="true" />
+        <endPadding launcher:matchWorkspace="true" />
+        <gutter launcher:matchWorkspace="true" />
+        <cellSize launcher:matchWorkspace="true" />
+    </allAppsSpec>
+
+</allAppsSpecs>
diff --git a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
new file mode 100644
index 0000000..77ea5ba
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.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 AllAppsSpecsTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["phone"]!!)
+    }
+
+    @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())
+            .isEqualTo(
+                "AllAppsSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=HEIGHT, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+
+        assertThat(allAppsSpecs.allAppsWidthSpecList.size).isEqualTo(1)
+        assertThat(allAppsSpecs.allAppsWidthSpecList[0].toString())
+            .isEqualTo(
+                "AllAppsSpec(" +
+                    "maxAvailableSize=26247, " +
+                    "specType=WIDTH, " +
+                    "startPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "endPadding=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "gutter=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647), " +
+                    "cellSize=SizeSpec(fixedSize=0.0, " +
+                    "ofAvailableSpace=0.0, " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=true, " +
+                    "maxSize=2147483647)" +
+                    ")"
+            )
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_missingTag_throwsError() {
+        AllAppsSpecs(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))
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_valueBiggerThan1_throwsError() {
+        AllAppsSpecs(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
new file mode 100644
index 0000000..9f981fa
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CalculatedAllAppsSpecTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    /**
+     * This test tests:
+     * - (height spec) copy values from workspace
+     * - (width spec) copy values from workspace
+     */
+    @Test
+    fun normalPhone_copiesFromWorkspace() {
+        val deviceSpec = deviceSpecs["phone"]!!
+        initializeVarsForPhone(deviceSpec)
+
+        val availableWidth = deviceSpec.naturalSize.first
+        // Hotseat size is roughly 495px on a real device,
+        // it doesn't need to be precise on unit tests
+        val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 495
+
+        val workspaceSpecs =
+            WorkspaceSpecs(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))
+
+        with(allAppsSpecs.getCalculatedWidthSpec(4, availableWidth, widthSpec)) {
+            assertThat(availableSpace).isEqualTo(availableWidth)
+            assertThat(cells).isEqualTo(4)
+            assertThat(startPaddingPx).isEqualTo(widthSpec.startPaddingPx)
+            assertThat(endPaddingPx).isEqualTo(widthSpec.endPaddingPx)
+            assertThat(gutterPx).isEqualTo(widthSpec.gutterPx)
+            assertThat(cellSizePx).isEqualTo(widthSpec.cellSizePx)
+        }
+
+        with(allAppsSpecs.getCalculatedHeightSpec(5, availableHeight, heightSpec)) {
+            assertThat(availableSpace).isEqualTo(availableHeight)
+            assertThat(cells).isEqualTo(5)
+            assertThat(startPaddingPx).isEqualTo(0)
+            assertThat(endPaddingPx).isEqualTo(0)
+            assertThat(gutterPx).isEqualTo(heightSpec.gutterPx)
+            assertThat(cellSizePx).isEqualTo(heightSpec.cellSizePx)
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
index 691a069..cf80ece 100644
--- a/tests/src/com/android/launcher3/util/TestResourceHelper.kt
+++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
@@ -31,6 +31,7 @@
                 styleId.contentEquals(R.styleable.SizeSpec) -> TestR.styleable.SizeSpec
                 styleId.contentEquals(R.styleable.WorkspaceSpec) -> TestR.styleable.WorkspaceSpec
                 styleId.contentEquals(R.styleable.FolderSpec) -> TestR.styleable.FolderSpec
+                styleId.contentEquals(R.styleable.AllAppsSpec) -> TestR.styleable.AllAppsSpec
                 else -> styleId.clone()
             }