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