Decrease icon size by steps

When the icon can't fit the cell size, decrease it by steps defined by UX until it fits or reach a minimum size.

Fix: 283929701
Test: DeviceProfileAlternativeDisplaysDumpTest
Test: DeviceProfileResponsiveAlternativeDisplaysDumpTest
Test: IconSizeStepsTest
Flag: ENABLE_RESPONSIVE_WORKSPACE
Change-Id: I2875b669c0a24ecd1c4d785a33e2cffb78c9fe76
diff --git a/res/values/config.xml b/res/values/config.xml
index 27211fd..8f9731c 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -217,4 +217,28 @@
     <!-- Whether the floating rotation button should be on the left/right in the device's natural
          orientation -->
     <bool name="floating_rotation_button_position_left">true</bool>
+
+    <!--  Mapping of visual icon size to XML value http://b/235886078  -->
+    <dimen name="iconSize48dp">52dp</dimen>
+    <dimen name="iconSize50dp">55dp</dimen>
+    <dimen name="iconSize52dp">57dp</dimen>
+    <dimen name="iconSize54dp">59dp</dimen>
+    <dimen name="iconSize56dp">61dp</dimen>
+    <dimen name="iconSize58dp">63dp</dimen>
+    <dimen name="iconSize60dp">66dp</dimen>
+    <dimen name="iconSize66dp">72dp</dimen>
+    <dimen name="iconSize72dp">79dp</dimen>
+
+    <!--  Icon size steps in dp  -->
+    <integer-array name="icon_size_steps">
+        <item>@dimen/iconSize48dp</item>
+        <item>@dimen/iconSize50dp</item>
+        <item>@dimen/iconSize52dp</item>
+        <item>@dimen/iconSize54dp</item>
+        <item>@dimen/iconSize56dp</item>
+        <item>@dimen/iconSize58dp</item>
+        <item>@dimen/iconSize60dp</item>
+        <item>@dimen/iconSize66dp</item>
+        <item>@dimen/iconSize72dp</item>
+    </integer-array>
 </resources>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 0498032..a166271 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -58,6 +58,7 @@
 import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
+import com.android.launcher3.util.IconSizeSteps;
 import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.workspace.CalculatedWorkspaceSpec;
@@ -83,6 +84,7 @@
     public final InvariantDeviceProfile inv;
     private final Info mInfo;
     private final DisplayMetrics mMetrics;
+    private final IconSizeSteps mIconSizeSteps;
 
     // Device properties
     public final boolean isTablet;
@@ -330,6 +332,8 @@
         final Resources res = context.getResources();
         mMetrics = res.getDisplayMetrics();
 
+        mIconSizeSteps = mIsResponsiveGrid ? new IconSizeSteps(res) : null;
+
         // Determine sizes.
         widthPx = windowBounds.bounds.width();
         heightPx = windowBounds.bounds.height();
