Merge "Adding match workspace to SizeSpec for responsive grid support" into udc-qpr-dev
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 275a3b8..80df78a 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -204,6 +204,7 @@
         <!-- File that contains the specs for the workspace.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="workspaceSpecsId" format="reference" />
+
         <!-- By default all categories are enabled -->
         <attr name="deviceCategory" format="integer">
             <!-- Enable on phone only -->
@@ -251,10 +252,11 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SpecSize">
+    <declare-styleable name="SizeSpec">
         <attr name="fixedSize" format="dimension" />
         <attr name="ofAvailableSpace" format="float" />
         <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
     </declare-styleable>
 
     <declare-styleable name="ProfileDisplayOption">
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
new file mode 100644
index 0000000..bf5ca1c
--- /dev/null
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -0,0 +1,73 @@
+package com.android.launcher3.responsive
+
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.util.Log
+import android.util.TypedValue
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceHelper
+
+data class SizeSpec(
+    val fixedSize: Float,
+    val ofAvailableSpace: Float,
+    val ofRemainderSpace: Float,
+    val matchWorkspace: Boolean
+) {
+
+    fun isValid(): Boolean {
+        // All attributes are empty
+        if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f && !matchWorkspace) {
+            Log.e(TAG, "SizeSpec#isValid - all attributes are empty")
+            return false
+        }
+
+        // More than one attribute is filled
+        val attrCount =
+            (if (fixedSize > 0) 1 else 0) +
+                (if (ofAvailableSpace > 0) 1 else 0) +
+                (if (ofRemainderSpace > 0) 1 else 0) +
+                (if (matchWorkspace) 1 else 0)
+        if (attrCount > 1) {
+            Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled")
+            return false
+        }
+
+        // Values should be between 0 and 1
+        if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) {
+            Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1")
+            return false
+        }
+
+        // Invalid fixed size
+        if (fixedSize < 0f) {
+            Log.e(TAG, "SizeSpec#isValid - values should be bigger or equal to zero.")
+            return false
+        }
+
+        return true
+    }
+
+    companion object {
+        private const val TAG = "WorkspaceSpecs::SizeSpec"
+        private fun getValue(a: TypedArray, index: Int): Float {
+            return when (a.getType(index)) {
+                TypedValue.TYPE_DIMENSION -> a.getDimensionPixelSize(index, 0).toFloat()
+                TypedValue.TYPE_FLOAT -> a.getFloat(index, 0f)
+                else -> 0f
+            }
+        }
+
+        fun create(resourceHelper: ResourceHelper, attrs: AttributeSet): SizeSpec {
+            val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SizeSpec)
+
+            val fixedSize = getValue(styledAttrs, R.styleable.SizeSpec_fixedSize)
+            val ofAvailableSpace = getValue(styledAttrs, R.styleable.SizeSpec_ofAvailableSpace)
+            val ofRemainderSpace = getValue(styledAttrs, R.styleable.SizeSpec_ofRemainderSpace)
+            val matchWorkspace = styledAttrs.getBoolean(R.styleable.SizeSpec_matchWorkspace, false)
+
+            styledAttrs.recycle()
+
+            return SizeSpec(fixedSize, ofAvailableSpace, ofRemainderSpace, matchWorkspace)
+        }
+    }
+}
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
index dc5ae47..4815924 100644
--- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
+++ b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
@@ -16,13 +16,12 @@
 
 package com.android.launcher3.workspace
 
-import android.content.res.TypedArray
 import android.content.res.XmlResourceParser
 import android.util.AttributeSet
 import android.util.Log
-import android.util.TypedValue
 import android.util.Xml
 import com.android.launcher3.R
+import com.android.launcher3.responsive.SizeSpec
 import com.android.launcher3.util.ResourceHelper
 import java.io.IOException
 import kotlin.math.roundToInt
@@ -95,16 +94,16 @@
                                 if (type == XmlPullParser.START_TAG) {
                                     when (parser.name) {
                                         XmlTags.START_PADDING -> {
-                                            startPadding = SizeSpec(resourceHelper, attr)
+                                            startPadding = SizeSpec.create(resourceHelper, attr)
                                         }
                                         XmlTags.END_PADDING -> {
-                                            endPadding = SizeSpec(resourceHelper, attr)
+                                            endPadding = SizeSpec.create(resourceHelper, attr)
                                         }
                                         XmlTags.GUTTER -> {
-                                            gutter = SizeSpec(resourceHelper, attr)
+                                            gutter = SizeSpec.create(resourceHelper, attr)
                                         }
                                         XmlTags.CELL_SIZE -> {
-                                            cellSize = SizeSpec(resourceHelper, attr)
+                                            cellSize = SizeSpec.create(resourceHelper, attr)
                                         }
                                     }
                                 }
