Merge "Create specs for hotseat" into udc-qpr-dev

This commit is contained in:
Thales Lima
2023-08-10 20:08:38 +00:00
committed by Android (Google) Code Review
13 changed files with 402 additions and 17 deletions

View File

@@ -202,6 +202,7 @@
<attr name="demoModeLayoutId" format="reference" />
<attr name="isScalable" format="boolean" />
<attr name="devicePaddingId" format="reference" />
<!-- File that contains the specs for the workspace.
Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
<attr name="workspaceSpecsId" format="reference" />
@@ -210,11 +211,14 @@
Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
<attr name="allAppsSpecsId" format="reference" />
<attr name="allAppsSpecsTwoPanelId" format="reference" />
<!-- File that contains the specs for the workspace.
Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
<attr name="folderSpecsId" format="reference" />
<attr name="folderSpecsTwoPanelId" format="reference" />
<!-- File that contains the specs for hotseat bar.
Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
<attr name="hotseatSpecsId" format="reference" />
<attr name="hotseatSpecsTwoPanelId" format="reference" />
<!-- By default all categories are enabled -->
<attr name="deviceCategory" format="integer">
@@ -278,6 +282,11 @@
<attr name="maxAvailableSize" />
</declare-styleable>
<declare-styleable name="HotseatSpec">
<attr name="specType" />
<attr name="maxAvailableSize" />
</declare-styleable>
<declare-styleable name="SizeSpec">
<attr name="fixedSize" format="dimension" />
<attr name="ofAvailableSpace" format="float" />

View File

