1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.content.res.Resources;
22 import android.graphics.Point;
23 import android.graphics.PointF;
24 import android.graphics.Rect;
25 import android.view.Surface;
26 
27 import com.android.launcher3.CellLayout.ContainerType;
28 import com.android.launcher3.graphics.IconShape;
29 import com.android.launcher3.icons.DotRenderer;
30 import com.android.launcher3.icons.IconNormalizer;
31 import com.android.launcher3.util.DefaultDisplay;
32 import com.android.launcher3.util.WindowBounds;
33 
34 public class DeviceProfile {
35 
36     private static final float TABLET_MIN_DPS = 600;
37     private static final float LARGE_TABLET_MIN_DPS = 720;
38 
39 
40     public final InvariantDeviceProfile inv;
41     private final DefaultDisplay.Info mInfo;
42 
43     // Device properties
44     public final boolean isTablet;
45     public final boolean isLargeTablet;
46     public final boolean isPhone;
47     public final boolean transposeLayoutWithOrientation;
48 
49     // Device properties in current orientation
50     public final boolean isLandscape;
51     public final boolean isMultiWindowMode;
52 
53     public final int windowX;
54     public final int windowY;
55     public final int widthPx;
56     public final int heightPx;
57     public final int availableWidthPx;
58     public final int availableHeightPx;
59 
60     public final float aspectRatio;
61 
62     /**
63      * The maximum amount of left/right workspace padding as a percentage of the screen width.
64      * To be clear, this means that up to 7% of the screen width can be used as left padding, and
65      * 7% of the screen width can be used as right padding.
66      */
67     private static final float MAX_HORIZONTAL_PADDING_PERCENT = 0.14f;
68 
69     private static final float TALL_DEVICE_ASPECT_RATIO_THRESHOLD = 2.0f;
70 
71     // To evenly space the icons, increase the left/right margins for tablets in portrait mode.
72     private static final int PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER = 4;
73 
74     // Workspace
75     public final int desiredWorkspaceLeftRightMarginPx;
76     public final int cellLayoutPaddingLeftRightPx;
77     public final int cellLayoutBottomPaddingPx;
78     public final int edgeMarginPx;
79     public float workspaceSpringLoadShrinkFactor;
80     public final int workspaceSpringLoadedBottomSpace;
81 
82     // Workspace page indicator
83     public final int workspacePageIndicatorHeight;
84     private final int mWorkspacePageIndicatorOverlapWorkspace;
85 
86     // Workspace icons
87     public int iconSizePx;
88     public int iconTextSizePx;
89     public int iconDrawablePaddingPx;
90     public int iconDrawablePaddingOriginalPx;
91 
92     public int cellWidthPx;
93     public int cellHeightPx;
94     public int workspaceCellPaddingXPx;
95 
96     // Folder
97     public int folderIconSizePx;
98     public int folderIconOffsetYPx;
99 
100     // Folder cell
101     public int folderCellWidthPx;
102     public int folderCellHeightPx;
103 
104     // Folder child
105     public int folderChildIconSizePx;
106     public int folderChildTextSizePx;
107     public int folderChildDrawablePaddingPx;
108 
109     // Hotseat
110     public int hotseatCellHeightPx;
111     // In portrait: size = height, in landscape: size = width
112     public int hotseatBarSizePx;
113     public final int hotseatBarTopPaddingPx;
114     public int hotseatBarBottomPaddingPx;
115     // Start is the side next to the nav bar, end is the side next to the workspace
116     public final int hotseatBarSidePaddingStartPx;
117     public final int hotseatBarSidePaddingEndPx;
118 
119     // All apps
120     public int allAppsCellHeightPx;
121     public int allAppsCellWidthPx;
122     public int allAppsIconSizePx;
123     public int allAppsIconDrawablePaddingPx;
124     public float allAppsIconTextSizePx;
125 
126     // Widgets
127     public final PointF appWidgetScale = new PointF(1.0f, 1.0f);
128 
129     // Drop Target
130     public int dropTargetBarSizePx;
131 
132     // Insets
133     private final Rect mInsets = new Rect();
134     public final Rect workspacePadding = new Rect();
135     private final Rect mHotseatPadding = new Rect();
136     // When true, nav bar is on the left side of the screen.
137     private boolean mIsSeascape;
138 
139     // Notification dots
140     public DotRenderer mDotRendererWorkSpace;
141     public DotRenderer mDotRendererAllApps;
142 
DeviceProfile(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info, Point minSize, Point maxSize, int width, int height, boolean isLandscape, boolean isMultiWindowMode, boolean transposeLayoutWithOrientation, Point windowPosition)143     DeviceProfile(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info,
144             Point minSize, Point maxSize, int width, int height, boolean isLandscape,
145             boolean isMultiWindowMode, boolean transposeLayoutWithOrientation,
146             Point windowPosition) {
147 
148         this.inv = inv;
149         this.isLandscape = isLandscape;
150         this.isMultiWindowMode = isMultiWindowMode;
151         windowX = windowPosition.x;
152         windowY = windowPosition.y;
153 
154         // Determine sizes.
155         widthPx = width;
156         heightPx = height;
157         if (isLandscape) {
158             availableWidthPx = maxSize.x;
159             availableHeightPx = minSize.y;
160         } else {
161             availableWidthPx = minSize.x;
162             availableHeightPx = maxSize.y;
163         }
164 
165         mInfo = info;
166 
167         // Constants from resources
168         float swDPs = Utilities.dpiFromPx(
169                 Math.min(info.smallestSize.x, info.smallestSize.y), info.metrics);
170         isTablet = swDPs >= TABLET_MIN_DPS;
171         isLargeTablet = swDPs >= LARGE_TABLET_MIN_DPS;
172         isPhone = !isTablet && !isLargeTablet;
173         aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
174         boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
175 
176         // Some more constants
177         this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
178 
179         context = getContext(context, info, isVerticalBarLayout()
180                 ? Configuration.ORIENTATION_LANDSCAPE
181                 : Configuration.ORIENTATION_PORTRAIT);
182         final Resources res = context.getResources();
183 
184         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
185         desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : edgeMarginPx;
186 
187         int cellLayoutPaddingLeftRightMultiplier = !isVerticalBarLayout() && isTablet
188                 ? PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER : 1;
189         int cellLayoutPadding = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
190         if (isLandscape) {
191             cellLayoutPaddingLeftRightPx = 0;
192             cellLayoutBottomPaddingPx = cellLayoutPadding;
193         } else {
194             cellLayoutPaddingLeftRightPx = cellLayoutPaddingLeftRightMultiplier * cellLayoutPadding;
195             cellLayoutBottomPaddingPx = 0;
196         }
197 
198         workspacePageIndicatorHeight = res.getDimensionPixelSize(
199                 R.dimen.workspace_page_indicator_height);
200         mWorkspacePageIndicatorOverlapWorkspace =
201                 res.getDimensionPixelSize(R.dimen.workspace_page_indicator_overlap_workspace);
202 
203         iconDrawablePaddingOriginalPx =
204                 res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);
205         dropTargetBarSizePx = res.getDimensionPixelSize(R.dimen.dynamic_grid_drop_target_size);
206         workspaceSpringLoadedBottomSpace =
207                 res.getDimensionPixelSize(R.dimen.dynamic_grid_min_spring_loaded_space);
208 
209         workspaceCellPaddingXPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_x);
210 
211         hotseatBarTopPaddingPx =
212                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_top_padding);
213         hotseatBarBottomPaddingPx = (isTallDevice ? 0
214                 : res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_non_tall_padding))
215                 + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
216         hotseatBarSidePaddingEndPx =
217                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
218         // Add a bit of space between nav bar and hotseat in vertical bar layout.
219         hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
220         hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
221                 + (isVerticalBarLayout()
222                 ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
223                 : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
224                         + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
225 
226         // Calculate all of the remaining variables.
227         updateAvailableDimensions(res);
228 
229         // Now that we have all of the variables calculated, we can tune certain sizes.
230         if (!isVerticalBarLayout() && isPhone && isTallDevice) {
231             // We increase the hotseat size when there is extra space.
232             // ie. For a display with a large aspect ratio, we can keep the icons on the workspace
233             // in portrait mode closer together by adding more height to the hotseat.
234             // Note: This calculation was created after noticing a pattern in the design spec.
235             int extraSpace = getCellSize().y - iconSizePx - iconDrawablePaddingPx * 2
236                     - workspacePageIndicatorHeight;
237             hotseatBarSizePx += extraSpace;
238             hotseatBarBottomPaddingPx += extraSpace;
239 
240             // Recalculate the available dimensions using the new hotseat size.
241             updateAvailableDimensions(res);
242         }
243         updateWorkspacePadding();
244 
245         // This is done last, after iconSizePx is calculated above.
246         mDotRendererWorkSpace = new DotRenderer(iconSizePx, IconShape.getShapePath(),
247                 IconShape.DEFAULT_PATH_SIZE);
248         mDotRendererAllApps = iconSizePx == allAppsIconSizePx ? mDotRendererWorkSpace :
249                 new DotRenderer(allAppsIconSizePx, IconShape.getShapePath(),
250                         IconShape.DEFAULT_PATH_SIZE);
251     }
252 
toBuilder(Context context)253     public Builder toBuilder(Context context) {
254         Point size = new Point(availableWidthPx, availableHeightPx);
255         return new Builder(context, inv, mInfo)
256                 .setSizeRange(size, size)
257                 .setSize(widthPx, heightPx)
258                 .setWindowPosition(windowX, windowY)
259                 .setMultiWindowMode(isMultiWindowMode);
260     }
261 
copy(Context context)262     public DeviceProfile copy(Context context) {
263         return toBuilder(context).build();
264     }
265 
266     /**
267      * TODO: Move this to the builder as part of setMultiWindowMode
268      */
getMultiWindowProfile(Context context, WindowBounds windowBounds)269     public DeviceProfile getMultiWindowProfile(Context context, WindowBounds windowBounds) {
270         // We take the minimum sizes of this profile and it's multi-window variant to ensure that
271         // the system decor is always excluded.
272         Point mwSize = new Point(Math.min(availableWidthPx, windowBounds.availableSize.x),
273                 Math.min(availableHeightPx, windowBounds.availableSize.y));
274 
275         DeviceProfile profile = toBuilder(context)
276                 .setSizeRange(mwSize, mwSize)
277                 .setSize(windowBounds.bounds.width(), windowBounds.bounds.height())
278                 .setWindowPosition(windowBounds.bounds.left, windowBounds.bounds.top)
279                 .setMultiWindowMode(true)
280                 .build();
281 
282         // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
283         float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
284                 - iconDrawablePaddingPx - profile.iconTextSizePx;
285         if (workspaceCellPaddingY < profile.iconDrawablePaddingPx * 2) {
286             profile.adjustToHideWorkspaceLabels();
287         }
288 
289         // We use these scales to measure and layout the widgets using their full invariant profile
290         // sizes and then draw them scaled and centered to fit in their multi-window mode cellspans.
291         float appWidgetScaleX = (float) profile.getCellSize().x / getCellSize().x;
292         float appWidgetScaleY = (float) profile.getCellSize().y / getCellSize().y;
293         profile.appWidgetScale.set(appWidgetScaleX, appWidgetScaleY);
294         profile.updateWorkspacePadding();
295 
296         return profile;
297     }
298 
299     /**
300      * Inverse of {@link #getMultiWindowProfile(Context, WindowBounds)}
301      * @return device profile corresponding to the current orientation in non multi-window mode.
302      */
getFullScreenProfile()303     public DeviceProfile getFullScreenProfile() {
304         return isLandscape ? inv.landscapeProfile : inv.portraitProfile;
305     }
306 
307     /**
308      * Adjusts the profile so that the labels on the Workspace are hidden.
309      * It is important to call this method after the All Apps variables have been set.
310      */
adjustToHideWorkspaceLabels()311     private void adjustToHideWorkspaceLabels() {
312         iconTextSizePx = 0;
313         iconDrawablePaddingPx = 0;
314         cellHeightPx = iconSizePx;
315         autoResizeAllAppsCells();
316     }
317 
318     /**
319      * Re-computes the all-apps cell size to be independent of workspace
320      */
autoResizeAllAppsCells()321     public void autoResizeAllAppsCells() {
322         int topBottomPadding = allAppsIconDrawablePaddingPx * (isVerticalBarLayout() ? 2 : 1);
323         allAppsCellHeightPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx
324                 + Utilities.calculateTextHeight(allAppsIconTextSizePx)
325                 + topBottomPadding * 2;
326     }
327 
updateAvailableDimensions(Resources res)328     private void updateAvailableDimensions(Resources res) {
329         updateIconSize(1f, res);
330 
331         // Check to see if the icons fit within the available height.  If not, then scale down.
332         float usedHeight = (cellHeightPx * inv.numRows);
333         int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
334         if (usedHeight > maxHeight) {
335             float scale = maxHeight / usedHeight;
336             updateIconSize(scale, res);
337         }
338         updateAvailableFolderCellDimensions(res);
339     }
340 
341     /**
342      * Updating the iconSize affects many aspects of the launcher layout, such as: iconSizePx,
343      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
344      * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
345      */
updateIconSize(float scale, Resources res)346     private void updateIconSize(float scale, Resources res) {
347         // Workspace
348         final boolean isVerticalLayout = isVerticalBarLayout();
349         float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
350         iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
351                 * scale));
352         iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
353         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
354 
355         cellHeightPx = iconSizePx + iconDrawablePaddingPx
356                 + Utilities.calculateTextHeight(iconTextSizePx);
357         int cellYPadding = (getCellSize().y - cellHeightPx) / 2;
358         if (iconDrawablePaddingPx > cellYPadding && !isVerticalLayout
359                 && !isMultiWindowMode) {
360             // Ensures that the label is closer to its corresponding icon. This is not an issue
361             // with vertical bar layout or multi-window mode since the issue is handled separately
362             // with their calls to {@link #adjustToHideWorkspaceLabels}.
363             cellHeightPx -= (iconDrawablePaddingPx - cellYPadding);
364             iconDrawablePaddingPx = cellYPadding;
365         }
366         cellWidthPx = iconSizePx + iconDrawablePaddingPx;
367 
368         // All apps
369         if (allAppsHasDifferentNumColumns()) {
370             allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
371             allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
372             allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
373             // We use 4 below to ensure labels are closer to their corresponding icon.
374             allAppsCellHeightPx = Math.round(allAppsIconSizePx + allAppsIconTextSizePx
375                     + (4 * allAppsIconDrawablePaddingPx));
376         } else {
377             allAppsIconSizePx = iconSizePx;
378             allAppsIconTextSizePx = iconTextSizePx;
379             allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
380             allAppsCellHeightPx = getCellSize().y;
381         }
382         allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
383 
384         if (isVerticalBarLayout()) {
385             // Always hide the Workspace text with vertical bar layout.
386             adjustToHideWorkspaceLabels();
387         }
388 
389         // Hotseat
390         if (isVerticalLayout) {
391             hotseatBarSizePx = iconSizePx + hotseatBarSidePaddingStartPx
392                     + hotseatBarSidePaddingEndPx;
393         }
394         hotseatCellHeightPx = iconSizePx;
395 
396         if (!isVerticalLayout) {
397             int expectedWorkspaceHeight = availableHeightPx - hotseatBarSizePx
398                     - workspacePageIndicatorHeight - edgeMarginPx;
399             float minRequiredHeight = dropTargetBarSizePx + workspaceSpringLoadedBottomSpace;
400             workspaceSpringLoadShrinkFactor = Math.min(
401                     res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f,
402                     1 - (minRequiredHeight / expectedWorkspaceHeight));
403         } else {
404             workspaceSpringLoadShrinkFactor =
405                     res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f;
406         }
407 
408         // Folder icon
409         folderIconSizePx = IconNormalizer.getNormalizedCircleSize(iconSizePx);
410         folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2;
411     }
412 
updateAvailableFolderCellDimensions(Resources res)413     private void updateAvailableFolderCellDimensions(Resources res) {
414         int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top)
415                 + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom)
416                 + Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size));
417 
418         updateFolderCellSize(1f, res);
419 
420         // Don't let the folder get too close to the edges of the screen.
421         int folderMargin = edgeMarginPx * 2;
422         Point totalWorkspacePadding = getTotalWorkspacePadding();
423 
424         // Check if the icons fit within the available height.
425         float contentUsedHeight = folderCellHeightPx * inv.numFolderRows;
426         int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
427                 - folderMargin;
428         float scaleY = contentMaxHeight / contentUsedHeight;
429 
430         // Check if the icons fit within the available width.
431         float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns;
432         int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
433         float scaleX = contentMaxWidth / contentUsedWidth;
434 
435         float scale = Math.min(scaleX, scaleY);
436         if (scale < 1f) {
437             updateFolderCellSize(scale, res);
438         }
439     }
440 
updateFolderCellSize(float scale, Resources res)441     private void updateFolderCellSize(float scale, Resources res) {
442         folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics) * scale);
443         folderChildTextSizePx =
444                 (int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale);
445 
446         int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
447         int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding) * scale);
448         int cellPaddingY = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_y_padding) * scale);
449 
450         folderCellWidthPx = folderChildIconSizePx + 2 * cellPaddingX;
451         folderCellHeightPx = folderChildIconSizePx + 2 * cellPaddingY + textHeight;
452         folderChildDrawablePaddingPx = Math.max(0,
453                 (folderCellHeightPx - folderChildIconSizePx - textHeight) / 3);
454     }
455 
updateInsets(Rect insets)456     public void updateInsets(Rect insets) {
457         mInsets.set(insets);
458         updateWorkspacePadding();
459     }
460 
461     /**
462      * The current device insets. This is generally same as the insets being dispatched to
463      * {@link Insettable} elements, but can differ if the element is using a different profile.
464      */
getInsets()465     public Rect getInsets() {
466         return mInsets;
467     }
468 
getCellSize()469     public Point getCellSize() {
470         return getCellSize(inv.numColumns, inv.numRows);
471     }
472 
getCellSize(int numColumns, int numRows)473     private Point getCellSize(int numColumns, int numRows) {
474         Point result = new Point();
475         // Since we are only concerned with the overall padding, layout direction does
476         // not matter.
477         Point padding = getTotalWorkspacePadding();
478         result.x = calculateCellWidth(availableWidthPx - padding.x
479                 - cellLayoutPaddingLeftRightPx * 2, numColumns);
480         result.y = calculateCellHeight(availableHeightPx - padding.y
481                 - cellLayoutBottomPaddingPx, numRows);
482         return result;
483     }
484 
getTotalWorkspacePadding()485     public Point getTotalWorkspacePadding() {
486         updateWorkspacePadding();
487         return new Point(workspacePadding.left + workspacePadding.right,
488                 workspacePadding.top + workspacePadding.bottom);
489     }
490 
491     /**
492      * Updates {@link #workspacePadding} as a result of any internal value change to reflect the
493      * new workspace padding
494      */
updateWorkspacePadding()495     private void updateWorkspacePadding() {
496         Rect padding = workspacePadding;
497         if (isVerticalBarLayout()) {
498             padding.top = 0;
499             padding.bottom = edgeMarginPx;
500             if (isSeascape()) {
501                 padding.left = hotseatBarSizePx;
502                 padding.right = hotseatBarSidePaddingStartPx;
503             } else {
504                 padding.left = hotseatBarSidePaddingStartPx;
505                 padding.right = hotseatBarSizePx;
506             }
507         } else {
508             int paddingBottom = hotseatBarSizePx + workspacePageIndicatorHeight
509                     - mWorkspacePageIndicatorOverlapWorkspace;
510             if (isTablet) {
511                 // Pad the left and right of the workspace to ensure consistent spacing
512                 // between all icons
513                 // The amount of screen space available for left/right padding.
514                 int availablePaddingX = Math.max(0, widthPx - ((inv.numColumns * cellWidthPx) +
515                         ((inv.numColumns - 1) * cellWidthPx)));
516                 availablePaddingX = (int) Math.min(availablePaddingX,
517                         widthPx * MAX_HORIZONTAL_PADDING_PERCENT);
518                 int availablePaddingY = Math.max(0, heightPx - edgeMarginPx - paddingBottom
519                         - (2 * inv.numRows * cellHeightPx) - hotseatBarTopPaddingPx
520                         - hotseatBarBottomPaddingPx);
521                 padding.set(availablePaddingX / 2, edgeMarginPx + availablePaddingY / 2,
522                         availablePaddingX / 2, paddingBottom + availablePaddingY / 2);
523             } else {
524                 // Pad the top and bottom of the workspace with search/hotseat bar sizes
525                 padding.set(desiredWorkspaceLeftRightMarginPx,
526                         edgeMarginPx,
527                         desiredWorkspaceLeftRightMarginPx,
528                         paddingBottom);
529             }
530         }
531     }
532 
getHotseatLayoutPadding()533     public Rect getHotseatLayoutPadding() {
534         if (isVerticalBarLayout()) {
535             if (isSeascape()) {
536                 mHotseatPadding.set(mInsets.left + hotseatBarSidePaddingStartPx,
537                         mInsets.top, hotseatBarSidePaddingEndPx, mInsets.bottom);
538             } else {
539                 mHotseatPadding.set(hotseatBarSidePaddingEndPx, mInsets.top,
540                         mInsets.right + hotseatBarSidePaddingStartPx, mInsets.bottom);
541             }
542         } else {
543             // We want the edges of the hotseat to line up with the edges of the workspace, but the
544             // icons in the hotseat are a different size, and so don't line up perfectly. To account
545             // for this, we pad the left and right of the hotseat with half of the difference of a
546             // workspace cell vs a hotseat cell.
547             float workspaceCellWidth = (float) widthPx / inv.numColumns;
548             float hotseatCellWidth = (float) widthPx / inv.numHotseatIcons;
549             int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2);
550             mHotseatPadding.set(
551                     hotseatAdjustment + workspacePadding.left + cellLayoutPaddingLeftRightPx
552                             + mInsets.left,
553                     hotseatBarTopPaddingPx,
554                     hotseatAdjustment + workspacePadding.right + cellLayoutPaddingLeftRightPx
555                             + mInsets.right,
556                     hotseatBarBottomPaddingPx + mInsets.bottom + cellLayoutBottomPaddingPx);
557         }
558         return mHotseatPadding;
559     }
560 
561     /**
562      * @return the bounds for which the open folders should be contained within
563      */
getAbsoluteOpenFolderBounds()564     public Rect getAbsoluteOpenFolderBounds() {
565         if (isVerticalBarLayout()) {
566             // Folders should only appear right of the drop target bar and left of the hotseat
567             return new Rect(mInsets.left + dropTargetBarSizePx + edgeMarginPx,
568                     mInsets.top,
569                     mInsets.left + availableWidthPx - hotseatBarSizePx - edgeMarginPx,
570                     mInsets.top + availableHeightPx);
571         } else {
572             // Folders should only appear below the drop target bar and above the hotseat
573             return new Rect(mInsets.left + edgeMarginPx,
574                     mInsets.top + dropTargetBarSizePx + edgeMarginPx,
575                     mInsets.left + availableWidthPx - edgeMarginPx,
576                     mInsets.top + availableHeightPx - hotseatBarSizePx
577                             - workspacePageIndicatorHeight - edgeMarginPx);
578         }
579     }
580 
calculateCellWidth(int width, int countX)581     public static int calculateCellWidth(int width, int countX) {
582         return width / countX;
583     }
calculateCellHeight(int height, int countY)584     public static int calculateCellHeight(int height, int countY) {
585         return height / countY;
586     }
587 
588     /**
589      * When {@code true}, the device is in landscape mode and the hotseat is on the right column.
590      * When {@code false}, either device is in portrait mode or the device is in landscape mode and
591      * the hotseat is on the bottom row.
592      */
isVerticalBarLayout()593     public boolean isVerticalBarLayout() {
594         return isLandscape && transposeLayoutWithOrientation;
595     }
596 
597     /**
598      * Returns true when the number of workspace columns and all apps columns differs.
599      */
allAppsHasDifferentNumColumns()600     private boolean allAppsHasDifferentNumColumns() {
601         return inv.numAllAppsColumns != inv.numColumns;
602     }
603 
604     /**
605      * Updates orientation information and returns true if it has changed from the previous value.
606      */
updateIsSeascape(Context context)607     public boolean updateIsSeascape(Context context) {
608         if (isVerticalBarLayout()) {
609             boolean isSeascape = DefaultDisplay.INSTANCE.get(context).getInfo().rotation
610                     == Surface.ROTATION_270;
611             if (mIsSeascape != isSeascape) {
612                 mIsSeascape = isSeascape;
613                 return true;
614             }
615         }
616         return false;
617     }
618 
isSeascape()619     public boolean isSeascape() {
620         return isVerticalBarLayout() && mIsSeascape;
621     }
622 
shouldFadeAdjacentWorkspaceScreens()623     public boolean shouldFadeAdjacentWorkspaceScreens() {
624         return isVerticalBarLayout() || isLargeTablet;
625     }
626 
getCellHeight(@ontainerType int containerType)627     public int getCellHeight(@ContainerType int containerType) {
628         switch (containerType) {
629             case CellLayout.WORKSPACE:
630                 return cellHeightPx;
631             case CellLayout.FOLDER:
632                 return folderCellHeightPx;
633             case CellLayout.HOTSEAT:
634                 return hotseatCellHeightPx;
635             default:
636                 // ??
637                 return 0;
638         }
639     }
640 
getContext(Context c, DefaultDisplay.Info info, int orientation)641     private static Context getContext(Context c, DefaultDisplay.Info info, int orientation) {
642         Configuration config = new Configuration(c.getResources().getConfiguration());
643         config.orientation = orientation;
644         config.densityDpi = info.metrics.densityDpi;
645         return c.createConfigurationContext(config);
646     }
647 
648     /**
649      * Callback when a component changes the DeviceProfile associated with it, as a result of
650      * configuration change
651      */
652     public interface OnDeviceProfileChangeListener {
653 
654         /**
655          * Called when the device profile is reassigned. Note that for layout and measurements, it
656          * is sufficient to listen for inset changes. Use this callback when you need to perform
657          * a one time operation.
658          */
onDeviceProfileChanged(DeviceProfile dp)659         void onDeviceProfileChanged(DeviceProfile dp);
660     }
661 
662     public static class Builder {
663         private Context mContext;
664         private InvariantDeviceProfile mInv;
665         private DefaultDisplay.Info mInfo;
666 
667         private final Point mWindowPosition = new Point();
668         private Point mMinSize, mMaxSize;
669         private int mWidth, mHeight;
670 
671         private boolean mIsLandscape;
672         private boolean mIsMultiWindowMode = false;
673         private boolean mTransposeLayoutWithOrientation;
674 
Builder(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info)675         public Builder(Context context, InvariantDeviceProfile inv, DefaultDisplay.Info info) {
676             mContext = context;
677             mInv = inv;
678             mInfo = info;
679             mTransposeLayoutWithOrientation = context.getResources()
680                     .getBoolean(R.bool.hotseat_transpose_layout_with_orientation);
681         }
682 
setSizeRange(Point minSize, Point maxSize)683         public Builder setSizeRange(Point minSize, Point maxSize) {
684             mMinSize = minSize;
685             mMaxSize = maxSize;
686             return this;
687         }
688 
setSize(int width, int height)689         public Builder setSize(int width, int height) {
690             mWidth = width;
691             mHeight = height;
692             mIsLandscape = mWidth > mHeight;
693             return this;
694         }
695 
setMultiWindowMode(boolean isMultiWindowMode)696         public Builder setMultiWindowMode(boolean isMultiWindowMode) {
697             mIsMultiWindowMode = isMultiWindowMode;
698             return this;
699         }
700 
701         /**
702          * Sets the window position if not full-screen
703          */
setWindowPosition(int x, int y)704         public Builder setWindowPosition(int x, int y) {
705             mWindowPosition.set(x, y);
706             return this;
707         }
708 
setTransposeLayoutWithOrientation(boolean transposeLayoutWithOrientation)709         public Builder setTransposeLayoutWithOrientation(boolean transposeLayoutWithOrientation) {
710             mTransposeLayoutWithOrientation = transposeLayoutWithOrientation;
711             return this;
712         }
713 
build()714         public DeviceProfile build() {
715             return new DeviceProfile(mContext, mInv, mInfo, mMinSize, mMaxSize,
716                     mWidth, mHeight, mIsLandscape, mIsMultiWindowMode,
717                     mTransposeLayoutWithOrientation, mWindowPosition);
718         }
719     }
720 
721 }
722