Merge "Create an XML parser for WorkspaceSpecs" into tm-qpr-dev
diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
index 9afd893..bc1b87d 100644
--- a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
@@ -19,7 +19,7 @@
import android.graphics.RectF
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.launcher3.DeviceProfileBaseTest
+import com.android.launcher3.FakeInvariantDeviceProfileTest
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
import com.android.quickstep.views.TaskView.FullscreenDrawParams
@@ -36,7 +36,7 @@
/** Test for FullscreenDrawParams class. */
@SmallTest
@RunWith(AndroidJUnit4::class)
-class FullscreenDrawParamsTest : DeviceProfileBaseTest() {
+class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() {
private val TASK_SCALE = 0.7f
private var mThumbnailData: ThumbnailData = mock(ThumbnailData::class.java)
diff --git a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
index adbca32..a347156 100644
--- a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
@@ -18,7 +18,7 @@
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.launcher3.DeviceProfileBaseTest
+import com.android.launcher3.FakeInvariantDeviceProfileTest
import com.android.launcher3.util.WindowBounds
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -26,7 +26,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class HotseatWidthCalculationTest : DeviceProfileBaseTest() {
+class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() {
/**
* This is a case when after setting the hotseat, the space needs to be recalculated but it
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 26180f3..c3bd90e 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -224,6 +224,21 @@
<attr name="alignOnIcon" format="boolean" />
</declare-styleable>
+ <!-- Responsive grids attributes -->
+ <declare-styleable name="WorkspaceSpec">
+ <attr name="specType" format="integer">
+ <enum name="height" value="0" />
+ <enum name="width" value="1" />
+ </attr>
+ <attr name="maxAvailableSize" format="dimension" />
+ </declare-styleable>
+
+ <declare-styleable name="SpecSize">
+ <attr name="fixedSize" format="dimension" />
+ <attr name="ofAvailableSpace" format="float" />
+ <attr name="ofRemainderSpace" format="float" />
+ </declare-styleable>
+
<declare-styleable name="ProfileDisplayOption">
<attr name="name" />
<attr name="minWidthDps" format="float" />
diff --git a/src/com/android/launcher3/util/ResourceHelper.kt b/src/com/android/launcher3/util/ResourceHelper.kt
new file mode 100644
index 0000000..0ca7888
--- /dev/null
+++ b/src/com/android/launcher3/util/ResourceHelper.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.content.res.XmlResourceParser
+import android.util.AttributeSet
+import kotlin.IntArray
+
+/**
+ * This class is a helper that can be subclassed in tests to provide a way to parse attributes
+ * correctly.
+ */
+open class ResourceHelper(private val context: Context, private val specsFileId: Int) {
+ open fun getXml(): XmlResourceParser {
+ return context.resources.getXml(specsFileId)
+ }
+
+ open fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
+ return context.obtainStyledAttributes(attrs, styleId)
+ }
+}
diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
new file mode 100644
index 0000000..0f6e1b0
--- /dev/null
+++ b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.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.util.ResourceHelper
+import java.io.IOException
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+private const val TAG = "WorkspaceSpecs"
+
+class WorkspaceSpecs(resourceHelper: ResourceHelper) {
+ object XmlTags {
+ const val WORKSPACE_SPECS = "workspaceSpecs"
+
+ const val WORKSPACE_SPEC = "workspaceSpec"
+ const val START_PADDING = "startPadding"
+ const val END_PADDING = "endPadding"
+ const val GUTTER = "gutter"
+ const val CELL_SIZE = "cellSize"
+ }
+
+ val workspaceHeightSpecList = mutableListOf<WorkspaceSpec>()
+ val workspaceWidthSpecList = mutableListOf<WorkspaceSpec>()
+
+ init {
+ try {
+ val parser: XmlResourceParser = 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.WORKSPACE_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.WORKSPACE_SPEC == parser.name
+ ) {
+ val attrs =
+ resourceHelper.obtainStyledAttributes(
+ Xml.asAttributeSet(parser),
+ R.styleable.WorkspaceSpec
+ )
+ val maxAvailableSize =
+ attrs.getDimensionPixelSize(
+ R.styleable.WorkspaceSpec_maxAvailableSize,
+ 0
+ )
+ val specType =
+ WorkspaceSpec.SpecType.values()[
+ attrs.getInt(
+ R.styleable.WorkspaceSpec_specType,
+ WorkspaceSpec.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(resourceHelper, attr)
+ }
+ XmlTags.END_PADDING -> {
+ endPadding = SizeSpec(resourceHelper, attr)
+ }
+ XmlTags.GUTTER -> {
+ gutter = SizeSpec(resourceHelper, attr)
+ }
+ XmlTags.CELL_SIZE -> {
+ cellSize = SizeSpec(resourceHelper, attr)
+ }
+ }
+ }
+ }
+
+ if (
+ startPadding == null ||
+ endPadding == null ||
+ gutter == null ||
+ cellSize == null
+ ) {
+ throw IllegalStateException(
+ "All attributes in workspaceSpec must be defined"
+ )
+ }
+
+ val workspaceSpec =
+ WorkspaceSpec(
+ maxAvailableSize,
+ specType,
+ startPadding,
+ endPadding,
+ gutter,
+ cellSize
+ )
+ if (workspaceSpec.isValid()) {
+ if (workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT)
+ workspaceHeightSpecList.add(workspaceSpec)
+ else workspaceWidthSpecList.add(workspaceSpec)
+ } else {
+ throw IllegalStateException("Invalid workspaceSpec found.")
+ }
+ }
+ }
+
+ if (workspaceWidthSpecList.isEmpty() || workspaceHeightSpecList.isEmpty()) {
+ throw IllegalStateException(
+ "WorkspaceSpecs is incomplete - " +
+ "height list size = ${workspaceHeightSpecList.size}; " +
+ "width list size = ${workspaceWidthSpecList.size}."
+ )
+ }
+ }
+ }
+ parser.close()
+ } catch (e: Exception) {
+ when (e) {
+ is IOException,
+ is XmlPullParserException -> {
+ throw RuntimeException("Failure parsing workspaces specs file.", e)
+ }
+ else -> throw e
+ }
+ }
+ }
+}
+
+data class WorkspaceSpec(
+ 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(TAG, "WorkspaceSpec#isValid - maxAvailableSize <= 0")
+ return false
+ }
+
+ // All specs need to be individually valid
+ if (!allSpecsAreValid()) {
+ Log.e(TAG, "WorkspaceSpec#isValid - !allSpecsAreValid()")
+ return false
+ }
+
+ return true
+ }
+
+ 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)"
+ }
+}
diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml
new file mode 100644
index 0000000..2310d9e
--- /dev/null
+++ b/tests/res/values/attrs.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+/* Copyright 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.
+*/
+-->
+
+<!-- Attributes have to be copied to test for correct parsing of files -->
+<resources>
+ <!-- Responsive grids attributes -->
+ <declare-styleable name="WorkspaceSpec">
+ <attr name="specType" format="integer">
+ <enum name="height" value="0" />
+ <enum name="width" value="1" />
+ </attr>
+ <attr name="maxAvailableSize" format="dimension" />
+ </declare-styleable>
+
+ <declare-styleable name="SpecSize">
+ <attr name="fixedSize" format="dimension" />
+ <attr name="ofAvailableSpace" format="float" />
+ <attr name="ofRemainderSpace" format="float" />
+ </declare-styleable>
+
+</resources>
diff --git a/tests/res/xml/invalid_workspace_file_case_1.xml b/tests/res/xml/invalid_workspace_file_case_1.xml
new file mode 100644
index 0000000..0be704b
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_1.xml
@@ -0,0 +1,56 @@
+<?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">
+ <!-- missing startPadding -->
+ <endPadding
+ launcher:ofAvailableSpace="0.05" />
+ <gutter
+ launcher:fixedSize="16dp" />
+ <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/res/xml/invalid_workspace_file_case_2.xml b/tests/res/xml/invalid_workspace_file_case_2.xml
new file mode 100644
index 0000000..5a37d97
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_2.xml
@@ -0,0 +1,59 @@
+<?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" />
+ <!-- more than 1 value in one tag -->
+ <gutter
+ launcher:ofAvailableSpace="0.0125"
+ launcher:fixedSize="16dp" />
+ <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/res/xml/invalid_workspace_file_case_3.xml b/tests/res/xml/invalid_workspace_file_case_3.xml
new file mode 100644
index 0000000..3e68edb
--- /dev/null
+++ b/tests/res/xml/invalid_workspace_file_case_3.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" />
+ <gutter
+ launcher:fixedSize="16dp" />
+ <!-- value bigger than 1 -->
+ <cellSize
+ launcher:ofRemainderSpace="1.001" />
+ </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/res/xml/valid_workspace_file.xml b/tests/res/xml/valid_workspace_file.xml
new file mode 100644
index 0000000..91a3e48
--- /dev/null
+++ b/tests/res/xml/valid_workspace_file.xml
@@ -0,0 +1,57 @@
+<?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" />
+ <gutter
+ launcher:fixedSize="16dp" />
+ <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/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
new file mode 100644
index 0000000..dcc669b
--- /dev/null
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -0,0 +1,272 @@
+/*
+ * 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
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Point
+import android.graphics.Rect
+import android.util.DisplayMetrics
+import android.view.Surface
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.NavigationMode
+import com.android.launcher3.util.WindowBounds
+import com.android.launcher3.util.window.CachedDisplayInfo
+import com.android.launcher3.util.window.WindowManagerProxy
+import kotlin.math.max
+import kotlin.math.min
+import org.junit.After
+import org.junit.Before
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when` as whenever
+
+/**
+ * This is an abstract class for DeviceProfile tests that create an InvariantDeviceProfile based on
+ * a real device spec.
+ *
+ * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
+ */
+abstract class AbstractDeviceProfileTest {
+ protected var context: Context? = null
+ protected open val runningContext: Context = ApplicationProvider.getApplicationContext()
+ private var displayController: DisplayController = mock(DisplayController::class.java)
+ private var windowManagerProxy: WindowManagerProxy = mock(WindowManagerProxy::class.java)
+ private lateinit var originalDisplayController: DisplayController
+ private lateinit var originalWindowManagerProxy: WindowManagerProxy
+
+ @Before
+ fun setUp() {
+ val appContext: Context = ApplicationProvider.getApplicationContext()
+ originalWindowManagerProxy = WindowManagerProxy.INSTANCE.get(appContext)
+ originalDisplayController = DisplayController.INSTANCE.get(appContext)
+ WindowManagerProxy.INSTANCE.initializeForTesting(windowManagerProxy)
+ DisplayController.INSTANCE.initializeForTesting(displayController)
+ }
+
+ @After
+ fun tearDown() {
+ WindowManagerProxy.INSTANCE.initializeForTesting(originalWindowManagerProxy)
+ DisplayController.INSTANCE.initializeForTesting(originalDisplayController)
+ }
+
+ class DeviceSpec(
+ val naturalSize: Pair<Int, Int>,
+ val densityDpi: Int,
+ val statusBarNaturalPx: Int,
+ val statusBarRotatedPx: Int,
+ val gesturePx: Int,
+ val cutoutPx: Int
+ )
+
+ open val deviceSpecs =
+ mapOf(
+ "phone" to
+ DeviceSpec(
+ Pair(1080, 2400),
+ densityDpi = 420,
+ statusBarNaturalPx = 118,
+ statusBarRotatedPx = 74,
+ gesturePx = 63,
+ cutoutPx = 118
+ ),
+ "tablet" to
+ DeviceSpec(
+ Pair(2560, 1600),
+ densityDpi = 320,
+ statusBarNaturalPx = 104,
+ statusBarRotatedPx = 104,
+ gesturePx = 0,
+ cutoutPx = 0
+ ),
+ )
+
+ protected fun initializeVarsForPhone(
+ deviceSpec: DeviceSpec,
+ isGestureMode: Boolean = true,
+ isVerticalBar: Boolean = false
+ ) {
+ val (naturalX, naturalY) = deviceSpec.naturalSize
+ val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
+ val displayInfo =
+ CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+ val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
+
+ initializeCommonVars(
+ perDisplayBoundsCache,
+ displayInfo,
+ rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0,
+ isGestureMode,
+ densityDpi = deviceSpec.densityDpi
+ )
+ }
+
+ protected fun initializeVarsForTablet(
+ deviceSpec: DeviceSpec,
+ isLandscape: Boolean = false,
+ isGestureMode: Boolean = true
+ ) {
+ val (naturalX, naturalY) = deviceSpec.naturalSize
+ val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
+ val displayInfo =
+ CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0))
+ val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds)
+
+ initializeCommonVars(
+ perDisplayBoundsCache,
+ displayInfo,
+ rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
+ isGestureMode,
+ densityDpi = deviceSpec.densityDpi
+ )
+ }
+
+ protected fun initializeVarsForTwoPanel(
+ deviceTabletSpec: DeviceSpec,
+ deviceSpec: DeviceSpec,
+ isLandscape: Boolean = false,
+ isGestureMode: Boolean = true
+ ) {
+ val (tabletNaturalX, tabletNaturalY) = deviceTabletSpec.naturalSize
+ val tabletWindowsBounds =
+ tabletWindowsBounds(deviceTabletSpec, tabletNaturalX, tabletNaturalY)
+ val tabletDisplayInfo =
+ CachedDisplayInfo(
+ Point(tabletNaturalX, tabletNaturalY),
+ Surface.ROTATION_0,
+ Rect(0, 0, 0, 0)
+ )
+
+ val (phoneNaturalX, phoneNaturalY) = deviceSpec.naturalSize
+ val phoneWindowsBounds =
+ phoneWindowsBounds(deviceSpec, isGestureMode, phoneNaturalX, phoneNaturalY)
+ val phoneDisplayInfo =
+ CachedDisplayInfo(
+ Point(phoneNaturalX, phoneNaturalY),
+ Surface.ROTATION_0,
+ Rect(0, 0, 0, 0)
+ )
+
+ val perDisplayBoundsCache =
+ mapOf(tabletDisplayInfo to tabletWindowsBounds, phoneDisplayInfo to phoneWindowsBounds)
+
+ initializeCommonVars(
+ perDisplayBoundsCache,
+ tabletDisplayInfo,
+ rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
+ isGestureMode,
+ densityDpi = deviceTabletSpec.densityDpi
+ )
+ }
+
+ private fun phoneWindowsBounds(
+ deviceSpec: DeviceSpec,
+ isGestureMode: Boolean,
+ naturalX: Int,
+ naturalY: Int
+ ): Array<WindowBounds> {
+ val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
+
+ val rotation0Insets =
+ Rect(
+ 0,
+ max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx),
+ 0,
+ if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight
+ )
+ val rotation90Insets =
+ Rect(
+ deviceSpec.cutoutPx,
+ deviceSpec.statusBarRotatedPx,
+ if (isGestureMode) 0 else buttonsNavHeight,
+ if (isGestureMode) deviceSpec.gesturePx else 0
+ )
+ val rotation180Insets =
+ Rect(
+ 0,
+ deviceSpec.statusBarNaturalPx,
+ 0,
+ max(
+ if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
+ deviceSpec.cutoutPx
+ )
+ )
+ val rotation270Insets =
+ Rect(
+ if (isGestureMode) 0 else buttonsNavHeight,
+ deviceSpec.statusBarRotatedPx,
+ deviceSpec.cutoutPx,
+ if (isGestureMode) deviceSpec.gesturePx else 0
+ )
+
+ return arrayOf(
+ WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0),
+ WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90),
+ WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180),
+ WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270)
+ )
+ }
+
+ private fun tabletWindowsBounds(
+ deviceSpec: DeviceSpec,
+ naturalX: Int,
+ naturalY: Int
+ ): Array<WindowBounds> {
+ val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
+ val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
+
+ return arrayOf(
+ WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0),
+ WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90),
+ WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180),
+ WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270)
+ )
+ }
+
+ private fun initializeCommonVars(
+ perDisplayBoundsCache: Map<CachedDisplayInfo, Array<WindowBounds>>,
+ displayInfo: CachedDisplayInfo,
+ rotation: Int,
+ isGestureMode: Boolean = true,
+ densityDpi: Int
+ ) {
+ val windowsBounds = perDisplayBoundsCache[displayInfo]!!
+ val realBounds = windowsBounds[rotation]
+ whenever(windowManagerProxy.getDisplayInfo(ArgumentMatchers.any())).thenReturn(displayInfo)
+ whenever(windowManagerProxy.getRealBounds(ArgumentMatchers.any(), ArgumentMatchers.any()))
+ .thenReturn(realBounds)
+ whenever(windowManagerProxy.getRotation(ArgumentMatchers.any())).thenReturn(rotation)
+ whenever(windowManagerProxy.getNavigationMode(ArgumentMatchers.any()))
+ .thenReturn(
+ if (isGestureMode) NavigationMode.NO_BUTTON else NavigationMode.THREE_BUTTONS
+ )
+
+ val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat()
+ val config =
+ Configuration(runningContext.resources.configuration).apply {
+ this.densityDpi = densityDpi
+ screenWidthDp = (realBounds.bounds.width() / density).toInt()
+ screenHeightDp = (realBounds.bounds.height() / density).toInt()
+ smallestScreenWidthDp = min(screenWidthDp, screenHeightDp)
+ }
+ context = runningContext.createConfigurationContext(config)
+
+ val info = DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache)
+ whenever(displayController.info).thenReturn(info)
+ whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode)
+ }
+}
diff --git a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
similarity index 96%
rename from tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
rename to tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index daf4608..a5f33c0 100644
--- a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
+++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -31,7 +31,13 @@
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
-abstract class DeviceProfileBaseTest {
+/**
+ * This is an abstract class for DeviceProfile tests that don't need the real Context and mock an
+ * InvariantDeviceProfile instead of creating one based on real values.
+ *
+ * For an implementation that creates InvariantDeviceProfile, use [AbstractDeviceProfileTest]
+ */
+abstract class FakeInvariantDeviceProfileTest {
protected var context: Context? = null
protected var inv: InvariantDeviceProfile? = null
diff --git a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
index 951f5f8..2a27487 100644
--- a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
@@ -18,7 +18,7 @@
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.launcher3.DeviceProfileBaseTest
+import com.android.launcher3.FakeInvariantDeviceProfileTest
import com.android.launcher3.util.WindowBounds
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -26,7 +26,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class HotseatWidthCalculationTest : DeviceProfileBaseTest() {
+class HotseatWidthCalculationTest : FakeInvariantDeviceProfileTest() {
/**
* This is a case when after setting the hotseat, the space needs to be recalculated but it
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
new file mode 100644
index 0000000..fb03fe1
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import com.android.launcher3.R
+import com.android.launcher3.tests.R as TestR
+import kotlin.IntArray
+
+class TestResourceHelper(private val context: Context, private val specsFileId: Int) :
+ ResourceHelper(context, specsFileId) {
+ override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
+ var clone = styleId.clone()
+ if (styleId == R.styleable.SpecSize) clone = TestR.styleable.SpecSize
+ 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
new file mode 100644
index 0000000..0fd8a54
--- /dev/null
+++ b/tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.workspace
+
+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 WorkspaceSpecsTest : AbstractDeviceProfileTest() {
+ override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
+
+ @Before
+ fun setup() {
+ initializeVarsForPhone(deviceSpecs["phone"]!!)
+ }
+
+ @Test
+ fun parseValidFile() {
+ val workspaceSpecs =
+ WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.valid_workspace_file))
+ assertThat(workspaceSpecs.workspaceHeightSpecList.size).isEqualTo(2)
+ assertThat(workspaceSpecs.workspaceHeightSpecList[0].toString())
+ .isEqualTo(
+ "WorkspaceSpec(" +
+ "maxAvailableSize=1701, " +
+ "specType=HEIGHT, " +
+ "startPadding=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.0125, " +
+ "ofRemainderSpace=0.0), " +
+ "endPadding=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.05, " +
+ "ofRemainderSpace=0.0), " +
+ "gutter=SizeSpec(fixedSize=42.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.0), " +
+ "cellSize=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.2)" +
+ ")"
+ )
+ assertThat(workspaceSpecs.workspaceHeightSpecList[1].toString())
+ .isEqualTo(
+ "WorkspaceSpec(" +
+ "maxAvailableSize=26247, " +
+ "specType=HEIGHT, " +
+ "startPadding=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.0306, " +
+ "ofRemainderSpace=0.0), " +
+ "endPadding=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.068, " +
+ "ofRemainderSpace=0.0), " +
+ "gutter=SizeSpec(fixedSize=42.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.0), " +
+ "cellSize=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.2)" +
+ ")"
+ )
+ assertThat(workspaceSpecs.workspaceWidthSpecList.size).isEqualTo(1)
+ assertThat(workspaceSpecs.workspaceWidthSpecList[0].toString())
+ .isEqualTo(
+ "WorkspaceSpec(" +
+ "maxAvailableSize=26247, " +
+ "specType=WIDTH, " +
+ "startPadding=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.21436226), " +
+ "endPadding=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.21436226), " +
+ "gutter=SizeSpec(fixedSize=0.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.11425509), " +
+ "cellSize=SizeSpec(fixedSize=315.0, " +
+ "ofAvailableSpace=0.0, " +
+ "ofRemainderSpace=0.0)" +
+ ")"
+ )
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun parseInvalidFile_missingTag_throwsError() {
+ WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_1))
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
+ WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_2))
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun parseInvalidFile_valueBiggerThan1_throwsError() {
+ WorkspaceSpecs(TestResourceHelper(context!!, TestR.xml.invalid_workspace_file_case_3))
+ }
+}