Update SizeSpec attributes to accept max size threshold

Add maxSize attribute to SizeSpec to limit the cell size up to a max size when matchWorkspace is true. The same validation was added when using fixedSize, ofAvailableSpace and ofRemainderSpace, so they could have a maxSize as a threshold.

Bug: 284155638
Flag: ENABLE_RESPONSIVE_WORKSPACE
Test: SizeSpecTest
Test: WorkspaceSpecsTest
Change-Id: I113657c241e6618eb3e501243412d8c5626fc3d5
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 1be1a1a..0ffe37b 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -257,6 +257,7 @@
         <attr name="ofAvailableSpace" format="float" />
         <attr name="ofRemainderSpace" format="float" />
         <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
     </declare-styleable>
 
     <declare-styleable name="FolderSpec">
diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index 407a212..3d618f9 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -15,22 +15,27 @@
  * @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.
+ * @param maxSize restricts the maximum value allowed for the [SizeSpec].
  */
 data class SizeSpec(
     val fixedSize: Float = 0f,
     val ofAvailableSpace: Float = 0f,
     val ofRemainderSpace: Float = 0f,
-    val matchWorkspace: Boolean = false
+    val matchWorkspace: Boolean = false,
+    val maxSize: Int = Int.MAX_VALUE
 ) {
 
     /** 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
-        }
+        val calculatedValue =
+            when {
+                fixedSize > 0 -> fixedSize.roundToInt()
+                matchWorkspace -> workspaceValue
+                ofAvailableSpace > 0 -> (ofAvailableSpace * availableSpace).roundToInt()
+                else -> 0
+            }
+
+        return calculatedValue.coerceAtMost(maxSize)
     }
 
     /**
@@ -38,11 +43,14 @@
      * is 0, returns a default value.
      */
     fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int): Int {
-        return if (ofRemainderSpace > 0) {
-            (ofRemainderSpace * remainderSpace).roundToInt()
-        } else {
-            defaultValue
-        }
+        val remainderSpaceValue =
+            if (ofRemainderSpace > 0) {
+                (ofRemainderSpace * remainderSpace).roundToInt()
+            } else {
+                defaultValue
+            }
+
+        return remainderSpaceValue.coerceAtMost(maxSize)
     }
 
     fun isValid(): Boolean {
@@ -69,12 +77,17 @@
             return false
         }
 
-        // Invalid fixed size
-        if (fixedSize < 0f) {
+        // Invalid fixed or max size
+        if (fixedSize < 0f || maxSize < 0f) {
             Log.e(TAG, "SizeSpec#isValid - values should be bigger or equal to zero.")
             return false
         }
 
+        if (fixedSize > 0f && fixedSize > maxSize) {
+            Log.e(TAG, "SizeSpec#isValid - fixed size should be smaller than the max size.")
+            return false
+        }
+
         return true
     }
 
@@ -96,10 +109,12 @@
             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)
+            val maxSize =
+                styledAttrs.getDimensionPixelSize(R.styleable.SizeSpec_maxSize, Int.MAX_VALUE)
 
             styledAttrs.recycle()
 
-            return SizeSpec(fixedSize, ofAvailableSpace, ofRemainderSpace, matchWorkspace)
+            return SizeSpec(fixedSize, ofAvailableSpace, ofRemainderSpace, matchWorkspace, maxSize)
         }
     }
 }
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
index 54f0381..32bc550 100644
--- a/tests/res/values/attrs.xml
+++ b/tests/res/values/attrs.xml
@@ -31,6 +31,7 @@
         <attr name="ofAvailableSpace" format="float" />
         <attr name="ofRemainderSpace" format="float" />
         <attr name="matchWorkspace" format="boolean" />
+        <attr name="maxSize" format="dimension" />
     </declare-styleable>
 
     <declare-styleable name="FolderSpec">
diff --git a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
index 5db86ff..088cae1 100644
--- a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
+++ b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt
@@ -45,7 +45,12 @@
                 SizeSpec(0f, 1f, 0f, false),
                 SizeSpec(0f, 0f, 1f, false),
                 SizeSpec(0f, 0f, 0f, false),
