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