@@ -56,8 +56,10 @@ import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.responsive.AllAppsSpecs;
import com.android.launcher3.responsive.CalculatedAllAppsSpec;
import com.android.launcher3.responsive.CalculatedFolderSpec;
import com.android.launcher3.responsive.CalculatedHotseatSpec;
import com.android.launcher3.responsive.CalculatedWorkspaceSpec;
import com.android.launcher3.responsive.FolderSpecs;
import com.android.launcher3.responsive.HotseatSpecs;
import com.android.launcher3.responsive.WorkspaceSpecs;
import com.android.launcher3.uioverrides.ApiWrapper;
import com.android.launcher3.util.DisplayController;
@@ -121,6 +123,7 @@ public class DeviceProfile {
private CalculatedAllAppsSpec mAllAppsResponsiveHeightSpec;
private CalculatedFolderSpec mResponsiveFolderWidthSpec;
private CalculatedFolderSpec mResponsiveFolderHeightSpec;
private CalculatedHotseatSpec mResponsiveHotseatSpec;
/**
* The maximum amount of left/right workspace padding as a percentage of the screen width.
@@ -316,7 +319,8 @@ public class DeviceProfile {
// TODO(b/241386436): shouldn't change any launcher behaviour
mIsResponsiveGrid = inv.workspaceSpecsId != INVALID_RESOURCE_HANDLE
&& inv.allAppsSpecsId != INVALID_RESOURCE_HANDLE
&& inv.folderSpecsId != INVALID_RESOURCE_HANDLE;
&& inv.folderSpecsId != INVALID_RESOURCE_HANDLE
&& inv.hotseatSpecsId != INVALID_RESOURCE_HANDLE;
mIsScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
// Determine device posture.
@@ -495,7 +499,17 @@ public class DeviceProfile {
int hotseatBarBottomSpace = pxFromDp(inv.hotseatBarBottomSpace[mTypeIndex], mMetrics);
int minQsbMargin = res.getDimensionPixelSize(R.dimen.min_qsb_margin);
hotseatQsbSpace = pxFromDp(inv.hotseatQsbSpace[mTypeIndex], mMetrics);
if (mIsResponsiveGrid) {
HotseatSpecs hotseatSpecs =
HotseatSpecs.create(new ResourceHelper(context,
isTwoPanels ? inv.hotseatSpecsTwoPanelId : inv.hotseatSpecsId));
mResponsiveHotseatSpec = hotseatSpecs.getCalculatedHeightSpec(heightPx);
hotseatQsbSpace = mResponsiveHotseatSpec.getHotseatQsbSpace();
} else {
hotseatQsbSpace = pxFromDp(inv.hotseatQsbSpace[mTypeIndex], mMetrics);
}
// Have a little space between the inset and the QSB
if (mInsets.bottom + minQsbMargin > hotseatBarBottomSpace) {
int availableSpace = hotseatQsbSpace - (mInsets.bottom - hotseatBarBottomSpace);
@@ -1985,6 +1999,7 @@ public class DeviceProfile {
+ mAllAppsResponsiveWidthSpec.toString());
writer.println(prefix + "\tmResponsiveFolderHeightSpec:" + mResponsiveFolderHeightSpec);
writer.println(prefix + "\tmResponsiveFolderWidthSpec:" + mResponsiveFolderWidthSpec);
writer.println(prefix + "\tmResponsiveHotseatSpec:" + mResponsiveHotseatSpec);
}
}

View File

@@ -190,6 +190,8 @@ public class InvariantDeviceProfile {
public int folderSpecsId = INVALID_RESOURCE_HANDLE;
@XmlRes
public int folderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
public int hotseatSpecsId = INVALID_RESOURCE_HANDLE;
public int hotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
public String dbFile;
public int defaultLayoutId;
@@ -369,6 +371,8 @@ public class InvariantDeviceProfile {
allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
folderSpecsId = closestProfile.mFolderSpecsId;
folderSpecsTwoPanelId = closestProfile.mFolderSpecsTwoPanelId;
hotseatSpecsId = closestProfile.mHotseatSpecsId;
hotseatSpecsTwoPanelId = closestProfile.mHotseatSpecsTwoPanelId;
this.deviceType = deviceType;
inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
@@ -820,6 +824,8 @@ public class InvariantDeviceProfile {
private final int mAllAppsSpecsTwoPanelId;
private final int mFolderSpecsId;
private final int mFolderSpecsTwoPanelId;
private final int mHotseatSpecsId;
private final int mHotseatSpecsTwoPanelId;
public GridOption(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(
@@ -897,6 +903,11 @@ public class InvariantDeviceProfile {
mFolderSpecsTwoPanelId = a.getResourceId(
R.styleable.GridDisplayOption_folderSpecsTwoPanelId,
INVALID_RESOURCE_HANDLE);
mHotseatSpecsId = a.getResourceId(
R.styleable.GridDisplayOption_hotseatSpecsId, INVALID_RESOURCE_HANDLE);
mHotseatSpecsTwoPanelId = a.getResourceId(
R.styleable.GridDisplayOption_hotseatSpecsTwoPanelId,
INVALID_RESOURCE_HANDLE);
} else {
mWorkspaceSpecsId = INVALID_RESOURCE_HANDLE;
mWorkspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
@@ -904,6 +915,8 @@ public class InvariantDeviceProfile {
mAllAppsSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
mFolderSpecsId = INVALID_RESOURCE_HANDLE;
mFolderSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
mHotseatSpecsId = INVALID_RESOURCE_HANDLE;
mHotseatSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
}
int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.responsive
import android.content.res.TypedArray
import android.util.Log
import com.android.launcher3.R
import com.android.launcher3.util.ResourceHelper
class HotseatSpecs(val specs: List<HotseatSpec>) {
fun getCalculatedHeightSpec(availableHeight: Int): CalculatedHotseatSpec {
val spec = specs.firstOrNull { availableHeight <= it.maxAvailableSize }
check(spec != null) { "No available height spec found within $availableHeight." }
return CalculatedHotseatSpec(availableHeight, spec)
}
companion object {
private const val XML_HOTSEAT_SPEC = "hotseatSpec"
@JvmStatic
fun create(resourceHelper: ResourceHelper): HotseatSpecs {
val parser = ResponsiveSpecsParser(resourceHelper)
val specs = parser.parseXML(XML_HOTSEAT_SPEC, ::HotseatSpec)
return HotseatSpecs(specs.filter { it.specType == ResponsiveSpec.SpecType.HEIGHT })
}
}
}
data class HotseatSpec(
val maxAvailableSize: Int,
val specType: ResponsiveSpec.SpecType,
val hotseatQsbSpace: SizeSpec
) {
init {
check(isValid()) { "Invalid HotseatSpec found." }
}
constructor(
attrs: TypedArray,
specs: Map<String, SizeSpec>
) : this(
maxAvailableSize =
attrs.getDimensionPixelSize(R.styleable.ResponsiveSpec_maxAvailableSize, 0),
specType =
ResponsiveSpec.SpecType.values()[
attrs.getInt(
R.styleable.ResponsiveSpec_specType,
ResponsiveSpec.SpecType.HEIGHT.ordinal
)],
hotseatQsbSpace = specs.getOrError(SizeSpec.XmlTags.HOTSEAT_QSB_SPACE)
)
fun isValid(): Boolean {
if (maxAvailableSize <= 0) {
Log.e(LOG_TAG, "${this::class.simpleName}#isValid - maxAvailableSize <= 0")
return false
}
// All specs need to be individually valid
if (!allSpecsAreValid()) {
Log.e(LOG_TAG, "${this::class.simpleName}#isValid - !allSpecsAreValid()")
return false
}
return true
}
private fun allSpecsAreValid(): Boolean {
return hotseatQsbSpace.isValid() && hotseatQsbSpace.onlyFixedSize()
}
companion object {
private const val LOG_TAG = "HotseatSpec"
}
}
class CalculatedHotseatSpec(val availableSpace: Int, val spec: HotseatSpec) {
var hotseatQsbSpace: Int = 0
private set
init {
hotseatQsbSpace = spec.hotseatQsbSpace.getCalculatedValue(availableSpace)
}
override fun hashCode(): Int {
var result = availableSpace.hashCode()
result = 31 * result + hotseatQsbSpace.hashCode()
result = 31 * result + spec.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
return other is CalculatedHotseatSpec &&
availableSpace == other.availableSpace &&
hotseatQsbSpace == other.hotseatQsbSpace &&
spec == other.spec
}
override fun toString(): String {
return "${this::class.simpleName}(" +
"availableSpace=$availableSpace, hotseatQsbSpace=$hotseatQsbSpace, " +
"${spec::class.simpleName}.maxAvailableSize=${spec.maxAvailableSize}" +
")"
}
}

View File

@@ -107,11 +107,20 @@ data class SizeSpec(
return true
}
fun onlyFixedSize(): Boolean {
if (ofAvailableSpace > 0 || ofRemainderSpace > 0 || matchWorkspace) {
Log.e(TAG, "SizeSpec#onlyFixedSize - only fixed size allowed for this tag")
return false
}
return true
}
object XmlTags {
const val START_PADDING = "startPadding"
const val END_PADDING = "endPadding"
const val GUTTER = "gutter"
const val CELL_SIZE = "cellSize"
const val HOTSEAT_QSB_SPACE = "hotseatQsbSpace"
}
companion object {

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<hotseatSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<hotseatSpec
launcher:specType="height"
launcher:maxAvailableSize="847dp">
<hotseatQsbSpace launcher:ofAvailableSpace="1" />
</hotseatSpec>
<hotseatSpec
launcher:specType="height"
launcher:maxAvailableSize="9999dp">
<hotseatQsbSpace launcher:fixedSize="36dp" />
</hotseatSpec>
</hotseatSpecs>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<hotseatSpecs xmlns:launcher="http://schemas.android.com/apk/res-auto">
<hotseatSpec
launcher:specType="height"
launcher:maxAvailableSize="847dp">
<hotseatQsbSpace launcher:fixedSize="24dp" />
</hotseatSpec>
<hotseatSpec
launcher:specType="height"
launcher:maxAvailableSize="9999dp">
<hotseatQsbSpace launcher:fixedSize="36dp" />
</hotseatSpec>
</hotseatSpecs>

View File

@@ -22,6 +22,7 @@ import android.graphics.Rect
import android.util.DisplayMetrics
import android.view.Surface
import androidx.test.core.app.ApplicationProvider
import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.NavigationMode
import com.android.launcher3.util.WindowBounds
@@ -320,4 +321,12 @@ abstract class AbstractDeviceProfileTest {
private fun writeToDevice(context: Context, fileName: String, content: String) {
File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content)
}
protected fun Float.dpToPx(): Float {
return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat()
}
protected fun Int.dpToPx(): Int {
return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
}
}

View File

@@ -21,7 +21,6 @@ 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.google.common.truth.Truth.assertThat
@@ -118,8 +117,4 @@ class CalculatedFolderSpecsTest : AbstractDeviceProfileTest() {
assertThat(cellSizePx).isEqualTo(calculatedWorkspace.cellSizePx)
}
}
private fun Int.dpToPx(): Int {
return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.responsive
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class CalculatedHotseatSpecTest : AbstractDeviceProfileTest() {
override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
/**
* This test tests:
* - (height spec) gets the correct breakpoint from the XML - skips the first breakpoint
*/
@Test
fun normalPhone_returnsSecondBreakpointSpec() {
val deviceSpec = deviceSpecs["phone"]!!
initializeVarsForPhone(deviceSpec)
// Hotseat uses the whole device height
val availableHeight = deviceSpec.naturalSize.second
val hotseatSpecs =
HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
val heightSpec = hotseatSpecs.getCalculatedHeightSpec(availableHeight)
assertThat(heightSpec.availableSpace).isEqualTo(availableHeight)
assertThat(heightSpec.hotseatQsbSpace).isEqualTo(95)
}
/**
* This test tests:
* - (height spec) gets the correct breakpoint from the XML - use the first breakpoint
*/
@Test
fun smallPhone_returnsFirstBreakpointSpec() {
val deviceSpec = deviceSpecs["phone"]!!
deviceSpec.densityDpi = 540 // larger display size
initializeVarsForPhone(deviceSpec)
// Hotseat uses the whole device height
val availableHeight = deviceSpec.naturalSize.second
val hotseatSpecs =
HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
val heightSpec = hotseatSpecs.getCalculatedHeightSpec(availableHeight)
assertThat(heightSpec.availableSpace).isEqualTo(availableHeight)
assertThat(heightSpec.hotseatQsbSpace).isEqualTo(81)
}
}

