Add border spacing and fixed cell height to grid.

- Border spacing is the spacing between the cells.
- Workspace cell height is now fixed, and we allocate
  all the "extra" space to three different variable height
  areas.

* Built behind ENABLE_FOUR_COLUMNS flag because it hinders the
default grid.

Bug: 175329686
Test: - set border spacing to 0 and confirm matches prior layout
      - test drag and drop still worked
      - test reordering
      - test widgets
      - test folders
      - test multiwindow

Change-Id: Ic6f3dff577d28ff214bda4b0a787ec7fd08c108b
diff --git a/src/com/android/launcher3/DevicePaddings.java b/src/com/android/launcher3/DevicePaddings.java
new file mode 100644
index 0000000..4827f36
--- /dev/null
+++ b/src/com/android/launcher3/DevicePaddings.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2021 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.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Workspace items have a fixed height, so we need a way to distribute any unused workspace height.
+ *
+ * The unused or "extra" height is allocated to three different variable heights:
+ * - The space above the workspace
+ * - The space between the workspace and hotseat
+ * - The espace below the hotseat
+ */
+public class DevicePaddings {
+
+    private static final String DEVICE_PADDING = "device-paddings";
+    private static final String DEVICE_PADDINGS = "device-padding";
+
+    private static final String WORKSPACE_TOP_PADDING = "workspaceTopPadding";
+    private static final String WORKSPACE_BOTTOM_PADDING = "workspaceBottomPadding";
+    private static final String HOTSEAT_BOTTOM_PADDING = "hotseatBottomPadding";
+
+    private static final String TAG = DevicePaddings.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    ArrayList<DevicePadding> mDevicePaddings = new ArrayList<>();
+
+    public DevicePaddings(Context context) {
+        try (XmlResourceParser parser = context.getResources().getXml(R.xml.size_limits)) {
+            final int depth = parser.getDepth();
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if ((type == XmlPullParser.START_TAG) && DEVICE_PADDING.equals(parser.getName())) {
+                    final int displayDepth = parser.getDepth();
+                    while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                            parser.getDepth() > displayDepth)
+                            && type != XmlPullParser.END_DOCUMENT) {
+                        if ((type == XmlPullParser.START_TAG)
+                                && DEVICE_PADDINGS.equals(parser.getName())) {
+                            TypedArray a = context.obtainStyledAttributes(
+                                    Xml.asAttributeSet(parser), R.styleable.DevicePadding);
+                            int maxWidthPx = a.getDimensionPixelSize(
+                                    R.styleable.DevicePadding_maxEmptySpace, 0);
+                            a.recycle();
+
+                            PaddingFormula workspaceTopPadding = null;
+                            PaddingFormula workspaceBottomPadding = null;
+                            PaddingFormula hotseatBottomPadding = null;
+
+                            final int limitDepth = parser.getDepth();
+                            while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                                    parser.getDepth() > limitDepth)
+                                    && type != XmlPullParser.END_DOCUMENT) {
+                                AttributeSet attr = Xml.asAttributeSet(parser);
+                                if ((type == XmlPullParser.START_TAG)) {
+                                    if (WORKSPACE_TOP_PADDING.equals(parser.getName())) {
+                                        workspaceTopPadding = new PaddingFormula(context, attr);
+                                    } else if (WORKSPACE_BOTTOM_PADDING.equals(parser.getName())) {
+                                        workspaceBottomPadding = new PaddingFormula(context, attr);
+                                    } else if (HOTSEAT_BOTTOM_PADDING.equals(parser.getName())) {
+                                        hotseatBottomPadding = new PaddingFormula(context, attr);
+                                    }
+                                }
+                            }
+
+                            if (workspaceTopPadding == null
+                                    || workspaceBottomPadding == null
+                                    || hotseatBottomPadding == null) {
+                                throw new RuntimeException("DevicePadding missing padding.");
+                            }
+
+                            mDevicePaddings.add(new DevicePadding(maxWidthPx, workspaceTopPadding,
+                                    workspaceBottomPadding, hotseatBottomPadding));
+                        }
+                    }
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+
+        // Sort ascending by maxEmptySpacePx
+        mDevicePaddings.sort((sl1, sl2) -> Integer.compare(sl1.maxEmptySpacePx,
+                sl2.maxEmptySpacePx));
+    }
+
+    public DevicePadding getDevicePadding(int extraSpacePx) {
+        for (DevicePadding limit : mDevicePaddings) {
+            if (extraSpacePx <= limit.maxEmptySpacePx) {
+                return limit;
+            }
+        }
+
+        return mDevicePaddings.get(mDevicePaddings.size() - 1);
+    }
+
+    /**
+     * Holds all the formulas to calculate the padding for a particular device based on the
+     * amount of extra space.
+     */
+    public static final class DevicePadding {
+
+        private final int maxEmptySpacePx;
+        private final PaddingFormula workspaceTopPadding;
+        private final PaddingFormula workspaceBottomPadding;
+        private final PaddingFormula hotseatBottomPadding;
+
+        public DevicePadding(int maxEmptySpacePx,
+                PaddingFormula workspaceTopPadding,
+                PaddingFormula workspaceBottomPadding,
+                PaddingFormula hotseatBottomPadding) {
+            this.maxEmptySpacePx = maxEmptySpacePx;
+            this.workspaceTopPadding = workspaceTopPadding;
+            this.workspaceBottomPadding = workspaceBottomPadding;
+            this.hotseatBottomPadding = hotseatBottomPadding;
+        }
+
+        public int getWorkspaceTopPadding(int extraSpacePx) {
+            return workspaceTopPadding.calculate(extraSpacePx);
+        }
+
+        public int getWorkspaceBottomPadding(int extraSpacePx) {
+            return workspaceBottomPadding.calculate(extraSpacePx);
+        }
+
+        public int getHotseatBottomPadding(int extraSpacePx) {
+            return hotseatBottomPadding.calculate(extraSpacePx);
+        }
+    }
+
+    /**
+     * Used to calculate a padding based on three variables: a, b, and c.
+     *
+     * Calculation: a * (extraSpace - c) + b
+     */
+    private static final class PaddingFormula {
+
+        private final float a;
+        private final float b;
+        private final float c;
+
+        public PaddingFormula(Context context, AttributeSet attrs) {
+            TypedArray t = context.obtainStyledAttributes(attrs,
+                    R.styleable.DevicePaddingFormula);
+
+            a = getValue(t, R.styleable.DevicePaddingFormula_a);
+            b = getValue(t, R.styleable.DevicePaddingFormula_b);
+            c = getValue(t, R.styleable.DevicePaddingFormula_c);
+
+            t.recycle();
+        }
+
+        public int calculate(int extraSpacePx) {
+            if (DEBUG) {
+                Log.d(TAG, "a=" + a + " * (" + extraSpacePx + " - " + c + ") + b=" + b);
+            }
+            return Math.round(a * (extraSpacePx - c) + b);
+        }
+
+        private static float getValue(TypedArray a, int index) {
+            if (a.getType(index) == TypedValue.TYPE_DIMENSION) {
+                return a.getDimensionPixelSize(index, 0);
+            } else if (a.getType(index) == TypedValue.TYPE_FLOAT) {
+                return a.getFloat(index, 0);
+            }
+            return 0;
+        }
+
+        @Override
+        public String toString() {
+            return "a=" + a + ", b=" + b + ", c=" + c;
+        }
+    }
+}