-                SizeSpec(0f, 0f, 0f, true)
+                SizeSpec(0f, 0f, 0f, true),
+                SizeSpec(100f, 0f, 0f, false, 100),
+                SizeSpec(0f, 1f, 0f, false, 100),
+                SizeSpec(0f, 0f, 1f, false, 100),
+                SizeSpec(0f, 0f, 0f, false, 100),
+                SizeSpec(0f, 0f, 0f, true, 100)
             )
 
         for (instance in combinations) {
@@ -62,7 +67,12 @@
                 SizeSpec(100f) to 100,
                 SizeSpec(ofAvailableSpace = .5f) to (availableSpace * .5f).roundToInt(),
                 SizeSpec(ofRemainderSpace = .5f) to 0,
-                SizeSpec(matchWorkspace = true) to matchWorkspaceValue
+                SizeSpec(matchWorkspace = true) to matchWorkspaceValue,
+                // Restricts max size up to 10 (calculated value > 10)
+                SizeSpec(100f, maxSize = 10) to 10,
+                SizeSpec(ofAvailableSpace = .5f, maxSize = 10) to 10,
+                SizeSpec(ofRemainderSpace = .5f, maxSize = 10) to 0,
+                SizeSpec(matchWorkspace = true, maxSize = 10) to 10
             )
 
         for ((sizeSpec, expectedValue) in combinations) {
@@ -74,13 +84,18 @@
     @Test
     fun validate_getRemainderSpaceValue() {
         val remainderSpace = 100
-        val defaultValue = 10
+        val defaultValue = 50
         val combinations =
             listOf(
                 SizeSpec(100f) to defaultValue,
                 SizeSpec(ofAvailableSpace = .5f) to defaultValue,
                 SizeSpec(ofRemainderSpace = .5f) to (remainderSpace * .5f).roundToInt(),
-                SizeSpec(matchWorkspace = true) to defaultValue
+                SizeSpec(matchWorkspace = true) to defaultValue,
+                // Restricts max size up to 10 (defaultValue > 10)
+                SizeSpec(100f, maxSize = 10) to 10,
+                SizeSpec(ofAvailableSpace = .5f, maxSize = 10) to 10,
+                SizeSpec(ofRemainderSpace = .5f, maxSize = 10) to 10,
+                SizeSpec(matchWorkspace = true, maxSize = 10) to 10,
             )
 
         for ((sizeSpec, expectedValue) in combinations) {
@@ -111,11 +126,13 @@
     fun invalid_values() {
         val combinations =
             listOf(
+                SizeSpec(-1f, 0f, 0f, false),
                 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)
+                SizeSpec(0f, 0f, 0f, false, -10),
+                SizeSpec(50f, 0f, 0f, false, 10)
             )
 
         for (instance in combinations) {
diff --git a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
index 51808e3..8b99a3a 100644
--- a/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
+++ b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
@@ -51,19 +51,23 @@
                     "startPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "endPadding=SizeSpec(fixedSize=84.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "cellSize=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.15808, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false)" +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
                     ")"
             )
         assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
@@ -74,19 +78,23 @@
                     "startPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "endPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=1.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "cellSize=SizeSpec(fixedSize=273.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false)" +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
                     ")"
             )
         assertThat(workspaceSpecs.workspaceHeightSpecList[2].toString())
@@ -97,19 +105,23 @@
                     "startPadding=SizeSpec(fixedSize=21.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "endPadding=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=1.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "cellSize=SizeSpec(fixedSize=273.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false)" +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
                     ")"
             )
         assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
@@ -121,19 +133,23 @@
                     "startPadding=SizeSpec(fixedSize=58.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "endPadding=SizeSpec(fixedSize=58.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "gutter=SizeSpec(fixedSize=42.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.0, " +
-                    "matchWorkspace=false), " +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647), " +
                     "cellSize=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
                     "ofRemainderSpace=0.25, " +
-                    "matchWorkspace=false)" +
+                    "matchWorkspace=false, " +
+                    "maxSize=2147483647)" +
                     ")"
             )
     }