From 1a2d4bd6f4c466bc1f38fe761f3cea4d0d090e88 Mon Sep 17 00:00:00 2001 From: Thales Lima Date: Mon, 28 Nov 2022 13:08:32 +0000 Subject: [PATCH] Create an XML parser for WorkspaceSpecs Extract DeviceProfileTest to Launcher3 so it can be used in other tests as well, and change name of previous base test to be more descriptive. Bug: 241386436 Test: WorkspaceSpecsTest Change-Id: I64613bb5a23c374ed15fb6d936192236a541ab9b --- .../quickstep/FullscreenDrawParamsTest.kt | 4 +- .../quickstep/HotseatWidthCalculationTest.kt | 4 +- res/values/attrs.xml | 15 + .../android/launcher3/util/ResourceHelper.kt | 37 +++ .../launcher3/workspace/WorkspaceSpecs.kt | 252 ++++++++++++++++ tests/res/values/attrs.xml | 35 +++ .../res/xml/invalid_workspace_file_case_1.xml | 56 ++++ .../res/xml/invalid_workspace_file_case_2.xml | 59 ++++ .../res/xml/invalid_workspace_file_case_3.xml | 58 ++++ tests/res/xml/valid_workspace_file.xml | 57 ++++ .../launcher3/AbstractDeviceProfileTest.kt | 272 ++++++++++++++++++ ...t.kt => FakeInvariantDeviceProfileTest.kt} | 8 +- .../HotseatWidthCalculationTest.kt | 4 +- .../launcher3/util/TestResourceHelper.kt | 34 +++ .../launcher3/workspace/WorkspaceSpecsTest.kt | 120 ++++++++ 15 files changed, 1008 insertions(+), 7 deletions(-) create mode 100644 src/com/android/launcher3/util/ResourceHelper.kt create mode 100644 src/com/android/launcher3/workspace/WorkspaceSpecs.kt create mode 100644 tests/res/values/attrs.xml create mode 100644 tests/res/xml/invalid_workspace_file_case_1.xml create mode 100644 tests/res/xml/invalid_workspace_file_case_2.xml create mode 100644 tests/res/xml/invalid_workspace_file_case_3.xml create mode 100644 tests/res/xml/valid_workspace_file.xml create mode 100644 tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt rename tests/src/com/android/launcher3/{DeviceProfileBaseTest.kt => FakeInvariantDeviceProfileTest.kt} (97%) create mode 100644 tests/src/com/android/launcher3/util/TestResourceHelper.kt create mode 100644 tests/src/com/android/launcher3/workspace/WorkspaceSpecsTest.kt 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)) + } +}