@@ -270,61 +269,12 @@
     }
 
     private fun allSpecsAreValid(): Boolean =
-        startPadding.isValid() && endPadding.isValid() && gutter.isValid() && cellSize.isValid()
-}
-
-class SizeSpec(resourceHelper: ResourceHelper, attrs: AttributeSet) {
-    val fixedSize: Float
-    val ofAvailableSpace: Float
-    val ofRemainderSpace: Float
-
-    init {
-        val styledAttrs = resourceHelper.obtainStyledAttributes(attrs, R.styleable.SpecSize)
-
-        fixedSize = getValue(styledAttrs, R.styleable.SpecSize_fixedSize)
-        ofAvailableSpace = getValue(styledAttrs, R.styleable.SpecSize_ofAvailableSpace)
-        ofRemainderSpace = getValue(styledAttrs, R.styleable.SpecSize_ofRemainderSpace)
-
-        styledAttrs.recycle()
-    }
-
-    private fun getValue(a: TypedArray, index: Int): Float {
-        if (a.getType(index) == TypedValue.TYPE_DIMENSION) {
-            return a.getDimensionPixelSize(index, 0).toFloat()
-        } else if (a.getType(index) == TypedValue.TYPE_FLOAT) {
-            return a.getFloat(index, 0f)
-        }
-        return 0f
-    }
-
-    fun isValid(): Boolean {
-        // All attributes are empty
-        if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f) {
-            Log.e(TAG, "SizeSpec#isValid - all attributes are empty")
-            return false
-        }
-
-        // More than one attribute is filled
-        val attrCount =
-            (if (fixedSize > 0) 1 else 0) +
-                (if (ofAvailableSpace > 0) 1 else 0) +
-                (if (ofRemainderSpace > 0) 1 else 0)
-        if (attrCount > 1) {
-            Log.e(TAG, "SizeSpec#isValid - more than one attribute is filled")
-            return false
-        }
-
-        // Values should be between 0 and 1
-        if (ofAvailableSpace !in 0f..1f || ofRemainderSpace !in 0f..1f) {
-            Log.e(TAG, "SizeSpec#isValid - values should be between 0 and 1")
-            return false
-        }
-
-        return true
-    }
-
-    override fun toString(): String {
-        return "SizeSpec(fixedSize=$fixedSize, ofAvailableSpace=$ofAvailableSpace, " +
-            "ofRemainderSpace=$ofRemainderSpace)"
-    }
+        startPadding.isValid() &&
+            endPadding.isValid() &&
+            gutter.isValid() &&
+            cellSize.isValid() &&
+            !startPadding.matchWorkspace &&
+            !endPadding.matchWorkspace &&
+            !gutter.matchWorkspace &&
+            !cellSize.matchWorkspace
 }
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index 2310d9e..cb6da3b 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -26,10 +26,10 @@
         <attr name="maxAvailableSize" format="dimension" />
     </declare-styleable>
 
-    <declare-styleable name="SpecSize">
+    <declare-styleable name="SizeSpec">
         <attr name="fixedSize" format="dimension" />
         <attr name="ofAvailableSpace" format="float" />
         <attr name="ofRemainderSpace" format="float" />
+        <attr name="matchWorkspace" format="boolean" />
     </declare-styleable>
-
 </resources>
