OSDN Git Service

Display Cutout: Add Cutout to WindowInsets
authorAdrian Roos <roosa@google.com>
Fri, 10 Nov 2017 14:48:01 +0000 (15:48 +0100)
committerAdrian Roos <roosa@google.com>
Tue, 28 Nov 2017 15:14:31 +0000 (16:14 +0100)
Adds a facility for communicating the display cutout to
windows. A follow-up CL will make this a public API.

Bug: 65689439
Test: runtest -x $ANDROID_BUILD_TOP/frameworks/base/core/tests/coretests/src/android/view/DisplayCutoutTest.java
Change-Id: I2290adea0130a7e5764a9412616fd3192800e06a

core/java/android/service/wallpaper/WallpaperService.java
core/java/android/view/DisplayCutout.java [new file with mode: 0644]
core/java/android/view/ViewRootImpl.java
core/java/android/view/WindowInsets.java
core/tests/coretests/src/android/view/DisplayCutoutTest.java [new file with mode: 0644]

index 1c6275f..dd0ae33 100644 (file)
@@ -889,7 +889,8 @@ public abstract class WallpaperService extends Service {
                             mFinalStableInsets.set(mDispatchedStableInsets);
                             WindowInsets insets = new WindowInsets(mFinalSystemInsets,
                                     null, mFinalStableInsets,
-                                    getResources().getConfiguration().isScreenRound(), false);
+                                    getResources().getConfiguration().isScreenRound(), false,
+                                    null /* displayCutout */);
                             if (DEBUG) {
                                 Log.v(TAG, "dispatching insets=" + insets);
                             }
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java
new file mode 100644 (file)
index 0000000..19cd42e
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a part of the display that is not functional for displaying content.
+ *
+ * <p>{@code DisplayCutout} is immutable.
+ *
+ * @hide will become API
+ */
+public final class DisplayCutout {
+
+    private static final Rect ZERO_RECT = new Rect(0, 0, 0, 0);
+    private static final ArrayList<Point> EMPTY_LIST = new ArrayList<>();
+
+    /**
+     * An instance where {@link #hasCutout()} returns {@code false}.
+     *
+     * @hide
+     */
+    public static final DisplayCutout NO_CUTOUT =
+            new DisplayCutout(ZERO_RECT, ZERO_RECT, EMPTY_LIST);
+
+    private final Rect mSafeInsets;
+    private final Rect mBoundingRect;
+    private final List<Point> mBoundingPolygon;
+
+    /**
+     * Creates a DisplayCutout instance.
+     *
+     * NOTE: the Rects passed into this instance are not copied and MUST remain unchanged.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public DisplayCutout(Rect safeInsets, Rect boundingRect, List<Point> boundingPolygon) {
+        mSafeInsets = safeInsets != null ? safeInsets : ZERO_RECT;
+        mBoundingRect = boundingRect != null ? boundingRect : ZERO_RECT;
+        mBoundingPolygon = boundingPolygon != null ? boundingPolygon : EMPTY_LIST;
+    }
+
+    /**
+     * Returns whether there is a cutout.
+     *
+     * If false, the safe insets will all return zero, and the bounding box or polygon will be
+     * empty or outside the content view.
+     *
+     * @return {@code true} if there is a cutout, {@code false} otherwise
+     */
+    public boolean hasCutout() {
+        return !mSafeInsets.equals(ZERO_RECT);
+    }
+
+    /** Returns the inset from the top which avoids the display cutout. */
+    public int getSafeInsetTop() {
+        return mSafeInsets.top;
+    }
+
+    /** Returns the inset from the bottom which avoids the display cutout. */
+    public int getSafeInsetBottom() {
+        return mSafeInsets.bottom;
+    }
+
+    /** Returns the inset from the left which avoids the display cutout. */
+    public int getSafeInsetLeft() {
+        return mSafeInsets.left;
+    }
+
+    /** Returns the inset from the right which avoids the display cutout. */
+    public int getSafeInsetRight() {
+        return mSafeInsets.right;
+    }
+
+    /**
+     * Obtains the safe insets in a rect.
+     *
+     * @param out a rect which is set to the safe insets.
+     * @hide
+     */
+    public void getSafeInsets(@NonNull Rect out) {
+        out.set(mSafeInsets);
+    }
+
+    /**
+     * Obtains the bounding rect of the cutout.
+     *
+     * @param outRect is filled with the bounding rect of the cutout. Coordinates are relative
+     *         to the top-left corner of the content view.
+     */
+    public void getBoundingRect(@NonNull Rect outRect) {
+        outRect.set(mBoundingRect);
+    }
+
+    /**
+     * Obtains the bounding polygon of the cutout.
+     *
+     * @param outPolygon is filled with a list of points representing the corners of a convex
+     *         polygon which covers the cutout. Coordinates are relative to the
+     *         top-left corner of the content view.
+     */
+    public void getBoundingPolygon(List<Point> outPolygon) {
+        outPolygon.clear();
+        for (int i = 0; i < mBoundingPolygon.size(); i++) {
+            outPolygon.add(new Point(mBoundingPolygon.get(i)));
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        int result = mSafeInsets.hashCode();
+        result = result * 31 + mBoundingRect.hashCode();
+        result = result * 31 + mBoundingPolygon.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (o instanceof DisplayCutout) {
+            DisplayCutout c = (DisplayCutout) o;
+            return mSafeInsets.equals(c.mSafeInsets)
+                    && mBoundingRect.equals(c.mBoundingRect)
+                    && mBoundingPolygon.equals(c.mBoundingPolygon);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "DisplayCutout{insets=" + mSafeInsets
+                + " bounding=" + mBoundingRect
+                + "}";
+    }
+
+    /**
+     * Insets the reference frame of the cutout in the given directions.
+     *
+     * @return a copy of this instance which has been inset
+     * @hide
+     */
+    public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) {
+        if (mBoundingRect.isEmpty()
+                || insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) {
+            return this;
+        }
+
+        Rect safeInsets = new Rect(mSafeInsets);
+        Rect boundingRect = new Rect(mBoundingRect);
+        ArrayList<Point> boundingPolygon = new ArrayList<>();
+        getBoundingPolygon(boundingPolygon);
+
+        // Note: it's not really well defined what happens when the inset is negative, because we
+        // don't know if the safe inset needs to expand in general.
+        if (insetTop > 0 || safeInsets.top > 0) {
+            safeInsets.top = atLeastZero(safeInsets.top - insetTop);
+        }
+        if (insetBottom > 0 || safeInsets.bottom > 0) {
+            safeInsets.bottom = atLeastZero(safeInsets.bottom - insetBottom);
+        }
+        if (insetLeft > 0 || safeInsets.left > 0) {
+            safeInsets.left = atLeastZero(safeInsets.left - insetLeft);
+        }
+        if (insetRight > 0 || safeInsets.right > 0) {
+            safeInsets.right = atLeastZero(safeInsets.right - insetRight);
+        }
+
+        boundingRect.offset(-insetLeft, -insetTop);
+        offset(boundingPolygon, -insetLeft, -insetTop);
+
+        return new DisplayCutout(safeInsets, boundingRect, boundingPolygon);
+    }
+
+    /**
+     * Calculates the safe insets relative to the given reference frame.
+     *
+     * @return a copy of this instance with the safe insets calculated
+     * @hide
+     */
+    public DisplayCutout calculateRelativeTo(Rect frame) {
+        if (mBoundingRect.isEmpty() || !Rect.intersects(frame, mBoundingRect)) {
+            return NO_CUTOUT;
+        }
+
+        Rect boundingRect = new Rect(mBoundingRect);
+        ArrayList<Point> boundingPolygon = new ArrayList<>();
+        getBoundingPolygon(boundingPolygon);
+
+        return DisplayCutout.calculateRelativeTo(frame, boundingRect, boundingPolygon);
+    }
+
+    private static DisplayCutout calculateRelativeTo(Rect frame, Rect boundingRect,
+            ArrayList<Point> boundingPolygon) {
+        Rect safeRect = new Rect();
+        int bestArea = 0;
+        int bestVariant = 0;
+        for (int variant = ROTATION_0; variant <= ROTATION_270; variant++) {
+            int area = calculateInsetVariantArea(frame, boundingRect, variant, safeRect);
+            if (bestArea < area) {
+                bestArea = area;
+                bestVariant = variant;
+            }
+        }
+        calculateInsetVariantArea(frame, boundingRect, bestVariant, safeRect);
+        if (safeRect.isEmpty()) {
+            // The entire frame overlaps with the cutout.
+            safeRect.set(0, frame.height(), 0, 0);
+        } else {
+            // Convert safeRect to insets relative to frame. We're reusing the rect here to avoid
+            // an allocation.
+            safeRect.set(
+                    Math.max(0, safeRect.left - frame.left),
+                    Math.max(0, safeRect.top - frame.top),
+                    Math.max(0, frame.right - safeRect.right),
+                    Math.max(0, frame.bottom - safeRect.bottom));
+        }
+
+        boundingRect.offset(-frame.left, -frame.top);
+        offset(boundingPolygon, -frame.left, -frame.top);
+
+        return new DisplayCutout(safeRect, boundingRect, boundingPolygon);
+    }
+
+    private static int calculateInsetVariantArea(Rect frame, Rect boundingRect, int variant,
+            Rect outSafeRect) {
+        switch (variant) {
+            case ROTATION_0:
+                outSafeRect.set(frame.left, frame.top, frame.right, boundingRect.top);
+                break;
+            case ROTATION_90:
+                outSafeRect.set(frame.left, frame.top, boundingRect.left, frame.bottom);
+                break;
+            case ROTATION_180:
+                outSafeRect.set(frame.left, boundingRect.bottom, frame.right, frame.bottom);
+                break;
+            case ROTATION_270:
+                outSafeRect.set(boundingRect.right, frame.top, frame.right, frame.bottom);
+                break;
+        }
+
+        return outSafeRect.isEmpty() ? 0 : outSafeRect.width() * outSafeRect.height();
+    }
+
+    private static int atLeastZero(int value) {
+        return value < 0 ? 0 : value;
+    }
+
+    private static void offset(ArrayList<Point> points, int dx, int dy) {
+        for (int i = 0; i < points.size(); i++) {
+            points.get(i).offset(dx, dy);
+        }
+    }
+
+    /**
+     * Creates an instance from a bounding polygon.
+     *
+     * @hide
+     */
+    public static DisplayCutout fromBoundingPolygon(List<Point> points) {
+        Rect boundingRect = new Rect(Integer.MAX_VALUE, Integer.MAX_VALUE,
+                Integer.MIN_VALUE, Integer.MIN_VALUE);
+        ArrayList<Point> boundingPolygon = new ArrayList<>();
+
+        for (int i = 0; i < points.size(); i++) {
+            Point point = points.get(i);
+            boundingRect.left = Math.min(boundingRect.left, point.x);
+            boundingRect.right = Math.max(boundingRect.right, point.x);
+            boundingRect.top = Math.min(boundingRect.top, point.y);
+            boundingRect.bottom = Math.max(boundingRect.bottom, point.y);
+            boundingPolygon.add(new Point(point));
+        }
+
+        return new DisplayCutout(ZERO_RECT, boundingRect, boundingPolygon);
+    }
+
+    /**
+     * Helper class for passing {@link DisplayCutout} through binder.
+     *
+     * Needed, because {@code readFromParcel} cannot be used with immutable classes.
+     *
+     * @hide
+     */
+    public static final class ParcelableWrapper implements Parcelable {
+
+        private DisplayCutout mInner;
+
+        public ParcelableWrapper() {
+            this(NO_CUTOUT);
+        }
+
+        public ParcelableWrapper(DisplayCutout cutout) {
+            mInner = cutout;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            if (mInner == NO_CUTOUT) {
+                out.writeInt(0);
+            } else {
+                out.writeInt(1);
+                out.writeTypedObject(mInner.mSafeInsets, flags);
+                out.writeTypedObject(mInner.mBoundingRect, flags);
+                out.writeTypedList(mInner.mBoundingPolygon, flags);
+            }
+        }
+
+        /**
+         * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing
+         * instance.
+         *
+         * Needed for AIDL out parameters.
+         */
+        public void readFromParcel(Parcel in) {
+            mInner = readCutout(in);
+        }
+
+        public static final Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() {
+            @Override
+            public ParcelableWrapper createFromParcel(Parcel in) {
+                return new ParcelableWrapper(readCutout(in));
+            }
+
+            @Override
+            public ParcelableWrapper[] newArray(int size) {
+                return new ParcelableWrapper[size];
+            }
+        };
+
+        private static DisplayCutout readCutout(Parcel in) {
+            if (in.readInt() == 0) {
+                return NO_CUTOUT;
+            }
+
+            ArrayList<Point> boundingPolygon = new ArrayList<>();
+
+            Rect safeInsets = in.readTypedObject(Rect.CREATOR);
+            Rect boundingRect = in.readTypedObject(Rect.CREATOR);
+            in.readTypedList(boundingPolygon, Point.CREATOR);
+
+            return new DisplayCutout(safeInsets, boundingRect, boundingPolygon);
+        }
+
+        public DisplayCutout get() {
+            return mInner;
+        }
+
+        public void set(ParcelableWrapper cutout) {
+            mInner = cutout.get();
+        }
+
+        public void set(DisplayCutout cutout) {
+            mInner = cutout;
+        }
+
+        @Override
+        public int hashCode() {
+            return mInner.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return o instanceof ParcelableWrapper
+                    && mInner.equals(((ParcelableWrapper) o).mInner);
+        }
+
+        @Override
+        public String toString() {
+            return String.valueOf(mInner);
+        }
+    }
+}
index e9509b7..1c9d863 100644 (file)
@@ -1563,7 +1563,7 @@ public final class ViewRootImpl implements ViewParent,
             mLastWindowInsets = new WindowInsets(contentInsets,
                     null /* windowDecorInsets */, stableInsets,
                     mContext.getResources().getConfiguration().isScreenRound(),
-                    mAttachInfo.mAlwaysConsumeNavBar);
+                    mAttachInfo.mAlwaysConsumeNavBar, null /* displayCutout */);
         }
         return mLastWindowInsets;
     }
