/* * 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 space below the hotseat */ public class DevicePaddings { private static final String DEVICE_PADDINGS = "device-paddings"; private static final String DEVICE_PADDING = "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 mDevicePaddings = new ArrayList<>(); public DevicePaddings(Context context, int devicePaddingId) { try (XmlResourceParser parser = context.getResources().getXml(devicePaddingId)) { 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_PADDINGS.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_PADDING.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) { if (Utilities.IS_DEBUG_DEVICE) { throw new RuntimeException("DevicePadding missing padding."); } } DevicePadding dp = new DevicePadding(maxWidthPx, workspaceTopPadding, workspaceBottomPadding, hotseatBottomPadding); if (dp.isValid()) { mDevicePaddings.add(dp); } else { Log.e(TAG, "Invalid device padding found."); if (Utilities.IS_DEBUG_DEVICE) { throw new RuntimeException("DevicePadding is invalid"); } } } } } } } catch (IOException | XmlPullParserException e) { Log.e(TAG, "Failure parsing device padding layout.", 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 { // One for each padding since they can each be off by 1 due to rounding errors. private static final int ROUNDING_THRESHOLD_PX = 3; 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 getMaxEmptySpacePx() { return maxEmptySpacePx; } 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); } public boolean isValid() { int workspaceTopPadding = getWorkspaceTopPadding(maxEmptySpacePx); int workspaceBottomPadding = getWorkspaceBottomPadding(maxEmptySpacePx); int hotseatBottomPadding = getHotseatBottomPadding(maxEmptySpacePx); int sum = workspaceTopPadding + workspaceBottomPadding + hotseatBottomPadding; int diff = Math.abs(sum - maxEmptySpacePx); if (DEBUG) { Log.d(TAG, "isValid: workspaceTopPadding=" + workspaceTopPadding + ", workspaceBottomPadding=" + workspaceBottomPadding + ", hotseatBottomPadding=" + hotseatBottomPadding + ", sum=" + sum + ", diff=" + diff); } return diff <= ROUNDING_THRESHOLD_PX; } } /** * 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; } } }