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.wm.shell.common;
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 
27 import android.annotation.IntDef;
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.content.ContentResolver;
31 import android.content.Context;
32 import android.content.res.Resources;
33 import android.graphics.Insets;
34 import android.graphics.Rect;
35 import android.os.SystemProperties;
36 import android.provider.Settings;
37 import android.util.DisplayMetrics;
38 import android.view.Display;
39 import android.view.DisplayCutout;
40 import android.view.DisplayInfo;
41 import android.view.InsetsState;
42 import android.view.Surface;
43 import android.view.WindowInsets;
44 
45 import androidx.annotation.VisibleForTesting;
46 
47 import com.android.internal.R;
48 import com.android.internal.policy.SystemBarUtils;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 import java.util.Objects;
53 
54 /**
55  * Contains information about the layout-properties of a display. This refers to internal layout
56  * like insets/cutout/rotation. In general, this can be thought of as the shell analog to
57  * DisplayPolicy.
58  */
59 public class DisplayLayout {
60     @IntDef(prefix = { "NAV_BAR_" }, value = {
61             NAV_BAR_LEFT,
62             NAV_BAR_RIGHT,
63             NAV_BAR_BOTTOM,
64     })
65     @Retention(RetentionPolicy.SOURCE)
66     public @interface NavBarPosition {}
67 
68     // Navigation bar position values
69     public static final int NAV_BAR_LEFT = 1 << 0;
70     public static final int NAV_BAR_RIGHT = 1 << 1;
71     public static final int NAV_BAR_BOTTOM = 1 << 2;
72 
73     private int mUiMode;
74     private int mWidth;
75     private int mHeight;
76     private DisplayCutout mCutout;
77     private int mRotation;
78     private int mDensityDpi;
79     private final Rect mNonDecorInsets = new Rect();
80     private final Rect mStableInsets = new Rect();
81     private boolean mHasNavigationBar = false;
82     private boolean mHasStatusBar = false;
83     private int mNavBarFrameHeight = 0;
84     private boolean mAllowSeamlessRotationDespiteNavBarMoving = false;
85     private boolean mNavigationBarCanMove = false;
86     private boolean mReverseDefaultRotation = false;
87     private InsetsState mInsetsState = new InsetsState();
88 
89     /**
90      * Different from {@link #equals(Object)}, this method compares the basic geometry properties
91      * of two {@link DisplayLayout} objects including width, height, rotation, density, cutout.
92      * @return {@code true} if the given {@link DisplayLayout} is identical geometry wise.
93      */
isSameGeometry(@onNull DisplayLayout other)94     public boolean isSameGeometry(@NonNull DisplayLayout other) {
95         return mWidth == other.mWidth
96                 && mHeight == other.mHeight
97                 && mRotation == other.mRotation
98                 && mDensityDpi == other.mDensityDpi
99                 && Objects.equals(mCutout, other.mCutout);
100     }
101 
102     @Override
equals(Object o)103     public boolean equals(Object o) {
104         if (this == o) return true;
105         if (!(o instanceof DisplayLayout)) return false;
106         final DisplayLayout other = (DisplayLayout) o;
107         return mUiMode == other.mUiMode
108                 && mWidth == other.mWidth
109                 && mHeight == other.mHeight
110                 && Objects.equals(mCutout, other.mCutout)
111                 && mRotation == other.mRotation
112                 && mDensityDpi == other.mDensityDpi
113                 && Objects.equals(mNonDecorInsets, other.mNonDecorInsets)
114                 && Objects.equals(mStableInsets, other.mStableInsets)
115                 && mHasNavigationBar == other.mHasNavigationBar
116                 && mHasStatusBar == other.mHasStatusBar
117                 && mAllowSeamlessRotationDespiteNavBarMoving
118                         == other.mAllowSeamlessRotationDespiteNavBarMoving
119                 && mNavigationBarCanMove == other.mNavigationBarCanMove
120                 && mReverseDefaultRotation == other.mReverseDefaultRotation
121                 && mNavBarFrameHeight == other.mNavBarFrameHeight
122                 && Objects.equals(mInsetsState, other.mInsetsState);
123     }
124 
125     @Override
hashCode()126     public int hashCode() {
127         return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi,
128                 mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar,
129                 mNavBarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving,
130                 mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState);
131     }
132 
133     /**
134      * Create empty layout.
135      */
DisplayLayout()136     public DisplayLayout() {
137     }
138 
139     /**
140      * Construct a custom display layout using a DisplayInfo.
141      * @param info
142      * @param res
143      */
DisplayLayout(DisplayInfo info, Resources res, boolean hasNavigationBar, boolean hasStatusBar)144     public DisplayLayout(DisplayInfo info, Resources res, boolean hasNavigationBar,
145             boolean hasStatusBar) {
146         init(info, res, hasNavigationBar, hasStatusBar);
147     }
148 
149     /**
150      * Construct a display layout based on a live display.
151      * @param context Used for resources.
152      */
DisplayLayout(@onNull Context context, @NonNull Display rawDisplay)153     public DisplayLayout(@NonNull Context context, @NonNull Display rawDisplay) {
154         final int displayId = rawDisplay.getDisplayId();
155         DisplayInfo info = new DisplayInfo();
156         rawDisplay.getDisplayInfo(info);
157         init(info, context.getResources(), hasNavigationBar(info, context, displayId),
158                 hasStatusBar(displayId));
159     }
160 
DisplayLayout(DisplayLayout dl)161     public DisplayLayout(DisplayLayout dl) {
162         set(dl);
163     }
164 
165     /** sets this DisplayLayout to a copy of another on. */
set(DisplayLayout dl)166     public void set(DisplayLayout dl) {
167         mUiMode = dl.mUiMode;
168         mWidth = dl.mWidth;
169         mHeight = dl.mHeight;
170         mCutout = dl.mCutout;
171         mRotation = dl.mRotation;
172         mDensityDpi = dl.mDensityDpi;
173         mHasNavigationBar = dl.mHasNavigationBar;
174         mHasStatusBar = dl.mHasStatusBar;
175         mAllowSeamlessRotationDespiteNavBarMoving = dl.mAllowSeamlessRotationDespiteNavBarMoving;
176         mNavigationBarCanMove = dl.mNavigationBarCanMove;
177         mReverseDefaultRotation = dl.mReverseDefaultRotation;
178         mNavBarFrameHeight = dl.mNavBarFrameHeight;
179         mNonDecorInsets.set(dl.mNonDecorInsets);
180         mStableInsets.set(dl.mStableInsets);
181         mInsetsState.set(dl.mInsetsState, true /* copySources */);
182     }
183 
init(DisplayInfo info, Resources res, boolean hasNavigationBar, boolean hasStatusBar)184     private void init(DisplayInfo info, Resources res, boolean hasNavigationBar,
185             boolean hasStatusBar) {
186         mUiMode = res.getConfiguration().uiMode;
187         mWidth = info.logicalWidth;
188         mHeight = info.logicalHeight;
189         mRotation = info.rotation;
190         mCutout = info.displayCutout;
191         mDensityDpi = info.logicalDensityDpi;
192         mHasNavigationBar = hasNavigationBar;
193         mHasStatusBar = hasStatusBar;
194         mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean(
195             R.bool.config_allowSeamlessRotationDespiteNavBarMoving);
196         mNavigationBarCanMove = res.getBoolean(R.bool.config_navBarCanMove);
197         mReverseDefaultRotation = res.getBoolean(R.bool.config_reverseDefaultRotation);
198         recalcInsets(res);
199     }
200 
201     /**
202      * Updates the current insets.
203      */
setInsets(Resources res, InsetsState state)204     public void setInsets(Resources res, InsetsState state) {
205         mInsetsState = state;
206         recalcInsets(res);
207     }
208 
209     @VisibleForTesting
recalcInsets(Resources res)210     void recalcInsets(Resources res) {
211         computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mInsetsState, mUiMode,
212                 mNonDecorInsets, mHasNavigationBar);
213         mStableInsets.set(mNonDecorInsets);
214         if (mHasStatusBar) {
215             convertNonDecorInsetsToStableInsets(res, mStableInsets, mCutout, mHasStatusBar);
216         }
217         mNavBarFrameHeight = getNavigationBarFrameHeight(res, mWidth > mHeight);
218     }
219 
220     /**
221      * Apply a rotation to this layout and its parameters.
222      */
rotateTo(Resources res, @Surface.Rotation int toRotation)223     public void rotateTo(Resources res, @Surface.Rotation int toRotation) {
224         final int origWidth = mWidth;
225         final int origHeight = mHeight;
226         final int fromRotation = mRotation;
227         final int rotationDelta = (toRotation - fromRotation + 4) % 4;
228         final boolean changeOrient = (rotationDelta % 2) != 0;
229 
230         mRotation = toRotation;
231         if (changeOrient) {
232             mWidth = origHeight;
233             mHeight = origWidth;
234         }
235 
236         if (mCutout != null) {
237             mCutout = mCutout.getRotated(origWidth, origHeight, fromRotation, toRotation);
238         }
239 
240         recalcInsets(res);
241     }
242 
243     /** Get this layout's non-decor insets. */
nonDecorInsets()244     public Rect nonDecorInsets() {
245         return mNonDecorInsets;
246     }
247 
248     /** Get this layout's stable insets. */
stableInsets()249     public Rect stableInsets() {
250         return mStableInsets;
251     }
252 
253     /** Get this layout's width. */
width()254     public int width() {
255         return mWidth;
256     }
257 
258     /** Get this layout's height. */
height()259     public int height() {
260         return mHeight;
261     }
262 
263     /** Get this layout's display rotation. */
rotation()264     public int rotation() {
265         return mRotation;
266     }
267 
268     /** Get this layout's display density. */
densityDpi()269     public int densityDpi() {
270         return mDensityDpi;
271     }
272 
273     /** Get the density scale for the display. */
density()274     public float density() {
275         return mDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
276     }
277 
278     /** Get whether this layout is landscape. */
isLandscape()279     public boolean isLandscape() {
280         return mWidth > mHeight;
281     }
282 
283     /** Get the navbar frame (or window) height (used by ime). */
navBarFrameHeight()284     public int navBarFrameHeight() {
285         return mNavBarFrameHeight;
286     }
287 
288     /** @return whether we can seamlessly rotate even if nav-bar can change sides. */
allowSeamlessRotationDespiteNavBarMoving()289     public boolean allowSeamlessRotationDespiteNavBarMoving() {
290         return mAllowSeamlessRotationDespiteNavBarMoving;
291     }
292 
293     /**
294      * Returns {@code true} if the navigation bar will change sides during rotation and the display
295      * is not square.
296      */
navigationBarCanMove()297     public boolean navigationBarCanMove() {
298         return mNavigationBarCanMove && mWidth != mHeight;
299     }
300 
301     /** @return the rotation that would make the physical display "upside down". */
getUpsideDownRotation()302     public int getUpsideDownRotation() {
303         boolean displayHardwareIsLandscape = mWidth > mHeight;
304         if ((mRotation % 2) != 0) {
305             displayHardwareIsLandscape = !displayHardwareIsLandscape;
306         }
307         if (displayHardwareIsLandscape) {
308             return mReverseDefaultRotation ? Surface.ROTATION_270 : Surface.ROTATION_90;
309         }
310         return Surface.ROTATION_180;
311     }
312 
313     /** Gets the orientation of this layout */
getOrientation()314     public int getOrientation() {
315         return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
316     }
317 
318     /** Gets the calculated stable-bounds for this layout */
getStableBounds(Rect outBounds)319     public void getStableBounds(Rect outBounds) {
320         outBounds.set(0, 0, mWidth, mHeight);
321         outBounds.inset(mStableInsets);
322     }
323 
324     /**
325      * Gets navigation bar position for this layout
326      * @return Navigation bar position for this layout.
327      */
getNavigationBarPosition(Resources res)328     public @NavBarPosition int getNavigationBarPosition(Resources res) {
329         return navigationBarPosition(res, mWidth, mHeight, mRotation);
330     }
331 
332     /** @return {@link DisplayCutout} instance. */
333     @Nullable
getDisplayCutout()334     public DisplayCutout getDisplayCutout() {
335         return mCutout;
336     }
337 
338     /**
339      * Calculates the stable insets if we already have the non-decor insets.
340      */
convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets, DisplayCutout cutout, boolean hasStatusBar)341     private void convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets,
342             DisplayCutout cutout, boolean hasStatusBar) {
343         if (!hasStatusBar) {
344             return;
345         }
346         int statusBarHeight = SystemBarUtils.getStatusBarHeight(res, cutout);
347         inOutInsets.top = Math.max(inOutInsets.top, statusBarHeight);
348     }
349 
350     /**
351      * Calculates the insets for the areas that could never be removed in Honeycomb, i.e. system
352      * bar or button bar.
353      *
354      * @param displayRotation the current display rotation
355      * @param displayWidth the current display width
356      * @param displayHeight the current display height
357      * @param displayCutout the current display cutout
358      * @param outInsets the insets to return
359      */
computeNonDecorInsets(Resources res, int displayRotation, int displayWidth, int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode, Rect outInsets, boolean hasNavigationBar)360     static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth,
361             int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode,
362             Rect outInsets, boolean hasNavigationBar) {
363         outInsets.setEmpty();
364 
365         // Only navigation bar
366         if (hasNavigationBar) {
367             final Insets insets = insetsState.calculateInsets(
368                     insetsState.getDisplayFrame(),
369                     WindowInsets.Type.navigationBars(),
370                     false /* ignoreVisibility */);
371             int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
372             int navBarSize =
373                     getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode);
374             if (position == NAV_BAR_BOTTOM) {
375                 outInsets.bottom = Math.max(insets.bottom , navBarSize);
376             } else if (position == NAV_BAR_RIGHT) {
377                 outInsets.right = Math.max(insets.right , navBarSize);
378             } else if (position == NAV_BAR_LEFT) {
379                 outInsets.left = Math.max(insets.left , navBarSize);
380             }
381         }
382 
383         if (displayCutout != null) {
384             outInsets.left += displayCutout.getSafeInsetLeft();
385             outInsets.top += displayCutout.getSafeInsetTop();
386             outInsets.right += displayCutout.getSafeInsetRight();
387             outInsets.bottom += displayCutout.getSafeInsetBottom();
388         }
389     }
390 
hasNavigationBar(DisplayInfo info, Context context, int displayId)391     static boolean hasNavigationBar(DisplayInfo info, Context context, int displayId) {
392         if (displayId == Display.DEFAULT_DISPLAY) {
393             // Allow a system property to override this. Used by the emulator.
394             final String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
395             if ("1".equals(navBarOverride)) {
396                 return false;
397             } else if ("0".equals(navBarOverride)) {
398                 return true;
399             }
400             return context.getResources().getBoolean(R.bool.config_showNavigationBar);
401         } else {
402             boolean isUntrustedVirtualDisplay = info.type == Display.TYPE_VIRTUAL
403                     && info.ownerUid != SYSTEM_UID;
404             final ContentResolver resolver = context.getContentResolver();
405             boolean forceDesktopOnExternal = Settings.Global.getInt(resolver,
406                     DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
407 
408             return ((info.flags & FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0
409                     || (forceDesktopOnExternal && !isUntrustedVirtualDisplay));
410             // TODO(b/142569966): make sure VR2D and DisplayWindowSettings are moved here somehow.
411         }
412     }
413 
hasStatusBar(int displayId)414     static boolean hasStatusBar(int displayId) {
415         return displayId == Display.DEFAULT_DISPLAY;
416     }
417 
418     /** Retrieve navigation bar position from resources based on rotation and size. */
navigationBarPosition(Resources res, int displayWidth, int displayHeight, int rotation)419     public static @NavBarPosition int navigationBarPosition(Resources res, int displayWidth,
420             int displayHeight, int rotation) {
421         boolean navBarCanMove = displayWidth != displayHeight && res.getBoolean(
422                 com.android.internal.R.bool.config_navBarCanMove);
423         if (navBarCanMove && displayWidth > displayHeight) {
424             if (rotation == Surface.ROTATION_90) {
425                 return NAV_BAR_RIGHT;
426             } else {
427                 return NAV_BAR_LEFT;
428             }
429         }
430         return NAV_BAR_BOTTOM;
431     }
432 
433     /** Retrieve navigation bar size from resources based on side/orientation/ui-mode */
getNavigationBarSize(Resources res, int navBarSide, boolean landscape, int uiMode)434     public static int getNavigationBarSize(Resources res, int navBarSide, boolean landscape,
435             int uiMode) {
436         final boolean carMode = (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR;
437         if (carMode) {
438             if (navBarSide == NAV_BAR_BOTTOM) {
439                 return res.getDimensionPixelSize(landscape
440                         ? R.dimen.navigation_bar_height_landscape_car_mode
441                         : R.dimen.navigation_bar_height_car_mode);
442             } else {
443                 return res.getDimensionPixelSize(R.dimen.navigation_bar_width_car_mode);
444             }
445 
446         } else {
447             if (navBarSide == NAV_BAR_BOTTOM) {
448                 return res.getDimensionPixelSize(landscape
449                         ? R.dimen.navigation_bar_height_landscape
450                         : R.dimen.navigation_bar_height);
451             } else {
452                 return res.getDimensionPixelSize(R.dimen.navigation_bar_width);
453             }
454         }
455     }
456 
457     /** @see com.android.server.wm.DisplayPolicy#getNavigationBarFrameHeight */
getNavigationBarFrameHeight(Resources res, boolean landscape)458     public static int getNavigationBarFrameHeight(Resources res, boolean landscape) {
459         return res.getDimensionPixelSize(landscape
460                 ? R.dimen.navigation_bar_frame_height_landscape
461                 : R.dimen.navigation_bar_frame_height);
462     }
463 }
464