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()
}