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