Make hotseat fit one line on tablets

When running on tablets in landscape, the QSB should inline with the hotseat icons. This does that and fixes small bugs that didn't account for the new hotseatBorderSpace.

Bug: 210118169
Test: atest Launcher3Tests:DeviceProfileTest
Test: visual, using HSV and Window
Change-Id: I5a89c5449bf59fde736618151eceaacca443ef67
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 086a254..656ff4d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -33,6 +33,7 @@
     <dimen name="dynamic_grid_hotseat_top_padding">8dp</dimen>
     <dimen name="dynamic_grid_hotseat_bottom_padding">2dp</dimen>
     <dimen name="dynamic_grid_hotseat_bottom_tall_padding">0dp</dimen>
+    <dimen name="inline_qsb_bottom_margin">0dp</dimen>
 
     <!-- Qsb -->
     <!-- Used for adjusting the position of QSB when placed in hotseat. This is a ratio and a higher
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 8a5b888..157ed04 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -61,6 +61,7 @@
     public final boolean isPhone;
     public final boolean transposeLayoutWithOrientation;
     public final boolean isTwoPanels;
+    public final boolean isQsbInline;
 
     // Device properties in current orientation
     public final boolean isLandscape;
@@ -165,6 +166,7 @@
 
     public final float qsbBottomMarginOriginalPx;
     public int qsbBottomMarginPx;
+    public int qsbWidth; // only used when isQsbInline
 
     // All apps
     public Point allAppsBorderSpacePx;
@@ -246,6 +248,7 @@
         isPhone = !isTablet;
         isTwoPanels = isTablet && useTwoPanels;
 
+
         aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
         boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
         mQsbCenterFactor = context.getResources().getFloat(R.dimen.qsb_center_factor);
@@ -270,7 +273,6 @@
             }
         }
 
-        hotseatQsbHeight = res.getDimensionPixelSize(R.dimen.qsb_widget_height);
         isTaskbarPresent = isTablet && ApiWrapper.TASKBAR_DRAWN_IN_PROCESS
                 && FeatureFlags.ENABLE_TASKBAR.get();
         if (isTaskbarPresent) {
@@ -333,6 +335,8 @@
 
         workspaceCellPaddingXPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x);
 
+        hotseatQsbHeight = res.getDimensionPixelSize(R.dimen.qsb_widget_height);
+        isQsbInline = isTablet && isLandscape && !isTwoPanels && hotseatQsbHeight > 0;
         numShownHotseatIcons =
                 isTwoPanels ? inv.numDatabaseHotseatIcons : inv.numShownHotseatIcons;
         numShownAllAppsColumns =
@@ -340,10 +344,17 @@
         hotseatBarSizeExtraSpacePx = 0;
         hotseatBarTopPaddingPx =
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_top_padding);
-        hotseatBarBottomPaddingPx = (isTallDevice ? res.getDimensionPixelSize(
-                R.dimen.dynamic_grid_hotseat_bottom_tall_padding)
-                : res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_non_tall_padding))
-                + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
+        if (isQsbInline) {
+            hotseatBarBottomPaddingPx = res.getDimensionPixelSize(R.dimen.inline_qsb_bottom_margin);
+            qsbWidth = calculateQsbWidth();
+        } else {
+            hotseatBarBottomPaddingPx = (isTallDevice ? res.getDimensionPixelSize(
+                    R.dimen.dynamic_grid_hotseat_bottom_tall_padding)
+                    : res.getDimensionPixelSize(
+                            R.dimen.dynamic_grid_hotseat_bottom_non_tall_padding))
+                    + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
+            qsbWidth = 0;
+        }
         hotseatBarSidePaddingEndPx =
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
         // Add a bit of space between nav bar and hotseat in vertical bar layout.
@@ -472,6 +483,14 @@
                 new DotRenderer(allAppsIconSizePx, dotPath, DEFAULT_DOT_SIZE);
     }
 
