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