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