diff --git a/tests/res/xml/invalid_workspace_file_case_4.xml b/tests/res/xml/invalid_workspace_file_case_4.xml
new file mode 100644
index 0000000..9e74c85
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_4.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<workspaceSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="648dp">
+        <startPadding
+            launcher:ofAvailableSpace="0.0125" />
+        <endPadding
+            launcher:ofAvailableSpace="0.05" />
+        <!--  value in workspace spec using matchWorkspace -->
+        <gutter
+            launcher:matchWorkspace="true" />
+        <cellSize
+            launcher:ofRemainderSpace="0.2" />
+    </workspaceSpec>
+
+    <workspaceSpec
+        launcher:specType="height"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding
+            launcher:ofAvailableSpace="0.0306" />
+        <endPadding
+            launcher:ofAvailableSpace="0.068" />
+        <gutter
+            launcher:fixedSize="16dp" />
+        <cellSize
+            launcher:ofRemainderSpace="0.2" />
+    </workspaceSpec>
+
+    <!-- Width spec is always the same -->
+    <workspaceSpec
+        launcher:specType="width"
+        launcher:maxAvailableSize="9999dp">
+        <startPadding
+            launcher:ofRemainderSpace="0.21436227" />
+        <endPadding
+            launcher:ofRemainderSpace="0.21436227" />
+        <gutter
+            launcher:ofRemainderSpace="0.11425509" />
+        <cellSize
+            launcher:fixedSize="120dp" />
+    </workspaceSpec>
+</workspaceSpecs>
diff --git a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
new file mode 100644
index 0000000..426777d
--- /dev/null
+++ b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SizeSpecTest : AbstractDeviceProfileTest() {
+    override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @Before
+    fun setup() {
+        initializeVarsForPhone(deviceSpecs["phone"]!!)
+    }
+
+    @Test
+    fun valid_values() {
+        val combinations =
+            listOf(
+                SizeSpec(100f, 0f, 0f, false),
+                SizeSpec(0f, 1f, 0f, false),
+                SizeSpec(0f, 0f, 1f, false),
+                SizeSpec(0f, 0f, 0f, false),
+                SizeSpec(0f, 0f, 0f, true)
+            )
+
+        for (instance in combinations) {
+            assertThat(instance.isValid()).isEqualTo(true)
+        }
+    }
+
+    @Test
+    fun multiple_values_assigned() {
+        val combinations =
+            listOf(
+                SizeSpec(1f, 1f, 0f, false),
+                SizeSpec(1f, 0f, 1f, false),
+                SizeSpec(1f, 0f, 0f, true),
+                SizeSpec(0f, 1f, 1f, false),
+                SizeSpec(0f, 1f, 0f, true),
+                SizeSpec(0f, 0f, 1f, true),
+                SizeSpec(1f, 1f, 1f, true)
+            )
+
+        for (instance in combinations) {
+            assertThat(instance.isValid()).isEqualTo(false)
+        }
+    }
+
+    @Test
+    fun invalid_values() {
+        val combinations =
+            listOf(
+                SizeSpec(0f, 1.1f, 0f, false),
+                SizeSpec(0f, -0.1f, 0f, false),
+                SizeSpec(0f, 0f, 1.1f, false),
+                SizeSpec(0f, 0f, -0.1f, false),
+                SizeSpec(-1f, 0f, 0f, false)
+            )
+
+        for (instance in combinations) {
+            assertThat(instance.isValid()).isEqualTo(false)
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
index fb03fe1..3f0054e 100644
--- a/tests/src/com/android/launcher3/util/TestResourceHelper.kt
+++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
@@ -27,7 +27,7 @@
     ResourceHelper(context, specsFileId) {
     override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
         var clone = styleId.clone()
-        if (styleId == R.styleable.SpecSize) clone = TestR.styleable.SpecSize
+        if (styleId == R.styleable.SizeSpec) clone = TestR.styleable.SizeSpec
         else if (styleId == R.styleable.WorkspaceSpec) clone = TestR.styleable.WorkspaceSpec
         return context.obtainStyledAttributes(attrs, clone)
     }
diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
index 9cd0a2e..51808e3 100644
--- a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
+++ b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
@@ -50,16 +50,20 @@
                     "specType=HEIGHT, " +
                     "startPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "endPadding=SizeSpec(fixedSize=84.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "cellSize=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.15808, " +
-                    "ofRemainderSpace=0.0)" +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false)" +
                     ")"
             )
         assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
@@ -69,16 +73,20 @@
                     "specType=HEIGHT, " +
                     "startPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "endPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=1.0), " +
+                    "ofRemainderSpace=1.0, " +
+                    "matchWorkspace=false), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "cellSize=SizeSpec(fixedSize=273.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0)" +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false)" +
                     ")"
             )
         assertThat(workspaceSpecs.workspaceHeightSpecList[2].toString())
@@ -88,16 +96,20 @@
                     "specType=HEIGHT, " +
                     "startPadding=SizeSpec(fixedSize=21.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "endPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=1.0), " +
+                    "ofRemainderSpace=1.0, " +
+                    "matchWorkspace=false), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "cellSize=SizeSpec(fixedSize=273.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0)" +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false)" +
                     ")"
             )
         assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
@@ -108,16 +120,20 @@
                     "specType=WIDTH, " +
                     "startPadding=SizeSpec(fixedSize=58.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "endPadding=SizeSpec(fixedSize=58.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.0), " +
+                    "ofRemainderSpace=0.0, " +
+                    "matchWorkspace=false), " +
                     "cellSize=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.25)" +
+                    "ofRemainderSpace=0.25, " +
+                    "matchWorkspace=false)" +
                     ")"
             )
     }
@@ -136,4 +152,9 @@
     fun parseInvalidFile_valueBiggerThan1_throwsError() {
         WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3))
     }
+
+    @Test(expected = IllegalStateException::class)
+    fun parseInvalidFile_matchWorkspace_true_throwsError() {
+        WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_4))
+    }
 }