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.BOUNDS; 22 import static android.view.DisplayCutoutProto.INSETS; 23 24 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 25 26 import android.content.res.Resources; 27 import android.graphics.Matrix; 28 import android.graphics.Path; 29 import android.graphics.Rect; 30 import android.graphics.RectF; 31 import android.graphics.Region; 32 import android.os.Parcel; 33 import android.os.Parcelable; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.util.Pair; 37 import android.util.PathParser; 38 import android.util.proto.ProtoOutputStream; 39 40 import com.android.internal.R; 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.annotations.VisibleForTesting; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 47 /** 48 * Represents the area of the display that is not functional for displaying content. 49 * 50 * <p>{@code DisplayCutout} is immutable. 51 */ 52 public final class DisplayCutout { 53 54 private static final String TAG = "DisplayCutout"; 55 private static final String BOTTOM_MARKER = "@bottom"; 56 private static final String DP_MARKER = "@dp"; 57 private static final String RIGHT_MARKER = "@right"; 58 59 /** 60 * Category for overlays that allow emulating a display cutout on devices that don't have 61 * one. 62 * 63 * @see android.content.om.IOverlayManager 64 * @hide 65 */ 66 public static final String EMULATION_OVERLAY_CATEGORY = 67 "com.android.internal.display_cutout_emulation"; 68 69 private static final Rect ZERO_RECT = new Rect(); 70 private static final Region EMPTY_REGION = new Region(); 71 72 /** 73 * An instance where {@link #isEmpty()} returns {@code true}. 74 * 75 * @hide 76 */ 77 public static final DisplayCutout NO_CUTOUT = new DisplayCutout(ZERO_RECT, EMPTY_REGION, 78 false /* copyArguments */); 79 80 81 private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null); 82 private static final Object CACHE_LOCK = new Object(); 83 84 @GuardedBy("CACHE_LOCK") 85 private static String sCachedSpec; 86 @GuardedBy("CACHE_LOCK") 87 private static int sCachedDisplayWidth; 88 @GuardedBy("CACHE_LOCK") 89 private static int sCachedDisplayHeight; 90 @GuardedBy("CACHE_LOCK") 91 private static float sCachedDensity; 92 @GuardedBy("CACHE_LOCK") 93 private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR; 94 95 private final Rect mSafeInsets; 96 private final Region mBounds; 97 98 /** 99 * Creates a DisplayCutout instance. 100 * 101 * @param safeInsets the insets from each edge which avoid the display cutout as returned by 102 * {@link #getSafeInsetTop()} etc. 103 * @param boundingRects the bounding rects of the display cutouts as returned by 104 * {@link #getBoundingRects()} ()}. 105 */ 106 // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE) DisplayCutout(Rect safeInsets, List<Rect> boundingRects)107 public DisplayCutout(Rect safeInsets, List<Rect> boundingRects) { 108 this(safeInsets != null ? new Rect(safeInsets) : ZERO_RECT, 109 boundingRectsToRegion(boundingRects), 110 true /* copyArguments */); 111 } 112 113 /** 114 * Creates a DisplayCutout instance. 115 * 116 * @param copyArguments if true, create a copy of the arguments. If false, the passed arguments 117 * are not copied and MUST remain unchanged forever. 118 */ DisplayCutout(Rect safeInsets, Region bounds, boolean copyArguments)119 private DisplayCutout(Rect safeInsets, Region bounds, boolean copyArguments) { 120 mSafeInsets = safeInsets == null ? ZERO_RECT : 121 (copyArguments ? new Rect(safeInsets) : safeInsets); 122 mBounds = bounds == null ? Region.obtain() : 123 (copyArguments ? Region.obtain(bounds) : bounds); 124 } 125 126 /** 127 * Returns true if the safe insets are empty (and therefore the current view does not 128 * overlap with the cutout or cutout area). 129 * 130 * @hide 131 */ isEmpty()132 public boolean isEmpty() { 133 return mSafeInsets.equals(ZERO_RECT); 134 } 135 136 /** 137 * Returns true if there is no cutout, i.e. the bounds are empty. 138 * 139 * @hide 140 */ isBoundsEmpty()141 public boolean isBoundsEmpty() { 142 return mBounds.isEmpty(); 143 } 144 145 /** Returns the inset from the top which avoids the display cutout in pixels. */ getSafeInsetTop()146 public int getSafeInsetTop() { 147 return mSafeInsets.top; 148 } 149 150 /** Returns the inset from the bottom which avoids the display cutout in pixels. */ getSafeInsetBottom()151 public int getSafeInsetBottom() { 152 return mSafeInsets.bottom; 153 } 154 155 /** Returns the inset from the left which avoids the display cutout in pixels. */ getSafeInsetLeft()156 public int getSafeInsetLeft() { 157 return mSafeInsets.left; 158 } 159 160 /** Returns the inset from the right which avoids the display cutout in pixels. */ getSafeInsetRight()161 public int getSafeInsetRight() { 162 return mSafeInsets.right; 163 } 164 165 /** 166 * Returns the safe insets in a rect in pixel units. 167 * 168 * @return a rect which is set to the safe insets. 169 * @hide 170 */ getSafeInsets()171 public Rect getSafeInsets() { 172 return new Rect(mSafeInsets); 173 } 174 175 /** 176 * Returns the bounding region of the cutout. 177 * 178 * <p> 179 * <strong>Note:</strong> There may be more than one cutout, in which case the returned 180 * {@code Region} will be non-contiguous and its bounding rect will be meaningless without 181 * intersecting it first. 182 * 183 * Example: 184 * <pre> 185 * // Getting the bounding rectangle of the top display cutout 186 * Region bounds = displayCutout.getBounds(); 187 * bounds.op(0, 0, Integer.MAX_VALUE, displayCutout.getSafeInsetTop(), Region.Op.INTERSECT); 188 * Rect topDisplayCutout = bounds.getBoundingRect(); 189 * </pre> 190 * 191 * @return the bounding region of the cutout. Coordinates are relative 192 * to the top-left corner of the content view and in pixel units. 193 * @hide 194 */ getBounds()195 public Region getBounds() { 196 return Region.obtain(mBounds); 197 } 198 199 /** 200 * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional 201 * area on the display. 202 * 203 * There will be at most one non-functional area per short edge of the device, and none on 204 * the long edges. 205 * 206 * @return a list of bounding {@code Rect}s, one for each display cutout area. 207 */ getBoundingRects()208 public List<Rect> getBoundingRects() { 209 List<Rect> result = new ArrayList<>(); 210 Region bounds = Region.obtain(); 211 // top 212 bounds.set(mBounds); 213 bounds.op(0, 0, Integer.MAX_VALUE, getSafeInsetTop(), Region.Op.INTERSECT); 214 if (!bounds.isEmpty()) { 215 result.add(bounds.getBounds()); 216 } 217 // left 218 bounds.set(mBounds); 219 bounds.op(0, 0, getSafeInsetLeft(), Integer.MAX_VALUE, Region.Op.INTERSECT); 220 if (!bounds.isEmpty()) { 221 result.add(bounds.getBounds()); 222 } 223 // right & bottom 224 bounds.set(mBounds); 225 bounds.op(getSafeInsetLeft() + 1, getSafeInsetTop() + 1, 226 Integer.MAX_VALUE, Integer.MAX_VALUE, Region.Op.INTERSECT); 227 if (!bounds.isEmpty()) { 228 result.add(bounds.getBounds()); 229 } 230 bounds.recycle(); 231 return result; 232 } 233 234 @Override hashCode()235 public int hashCode() { 236 int result = mSafeInsets.hashCode(); 237 result = result * 31 + mBounds.getBounds().hashCode(); 238 return result; 239 } 240 241 @Override equals(Object o)242 public boolean equals(Object o) { 243 if (o == this) { 244 return true; 245 } 246 if (o instanceof DisplayCutout) { 247 DisplayCutout c = (DisplayCutout) o; 248 return mSafeInsets.equals(c.mSafeInsets) 249 && mBounds.equals(c.mBounds); 250 } 251 return false; 252 } 253 254 @Override toString()255 public String toString() { 256 return "DisplayCutout{insets=" + mSafeInsets 257 + " boundingRect=" + mBounds.getBounds() 258 + "}"; 259 } 260 261 /** 262 * @hide 263 */ writeToProto(ProtoOutputStream proto, long fieldId)264 public void writeToProto(ProtoOutputStream proto, long fieldId) { 265 final long token = proto.start(fieldId); 266 mSafeInsets.writeToProto(proto, INSETS); 267 mBounds.getBounds().writeToProto(proto, BOUNDS); 268 proto.end(token); 269 } 270 271 /** 272 * Insets the reference frame of the cutout in the given directions. 273 * 274 * @return a copy of this instance which has been inset 275 * @hide 276 */ inset(int insetLeft, int insetTop, int insetRight, int insetBottom)277 public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { 278 if (mBounds.isEmpty() 279 || insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) { 280 return this; 281 } 282 283 Rect safeInsets = new Rect(mSafeInsets); 284 Region bounds = Region.obtain(mBounds); 285 286 // Note: it's not really well defined what happens when the inset is negative, because we 287 // don't know if the safe inset needs to expand in general. 288 if (insetTop > 0 || safeInsets.top > 0) { 289 safeInsets.top = atLeastZero(safeInsets.top - insetTop); 290 } 291 if (insetBottom > 0 || safeInsets.bottom > 0) { 292 safeInsets.bottom = atLeastZero(safeInsets.bottom - insetBottom); 293 } 294 if (insetLeft > 0 || safeInsets.left > 0) { 295 safeInsets.left = atLeastZero(safeInsets.left - insetLeft); 296 } 297 if (insetRight > 0 || safeInsets.right > 0) { 298 safeInsets.right = atLeastZero(safeInsets.right - insetRight); 299 } 300 301 bounds.translate(-insetLeft, -insetTop); 302 return new DisplayCutout(safeInsets, bounds, false /* copyArguments */); 303 } 304 305 /** 306 * Returns a copy of this instance with the safe insets replaced with the parameter. 307 * 308 * @param safeInsets the new safe insets in pixels 309 * @return a copy of this instance with the safe insets replaced with the argument. 310 * 311 * @hide 312 */ replaceSafeInsets(Rect safeInsets)313 public DisplayCutout replaceSafeInsets(Rect safeInsets) { 314 return new DisplayCutout(new Rect(safeInsets), mBounds, false /* copyArguments */); 315 } 316 atLeastZero(int value)317 private static int atLeastZero(int value) { 318 return value < 0 ? 0 : value; 319 } 320 321 322 /** 323 * Creates an instance from a bounding rect. 324 * 325 * @hide 326 */ fromBoundingRect(int left, int top, int right, int bottom)327 public static DisplayCutout fromBoundingRect(int left, int top, int right, int bottom) { 328 Path path = new Path(); 329 path.reset(); 330 path.moveTo(left, top); 331 path.lineTo(left, bottom); 332 path.lineTo(right, bottom); 333 path.lineTo(right, top); 334 path.close(); 335 return fromBounds(path); 336 } 337 338 /** 339 * Creates an instance from a bounding {@link Path}. 340 * 341 * @hide 342 */ fromBounds(Path path)343 public static DisplayCutout fromBounds(Path path) { 344 RectF clipRect = new RectF(); 345 path.computeBounds(clipRect, false /* unused */); 346 Region clipRegion = Region.obtain(); 347 clipRegion.set((int) clipRect.left, (int) clipRect.top, 348 (int) clipRect.right, (int) clipRect.bottom); 349 350 Region bounds = new Region(); 351 bounds.setPath(path, clipRegion); 352 clipRegion.recycle(); 353 return new DisplayCutout(ZERO_RECT, bounds, false /* copyArguments */); 354 } 355 356 /** 357 * Creates the bounding path according to @android:string/config_mainBuiltInDisplayCutout. 358 * 359 * @hide 360 */ fromResources(Resources res, int displayWidth, int displayHeight)361 public static DisplayCutout fromResources(Resources res, int displayWidth, int displayHeight) { 362 return fromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout), 363 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT); 364 } 365 366 /** 367 * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout. 368 * 369 * @hide 370 */ pathFromResources(Resources res, int displayWidth, int displayHeight)371 public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) { 372 return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout), 373 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT).first; 374 } 375 376 /** 377 * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec. 378 * 379 * @hide 380 */ 381 @VisibleForTesting(visibility = PRIVATE) fromSpec(String spec, int displayWidth, int displayHeight, float density)382 public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight, 383 float density) { 384 return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second; 385 } 386 pathAndDisplayCutoutFromSpec(String spec, int displayWidth, int displayHeight, float density)387 private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec, 388 int displayWidth, int displayHeight, float density) { 389 if (TextUtils.isEmpty(spec)) { 390 return NULL_PAIR; 391 } 392 synchronized (CACHE_LOCK) { 393 if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth 394 && sCachedDisplayHeight == displayHeight 395 && sCachedDensity == density) { 396 return sCachedCutout; 397 } 398 } 399 spec = spec.trim(); 400 final float offsetX; 401 if (spec.endsWith(RIGHT_MARKER)) { 402 offsetX = displayWidth; 403 spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim(); 404 } else { 405 offsetX = displayWidth / 2f; 406 } 407 final boolean inDp = spec.endsWith(DP_MARKER); 408 if (inDp) { 409 spec = spec.substring(0, spec.length() - DP_MARKER.length()); 410 } 411 412 String bottomSpec = null; 413 if (spec.contains(BOTTOM_MARKER)) { 414 String[] splits = spec.split(BOTTOM_MARKER, 2); 415 spec = splits[0].trim(); 416 bottomSpec = splits[1].trim(); 417 } 418 419 final Path p; 420 try { 421 p = PathParser.createPathFromPathData(spec); 422 } catch (Throwable e) { 423 Log.wtf(TAG, "Could not inflate cutout: ", e); 424 return NULL_PAIR; 425 } 426 427 final Matrix m = new Matrix(); 428 if (inDp) { 429 m.postScale(density, density); 430 } 431 m.postTranslate(offsetX, 0); 432 p.transform(m); 433 434 if (bottomSpec != null) { 435 final Path bottomPath; 436 try { 437 bottomPath = PathParser.createPathFromPathData(bottomSpec); 438 } catch (Throwable e) { 439 Log.wtf(TAG, "Could not inflate bottom cutout: ", e); 440 return NULL_PAIR; 441 } 442 // Keep top transform 443 m.postTranslate(0, displayHeight); 444 bottomPath.transform(m); 445 p.addPath(bottomPath); 446 } 447 448 final Pair<Path, DisplayCutout> result = new Pair<>(p, fromBounds(p)); 449 synchronized (CACHE_LOCK) { 450 sCachedSpec = spec; 451 sCachedDisplayWidth = displayWidth; 452 sCachedDisplayHeight = displayHeight; 453 sCachedDensity = density; 454 sCachedCutout = result; 455 } 456 return result; 457 } 458 boundingRectsToRegion(List<Rect> rects)459 private static Region boundingRectsToRegion(List<Rect> rects) { 460 Region result = Region.obtain(); 461 if (rects != null) { 462 for (Rect r : rects) { 463 result.op(r, Region.Op.UNION); 464 } 465 } 466 return result; 467 } 468 469 /** 470 * Helper class for passing {@link DisplayCutout} through binder. 471 * 472 * Needed, because {@code readFromParcel} cannot be used with immutable classes. 473 * 474 * @hide 475 */ 476 public static final class ParcelableWrapper implements Parcelable { 477 478 private DisplayCutout mInner; 479 ParcelableWrapper()480 public ParcelableWrapper() { 481 this(NO_CUTOUT); 482 } 483 ParcelableWrapper(DisplayCutout cutout)484 public ParcelableWrapper(DisplayCutout cutout) { 485 mInner = cutout; 486 } 487 488 @Override describeContents()489 public int describeContents() { 490 return 0; 491 } 492 493 @Override writeToParcel(Parcel out, int flags)494 public void writeToParcel(Parcel out, int flags) { 495 writeCutoutToParcel(mInner, out, flags); 496 } 497 498 /** 499 * Writes a DisplayCutout to a {@link Parcel}. 500 * 501 * @see #readCutoutFromParcel(Parcel) 502 */ writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags)503 public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) { 504 if (cutout == null) { 505 out.writeInt(-1); 506 } else if (cutout == NO_CUTOUT) { 507 out.writeInt(0); 508 } else { 509 out.writeInt(1); 510 out.writeTypedObject(cutout.mSafeInsets, flags); 511 out.writeTypedObject(cutout.mBounds, flags); 512 } 513 } 514 515 /** 516 * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing 517 * instance. 518 * 519 * Needed for AIDL out parameters. 520 */ readFromParcel(Parcel in)521 public void readFromParcel(Parcel in) { 522 mInner = readCutoutFromParcel(in); 523 } 524 525 public static final Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() { 526 @Override 527 public ParcelableWrapper createFromParcel(Parcel in) { 528 return new ParcelableWrapper(readCutoutFromParcel(in)); 529 } 530 531 @Override 532 public ParcelableWrapper[] newArray(int size) { 533 return new ParcelableWrapper[size]; 534 } 535 }; 536 537 /** 538 * Reads a DisplayCutout from a {@link Parcel}. 539 * 540 * @see #writeCutoutToParcel(DisplayCutout, Parcel, int) 541 */ readCutoutFromParcel(Parcel in)542 public static DisplayCutout readCutoutFromParcel(Parcel in) { 543 int variant = in.readInt(); 544 if (variant == -1) { 545 return null; 546 } 547 if (variant == 0) { 548 return NO_CUTOUT; 549 } 550 551 Rect safeInsets = in.readTypedObject(Rect.CREATOR); 552 Region bounds = in.readTypedObject(Region.CREATOR); 553 554 return new DisplayCutout(safeInsets, bounds, false /* copyArguments */); 555 } 556 get()557 public DisplayCutout get() { 558 return mInner; 559 } 560 set(ParcelableWrapper cutout)561 public void set(ParcelableWrapper cutout) { 562 mInner = cutout.get(); 563 } 564 set(DisplayCutout cutout)565 public void set(DisplayCutout cutout) { 566 mInner = cutout; 567 } 568 569 @Override hashCode()570 public int hashCode() { 571 return mInner.hashCode(); 572 } 573 574 @Override equals(Object o)575 public boolean equals(Object o) { 576 return o instanceof ParcelableWrapper 577 && mInner.equals(((ParcelableWrapper) o).mInner); 578 } 579 580 @Override toString()581 public String toString() { 582 return String.valueOf(mInner); 583 } 584 } 585 } 586