1 /* 2 * Copyright 2017 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 android.view; 18 19 import static android.util.DisplayMetrics.DENSITY_DEFAULT; 20 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE; 21 import static android.view.DisplayCutoutProto.BOUND_BOTTOM; 22 import static android.view.DisplayCutoutProto.BOUND_LEFT; 23 import static android.view.DisplayCutoutProto.BOUND_RIGHT; 24 import static android.view.DisplayCutoutProto.BOUND_TOP; 25 import static android.view.DisplayCutoutProto.INSETS; 26 import static android.view.Surface.ROTATION_0; 27 28 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 29 30 import android.annotation.IntDef; 31 import android.annotation.NonNull; 32 import android.annotation.Nullable; 33 import android.content.res.Resources; 34 import android.content.res.TypedArray; 35 import android.graphics.Insets; 36 import android.graphics.Matrix; 37 import android.graphics.Path; 38 import android.graphics.Rect; 39 import android.os.Parcel; 40 import android.os.Parcelable; 41 import android.text.TextUtils; 42 import android.util.DisplayUtils; 43 import android.util.Pair; 44 import android.util.RotationUtils; 45 import android.util.proto.ProtoOutputStream; 46 import android.view.Surface.Rotation; 47 48 import com.android.internal.R; 49 import com.android.internal.annotations.GuardedBy; 50 import com.android.internal.annotations.VisibleForTesting; 51 52 import java.lang.annotation.Retention; 53 import java.lang.annotation.RetentionPolicy; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.Collections; 57 import java.util.List; 58 59 /** 60 * Represents the area of the display that is not functional for displaying content. 61 * 62 * <p>{@code DisplayCutout} is immutable. 63 */ 64 public final class DisplayCutout { 65 66 private static final String TAG = "DisplayCutout"; 67 68 /** 69 * Category for overlays that allow emulating a display cutout on devices that don't have 70 * one. 71 * 72 * @see android.content.om.IOverlayManager 73 * @hide 74 */ 75 public static final String EMULATION_OVERLAY_CATEGORY = 76 "com.android.internal.display_cutout_emulation"; 77 78 private static final Rect ZERO_RECT = new Rect(); 79 private static final CutoutPathParserInfo EMPTY_PARSER_INFO = new CutoutPathParserInfo( 80 0 /* displayWidth */, 0 /* physicalDisplayHeight */, 81 0 /* physicalDisplayHeight */, 0 /* displayHeight */, 0f /* density */, 82 "" /* cutoutSpec */, 0 /* ROTATION_0 */, 0f /* scale */, 83 0f /* physicalPixelDisplaySizeRatio*/); 84 85 /** 86 * An instance where {@link #isEmpty()} returns {@code true}. 87 * 88 * @hide 89 */ 90 public static final DisplayCutout NO_CUTOUT = new DisplayCutout( 91 ZERO_RECT, Insets.NONE, ZERO_RECT, ZERO_RECT, ZERO_RECT, ZERO_RECT, EMPTY_PARSER_INFO, 92 false /* copyArguments */); 93 94 95 private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null); 96 private static final Object CACHE_LOCK = new Object(); 97 98 @GuardedBy("CACHE_LOCK") 99 private static String sCachedSpec; 100 @GuardedBy("CACHE_LOCK") 101 private static int sCachedDisplayWidth; 102 @GuardedBy("CACHE_LOCK") 103 private static int sCachedDisplayHeight; 104 @GuardedBy("CACHE_LOCK") 105 private static float sCachedDensity; 106 @GuardedBy("CACHE_LOCK") 107 private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR; 108 @GuardedBy("CACHE_LOCK") 109 private static Insets sCachedWaterfallInsets; 110 @GuardedBy("CACHE_LOCK") 111 private static float sCachedPhysicalPixelDisplaySizeRatio; 112 113 @GuardedBy("CACHE_LOCK") 114 private static CutoutPathParserInfo sCachedCutoutPathParserInfo; 115 @GuardedBy("CACHE_LOCK") 116 private static Path sCachedCutoutPath; 117 118 private final Rect mSafeInsets; 119 @NonNull 120 private final Insets mWaterfallInsets; 121 122 /** 123 * The bound is at the left of the screen. 124 * @hide 125 */ 126 public static final int BOUNDS_POSITION_LEFT = 0; 127 128 /** 129 * The bound is at the top of the screen. 130 * @hide 131 */ 132 public static final int BOUNDS_POSITION_TOP = 1; 133 134 /** 135 * The bound is at the right of the screen. 136 * @hide 137 */ 138 public static final int BOUNDS_POSITION_RIGHT = 2; 139 140 /** 141 * The bound is at the bottom of the screen. 142 * @hide 143 */ 144 public static final int BOUNDS_POSITION_BOTTOM = 3; 145 146 /** 147 * The number of possible positions at which bounds can be located. 148 * @hide 149 */ 150 public static final int BOUNDS_POSITION_LENGTH = 4; 151 152 /** @hide */ 153 @IntDef(prefix = { "BOUNDS_POSITION_" }, value = { 154 BOUNDS_POSITION_LEFT, 155 BOUNDS_POSITION_TOP, 156 BOUNDS_POSITION_RIGHT, 157 BOUNDS_POSITION_BOTTOM 158 }) 159 @Retention(RetentionPolicy.SOURCE) 160 public @interface BoundsPosition {} 161 162 private static class Bounds { 163 private final Rect[] mRects; 164 Bounds(Rect left, Rect top, Rect right, Rect bottom, boolean copyArguments)165 private Bounds(Rect left, Rect top, Rect right, Rect bottom, boolean copyArguments) { 166 mRects = new Rect[BOUNDS_POSITION_LENGTH]; 167 mRects[BOUNDS_POSITION_LEFT] = getCopyOrRef(left, copyArguments); 168 mRects[BOUNDS_POSITION_TOP] = getCopyOrRef(top, copyArguments); 169 mRects[BOUNDS_POSITION_RIGHT] = getCopyOrRef(right, copyArguments); 170 mRects[BOUNDS_POSITION_BOTTOM] = getCopyOrRef(bottom, copyArguments); 171 172 } 173 Bounds(Rect[] rects, boolean copyArguments)174 private Bounds(Rect[] rects, boolean copyArguments) { 175 if (rects.length != BOUNDS_POSITION_LENGTH) { 176 throw new IllegalArgumentException( 177 "rects must have exactly 4 elements: rects=" + Arrays.toString(rects)); 178 } 179 if (copyArguments) { 180 mRects = new Rect[BOUNDS_POSITION_LENGTH]; 181 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 182 mRects[i] = new Rect(rects[i]); 183 } 184 } else { 185 for (Rect rect : rects) { 186 if (rect == null) { 187 throw new IllegalArgumentException( 188 "rects must have non-null elements: rects=" 189 + Arrays.toString(rects)); 190 } 191 } 192 mRects = rects; 193 } 194 } 195 isEmpty()196 private boolean isEmpty() { 197 for (Rect rect : mRects) { 198 if (!rect.isEmpty()) { 199 return false; 200 } 201 } 202 return true; 203 } 204 getRect(@oundsPosition int pos)205 private Rect getRect(@BoundsPosition int pos) { 206 return new Rect(mRects[pos]); 207 } 208 getRects()209 private Rect[] getRects() { 210 Rect[] rects = new Rect[BOUNDS_POSITION_LENGTH]; 211 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 212 rects[i] = new Rect(mRects[i]); 213 } 214 return rects; 215 } 216 scale(float scale)217 private void scale(float scale) { 218 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 219 mRects[i].scale(scale); 220 } 221 } 222 223 @Override hashCode()224 public int hashCode() { 225 int result = 0; 226 for (Rect rect : mRects) { 227 result = result * 48271 + rect.hashCode(); 228 } 229 return result; 230 } 231 232 @Override equals(@ullable Object o)233 public boolean equals(@Nullable Object o) { 234 if (o == this) { 235 return true; 236 } 237 if (o instanceof Bounds) { 238 Bounds b = (Bounds) o; 239 return Arrays.deepEquals(mRects, b.mRects); 240 } 241 return false; 242 } 243 244 @Override toString()245 public String toString() { 246 return "Bounds=" + Arrays.toString(mRects); 247 } 248 249 } 250 251 private final Bounds mBounds; 252 253 /** 254 * Stores all the needed info to create the cutout paths. 255 * 256 * @hide 257 */ 258 public static class CutoutPathParserInfo { 259 private final int mDisplayWidth; 260 private final int mDisplayHeight; 261 private final int mPhysicalDisplayWidth; 262 private final int mPhysicalDisplayHeight; 263 private final float mDensity; 264 private final String mCutoutSpec; 265 private final @Rotation int mRotation; 266 private final float mScale; 267 private final float mPhysicalPixelDisplaySizeRatio; 268 CutoutPathParserInfo(int displayWidth, int displayHeight, int physicalDisplayWidth, int physicalDisplayHeight, float density, @Nullable String cutoutSpec, @Rotation int rotation, float scale, float physicalPixelDisplaySizeRatio)269 public CutoutPathParserInfo(int displayWidth, int displayHeight, int physicalDisplayWidth, 270 int physicalDisplayHeight, float density, @Nullable String cutoutSpec, 271 @Rotation int rotation, float scale, float physicalPixelDisplaySizeRatio) { 272 mDisplayWidth = displayWidth; 273 mDisplayHeight = displayHeight; 274 mPhysicalDisplayWidth = physicalDisplayWidth; 275 mPhysicalDisplayHeight = physicalDisplayHeight; 276 mDensity = density; 277 mCutoutSpec = cutoutSpec == null ? "" : cutoutSpec; 278 mRotation = rotation; 279 mScale = scale; 280 mPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio; 281 } 282 CutoutPathParserInfo(@onNull CutoutPathParserInfo cutoutPathParserInfo)283 public CutoutPathParserInfo(@NonNull CutoutPathParserInfo cutoutPathParserInfo) { 284 mDisplayWidth = cutoutPathParserInfo.mDisplayWidth; 285 mDisplayHeight = cutoutPathParserInfo.mDisplayHeight; 286 mPhysicalDisplayWidth = cutoutPathParserInfo.mPhysicalDisplayWidth; 287 mPhysicalDisplayHeight = cutoutPathParserInfo.mPhysicalDisplayHeight; 288 mDensity = cutoutPathParserInfo.mDensity; 289 mCutoutSpec = cutoutPathParserInfo.mCutoutSpec; 290 mRotation = cutoutPathParserInfo.mRotation; 291 mScale = cutoutPathParserInfo.mScale; 292 mPhysicalPixelDisplaySizeRatio = cutoutPathParserInfo.mPhysicalPixelDisplaySizeRatio; 293 } 294 getDisplayWidth()295 public int getDisplayWidth() { 296 return mDisplayWidth; 297 } 298 getDisplayHeight()299 public int getDisplayHeight() { 300 return mDisplayHeight; 301 } 302 getPhysicalDisplayWidth()303 public int getPhysicalDisplayWidth() { 304 return mPhysicalDisplayWidth; 305 } 306 getPhysicalDisplayHeight()307 public int getPhysicalDisplayHeight() { 308 return mPhysicalDisplayHeight; 309 } 310 getDensity()311 public float getDensity() { 312 return mDensity; 313 } 314 getCutoutSpec()315 public @NonNull String getCutoutSpec() { 316 return mCutoutSpec; 317 } 318 getRotation()319 public int getRotation() { 320 return mRotation; 321 } 322 getScale()323 public float getScale() { 324 return mScale; 325 } 326 getPhysicalPixelDisplaySizeRatio()327 public float getPhysicalPixelDisplaySizeRatio() { 328 return mPhysicalPixelDisplaySizeRatio; 329 } 330 hasCutout()331 private boolean hasCutout() { 332 return !mCutoutSpec.isEmpty(); 333 } 334 335 @Override hashCode()336 public int hashCode() { 337 int result = 0; 338 result = result * 48271 + Integer.hashCode(mDisplayWidth); 339 result = result * 48271 + Integer.hashCode(mDisplayHeight); 340 result = result * 48271 + Float.hashCode(mDensity); 341 result = result * 48271 + mCutoutSpec.hashCode(); 342 result = result * 48271 + Integer.hashCode(mRotation); 343 result = result * 48271 + Float.hashCode(mScale); 344 result = result * 48271 + Float.hashCode(mPhysicalPixelDisplaySizeRatio); 345 result = result * 48271 + Integer.hashCode(mPhysicalDisplayWidth); 346 result = result * 48271 + Integer.hashCode(mPhysicalDisplayHeight); 347 return result; 348 } 349 350 @Override equals(@ullable Object o)351 public boolean equals(@Nullable Object o) { 352 if (o == this) { 353 return true; 354 } 355 if (o instanceof CutoutPathParserInfo) { 356 CutoutPathParserInfo c = (CutoutPathParserInfo) o; 357 return mDisplayWidth == c.mDisplayWidth && mDisplayHeight == c.mDisplayHeight 358 && mPhysicalDisplayWidth == c.mPhysicalDisplayWidth 359 && mPhysicalDisplayHeight == c.mPhysicalDisplayHeight 360 && mDensity == c.mDensity && mCutoutSpec.equals(c.mCutoutSpec) 361 && mRotation == c.mRotation && mScale == c.mScale 362 && mPhysicalPixelDisplaySizeRatio == c.mPhysicalPixelDisplaySizeRatio; 363 } 364 return false; 365 } 366 367 @Override toString()368 public String toString() { 369 return "CutoutPathParserInfo{displayWidth=" + mDisplayWidth 370 + " displayHeight=" + mDisplayHeight 371 + " physicalDisplayWidth=" + mPhysicalDisplayWidth 372 + " physicalDisplayHeight=" + mPhysicalDisplayHeight 373 + " density={" + mDensity + "}" 374 + " cutoutSpec={" + mCutoutSpec + "}" 375 + " rotation={" + mRotation + "}" 376 + " scale={" + mScale + "}" 377 + " physicalPixelDisplaySizeRatio={" + mPhysicalPixelDisplaySizeRatio + "}" 378 + "}"; 379 } 380 } 381 382 private final @NonNull CutoutPathParserInfo mCutoutPathParserInfo; 383 384 /** 385 * Creates a DisplayCutout instance. 386 * 387 * <p>Note that this is only useful for tests. For production code, developers should always 388 * use a {@link DisplayCutout} obtained from the system.</p> 389 * 390 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 391 * {@link #getSafeInsetTop()} etc. 392 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 393 * it's treated as an empty rectangle (0,0)-(0,0). 394 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 395 * it's treated as an empty rectangle (0,0)-(0,0). 396 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 397 * passed, it's treated as an empty rectangle (0,0)-(0,0). 398 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 399 * passed, it's treated as an empty rectangle (0,0)-(0,0). 400 */ 401 // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) DisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom)402 public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 403 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom) { 404 this(safeInsets.toRect(), Insets.NONE, boundLeft, boundTop, boundRight, boundBottom, null, 405 true); 406 } 407 408 /** 409 * Creates a DisplayCutout instance. 410 * 411 * <p>Note that this is only useful for tests. For production code, developers should always 412 * use a {@link DisplayCutout} obtained from the system.</p> 413 * 414 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 415 * {@link #getSafeInsetTop()} etc. 416 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 417 * it's treated as an empty rectangle (0,0)-(0,0). 418 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 419 * it's treated as an empty rectangle (0,0)-(0,0). 420 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 421 * passed, it's treated as an empty rectangle (0,0)-(0,0). 422 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 423 * passed, it's treated as an empty rectangle (0,0)-(0,0). 424 * @param waterfallInsets the insets for the curved areas in waterfall display. 425 * @param info the cutout path parser info. 426 * @hide 427 */ DisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets, @Nullable CutoutPathParserInfo info)428 public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 429 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, 430 @NonNull Insets waterfallInsets, @Nullable CutoutPathParserInfo info) { 431 this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, 432 info, true); 433 } 434 435 /** 436 * Creates a DisplayCutout instance. 437 * 438 * <p>Note that this is only useful for tests. For production code, developers should always 439 * use a {@link DisplayCutout} obtained from the system.</p> 440 * 441 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 442 * {@link #getSafeInsetTop()} etc. 443 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 444 * it's treated as an empty rectangle (0,0)-(0,0). 445 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 446 * it's treated as an empty rectangle (0,0)-(0,0). 447 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 448 * passed, it's treated as an empty rectangle (0,0)-(0,0). 449 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 450 * passed, it's treated as an empty rectangle (0,0)-(0,0). 451 * @param waterfallInsets the insets for the curved areas in waterfall display. 452 */ DisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets)453 public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 454 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, 455 @NonNull Insets waterfallInsets) { 456 this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, 457 null, true); 458 } 459 460 /** 461 * Creates a DisplayCutout instance. 462 * 463 * <p>Note that this is only useful for tests. For production code, developers should always 464 * use a {@link DisplayCutout} obtained from the system.</p> 465 * 466 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 467 * {@link #getSafeInsetTop()} etc. 468 * @param boundingRects the bounding rects of the display cutouts as returned by 469 * {@link #getBoundingRects()} ()}. 470 * @deprecated Use {@link DisplayCutout#DisplayCutout(Insets, Rect, Rect, Rect, Rect)} instead. 471 */ 472 // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) 473 @Deprecated DisplayCutout(@ullable Rect safeInsets, @Nullable List<Rect> boundingRects)474 public DisplayCutout(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) { 475 this(safeInsets, Insets.NONE, extractBoundsFromList(safeInsets, boundingRects), null, 476 true /* copyArguments */); 477 } 478 479 /** 480 * Creates a DisplayCutout instance. 481 * 482 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 483 * {@link #getSafeInsetTop()} etc. 484 * @param waterfallInsets the insets for the curved areas in waterfall display. 485 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 486 * it's treated as an empty rectangle (0,0)-(0,0). 487 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 488 * it's treated as an empty rectangle (0,0)-(0,0). 489 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 490 * passed, it's treated as an empty rectangle (0,0)-(0,0). 491 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 492 * passed, it's treated as an empty rectangle (0,0)-(0,0). 493 * @param info the cutout path parser info. 494 * @param copyArguments if true, create a copy of the arguments. If false, the passed arguments 495 * are not copied and MUST remain unchanged forever. 496 */ DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect boundLeft, Rect boundTop, Rect boundRight, Rect boundBottom, CutoutPathParserInfo info, boolean copyArguments)497 private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect boundLeft, Rect boundTop, 498 Rect boundRight, Rect boundBottom, CutoutPathParserInfo info, 499 boolean copyArguments) { 500 mSafeInsets = getCopyOrRef(safeInsets, copyArguments); 501 mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; 502 mBounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments); 503 mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; 504 } 505 DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect[] bounds, CutoutPathParserInfo info, boolean copyArguments)506 private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect[] bounds, 507 CutoutPathParserInfo info, boolean copyArguments) { 508 mSafeInsets = getCopyOrRef(safeInsets, copyArguments); 509 mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; 510 mBounds = new Bounds(bounds, copyArguments); 511 mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; 512 } 513 DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds, CutoutPathParserInfo info)514 private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds, 515 CutoutPathParserInfo info) { 516 mSafeInsets = safeInsets; 517 mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; 518 mBounds = bounds; 519 mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; 520 } 521 getCopyOrRef(Rect r, boolean copyArguments)522 private static Rect getCopyOrRef(Rect r, boolean copyArguments) { 523 if (r == null) { 524 return ZERO_RECT; 525 } else if (copyArguments) { 526 return new Rect(r); 527 } else { 528 return r; 529 } 530 } 531 532 /** 533 * Returns the insets representing the curved areas of a waterfall display. 534 * 535 * A waterfall display has curved areas along the edges of the screen. Apps should be careful 536 * when showing UI and handling touch input in those insets because the curve may impair 537 * legibility and can frequently lead to unintended touch inputs. 538 * 539 * @return the insets for the curved areas of a waterfall display in pixels or {@code 540 * Insets.NONE} if there are no curved areas or they don't overlap with the window. 541 */ getWaterfallInsets()542 public @NonNull Insets getWaterfallInsets() { 543 return mWaterfallInsets; 544 } 545 546 547 /** 548 * Find the position of the bounding rect, and create an array of Rect whose index represents 549 * the position (= BoundsPosition). 550 * 551 * @hide 552 */ extractBoundsFromList(Rect safeInsets, List<Rect> boundingRects)553 public static Rect[] extractBoundsFromList(Rect safeInsets, List<Rect> boundingRects) { 554 Rect[] sortedBounds = new Rect[BOUNDS_POSITION_LENGTH]; 555 for (int i = 0; i < sortedBounds.length; ++i) { 556 sortedBounds[i] = ZERO_RECT; 557 } 558 if (safeInsets != null && boundingRects != null) { 559 // There is at most one non-functional area per short edge of the device, but none 560 // on the long edges, so either a) safeInsets.top and safeInsets.bottom is 0, or 561 // b) safeInsets.left and safeInset.right is 0. 562 final boolean topBottomInset = safeInsets.top > 0 || safeInsets.bottom > 0; 563 for (Rect bound : boundingRects) { 564 if (topBottomInset) { 565 if (bound.top == 0) { 566 sortedBounds[BOUNDS_POSITION_TOP] = bound; 567 } else { 568 sortedBounds[BOUNDS_POSITION_BOTTOM] = bound; 569 } 570 } else { 571 if (bound.left == 0) { 572 sortedBounds[BOUNDS_POSITION_LEFT] = bound; 573 } else { 574 sortedBounds[BOUNDS_POSITION_RIGHT] = bound; 575 } 576 } 577 } 578 } 579 return sortedBounds; 580 } 581 582 /** 583 * Returns true if there is no cutout, i.e. the bounds are empty. 584 * 585 * @hide 586 */ isBoundsEmpty()587 public boolean isBoundsEmpty() { 588 return mBounds.isEmpty(); 589 } 590 591 /** 592 * Returns true if the safe insets are empty (and therefore the current view does not 593 * overlap with the cutout or cutout area). 594 * 595 * @hide 596 */ isEmpty()597 public boolean isEmpty() { 598 return mSafeInsets.equals(ZERO_RECT); 599 } 600 601 /** 602 * Returns the inset from the top which avoids the display cutout in pixels. 603 * 604 * @see WindowInsets.Type#displayCutout() 605 */ getSafeInsetTop()606 public int getSafeInsetTop() { 607 return mSafeInsets.top; 608 } 609 610 /** 611 * Returns the inset from the bottom which avoids the display cutout in pixels. 612 * 613 * @see WindowInsets.Type#displayCutout() 614 */ getSafeInsetBottom()615 public int getSafeInsetBottom() { 616 return mSafeInsets.bottom; 617 } 618 619 /** 620 * Returns the inset from the left which avoids the display cutout in pixels. 621 * 622 * @see WindowInsets.Type#displayCutout() 623 */ getSafeInsetLeft()624 public int getSafeInsetLeft() { 625 return mSafeInsets.left; 626 } 627 628 /** 629 * Returns the inset from the right which avoids the display cutout in pixels. 630 * 631 * @see WindowInsets.Type#displayCutout() 632 */ getSafeInsetRight()633 public int getSafeInsetRight() { 634 return mSafeInsets.right; 635 } 636 637 /** 638 * Returns the safe insets in a rect in pixel units. 639 * 640 * @return a rect which is set to the safe insets. 641 * @hide 642 */ getSafeInsets()643 public Rect getSafeInsets() { 644 return new Rect(mSafeInsets); 645 } 646 647 /** 648 * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional 649 * area on the display. 650 * 651 * There will be at most one non-functional area per edge of the device. 652 * 653 * <p>Note that there is no bounding rectangle for waterfall cutout since it just represents the 654 * curved areas of the display but not the non-functional areas.</p> 655 * 656 * @return a list of bounding {@code Rect}s, one for each display cutout area. No empty Rect is 657 * returned. 658 */ 659 @NonNull getBoundingRects()660 public List<Rect> getBoundingRects() { 661 List<Rect> result = new ArrayList<>(); 662 for (Rect bound : getBoundingRectsAll()) { 663 if (!bound.isEmpty()) { 664 result.add(new Rect(bound)); 665 } 666 } 667 return result; 668 } 669 670 /** 671 * Returns an array of {@code Rect}s, each of which is the bounding rectangle for a non- 672 * functional area on the display. Ordinal value of BoundPosition is used as an index of 673 * the array. 674 * 675 * There will be at most one non-functional area per edge of the device. 676 * 677 * <p>Note that there is no bounding rectangle for waterfall cutout since it just represents the 678 * curved areas of the display but not the non-functional areas.</p> 679 * 680 * @return an array of bounding {@code Rect}s, one for each display cutout area. This might 681 * contain ZERO_RECT, which means there is no cutout area at the position. 682 * 683 * @hide 684 */ getBoundingRectsAll()685 public Rect[] getBoundingRectsAll() { 686 return mBounds.getRects(); 687 } 688 689 /** 690 * Returns a bounding rectangle for a non-functional area on the display which is located on 691 * the left of the screen. 692 * 693 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 694 * is returned. 695 */ getBoundingRectLeft()696 public @NonNull Rect getBoundingRectLeft() { 697 return mBounds.getRect(BOUNDS_POSITION_LEFT); 698 } 699 700 /** 701 * Returns a bounding rectangle for a non-functional area on the display which is located on 702 * the top of the screen. 703 * 704 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 705 * is returned. 706 */ getBoundingRectTop()707 public @NonNull Rect getBoundingRectTop() { 708 return mBounds.getRect(BOUNDS_POSITION_TOP); 709 } 710 711 /** 712 * Returns a bounding rectangle for a non-functional area on the display which is located on 713 * the right of the screen. 714 * 715 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 716 * is returned. 717 */ getBoundingRectRight()718 public @NonNull Rect getBoundingRectRight() { 719 return mBounds.getRect(BOUNDS_POSITION_RIGHT); 720 } 721 722 /** 723 * Returns a bounding rectangle for a non-functional area on the display which is located on 724 * the bottom of the screen. 725 * 726 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 727 * is returned. 728 */ getBoundingRectBottom()729 public @NonNull Rect getBoundingRectBottom() { 730 return mBounds.getRect(BOUNDS_POSITION_BOTTOM); 731 } 732 733 /** 734 * Returns a {@link Path} that contains the cutout paths of all sides on the display. 735 * 736 * To get a cutout path for one specific side, apps can intersect the {@link Path} with the 737 * {@link Rect} obtained from {@link #getBoundingRectLeft()}, {@link #getBoundingRectTop()}, 738 * {@link #getBoundingRectRight()} or {@link #getBoundingRectBottom()}. 739 * 740 * @return a {@link Path} contains all the cutout paths based on display coordinate. Returns 741 * null if there is no cutout on the display. 742 */ getCutoutPath()743 public @Nullable Path getCutoutPath() { 744 if (!mCutoutPathParserInfo.hasCutout()) { 745 return null; 746 } 747 synchronized (CACHE_LOCK) { 748 if (mCutoutPathParserInfo.equals(sCachedCutoutPathParserInfo)) { 749 return sCachedCutoutPath; 750 } 751 } 752 final CutoutSpecification cutoutSpec = new CutoutSpecification.Parser( 753 mCutoutPathParserInfo.getDensity(), mCutoutPathParserInfo.getPhysicalDisplayWidth(), 754 mCutoutPathParserInfo.getPhysicalDisplayHeight(), 755 mCutoutPathParserInfo.getPhysicalPixelDisplaySizeRatio()) 756 .parse(mCutoutPathParserInfo.getCutoutSpec()); 757 758 final Path cutoutPath = cutoutSpec.getPath(); 759 if (cutoutPath == null || cutoutPath.isEmpty()) { 760 return null; 761 } 762 final Matrix matrix = new Matrix(); 763 if (mCutoutPathParserInfo.getRotation() != ROTATION_0) { 764 RotationUtils.transformPhysicalToLogicalCoordinates( 765 mCutoutPathParserInfo.getRotation(), 766 mCutoutPathParserInfo.getDisplayWidth(), 767 mCutoutPathParserInfo.getDisplayHeight(), 768 matrix 769 ); 770 } 771 matrix.postScale(mCutoutPathParserInfo.getScale(), mCutoutPathParserInfo.getScale()); 772 cutoutPath.transform(matrix); 773 774 synchronized (CACHE_LOCK) { 775 sCachedCutoutPathParserInfo = new CutoutPathParserInfo(mCutoutPathParserInfo); 776 sCachedCutoutPath = cutoutPath; 777 } 778 return cutoutPath; 779 } 780 781 /** 782 * @return the {@link CutoutPathParserInfo}; 783 * 784 * @hide 785 */ getCutoutPathParserInfo()786 public CutoutPathParserInfo getCutoutPathParserInfo() { 787 return mCutoutPathParserInfo; 788 } 789 790 @Override hashCode()791 public int hashCode() { 792 int result = 0; 793 result = 48271 * result + mSafeInsets.hashCode(); 794 result = 48271 * result + mBounds.hashCode(); 795 result = 48271 * result + mWaterfallInsets.hashCode(); 796 result = 48271 * result + mCutoutPathParserInfo.hashCode(); 797 return result; 798 } 799 800 @Override equals(@ullable Object o)801 public boolean equals(@Nullable Object o) { 802 if (o == this) { 803 return true; 804 } 805 if (o instanceof DisplayCutout) { 806 DisplayCutout c = (DisplayCutout) o; 807 return mSafeInsets.equals(c.mSafeInsets) && mBounds.equals(c.mBounds) 808 && mWaterfallInsets.equals(c.mWaterfallInsets) 809 && mCutoutPathParserInfo.equals(c.mCutoutPathParserInfo); 810 } 811 return false; 812 } 813 814 @Override toString()815 public String toString() { 816 return "DisplayCutout{insets=" + mSafeInsets 817 + " waterfall=" + mWaterfallInsets 818 + " boundingRect={" + mBounds + "}" 819 + " cutoutPathParserInfo={" + mCutoutPathParserInfo + "}" 820 + "}"; 821 } 822 823 /** 824 * @hide 825 */ dumpDebug(ProtoOutputStream proto, long fieldId)826 public void dumpDebug(ProtoOutputStream proto, long fieldId) { 827 final long token = proto.start(fieldId); 828 mSafeInsets.dumpDebug(proto, INSETS); 829 mBounds.getRect(BOUNDS_POSITION_LEFT).dumpDebug(proto, BOUND_LEFT); 830 mBounds.getRect(BOUNDS_POSITION_TOP).dumpDebug(proto, BOUND_TOP); 831 mBounds.getRect(BOUNDS_POSITION_RIGHT).dumpDebug(proto, BOUND_RIGHT); 832 mBounds.getRect(BOUNDS_POSITION_BOTTOM).dumpDebug(proto, BOUND_BOTTOM); 833 mWaterfallInsets.toRect().dumpDebug(proto, INSETS); 834 proto.end(token); 835 } 836 837 /** 838 * Insets the reference frame of the cutout in the given directions. 839 * 840 * @return a copy of this instance which has been inset 841 * @hide 842 */ inset(int insetLeft, int insetTop, int insetRight, int insetBottom)843 public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { 844 if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0 845 || (isBoundsEmpty() && mWaterfallInsets.equals(Insets.NONE))) { 846 return this; 847 } 848 849 Rect safeInsets = insetInsets(insetLeft, insetTop, insetRight, insetBottom, 850 new Rect(mSafeInsets)); 851 852 // If we are not cutting off part of the cutout by insetting it on bottom/right, and we also 853 // don't move it around, we can avoid the allocation and copy of the instance. 854 if (insetLeft == 0 && insetTop == 0 && mSafeInsets.equals(safeInsets)) { 855 return this; 856 } 857 858 Rect waterfallInsets = insetInsets(insetLeft, insetTop, insetRight, insetBottom, 859 mWaterfallInsets.toRect()); 860 861 Rect[] bounds = mBounds.getRects(); 862 for (int i = 0; i < bounds.length; ++i) { 863 if (!bounds[i].equals(ZERO_RECT)) { 864 bounds[i].offset(-insetLeft, -insetTop); 865 } 866 } 867 868 return new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, 869 mCutoutPathParserInfo, false /* copyArguments */); 870 } 871 insetInsets(int insetLeft, int insetTop, int insetRight, int insetBottom, Rect insets)872 private Rect insetInsets(int insetLeft, int insetTop, int insetRight, int insetBottom, 873 Rect insets) { 874 // Note: it's not really well defined what happens when the inset is negative, because we 875 // don't know if the safe inset needs to expand in general. 876 if (insetTop > 0 || insets.top > 0) { 877 insets.top = atLeastZero(insets.top - insetTop); 878 } 879 if (insetBottom > 0 || insets.bottom > 0) { 880 insets.bottom = atLeastZero(insets.bottom - insetBottom); 881 } 882 if (insetLeft > 0 || insets.left > 0) { 883 insets.left = atLeastZero(insets.left - insetLeft); 884 } 885 if (insetRight > 0 || insets.right > 0) { 886 insets.right = atLeastZero(insets.right - insetRight); 887 } 888 return insets; 889 } 890 891 /** 892 * Returns a copy of this instance with the safe insets replaced with the parameter. 893 * 894 * @param safeInsets the new safe insets in pixels 895 * @return a copy of this instance with the safe insets replaced with the argument. 896 * 897 * @hide 898 */ replaceSafeInsets(Rect safeInsets)899 public DisplayCutout replaceSafeInsets(Rect safeInsets) { 900 return new DisplayCutout(new Rect(safeInsets), mWaterfallInsets, mBounds, 901 mCutoutPathParserInfo); 902 } 903 atLeastZero(int value)904 private static int atLeastZero(int value) { 905 return value < 0 ? 0 : value; 906 } 907 908 909 /** 910 * Creates an instance from a bounding rect. 911 * 912 * @hide 913 */ 914 @VisibleForTesting fromBoundingRect( int left, int top, int right, int bottom, @BoundsPosition int pos)915 public static DisplayCutout fromBoundingRect( 916 int left, int top, int right, int bottom, @BoundsPosition int pos) { 917 Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH]; 918 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 919 bounds[i] = (pos == i) ? new Rect(left, top, right, bottom) : new Rect(); 920 } 921 return new DisplayCutout(ZERO_RECT, Insets.NONE, bounds, null, false /* copyArguments */); 922 } 923 924 /** 925 * Creates an instance from bounds, waterfall insets and CutoutPathParserInfo. 926 * 927 * @hide 928 */ constructDisplayCutout(Rect[] bounds, Insets waterfallInsets, CutoutPathParserInfo info)929 public static DisplayCutout constructDisplayCutout(Rect[] bounds, Insets waterfallInsets, 930 CutoutPathParserInfo info) { 931 return new DisplayCutout(ZERO_RECT, waterfallInsets, bounds, info, 932 false /* copyArguments */); 933 } 934 935 /** 936 * Creates an instance from a bounding {@link Path}. 937 * 938 * @hide 939 */ fromBounds(Rect[] bounds)940 public static DisplayCutout fromBounds(Rect[] bounds) { 941 return new DisplayCutout(ZERO_RECT, Insets.NONE, bounds, null /* cutoutPathParserInfo */, 942 false /* copyArguments */); 943 } 944 945 /** 946 * Gets the display cutout by the given display unique id. 947 * 948 * Loads the default config {@link R.string#config_mainBuiltInDisplayCutout) if 949 * {@link R.array#config_displayUniqueIdArray} is not set. 950 */ getDisplayCutoutPath(Resources res, String displayUniqueId)951 private static String getDisplayCutoutPath(Resources res, String displayUniqueId) { 952 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 953 final String[] array = res.getStringArray(R.array.config_displayCutoutPathArray); 954 if (index >= 0 && index < array.length) { 955 return array[index]; 956 } 957 return res.getString(R.string.config_mainBuiltInDisplayCutout); 958 } 959 960 /** 961 * Gets the display cutout approximation rect by the given display unique id. 962 * 963 * Loads the default config {@link R.string#config_mainBuiltInDisplayCutoutRectApproximation} if 964 * {@link R.array#config_displayUniqueIdArray} is not set. 965 */ getDisplayCutoutApproximationRect(Resources res, String displayUniqueId)966 private static String getDisplayCutoutApproximationRect(Resources res, String displayUniqueId) { 967 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 968 final String[] array = res.getStringArray( 969 R.array.config_displayCutoutApproximationRectArray); 970 if (index >= 0 && index < array.length) { 971 return array[index]; 972 } 973 return res.getString(R.string.config_mainBuiltInDisplayCutoutRectApproximation); 974 } 975 976 /** 977 * Gets whether to mask a built-in display cutout of a display which is determined by the 978 * given display unique id. 979 * 980 * Loads the default config {@link R.bool#config_maskMainBuiltInDisplayCutout} if 981 * {@link R.array#config_displayUniqueIdArray} is not set. 982 * 983 * @hide 984 */ getMaskBuiltInDisplayCutout(Resources res, String displayUniqueId)985 public static boolean getMaskBuiltInDisplayCutout(Resources res, String displayUniqueId) { 986 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 987 final TypedArray array = res.obtainTypedArray(R.array.config_maskBuiltInDisplayCutoutArray); 988 boolean maskCutout; 989 if (index >= 0 && index < array.length()) { 990 maskCutout = array.getBoolean(index, false); 991 } else { 992 maskCutout = res.getBoolean(R.bool.config_maskMainBuiltInDisplayCutout); 993 } 994 array.recycle(); 995 return maskCutout; 996 } 997 998 /** 999 * Gets whether to fill a built-in display cutout of a display which is determined by the 1000 * given display unique id. 1001 * 1002 * Loads the default config{@link R.bool#config_fillMainBuiltInDisplayCutout} if 1003 * {@link R.array#config_displayUniqueIdArray} is not set. 1004 * 1005 * @hide 1006 */ getFillBuiltInDisplayCutout(Resources res, String displayUniqueId)1007 public static boolean getFillBuiltInDisplayCutout(Resources res, String displayUniqueId) { 1008 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 1009 final TypedArray array = res.obtainTypedArray(R.array.config_fillBuiltInDisplayCutoutArray); 1010 boolean fillCutout; 1011 if (index >= 0 && index < array.length()) { 1012 fillCutout = array.getBoolean(index, false); 1013 } else { 1014 fillCutout = res.getBoolean(R.bool.config_fillMainBuiltInDisplayCutout); 1015 } 1016 array.recycle(); 1017 return fillCutout; 1018 } 1019 1020 /** 1021 * Gets the waterfall cutout by the given display unique id. 1022 * 1023 * Loads the default waterfall dimens if {@link R.array#config_displayUniqueIdArray} is not set. 1024 * {@link R.dimen#waterfall_display_left_edge_size}, 1025 * {@link R.dimen#waterfall_display_top_edge_size}, 1026 * {@link R.dimen#waterfall_display_right_edge_size}, 1027 * {@link R.dimen#waterfall_display_bottom_edge_size} 1028 */ getWaterfallInsets(Resources res, String displayUniqueId)1029 private static Insets getWaterfallInsets(Resources res, String displayUniqueId) { 1030 Insets insets; 1031 final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId); 1032 final TypedArray array = res.obtainTypedArray(R.array.config_waterfallCutoutArray); 1033 if (index >= 0 && index < array.length() && array.getResourceId(index, 0) > 0) { 1034 final int resourceId = array.getResourceId(index, 0); 1035 final TypedArray waterfall = res.obtainTypedArray(resourceId); 1036 insets = Insets.of( 1037 waterfall.getDimensionPixelSize(0 /* waterfall left edge size */, 0), 1038 waterfall.getDimensionPixelSize(1 /* waterfall top edge size */, 0), 1039 waterfall.getDimensionPixelSize(2 /* waterfall right edge size */, 0), 1040 waterfall.getDimensionPixelSize(3 /* waterfall bottom edge size */, 0)); 1041 waterfall.recycle(); 1042 } else { 1043 insets = loadWaterfallInset(res); 1044 } 1045 array.recycle(); 1046 return insets; 1047 } 1048 1049 /** 1050 * Creates the display cutout according to 1051 * @android:string/config_mainBuiltInDisplayCutoutRectApproximation, which is the closest 1052 * rectangle-base approximation of the cutout. 1053 * @hide 1054 */ fromResourcesRectApproximation(Resources res, String displayUniqueId, int physicalDisplayWidth, int physicalDisplayHeight, int displayWidth, int displayHeight)1055 public static DisplayCutout fromResourcesRectApproximation(Resources res, 1056 String displayUniqueId, int physicalDisplayWidth, int physicalDisplayHeight, 1057 int displayWidth, int displayHeight) { 1058 return pathAndDisplayCutoutFromSpec(getDisplayCutoutPath(res, displayUniqueId), 1059 getDisplayCutoutApproximationRect(res, displayUniqueId), physicalDisplayWidth, 1060 physicalDisplayHeight, displayWidth, displayHeight, 1061 DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT, 1062 getWaterfallInsets(res, displayUniqueId)).second; 1063 } 1064 1065 /** 1066 * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec. 1067 * 1068 * @hide 1069 */ 1070 @VisibleForTesting(visibility = PRIVATE) fromSpec(String pathSpec, int displayWidth, int displayHeight, float density, Insets waterfallInsets)1071 public static DisplayCutout fromSpec(String pathSpec, int displayWidth, 1072 int displayHeight, float density, Insets waterfallInsets) { 1073 return pathAndDisplayCutoutFromSpec( 1074 pathSpec, null, displayWidth, displayHeight, displayWidth, displayHeight, density, 1075 waterfallInsets).second; 1076 } 1077 1078 /** 1079 * Gets the cutout path and the corresponding DisplayCutout instance from the spec string. 1080 * 1081 * @param pathSpec the spec string read from config_mainBuiltInDisplayCutout. 1082 * @param rectSpec the spec string read from config_mainBuiltInDisplayCutoutRectApproximation. 1083 * @param physicalDisplayWidth the max physical display width the display supports. 1084 * @param physicalDisplayHeight the max physical display height the display supports. 1085 * @param displayWidth the display width. 1086 * @param displayHeight the display height. 1087 * @param density the display density. 1088 * @param waterfallInsets the waterfall insets of the display. 1089 * @return a Pair contains the cutout path and the corresponding DisplayCutout instance. 1090 */ pathAndDisplayCutoutFromSpec( String pathSpec, String rectSpec, int physicalDisplayWidth, int physicalDisplayHeight, int displayWidth, int displayHeight, float density, Insets waterfallInsets)1091 private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec( 1092 String pathSpec, String rectSpec, int physicalDisplayWidth, int physicalDisplayHeight, 1093 int displayWidth, int displayHeight, float density, Insets waterfallInsets) { 1094 // Always use the rect approximation spec to create the cutout if it's not null because 1095 // transforming and sending a Region constructed from a path is very costly. 1096 String spec = rectSpec != null ? rectSpec : pathSpec; 1097 if (TextUtils.isEmpty(spec) && waterfallInsets.equals(Insets.NONE)) { 1098 return NULL_PAIR; 1099 } 1100 1101 final float physicalPixelDisplaySizeRatio = DisplayUtils.getPhysicalPixelDisplaySizeRatio( 1102 physicalDisplayWidth, physicalDisplayHeight, displayWidth, displayHeight); 1103 1104 synchronized (CACHE_LOCK) { 1105 if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth 1106 && sCachedDisplayHeight == displayHeight 1107 && sCachedDensity == density 1108 && waterfallInsets.equals(sCachedWaterfallInsets) 1109 && sCachedPhysicalPixelDisplaySizeRatio == physicalPixelDisplaySizeRatio) { 1110 return sCachedCutout; 1111 } 1112 } 1113 1114 spec = spec.trim(); 1115 1116 CutoutSpecification cutoutSpec = new CutoutSpecification.Parser(density, 1117 physicalDisplayWidth, physicalDisplayHeight, physicalPixelDisplaySizeRatio) 1118 .parse(spec); 1119 Rect safeInset = cutoutSpec.getSafeInset(); 1120 final Rect boundLeft = cutoutSpec.getLeftBound(); 1121 final Rect boundTop = cutoutSpec.getTopBound(); 1122 final Rect boundRight = cutoutSpec.getRightBound(); 1123 final Rect boundBottom = cutoutSpec.getBottomBound(); 1124 1125 1126 if (!waterfallInsets.equals(Insets.NONE)) { 1127 safeInset.set( 1128 Math.max(waterfallInsets.left, safeInset.left), 1129 Math.max(waterfallInsets.top, safeInset.top), 1130 Math.max(waterfallInsets.right, safeInset.right), 1131 Math.max(waterfallInsets.bottom, safeInset.bottom)); 1132 } 1133 1134 final CutoutPathParserInfo cutoutPathParserInfo = new CutoutPathParserInfo( 1135 displayWidth, displayHeight, physicalDisplayWidth, physicalDisplayHeight, density, 1136 pathSpec.trim(), ROTATION_0, 1f /* scale */, physicalPixelDisplaySizeRatio); 1137 1138 final DisplayCutout cutout = new DisplayCutout( 1139 safeInset, waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, 1140 cutoutPathParserInfo , false /* copyArguments */); 1141 final Pair<Path, DisplayCutout> result = new Pair<>(cutoutSpec.getPath(), cutout); 1142 synchronized (CACHE_LOCK) { 1143 sCachedSpec = spec; 1144 sCachedDisplayWidth = displayWidth; 1145 sCachedDisplayHeight = displayHeight; 1146 sCachedDensity = density; 1147 sCachedCutout = result; 1148 sCachedWaterfallInsets = waterfallInsets; 1149 sCachedPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio; 1150 } 1151 return result; 1152 } 1153 loadWaterfallInset(Resources res)1154 private static Insets loadWaterfallInset(Resources res) { 1155 return Insets.of( 1156 res.getDimensionPixelSize(R.dimen.waterfall_display_left_edge_size), 1157 res.getDimensionPixelSize(R.dimen.waterfall_display_top_edge_size), 1158 res.getDimensionPixelSize(R.dimen.waterfall_display_right_edge_size), 1159 res.getDimensionPixelSize(R.dimen.waterfall_display_bottom_edge_size)); 1160 } 1161 1162 /** 1163 * @return a copy of this cutout that has been rotated for a display in toRotation. 1164 * @hide 1165 */ getRotated(int startWidth, int startHeight, int fromRotation, int toRotation)1166 public DisplayCutout getRotated(int startWidth, int startHeight, 1167 int fromRotation, int toRotation) { 1168 if (this == DisplayCutout.NO_CUTOUT) { 1169 return DisplayCutout.NO_CUTOUT; 1170 } 1171 final int rotation = RotationUtils.deltaRotation(fromRotation, toRotation); 1172 if (rotation == ROTATION_0) { 1173 return this; 1174 } 1175 final Insets waterfallInsets = RotationUtils.rotateInsets(getWaterfallInsets(), rotation); 1176 // returns a copy 1177 final Rect[] newBounds = getBoundingRectsAll(); 1178 final Rect displayBounds = new Rect(0, 0, startWidth, startHeight); 1179 for (int i = 0; i < newBounds.length; ++i) { 1180 if (newBounds[i].isEmpty()) continue; 1181 RotationUtils.rotateBounds(newBounds[i], displayBounds, rotation); 1182 } 1183 Collections.rotate(Arrays.asList(newBounds), -rotation); 1184 final CutoutPathParserInfo info = getCutoutPathParserInfo(); 1185 final CutoutPathParserInfo newInfo = new CutoutPathParserInfo( 1186 info.getDisplayWidth(), info.getDisplayHeight(), info.getPhysicalDisplayWidth(), 1187 info.getPhysicalDisplayHeight(), info.getDensity(), info.getCutoutSpec(), 1188 toRotation, info.getScale(), info.getPhysicalPixelDisplaySizeRatio()); 1189 final boolean swapAspect = (rotation % 2) != 0; 1190 final int endWidth = swapAspect ? startHeight : startWidth; 1191 final int endHeight = swapAspect ? startWidth : startHeight; 1192 final DisplayCutout tmp = 1193 DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo); 1194 final Rect safeInsets = DisplayCutout.computeSafeInsets(endWidth, endHeight, tmp); 1195 return tmp.replaceSafeInsets(safeInsets); 1196 } 1197 1198 /** 1199 * Compute the insets derived from a cutout. This is usually used to populate the safe-insets 1200 * of the cutout via {@link #replaceSafeInsets}. 1201 * @hide 1202 */ computeSafeInsets(int displayW, int displayH, DisplayCutout cutout)1203 public static Rect computeSafeInsets(int displayW, int displayH, DisplayCutout cutout) { 1204 if (displayW == displayH) { 1205 throw new UnsupportedOperationException("not implemented: display=" + displayW + "x" 1206 + displayH + " cutout=" + cutout); 1207 } 1208 1209 int leftInset = Math.max(cutout.getWaterfallInsets().left, findCutoutInsetForSide( 1210 displayW, displayH, cutout.getBoundingRectLeft(), Gravity.LEFT)); 1211 int topInset = Math.max(cutout.getWaterfallInsets().top, findCutoutInsetForSide( 1212 displayW, displayH, cutout.getBoundingRectTop(), Gravity.TOP)); 1213 int rightInset = Math.max(cutout.getWaterfallInsets().right, findCutoutInsetForSide( 1214 displayW, displayH, cutout.getBoundingRectRight(), Gravity.RIGHT)); 1215 int bottomInset = Math.max(cutout.getWaterfallInsets().bottom, findCutoutInsetForSide( 1216 displayW, displayH, cutout.getBoundingRectBottom(), Gravity.BOTTOM)); 1217 1218 return new Rect(leftInset, topInset, rightInset, bottomInset); 1219 } 1220 findCutoutInsetForSide(int displayW, int displayH, Rect boundingRect, int gravity)1221 private static int findCutoutInsetForSide(int displayW, int displayH, Rect boundingRect, 1222 int gravity) { 1223 if (boundingRect.isEmpty()) { 1224 return 0; 1225 } 1226 1227 int inset = 0; 1228 switch (gravity) { 1229 case Gravity.TOP: 1230 return Math.max(inset, boundingRect.bottom); 1231 case Gravity.BOTTOM: 1232 return Math.max(inset, displayH - boundingRect.top); 1233 case Gravity.LEFT: 1234 return Math.max(inset, boundingRect.right); 1235 case Gravity.RIGHT: 1236 return Math.max(inset, displayW - boundingRect.left); 1237 default: 1238 throw new IllegalArgumentException("unknown gravity: " + gravity); 1239 } 1240 } 1241 1242 /** 1243 * Helper class for passing {@link DisplayCutout} through binder. 1244 * 1245 * Needed, because {@code readFromParcel} cannot be used with immutable classes. 1246 * 1247 * @hide 1248 */ 1249 public static final class ParcelableWrapper implements Parcelable { 1250 1251 private DisplayCutout mInner; 1252 ParcelableWrapper()1253 public ParcelableWrapper() { 1254 this(NO_CUTOUT); 1255 } 1256 ParcelableWrapper(DisplayCutout cutout)1257 public ParcelableWrapper(DisplayCutout cutout) { 1258 mInner = cutout; 1259 } 1260 1261 @Override describeContents()1262 public int describeContents() { 1263 return 0; 1264 } 1265 1266 @Override writeToParcel(Parcel out, int flags)1267 public void writeToParcel(Parcel out, int flags) { 1268 writeCutoutToParcel(mInner, out, flags); 1269 } 1270 1271 /** 1272 * Writes a DisplayCutout to a {@link Parcel}. 1273 * 1274 * @see #readCutoutFromParcel(Parcel) 1275 */ writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags)1276 public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) { 1277 if (cutout == null) { 1278 out.writeInt(-1); 1279 } else if (cutout == NO_CUTOUT) { 1280 out.writeInt(0); 1281 } else { 1282 out.writeInt(1); 1283 out.writeTypedObject(cutout.mSafeInsets, flags); 1284 out.writeTypedArray(cutout.mBounds.getRects(), flags); 1285 out.writeTypedObject(cutout.mWaterfallInsets, flags); 1286 out.writeInt(cutout.mCutoutPathParserInfo.getDisplayWidth()); 1287 out.writeInt(cutout.mCutoutPathParserInfo.getDisplayHeight()); 1288 out.writeInt(cutout.mCutoutPathParserInfo.getPhysicalDisplayWidth()); 1289 out.writeInt(cutout.mCutoutPathParserInfo.getPhysicalDisplayHeight()); 1290 out.writeFloat(cutout.mCutoutPathParserInfo.getDensity()); 1291 out.writeString(cutout.mCutoutPathParserInfo.getCutoutSpec()); 1292 out.writeInt(cutout.mCutoutPathParserInfo.getRotation()); 1293 out.writeFloat(cutout.mCutoutPathParserInfo.getScale()); 1294 out.writeFloat(cutout.mCutoutPathParserInfo.getPhysicalPixelDisplaySizeRatio()); 1295 } 1296 } 1297 1298 /** 1299 * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing 1300 * instance. 1301 * 1302 * Needed for AIDL out parameters. 1303 */ readFromParcel(Parcel in)1304 public void readFromParcel(Parcel in) { 1305 mInner = readCutoutFromParcel(in); 1306 } 1307 1308 public static final @android.annotation.NonNull Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() { 1309 @Override 1310 public ParcelableWrapper createFromParcel(Parcel in) { 1311 return new ParcelableWrapper(readCutoutFromParcel(in)); 1312 } 1313 1314 @Override 1315 public ParcelableWrapper[] newArray(int size) { 1316 return new ParcelableWrapper[size]; 1317 } 1318 }; 1319 1320 /** 1321 * Reads a DisplayCutout from a {@link Parcel}. 1322 * 1323 * @see #writeCutoutToParcel(DisplayCutout, Parcel, int) 1324 */ readCutoutFromParcel(Parcel in)1325 public static DisplayCutout readCutoutFromParcel(Parcel in) { 1326 int variant = in.readInt(); 1327 if (variant == -1) { 1328 return null; 1329 } 1330 if (variant == 0) { 1331 return NO_CUTOUT; 1332 } 1333 1334 Rect safeInsets = in.readTypedObject(Rect.CREATOR); 1335 Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH]; 1336 in.readTypedArray(bounds, Rect.CREATOR); 1337 Insets waterfallInsets = in.readTypedObject(Insets.CREATOR); 1338 int displayWidth = in.readInt(); 1339 int displayHeight = in.readInt(); 1340 int physicalDisplayWidth = in.readInt(); 1341 int physicalDisplayHeight = in.readInt(); 1342 float density = in.readFloat(); 1343 String cutoutSpec = in.readString(); 1344 int rotation = in.readInt(); 1345 float scale = in.readFloat(); 1346 float physicalPixelDisplaySizeRatio = in.readFloat(); 1347 final CutoutPathParserInfo info = new CutoutPathParserInfo( 1348 displayWidth, displayHeight, physicalDisplayWidth, physicalDisplayHeight, 1349 density, cutoutSpec, rotation, scale, physicalPixelDisplaySizeRatio); 1350 1351 return new DisplayCutout( 1352 safeInsets, waterfallInsets, bounds, info, false /* copyArguments */); 1353 } 1354 get()1355 public DisplayCutout get() { 1356 return mInner; 1357 } 1358 set(ParcelableWrapper cutout)1359 public void set(ParcelableWrapper cutout) { 1360 mInner = cutout.get(); 1361 } 1362 set(DisplayCutout cutout)1363 public void set(DisplayCutout cutout) { 1364 mInner = cutout; 1365 } 1366 scale(float scale)1367 public void scale(float scale) { 1368 final Rect safeInsets = mInner.getSafeInsets(); 1369 safeInsets.scale(scale); 1370 final Bounds bounds = new Bounds(mInner.mBounds.mRects, true); 1371 bounds.scale(scale); 1372 final Rect waterfallInsets = mInner.mWaterfallInsets.toRect(); 1373 waterfallInsets.scale(scale); 1374 final CutoutPathParserInfo info = new CutoutPathParserInfo( 1375 mInner.mCutoutPathParserInfo.getDisplayWidth(), 1376 mInner.mCutoutPathParserInfo.getDisplayHeight(), 1377 mInner.mCutoutPathParserInfo.getPhysicalDisplayWidth(), 1378 mInner.mCutoutPathParserInfo.getPhysicalDisplayHeight(), 1379 mInner.mCutoutPathParserInfo.getDensity(), 1380 mInner.mCutoutPathParserInfo.getCutoutSpec(), 1381 mInner.mCutoutPathParserInfo.getRotation(), 1382 scale, 1383 mInner.mCutoutPathParserInfo.getPhysicalPixelDisplaySizeRatio()); 1384 1385 mInner = new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, info); 1386 } 1387 1388 @Override hashCode()1389 public int hashCode() { 1390 return mInner.hashCode(); 1391 } 1392 1393 @Override equals(@ullable Object o)1394 public boolean equals(@Nullable Object o) { 1395 return o instanceof ParcelableWrapper 1396 && mInner.equals(((ParcelableWrapper) o).mInner); 1397 } 1398 1399 @Override toString()1400 public String toString() { 1401 return String.valueOf(mInner); 1402 } 1403 } 1404 1405 /** 1406 * A Builder class to construct a DisplayCutout instance. 1407 * 1408 * <p>Note that this is only for tests purpose. For production code, developers should always 1409 * use a {@link DisplayCutout} obtained from the system.</p> 1410 */ 1411 public static final class Builder { 1412 private Insets mSafeInsets = Insets.NONE; 1413 private Insets mWaterfallInsets = Insets.NONE; 1414 private Path mCutoutPath; 1415 private final Rect mBoundingRectLeft = new Rect(); 1416 private final Rect mBoundingRectTop = new Rect(); 1417 private final Rect mBoundingRectRight = new Rect(); 1418 private final Rect mBoundingRectBottom = new Rect(); 1419 1420 /** 1421 * Begin building a DisplayCutout. 1422 */ Builder()1423 public Builder() { 1424 } 1425 1426 /** 1427 * Construct a new {@link DisplayCutout} with the set parameters. 1428 */ 1429 @NonNull build()1430 public DisplayCutout build() { 1431 final CutoutPathParserInfo info; 1432 if (mCutoutPath != null) { 1433 // Create a fake CutoutPathParserInfo and set it to sCachedCutoutPathParserInfo so 1434 // that when getCutoutPath() is called, it will return the cached Path. 1435 info = new CutoutPathParserInfo(0, 0, 0, 0, 0, "test", ROTATION_0, 1f, 1f); 1436 synchronized (CACHE_LOCK) { 1437 DisplayCutout.sCachedCutoutPathParserInfo = info; 1438 DisplayCutout.sCachedCutoutPath = mCutoutPath; 1439 } 1440 } else { 1441 info = null; 1442 } 1443 return new DisplayCutout(mSafeInsets.toRect(), mWaterfallInsets, mBoundingRectLeft, 1444 mBoundingRectTop, mBoundingRectRight, mBoundingRectBottom, info, false); 1445 } 1446 1447 /** 1448 * Set the safe insets. If not set, the default value is {@link Insets#NONE}. 1449 */ 1450 @SuppressWarnings("MissingGetterMatchingBuilder") 1451 @NonNull setSafeInsets(@onNull Insets safeInsets)1452 public Builder setSafeInsets(@NonNull Insets safeInsets) { 1453 mSafeInsets = safeInsets; 1454 return this; 1455 } 1456 1457 /** 1458 * Set the waterfall insets of the DisplayCutout. If not set, the default value is 1459 * {@link Insets#NONE} 1460 */ 1461 @NonNull setWaterfallInsets(@onNull Insets waterfallInsets)1462 public Builder setWaterfallInsets(@NonNull Insets waterfallInsets) { 1463 mWaterfallInsets = waterfallInsets; 1464 return this; 1465 } 1466 1467 /** 1468 * Set a bounding rectangle for a non-functional area on the display which is located on 1469 * the left of the screen. If not set, the default value is an empty rectangle. 1470 */ 1471 @NonNull setBoundingRectLeft(@onNull Rect boundingRectLeft)1472 public Builder setBoundingRectLeft(@NonNull Rect boundingRectLeft) { 1473 mBoundingRectLeft.set(boundingRectLeft); 1474 return this; 1475 } 1476 1477 /** 1478 * Set a bounding rectangle for a non-functional area on the display which is located on 1479 * the top of the screen. If not set, the default value is an empty rectangle. 1480 */ 1481 @NonNull setBoundingRectTop(@onNull Rect boundingRectTop)1482 public Builder setBoundingRectTop(@NonNull Rect boundingRectTop) { 1483 mBoundingRectTop.set(boundingRectTop); 1484 return this; 1485 } 1486 1487 /** 1488 * Set a bounding rectangle for a non-functional area on the display which is located on 1489 * the right of the screen. If not set, the default value is an empty rectangle. 1490 */ 1491 @NonNull setBoundingRectRight(@onNull Rect boundingRectRight)1492 public Builder setBoundingRectRight(@NonNull Rect boundingRectRight) { 1493 mBoundingRectRight.set(boundingRectRight); 1494 return this; 1495 } 1496 1497 /** 1498 * Set a bounding rectangle for a non-functional area on the display which is located on 1499 * the bottom of the screen. If not set, the default value is an empty rectangle. 1500 */ 1501 @NonNull setBoundingRectBottom(@onNull Rect boundingRectBottom)1502 public Builder setBoundingRectBottom(@NonNull Rect boundingRectBottom) { 1503 mBoundingRectBottom.set(boundingRectBottom); 1504 return this; 1505 } 1506 1507 /** 1508 * Set the cutout {@link Path}. 1509 * 1510 * Note that not support creating/testing multiple display cutouts with setCutoutPath() in 1511 * parallel. 1512 */ 1513 @NonNull setCutoutPath(@onNull Path cutoutPath)1514 public Builder setCutoutPath(@NonNull Path cutoutPath) { 1515 mCutoutPath = cutoutPath; 1516 return this; 1517 } 1518 } 1519 } 1520