From 7ae9e3a6f53d7fcd8a60b2a9777532c0b4744171 Mon Sep 17 00:00:00 2001 From: Jordan Silva Date: Thu, 8 Jun 2023 14:47:57 +0100 Subject: [PATCH] Add FolderSpec for responsive grid support Adds responsive grid implementation for folders. It follows the same concept as WorkspaceSpecs, so we have a parser similar to WorkspaceSpecs for FolderSpecs. Bug: 284155638 Flag: ENABLE_RESPONSIVE_WORKSPACE Test: FolderSpecsTest Test: CalculatedFolderSpecsTest Change-Id: Iea6d7d88ef42d1273aed7cf2ed5b397035518a52 --- res/values/attrs.xml | 5 + .../launcher3/responsive/FolderSpecs.kt | 280 ++++++++++++++++++ .../android/launcher3/responsive/SizeSpec.kt | 42 ++- .../launcher3/workspace/WorkspaceSpecs.kt | 1 + tests/res/values/attrs.xml | 6 + tests/res/xml/invalid_folders_specs_1.xml | 40 +++ tests/res/xml/invalid_folders_specs_2.xml | 43 +++ tests/res/xml/invalid_folders_specs_3.xml | 41 +++ tests/res/xml/invalid_folders_specs_4.xml | 24 ++ tests/res/xml/invalid_folders_specs_5.xml | 33 +++ tests/res/xml/valid_folders_specs.xml | 38 +++ .../responsive/CalculatedFolderSpecsTest.kt | 126 ++++++++ .../launcher3/responsive/FolderSpecsTest.kt | 269 +++++++++++++++++ .../launcher3/responsive/SizeSpecTest.kt | 37 +++ .../launcher3/util/TestResourceHelper.kt | 13 +- 15 files changed, 989 insertions(+), 9 deletions(-) create mode 100644 src/com/android/launcher3/responsive/FolderSpecs.kt create mode 100644 tests/res/xml/invalid_folders_specs_1.xml create mode 100644 tests/res/xml/invalid_folders_specs_2.xml create mode 100644 tests/res/xml/invalid_folders_specs_3.xml create mode 100644 tests/res/xml/invalid_folders_specs_4.xml create mode 100644 tests/res/xml/invalid_folders_specs_5.xml create mode 100644 tests/res/xml/valid_folders_specs.xml create mode 100644 tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt create mode 100644 tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 80df78ae54..1be1a1a13d 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -259,6 +259,11 @@ + + + + + diff --git a/src/com/android/launcher3/responsive/FolderSpecs.kt b/src/com/android/launcher3/responsive/FolderSpecs.kt new file mode 100644 index 0000000000..be97cf8475 --- /dev/null +++ b/src/com/android/launcher3/responsive/FolderSpecs.kt @@ -0,0 +1,280 @@ +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.responsive.FolderSpec.* +import com.android.launcher3.util.ResourceHelper +import com.android.launcher3.workspace.CalculatedWorkspaceSpec +import com.android.launcher3.workspace.WorkspaceSpec +import java.io.IOException +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException + +private const val LOG_TAG = "FolderSpecs" + +class FolderSpecs(resourceHelper: ResourceHelper) { + + object XmlTags { + const val FOLDER_SPECS = "folderSpecs" + + const val FOLDER_SPEC = "folderSpec" + const val START_PADDING = "startPadding" + const val END_PADDING = "endPadding" + const val GUTTER = "gutter" + const val CELL_SIZE = "cellSize" + } + + private val _heightSpecs = mutableListOf() + val heightSpecs: List + get() = _heightSpecs + + private val _widthSpecs = mutableListOf() + val widthSpecs: List + get() = _widthSpecs + + // 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.FOLDER_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.FOLDER_SPEC == parser.name) { + val attrs = + resourceHelper.obtainStyledAttributes( + Xml.asAttributeSet(parser), + R.styleable.FolderSpec + ) + val maxAvailableSize = + attrs.getDimensionPixelSize( + R.styleable.FolderSpec_maxAvailableSize, + 0 + ) + val specType = + SpecType.values()[ + attrs.getInt( + R.styleable.FolderSpec_specType, + 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) { + val sizeSpec = SizeSpec.create(resourceHelper, attr) + when (parser.name) { + XmlTags.START_PADDING -> startPadding = sizeSpec + XmlTags.END_PADDING -> endPadding = sizeSpec + XmlTags.GUTTER -> gutter = sizeSpec + XmlTags.CELL_SIZE -> cellSize = sizeSpec + } + } + } + + checkNotNull(startPadding) { + "Attr 'startPadding' in FolderSpec must be defined." + } + checkNotNull(endPadding) { + "Attr 'endPadding' in FolderSpec must be defined." + } + checkNotNull(gutter) { "Attr 'gutter' in FolderSpec must be defined." } + checkNotNull(cellSize) { + "Attr 'cellSize' in FolderSpec must be defined." + } + + val folderSpec = + FolderSpec( + maxAvailableSize, + specType, + startPadding, + endPadding, + gutter, + cellSize + ) + + check(folderSpec.isValid()) { "Invalid FolderSpec found." } + + if (folderSpec.specType == SpecType.HEIGHT) { + _heightSpecs += folderSpec + } else { + _widthSpecs += folderSpec + } + } + } + + check(_widthSpecs.isNotEmpty() && _heightSpecs.isNotEmpty()) { + "FolderSpecs is incomplete - " + + "height list size = ${_heightSpecs.size}; " + + "width list size = ${_widthSpecs.size}." + } + } + } + } catch (e: Exception) { + when (e) { + is IOException, + is XmlPullParserException -> { + throw RuntimeException("Failure parsing folder specs file.", e) + } + else -> throw e + } + } finally { + parser?.close() + } + } + + /** + * Returns the [CalculatedFolderSpec] for width, based on the available width, FolderSpecs and + * WorkspaceSpecs. + */ + fun getWidthSpec( + columns: Int, + availableWidth: Int, + workspaceSpec: CalculatedWorkspaceSpec + ): CalculatedFolderSpec { + check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.WIDTH) { + "Invalid specType for CalculatedWorkspaceSpec. " + + "Expected: ${WorkspaceSpec.SpecType.WIDTH} - " + + "Found: ${workspaceSpec.workspaceSpec.specType}}" + } + + val widthSpec = _widthSpecs.firstOrNull { availableWidth <= it.maxAvailableSize } + check(widthSpec != null) { "No FolderSpec for width spec found with $availableWidth." } + + return convertToCalculatedFolderSpec(widthSpec, availableWidth, columns, workspaceSpec) + } + + /** + * Returns the [CalculatedFolderSpec] for height, based on the available height, FolderSpecs and + * WorkspaceSpecs. + */ + fun getHeightSpec( + rows: Int, + availableHeight: Int, + workspaceSpec: CalculatedWorkspaceSpec + ): CalculatedFolderSpec { + check(workspaceSpec.workspaceSpec.specType == WorkspaceSpec.SpecType.HEIGHT) { + "Invalid specType for CalculatedWorkspaceSpec. " + + "Expected: ${WorkspaceSpec.SpecType.HEIGHT} - " + + "Found: ${workspaceSpec.workspaceSpec.specType}}" + } + + val heightSpec = _heightSpecs.firstOrNull { availableHeight <= it.maxAvailableSize } + check(heightSpec != null) { "No FolderSpec for height spec found with $availableHeight." } + + return convertToCalculatedFolderSpec(heightSpec, availableHeight, rows, workspaceSpec) + } +} + +data class CalculatedFolderSpec( + val startPaddingPx: Int, + val endPaddingPx: Int, + val gutterPx: Int, + val cellSizePx: Int, + val availableSpace: Int, + val cells: Int +) + +/** + * Responsive folder specs to be used to calculate the paddings, gutter and cell size for folders in + * the workspace. + * + * @param maxAvailableSize indicates the breakpoint to use this specification. + * @param specType indicates whether the paddings and gutters will be applied vertically or + * horizontally. + * @param startPadding padding used at the top or left (right in RTL) in the workspace folder. + * @param endPadding padding used at the bottom or right (left in RTL) in the workspace folder. + * @param gutter the space between the cells vertically or horizontally depending on the [specType]. + * @param cellSize height or width of the cell depending on the [specType]. + */ +data class FolderSpec( + 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, "FolderSpec#isValid - maxAvailableSize <= 0") + return false + } + + // All specs are valid + if ( + !(startPadding.isValid() && + endPadding.isValid() && + gutter.isValid() && + cellSize.isValid()) + ) { + Log.e(LOG_TAG, "FolderSpec#isValid - !allSpecsAreValid()") + return false + } + + return true + } +} + +/** Helper function to convert [FolderSpec] to [CalculatedFolderSpec] */ +private fun convertToCalculatedFolderSpec( + folderSpec: FolderSpec, + availableSpace: Int, + cells: Int, + workspaceSpec: CalculatedWorkspaceSpec +): CalculatedFolderSpec { + // Map if is fixedSize, ofAvailableSpace or matchWorkspace + var startPaddingPx = + folderSpec.startPadding.getCalculatedValue(availableSpace, workspaceSpec.startPaddingPx) + var endPaddingPx = + folderSpec.endPadding.getCalculatedValue(availableSpace, workspaceSpec.endPaddingPx) + var gutterPx = folderSpec.gutter.getCalculatedValue(availableSpace, workspaceSpec.gutterPx) + var cellSizePx = + folderSpec.cellSize.getCalculatedValue(availableSpace, workspaceSpec.cellSizePx) + + // Remainder space + val gutters = cells - 1 + val usedSpace = startPaddingPx + endPaddingPx + (gutterPx * gutters) + (cellSizePx * cells) + val remainderSpace = availableSpace - usedSpace + + startPaddingPx = folderSpec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx) + endPaddingPx = folderSpec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx) + gutterPx = folderSpec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx) + cellSizePx = folderSpec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx) + + return CalculatedFolderSpec( + startPaddingPx = startPaddingPx, + endPaddingPx = endPaddingPx, + gutterPx = gutterPx, + cellSizePx = cellSizePx, + availableSpace = availableSpace, + cells = cells + ) +} diff --git a/src/com/android/launcher3/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt index bf5ca1cb89..407a21231b 100644 --- a/src/com/android/launcher3/responsive/SizeSpec.kt +++ b/src/com/android/launcher3/responsive/SizeSpec.kt @@ -6,14 +6,45 @@ import android.util.Log import android.util.TypedValue import com.android.launcher3.R import com.android.launcher3.util.ResourceHelper +import kotlin.math.roundToInt +/** + * [SizeSpec] is an attribute used to represent a property in the responsive grid specs. + * + * @param fixedSize a fixed size in dp to be used + * @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. + */ data class SizeSpec( - val fixedSize: Float, - val ofAvailableSpace: Float, - val ofRemainderSpace: Float, - val matchWorkspace: Boolean + val fixedSize: Float = 0f, + val ofAvailableSpace: Float = 0f, + val ofRemainderSpace: Float = 0f, + val matchWorkspace: Boolean = false ) { + /** 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 + } + } + + /** + * Calculates the [SizeSpec] value when remainder space value is defined. If no remainderSpace + * is 0, returns a default value. + */ + fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int): Int { + return if (ofRemainderSpace > 0) { + (ofRemainderSpace * remainderSpace).roundToInt() + } else { + defaultValue + } + } + fun isValid(): Boolean { // All attributes are empty if (fixedSize < 0f && ofAvailableSpace <= 0f && ofRemainderSpace <= 0f && !matchWorkspace) { @@ -48,7 +79,8 @@ data class SizeSpec( } companion object { - private const val TAG = "WorkspaceSpecs::SizeSpec" + private const val TAG = "SizeSpec" + private fun getValue(a: TypedArray, index: Int): Float { return when (a.getType(index)) { TypedValue.TYPE_DIMENSION -> a.getDimensionPixelSize(index, 0).toFloat() diff --git a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt index 48159249a0..8cc0c5936a 100644 --- a/src/com/android/launcher3/workspace/WorkspaceSpecs.kt +++ b/src/com/android/launcher3/workspace/WorkspaceSpecs.kt @@ -44,6 +44,7 @@ class WorkspaceSpecs(resourceHelper: ResourceHelper) { val workspaceHeightSpecList = mutableListOf() val workspaceWidthSpecList = mutableListOf() + // TODO(b/286538013) Remove this init after a more generic or reusable parser is created init { try { val parser: XmlResourceParser = resourceHelper.getXml() diff --git a/tests/res/values/attrs.xml b/tests/res/values/attrs.xml index cb6da3ba3f..54f038110b 100644 --- a/tests/res/values/attrs.xml +++ b/tests/res/values/attrs.xml @@ -32,4 +32,10 @@ + + + + + + diff --git a/tests/res/xml/invalid_folders_specs_1.xml b/tests/res/xml/invalid_folders_specs_1.xml new file mode 100644 index 0000000000..0864249f02 --- /dev/null +++ b/tests/res/xml/invalid_folders_specs_1.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/invalid_folders_specs_2.xml b/tests/res/xml/invalid_folders_specs_2.xml new file mode 100644 index 0000000000..0b7dd627d6 --- /dev/null +++ b/tests/res/xml/invalid_folders_specs_2.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/invalid_folders_specs_3.xml b/tests/res/xml/invalid_folders_specs_3.xml new file mode 100644 index 0000000000..83fd3e167f --- /dev/null +++ b/tests/res/xml/invalid_folders_specs_3.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/invalid_folders_specs_4.xml b/tests/res/xml/invalid_folders_specs_4.xml new file mode 100644 index 0000000000..2d8c730164 --- /dev/null +++ b/tests/res/xml/invalid_folders_specs_4.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/tests/res/xml/invalid_folders_specs_5.xml b/tests/res/xml/invalid_folders_specs_5.xml new file mode 100644 index 0000000000..b4f1f4dce9 --- /dev/null +++ b/tests/res/xml/invalid_folders_specs_5.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/valid_folders_specs.xml b/tests/res/xml/valid_folders_specs.xml new file mode 100644 index 0000000000..0c45544752 --- /dev/null +++ b/tests/res/xml/valid_folders_specs.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt new file mode 100644 index 0000000000..c14722c828 --- /dev/null +++ b/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecsTest.kt @@ -0,0 +1,126 @@ +/* + * 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.testing.shared.ResourceUtils +import com.android.launcher3.tests.R +import com.android.launcher3.util.TestResourceHelper +import com.android.launcher3.workspace.WorkspaceSpecs +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 CalculatedFolderSpecsTest : AbstractDeviceProfileTest() { + override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context + + private val deviceSpec = deviceSpecs["phone"]!! + + @Before + fun setup() { + initializeVarsForPhone(deviceSpec) + } + + @Test + fun validate_matchWidthWorkspace() { + val columns = 6 + + // Loading workspace specs + val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file) + val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace) + + // Loading folders specs + val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs) + val folderSpecs = FolderSpecs(resourceHelperFolder) + + assertThat(folderSpecs.widthSpecs.size).isEqualTo(2) + assertThat(folderSpecs.widthSpecs[0].cellSize.matchWorkspace).isEqualTo(true) + assertThat(folderSpecs.widthSpecs[1].cellSize.matchWorkspace).isEqualTo(false) + + // Validate width spec <= 800 + var availableWidth = deviceSpec.naturalSize.first + var calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth) + var calculatedWidthFolderSpec = + folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace) + with(calculatedWidthFolderSpec) { + assertThat(availableSpace).isEqualTo(availableWidth) + assertThat(cells).isEqualTo(columns) + assertThat(startPaddingPx).isEqualTo(16.dpToPx()) + assertThat(endPaddingPx).isEqualTo(16.dpToPx()) + assertThat(gutterPx).isEqualTo(16.dpToPx()) + assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx) + } + + // Validate width spec > 800 + availableWidth = 2000.dpToPx() + calculatedWorkspace = workspaceSpecs.getCalculatedWidthSpec(columns, availableWidth) + calculatedWidthFolderSpec = + folderSpecs.getWidthSpec(columns, availableWidth, calculatedWorkspace) + with(calculatedWidthFolderSpec) { + assertThat(availableSpace).isEqualTo(availableWidth) + assertThat(cells).isEqualTo(columns) + assertThat(startPaddingPx).isEqualTo(16.dpToPx()) + assertThat(endPaddingPx).isEqualTo(16.dpToPx()) + assertThat(gutterPx).isEqualTo(16.dpToPx()) + assertThat(cellSizePx).isEqualTo(102.dpToPx()) + } + } + + @Test + fun validate_matchHeightWorkspace() { + // Hotseat is roughly 495px on a real device, it doesn't need to be precise on unit tests + val hotseatSize = 495 + val statusBarHeight = deviceSpec.statusBarNaturalPx + val availableHeight = deviceSpec.naturalSize.second - statusBarHeight - hotseatSize + val rows = 5 + + // Loading workspace specs + val resourceHelperWorkspace = TestResourceHelper(context!!, R.xml.valid_workspace_file) + val workspaceSpecs = WorkspaceSpecs(resourceHelperWorkspace) + + // Loading folders specs + val resourceHelperFolder = TestResourceHelper(context!!, R.xml.valid_folders_specs) + val folderSpecs = FolderSpecs(resourceHelperFolder) + + assertThat(folderSpecs.heightSpecs.size).isEqualTo(1) + assertThat(folderSpecs.heightSpecs[0].cellSize.matchWorkspace).isEqualTo(true) + + // Validate height spec + val calculatedWorkspace = workspaceSpecs.getCalculatedHeightSpec(rows, availableHeight) + val calculatedFolderSpec = + folderSpecs.getHeightSpec(rows, availableHeight, calculatedWorkspace) + with(calculatedFolderSpec) { + assertThat(availableSpace).isEqualTo(availableHeight) + assertThat(cells).isEqualTo(rows) + assertThat(startPaddingPx).isEqualTo(24.dpToPx()) + assertThat(endPaddingPx).isEqualTo(64.dpToPx()) + assertThat(gutterPx).isEqualTo(16.dpToPx()) + assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx) + } + } + + private fun Int.dpToPx(): Int { + return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics) + } +} diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt new file mode 100644 index 0000000000..796bf9adce --- /dev/null +++ b/tests/src/com/android/launcher3/responsive/FolderSpecsTest.kt @@ -0,0 +1,269 @@ +/* + * 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.testing.shared.ResourceUtils +import com.android.launcher3.tests.R +import com.android.launcher3.util.TestResourceHelper +import com.android.launcher3.workspace.CalculatedWorkspaceSpec +import com.android.launcher3.workspace.WorkspaceSpec +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 FolderSpecsTest : AbstractDeviceProfileTest() { + override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context + + @Before + fun setup() { + initializeVarsForPhone(deviceSpecs["tablet"]!!) + } + + @Test + fun parseValidFile() { + val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs) + val folderSpecs = FolderSpecs(resourceHelper) + + val sizeSpec16 = SizeSpec(16f.dpToPx()) + val widthSpecsExpected = + listOf( + FolderSpec( + maxAvailableSize = 800.dpToPx(), + specType = FolderSpec.SpecType.WIDTH, + startPadding = sizeSpec16, + endPadding = sizeSpec16, + gutter = sizeSpec16, + cellSize = SizeSpec(matchWorkspace = true) + ), + FolderSpec( + maxAvailableSize = 9999.dpToPx(), + specType = FolderSpec.SpecType.WIDTH, + startPadding = sizeSpec16, + endPadding = sizeSpec16, + gutter = sizeSpec16, + cellSize = SizeSpec(102f.dpToPx()) + ) + ) + + val heightSpecsExpected = + FolderSpec( + maxAvailableSize = 9999.dpToPx(), + specType = FolderSpec.SpecType.HEIGHT, + startPadding = SizeSpec(24f.dpToPx()), + endPadding = SizeSpec(64f.dpToPx()), + gutter = sizeSpec16, + cellSize = SizeSpec(matchWorkspace = true) + ) + + assertThat(folderSpecs.widthSpecs.size).isEqualTo(widthSpecsExpected.size) + assertThat(folderSpecs.widthSpecs[0]).isEqualTo(widthSpecsExpected[0]) + assertThat(folderSpecs.widthSpecs[1]).isEqualTo(widthSpecsExpected[1]) + + assertThat(folderSpecs.heightSpecs.size).isEqualTo(1) + assertThat(folderSpecs.heightSpecs[0]).isEqualTo(heightSpecsExpected) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_missingTag_throwsError() { + val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_1) + FolderSpecs(resourceHelper) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_moreThanOneValuePerTag_throwsError() { + val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_2) + FolderSpecs(resourceHelper) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_valueBiggerThan1_throwsError() { + val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_3) + FolderSpecs(resourceHelper) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_missingSpecs_throwsError() { + val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_4) + FolderSpecs(resourceHelper) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_missingWidthBreakpoint_throwsError() { + val availableSpace = 900.dpToPx() + val cells = 3 + + val workspaceSpec = + WorkspaceSpec( + maxAvailableSize = availableSpace, + specType = WorkspaceSpec.SpecType.WIDTH, + startPadding = SizeSpec(fixedSize = 10f), + endPadding = SizeSpec(fixedSize = 10f), + gutter = SizeSpec(fixedSize = 10f), + cellSize = SizeSpec(fixedSize = 10f) + ) + val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec) + + val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5) + val folderSpecs = FolderSpecs(resourceHelper) + folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec) + } + + @Test(expected = IllegalStateException::class) + fun parseInvalidFile_missingHeightBreakpoint_throwsError() { + val availableSpace = 900.dpToPx() + val cells = 3 + + val workspaceSpec = + WorkspaceSpec( + maxAvailableSize = availableSpace, + specType = WorkspaceSpec.SpecType.HEIGHT, + startPadding = SizeSpec(fixedSize = 10f), + endPadding = SizeSpec(fixedSize = 10f), + gutter = SizeSpec(fixedSize = 10f), + cellSize = SizeSpec(fixedSize = 10f) + ) + val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec) + + val resourceHelper = TestResourceHelper(context!!, R.xml.invalid_folders_specs_5) + val folderSpecs = FolderSpecs(resourceHelper) + folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec) + } + + @Test + fun retrievesCalculatedWidthSpec() { + val availableSpace = 800.dpToPx() + val cells = 3 + + val workspaceSpec = + WorkspaceSpec( + maxAvailableSize = availableSpace, + specType = WorkspaceSpec.SpecType.WIDTH, + startPadding = SizeSpec(fixedSize = 10f), + endPadding = SizeSpec(fixedSize = 10f), + gutter = SizeSpec(fixedSize = 10f), + cellSize = SizeSpec(fixedSize = 10f) + ) + val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec) + + val expectedResult = + CalculatedFolderSpec( + startPaddingPx = 16.dpToPx(), + endPaddingPx = 16.dpToPx(), + gutterPx = 16.dpToPx(), + cellSizePx = calculatedWorkspaceSpec.cellSizePx, + availableSpace = availableSpace, + cells = cells + ) + + val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs) + val folderSpecs = FolderSpecs(resourceHelper) + val calculatedWidthSpec = + folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec) + assertThat(calculatedWidthSpec).isEqualTo(expectedResult) + } + + @Test(expected = IllegalStateException::class) + fun retrievesCalculatedWidthSpec_invalidCalculatedWorkspaceSpecType_throwsError() { + val availableSpace = 10.dpToPx() + val cells = 3 + + val workspaceSpec = + WorkspaceSpec( + maxAvailableSize = availableSpace, + specType = WorkspaceSpec.SpecType.HEIGHT, + startPadding = SizeSpec(fixedSize = 10f), + endPadding = SizeSpec(fixedSize = 10f), + gutter = SizeSpec(fixedSize = 10f), + cellSize = SizeSpec(fixedSize = 10f) + ) + val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec) + + val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs) + val folderSpecs = FolderSpecs(resourceHelper) + folderSpecs.getWidthSpec(cells, availableSpace, calculatedWorkspaceSpec) + } + + @Test + fun retrievesCalculatedHeightSpec() { + val availableSpace = 700.dpToPx() + val cells = 3 + + val workspaceSpec = + WorkspaceSpec( + maxAvailableSize = availableSpace, + specType = WorkspaceSpec.SpecType.HEIGHT, + startPadding = SizeSpec(fixedSize = 10f), + endPadding = SizeSpec(fixedSize = 10f), + gutter = SizeSpec(fixedSize = 10f), + cellSize = SizeSpec(fixedSize = 10f) + ) + val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec) + + val expectedResult = + CalculatedFolderSpec( + startPaddingPx = 24.dpToPx(), + endPaddingPx = 64.dpToPx(), + gutterPx = 16.dpToPx(), + cellSizePx = calculatedWorkspaceSpec.cellSizePx, + availableSpace = availableSpace, + cells = cells + ) + + val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs) + val folderSpecs = FolderSpecs(resourceHelper) + val calculatedHeightSpec = + folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec) + assertThat(calculatedHeightSpec).isEqualTo(expectedResult) + } + + @Test(expected = IllegalStateException::class) + fun retrievesCalculatedHeightSpec_invalidCalculatedWorkspaceSpecType_throwsError() { + val availableSpace = 10.dpToPx() + val cells = 3 + + val workspaceSpec = + WorkspaceSpec( + maxAvailableSize = availableSpace, + specType = WorkspaceSpec.SpecType.WIDTH, + startPadding = SizeSpec(fixedSize = 10f), + endPadding = SizeSpec(fixedSize = 10f), + gutter = SizeSpec(fixedSize = 10f), + cellSize = SizeSpec(fixedSize = 10f) + ) + val calculatedWorkspaceSpec = CalculatedWorkspaceSpec(availableSpace, cells, workspaceSpec) + + val resourceHelper = TestResourceHelper(context!!, R.xml.valid_folders_specs) + val folderSpecs = FolderSpecs(resourceHelper) + folderSpecs.getHeightSpec(cells, availableSpace, calculatedWorkspaceSpec) + } + + private fun Float.dpToPx(): Float { + return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat() + } + + private fun Int.dpToPx(): Int { + return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics) + } +} diff --git a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt index 426777de26..5db86ff2b4 100644 --- a/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt +++ b/tests/src/com/android/launcher3/responsive/SizeSpecTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.AbstractDeviceProfileTest import com.google.common.truth.Truth.assertThat +import kotlin.math.roundToInt import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -52,6 +53,42 @@ class SizeSpecTest : AbstractDeviceProfileTest() { } } + @Test + fun validate_getCalculatedValue() { + val availableSpace = 100 + val matchWorkspaceValue = 101 + val combinations = + listOf( + SizeSpec(100f) to 100, + SizeSpec(ofAvailableSpace = .5f) to (availableSpace * .5f).roundToInt(), + SizeSpec(ofRemainderSpace = .5f) to 0, + SizeSpec(matchWorkspace = true) to matchWorkspaceValue + ) + + for ((sizeSpec, expectedValue) in combinations) { + val value = sizeSpec.getCalculatedValue(availableSpace, matchWorkspaceValue) + assertThat(value).isEqualTo(expectedValue) + } + } + + @Test + fun validate_getRemainderSpaceValue() { + val remainderSpace = 100 + val defaultValue = 10 + val combinations = + listOf( + SizeSpec(100f) to defaultValue, + SizeSpec(ofAvailableSpace = .5f) to defaultValue, + SizeSpec(ofRemainderSpace = .5f) to (remainderSpace * .5f).roundToInt(), + SizeSpec(matchWorkspace = true) to defaultValue + ) + + for ((sizeSpec, expectedValue) in combinations) { + val value = sizeSpec.getRemainderSpaceValue(remainderSpace, defaultValue) + assertThat(value).isEqualTo(expectedValue) + } + } + @Test fun multiple_values_assigned() { val combinations = diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt index 3f0054e0c3..691a069b7b 100644 --- a/tests/src/com/android/launcher3/util/TestResourceHelper.kt +++ b/tests/src/com/android/launcher3/util/TestResourceHelper.kt @@ -23,12 +23,17 @@ 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) : +class TestResourceHelper(private val context: Context, specsFileId: Int) : ResourceHelper(context, specsFileId) { override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray { - var clone = styleId.clone() - if (styleId == R.styleable.SizeSpec) clone = TestR.styleable.SizeSpec - else if (styleId == R.styleable.WorkspaceSpec) clone = TestR.styleable.WorkspaceSpec + val clone = + when { + styleId.contentEquals(R.styleable.SizeSpec) -> TestR.styleable.SizeSpec + styleId.contentEquals(R.styleable.WorkspaceSpec) -> TestR.styleable.WorkspaceSpec + styleId.contentEquals(R.styleable.FolderSpec) -> TestR.styleable.FolderSpec + else -> styleId.clone() + } + return context.obtainStyledAttributes(attrs, clone) } }