index 750931a..df124ac 100644 (file)
@@ -17,6 +17,7 @@
 
 package android.view;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 
 /**
@@ -36,6 +37,7 @@ public final class WindowInsets {
     private Rect mStableInsets;
     private Rect mTempRect;
     private boolean mIsRound;
+    private DisplayCutout mDisplayCutout;
 
     /**
      * In multi-window we force show the navigation bar. Because we don't want that the surface size
@@ -47,6 +49,7 @@ public final class WindowInsets {
     private boolean mSystemWindowInsetsConsumed = false;
     private boolean mWindowDecorInsetsConsumed = false;
     private boolean mStableInsetsConsumed = false;
+    private boolean mCutoutConsumed = false;
 
     private static final Rect EMPTY_RECT = new Rect(0, 0, 0, 0);
 
@@ -59,12 +62,12 @@ public final class WindowInsets {
     public static final WindowInsets CONSUMED;
 
     static {
-        CONSUMED = new WindowInsets(null, null, null, false, false);
+        CONSUMED = new WindowInsets(null, null, null, false, false, null);
     }
 
     /** @hide */
     public WindowInsets(Rect systemWindowInsets, Rect windowDecorInsets, Rect stableInsets,
-            boolean isRound, boolean alwaysConsumeNavBar) {
+            boolean isRound, boolean alwaysConsumeNavBar, DisplayCutout displayCutout) {
         mSystemWindowInsetsConsumed = systemWindowInsets == null;
         mSystemWindowInsets = mSystemWindowInsetsConsumed ? EMPTY_RECT : systemWindowInsets;
 
@@ -76,6 +79,9 @@ public final class WindowInsets {
 
         mIsRound = isRound;
         mAlwaysConsumeNavBar = alwaysConsumeNavBar;
+
+        mCutoutConsumed = displayCutout == null;
+        mDisplayCutout = mCutoutConsumed ? DisplayCutout.NO_CUTOUT : displayCutout;
     }
 
     /**
@@ -92,11 +98,13 @@ public final class WindowInsets {
         mStableInsetsConsumed = src.mStableInsetsConsumed;
         mIsRound = src.mIsRound;
         mAlwaysConsumeNavBar = src.mAlwaysConsumeNavBar;
+        mDisplayCutout = src.mDisplayCutout;
+        mCutoutConsumed = src.mCutoutConsumed;
     }
 
     /** @hide */
     public WindowInsets(Rect systemWindowInsets) {
-        this(systemWindowInsets, null, null, false, false);
+        this(systemWindowInsets, null, null, false, false, null);
     }
 
     /**
@@ -260,9 +268,34 @@ public final class WindowInsets {
      * @return true if any inset values are nonzero
      */
     public boolean hasInsets() {
-        return hasSystemWindowInsets() || hasWindowDecorInsets() || hasStableInsets();
+        return hasSystemWindowInsets() || hasWindowDecorInsets() || hasStableInsets()
+                || mDisplayCutout.hasCutout();
+    }
+
+    /**
+     * @return the display cutout
+     * @see DisplayCutout
+     * @hide pending API
+     */
+    @NonNull
+    public DisplayCutout getDisplayCutout() {
+        return mDisplayCutout;
+    }
+
+    /**
+     * Returns a copy of this WindowInsets with the cutout fully consumed.
+     *
+     * @return A modified copy of this WindowInsets
+     * @hide pending API
+     */
+    public WindowInsets consumeCutout() {
+        final WindowInsets result = new WindowInsets(this);
+        result.mDisplayCutout = DisplayCutout.NO_CUTOUT;
+        result.mCutoutConsumed = true;
+        return result;
     }
 
+
     /**
      * Check if these insets have been fully consumed.
      *
@@ -277,7 +310,8 @@ public final class WindowInsets {
      * @return true if the insets have been fully consumed.
      */
     public boolean isConsumed() {
-        return mSystemWindowInsetsConsumed && mWindowDecorInsetsConsumed && mStableInsetsConsumed;
+        return mSystemWindowInsetsConsumed && mWindowDecorInsetsConsumed && mStableInsetsConsumed
+                && mCutoutConsumed;
     }
 
     /**
@@ -495,7 +529,9 @@ public final class WindowInsets {
     public String toString() {
         return "WindowInsets{systemWindowInsets=" + mSystemWindowInsets
                 + " windowDecorInsets=" + mWindowDecorInsets
-                + " stableInsets=" + mStableInsets +
-                (isRound() ? " round}" : "}");
+                + " stableInsets=" + mStableInsets
+                + (mDisplayCutout.hasCutout() ? " cutout=" + mDisplayCutout : "")
+                + (isRound() ? " round" : "")
+                + "}";
     }
 }
diff --git a/core/tests/coretests/src/android/view/DisplayCutoutTest.java b/core/tests/coretests/src/android/view/DisplayCutoutTest.java
new file mode 100644 (file)
index 0000000..6dd787d
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.DisplayCutout.NO_CUTOUT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.DisplayCutout.ParcelableWrapper;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class DisplayCutoutTest {
+
+    /** This is not a consistent cutout. Useful for verifying insets in one go though. */
+    final DisplayCutout mCutoutNumbers = new DisplayCutout(
+            new Rect(1, 2, 3, 4),
+            new Rect(5, 6, 7, 8),
+            Arrays.asList(
+                    new Point(9, 10),
+                    new Point(11, 12),
+                    new Point(13, 14),
+                    new Point(15, 16)));
+
+    final DisplayCutout mCutoutTop = createCutoutTop();
+
+    @Test
+    public void hasCutout() throws Exception {
+        assertFalse(NO_CUTOUT.hasCutout());
+        assertTrue(mCutoutTop.hasCutout());
+    }
+
+    @Test
+    public void getSafeInsets() throws Exception {
+        assertEquals(1, mCutoutNumbers.getSafeInsetLeft());
+        assertEquals(2, mCutoutNumbers.getSafeInsetTop());
+        assertEquals(3, mCutoutNumbers.getSafeInsetRight());
+        assertEquals(4, mCutoutNumbers.getSafeInsetBottom());
+
+        Rect safeInsets = new Rect();
+        mCutoutNumbers.getSafeInsets(safeInsets);
+
+        assertEquals(new Rect(1, 2, 3, 4), safeInsets);
+    }
+
+    @Test
+    public void getBoundingRect() throws Exception {
+        Rect boundingRect = new Rect();
+        mCutoutTop.getBoundingRect(boundingRect);
+
+        assertEquals(new Rect(50, 0, 75, 100), boundingRect);
+    }
+
+    @Test
+    public void getBoundingPolygon() throws Exception {
+        ArrayList<Point> boundingPolygon = new ArrayList<>();
+        mCutoutTop.getBoundingPolygon(boundingPolygon);
+
+        assertEquals(Arrays.asList(
+                new Point(75, 0),
+                new Point(50, 0),
+                new Point(75, 100),
+                new Point(50, 100)), boundingPolygon);
+    }
+
+    @Test
+    public void testHashCode() throws Exception {
+        assertEquals(mCutoutTop.hashCode(), createCutoutTop().hashCode());
+        assertNotEquals(mCutoutTop.hashCode(), mCutoutNumbers.hashCode());
+    }
+
+    @Test
+    public void testEquals() throws Exception {
+        assertEquals(mCutoutTop, createCutoutTop());
+        assertNotEquals(mCutoutTop, mCutoutNumbers);
+    }
+
+    @Test
+    public void testToString() throws Exception {
+        assertFalse(mCutoutTop.toString().isEmpty());
+        assertFalse(mCutoutNumbers.toString().isEmpty());
+    }
+
+    @Test
+    public void inset_immutable() throws Exception {
+        DisplayCutout cutout = mCutoutTop.inset(1, 2, 3, 4);
+
+        assertEquals("original instance must not be mutated", createCutoutTop(), mCutoutTop);
+    }
+
+    @Test
+    public void inset_insets_withLeftCutout() throws Exception {
+        DisplayCutout cutout = createCutoutWithInsets(100, 0, 0, 0).inset(1, 2, 3, 4);
+
+        assertEquals(cutout.getSafeInsetLeft(), 99);
+        assertEquals(cutout.getSafeInsetTop(), 0);
+        assertEquals(cutout.getSafeInsetRight(), 0);
+        assertEquals(cutout.getSafeInsetBottom(), 0);
+    }
+
+    @Test
+    public void inset_insets_withTopCutout() throws Exception {
+        DisplayCutout cutout = mCutoutTop.inset(1, 2, 3, 4);
+
+        assertEquals(cutout.getSafeInsetLeft(), 0);
+        assertEquals(cutout.getSafeInsetTop(), 98);
+        assertEquals(cutout.getSafeInsetRight(), 0);
+        assertEquals(cutout.getSafeInsetBottom(), 0);
+    }
+
+    @Test
+    public void inset_insets_withRightCutout() throws Exception {
+        DisplayCutout cutout = createCutoutWithInsets(0, 0, 100, 0).inset(1, 2, 3, 4);
+
+        assertEquals(cutout.getSafeInsetLeft(), 0);
+        assertEquals(cutout.getSafeInsetTop(), 0);
+        assertEquals(cutout.getSafeInsetRight(), 97);
+        assertEquals(cutout.getSafeInsetBottom(), 0);
+    }
+
+    @Test
+    public void inset_insets_withBottomCutout() throws Exception {
+        DisplayCutout cutout = createCutoutWithInsets(0, 0, 0, 100).inset(1, 2, 3, 4);
+
+        assertEquals(cutout.getSafeInsetLeft(), 0);
+        assertEquals(cutout.getSafeInsetTop(), 0);
+        assertEquals(cutout.getSafeInsetRight(), 0);
+        assertEquals(cutout.getSafeInsetBottom(), 96);
+    }
+
+    @Test
+    public void inset_insets_consumeInset() throws Exception {
+        DisplayCutout cutout = mCutoutTop.inset(0, 1000, 0, 0);
+
+        assertEquals(cutout.getSafeInsetLeft(), 0);
+        assertEquals(cutout.getSafeInsetTop(), 0);
+        assertEquals(cutout.getSafeInsetRight(), 0);
+        assertEquals(cutout.getSafeInsetBottom(), 0);
+
+        assertFalse(cutout.hasCutout());
+    }
+
+    @Test
+    public void inset_bounds() throws Exception {
+        DisplayCutout cutout = mCutoutTop.inset(1, 2, 3, 4);
+
+        Rect boundingRect = new Rect();
+        cutout.getBoundingRect(boundingRect);
+
+        assertEquals(new Rect(49, -2, 74, 98), boundingRect);
+
+        ArrayList<Point> boundingPolygon = new ArrayList<>();
+        cutout.getBoundingPolygon(boundingPolygon);
+
+        assertEquals(Arrays.asList(
+                new Point(74, -2),
+                new Point(49, -2),
+                new Point(74, 98),
+                new Point(49, 98)), boundingPolygon);
+    }
+
+    @Test
+    public void calculateRelativeTo_top() throws Exception {
+        DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(0, 0, 200, 400));
+
+        Rect insets = new Rect();
+        cutout.getSafeInsets(insets);
+
+        assertEquals(new Rect(0, 100, 0, 0), insets);
+    }
+
+    @Test
+    public void calculateRelativeTo_left() throws Exception {
+        DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(0, 0, 400, 200));
+
+        Rect insets = new Rect();
+        cutout.getSafeInsets(insets);
+
+        assertEquals(new Rect(75, 0, 0, 0), insets);
+    }
+
+    @Test
+    public void calculateRelativeTo_bottom() throws Exception {
+        DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(0, -300, 200, 100));
+
+        Rect insets = new Rect();
+        cutout.getSafeInsets(insets);
+
+        assertEquals(new Rect(0, 0, 0, 100), insets);
+    }
+
+    @Test
+    public void calculateRelativeTo_right() throws Exception {
+        DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(-400, -200, 100, 100));
+
+        Rect insets = new Rect();
+        cutout.getSafeInsets(insets);
+
+        assertEquals(new Rect(0, 0, 50, 0), insets);
+    }
+
+    @Test
+    public void calculateRelativeTo_bounds() throws Exception {
+        DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(-1000, -2000, 100, 200));
+
+
+        Rect boundingRect = new Rect();
+        cutout.getBoundingRect(boundingRect);
+        assertEquals(new Rect(1050, 2000, 1075, 2100), boundingRect);
+
+        ArrayList<Point> boundingPolygon = new ArrayList<>();
+        cutout.getBoundingPolygon(boundingPolygon);
+
+        assertEquals(Arrays.asList(
+                new Point(1075, 2000),
+                new Point(1050, 2000),
+                new Point(1075, 2100),
+                new Point(1050, 2100)), boundingPolygon);
+    }
+
+    @Test
+    public void fromBoundingPolygon() throws Exception {
+        assertEquals(
+                new DisplayCutout(
+                        new Rect(0, 0, 0, 0), // fromBoundingPolygon won't calculate safe insets.
+                        new Rect(50, 0, 75, 100),
+                        Arrays.asList(
+                                new Point(75, 0),
+                                new Point(50, 0),
+                                new Point(75, 100),
+                                new Point(50, 100))),
+                DisplayCutout.fromBoundingPolygon(
+                        Arrays.asList(
+                                new Point(75, 0),
+                                new Point(50, 0),
+                                new Point(75, 100),
+                                new Point(50, 100))));
+    }
+
+    @Test
+    public void parcel_unparcel_regular() {
+        Parcel p = Parcel.obtain();
+
+        new ParcelableWrapper(mCutoutTop).writeToParcel(p, 0);
+        int posAfterWrite = p.dataPosition();
+
+        p.setDataPosition(0);
+
+        assertEquals(mCutoutTop, ParcelableWrapper.CREATOR.createFromParcel(p).get());
+        assertEquals(posAfterWrite, p.dataPosition());
+    }
+
+    @Test
+    public void parcel_unparcel_nocutout() {
+        Parcel p = Parcel.obtain();
+
+        new ParcelableWrapper(NO_CUTOUT).writeToParcel(p, 0);
+        int posAfterWrite = p.dataPosition();
+
+        p.setDataPosition(0);
+
+        assertEquals(NO_CUTOUT, ParcelableWrapper.CREATOR.createFromParcel(p).get());
+        assertEquals(posAfterWrite, p.dataPosition());
+    }
+
+    @Test
+    public void parcel_unparcel_inplace() {
+        Parcel p = Parcel.obtain();
+
+        new ParcelableWrapper(mCutoutTop).writeToParcel(p, 0);
+        int posAfterWrite = p.dataPosition();
+
+        p.setDataPosition(0);
+
+        ParcelableWrapper wrapper = new ParcelableWrapper();
+        wrapper.readFromParcel(p);
+
+        assertEquals(mCutoutTop, wrapper.get());
+        assertEquals(posAfterWrite, p.dataPosition());
+    }
+
+    @Test
+    public void wrapper_hashcode() throws Exception {
+        assertEquals(new ParcelableWrapper(mCutoutTop).hashCode(),
+                new ParcelableWrapper(createCutoutTop()).hashCode());
+        assertNotEquals(new ParcelableWrapper(mCutoutTop).hashCode(),
+                new ParcelableWrapper(mCutoutNumbers).hashCode());
+    }
+
+    @Test
+    public void wrapper_equals() throws Exception {
+        assertEquals(new ParcelableWrapper(mCutoutTop), new ParcelableWrapper(createCutoutTop()));
+        assertNotEquals(new ParcelableWrapper(mCutoutTop), new ParcelableWrapper(mCutoutNumbers));
+    }
+
+    private static DisplayCutout createCutoutTop() {
+        return new DisplayCutout(
+                new Rect(0, 100, 0, 0),
+                new Rect(50, 0, 75, 100),
+                Arrays.asList(
+                        new Point(75, 0),
+                        new Point(50, 0),
+                        new Point(75, 100),
+                        new Point(50, 100)));
+    }
+
+    private static DisplayCutout createCutoutWithInsets(int left, int top, int right, int bottom) {
+        return new DisplayCutout(
+                new Rect(left, top, right, bottom),
+                new Rect(50, 0, 75, 100),
+                Arrays.asList(
+                        new Point(75, 0),
+                        new Point(50, 0),
+                        new Point(75, 100),
+                        new Point(50, 100)));
+    }
+}