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