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