1 /*
2  * Copyright 2018 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 androidx.core.view;
18 
19 import static android.os.Build.VERSION.SDK_INT;
20 
21 import android.graphics.Path;
22 import android.graphics.Rect;
23 import android.os.Build;
24 import android.view.DisplayCutout;
25 
26 import androidx.annotation.RequiresApi;
27 import androidx.core.graphics.Insets;
28 import androidx.core.util.ObjectsCompat;
29 
30 import org.jspecify.annotations.NonNull;
31 import org.jspecify.annotations.Nullable;
32 
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.List;
36 
37 /**
38  * Represents the area of the display that is not functional for displaying content.
39  *
40  * <p>{@code DisplayCutoutCompat} instances are immutable.
41  */
42 public final class DisplayCutoutCompat {
43 
44     private final DisplayCutout mDisplayCutout;
45 
46     /**
47      * Creates a DisplayCutout instance.
48      *
49      * @param safeInsets the insets from each edge which avoid the display cutout as returned by
50      *                   {@link #getSafeInsetTop()} etc.
51      * @param boundingRects the bounding rects of the display cutouts as returned by
52      *               {@link #getBoundingRects()} ()}.
53      */
DisplayCutoutCompat(@ullable Rect safeInsets, @Nullable List<Rect> boundingRects)54     public DisplayCutoutCompat(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) {
55         this(SDK_INT >= 28 ? Api28Impl.createDisplayCutout(safeInsets, boundingRects) : null);
56     }
57 
58     /**
59      * Creates a DisplayCutout instance.
60      *
61      * @param safeInsets the insets from each edge which avoid the display cutout as returned by
62      *                   {@link #getSafeInsetTop()} etc.
63      * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed,
64      *                  it's treated as an empty rectangle (0,0)-(0,0).
65      * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed,
66      *                  it's treated as an empty rectangle (0,0)-(0,0).
67      * @param boundRight the right bounding rect of the display cutout in pixels. If null is
68      *                  passed, it's treated as an empty rectangle (0,0)-(0,0).
69      * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is
70      *                   passed, it's treated as an empty rectangle (0,0)-(0,0).
71      * @param waterfallInsets the insets for the curved areas in waterfall display.
72      */
DisplayCutoutCompat(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets)73     public DisplayCutoutCompat(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
74             @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
75             @NonNull Insets waterfallInsets) {
76         this(constructDisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom,
77                 waterfallInsets, null));
78     }
79 
80     /**
81      * Creates a DisplayCutout instance.
82      *
83      * @param safeInsets the insets from each edge which avoid the display cutout as returned by
84      *                   {@link #getSafeInsetTop()} etc.
85      * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed,
86      *                  it's treated as an empty rectangle (0,0)-(0,0).
87      * @param boundTop the top bounding rect of the display cutout in pixels. If null is passed,
88      *                  it's treated as an empty rectangle (0,0)-(0,0).
89      * @param boundRight the right bounding rect of the display cutout in pixels. If null is
90      *                  passed, it's treated as an empty rectangle (0,0)-(0,0).
91      * @param boundBottom the bottom bounding rect of the display cutout in pixels. If null is
92      *                   passed, it's treated as an empty rectangle (0,0)-(0,0).
93      * @param waterfallInsets the insets for the curved areas in waterfall display.
94      * @param cutoutPath the path of the display cutout. Specifying a path with this
95      *                   constructor is only supported on API 33 and above, even though a real
96      *                   DisplayCutout can have a cutout path on API 31 and above. On API 32 and
97      *                   below, this path is ignored.
98      */
DisplayCutoutCompat(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets, @Nullable Path cutoutPath)99     public DisplayCutoutCompat(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
100             @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
101             @NonNull Insets waterfallInsets, @Nullable Path cutoutPath) {
102         this(constructDisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom,
103                 waterfallInsets, cutoutPath));
104     }
105 
constructDisplayCutout(@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, @NonNull Insets waterfallInsets, @Nullable Path cutoutPath)106     private static DisplayCutout constructDisplayCutout(@NonNull Insets safeInsets,
107             @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight,
108             @Nullable Rect boundBottom, @NonNull Insets waterfallInsets,
109             @Nullable Path cutoutPath) {
110         if (SDK_INT >= 33) {
111             return Api33Impl.createDisplayCutout(safeInsets.toPlatformInsets(), boundLeft, boundTop,
112                     boundRight, boundBottom, waterfallInsets.toPlatformInsets(), cutoutPath);
113         } else if (SDK_INT >= 30) {
114             return Api30Impl.createDisplayCutout(safeInsets.toPlatformInsets(), boundLeft, boundTop,
115                     boundRight, boundBottom, waterfallInsets.toPlatformInsets());
116         } else if (SDK_INT >= Build.VERSION_CODES.Q) {
117             return Api29Impl.createDisplayCutout(safeInsets.toPlatformInsets(), boundLeft, boundTop,
118                     boundRight, boundBottom);
119         } else if (SDK_INT >= Build.VERSION_CODES.P) {
120             final Rect safeInsetRect = new Rect(safeInsets.left, safeInsets.top, safeInsets.right,
121                     safeInsets.bottom);
122             final ArrayList<Rect> boundingRects = new ArrayList<>();
123             if (boundLeft != null) {
124                 boundingRects.add(boundLeft);
125             }
126             if (boundTop != null) {
127                 boundingRects.add(boundTop);
128             }
129             if (boundRight != null) {
130                 boundingRects.add(boundRight);
131             }
132             if (boundBottom != null) {
133                 boundingRects.add(boundBottom);
134             }
135             return Api28Impl.createDisplayCutout(safeInsetRect, boundingRects);
136         } else {
137             return null;
138         }
139     }
140 
DisplayCutoutCompat(DisplayCutout displayCutout)141     private DisplayCutoutCompat(DisplayCutout displayCutout) {
142         mDisplayCutout = displayCutout;
143     }
144 
145     /** Returns the inset from the top which avoids the display cutout in pixels. */
getSafeInsetTop()146     public int getSafeInsetTop() {
147         if (SDK_INT >= 28) {
148             return Api28Impl.getSafeInsetTop(mDisplayCutout);
149         } else {
150             return 0;
151         }
152     }
153 
154     /** Returns the inset from the bottom which avoids the display cutout in pixels. */
getSafeInsetBottom()155     public int getSafeInsetBottom() {
156         if (SDK_INT >= 28) {
157             return Api28Impl.getSafeInsetBottom(mDisplayCutout);
158         } else {
159             return 0;
160         }
161     }
162 
163     /** Returns the inset from the left which avoids the display cutout in pixels. */
getSafeInsetLeft()164     public int getSafeInsetLeft() {
165         if (SDK_INT >= 28) {
166             return Api28Impl.getSafeInsetLeft(mDisplayCutout);
167         } else {
168             return 0;
169         }
170     }
171 
172     /** Returns the inset from the right which avoids the display cutout in pixels. */
getSafeInsetRight()173     public int getSafeInsetRight() {
174         if (SDK_INT >= 28) {
175             return Api28Impl.getSafeInsetRight(mDisplayCutout);
176         } else {
177             return 0;
178         }
179     }
180 
181     /**
182      * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional
183      * area on the display.
184      *
185      * There will be at most one non-functional area per short edge of the device, and none on
186      * the long edges.
187      *
188      * @return a list of bounding {@code Rect}s, one for each display cutout area.
189      */
getBoundingRects()190     public @NonNull List<Rect> getBoundingRects() {
191         if (SDK_INT >= 28) {
192             return Api28Impl.getBoundingRects(mDisplayCutout);
193         } else {
194             return Collections.emptyList();
195         }
196     }
197 
198     /**
199      * Returns the insets representing the curved areas of a waterfall display.
200      *
201      * A waterfall display has curved areas along the edges of the screen. Apps should be careful
202      * when showing UI and handling touch input in those insets because the curve may impair
203      * legibility and can frequently lead to unintended touch inputs.
204      *
205      * @return the insets for the curved areas of a waterfall display in pixels or {@code
206      * Insets.NONE} if there are no curved areas or they don't overlap with the window.
207      */
getWaterfallInsets()208     public @NonNull Insets getWaterfallInsets() {
209         if (SDK_INT >= 30) {
210             return Insets.toCompatInsets(Api30Impl.getWaterfallInsets(mDisplayCutout));
211         } else {
212             return Insets.NONE;
213         }
214     }
215 
216     /**
217      * Returns a Path that contains the cutout paths of all sides on the display.
218      * To get a cutout path for one specific side, apps can intersect the Path with the Rect
219      * obtained from getBoundingRectLeft(), getBoundingRectTop(), getBoundingRectRight() or
220      * getBoundingRectBottom().
221      *
222      * @return the path corresponding to the cutout, or null if there is no cutout on the display.
223      */
getCutoutPath()224     public @Nullable Path getCutoutPath() {
225         if (SDK_INT >= 31) {
226             return Api31Impl.getCutoutPath(mDisplayCutout);
227         } else {
228             return null;
229         }
230     }
231 
232     @Override
equals(Object o)233     public boolean equals(Object o) {
234         if (this == o) {
235             return true;
236         }
237         if (o == null || getClass() != o.getClass()) {
238             return false;
239         }
240         DisplayCutoutCompat other = (DisplayCutoutCompat) o;
241         return ObjectsCompat.equals(mDisplayCutout, other.mDisplayCutout);
242     }
243 
244     @Override
hashCode()245     public int hashCode() {
246         return mDisplayCutout == null ? 0 : mDisplayCutout.hashCode();
247     }
248 
249     @Override
toString()250     public @NonNull String toString() {
251         return "DisplayCutoutCompat{" + mDisplayCutout + "}";
252     }
253 
wrap(DisplayCutout displayCutout)254     static DisplayCutoutCompat wrap(DisplayCutout displayCutout) {
255         return displayCutout == null ? null : new DisplayCutoutCompat(displayCutout);
256     }
257 
258     @RequiresApi(28)
unwrap()259     DisplayCutout unwrap() {
260         return mDisplayCutout;
261     }
262 
263     @RequiresApi(28)
264     static class Api28Impl {
Api28Impl()265         private Api28Impl() {
266             // This class is not instantiable.
267         }
268 
createDisplayCutout( @ullable Rect safeInsets, @Nullable List<Rect> boundingRects)269         static DisplayCutout createDisplayCutout(
270                 @Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) {
271             return new DisplayCutout(safeInsets, boundingRects);
272         }
273 
getSafeInsetTop(DisplayCutout displayCutout)274         static int getSafeInsetTop(DisplayCutout displayCutout) {
275             return displayCutout.getSafeInsetTop();
276         }
277 
getSafeInsetBottom(DisplayCutout displayCutout)278         static int getSafeInsetBottom(DisplayCutout displayCutout) {
279             return displayCutout.getSafeInsetBottom();
280         }
281 
getSafeInsetLeft(DisplayCutout displayCutout)282         static int getSafeInsetLeft(DisplayCutout displayCutout) {
283             return displayCutout.getSafeInsetLeft();
284         }
285 
getSafeInsetRight(DisplayCutout displayCutout)286         static int getSafeInsetRight(DisplayCutout displayCutout) {
287             return displayCutout.getSafeInsetRight();
288         }
289 
getBoundingRects(DisplayCutout displayCutout)290         static List<Rect> getBoundingRects(DisplayCutout displayCutout) {
291             return displayCutout.getBoundingRects();
292         }
293     }
294 
295     @RequiresApi(29)
296     static class Api29Impl {
Api29Impl()297         private Api29Impl() {
298             // This class is not instantiable.
299         }
300 
createDisplayCutout(android.graphics.@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom)301         static DisplayCutout createDisplayCutout(android.graphics.@NonNull Insets safeInsets,
302                 @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight,
303                 @Nullable Rect boundBottom) {
304             return new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom);
305         }
306     }
307 
308     @RequiresApi(30)
309     static class Api30Impl {
Api30Impl()310         private Api30Impl() {
311             // This class is not instantiable.
312         }
313 
createDisplayCutout( android.graphics.@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, android.graphics.@NonNull Insets waterfallInsets)314         static DisplayCutout createDisplayCutout(
315                 android.graphics.@NonNull Insets safeInsets, @Nullable Rect boundLeft,
316                 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
317                 android.graphics.@NonNull Insets waterfallInsets) {
318             return new DisplayCutout(safeInsets, boundLeft, boundTop, boundRight, boundBottom,
319                     waterfallInsets);
320         }
321 
getWaterfallInsets(DisplayCutout displayCutout)322         static android.graphics.Insets getWaterfallInsets(DisplayCutout displayCutout) {
323             return displayCutout.getWaterfallInsets();
324         }
325     }
326 
327     @RequiresApi(31)
328     static class Api31Impl {
Api31Impl()329         private Api31Impl() {
330             // This class is not instantiable.
331         }
332 
getCutoutPath(DisplayCutout displayCutout)333         static @Nullable Path getCutoutPath(DisplayCutout displayCutout) {
334             return displayCutout.getCutoutPath();
335         }
336     }
337 
338     @RequiresApi(33)
339     static class Api33Impl {
Api33Impl()340         private Api33Impl() {
341             // This class is not instantiable.
342         }
343 
createDisplayCutout( android.graphics.@onNull Insets safeInsets, @Nullable Rect boundLeft, @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom, android.graphics.@NonNull Insets waterfallInsets, @Nullable Path cutoutPath)344         static DisplayCutout createDisplayCutout(
345                 android.graphics.@NonNull Insets safeInsets, @Nullable Rect boundLeft,
346                 @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
347                 android.graphics.@NonNull Insets waterfallInsets, @Nullable Path cutoutPath) {
348             DisplayCutout.Builder builder = new DisplayCutout.Builder()
349                     .setSafeInsets(safeInsets)
350                     .setWaterfallInsets(waterfallInsets);
351 
352             if (boundLeft != null) {
353                 builder.setBoundingRectLeft(boundLeft);
354             }
355             if (boundTop != null) {
356                 builder.setBoundingRectTop(boundTop);
357             }
358             if (boundRight != null) {
359                 builder.setBoundingRectRight(boundRight);
360             }
361             if (boundBottom != null) {
362                 builder.setBoundingRectBottom(boundBottom);
363             }
364             if (cutoutPath != null) {
365                 builder.setCutoutPath(cutoutPath);
366             }
367             return builder.build();
368         }
369     }
370 }
371