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