diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt index 9afd893894..bc1b87deb0 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.Rect 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 @@ import org.mockito.Mockito.mock /** 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 adbca32fba..a347156769 100644 --- a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt +++ b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt @@ -18,7 +18,7 @@ package com.android.quickstep 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 @@ import org.junit.runner.RunWith @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 26180f345f..c3bd90e529 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -224,6 +224,21 @@ + + + + + + + + + + + + + + + diff --git a/src/com/android/launcher3/util/ResourceHelper.kt b/src/com/android/launcher3/util/ResourceHelper.kt new file mode 100644 index 0000000000..0ca788840f --- /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 0000000000..0f6e1b0af1 --- /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() + val workspaceWidthSpecList = mutableListOf() + + 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 0000000000..2310d9ef66 --- /dev/null +++ b/tests/res/values/attrs.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + 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 0000000000..0be704bd8f --- /dev/null +++ b/tests/res/xml/invalid_workspace_file_case_1.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000..5a37d97ba2 --- /dev/null +++ b/tests/res/xml/invalid_workspace_file_case_2.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000..3e68edb15d --- /dev/null +++ b/tests/res/xml/invalid_workspace_file_case_3.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/res/xml/valid_workspace_file.xml b/tests/res/xml/valid_workspace_file.xml new file mode 100644 index 0000000000..91a3e48d21 --- /dev/null +++ b/tests/res/xml/valid_workspace_file.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt new file mode 100644 index 0000000000..dcc669bc38 --- /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, + 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 { + 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 { + 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>, + 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 97% rename from tests/src/com/android/launcher3/DeviceProfileBaseTest.kt rename to tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt index daf4608df7..a5f33c0338 100644 --- a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt +++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt @@ -31,7 +31,13 @@ import org.mockito.ArgumentMatchers.any 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 951f5f86fa..2a27487e3b 100644 --- a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt +++ b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt @@ -18,7 +18,7 @@ package com.android.launcher3.nonquickstep 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 @@ import org.junit.runner.RunWith @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 0000000000..fb03fe1b53 --- /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 0000000000..0fd8a5460e --- /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)) + } +}