1 /*
2  * Copyright (C) 2019 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.systemui.wm;
18 
19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
21 import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
22 import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
23 import static android.os.Process.SYSTEM_UID;
24 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
25 import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
26 import static android.view.Surface.ROTATION_0;
27 import static android.view.Surface.ROTATION_270;
28 import static android.view.Surface.ROTATION_90;
29 
30 import android.annotation.IntDef;
31 import android.annotation.NonNull;
32 import android.content.ContentResolver;
33 import android.content.Context;
34 import android.content.res.Resources;
35 import android.graphics.Insets;
36 import android.graphics.Rect;
37 import android.os.SystemProperties;
38 import android.provider.Settings;
39 import android.util.DisplayMetrics;
40 import android.util.RotationUtils;
41 import android.util.Size;
42 import android.view.Display;
43 import android.view.DisplayCutout;
44 import android.view.DisplayInfo;
45 import android.view.Gravity;
46 import android.view.Surface;
47 
48 import com.android.internal.R;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 
53 /**
54  * Contains information about the layout-properties of a display. This refers to internal layout
55  * like insets/cutout/rotation. In general, this can be thought of as the System-UI analog to
56  * DisplayPolicy.
57  */
58 public class DisplayLayout {
59     @IntDef(prefix = { "NAV_BAR_" }, value = {
60             NAV_BAR_LEFT,
61             NAV_BAR_RIGHT,
62             NAV_BAR_BOTTOM,
63     })
64     @Retention(RetentionPolicy.SOURCE)
65     public @interface NavBarPosition {}
66 
67     // Navigation bar position values
68     public static final int NAV_BAR_LEFT = 1 << 0;
69     public static final int NAV_BAR_RIGHT = 1 << 1;
70     public static final int NAV_BAR_BOTTOM = 1 << 2;
71 
72     private int mUiMode;
73     private int mWidth;
74     private int mHeight;
75     private DisplayCutout mCutout;
76     private int mRotation;
77     private int mDensityDpi;
78     private final Rect mNonDecorInsets = new Rect();
79     private final Rect mStableInsets = new Rect();
80     private boolean mHasNavigationBar = false;
81     private boolean mHasStatusBar = false;
82     private int mNavBarFrameHeight = 0;
83 
84     /**
85      * Create empty layout.
86      */
DisplayLayout()87     public DisplayLayout() {
88     }
89 
90     /**
91      * Construct a custom display layout using a DisplayInfo.
92      * @param info
93      * @param res
94      */
DisplayLayout(DisplayInfo info, Resources res, boolean hasNavigationBar, boolean hasStatusBar)95     public DisplayLayout(DisplayInfo info, Resources res, boolean hasNavigationBar,
96             boolean hasStatusBar) {
97         init(info, res, hasNavigationBar, hasStatusBar);
98     }
99 
100     /**
101      * Construct a display layout based on a live display.
102      * @param context Used for resources.
103      */
DisplayLayout(@onNull Context context, @NonNull Display rawDisplay)104     public DisplayLayout(@NonNull Context context, @NonNull Display rawDisplay) {
105         final int displayId = rawDisplay.getDisplayId();
106         DisplayInfo info = new DisplayInfo();
107         rawDisplay.getDisplayInfo(info);
108         init(info, context.getResources(), hasNavigationBar(info, context, displayId),
109                 hasStatusBar(displayId));
110     }
111 
DisplayLayout(DisplayLayout dl)112     public DisplayLayout(DisplayLayout dl) {
113         set(dl);
114     }
115 
116     /** sets this DisplayLayout to a copy of another on. */
set(DisplayLayout dl)117     public void set(DisplayLayout dl) {
118         mUiMode = dl.mUiMode;
119         mWidth = dl.mWidth;
120         mHeight = dl.mHeight;
121         mCutout = dl.mCutout;
122         mRotation = dl.mRotation;
123         mDensityDpi = dl.mDensityDpi;
124         mHasNavigationBar = dl.mHasNavigationBar;
125         mHasStatusBar = dl.mHasStatusBar;
126         mNonDecorInsets.set(dl.mNonDecorInsets);
127         mStableInsets.set(dl.mStableInsets);
128     }
129 
init(DisplayInfo info, Resources res, boolean hasNavigationBar, boolean hasStatusBar)130     private void init(DisplayInfo info, Resources res, boolean hasNavigationBar,
131             boolean hasStatusBar) {
132         mUiMode = res.getConfiguration().uiMode;
133         mWidth = info.logicalWidth;
134         mHeight = info.logicalHeight;
135         mRotation = info.rotation;
136         mCutout = info.displayCutout;
137         mDensityDpi = info.logicalDensityDpi;
138         mHasNavigationBar = hasNavigationBar;
139         mHasStatusBar = hasStatusBar;
140         recalcInsets(res);
141     }
142 
recalcInsets(Resources res)143     private void recalcInsets(Resources res) {
144         computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets,
145                 mHasNavigationBar);
146         mStableInsets.set(mNonDecorInsets);
147         if (mHasStatusBar) {
148             convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar);
149         }
150         mNavBarFrameHeight = getNavigationBarFrameHeight(res, mWidth > mHeight);
151     }
152 
153     /**
154      * Apply a rotation to this layout and its parameters.
155      * @param res
156      * @param targetRotation
157      */
rotateTo(Resources res, @Surface.Rotation int targetRotation)158     public void rotateTo(Resources res, @Surface.Rotation int targetRotation) {
159         final int rotationDelta = (targetRotation - mRotation + 4) % 4;
160         final boolean changeOrient = (rotationDelta % 2) != 0;
161 
162         final int origWidth = mWidth;
163         final int origHeight = mHeight;
164 
165         mRotation = targetRotation;
166         if (changeOrient) {
167             mWidth = origHeight;
168             mHeight = origWidth;
169         }
170 
171         if (mCutout != null && !mCutout.isEmpty()) {
172             mCutout = calculateDisplayCutoutForRotation(mCutout, rotationDelta, origWidth,
173                     origHeight);
174         }
175 
176         recalcInsets(res);
177     }
178 
179     /** Get this layout's non-decor insets. */
nonDecorInsets()180     public Rect nonDecorInsets() {
181         return mNonDecorInsets;
182     }
183 
184     /** Get this layout's stable insets. */
stableInsets()185     public Rect stableInsets() {
186         return mStableInsets;
187     }
188 
189     /** Get this layout's width. */
width()190     public int width() {
191         return mWidth;
192     }
193 
194     /** Get this layout's height. */
height()195     public int height() {
196         return mHeight;
197     }
198 
199     /** Get this layout's display rotation. */
rotation()200     public int rotation() {
201         return mRotation;
202     }
203 
204     /** Get this layout's display density. */
densityDpi()205     public int densityDpi() {
206         return mDensityDpi;
207     }
208 
209     /** Get the density scale for the display. */
density()210     public float density() {
211         return mDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
212     }
213 
214     /** Get whether this layout is landscape. */
isLandscape()215     public boolean isLandscape() {
216         return mWidth > mHeight;
217     }
218 
219     /** Get the navbar frame height (used by ime). */
navBarFrameHeight()220     public int navBarFrameHeight() {
221         return mNavBarFrameHeight;
222     }
223 
224     /** Gets the orientation of this layout */
getOrientation()225     public int getOrientation() {
226         return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
227     }
228 
229     /** Gets the calculated stable-bounds for this layout */
getStableBounds(Rect outBounds)230     public void getStableBounds(Rect outBounds) {
231         outBounds.set(0, 0, mWidth, mHeight);
232         outBounds.inset(mStableInsets);
233     }
234 
235     /**
236      * Gets navigation bar position for this layout
237      * @return Navigation bar position for this layout.
238      */
getNavigationBarPosition(Resources res)239     public @NavBarPosition int getNavigationBarPosition(Resources res) {
240         return navigationBarPosition(res, mWidth, mHeight, mRotation);
241     }
242 
243     /**
244      * Rotates bounds as if parentBounds and bounds are a group. The group is rotated by `delta`
245      * 90-degree counter-clockwise increments. This assumes that parentBounds is at 0,0 and
246      * remains at 0,0 after rotation.
247      *
248      * Only 'bounds' is mutated.
249      */
rotateBounds(Rect inOutBounds, Rect parentBounds, int delta)250     public static void rotateBounds(Rect inOutBounds, Rect parentBounds, int delta) {
251         int rdelta = ((delta % 4) + 4) % 4;
252         int origLeft = inOutBounds.left;
253         switch (rdelta) {
254             case 0:
255                 return;
256             case 1:
257                 inOutBounds.left = inOutBounds.top;
258                 inOutBounds.top = parentBounds.right - inOutBounds.right;
259                 inOutBounds.right = inOutBounds.bottom;
260                 inOutBounds.bottom = parentBounds.right - origLeft;
261                 return;
262             case 2:
263                 inOutBounds.left = parentBounds.right - inOutBounds.right;
264                 inOutBounds.right = parentBounds.right - origLeft;
265                 return;
266             case 3:
267                 inOutBounds.left = parentBounds.bottom - inOutBounds.bottom;
268                 inOutBounds.bottom = inOutBounds.right;
269                 inOutBounds.right = parentBounds.bottom - inOutBounds.top;
270                 inOutBounds.top = origLeft;
271                 return;
272         }
273     }
274 
275     /**
276      * Calculates the stable insets if we already have the non-decor insets.
277      */
convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets, int displayWidth, int displayHeight, boolean hasStatusBar)278     private static void convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets,
279             int displayWidth, int displayHeight, boolean hasStatusBar) {
280         if (!hasStatusBar) {
281             return;
282         }
283         int statusBarHeight = getStatusBarHeight(displayWidth > displayHeight, res);
284         inOutInsets.top = Math.max(inOutInsets.top, statusBarHeight);
285     }
286 
287     /**
288      * Calculates the insets for the areas that could never be removed in Honeycomb, i.e. system
289      * bar or button bar.
290      *
291      * @param displayRotation the current display rotation
292      * @param displayWidth the current display width
293      * @param displayHeight the current display height
294      * @param displayCutout the current display cutout
295      * @param outInsets the insets to return
296      */
computeNonDecorInsets(Resources res, int displayRotation, int displayWidth, int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, boolean hasNavigationBar)297     static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth,
298             int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
299             boolean hasNavigationBar) {
300         outInsets.setEmpty();
301 
302         // Only navigation bar
303         if (hasNavigationBar) {
304             int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
305             int navBarSize =
306                     getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode);
307             if (position == NAV_BAR_BOTTOM) {
308                 outInsets.bottom = navBarSize;
309             } else if (position == NAV_BAR_RIGHT) {
310                 outInsets.right = navBarSize;
311             } else if (position == NAV_BAR_LEFT) {
312                 outInsets.left = navBarSize;
313             }
314         }
315 
316         if (displayCutout != null) {
317             outInsets.left += displayCutout.getSafeInsetLeft();
318             outInsets.top += displayCutout.getSafeInsetTop();
319             outInsets.right += displayCutout.getSafeInsetRight();
320             outInsets.bottom += displayCutout.getSafeInsetBottom();
321         }
322     }
323 
324     /**
325      * Calculates the stable insets without running a layout.
326      *
327      * @param displayRotation the current display rotation
328      * @param displayWidth the current display width
329      * @param displayHeight the current display height
330      * @param displayCutout the current display cutout
331      * @param outInsets the insets to return
332      */
computeStableInsets(Resources res, int displayRotation, int displayWidth, int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, boolean hasNavigationBar, boolean hasStatusBar)333     static void computeStableInsets(Resources res, int displayRotation, int displayWidth,
334             int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
335             boolean hasNavigationBar, boolean hasStatusBar) {
336         outInsets.setEmpty();
337 
338         // Navigation bar and status bar.
339         computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout,
340                 uiMode, outInsets, hasNavigationBar);
341         convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight,
342                 hasStatusBar);
343     }
344 
345     /** Retrieve the statusbar height from resources. */
getStatusBarHeight(boolean landscape, Resources res)346     static int getStatusBarHeight(boolean landscape, Resources res) {
347         return landscape ? res.getDimensionPixelSize(
348                     com.android.internal.R.dimen.status_bar_height_landscape)
349                     : res.getDimensionPixelSize(
350                             com.android.internal.R.dimen.status_bar_height_portrait);
351     }
352 
353     /** Calculate the DisplayCutout for a particular display size/rotation. */
calculateDisplayCutoutForRotation( DisplayCutout cutout, int rotation, int displayWidth, int displayHeight)354     public static DisplayCutout calculateDisplayCutoutForRotation(
355             DisplayCutout cutout, int rotation, int displayWidth, int displayHeight) {
356         if (cutout == null || cutout == DisplayCutout.NO_CUTOUT) {
357             return null;
358         }
359         final Insets waterfallInsets =
360                 RotationUtils.rotateInsets(cutout.getWaterfallInsets(), rotation);
361         if (rotation == ROTATION_0) {
362             return computeSafeInsets(cutout, displayWidth, displayHeight);
363         }
364         final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
365         Rect[] cutoutRects = cutout.getBoundingRectsAll();
366         final Rect[] newBounds = new Rect[cutoutRects.length];
367         final Rect displayBounds = new Rect(0, 0, displayWidth, displayHeight);
368         for (int i = 0; i < cutoutRects.length; ++i) {
369             final Rect rect = new Rect(cutoutRects[i]);
370             if (!rect.isEmpty()) {
371                 rotateBounds(rect, displayBounds, rotation);
372             }
373             newBounds[getBoundIndexFromRotation(i, rotation)] = rect;
374         }
375         return computeSafeInsets(
376                 DisplayCutout.fromBoundsAndWaterfall(newBounds, waterfallInsets),
377                 rotated ? displayHeight : displayWidth,
378                 rotated ? displayWidth : displayHeight);
379     }
380 
getBoundIndexFromRotation(int index, int rotation)381     private static int getBoundIndexFromRotation(int index, int rotation) {
382         return (index - rotation) < 0
383                 ? index - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH
384                 : index - rotation;
385     }
386 
387     /** Calculate safe insets. */
computeSafeInsets(DisplayCutout inner, int displayWidth, int displayHeight)388     public static DisplayCutout computeSafeInsets(DisplayCutout inner,
389             int displayWidth, int displayHeight) {
390         if (inner == DisplayCutout.NO_CUTOUT) {
391             return null;
392         }
393 
394         final Size displaySize = new Size(displayWidth, displayHeight);
395         final Rect safeInsets = computeSafeInsets(displaySize, inner);
396         return inner.replaceSafeInsets(safeInsets);
397     }
398 
computeSafeInsets( Size displaySize, DisplayCutout cutout)399     private static Rect computeSafeInsets(
400             Size displaySize, DisplayCutout cutout) {
401         if (displaySize.getWidth() == displaySize.getHeight()) {
402             throw new UnsupportedOperationException("not implemented: display=" + displaySize
403                     + " cutout=" + cutout);
404         }
405 
406         int leftInset = Math.max(cutout.getWaterfallInsets().left,
407                 findCutoutInsetForSide(displaySize, cutout.getBoundingRectLeft(), Gravity.LEFT));
408         int topInset = Math.max(cutout.getWaterfallInsets().top,
409                 findCutoutInsetForSide(displaySize, cutout.getBoundingRectTop(), Gravity.TOP));
410         int rightInset = Math.max(cutout.getWaterfallInsets().right,
411                 findCutoutInsetForSide(displaySize, cutout.getBoundingRectRight(), Gravity.RIGHT));
412         int bottomInset = Math.max(cutout.getWaterfallInsets().bottom,
413                 findCutoutInsetForSide(displaySize, cutout.getBoundingRectBottom(),
414                         Gravity.BOTTOM));
415 
416         return new Rect(leftInset, topInset, rightInset, bottomInset);
417     }
418 
findCutoutInsetForSide(Size display, Rect boundingRect, int gravity)419     private static int findCutoutInsetForSide(Size display, Rect boundingRect, int gravity) {
420         if (boundingRect.isEmpty()) {
421             return 0;
422         }
423 
424         int inset = 0;
425         switch (gravity) {
426             case Gravity.TOP:
427                 return Math.max(inset, boundingRect.bottom);
428             case Gravity.BOTTOM:
429                 return Math.max(inset, display.getHeight() - boundingRect.top);
430             case Gravity.LEFT:
431                 return Math.max(inset, boundingRect.right);
432             case Gravity.RIGHT:
433                 return Math.max(inset, display.getWidth() - boundingRect.left);
434             default:
435                 throw new IllegalArgumentException("unknown gravity: " + gravity);
436         }
437     }
438 
hasNavigationBar(DisplayInfo info, Context context, int displayId)439     static boolean hasNavigationBar(DisplayInfo info, Context context, int displayId) {
440         if (displayId == Display.DEFAULT_DISPLAY) {
441             // Allow a system property to override this. Used by the emulator.
442             final String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
443             if ("1".equals(navBarOverride)) {
444                 return false;
445             } else if ("0".equals(navBarOverride)) {
446                 return true;
447             }
448             return context.getResources().getBoolean(R.bool.config_showNavigationBar);
449         } else {
450             boolean isUntrustedVirtualDisplay = info.type == Display.TYPE_VIRTUAL
451                     && info.ownerUid != SYSTEM_UID;
452             final ContentResolver resolver = context.getContentResolver();
453             boolean forceDesktopOnExternal = Settings.Global.getInt(resolver,
454                     DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
455 
456             return ((info.flags & FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0
457                     || (forceDesktopOnExternal && !isUntrustedVirtualDisplay));
458             // TODO(b/142569966): make sure VR2D and DisplayWindowSettings are moved here somehow.
459         }
460     }
461 
hasStatusBar(int displayId)462     static boolean hasStatusBar(int displayId) {
463         return displayId == Display.DEFAULT_DISPLAY;
464     }
465 
466     /** Retrieve navigation bar position from resources based on rotation and size. */
navigationBarPosition(Resources res, int displayWidth, int displayHeight, int rotation)467     public static @NavBarPosition int navigationBarPosition(Resources res, int displayWidth,
468             int displayHeight, int rotation) {
469         boolean navBarCanMove = displayWidth != displayHeight && res.getBoolean(
470                 com.android.internal.R.bool.config_navBarCanMove);
471         if (navBarCanMove && displayWidth > displayHeight) {
472             if (rotation == Surface.ROTATION_90) {
473                 return NAV_BAR_RIGHT;
474             } else {
475                 return NAV_BAR_LEFT;
476             }
477         }
478         return NAV_BAR_BOTTOM;
479     }
480 
481     /** Retrieve navigation bar size from resources based on side/orientation/ui-mode */
getNavigationBarSize(Resources res, int navBarSide, boolean landscape, int uiMode)482     public static int getNavigationBarSize(Resources res, int navBarSide, boolean landscape,
483             int uiMode) {
484         final boolean carMode = (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR;
485         if (carMode) {
486             if (navBarSide == NAV_BAR_BOTTOM) {
487                 return res.getDimensionPixelSize(landscape
488                         ? R.dimen.navigation_bar_height_landscape_car_mode
489                         : R.dimen.navigation_bar_height_car_mode);
490             } else {
491                 return res.getDimensionPixelSize(R.dimen.navigation_bar_width_car_mode);
492             }
493 
494         } else {
495             if (navBarSide == NAV_BAR_BOTTOM) {
496                 return res.getDimensionPixelSize(landscape
497                         ? R.dimen.navigation_bar_height_landscape
498                         : R.dimen.navigation_bar_height);
499             } else {
500                 return res.getDimensionPixelSize(R.dimen.navigation_bar_width);
501             }
502         }
503     }
504 
505     /** @see com.android.server.wm.DisplayPolicy#getNavigationBarFrameHeight */
getNavigationBarFrameHeight(Resources res, boolean landscape)506     public static int getNavigationBarFrameHeight(Resources res, boolean landscape) {
507         return res.getDimensionPixelSize(landscape
508                 ? R.dimen.navigation_bar_frame_height_landscape
509                 : R.dimen.navigation_bar_frame_height);
510     }
511 }
512