View File

@@ -22,7 +22,6 @@ import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.SpecType
import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
@@ -249,12 +248,4 @@ class FolderSpecsTest : AbstractDeviceProfileTest() {
val folderSpecs = FolderSpecs.create(resourceHelper)
folderSpecs.getCalculatedHeightSpec(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)
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.responsive
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.android.systemui.util.dpToPx
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 HotseatSpecsTest : AbstractDeviceProfileTest() {
override val runningContext: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun setup() {
initializeVarsForPhone(deviceSpecs["phone"]!!)
}
@Test
fun parseValidFile() {
val hotseatSpecs =
HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.valid_hotseat_file))
assertThat(hotseatSpecs.specs.size).isEqualTo(2)
val expectedSpecs =
listOf(
HotseatSpec(
maxAvailableSize = 847.dpToPx(),
specType = ResponsiveSpec.SpecType.HEIGHT,
hotseatQsbSpace = SizeSpec(24f.dpToPx())
),
HotseatSpec(
maxAvailableSize = 9999.dpToPx(),
specType = ResponsiveSpec.SpecType.HEIGHT,
hotseatQsbSpace = SizeSpec(36f.dpToPx())
),
)
assertThat(hotseatSpecs.specs.size).isEqualTo(expectedSpecs.size)
assertThat(hotseatSpecs.specs[0]).isEqualTo(expectedSpecs[0])
assertThat(hotseatSpecs.specs[1]).isEqualTo(expectedSpecs[1])
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_spaceIsNotFixedSize_throwsError() {
HotseatSpecs.create(TestResourceHelper(context!!, TestR.xml.invalid_hotseat_file_case_1))
}
}

View File

@@ -139,4 +139,20 @@ class SizeSpecTest : AbstractDeviceProfileTest() {
assertThat(instance.isValid()).isEqualTo(false)
}
}
@Test
fun onlyFixedSize() {
assertThat(SizeSpec(fixedSize = 16f).onlyFixedSize()).isEqualTo(true)
val combinations =
listOf(
SizeSpec(0f, 1.1f, 0f, false),
SizeSpec(0f, 0f, 1.1f, false),
SizeSpec(0f, 0f, 0f, true)
)
for (instance in combinations) {
assertThat(instance.onlyFixedSize()).isEqualTo(false)
}
}
}