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