+    private int calculateQsbWidth() {
+        return cellWidthPx * inv.numColumns
+                + cellLayoutBorderSpacePx.x * (inv.numColumns - 1)
+                - (cellWidthPx - iconSizePx) // left and right cell space
+                - iconSizePx * numShownHotseatIcons
+                - hotseatBorderSpace * numShownHotseatIcons;
+    }
+
     private int getHorizontalMarginPx(InvariantDeviceProfile idp, Resources res) {
         if (isVerticalBarLayout()) {
             return 0;
@@ -717,6 +736,11 @@
 
         // Hotseat
         hotseatBorderSpace = pxFromDp(inv.hotseatBorderSpaces[mTypeIndex], mMetrics, scale);
+        if (isQsbInline) {
+            qsbWidth = calculateQsbWidth();
+        } else {
+            qsbWidth = 0;
+        }
         updateHotseatIconSize(iconSizePx);
 
         if (!isVerticalLayout) {
@@ -895,15 +919,24 @@
         } else if (isTaskbarPresent) {
             int hotseatHeight = workspacePadding.bottom;
             int taskbarOffset = getTaskbarOffsetY();
+            int additionalLeftSpace = 0;
+
+            // Center the QSB with hotseat and push icons to the right
+            if (isQsbInline) {
+                additionalLeftSpace = qsbWidth + hotseatBorderSpace;
+            }
+
             int hotseatTopDiff = hotseatHeight - taskbarOffset;
 
             int endOffset = ApiWrapper.getHotseatEndOffset(context);
             int requiredWidth = iconSizePx * numShownHotseatIcons
-                    + hotseatBorderSpace * (numShownHotseatIcons - 1);
+                    + hotseatBorderSpace * (numShownHotseatIcons - 1)
+                    + additionalLeftSpace;
 
             int hotseatSize = Math.min(requiredWidth, availableWidthPx - endOffset);
             int sideSpacing = (availableWidthPx - hotseatSize) / 2;
-            mHotseatPadding.set(sideSpacing, hotseatTopDiff, sideSpacing, taskbarOffset);
+            mHotseatPadding.set(sideSpacing + additionalLeftSpace, hotseatTopDiff, sideSpacing,
+                    taskbarOffset);
 
             if (endOffset > sideSpacing) {
                 int diff = Utilities.isRtl(context.getResources())
@@ -936,6 +969,10 @@
      * Returns the number of pixels the QSB is translated from the bottom of the screen.
      */
     public int getQsbOffsetY() {
+        if (isQsbInline) {
+            return hotseatBarBottomPaddingPx;
+        }
+
         int freeSpace = isTaskbarPresent
                 ? workspacePadding.bottom
                 : hotseatBarSizePx - hotseatCellHeightPx - hotseatQsbHeight;
@@ -953,7 +990,11 @@
      * Returns the number of pixels the taskbar is translated from the bottom of the screen.
      */
     public int getTaskbarOffsetY() {
-        return (getQsbOffsetY() - taskbarSize) / 2;
+        if (isQsbInline) {
+            return getQsbOffsetY() + (Math.abs(hotseatQsbHeight - iconSizePx) / 2);
+        } else {
+            return (getQsbOffsetY() - taskbarSize) / 2;
+        }
     }
 
     /**
@@ -1113,6 +1154,8 @@
         writer.println(prefix + pxToDpStr("hotseatBarSidePaddingEndPx",
                 hotseatBarSidePaddingEndPx));
         writer.println(prefix + "\tnumShownHotseatIcons: " + numShownHotseatIcons);
+        writer.println(prefix + "\tisQsbInline: " + isQsbInline);
+        writer.println(prefix + pxToDpStr("qsbWidth", qsbWidth));
 
         writer.println(prefix + "\tisTaskbarPresent:" + isTaskbarPresent);
         writer.println(prefix + "\tisTaskbarPresentInApps:" + isTaskbarPresentInApps);
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index ffe3816..cb0cc11 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -173,7 +173,10 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
-        int width = getShortcutsAndWidgets().getMeasuredWidth();
+        int width = mActivity.getDeviceProfile().isQsbInline
+                ? mActivity.getDeviceProfile().qsbWidth
+                : getShortcutsAndWidgets().getMeasuredWidth();
+
         mQsb.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                 MeasureSpec.makeMeasureSpec(mQsbHeight, MeasureSpec.EXACTLY));
     }
@@ -183,7 +186,13 @@
         super.onLayout(changed, l, t, r, b);
 
         int qsbWidth = mQsb.getMeasuredWidth();
-        int left = (r - l - qsbWidth) / 2;
+        int left;
+        if (mActivity.getDeviceProfile().isQsbInline) {
+            int qsbSpace = mActivity.getDeviceProfile().hotseatBorderSpace;
+            left = l + getPaddingLeft() - qsbWidth - qsbSpace;
+        } else {
+            left = (r - l - qsbWidth) / 2;
+        }
         int right = left + qsbWidth;
 
         int bottom = b - t - mActivity.getDeviceProfile().getQsbOffsetY();
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index fec1d68..5583eae 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -19,6 +19,7 @@
 import static android.view.MotionEvent.ACTION_DOWN;
 
 import static com.android.launcher3.CellLayout.FOLDER;
+import static com.android.launcher3.CellLayout.HOTSEAT;
 import static com.android.launcher3.CellLayout.WORKSPACE;
 
 import android.app.WallpaperManager;
@@ -146,7 +147,8 @@
             // No need to add padding when cell layout border spacing is present.
             boolean noPaddingX =
                     (dp.cellLayoutBorderSpacePx.x > 0 && mContainerType == WORKSPACE)
-                            || (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER);
+                            || (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER)
+                            || (dp.hotseatBorderSpace > 0 && mContainerType == HOTSEAT);
             int cellPaddingX = noPaddingX
                     ? 0
                     : mContainerType == WORKSPACE
@@ -251,7 +253,7 @@
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
         // While the folder is open, the position of the icon cannot change.
         lp.canReorder = false;
-        if (mContainerType == CellLayout.HOTSEAT) {
+        if (mContainerType == HOTSEAT) {
             CellLayout cl = (CellLayout) getParent();
             cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
         }
@@ -260,7 +262,7 @@
     @Override
     public void clearFolderLeaveBehind(FolderIcon child) {
         ((CellLayout.LayoutParams) child.getLayoutParams()).canReorder = true;
-        if (mContainerType == CellLayout.HOTSEAT) {
+        if (mContainerType == HOTSEAT) {
             CellLayout cl = (CellLayout) getParent();
             cl.clearFolderLeaveBehind();
         }
diff --git a/tests/Android.bp b/tests/Android.bp
index 8def20f..f3e0500 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -23,7 +23,10 @@
 // Source code used for test
 filegroup {
     name: "launcher-tests-src",
-    srcs: ["src/**/*.java"],
+    srcs: [
+      "src/**/*.java",
+      "src/**/*.kt"
+    ],
 }
 
 // Source code used for oop test helpers
diff --git a/tests/src/com/android/launcher3/DeviceProfileTest.kt b/tests/src/com/android/launcher3/DeviceProfileTest.kt
new file mode 100644
index 0000000..6c99a21
--- /dev/null
+++ b/tests/src/com/android/launcher3/DeviceProfileTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2022 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.graphics.PointF
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.DisplayController.Info
+import com.android.launcher3.util.WindowBounds
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.*
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceProfileTest {
+
+    private var context: Context? = null
+    private var inv: InvariantDeviceProfile? = null
+    private var info: Info = mock(Info::class.java)
+    private var windowBounds: WindowBounds? = null
+    private var isMultiWindowMode: Boolean = false
+    private var transposeLayoutWithOrientation: Boolean = false
+    private var useTwoPanels: Boolean = false
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+        // make sure to reset values
+        useTwoPanels = false
+    }
+
+    @Test
+    fun qsbWidth_is_match_parent_for_phones() {
+        initializeVarsForPhone()
+
+        val dp = DeviceProfile(
+            context,
+            inv,
+            info,
+            windowBounds,
+            isMultiWindowMode,
+            transposeLayoutWithOrientation,
+            useTwoPanels
+        )
+
+        assertThat(dp.isQsbInline).isFalse()
+        assertThat(dp.qsbWidth).isEqualTo(0)
+    }
+
+    @Test
+    fun qsbWidth_is_match_parent_for_tablet_portrait() {
+        initializeVarsForTablet()
+
+        val dp = DeviceProfile(
+            context,
+            inv,
+            info,
+            windowBounds,
+            isMultiWindowMode,
+            transposeLayoutWithOrientation,
+            useTwoPanels
+        )
+
+        assertThat(dp.isQsbInline).isFalse()
+        assertThat(dp.qsbWidth).isEqualTo(0)
+    }
+
+    @Test
+    fun qsbWidth_has_size_for_tablet_landscape() {
+        initializeVarsForTablet(true)
+
+        val dp = DeviceProfile(
+            context,
+            inv,
+            info,
+            windowBounds,
+            isMultiWindowMode,
+            transposeLayoutWithOrientation,
+            useTwoPanels
+        )
+
+        if (dp.hotseatQsbHeight > 0) {
+            assertThat(dp.isQsbInline).isTrue()
+            assertThat(dp.qsbWidth).isGreaterThan(0)
+        } else {
+            assertThat(dp.isQsbInline).isFalse()
+            assertThat(dp.qsbWidth).isEqualTo(0)
+        }
+    }
+
+    /**
+     * This test is to make sure that two panels don't inline the QSB as tablets do
+     */
+    @Test
+    fun qsbWidth_is_match_parent_for_two_panel_landscape() {
+        initializeVarsForTablet(true)
+        useTwoPanels = true
+
+        val dp = DeviceProfile(
+            context,
+            inv,
+            info,
+            windowBounds,
+            isMultiWindowMode,
+            transposeLayoutWithOrientation,
+            useTwoPanels
+        )
+
+        assertThat(dp.isQsbInline).isFalse()
+        assertThat(dp.qsbWidth).isEqualTo(0)
+    }
+
+    private fun initializeVarsForPhone(isLandscape: Boolean = false) {
+        val (x, y) = if (isLandscape)
+            Pair(3120, 1440)
+        else
+            Pair(1440, 3120)
+
+        windowBounds = WindowBounds(x, y, x, y - 100)
+
+        `when`(info.isTablet(any())).thenReturn(false)
+
+        scalableInvariantDeviceProfile()
+    }
+
+    private fun initializeVarsForTablet(isLandscape: Boolean = false) {
+        val (x, y) = if (isLandscape)
+            Pair(2560, 1600)
+        else
+            Pair(1600, 2560)
+
+        windowBounds = WindowBounds(x, y, x, y - 100)
+
+        `when`(info.isTablet(any())).thenReturn(true)
+
+        scalableInvariantDeviceProfile()
+    }
+
+    /**
+     * A very generic grid, just to make qsb tests work. For real calculations, make sure to use
+     * values that better represent a real grid.
+     */
+    private fun scalableInvariantDeviceProfile() {
+        inv = InvariantDeviceProfile().apply {
+            isScalable = true
+            numColumns = 5
+            numRows = 5
+            horizontalMargin = FloatArray(4) { 22f }
+            borderSpaces = listOf(
+                PointF(16f, 16f),
+                PointF(16f, 16f),
+                PointF(16f, 16f),
+                PointF(16f, 16f)
+            ).toTypedArray()
+            allAppsBorderSpaces = listOf(
+                PointF(16f, 16f),
+                PointF(16f, 16f),
+                PointF(16f, 16f),
+                PointF(16f, 16f)
+            ).toTypedArray()
+            hotseatBorderSpaces = FloatArray(4) { 16f }
+            iconSize = FloatArray(4) { 56f }
+            allAppsIconSize = FloatArray(4) { 56f }
+            iconTextSize = FloatArray(4) { 14f }
+            allAppsIconTextSize = FloatArray(4) { 14f }
+            minCellSize = listOf(
+                PointF(64f, 83f),
+                PointF(64f, 83f),
+                PointF(64f, 83f),
+                PointF(64f, 83f)
+            ).toTypedArray()
+        }
+    }
+}
\ No newline at end of file