blob: cfec1c07ff1defab01aebb1b6ee13ba0c39097c4 [file] [log] [blame]
/*
* Copyright (C) 2019 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.systemui.wm;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
import static android.os.Process.SYSTEM_UID;
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.RotationUtils;
import android.util.Size;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.Surface;
import com.android.internal.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Contains information about the layout-properties of a display. This refers to internal layout
* like insets/cutout/rotation. In general, this can be thought of as the System-UI analog to
* DisplayPolicy.
*/
public class DisplayLayout {
@IntDef(prefix = { "NAV_BAR_" }, value = {
NAV_BAR_LEFT,
NAV_BAR_RIGHT,
NAV_BAR_BOTTOM,
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavBarPosition {}
// Navigation bar position values
public static final int NAV_BAR_LEFT = 1 << 0;
public static final int NAV_BAR_RIGHT = 1 << 1;
public static final int NAV_BAR_BOTTOM = 1 << 2;
private int mUiMode;
private int mWidth;
private int mHeight;
private DisplayCutout mCutout;
private int mRotation;
private int mDensityDpi;
private final Rect mNonDecorInsets = new Rect();
private final Rect mStableInsets = new Rect();
private boolean mHasNavigationBar = false;
private boolean mHasStatusBar = false;
/**
* Create empty layout.
*/
public DisplayLayout() {
}
/**
* Construct a custom display layout using a DisplayInfo.
* @param info
* @param res
*/
public DisplayLayout(DisplayInfo info, Resources res, boolean hasNavigationBar,
boolean hasStatusBar) {
init(info, res, hasNavigationBar, hasStatusBar);
}
/**
* Construct a display layout based on a live display.
* @param context Used for resources.
*/
public DisplayLayout(@NonNull Context context, @NonNull Display rawDisplay) {
final int displayId = rawDisplay.getDisplayId();
DisplayInfo info = new DisplayInfo();
rawDisplay.getDisplayInfo(info);
init(info, context.getResources(), hasNavigationBar(info, context, displayId),
hasStatusBar(displayId));
}
public DisplayLayout(DisplayLayout dl) {
set(dl);
}
/** sets this DisplayLayout to a copy of another on. */
public void set(DisplayLayout dl) {
mUiMode = dl.mUiMode;
mWidth = dl.mWidth;
mHeight = dl.mHeight;
mCutout = dl.mCutout;
mRotation = dl.mRotation;
mDensityDpi = dl.mDensityDpi;
mHasNavigationBar = dl.mHasNavigationBar;
mHasStatusBar = dl.mHasStatusBar;
mNonDecorInsets.set(dl.mNonDecorInsets);
mStableInsets.set(dl.mStableInsets);
}
private void init(DisplayInfo info, Resources res, boolean hasNavigationBar,
boolean hasStatusBar) {
mUiMode = res.getConfiguration().uiMode;
mWidth = info.logicalWidth;
mHeight = info.logicalHeight;
mRotation = info.rotation;
mCutout = info.displayCutout;
mDensityDpi = info.logicalDensityDpi;
mHasNavigationBar = hasNavigationBar;
mHasStatusBar = hasStatusBar;
recalcInsets(res);
}
private void recalcInsets(Resources res) {
computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets,
mHasNavigationBar);
mStableInsets.set(mNonDecorInsets);
if (mHasStatusBar) {
convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar);
}
}
/**
* Apply a rotation to this layout and its parameters.
* @param res
* @param targetRotation
*/
public void rotateTo(Resources res, @Surface.Rotation int targetRotation) {
final int rotationDelta = (targetRotation - mRotation + 4) % 4;
final boolean changeOrient = (rotationDelta % 2) != 0;
final int origWidth = mWidth;
final int origHeight = mHeight;
mRotation = targetRotation;
if (changeOrient) {
mWidth = origHeight;
mHeight = origWidth;
}
if (mCutout != null && !mCutout.isEmpty()) {
mCutout = calculateDisplayCutoutForRotation(mCutout, rotationDelta, origWidth,
origHeight);
}
recalcInsets(res);
}
/** Get this layout's non-decor insets. */
public Rect nonDecorInsets() {
return mNonDecorInsets;
}
/** Get this layout's stable insets. */
public Rect stableInsets() {
return mStableInsets;
}
/** Get this layout's width. */
public int width() {
return mWidth;
}
/** Get this layout's height. */
public int height() {
return mHeight;
}
/** Get this layout's display rotation. */
public int rotation() {
return mRotation;
}
/** Get this layout's display density. */
public int densityDpi() {
return mDensityDpi;
}
/** Get the density scale for the display. */
public float density() {
return mDensityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
}
/** Get whether this layout is landscape. */
public boolean isLandscape() {
return mWidth > mHeight;
}
/** Gets the orientation of this layout */
public int getOrientation() {
return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
}
/** Gets the calculated stable-bounds for this layout */
public void getStableBounds(Rect outBounds) {
outBounds.set(0, 0, mWidth, mHeight);
outBounds.inset(mStableInsets);
}
/**
* Gets navigation bar position for this layout
* @return Navigation bar position for this layout.
*/
public @NavBarPosition int getNavigationBarPosition(Resources res) {
return navigationBarPosition(res, mWidth, mHeight, mRotation);
}
/**
* Rotates bounds as if parentBounds and bounds are a group. The group is rotated by `delta`
* 90-degree counter-clockwise increments. This assumes that parentBounds is at 0,0 and
* remains at 0,0 after rotation.
*
* Only 'bounds' is mutated.
*/
public static void rotateBounds(Rect inOutBounds, Rect parentBounds, int delta) {
int rdelta = ((delta % 4) + 4) % 4;
int origLeft = inOutBounds.left;
switch (rdelta) {
case 0:
return;
case 1:
inOutBounds.left = inOutBounds.top;
inOutBounds.top = parentBounds.right - inOutBounds.right;
inOutBounds.right = inOutBounds.bottom;
inOutBounds.bottom = parentBounds.right - origLeft;
return;
case 2:
inOutBounds.left = parentBounds.right - inOutBounds.right;
inOutBounds.right = parentBounds.right - origLeft;
return;
case 3:
inOutBounds.left = parentBounds.bottom - inOutBounds.bottom;
inOutBounds.bottom = inOutBounds.right;
inOutBounds.right = parentBounds.bottom - inOutBounds.top;
inOutBounds.top = origLeft;
return;
}
}
/**
* Calculates the stable insets if we already have the non-decor insets.
*/
private static void convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets,
int displayWidth, int displayHeight, boolean hasStatusBar) {
if (!hasStatusBar) {
return;
}
int statusBarHeight = getStatusBarHeight(displayWidth > displayHeight, res);
inOutInsets.top = Math.max(inOutInsets.top, statusBarHeight);
}
/**
* Calculates the insets for the areas that could never be removed in Honeycomb, i.e. system
* bar or button bar.
*
* @param displayRotation the current display rotation
* @param displayWidth the current display width
* @param displayHeight the current display height
* @param displayCutout the current display cutout
* @param outInsets the insets to return
*/
static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth,
int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
boolean hasNavigationBar) {
outInsets.setEmpty();
// Only navigation bar
if (hasNavigationBar) {
int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation);
int navBarSize =
getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode);
if (position == NAV_BAR_BOTTOM) {
outInsets.bottom = navBarSize;
} else if (position == NAV_BAR_RIGHT) {
outInsets.right = navBarSize;
} else if (position == NAV_BAR_LEFT) {
outInsets.left = navBarSize;
}
}
if (displayCutout != null) {
outInsets.left += displayCutout.getSafeInsetLeft();
outInsets.top += displayCutout.getSafeInsetTop();
outInsets.right += displayCutout.getSafeInsetRight();
outInsets.bottom += displayCutout.getSafeInsetBottom();
}
}
/**
* Calculates the stable insets without running a layout.
*
* @param displayRotation the current display rotation
* @param displayWidth the current display width
* @param displayHeight the current display height
* @param displayCutout the current display cutout
* @param outInsets the insets to return
*/
static void computeStableInsets(Resources res, int displayRotation, int displayWidth,
int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets,
boolean hasNavigationBar, boolean hasStatusBar) {
outInsets.setEmpty();
// Navigation bar and status bar.
computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout,
uiMode, outInsets, hasNavigationBar);
convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight,
hasStatusBar);
}
/** Retrieve the statusbar height from resources. */
static int getStatusBarHeight(boolean landscape, Resources res) {
return landscape ? res.getDimensionPixelSize(
com.android.internal.R.dimen.status_bar_height_landscape)
: res.getDimensionPixelSize(
com.android.internal.R.dimen.status_bar_height_portrait);
}
/** Calculate the DisplayCutout for a particular display size/rotation. */
public static DisplayCutout calculateDisplayCutoutForRotation(
DisplayCutout cutout, int rotation, int displayWidth, int displayHeight) {
if (cutout == null || cutout == DisplayCutout.NO_CUTOUT) {
return null;
}
final Insets waterfallInsets =
RotationUtils.rotateInsets(cutout.getWaterfallInsets(), rotation);
if (rotation == ROTATION_0) {
return computeSafeInsets(cutout, displayWidth, displayHeight);
}
final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
Rect[] cutoutRects = cutout.getBoundingRectsAll();
final Rect[] newBounds = new Rect[cutoutRects.length];
final Rect displayBounds = new Rect(0, 0, displayWidth, displayHeight);
for (int i = 0; i < cutoutRects.length; ++i) {
final Rect rect = new Rect(cutoutRects[i]);
if (!rect.isEmpty()) {
rotateBounds(rect, displayBounds, rotation);
}
newBounds[getBoundIndexFromRotation(i, rotation)] = rect;
}
return computeSafeInsets(
DisplayCutout.fromBoundsAndWaterfall(newBounds, waterfallInsets),
rotated ? displayHeight : displayWidth,
rotated ? displayWidth : displayHeight);
}
private static int getBoundIndexFromRotation(int index, int rotation) {
return (index - rotation) < 0
? index - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH
: index - rotation;
}
/** Calculate safe insets. */
public static DisplayCutout computeSafeInsets(DisplayCutout inner,
int displayWidth, int displayHeight) {
if (inner == DisplayCutout.NO_CUTOUT) {
return null;
}
final Size displaySize = new Size(displayWidth, displayHeight);
final Rect safeInsets = computeSafeInsets(displaySize, inner);
return inner.replaceSafeInsets(safeInsets);
}
private static Rect computeSafeInsets(
Size displaySize, DisplayCutout cutout) {
if (displaySize.getWidth() == displaySize.getHeight()) {
throw new UnsupportedOperationException("not implemented: display=" + displaySize
+ " cutout=" + cutout);
}
int leftInset = Math.max(cutout.getWaterfallInsets().left,
findCutoutInsetForSide(displaySize, cutout.getBoundingRectLeft(), Gravity.LEFT));
int topInset = Math.max(cutout.getWaterfallInsets().top,
findCutoutInsetForSide(displaySize, cutout.getBoundingRectTop(), Gravity.TOP));
int rightInset = Math.max(cutout.getWaterfallInsets().right,
findCutoutInsetForSide(displaySize, cutout.getBoundingRectRight(), Gravity.RIGHT));
int bottomInset = Math.max(cutout.getWaterfallInsets().bottom,
findCutoutInsetForSide(displaySize, cutout.getBoundingRectBottom(),
Gravity.BOTTOM));
return new Rect(leftInset, topInset, rightInset, bottomInset);
}
private static int findCutoutInsetForSide(Size display, Rect boundingRect, int gravity) {
if (boundingRect.isEmpty()) {
return 0;
}
int inset = 0;
switch (gravity) {
case Gravity.TOP:
return Math.max(inset, boundingRect.bottom);
case Gravity.BOTTOM:
return Math.max(inset, display.getHeight() - boundingRect.top);
case Gravity.LEFT:
return Math.max(inset, boundingRect.right);
case Gravity.RIGHT:
return Math.max(inset, display.getWidth() - boundingRect.left);
default:
throw new IllegalArgumentException("unknown gravity: " + gravity);
}
}
static boolean hasNavigationBar(DisplayInfo info, Context context, int displayId) {
if (displayId == Display.DEFAULT_DISPLAY) {
// Allow a system property to override this. Used by the emulator.
final String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
if ("1".equals(navBarOverride)) {
return false;
} else if ("0".equals(navBarOverride)) {
return true;
}
return context.getResources().getBoolean(R.bool.config_showNavigationBar);
} else {
boolean isUntrustedVirtualDisplay = info.type == Display.TYPE_VIRTUAL
&& info.ownerUid != SYSTEM_UID;
final ContentResolver resolver = context.getContentResolver();
boolean forceDesktopOnExternal = Settings.Global.getInt(resolver,
DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
return ((info.flags & FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0
|| (forceDesktopOnExternal && !isUntrustedVirtualDisplay));
// TODO(b/142569966): make sure VR2D and DisplayWindowSettings are moved here somehow.
}
}
static boolean hasStatusBar(int displayId) {
return displayId == Display.DEFAULT_DISPLAY;
}
/** Retrieve navigation bar position from resources based on rotation and size. */
public static @NavBarPosition int navigationBarPosition(Resources res, int displayWidth,
int displayHeight, int rotation) {
boolean navBarCanMove = displayWidth != displayHeight && res.getBoolean(
com.android.internal.R.bool.config_navBarCanMove);
if (navBarCanMove && displayWidth > displayHeight) {
if (rotation == Surface.ROTATION_90) {
return NAV_BAR_RIGHT;
} else {
return NAV_BAR_LEFT;
}
}
return NAV_BAR_BOTTOM;
}
/** Retrieve navigation bar size from resources based on side/orientation/ui-mode */
public static int getNavigationBarSize(Resources res, int navBarSide, boolean landscape,
int uiMode) {
final boolean carMode = (uiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_CAR;
if (carMode) {
if (navBarSide == NAV_BAR_BOTTOM) {
return res.getDimensionPixelSize(landscape
? R.dimen.navigation_bar_height_landscape_car_mode
: R.dimen.navigation_bar_height_car_mode);
} else {
return res.getDimensionPixelSize(R.dimen.navigation_bar_width_car_mode);
}
} else {
if (navBarSide == NAV_BAR_BOTTOM) {
return res.getDimensionPixelSize(landscape
? R.dimen.navigation_bar_height_landscape
: R.dimen.navigation_bar_height);
} else {
return res.getDimensionPixelSize(R.dimen.navigation_bar_width);
}
}
}
}