1/*
2 * Copyright 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view;
18
19import static android.util.DisplayMetrics.DENSITY_DEFAULT;
20import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
21import static android.view.DisplayCutoutProto.BOUNDS;
22import static android.view.DisplayCutoutProto.INSETS;
23
24import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
25
26import android.content.res.Resources;
27import android.graphics.Matrix;
28import android.graphics.Path;
29import android.graphics.Rect;
30import android.graphics.RectF;
31import android.graphics.Region;
32import android.os.Parcel;
33import android.os.Parcelable;
34import android.text.TextUtils;
35import android.util.Log;
36import android.util.Pair;
37import android.util.PathParser;
38import android.util.proto.ProtoOutputStream;
39
40import com.android.internal.R;
41import com.android.internal.annotations.GuardedBy;
42import com.android.internal.annotations.VisibleForTesting;
43
44import java.util.ArrayList;
45import 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 */
52public 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)
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     */
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     */
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     */
141    public boolean isBoundsEmpty() {
142        return mBounds.isEmpty();
143    }
144
145    /** Returns the inset from the top which avoids the display cutout in pixels. */
146    public int getSafeInsetTop() {
147        return mSafeInsets.top;
148    }
149
150    /** Returns the inset from the bottom which avoids the display cutout in pixels. */
151    public int getSafeInsetBottom() {
152        return mSafeInsets.bottom;
153    }
154
155    /** Returns the inset from the left which avoids the display cutout in pixels. */
156    public int getSafeInsetLeft() {
157        return mSafeInsets.left;
158    }
159
160    /** Returns the inset from the right which avoids the display cutout in pixels. */
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     */
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     */
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     */
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
235    public int hashCode() {
236        int result = mSafeInsets.hashCode();
237        result = result * 31 + mBounds.getBounds().hashCode();
238        return result;
239    }
240
241    @Override
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
255    public String toString() {
256        return "DisplayCutout{insets=" + mSafeInsets
257                + " boundingRect=" + mBounds.getBounds()
258                + "}";
259    }
260
261    /**
262     * @hide
263     */
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     */
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     */
313    public DisplayCutout replaceSafeInsets(Rect safeInsets) {
314        return new DisplayCutout(new Rect(safeInsets), mBounds, false /* copyArguments */);
315    }
316
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     */
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     */
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     */
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     */
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)
382    public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight,
383            float density) {
384        return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second;
385    }
386
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
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
480        public ParcelableWrapper() {
481            this(NO_CUTOUT);
482        }
483
484        public ParcelableWrapper(DisplayCutout cutout) {
485            mInner = cutout;
486        }
487
488        @Override
489        public int describeContents() {
490            return 0;
491        }
492
493        @Override
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         */
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         */
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         */
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
557        public DisplayCutout get() {
558            return mInner;
559        }
560
561        public void set(ParcelableWrapper cutout) {
562            mInner = cutout.get();
563        }
564
565        public void set(DisplayCutout cutout) {
566            mInner = cutout;
567        }
568
569        @Override
570        public int hashCode() {
571            return mInner.hashCode();
572        }
573
574        @Override
575        public boolean equals(Object o) {
576            return o instanceof ParcelableWrapper
577                    && mInner.equals(((ParcelableWrapper) o).mInner);
578        }
579
580        @Override
581        public String toString() {
582            return String.valueOf(mInner);
583        }
584    }
585}
586