@@ -535,12 +539,16 @@
         // for the available height to be correct
         if (mIsResponsiveGrid) {
             mWorkspaceSpecs = new WorkspaceSpecs(new ResourceHelper(context, inv.workspaceSpecsId));
+            int availableResponsiveWidth =
+                    availableWidthPx - (isVerticalBarLayout() ? hotseatBarSizePx : 0);
+            // don't use availableHeightPx because it subtracts bottom padding,
+            // but the workspace go behind it
+            int availableResponsiveHeight =
+                    heightPx - mInsets.top - (isVerticalBarLayout() ? 0 : hotseatBarSizePx);
             mResponsiveWidthSpec = mWorkspaceSpecs.getCalculatedWidthSpec(inv.numColumns,
-                    availableWidthPx);
+                    availableResponsiveWidth);
             mResponsiveHeightSpec = mWorkspaceSpecs.getCalculatedHeightSpec(inv.numRows,
-                    // don't use availableHeightPx because it subtracts bottom padding,
-                    // but the hotseat go behind it
-                    heightPx - mInsets.top - hotseatBarSizePx);
+                    availableResponsiveHeight);
 
             mAllAppsSpecs = new AllAppsSpecs(new ResourceHelper(context, inv.allAppsSpecsId));
             mAllAppsResponsiveWidthSpec = mAllAppsSpecs.getCalculatedWidthSpec(inv.numColumns,
@@ -926,9 +934,32 @@
 
             cellWidthPx = mResponsiveWidthSpec.getCellSizePx();
             cellHeightPx = mResponsiveHeightSpec.getCellSizePx();
-            cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
 
-            // TODO(b/283929701): decrease icon size if content doesn't fit on cell
+            if (cellWidthPx < iconSizePx) {
+                // get a smaller icon size
+                iconSizePx = mIconSizeSteps.getIconSmallerThan(cellWidthPx);
+                // calculate new cellContentHeight
+                cellContentHeight = iconSizePx + cellTextAndPaddingHeight;
+            }
+
+            while (iconSizePx > mIconSizeSteps.minimumIconSize()
+                    && cellContentHeight > cellHeightPx) {
+                int extraHeightRequired = cellContentHeight - cellHeightPx;
+                int newPadding = iconDrawablePaddingPx - extraHeightRequired;
+                if (newPadding >= 0) {
+                    // Responsive uses the padding without scaling
+                    iconDrawablePaddingPx = iconDrawablePaddingOriginalPx = newPadding;
+                    cellTextAndPaddingHeight =
+                            iconDrawablePaddingPx + Utilities.calculateTextHeight(iconTextSizePx);
+                } else {
+                    // get a smaller icon size
+                    iconSizePx = mIconSizeSteps.getNextLowerIconSize(iconSizePx);
+                }
+                // calculate new cellContentHeight
+                cellContentHeight = iconSizePx + cellTextAndPaddingHeight;
+            }
+
+            cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
         } else if (mIsScalableGrid) {
             cellWidthPx = pxFromDp(inv.minCellSize[mTypeIndex].x, mMetrics, scale);
             cellHeightPx = pxFromDp(inv.minCellSize[mTypeIndex].y, mMetrics, scale);
diff --git a/src/com/android/launcher3/util/IconSizeSteps.kt b/src/com/android/launcher3/util/IconSizeSteps.kt
new file mode 100644
index 0000000..2a5afe0
--- /dev/null
+++ b/src/com/android/launcher3/util/IconSizeSteps.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.res.Resources
+import androidx.core.content.res.getDimensionOrThrow
+import androidx.core.content.res.use
+import com.android.launcher3.R
+import kotlin.math.max
+
+class IconSizeSteps(res: Resources) {
+    private val steps: List<Int>
+
+    init {
+        steps =
+            res.obtainTypedArray(R.array.icon_size_steps).use {
+                (0 until it.length()).map { step -> it.getDimensionOrThrow(step).toInt() }.sorted()
+            }
+    }
+
+    fun minimumIconSize(): Int = steps[0]
+
+    fun getNextLowerIconSize(iconSizePx: Int): Int {
+        return steps[max(0, getIndexForIconSize(iconSizePx) - 1)]
+    }
+
+    fun getIconSmallerThan(cellWidth: Int): Int {
+        return steps.lastOrNull { it <= cellWidth } ?: steps[0]
+    }
+
+    private fun getIndexForIconSize(iconSizePx: Int): Int {
+        return max(0, steps.indexOfFirst { iconSizePx <= it })
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/IconSizeStepsTest.kt b/tests/src/com/android/launcher3/util/IconSizeStepsTest.kt
new file mode 100644
index 0000000..d9e3377
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/IconSizeStepsTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.Configuration
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IconSizeStepsTest {
+    private var context: Context? = null
+    private val runningContext: Context = ApplicationProvider.getApplicationContext()
+    private lateinit var iconSizeSteps: IconSizeSteps
+
+    @Before
+    fun setup() {
+        // 160dp makes 1px = 1dp
+        val config =
+            Configuration(runningContext.resources.configuration).apply { this.densityDpi = 160 }
+        context = runningContext.createConfigurationContext(config)
+        iconSizeSteps = IconSizeSteps(context!!.resources)
+    }
+
+    @Test
+    fun minimumIconSize() {
+        assertThat(iconSizeSteps.minimumIconSize()).isEqualTo(52)
+    }
+
+    @Test
+    fun getNextLowerIconSize() {
+        assertThat(iconSizeSteps.getNextLowerIconSize(66)).isEqualTo(63)
+
+        // Assert that never goes below minimum
+        assertThat(iconSizeSteps.getNextLowerIconSize(52)).isEqualTo(52)
+        assertThat(iconSizeSteps.getNextLowerIconSize(30)).isEqualTo(52)
+    }
+
+    @Test
+    fun getIconSmallerThan() {
+        assertThat(iconSizeSteps.getIconSmallerThan(60)).isEqualTo(59)
+
+        // Assert that never goes below minimum
+        assertThat(iconSizeSteps.getIconSmallerThan(52)).isEqualTo(52)
+        assertThat(iconSizeSteps.getIconSmallerThan(30)).isEqualTo(52)
+    }
+}