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.graphics.Insets; 35 import android.graphics.Matrix; 36 import android.graphics.Path; 37 import android.graphics.Rect; 38 import android.os.Parcel; 39 import android.os.Parcelable; 40 import android.text.TextUtils; 41 import android.util.Pair; 42 import android.util.RotationUtils; 43 import android.util.proto.ProtoOutputStream; 44 import android.view.Surface.Rotation; 45 46 import com.android.internal.R; 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.internal.annotations.VisibleForTesting; 49 50 import java.lang.annotation.Retention; 51 import java.lang.annotation.RetentionPolicy; 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.List; 55 56 /** 57 * Represents the area of the display that is not functional for displaying content. 58 * 59 * <p>{@code DisplayCutout} is immutable. 60 */ 61 public final class DisplayCutout { 62 63 private static final String TAG = "DisplayCutout"; 64 65 /** 66 * Category for overlays that allow emulating a display cutout on devices that don't have 67 * one. 68 * 69 * @see android.content.om.IOverlayManager 70 * @hide 71 */ 72 public static final String EMULATION_OVERLAY_CATEGORY = 73 "com.android.internal.display_cutout_emulation"; 74 75 private static final Rect ZERO_RECT = new Rect(); 76 private static final CutoutPathParserInfo EMPTY_PARSER_INFO = new CutoutPathParserInfo( 77 0 /* displayWidth */, 0 /* displayHeight */, 0f /* density */, "" /* cutoutSpec */, 78 0 /* rotation */, 0f /* scale */); 79 80 /** 81 * An instance where {@link #isEmpty()} returns {@code true}. 82 * 83 * @hide 84 */ 85 public static final DisplayCutout NO_CUTOUT = new DisplayCutout( 86 ZERO_RECT, Insets.NONE, ZERO_RECT, ZERO_RECT, ZERO_RECT, ZERO_RECT, EMPTY_PARSER_INFO, 87 false /* copyArguments */); 88 89 90 private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null); 91 private static final Object CACHE_LOCK = new Object(); 92 93 @GuardedBy("CACHE_LOCK") 94 private static String sCachedSpec; 95 @GuardedBy("CACHE_LOCK") 96 private static int sCachedDisplayWidth; 97 @GuardedBy("CACHE_LOCK") 98 private static int sCachedDisplayHeight; 99 @GuardedBy("CACHE_LOCK") 100 private static float sCachedDensity; 101 @GuardedBy("CACHE_LOCK") 102 private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR; 103 @GuardedBy("CACHE_LOCK") 104 private static Insets sCachedWaterfallInsets; 105 106 @GuardedBy("CACHE_LOCK") 107 private static CutoutPathParserInfo sCachedCutoutPathParserInfo; 108 @GuardedBy("CACHE_LOCK") 109 private static Path sCachedCutoutPath; 110 111 private final Rect mSafeInsets; 112 @NonNull 113 private final Insets mWaterfallInsets; 114 115 /** 116 * The bound is at the left of the screen. 117 * @hide 118 */ 119 public static final int BOUNDS_POSITION_LEFT = 0; 120 121 /** 122 * The bound is at the top of the screen. 123 * @hide 124 */ 125 public static final int BOUNDS_POSITION_TOP = 1; 126 127 /** 128 * The bound is at the right of the screen. 129 * @hide 130 */ 131 public static final int BOUNDS_POSITION_RIGHT = 2; 132 133 /** 134 * The bound is at the bottom of the screen. 135 * @hide 136 */ 137 public static final int BOUNDS_POSITION_BOTTOM = 3; 138 139 /** 140 * The number of possible positions at which bounds can be located. 141 * @hide 142 */ 143 public static final int BOUNDS_POSITION_LENGTH = 4; 144 145 /** @hide */ 146 @IntDef(prefix = { "BOUNDS_POSITION_" }, value = { 147 BOUNDS_POSITION_LEFT, 148 BOUNDS_POSITION_TOP, 149 BOUNDS_POSITION_RIGHT, 150 BOUNDS_POSITION_BOTTOM 151 }) 152 @Retention(RetentionPolicy.SOURCE) 153 public @interface BoundsPosition {} 154 155 private static class Bounds { 156 private final Rect[] mRects; 157 Bounds(Rect left, Rect top, Rect right, Rect bottom, boolean copyArguments)158 private Bounds(Rect left, Rect top, Rect right, Rect bottom, boolean copyArguments) { 159 mRects = new Rect[BOUNDS_POSITION_LENGTH]; 160 mRects[BOUNDS_POSITION_LEFT] = getCopyOrRef(left, copyArguments); 161 mRects[BOUNDS_POSITION_TOP] = getCopyOrRef(top, copyArguments); 162 mRects[BOUNDS_POSITION_RIGHT] = getCopyOrRef(right, copyArguments); 163 mRects[BOUNDS_POSITION_BOTTOM] = getCopyOrRef(bottom, copyArguments); 164 165 } 166 Bounds(Rect[] rects, boolean copyArguments)167 private Bounds(Rect[] rects, boolean copyArguments) { 168 if (rects.length != BOUNDS_POSITION_LENGTH) { 169 throw new IllegalArgumentException( 170 "rects must have exactly 4 elements: rects=" + Arrays.toString(rects)); 171 } 172 if (copyArguments) { 173 mRects = new Rect[BOUNDS_POSITION_LENGTH]; 174 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 175 mRects[i] = new Rect(rects[i]); 176 } 177 } else { 178 for (Rect rect : rects) { 179 if (rect == null) { 180 throw new IllegalArgumentException( 181 "rects must have non-null elements: rects=" 182 + Arrays.toString(rects)); 183 } 184 } 185 mRects = rects; 186 } 187 } 188 isEmpty()189 private boolean isEmpty() { 190 for (Rect rect : mRects) { 191 if (!rect.isEmpty()) { 192 return false; 193 } 194 } 195 return true; 196 } 197 getRect(@oundsPosition int pos)198 private Rect getRect(@BoundsPosition int pos) { 199 return new Rect(mRects[pos]); 200 } 201 getRects()202 private Rect[] getRects() { 203 Rect[] rects = new Rect[BOUNDS_POSITION_LENGTH]; 204 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 205 rects[i] = new Rect(mRects[i]); 206 } 207 return rects; 208 } 209 scale(float scale)210 private void scale(float scale) { 211 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 212 mRects[i].scale(scale); 213 } 214 } 215 216 @Override hashCode()217 public int hashCode() { 218 int result = 0; 219 for (Rect rect : mRects) { 220 result = result * 48271 + rect.hashCode(); 221 } 222 return result; 223 } 224 225 @Override equals(@ullable Object o)226 public boolean equals(@Nullable Object o) { 227 if (o == this) { 228 return true; 229 } 230 if (o instanceof Bounds) { 231 Bounds b = (Bounds) o; 232 return Arrays.deepEquals(mRects, b.mRects); 233 } 234 return false; 235 } 236 237 @Override toString()238 public String toString() { 239 return "Bounds=" + Arrays.toString(mRects); 240 } 241 242 } 243 244 private final Bounds mBounds; 245 246 /** 247 * Stores all the needed info to create the cutout paths. 248 * 249 * @hide 250 */ 251 public static class CutoutPathParserInfo { 252 private final int mDisplayWidth; 253 private final int mDisplayHeight; 254 private final float mDensity; 255 private final String mCutoutSpec; 256 private final @Rotation int mRotation; 257 private final float mScale; 258 CutoutPathParserInfo(int displayWidth, int displayHeight, float density, String cutoutSpec, @Rotation int rotation, float scale)259 public CutoutPathParserInfo(int displayWidth, int displayHeight, float density, 260 String cutoutSpec, @Rotation int rotation, float scale) { 261 mDisplayWidth = displayWidth; 262 mDisplayHeight = displayHeight; 263 mDensity = density; 264 mCutoutSpec = cutoutSpec == null ? "" : cutoutSpec; 265 mRotation = rotation; 266 mScale = scale; 267 } 268 CutoutPathParserInfo(CutoutPathParserInfo cutoutPathParserInfo)269 public CutoutPathParserInfo(CutoutPathParserInfo cutoutPathParserInfo) { 270 mDisplayWidth = cutoutPathParserInfo.mDisplayWidth; 271 mDisplayHeight = cutoutPathParserInfo.mDisplayHeight; 272 mDensity = cutoutPathParserInfo.mDensity; 273 mCutoutSpec = cutoutPathParserInfo.mCutoutSpec; 274 mRotation = cutoutPathParserInfo.mRotation; 275 mScale = cutoutPathParserInfo.mScale; 276 } 277 getDisplayWidth()278 public int getDisplayWidth() { 279 return mDisplayWidth; 280 } 281 getDisplayHeight()282 public int getDisplayHeight() { 283 return mDisplayHeight; 284 } 285 getDensity()286 public float getDensity() { 287 return mDensity; 288 } 289 getCutoutSpec()290 public @NonNull String getCutoutSpec() { 291 return mCutoutSpec; 292 } 293 getRotation()294 public int getRotation() { 295 return mRotation; 296 } 297 getScale()298 public float getScale() { 299 return mScale; 300 } 301 hasCutout()302 private boolean hasCutout() { 303 return !mCutoutSpec.isEmpty(); 304 } 305 306 @Override hashCode()307 public int hashCode() { 308 int result = 0; 309 result = result * 48271 + Integer.hashCode(mDisplayWidth); 310 result = result * 48271 + Integer.hashCode(mDisplayHeight); 311 result = result * 48271 + Float.hashCode(mDensity); 312 result = result * 48271 + mCutoutSpec.hashCode(); 313 result = result * 48271 + Integer.hashCode(mRotation); 314 result = result * 48271 + Float.hashCode(mScale); 315 return result; 316 } 317 318 @Override equals(@ullable Object o)319 public boolean equals(@Nullable Object o) { 320 if (o == this) { 321 return true; 322 } 323 if (o instanceof CutoutPathParserInfo) { 324 CutoutPathParserInfo c = (CutoutPathParserInfo) o; 325 return mDisplayWidth == c.mDisplayWidth && mDisplayHeight == c.mDisplayHeight 326 && mDensity == c.mDensity && mCutoutSpec.equals(c.mCutoutSpec) 327 && mRotation == c.mRotation && mScale == c.mScale; 328 } 329 return false; 330 } 331 332 @Override toString()333 public String toString() { 334 return "CutoutPathParserInfo{displayWidth=" + mDisplayWidth 335 + " displayHeight=" + mDisplayHeight 336 + " density={" + mDensity + "}" 337 + " cutoutSpec={" + mCutoutSpec + "}" 338 + " rotation={" + mRotation + "}" 339 + " scale={" + mScale + "}" 340 + "}"; 341 } 342 } 343 344 private final @NonNull CutoutPathParserInfo mCutoutPathParserInfo; 345 346 /** 347 * Creates a DisplayCutout instance. 348 * 349 * <p>Note that this is only useful for tests. For production code, developers should always 350 * use a {@link DisplayCutout} obtained from the system.</p> 351 * 352 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 353 * {@link #getSafeInsetTop()} etc. 354 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 355 * it's treated as an empty rectangle (0,0)-(0,0). 356 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 357 * it's treated as an empty rectangle (0,0)-(0,0). 358 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 359 * passed, it's treated as an empty rectangle (0,0)-(0,0). 360 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 361 * passed, it's treated as an empty rectangle (0,0)-(0,0). 362 */ 363 // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) DisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom)364 public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 365 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom) { 366 this(safeInsets.toRect(), Insets.NONE, boundLeft, boundTop, boundRight, boundBottom, null, 367 true); 368 } 369 370 /** 371 * Creates a DisplayCutout instance. 372 * 373 * <p>Note that this is only useful for tests. For production code, developers should always 374 * use a {@link DisplayCutout} obtained from the system.</p> 375 * 376 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 377 * {@link #getSafeInsetTop()} etc. 378 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 379 * it's treated as an empty rectangle (0,0)-(0,0). 380 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 381 * it's treated as an empty rectangle (0,0)-(0,0). 382 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 383 * passed, it's treated as an empty rectangle (0,0)-(0,0). 384 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 385 * passed, it's treated as an empty rectangle (0,0)-(0,0). 386 * @param waterfallInsets the insets for the curved areas in waterfall display. 387 */ DisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets)388 public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 389 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, 390 @NonNull Insets waterfallInsets) { 391 this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, 392 null, true); 393 } 394 395 /** 396 * Creates a DisplayCutout instance. 397 * 398 * <p>Note that this is only useful for tests. For production code, developers should always 399 * use a {@link DisplayCutout} obtained from the system.</p> 400 * 401 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 402 * {@link #getSafeInsetTop()} etc. 403 * @param boundingRects the bounding rects of the display cutouts as returned by 404 * {@link #getBoundingRects()} ()}. 405 * @deprecated Use {@link DisplayCutout#DisplayCutout(Insets, Rect, Rect, Rect, Rect)} instead. 406 */ 407 // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) 408 @Deprecated DisplayCutout(@ullable Rect safeInsets, @Nullable List<Rect> boundingRects)409 public DisplayCutout(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) { 410 this(safeInsets, Insets.NONE, extractBoundsFromList(safeInsets, boundingRects), null, 411 true /* copyArguments */); 412 } 413 414 /** 415 * Creates a DisplayCutout instance. 416 * 417 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 418 * {@link #getSafeInsetTop()} etc. 419 * @param waterfallInsets the insets for the curved areas in waterfall display. 420 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 421 * it's treated as an empty rectangle (0,0)-(0,0). 422 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 423 * it's treated as an empty rectangle (0,0)-(0,0). 424 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 425 * passed, it's treated as an empty rectangle (0,0)-(0,0). 426 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 427 * passed, it's treated as an empty rectangle (0,0)-(0,0). 428 * @param info the cutout path parser info. 429 * @param copyArguments if true, create a copy of the arguments. If false, the passed arguments 430 * are not copied and MUST remain unchanged forever. 431 */ DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect boundLeft, Rect boundTop, Rect boundRight, Rect boundBottom, CutoutPathParserInfo info, boolean copyArguments)432 private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect boundLeft, Rect boundTop, 433 Rect boundRight, Rect boundBottom, CutoutPathParserInfo info, 434 boolean copyArguments) { 435 mSafeInsets = getCopyOrRef(safeInsets, copyArguments); 436 mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; 437 mBounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments); 438 mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; 439 } 440 DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect[] bounds, CutoutPathParserInfo info, boolean copyArguments)441 private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect[] bounds, 442 CutoutPathParserInfo info, boolean copyArguments) { 443 mSafeInsets = getCopyOrRef(safeInsets, copyArguments); 444 mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; 445 mBounds = new Bounds(bounds, copyArguments); 446 mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; 447 } 448 DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds, CutoutPathParserInfo info)449 private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds, 450 CutoutPathParserInfo info) { 451 mSafeInsets = safeInsets; 452 mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets; 453 mBounds = bounds; 454 mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info; 455 } 456 getCopyOrRef(Rect r, boolean copyArguments)457 private static Rect getCopyOrRef(Rect r, boolean copyArguments) { 458 if (r == null) { 459 return ZERO_RECT; 460 } else if (copyArguments) { 461 return new Rect(r); 462 } else { 463 return r; 464 } 465 } 466 467 /** 468 * Returns the insets representing the curved areas of a waterfall display. 469 * 470 * A waterfall display has curved areas along the edges of the screen. Apps should be careful 471 * when showing UI and handling touch input in those insets because the curve may impair 472 * legibility and can frequently lead to unintended touch inputs. 473 * 474 * @return the insets for the curved areas of a waterfall display in pixels or {@code 475 * Insets.NONE} if there are no curved areas or they don't overlap with the window. 476 */ getWaterfallInsets()477 public @NonNull Insets getWaterfallInsets() { 478 return mWaterfallInsets; 479 } 480 481 482 /** 483 * Find the position of the bounding rect, and create an array of Rect whose index represents 484 * the position (= BoundsPosition). 485 * 486 * @hide 487 */ extractBoundsFromList(Rect safeInsets, List<Rect> boundingRects)488 public static Rect[] extractBoundsFromList(Rect safeInsets, List<Rect> boundingRects) { 489 Rect[] sortedBounds = new Rect[BOUNDS_POSITION_LENGTH]; 490 for (int i = 0; i < sortedBounds.length; ++i) { 491 sortedBounds[i] = ZERO_RECT; 492 } 493 if (safeInsets != null && boundingRects != null) { 494 // There is at most one non-functional area per short edge of the device, but none 495 // on the long edges, so either a) safeInsets.top and safeInsets.bottom is 0, or 496 // b) safeInsets.left and safeInset.right is 0. 497 final boolean topBottomInset = safeInsets.top > 0 || safeInsets.bottom > 0; 498 for (Rect bound : boundingRects) { 499 if (topBottomInset) { 500 if (bound.top == 0) { 501 sortedBounds[BOUNDS_POSITION_TOP] = bound; 502 } else { 503 sortedBounds[BOUNDS_POSITION_BOTTOM] = bound; 504 } 505 } else { 506 if (bound.left == 0) { 507 sortedBounds[BOUNDS_POSITION_LEFT] = bound; 508 } else { 509 sortedBounds[BOUNDS_POSITION_RIGHT] = bound; 510 } 511 } 512 } 513 } 514 return sortedBounds; 515 } 516 517 /** 518 * Returns true if there is no cutout, i.e. the bounds are empty. 519 * 520 * @hide 521 */ isBoundsEmpty()522 public boolean isBoundsEmpty() { 523 return mBounds.isEmpty(); 524 } 525 526 /** 527 * Returns true if the safe insets are empty (and therefore the current view does not 528 * overlap with the cutout or cutout area). 529 * 530 * @hide 531 */ isEmpty()532 public boolean isEmpty() { 533 return mSafeInsets.equals(ZERO_RECT); 534 } 535 536 /** 537 * Returns the inset from the top which avoids the display cutout in pixels. 538 * 539 * @see WindowInsets.Type#displayCutout() 540 */ getSafeInsetTop()541 public int getSafeInsetTop() { 542 return mSafeInsets.top; 543 } 544 545 /** 546 * Returns the inset from the bottom which avoids the display cutout in pixels. 547 * 548 * @see WindowInsets.Type#displayCutout() 549 */ getSafeInsetBottom()550 public int getSafeInsetBottom() { 551 return mSafeInsets.bottom; 552 } 553 554 /** 555 * Returns the inset from the left which avoids the display cutout in pixels. 556 * 557 * @see WindowInsets.Type#displayCutout() 558 */ getSafeInsetLeft()559 public int getSafeInsetLeft() { 560 return mSafeInsets.left; 561 } 562 563 /** 564 * Returns the inset from the right which avoids the display cutout in pixels. 565 * 566 * @see WindowInsets.Type#displayCutout() 567 */ getSafeInsetRight()568 public int getSafeInsetRight() { 569 return mSafeInsets.right; 570 } 571 572 /** 573 * Returns the safe insets in a rect in pixel units. 574 * 575 * @return a rect which is set to the safe insets. 576 * @hide 577 */ getSafeInsets()578 public Rect getSafeInsets() { 579 return new Rect(mSafeInsets); 580 } 581 582 /** 583 * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional 584 * area on the display. 585 * 586 * There will be at most one non-functional area per short edge of the device, and none on 587 * the long edges. 588 * 589 * @return a list of bounding {@code Rect}s, one for each display cutout area. No empty Rect is 590 * returned. 591 */ 592 @NonNull getBoundingRects()593 public List<Rect> getBoundingRects() { 594 List<Rect> result = new ArrayList<>(); 595 for (Rect bound : getBoundingRectsAll()) { 596 if (!bound.isEmpty()) { 597 result.add(new Rect(bound)); 598 } 599 } 600 return result; 601 } 602 603 /** 604 * Returns an array of {@code Rect}s, each of which is the bounding rectangle for a non- 605 * functional area on the display. Ordinal value of BoundPosition is used as an index of 606 * the array. 607 * 608 * There will be at most one non-functional area per short edge of the device, and none on 609 * the long edges. 610 * 611 * @return an array of bounding {@code Rect}s, one for each display cutout area. This might 612 * contain ZERO_RECT, which means there is no cutout area at the position. 613 * 614 * @hide 615 */ getBoundingRectsAll()616 public Rect[] getBoundingRectsAll() { 617 return mBounds.getRects(); 618 } 619 620 /** 621 * Returns a bounding rectangle for a non-functional area on the display which is located on 622 * the left of the screen. 623 * 624 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 625 * is returned. 626 */ getBoundingRectLeft()627 public @NonNull Rect getBoundingRectLeft() { 628 return mBounds.getRect(BOUNDS_POSITION_LEFT); 629 } 630 631 /** 632 * Returns a bounding rectangle for a non-functional area on the display which is located on 633 * the top of the screen. 634 * 635 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 636 * is returned. 637 */ getBoundingRectTop()638 public @NonNull Rect getBoundingRectTop() { 639 return mBounds.getRect(BOUNDS_POSITION_TOP); 640 } 641 642 /** 643 * Returns a bounding rectangle for a non-functional area on the display which is located on 644 * the right of the screen. 645 * 646 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 647 * is returned. 648 */ getBoundingRectRight()649 public @NonNull Rect getBoundingRectRight() { 650 return mBounds.getRect(BOUNDS_POSITION_RIGHT); 651 } 652 653 /** 654 * Returns a bounding rectangle for a non-functional area on the display which is located on 655 * the bottom of the screen. 656 * 657 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 658 * is returned. 659 */ getBoundingRectBottom()660 public @NonNull Rect getBoundingRectBottom() { 661 return mBounds.getRect(BOUNDS_POSITION_BOTTOM); 662 } 663 664 /** 665 * Returns a {@link Path} that contains the cutout paths of all sides on the display. 666 * 667 * To get a cutout path for one specific side, apps can intersect the {@link Path} with the 668 * {@link Rect} obtained from {@link #getBoundingRectLeft()}, {@link #getBoundingRectTop()}, 669 * {@link #getBoundingRectRight()} or {@link #getBoundingRectBottom()}. 670 * 671 * @return a {@link Path} contains all the cutout paths based on display coordinate. Returns 672 * null if there is no cutout on the display. 673 */ getCutoutPath()674 public @Nullable Path getCutoutPath() { 675 if (!mCutoutPathParserInfo.hasCutout()) { 676 return null; 677 } 678 synchronized (CACHE_LOCK) { 679 if (mCutoutPathParserInfo.equals(sCachedCutoutPathParserInfo)) { 680 return sCachedCutoutPath; 681 } 682 } 683 final CutoutSpecification cutoutSpec = new CutoutSpecification.Parser( 684 mCutoutPathParserInfo.getDensity(), mCutoutPathParserInfo.getDisplayWidth(), 685 mCutoutPathParserInfo.getDisplayHeight()) 686 .parse(mCutoutPathParserInfo.getCutoutSpec()); 687 688 final Path cutoutPath = cutoutSpec.getPath(); 689 if (cutoutPath == null || cutoutPath.isEmpty()) { 690 return null; 691 } 692 final Matrix matrix = new Matrix(); 693 if (mCutoutPathParserInfo.getRotation() != ROTATION_0) { 694 RotationUtils.transformPhysicalToLogicalCoordinates( 695 mCutoutPathParserInfo.getRotation(), 696 mCutoutPathParserInfo.getDisplayWidth(), 697 mCutoutPathParserInfo.getDisplayHeight(), 698 matrix 699 ); 700 } 701 matrix.postScale(mCutoutPathParserInfo.getScale(), mCutoutPathParserInfo.getScale()); 702 cutoutPath.transform(matrix); 703 704 synchronized (CACHE_LOCK) { 705 sCachedCutoutPathParserInfo = new CutoutPathParserInfo(mCutoutPathParserInfo); 706 sCachedCutoutPath = cutoutPath; 707 } 708 return cutoutPath; 709 } 710 711 /** 712 * @return the {@link CutoutPathParserInfo}; 713 * 714 * @hide 715 */ getCutoutPathParserInfo()716 public CutoutPathParserInfo getCutoutPathParserInfo() { 717 return mCutoutPathParserInfo; 718 } 719 720 @Override hashCode()721 public int hashCode() { 722 int result = 0; 723 result = 48271 * result + mSafeInsets.hashCode(); 724 result = 48271 * result + mBounds.hashCode(); 725 result = 48271 * result + mWaterfallInsets.hashCode(); 726 result = 48271 * result + mCutoutPathParserInfo.hashCode(); 727 return result; 728 } 729 730 @Override equals(@ullable Object o)731 public boolean equals(@Nullable Object o) { 732 if (o == this) { 733 return true; 734 } 735 if (o instanceof DisplayCutout) { 736 DisplayCutout c = (DisplayCutout) o; 737 return mSafeInsets.equals(c.mSafeInsets) && mBounds.equals(c.mBounds) 738 && mWaterfallInsets.equals(c.mWaterfallInsets) 739 && mCutoutPathParserInfo.equals(c.mCutoutPathParserInfo); 740 } 741 return false; 742 } 743 744 @Override toString()745 public String toString() { 746 return "DisplayCutout{insets=" + mSafeInsets 747 + " waterfall=" + mWaterfallInsets 748 + " boundingRect={" + mBounds + "}" 749 + " cutoutPathParserInfo={" + mCutoutPathParserInfo + "}" 750 + "}"; 751 } 752 753 /** 754 * @hide 755 */ dumpDebug(ProtoOutputStream proto, long fieldId)756 public void dumpDebug(ProtoOutputStream proto, long fieldId) { 757 final long token = proto.start(fieldId); 758 mSafeInsets.dumpDebug(proto, INSETS); 759 mBounds.getRect(BOUNDS_POSITION_LEFT).dumpDebug(proto, BOUND_LEFT); 760 mBounds.getRect(BOUNDS_POSITION_TOP).dumpDebug(proto, BOUND_TOP); 761 mBounds.getRect(BOUNDS_POSITION_RIGHT).dumpDebug(proto, BOUND_RIGHT); 762 mBounds.getRect(BOUNDS_POSITION_BOTTOM).dumpDebug(proto, BOUND_BOTTOM); 763 mWaterfallInsets.toRect().dumpDebug(proto, INSETS); 764 proto.end(token); 765 } 766 767 /** 768 * Insets the reference frame of the cutout in the given directions. 769 * 770 * @return a copy of this instance which has been inset 771 * @hide 772 */ inset(int insetLeft, int insetTop, int insetRight, int insetBottom)773 public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { 774 if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0 775 || (isBoundsEmpty() && mWaterfallInsets.equals(Insets.NONE))) { 776 return this; 777 } 778 779 Rect safeInsets = insetInsets(insetLeft, insetTop, insetRight, insetBottom, 780 new Rect(mSafeInsets)); 781 782 // If we are not cutting off part of the cutout by insetting it on bottom/right, and we also 783 // don't move it around, we can avoid the allocation and copy of the instance. 784 if (insetLeft == 0 && insetTop == 0 && mSafeInsets.equals(safeInsets)) { 785 return this; 786 } 787 788 Rect waterfallInsets = insetInsets(insetLeft, insetTop, insetRight, insetBottom, 789 mWaterfallInsets.toRect()); 790 791 Rect[] bounds = mBounds.getRects(); 792 for (int i = 0; i < bounds.length; ++i) { 793 if (!bounds[i].equals(ZERO_RECT)) { 794 bounds[i].offset(-insetLeft, -insetTop); 795 } 796 } 797 798 return new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, 799 mCutoutPathParserInfo, false /* copyArguments */); 800 } 801 insetInsets(int insetLeft, int insetTop, int insetRight, int insetBottom, Rect insets)802 private Rect insetInsets(int insetLeft, int insetTop, int insetRight, int insetBottom, 803 Rect insets) { 804 // Note: it's not really well defined what happens when the inset is negative, because we 805 // don't know if the safe inset needs to expand in general. 806 if (insetTop > 0 || insets.top > 0) { 807 insets.top = atLeastZero(insets.top - insetTop); 808 } 809 if (insetBottom > 0 || insets.bottom > 0) { 810 insets.bottom = atLeastZero(insets.bottom - insetBottom); 811 } 812 if (insetLeft > 0 || insets.left > 0) { 813 insets.left = atLeastZero(insets.left - insetLeft); 814 } 815 if (insetRight > 0 || insets.right > 0) { 816 insets.right = atLeastZero(insets.right - insetRight); 817 } 818 return insets; 819 } 820 821 /** 822 * Returns a copy of this instance with the safe insets replaced with the parameter. 823 * 824 * @param safeInsets the new safe insets in pixels 825 * @return a copy of this instance with the safe insets replaced with the argument. 826 * 827 * @hide 828 */ replaceSafeInsets(Rect safeInsets)829 public DisplayCutout replaceSafeInsets(Rect safeInsets) { 830 return new DisplayCutout(new Rect(safeInsets), mWaterfallInsets, mBounds, 831 mCutoutPathParserInfo); 832 } 833 atLeastZero(int value)834 private static int atLeastZero(int value) { 835 return value < 0 ? 0 : value; 836 } 837 838 839 /** 840 * Creates an instance from a bounding rect. 841 * 842 * @hide 843 */ 844 @VisibleForTesting fromBoundingRect( int left, int top, int right, int bottom, @BoundsPosition int pos)845 public static DisplayCutout fromBoundingRect( 846 int left, int top, int right, int bottom, @BoundsPosition int pos) { 847 Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH]; 848 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 849 bounds[i] = (pos == i) ? new Rect(left, top, right, bottom) : new Rect(); 850 } 851 return new DisplayCutout(ZERO_RECT, Insets.NONE, bounds, null, false /* copyArguments */); 852 } 853 854 /** 855 * Creates an instance from bounds, waterfall insets and CutoutPathParserInfo. 856 * 857 * @hide 858 */ constructDisplayCutout(Rect[] bounds, Insets waterfallInsets, CutoutPathParserInfo info)859 public static DisplayCutout constructDisplayCutout(Rect[] bounds, Insets waterfallInsets, 860 CutoutPathParserInfo info) { 861 return new DisplayCutout(ZERO_RECT, waterfallInsets, bounds, info, 862 false /* copyArguments */); 863 } 864 865 /** 866 * Creates an instance from a bounding {@link Path}. 867 * 868 * @hide 869 */ fromBounds(Rect[] bounds)870 public static DisplayCutout fromBounds(Rect[] bounds) { 871 return new DisplayCutout(ZERO_RECT, Insets.NONE, bounds, null /* cutoutPathParserInfo */, 872 false /* copyArguments */); 873 } 874 875 /** 876 * Creates the display cutout according to 877 * @android:string/config_mainBuiltInDisplayCutoutRectApproximation, which is the closest 878 * rectangle-base approximation of the cutout. 879 * 880 * @hide 881 */ fromResourcesRectApproximation(Resources res, int displayWidth, int displayHeight)882 public static DisplayCutout fromResourcesRectApproximation(Resources res, int displayWidth, 883 int displayHeight) { 884 return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout), 885 res.getString(R.string.config_mainBuiltInDisplayCutoutRectApproximation), 886 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT, 887 loadWaterfallInset(res)).second; 888 } 889 890 /** 891 * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout. 892 * 893 * @hide 894 */ pathFromResources(Resources res, int displayWidth, int displayHeight)895 public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) { 896 return pathAndDisplayCutoutFromSpec( 897 res.getString(R.string.config_mainBuiltInDisplayCutout), null, 898 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT, 899 loadWaterfallInset(res)).first; 900 } 901 902 /** 903 * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec. 904 * 905 * @hide 906 */ 907 @VisibleForTesting(visibility = PRIVATE) fromSpec(String pathSpec, int displayWidth, int displayHeight, float density, Insets waterfallInsets)908 public static DisplayCutout fromSpec(String pathSpec, int displayWidth, 909 int displayHeight, float density, Insets waterfallInsets) { 910 return pathAndDisplayCutoutFromSpec( 911 pathSpec, null, displayWidth, displayHeight, density, waterfallInsets) 912 .second; 913 } 914 915 /** 916 * Gets the cutout path and the corresponding DisplayCutout instance from the spec string. 917 * 918 * @param pathSpec the spec string read from config_mainBuiltInDisplayCutout. 919 * @param rectSpec the spec string read from config_mainBuiltInDisplayCutoutRectApproximation. 920 * @param displayWidth the display width. 921 * @param displayHeight the display height. 922 * @param density the display density. 923 * @param waterfallInsets the waterfall insets of the display. 924 * @return a Pair contains the cutout path and the corresponding DisplayCutout instance. 925 */ pathAndDisplayCutoutFromSpec( String pathSpec, String rectSpec, int displayWidth, int displayHeight, float density, Insets waterfallInsets)926 private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec( 927 String pathSpec, String rectSpec, int displayWidth, int displayHeight, float density, 928 Insets waterfallInsets) { 929 // Always use the rect approximation spec to create the cutout if it's not null because 930 // transforming and sending a Region constructed from a path is very costly. 931 String spec = rectSpec != null ? rectSpec : pathSpec; 932 if (TextUtils.isEmpty(spec) && waterfallInsets.equals(Insets.NONE)) { 933 return NULL_PAIR; 934 } 935 936 synchronized (CACHE_LOCK) { 937 if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth 938 && sCachedDisplayHeight == displayHeight 939 && sCachedDensity == density 940 && waterfallInsets.equals(sCachedWaterfallInsets)) { 941 return sCachedCutout; 942 } 943 } 944 945 spec = spec.trim(); 946 947 CutoutSpecification cutoutSpec = new CutoutSpecification.Parser(density, 948 displayWidth, displayHeight).parse(spec); 949 Rect safeInset = cutoutSpec.getSafeInset(); 950 final Rect boundLeft = cutoutSpec.getLeftBound(); 951 final Rect boundTop = cutoutSpec.getTopBound(); 952 final Rect boundRight = cutoutSpec.getRightBound(); 953 final Rect boundBottom = cutoutSpec.getBottomBound(); 954 955 956 if (!waterfallInsets.equals(Insets.NONE)) { 957 safeInset.set( 958 Math.max(waterfallInsets.left, safeInset.left), 959 Math.max(waterfallInsets.top, safeInset.top), 960 Math.max(waterfallInsets.right, safeInset.right), 961 Math.max(waterfallInsets.bottom, safeInset.bottom)); 962 } 963 964 final CutoutPathParserInfo cutoutPathParserInfo = new CutoutPathParserInfo(displayWidth, 965 displayHeight, density, pathSpec.trim(), ROTATION_0, 1f /* scale */); 966 967 final DisplayCutout cutout = new DisplayCutout( 968 safeInset, waterfallInsets, boundLeft, boundTop, boundRight, boundBottom, 969 cutoutPathParserInfo , false /* copyArguments */); 970 final Pair<Path, DisplayCutout> result = new Pair<>(cutoutSpec.getPath(), cutout); 971 synchronized (CACHE_LOCK) { 972 sCachedSpec = spec; 973 sCachedDisplayWidth = displayWidth; 974 sCachedDisplayHeight = displayHeight; 975 sCachedDensity = density; 976 sCachedCutout = result; 977 sCachedWaterfallInsets = waterfallInsets; 978 } 979 return result; 980 } 981 loadWaterfallInset(Resources res)982 private static Insets loadWaterfallInset(Resources res) { 983 return Insets.of( 984 res.getDimensionPixelSize(R.dimen.waterfall_display_left_edge_size), 985 res.getDimensionPixelSize(R.dimen.waterfall_display_top_edge_size), 986 res.getDimensionPixelSize(R.dimen.waterfall_display_right_edge_size), 987 res.getDimensionPixelSize(R.dimen.waterfall_display_bottom_edge_size)); 988 } 989 990 /** 991 * Helper class for passing {@link DisplayCutout} through binder. 992 * 993 * Needed, because {@code readFromParcel} cannot be used with immutable classes. 994 * 995 * @hide 996 */ 997 public static final class ParcelableWrapper implements Parcelable { 998 999 private DisplayCutout mInner; 1000 ParcelableWrapper()1001 public ParcelableWrapper() { 1002 this(NO_CUTOUT); 1003 } 1004 ParcelableWrapper(DisplayCutout cutout)1005 public ParcelableWrapper(DisplayCutout cutout) { 1006 mInner = cutout; 1007 } 1008 1009 @Override describeContents()1010 public int describeContents() { 1011 return 0; 1012 } 1013 1014 @Override writeToParcel(Parcel out, int flags)1015 public void writeToParcel(Parcel out, int flags) { 1016 writeCutoutToParcel(mInner, out, flags); 1017 } 1018 1019 /** 1020 * Writes a DisplayCutout to a {@link Parcel}. 1021 * 1022 * @see #readCutoutFromParcel(Parcel) 1023 */ writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags)1024 public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) { 1025 if (cutout == null) { 1026 out.writeInt(-1); 1027 } else if (cutout == NO_CUTOUT) { 1028 out.writeInt(0); 1029 } else { 1030 out.writeInt(1); 1031 out.writeTypedObject(cutout.mSafeInsets, flags); 1032 out.writeTypedArray(cutout.mBounds.getRects(), flags); 1033 out.writeTypedObject(cutout.mWaterfallInsets, flags); 1034 out.writeInt(cutout.mCutoutPathParserInfo.getDisplayWidth()); 1035 out.writeInt(cutout.mCutoutPathParserInfo.getDisplayHeight()); 1036 out.writeFloat(cutout.mCutoutPathParserInfo.getDensity()); 1037 out.writeString(cutout.mCutoutPathParserInfo.getCutoutSpec()); 1038 out.writeInt(cutout.mCutoutPathParserInfo.getRotation()); 1039 out.writeFloat(cutout.mCutoutPathParserInfo.getScale()); 1040 } 1041 } 1042 1043 /** 1044 * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing 1045 * instance. 1046 * 1047 * Needed for AIDL out parameters. 1048 */ readFromParcel(Parcel in)1049 public void readFromParcel(Parcel in) { 1050 mInner = readCutoutFromParcel(in); 1051 } 1052 1053 public static final @android.annotation.NonNull Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() { 1054 @Override 1055 public ParcelableWrapper createFromParcel(Parcel in) { 1056 return new ParcelableWrapper(readCutoutFromParcel(in)); 1057 } 1058 1059 @Override 1060 public ParcelableWrapper[] newArray(int size) { 1061 return new ParcelableWrapper[size]; 1062 } 1063 }; 1064 1065 /** 1066 * Reads a DisplayCutout from a {@link Parcel}. 1067 * 1068 * @see #writeCutoutToParcel(DisplayCutout, Parcel, int) 1069 */ readCutoutFromParcel(Parcel in)1070 public static DisplayCutout readCutoutFromParcel(Parcel in) { 1071 int variant = in.readInt(); 1072 if (variant == -1) { 1073 return null; 1074 } 1075 if (variant == 0) { 1076 return NO_CUTOUT; 1077 } 1078 1079 Rect safeInsets = in.readTypedObject(Rect.CREATOR); 1080 Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH]; 1081 in.readTypedArray(bounds, Rect.CREATOR); 1082 Insets waterfallInsets = in.readTypedObject(Insets.CREATOR); 1083 int displayWidth = in.readInt(); 1084 int displayHeight = in.readInt(); 1085 float density = in.readFloat(); 1086 String cutoutSpec = in.readString(); 1087 int rotation = in.readInt(); 1088 float scale = in.readFloat(); 1089 final CutoutPathParserInfo info = new CutoutPathParserInfo( 1090 displayWidth, displayHeight, density, cutoutSpec, rotation, scale); 1091 1092 return new DisplayCutout( 1093 safeInsets, waterfallInsets, bounds, info, false /* copyArguments */); 1094 } 1095 get()1096 public DisplayCutout get() { 1097 return mInner; 1098 } 1099 set(ParcelableWrapper cutout)1100 public void set(ParcelableWrapper cutout) { 1101 mInner = cutout.get(); 1102 } 1103 set(DisplayCutout cutout)1104 public void set(DisplayCutout cutout) { 1105 mInner = cutout; 1106 } 1107 scale(float scale)1108 public void scale(float scale) { 1109 final Rect safeInsets = mInner.getSafeInsets(); 1110 safeInsets.scale(scale); 1111 final Bounds bounds = new Bounds(mInner.mBounds.mRects, true); 1112 bounds.scale(scale); 1113 final Rect waterfallInsets = mInner.mWaterfallInsets.toRect(); 1114 waterfallInsets.scale(scale); 1115 final CutoutPathParserInfo info = new CutoutPathParserInfo( 1116 mInner.mCutoutPathParserInfo.getDisplayWidth(), 1117 mInner.mCutoutPathParserInfo.getDisplayHeight(), 1118 mInner.mCutoutPathParserInfo.getDensity(), 1119 mInner.mCutoutPathParserInfo.getCutoutSpec(), 1120 mInner.mCutoutPathParserInfo.getRotation(), 1121 scale); 1122 1123 mInner = new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, info); 1124 } 1125 1126 @Override hashCode()1127 public int hashCode() { 1128 return mInner.hashCode(); 1129 } 1130 1131 @Override equals(@ullable Object o)1132 public boolean equals(@Nullable Object o) { 1133 return o instanceof ParcelableWrapper 1134 && mInner.equals(((ParcelableWrapper) o).mInner); 1135 } 1136 1137 @Override toString()1138 public String toString() { 1139 return String.valueOf(mInner); 1140 } 1141 } 1142 } 1143