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 27 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 28 29 import android.annotation.IntDef; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.content.res.Resources; 33 import android.graphics.Insets; 34 import android.graphics.Matrix; 35 import android.graphics.Path; 36 import android.graphics.Rect; 37 import android.graphics.RectF; 38 import android.graphics.Region; 39 import android.graphics.Region.Op; 40 import android.os.Parcel; 41 import android.os.Parcelable; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.util.Pair; 45 import android.util.PathParser; 46 import android.util.proto.ProtoOutputStream; 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.List; 57 58 /** 59 * Represents the area of the display that is not functional for displaying content. 60 * 61 * <p>{@code DisplayCutout} is immutable. 62 */ 63 public final class DisplayCutout { 64 65 private static final String TAG = "DisplayCutout"; 66 private static final String BOTTOM_MARKER = "@bottom"; 67 private static final String DP_MARKER = "@dp"; 68 private static final String RIGHT_MARKER = "@right"; 69 70 /** 71 * Category for overlays that allow emulating a display cutout on devices that don't have 72 * one. 73 * 74 * @see android.content.om.IOverlayManager 75 * @hide 76 */ 77 public static final String EMULATION_OVERLAY_CATEGORY = 78 "com.android.internal.display_cutout_emulation"; 79 80 private static final Rect ZERO_RECT = new Rect(); 81 82 /** 83 * An instance where {@link #isEmpty()} returns {@code true}. 84 * 85 * @hide 86 */ 87 public static final DisplayCutout NO_CUTOUT = new DisplayCutout( 88 ZERO_RECT, ZERO_RECT, ZERO_RECT, ZERO_RECT, ZERO_RECT, 89 false /* copyArguments */); 90 91 92 private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null); 93 private static final Object CACHE_LOCK = new Object(); 94 95 @GuardedBy("CACHE_LOCK") 96 private static String sCachedSpec; 97 @GuardedBy("CACHE_LOCK") 98 private static int sCachedDisplayWidth; 99 @GuardedBy("CACHE_LOCK") 100 private static int sCachedDisplayHeight; 101 @GuardedBy("CACHE_LOCK") 102 private static float sCachedDensity; 103 @GuardedBy("CACHE_LOCK") 104 private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR; 105 106 private final Rect mSafeInsets; 107 108 109 /** 110 * The bound is at the left of the screen. 111 * @hide 112 */ 113 public static final int BOUNDS_POSITION_LEFT = 0; 114 115 /** 116 * The bound is at the top of the screen. 117 * @hide 118 */ 119 public static final int BOUNDS_POSITION_TOP = 1; 120 121 /** 122 * The bound is at the right of the screen. 123 * @hide 124 */ 125 public static final int BOUNDS_POSITION_RIGHT = 2; 126 127 /** 128 * The bound is at the bottom of the screen. 129 * @hide 130 */ 131 public static final int BOUNDS_POSITION_BOTTOM = 3; 132 133 /** 134 * The number of possible positions at which bounds can be located. 135 * @hide 136 */ 137 public static final int BOUNDS_POSITION_LENGTH = 4; 138 139 /** @hide */ 140 @IntDef(prefix = { "BOUNDS_POSITION_" }, value = { 141 BOUNDS_POSITION_LEFT, 142 BOUNDS_POSITION_TOP, 143 BOUNDS_POSITION_RIGHT, 144 BOUNDS_POSITION_BOTTOM 145 }) 146 @Retention(RetentionPolicy.SOURCE) 147 public @interface BoundsPosition {} 148 149 private static class Bounds { 150 private final Rect[] mRects; 151 Bounds(Rect left, Rect top, Rect right, Rect bottom, boolean copyArguments)152 private Bounds(Rect left, Rect top, Rect right, Rect bottom, boolean copyArguments) { 153 mRects = new Rect[BOUNDS_POSITION_LENGTH]; 154 mRects[BOUNDS_POSITION_LEFT] = getCopyOrRef(left, copyArguments); 155 mRects[BOUNDS_POSITION_TOP] = getCopyOrRef(top, copyArguments); 156 mRects[BOUNDS_POSITION_RIGHT] = getCopyOrRef(right, copyArguments); 157 mRects[BOUNDS_POSITION_BOTTOM] = getCopyOrRef(bottom, copyArguments); 158 159 } 160 Bounds(Rect[] rects, boolean copyArguments)161 private Bounds(Rect[] rects, boolean copyArguments) { 162 if (rects.length != BOUNDS_POSITION_LENGTH) { 163 throw new IllegalArgumentException( 164 "rects must have exactly 4 elements: rects=" + Arrays.toString(rects)); 165 } 166 if (copyArguments) { 167 mRects = new Rect[BOUNDS_POSITION_LENGTH]; 168 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 169 mRects[i] = new Rect(rects[i]); 170 } 171 } else { 172 for (Rect rect : rects) { 173 if (rect == null) { 174 throw new IllegalArgumentException( 175 "rects must have non-null elements: rects=" 176 + Arrays.toString(rects)); 177 } 178 } 179 mRects = rects; 180 } 181 } 182 isEmpty()183 private boolean isEmpty() { 184 for (Rect rect : mRects) { 185 if (!rect.isEmpty()) { 186 return false; 187 } 188 } 189 return true; 190 } 191 getRect(@oundsPosition int pos)192 private Rect getRect(@BoundsPosition int pos) { 193 return new Rect(mRects[pos]); 194 } 195 getRects()196 private Rect[] getRects() { 197 Rect[] rects = new Rect[BOUNDS_POSITION_LENGTH]; 198 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 199 rects[i] = new Rect(mRects[i]); 200 } 201 return rects; 202 } 203 204 @Override hashCode()205 public int hashCode() { 206 int result = 0; 207 for (Rect rect : mRects) { 208 result = result * 48271 + rect.hashCode(); 209 } 210 return result; 211 } 212 @Override equals(Object o)213 public boolean equals(Object o) { 214 if (o == this) { 215 return true; 216 } 217 if (o instanceof Bounds) { 218 Bounds b = (Bounds) o; 219 return Arrays.deepEquals(mRects, b.mRects); 220 } 221 return false; 222 } 223 224 @Override toString()225 public String toString() { 226 return "Bounds=" + Arrays.toString(mRects); 227 } 228 229 } 230 231 private final Bounds mBounds; 232 233 /** 234 * Creates a DisplayCutout instance. 235 * 236 * <p>Note that this is only useful for tests. For production code, developers should always 237 * use a {@link DisplayCutout} obtained from the system.</p> 238 * 239 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 240 * {@link #getSafeInsetTop()} etc. 241 * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed, 242 * it's treated as an empty rectangle (0,0)-(0,0). 243 * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed, 244 * it's treated as an empty rectangle (0,0)-(0,0). 245 * @param boundRight the right bounding rect of the display cutout in pixels. If null is 246 * passed, it's treated as an empty rectangle (0,0)-(0,0). 247 * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is 248 * passed, it's treated as an empty rectangle (0,0)-(0,0). 249 */ 250 // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) DisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom)251 public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft, 252 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom) { 253 this(safeInsets.toRect(), boundLeft, boundTop, boundRight, boundBottom, true); 254 } 255 256 /** 257 * Creates a DisplayCutout instance. 258 * 259 * <p>Note that this is only useful for tests. For production code, developers should always 260 * use a {@link DisplayCutout} obtained from the system.</p> 261 * 262 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 263 * {@link #getSafeInsetTop()} etc. 264 * @param boundingRects the bounding rects of the display cutouts as returned by 265 * {@link #getBoundingRects()} ()}. 266 * @deprecated Use {@link DisplayCutout#DisplayCutout(Insets, Rect, Rect, Rect, Rect)} instead. 267 */ 268 // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) 269 @Deprecated DisplayCutout(@ullable Rect safeInsets, @Nullable List<Rect> boundingRects)270 public DisplayCutout(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) { 271 this(safeInsets, extractBoundsFromList(safeInsets, boundingRects), 272 true /* copyArguments */); 273 } 274 275 /** 276 * Creates a DisplayCutout instance. 277 * 278 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 279 * {@link #getSafeInsetTop()} etc. 280 * @param copyArguments if true, create a copy of the arguments. If false, the passed arguments 281 * are not copied and MUST remain unchanged forever. 282 */ DisplayCutout(Rect safeInsets, Rect boundLeft, Rect boundTop, Rect boundRight, Rect boundBottom, boolean copyArguments)283 private DisplayCutout(Rect safeInsets, Rect boundLeft, Rect boundTop, Rect boundRight, 284 Rect boundBottom, boolean copyArguments) { 285 mSafeInsets = getCopyOrRef(safeInsets, copyArguments); 286 mBounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments); 287 } 288 DisplayCutout(Rect safeInsets, Rect[] bounds, boolean copyArguments)289 private DisplayCutout(Rect safeInsets, Rect[] bounds, boolean copyArguments) { 290 mSafeInsets = getCopyOrRef(safeInsets, copyArguments); 291 mBounds = new Bounds(bounds, copyArguments); 292 } 293 DisplayCutout(Rect safeInsets, Bounds bounds)294 private DisplayCutout(Rect safeInsets, Bounds bounds) { 295 mSafeInsets = safeInsets; 296 mBounds = bounds; 297 298 } 299 getCopyOrRef(Rect r, boolean copyArguments)300 private static Rect getCopyOrRef(Rect r, boolean copyArguments) { 301 if (r == null) { 302 return ZERO_RECT; 303 } else if (copyArguments) { 304 return new Rect(r); 305 } else { 306 return r; 307 } 308 } 309 310 /** 311 * Find the position of the bounding rect, and create an array of Rect whose index represents 312 * the position (= BoundsPosition). 313 * 314 * @hide 315 */ extractBoundsFromList(Rect safeInsets, List<Rect> boundingRects)316 public static Rect[] extractBoundsFromList(Rect safeInsets, List<Rect> boundingRects) { 317 Rect[] sortedBounds = new Rect[BOUNDS_POSITION_LENGTH]; 318 for (int i = 0; i < sortedBounds.length; ++i) { 319 sortedBounds[i] = ZERO_RECT; 320 } 321 if (safeInsets != null && boundingRects != null) { 322 for (Rect bound : boundingRects) { 323 // There is at most one non-functional area per short edge of the device, but none 324 // on the long edges, so either safeInsets.right or safeInsets.bottom must be 0. 325 // TODO(b/117199965): Refine the logic to handle edge cases. 326 if (bound.left == 0) { 327 sortedBounds[BOUNDS_POSITION_LEFT] = bound; 328 } else if (bound.top == 0) { 329 sortedBounds[BOUNDS_POSITION_TOP] = bound; 330 } else if (safeInsets.right > 0) { 331 sortedBounds[BOUNDS_POSITION_RIGHT] = bound; 332 } else if (safeInsets.bottom > 0) { 333 sortedBounds[BOUNDS_POSITION_BOTTOM] = bound; 334 } 335 } 336 } 337 return sortedBounds; 338 } 339 340 /** 341 * Returns true if there is no cutout, i.e. the bounds are empty. 342 * 343 * @hide 344 */ isBoundsEmpty()345 public boolean isBoundsEmpty() { 346 return mBounds.isEmpty(); 347 } 348 349 /** 350 * Returns true if the safe insets are empty (and therefore the current view does not 351 * overlap with the cutout or cutout area). 352 * 353 * @hide 354 */ isEmpty()355 public boolean isEmpty() { 356 return mSafeInsets.equals(ZERO_RECT); 357 } 358 359 /** Returns the inset from the top which avoids the display cutout in pixels. */ getSafeInsetTop()360 public int getSafeInsetTop() { 361 return mSafeInsets.top; 362 } 363 364 /** Returns the inset from the bottom which avoids the display cutout in pixels. */ getSafeInsetBottom()365 public int getSafeInsetBottom() { 366 return mSafeInsets.bottom; 367 } 368 369 /** Returns the inset from the left which avoids the display cutout in pixels. */ getSafeInsetLeft()370 public int getSafeInsetLeft() { 371 return mSafeInsets.left; 372 } 373 374 /** Returns the inset from the right which avoids the display cutout in pixels. */ getSafeInsetRight()375 public int getSafeInsetRight() { 376 return mSafeInsets.right; 377 } 378 379 /** 380 * Returns the safe insets in a rect in pixel units. 381 * 382 * @return a rect which is set to the safe insets. 383 * @hide 384 */ getSafeInsets()385 public Rect getSafeInsets() { 386 return new Rect(mSafeInsets); 387 } 388 389 /** 390 * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional 391 * area on the display. 392 * 393 * There will be at most one non-functional area per short edge of the device, and none on 394 * the long edges. 395 * 396 * @return a list of bounding {@code Rect}s, one for each display cutout area. No empty Rect is 397 * returned. 398 */ 399 @NonNull getBoundingRects()400 public List<Rect> getBoundingRects() { 401 List<Rect> result = new ArrayList<>(); 402 for (Rect bound : getBoundingRectsAll()) { 403 if (!bound.isEmpty()) { 404 result.add(new Rect(bound)); 405 } 406 } 407 return result; 408 } 409 410 /** 411 * Returns an array of {@code Rect}s, each of which is the bounding rectangle for a non- 412 * functional area on the display. Ordinal value of BoundPosition is used as an index of 413 * the array. 414 * 415 * There will be at most one non-functional area per short edge of the device, and none on 416 * the long edges. 417 * 418 * @return an array of bounding {@code Rect}s, one for each display cutout area. This might 419 * contain ZERO_RECT, which means there is no cutout area at the position. 420 * 421 * @hide 422 */ getBoundingRectsAll()423 public Rect[] getBoundingRectsAll() { 424 return mBounds.getRects(); 425 } 426 427 /** 428 * Returns a bounding rectangle for a non-functional area on the display which is located on 429 * the left of the screen. 430 * 431 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 432 * is returned. 433 */ getBoundingRectLeft()434 public @NonNull Rect getBoundingRectLeft() { 435 return mBounds.getRect(BOUNDS_POSITION_LEFT); 436 } 437 438 /** 439 * Returns a bounding rectangle for a non-functional area on the display which is located on 440 * the top of the screen. 441 * 442 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 443 * is returned. 444 */ getBoundingRectTop()445 public @NonNull Rect getBoundingRectTop() { 446 return mBounds.getRect(BOUNDS_POSITION_TOP); 447 } 448 449 /** 450 * Returns a bounding rectangle for a non-functional area on the display which is located on 451 * the right of the screen. 452 * 453 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 454 * is returned. 455 */ getBoundingRectRight()456 public @NonNull Rect getBoundingRectRight() { 457 return mBounds.getRect(BOUNDS_POSITION_RIGHT); 458 } 459 460 /** 461 * Returns a bounding rectangle for a non-functional area on the display which is located on 462 * the bottom of the screen. 463 * 464 * @return bounding rectangle in pixels. In case of no bounding rectangle, an empty rectangle 465 * is returned. 466 */ getBoundingRectBottom()467 public @NonNull Rect getBoundingRectBottom() { 468 return mBounds.getRect(BOUNDS_POSITION_BOTTOM); 469 } 470 471 @Override hashCode()472 public int hashCode() { 473 return mSafeInsets.hashCode() * 48271 + mBounds.hashCode(); 474 } 475 476 @Override equals(Object o)477 public boolean equals(Object o) { 478 if (o == this) { 479 return true; 480 } 481 if (o instanceof DisplayCutout) { 482 DisplayCutout c = (DisplayCutout) o; 483 return mSafeInsets.equals(c.mSafeInsets) && mBounds.equals(c.mBounds); 484 } 485 return false; 486 } 487 488 @Override toString()489 public String toString() { 490 return "DisplayCutout{insets=" + mSafeInsets 491 + " boundingRect={" + mBounds + "}" 492 + "}"; 493 } 494 495 /** 496 * @hide 497 */ writeToProto(ProtoOutputStream proto, long fieldId)498 public void writeToProto(ProtoOutputStream proto, long fieldId) { 499 final long token = proto.start(fieldId); 500 mSafeInsets.writeToProto(proto, INSETS); 501 mBounds.getRect(BOUNDS_POSITION_LEFT).writeToProto(proto, BOUND_LEFT); 502 mBounds.getRect(BOUNDS_POSITION_TOP).writeToProto(proto, BOUND_TOP); 503 mBounds.getRect(BOUNDS_POSITION_RIGHT).writeToProto(proto, BOUND_RIGHT); 504 mBounds.getRect(BOUNDS_POSITION_BOTTOM).writeToProto(proto, BOUND_BOTTOM); 505 proto.end(token); 506 } 507 508 /** 509 * Insets the reference frame of the cutout in the given directions. 510 * 511 * @return a copy of this instance which has been inset 512 * @hide 513 */ inset(int insetLeft, int insetTop, int insetRight, int insetBottom)514 public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { 515 if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0 516 || isBoundsEmpty()) { 517 return this; 518 } 519 520 Rect safeInsets = new Rect(mSafeInsets); 521 522 // Note: it's not really well defined what happens when the inset is negative, because we 523 // don't know if the safe inset needs to expand in general. 524 if (insetTop > 0 || safeInsets.top > 0) { 525 safeInsets.top = atLeastZero(safeInsets.top - insetTop); 526 } 527 if (insetBottom > 0 || safeInsets.bottom > 0) { 528 safeInsets.bottom = atLeastZero(safeInsets.bottom - insetBottom); 529 } 530 if (insetLeft > 0 || safeInsets.left > 0) { 531 safeInsets.left = atLeastZero(safeInsets.left - insetLeft); 532 } 533 if (insetRight > 0 || safeInsets.right > 0) { 534 safeInsets.right = atLeastZero(safeInsets.right - insetRight); 535 } 536 537 // If we are not cutting off part of the cutout by insetting it on bottom/right, and we also 538 // don't move it around, we can avoid the allocation and copy of the instance. 539 if (insetLeft == 0 && insetTop == 0 && mSafeInsets.equals(safeInsets)) { 540 return this; 541 } 542 543 Rect[] bounds = mBounds.getRects(); 544 for (int i = 0; i < bounds.length; ++i) { 545 if (!bounds[i].equals(ZERO_RECT)) { 546 bounds[i].offset(-insetLeft, -insetTop); 547 } 548 } 549 550 return new DisplayCutout(safeInsets, bounds, false /* copyArguments */); 551 } 552 553 /** 554 * Returns a copy of this instance with the safe insets replaced with the parameter. 555 * 556 * @param safeInsets the new safe insets in pixels 557 * @return a copy of this instance with the safe insets replaced with the argument. 558 * 559 * @hide 560 */ replaceSafeInsets(Rect safeInsets)561 public DisplayCutout replaceSafeInsets(Rect safeInsets) { 562 return new DisplayCutout(new Rect(safeInsets), mBounds); 563 } 564 atLeastZero(int value)565 private static int atLeastZero(int value) { 566 return value < 0 ? 0 : value; 567 } 568 569 570 /** 571 * Creates an instance from a bounding rect. 572 * 573 * @hide 574 */ 575 @VisibleForTesting fromBoundingRect( int left, int top, int right, int bottom, @BoundsPosition int pos)576 public static DisplayCutout fromBoundingRect( 577 int left, int top, int right, int bottom, @BoundsPosition int pos) { 578 Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH]; 579 for (int i = 0; i < BOUNDS_POSITION_LENGTH; ++i) { 580 bounds[i] = (pos == i) ? new Rect(left, top, right, bottom) : new Rect(); 581 } 582 return new DisplayCutout(ZERO_RECT, bounds, false /* copyArguments */); 583 } 584 585 /** 586 * Creates an instance from a bounding {@link Path}. 587 * 588 * @hide 589 */ fromBounds(Rect[] bounds)590 public static DisplayCutout fromBounds(Rect[] bounds) { 591 return new DisplayCutout(ZERO_RECT, bounds, false /* copyArguments */); 592 } 593 594 /** 595 * Creates the display cutout according to 596 * @android:string/config_mainBuiltInDisplayCutoutRectApproximation, which is the closest 597 * rectangle-base approximation of the cutout. 598 * 599 * @hide 600 */ fromResourcesRectApproximation(Resources res, int displayWidth, int displayHeight)601 public static DisplayCutout fromResourcesRectApproximation(Resources res, int displayWidth, int displayHeight) { 602 return fromSpec(res.getString(R.string.config_mainBuiltInDisplayCutoutRectApproximation), 603 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT); 604 } 605 606 /** 607 * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout. 608 * 609 * @hide 610 */ pathFromResources(Resources res, int displayWidth, int displayHeight)611 public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) { 612 return pathAndDisplayCutoutFromSpec( 613 res.getString(R.string.config_mainBuiltInDisplayCutout), 614 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT).first; 615 } 616 617 /** 618 * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec. 619 * 620 * @hide 621 */ 622 @VisibleForTesting(visibility = PRIVATE) fromSpec(String spec, int displayWidth, int displayHeight, float density)623 public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight, 624 float density) { 625 return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second; 626 } 627 pathAndDisplayCutoutFromSpec(String spec, int displayWidth, int displayHeight, float density)628 private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec, 629 int displayWidth, int displayHeight, float density) { 630 if (TextUtils.isEmpty(spec)) { 631 return NULL_PAIR; 632 } 633 synchronized (CACHE_LOCK) { 634 if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth 635 && sCachedDisplayHeight == displayHeight 636 && sCachedDensity == density) { 637 return sCachedCutout; 638 } 639 } 640 spec = spec.trim(); 641 final float offsetX; 642 if (spec.endsWith(RIGHT_MARKER)) { 643 offsetX = displayWidth; 644 spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim(); 645 } else { 646 offsetX = displayWidth / 2f; 647 } 648 final boolean inDp = spec.endsWith(DP_MARKER); 649 if (inDp) { 650 spec = spec.substring(0, spec.length() - DP_MARKER.length()); 651 } 652 653 String bottomSpec = null; 654 if (spec.contains(BOTTOM_MARKER)) { 655 String[] splits = spec.split(BOTTOM_MARKER, 2); 656 spec = splits[0].trim(); 657 bottomSpec = splits[1].trim(); 658 } 659 660 final Path p; 661 final Region r = Region.obtain(); 662 try { 663 p = PathParser.createPathFromPathData(spec); 664 } catch (Throwable e) { 665 Log.wtf(TAG, "Could not inflate cutout: ", e); 666 return NULL_PAIR; 667 } 668 669 final Matrix m = new Matrix(); 670 if (inDp) { 671 m.postScale(density, density); 672 } 673 m.postTranslate(offsetX, 0); 674 p.transform(m); 675 676 Rect boundTop = new Rect(); 677 toRectAndAddToRegion(p, r, boundTop); 678 final int topInset = boundTop.bottom; 679 680 Rect boundBottom = null; 681 final int bottomInset; 682 if (bottomSpec != null) { 683 final Path bottomPath; 684 try { 685 bottomPath = PathParser.createPathFromPathData(bottomSpec); 686 } catch (Throwable e) { 687 Log.wtf(TAG, "Could not inflate bottom cutout: ", e); 688 return NULL_PAIR; 689 } 690 // Keep top transform 691 m.postTranslate(0, displayHeight); 692 bottomPath.transform(m); 693 p.addPath(bottomPath); 694 boundBottom = new Rect(); 695 toRectAndAddToRegion(bottomPath, r, boundBottom); 696 bottomInset = displayHeight - boundBottom.top; 697 } else { 698 bottomInset = 0; 699 } 700 701 Rect safeInset = new Rect(0, topInset, 0, bottomInset); 702 final DisplayCutout cutout = new DisplayCutout( 703 safeInset, null /* boundLeft */, boundTop, null /* boundRight */, boundBottom, 704 false /* copyArguments */); 705 706 final Pair<Path, DisplayCutout> result = new Pair<>(p, cutout); 707 synchronized (CACHE_LOCK) { 708 sCachedSpec = spec; 709 sCachedDisplayWidth = displayWidth; 710 sCachedDisplayHeight = displayHeight; 711 sCachedDensity = density; 712 sCachedCutout = result; 713 } 714 return result; 715 } 716 toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect)717 private static void toRectAndAddToRegion(Path p, Region inoutRegion, Rect inoutRect) { 718 final RectF rectF = new RectF(); 719 p.computeBounds(rectF, false /* unused */); 720 rectF.round(inoutRect); 721 inoutRegion.op(inoutRect, Op.UNION); 722 } 723 724 /** 725 * Helper class for passing {@link DisplayCutout} through binder. 726 * 727 * Needed, because {@code readFromParcel} cannot be used with immutable classes. 728 * 729 * @hide 730 */ 731 public static final class ParcelableWrapper implements Parcelable { 732 733 private DisplayCutout mInner; 734 ParcelableWrapper()735 public ParcelableWrapper() { 736 this(NO_CUTOUT); 737 } 738 ParcelableWrapper(DisplayCutout cutout)739 public ParcelableWrapper(DisplayCutout cutout) { 740 mInner = cutout; 741 } 742 743 @Override describeContents()744 public int describeContents() { 745 return 0; 746 } 747 748 @Override writeToParcel(Parcel out, int flags)749 public void writeToParcel(Parcel out, int flags) { 750 writeCutoutToParcel(mInner, out, flags); 751 } 752 753 /** 754 * Writes a DisplayCutout to a {@link Parcel}. 755 * 756 * @see #readCutoutFromParcel(Parcel) 757 */ writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags)758 public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) { 759 if (cutout == null) { 760 out.writeInt(-1); 761 } else if (cutout == NO_CUTOUT) { 762 out.writeInt(0); 763 } else { 764 out.writeInt(1); 765 out.writeTypedObject(cutout.mSafeInsets, flags); 766 out.writeTypedArray(cutout.mBounds.getRects(), flags); 767 } 768 } 769 770 /** 771 * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing 772 * instance. 773 * 774 * Needed for AIDL out parameters. 775 */ readFromParcel(Parcel in)776 public void readFromParcel(Parcel in) { 777 mInner = readCutoutFromParcel(in); 778 } 779 780 public static final @android.annotation.NonNull Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() { 781 @Override 782 public ParcelableWrapper createFromParcel(Parcel in) { 783 return new ParcelableWrapper(readCutoutFromParcel(in)); 784 } 785 786 @Override 787 public ParcelableWrapper[] newArray(int size) { 788 return new ParcelableWrapper[size]; 789 } 790 }; 791 792 /** 793 * Reads a DisplayCutout from a {@link Parcel}. 794 * 795 * @see #writeCutoutToParcel(DisplayCutout, Parcel, int) 796 */ readCutoutFromParcel(Parcel in)797 public static DisplayCutout readCutoutFromParcel(Parcel in) { 798 int variant = in.readInt(); 799 if (variant == -1) { 800 return null; 801 } 802 if (variant == 0) { 803 return NO_CUTOUT; 804 } 805 806 Rect safeInsets = in.readTypedObject(Rect.CREATOR); 807 Rect[] bounds = new Rect[BOUNDS_POSITION_LENGTH]; 808 in.readTypedArray(bounds, Rect.CREATOR); 809 810 return new DisplayCutout(safeInsets, bounds, false /* copyArguments */); 811 } 812 get()813 public DisplayCutout get() { 814 return mInner; 815 } 816 set(ParcelableWrapper cutout)817 public void set(ParcelableWrapper cutout) { 818 mInner = cutout.get(); 819 } 820 set(DisplayCutout cutout)821 public void set(DisplayCutout cutout) { 822 mInner = cutout; 823 } 824 825 @Override hashCode()826 public int hashCode() { 827 return mInner.hashCode(); 828 } 829 830 @Override equals(Object o)831 public boolean equals(Object o) { 832 return o instanceof ParcelableWrapper 833 && mInner.equals(((ParcelableWrapper) o).mInner); 834 } 835 836 @Override toString()837 public String toString() { 838 return String.valueOf(mInner); 839 } 840 } 841 } 842