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