Merge "Support display cutout side override" into main
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java
index 712d1d6..f819c9b 100644
--- a/core/java/android/view/DisplayCutout.java
+++ b/core/java/android/view/DisplayCutout.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.content.res.Resources.ID_NULL;
 import static android.util.DisplayMetrics.DENSITY_DEFAULT;
 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
 import static android.view.DisplayCutoutProto.BOUND_BOTTOM;
@@ -23,8 +24,12 @@
 import static android.view.DisplayCutoutProto.BOUND_RIGHT;
 import static android.view.DisplayCutoutProto.BOUND_TOP;
 import static android.view.DisplayCutoutProto.INSETS;
+import static android.view.DisplayCutoutProto.SIDE_OVERRIDES;
 import static android.view.DisplayCutoutProto.WATERFALL_INSETS;
 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 static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
@@ -49,6 +54,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -112,6 +118,9 @@
     private static float sCachedPhysicalPixelDisplaySizeRatio;
 
     @GuardedBy("CACHE_LOCK")
+    private static int[] sCachedSideOverrides;
+
+    @GuardedBy("CACHE_LOCK")
     private static CutoutPathParserInfo sCachedCutoutPathParserInfo;
     @GuardedBy("CACHE_LOCK")
     private static Path sCachedCutoutPath;
@@ -150,6 +159,15 @@
      */
     public static final int BOUNDS_POSITION_LENGTH = 4;
 
+    private static final int INVALID_SIDE_OVERRIDE = -1;
+    private static final String SIDE_STRING_TOP = "top";
+    private static final String SIDE_STRING_BOTTOM = "bottom";
+    private static final String SIDE_STRING_RIGHT = "right";
+    private static final String SIDE_STRING_LEFT = "left";
+
+    // The side index is always under the natural rotation of the device.
+    private int[] mSideOverrides;
+
     /** @hide */
     @IntDef(prefix = { "BOUNDS_POSITION_" }, value = {
             BOUNDS_POSITION_LEFT,
@@ -402,8 +420,36 @@
     // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE)
     public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
             @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom) {
-        this(safeInsets.toRect(), Insets.NONE, boundLeft, boundTop, boundRight, boundBottom, null,
-                true);
+        this(getCopyOrRef(safeInsets.toRect(), true), Insets.NONE,
+                new Bounds(boundLeft, boundTop, boundRight, boundBottom, true), null, null);
+    }
+
+    /**
+     * Creates a DisplayCutout instance.
+     *
+     * <p>Note that this is only useful for tests. For production code, developers should always
+     * use a {@link DisplayCutout} obtained from the system.</p>
+     *
+     * @param safeInsets the insets from each edge which avoid the display cutout as returned by
+     *                   {@link #getSafeInsetTop()} etc.
+     * @param boundLeft the left bounding rect of the display cutout in pixels. If null is passed,
+     *                  it's treated as an empty rectangle (0,0)-(0,0).
+     * @param boundTop the top bounding rect of the display cutout in pixels.  If null is passed,
+     *                  it's treated as an empty rectangle (0,0)-(0,0).
+     * @param boundRight the right bounding rect of the display cutout in pixels.  If null is
+     *                  passed, it's treated as an empty rectangle (0,0)-(0,0).
+     * @param boundBottom the bottom bounding rect of the display cutout in pixels.  If null is
+     *                   passed, it's treated as an empty rectangle (0,0)-(0,0).
+     * @param waterfallInsets the insets for the curved areas in waterfall display.
+     * @param info the cutout path parser info.
+     * @hide
+     */
+    @VisibleForTesting
+    public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
+            @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
+            @NonNull Insets waterfallInsets, @Nullable CutoutPathParserInfo info) {
+        this(getCopyOrRef(safeInsets.toRect(), true), waterfallInsets,
+                new Bounds(boundLeft, boundTop, boundRight, boundBottom, true), info, null);
     }
 
     /**
@@ -428,9 +474,11 @@
      */
     public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
             @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
-            @NonNull Insets waterfallInsets, @Nullable CutoutPathParserInfo info) {
-        this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom,
-                info, true);
+            @NonNull Insets waterfallInsets, @Nullable CutoutPathParserInfo info,
+            @Nullable int[] sideOverrides) {
+        this(safeInsets.toRect(), waterfallInsets,
+                new Bounds(boundLeft, boundTop, boundRight, boundBottom, true),
+                info, sideOverrides);
     }
 
     /**
@@ -454,8 +502,8 @@
     public DisplayCutout(@NonNull Insets safeInsets, @Nullable Rect boundLeft,
             @Nullable Rect boundTop, @Nullable Rect boundRight, @Nullable Rect boundBottom,
             @NonNull Insets waterfallInsets) {
-        this(safeInsets.toRect(), waterfallInsets, boundLeft, boundTop, boundRight, boundBottom,
-                null, true);
+        this(getCopyOrRef(safeInsets.toRect(), true), waterfallInsets,
+                new Bounds(boundLeft, boundTop, boundRight, boundBottom, true), null, null);
     }
 
     /**
@@ -473,8 +521,8 @@
     // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE)
     @Deprecated
     public DisplayCutout(@Nullable Rect safeInsets, @Nullable List<Rect> boundingRects) {
-        this(safeInsets, Insets.NONE, extractBoundsFromList(safeInsets, boundingRects), null,
-                true /* copyArguments */);
+        this(getCopyOrRef(safeInsets, true), Insets.NONE,
+                new Bounds(extractBoundsFromList(safeInsets, boundingRects), true), null, null);
     }
 
     /**
@@ -498,26 +546,29 @@
     private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect boundLeft, Rect boundTop,
             Rect boundRight, Rect boundBottom, CutoutPathParserInfo info,
             boolean copyArguments) {
-        mSafeInsets = getCopyOrRef(safeInsets, copyArguments);
-        mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets;
-        mBounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments);
-        mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info;
+        this(getCopyOrRef(safeInsets, copyArguments), waterfallInsets,
+                new Bounds(boundLeft, boundTop, boundRight, boundBottom, copyArguments), info,
+                null);
     }
 
     private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Rect[] bounds,
             CutoutPathParserInfo info, boolean copyArguments) {
-        mSafeInsets = getCopyOrRef(safeInsets, copyArguments);
-        mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets;
-        mBounds = new Bounds(bounds, copyArguments);
-        mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info;
+        this(getCopyOrRef(safeInsets, copyArguments), waterfallInsets,
+                new Bounds(bounds, copyArguments), info, null);
     }
 
     private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds,
             CutoutPathParserInfo info) {
+        this(safeInsets, waterfallInsets, bounds, info, null);
+    }
+
+    private DisplayCutout(Rect safeInsets, Insets waterfallInsets, Bounds bounds,
+            CutoutPathParserInfo info, int[] sideOverrides) {
         mSafeInsets = safeInsets;
         mWaterfallInsets = waterfallInsets == null ? Insets.NONE : waterfallInsets;
         mBounds = bounds;
         mCutoutPathParserInfo = info == null ? EMPTY_PARSER_INFO : info;
+        mSideOverrides = sideOverrides;
     }
 
     private static Rect getCopyOrRef(Rect r, boolean copyArguments) {
@@ -795,6 +846,7 @@
         result = 48271 * result + mBounds.hashCode();
         result = 48271 * result + mWaterfallInsets.hashCode();
         result = 48271 * result + mCutoutPathParserInfo.hashCode();
+        result = 48271 * result + Arrays.hashCode(mSideOverrides);
         return result;
     }
 
@@ -807,7 +859,8 @@
             DisplayCutout c = (DisplayCutout) o;
             return mSafeInsets.equals(c.mSafeInsets) && mBounds.equals(c.mBounds)
                     && mWaterfallInsets.equals(c.mWaterfallInsets)
-                    && mCutoutPathParserInfo.equals(c.mCutoutPathParserInfo);
+                    && mCutoutPathParserInfo.equals(c.mCutoutPathParserInfo)
+                    && Arrays.equals(mSideOverrides, c.mSideOverrides);
         }
         return false;
     }
@@ -818,9 +871,48 @@
                 + " waterfall=" + mWaterfallInsets
                 + " boundingRect={" + mBounds + "}"
                 + " cutoutPathParserInfo={" + mCutoutPathParserInfo + "}"
+                + " sideOverrides=" + sideOverridesToString(mSideOverrides)
                 + "}";
     }
 
+    private static String sideOverridesToString(int[] sideOverrides) {
+        if (sideOverrides == null) {
+            return "null";
+        }
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{");
+        final int length = sideOverrides.length;
+        if (length != BOUNDS_POSITION_LENGTH) {
+            sb.append("length=").append(sideOverrides.length).append(". ");
+        }
+        boolean hasContent = false;
+        for (int i = ROTATION_0; i < length; i++) {
+            final int override = sideOverrides[i];
+            if (override != INVALID_SIDE_OVERRIDE) {
+                if (hasContent) {
+                    sb.append(", ");
+                }
+                sb.append(Surface.rotationToString(i)).append(": ");
+                switch(override) {
+                    case BOUNDS_POSITION_LEFT:
+                        sb.append(SIDE_STRING_LEFT);
+                        break;
+                    case BOUNDS_POSITION_TOP:
+                        sb.append(SIDE_STRING_TOP);
+                        break;
+                    case BOUNDS_POSITION_RIGHT:
+                        sb.append(SIDE_STRING_RIGHT);
+                        break;
+                    case BOUNDS_POSITION_BOTTOM:
+                        sb.append(SIDE_STRING_BOTTOM);
+                        break;
+                }
+                hasContent = true;
+            }
+        }
+        return sb.append("}").toString();
+    }
+
     /**
      * @hide
      */
@@ -832,6 +924,11 @@
         mBounds.getRect(BOUNDS_POSITION_RIGHT).dumpDebug(proto, BOUND_RIGHT);
         mBounds.getRect(BOUNDS_POSITION_BOTTOM).dumpDebug(proto, BOUND_BOTTOM);
         mWaterfallInsets.toRect().dumpDebug(proto, WATERFALL_INSETS);
+        if (mSideOverrides != null) {
+            for (int sideOverride : mSideOverrides) {
+                proto.write(SIDE_OVERRIDES, sideOverride);
+            }
+        }
         proto.end(token);
     }
 
@@ -899,7 +996,7 @@
      */
     public DisplayCutout replaceSafeInsets(Rect safeInsets) {
         return new DisplayCutout(new Rect(safeInsets), mWaterfallInsets, mBounds,
-                mCutoutPathParserInfo);
+                mCutoutPathParserInfo, mSideOverrides);
     }
 
     private static int atLeastZero(int value) {
@@ -1031,8 +1128,10 @@
         Insets insets;
         final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
         final TypedArray array = res.obtainTypedArray(R.array.config_waterfallCutoutArray);
-        if (index >= 0 && index < array.length() && array.getResourceId(index, 0) > 0) {
-            final int resourceId = array.getResourceId(index, 0);
+        final int resourceId = index >= 0 && index < array.length()
+                ? array.getResourceId(index, ID_NULL)
+                : ID_NULL;
+        if (resourceId != ID_NULL) {
             final TypedArray waterfall = res.obtainTypedArray(resourceId);
             insets = Insets.of(
                     waterfall.getDimensionPixelSize(0 /* waterfall left edge size */, 0),
@@ -1047,6 +1146,48 @@
         return insets;
     }
 
+    private static int[] getDisplayCutoutSideOverrides(Resources res, String displayUniqueId)
+            throws IllegalArgumentException {
+        if (!Flags.movableCutoutConfiguration()) {
+            return null;
+        }
+        final int index = DisplayUtils.getDisplayUniqueIdConfigIndex(res, displayUniqueId);
+        final TypedArray array = res.obtainTypedArray(
+                R.array.config_displayCutoutSideOverrideArray);
+        final int resourceId = index >= 0 && index < array.length()
+                ? array.getResourceId(index, ID_NULL)
+                : ID_NULL;
+        final String[] rawOverrides = resourceId != ID_NULL
+                ? array.getResources().getStringArray(resourceId)
+                : res.getStringArray(R.array.config_mainBuiltInDisplayCutoutSideOverride);
+        array.recycle();
+        final int[] override = new int[]{INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE,
+                INVALID_SIDE_OVERRIDE, INVALID_SIDE_OVERRIDE};
+        for (String rawOverride : rawOverrides) {
+            int rotation;
+            String[] split = rawOverride.split(" *, *");
+            switch (split[0]) {
+                case "0" -> rotation = ROTATION_0;
+                case "90" -> rotation = ROTATION_90;
+                case "180" -> rotation = ROTATION_180;
+                case "270" -> rotation = ROTATION_270;
+                default -> throw new IllegalArgumentException("Invalid side override definition: "
+                            + rawOverride);
+            }
+            int side;
+            switch (split[1]) {
+                case SIDE_STRING_LEFT -> side = BOUNDS_POSITION_LEFT;
+                case SIDE_STRING_TOP -> side = BOUNDS_POSITION_TOP;
+                case SIDE_STRING_RIGHT -> side = BOUNDS_POSITION_RIGHT;
+                case SIDE_STRING_BOTTOM -> side = BOUNDS_POSITION_BOTTOM;
+                default -> throw new IllegalArgumentException("Invalid side override definition: "
+                        + rawOverride);
+            }
+            override[rotation] = side;
+        }
+        return override;
+    }
+
     /**
      * Creates the display cutout according to
      * @android:string/config_mainBuiltInDisplayCutoutRectApproximation, which is the closest
@@ -1060,7 +1201,8 @@
                 getDisplayCutoutApproximationRect(res, displayUniqueId), physicalDisplayWidth,
                 physicalDisplayHeight, displayWidth, displayHeight,
                 DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT,
-                getWaterfallInsets(res, displayUniqueId)).second;
+                getWaterfallInsets(res, displayUniqueId),
+                getDisplayCutoutSideOverrides(res, displayUniqueId)).second;
     }
 
     /**
@@ -1070,17 +1212,17 @@
      */
     @VisibleForTesting(visibility = PRIVATE)
     public static DisplayCutout fromSpec(String pathSpec, int displayWidth,
-            int displayHeight, float density, Insets waterfallInsets) {
+            int displayHeight, float density, Insets waterfallInsets, int[] sideOverrides) {
         return pathAndDisplayCutoutFromSpec(
                 pathSpec, null, displayWidth, displayHeight, displayWidth, displayHeight, density,
-                waterfallInsets).second;
+                waterfallInsets, sideOverrides).second;
     }
 
     /**
      * Gets the cutout path and the corresponding DisplayCutout instance from the spec string.
      *
-     * @param pathSpec the spec string read from config_mainBuiltInDisplayCutout.
-     * @param rectSpec the spec string read from config_mainBuiltInDisplayCutoutRectApproximation.
+     * @param pathSpec the spec string read from config for certain display.
+     * @param rectSpec the rect approximation spec string read from config for certain display.
      * @param physicalDisplayWidth the max physical display width the display supports.
      * @param physicalDisplayHeight the max physical display height the display supports.
      * @param displayWidth the display width.
@@ -1091,7 +1233,8 @@
      */
     private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(
             String pathSpec, String rectSpec, int physicalDisplayWidth, int physicalDisplayHeight,
-            int displayWidth, int displayHeight, float density, Insets waterfallInsets) {
+            int displayWidth, int displayHeight, float density, Insets waterfallInsets,
+            int[] sideOverrides) {
         // Always use the rect approximation spec to create the cutout if it's not null because
         // transforming and sending a Region constructed from a path is very costly.
         String spec = rectSpec != null ? rectSpec : pathSpec;
@@ -1107,7 +1250,8 @@
                     && sCachedDisplayHeight == displayHeight
                     && sCachedDensity == density
                     && waterfallInsets.equals(sCachedWaterfallInsets)
-                    && sCachedPhysicalPixelDisplaySizeRatio == physicalPixelDisplaySizeRatio) {
+                    && sCachedPhysicalPixelDisplaySizeRatio == physicalPixelDisplaySizeRatio
+                    && Arrays.equals(sCachedSideOverrides, sideOverrides)) {
                 return sCachedCutout;
             }
         }
@@ -1123,7 +1267,6 @@
         final Rect boundRight = cutoutSpec.getRightBound();
         final Rect boundBottom = cutoutSpec.getBottomBound();
 
-
         if (!waterfallInsets.equals(Insets.NONE)) {
             safeInset.set(
                     Math.max(waterfallInsets.left, safeInset.left),
@@ -1135,10 +1278,21 @@
         final CutoutPathParserInfo cutoutPathParserInfo = new CutoutPathParserInfo(
                 displayWidth, displayHeight, physicalDisplayWidth, physicalDisplayHeight, density,
                 pathSpec.trim(), ROTATION_0, 1f /* scale */, physicalPixelDisplaySizeRatio);
+        final int sideOverride = getSideOverride(sideOverrides, ROTATION_0);
+        final Rect[] bounds = new Bounds(boundLeft, boundTop, boundRight, boundBottom, false)
+                .getRects();
+        final int rotateDistance = getRotationToOverride(sideOverride, bounds,
+                ROTATION_0 /* defaultRotation */);
+        if (rotateDistance != ROTATION_0) {
+            Collections.rotate(Arrays.asList(bounds), rotateDistance);
+        }
+        final Rect safeInsets = DisplayCutout.computeSafeInsets(displayWidth, displayHeight,
+                waterfallInsets, bounds);
+        final DisplayCutout cutout = new DisplayCutout(safeInsets, waterfallInsets,
+                new Bounds(bounds[BOUNDS_POSITION_LEFT], bounds[BOUNDS_POSITION_TOP],
+                        bounds[BOUNDS_POSITION_RIGHT], bounds[BOUNDS_POSITION_BOTTOM], false),
+                cutoutPathParserInfo, sideOverrides);
 
-        final DisplayCutout cutout = new DisplayCutout(
-                safeInset, waterfallInsets, boundLeft, boundTop, boundRight, boundBottom,
-                cutoutPathParserInfo , false /* copyArguments */);
         final Pair<Path, DisplayCutout> result = new Pair<>(cutoutSpec.getPath(), cutout);
         synchronized (CACHE_LOCK) {
             sCachedSpec = spec;
@@ -1148,6 +1302,7 @@
             sCachedCutout = result;
             sCachedWaterfallInsets = waterfallInsets;
             sCachedPhysicalPixelDisplaySizeRatio = physicalPixelDisplaySizeRatio;
+            sCachedSideOverrides = sideOverrides;
         }
         return result;
     }
@@ -1181,7 +1336,10 @@
             if (newBounds[i].isEmpty()) continue;
             RotationUtils.rotateBounds(newBounds[i], displayBounds, rotation);
         }
-        Collections.rotate(Arrays.asList(newBounds), -rotation);
+        final int defaultRotation = -rotation;
+        final int override = getSideOverride(mSideOverrides, toRotation);
+        Collections.rotate(Arrays.asList(newBounds),
+                getRotationToOverride(override, newBounds, defaultRotation));
         final CutoutPathParserInfo info = getCutoutPathParserInfo();
         final CutoutPathParserInfo newInfo = new CutoutPathParserInfo(
                 info.getDisplayWidth(), info.getDisplayHeight(), info.getPhysicalDisplayWidth(),
@@ -1193,51 +1351,87 @@
         final DisplayCutout tmp =
                 DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo);
         final Rect safeInsets = DisplayCutout.computeSafeInsets(endWidth, endHeight, tmp);
+        tmp.mSideOverrides = mSideOverrides;
         return tmp.replaceSafeInsets(safeInsets);
     }
 
+    private static int getSideOverride(int[] sideOverrides, @Rotation int rotation) {
+        if (sideOverrides == null || sideOverrides.length != 4) {
+            return INVALID_SIDE_OVERRIDE;
+        }
+        return sideOverrides[rotation];
+    }
+
+    /** @return the rotation needed to rotate from the original side to the overridden one. */
+    private static @Rotation int getRotationToOverride(int sideOverride, Rect[] bounds,
+            @Rotation int defaultRotation) {
+        if (sideOverride == INVALID_SIDE_OVERRIDE) {
+            return defaultRotation;
+        }
+        int side = -1;
+        for (int i = 0; i <= BOUNDS_POSITION_BOTTOM; i++) {
+            if (bounds[i].isEmpty()) {
+                continue;
+            }
+            if (side != -1) {
+                // We don't rotate at all when there are multiple non empty cutout bounds.
+                return defaultRotation;
+            }
+            side = i;
+        }
+        if (side == -1) {
+            return defaultRotation;
+        }
+        int rotation = sideOverride - side;
+        if (rotation < 0) {
+            rotation += 4;
+        }
+        return rotation;
+    }
+
     /**
      * Compute the insets derived from a cutout. This is usually used to populate the safe-insets
      * of the cutout via {@link #replaceSafeInsets}.
      * @hide
      */
     public static Rect computeSafeInsets(int displayW, int displayH, DisplayCutout cutout) {
+        return computeSafeInsets(displayW, displayH, cutout.getWaterfallInsets(),
+                cutout.getBoundingRectsAll());
+    }
+
+    private static Rect computeSafeInsets(int displayW, int displayH, Insets waterFallInsets,
+            Rect[] bounds) {
         if (displayW == displayH) {
             throw new UnsupportedOperationException("not implemented: display=" + displayW + "x"
-                    + displayH + " cutout=" + cutout);
+                    + displayH + " bounding rects=" + Arrays.toString(bounds));
         }
 
-        int leftInset = Math.max(cutout.getWaterfallInsets().left, findCutoutInsetForSide(
-                displayW, displayH, cutout.getBoundingRectLeft(), Gravity.LEFT));
-        int topInset = Math.max(cutout.getWaterfallInsets().top, findCutoutInsetForSide(
-                displayW, displayH, cutout.getBoundingRectTop(), Gravity.TOP));
-        int rightInset = Math.max(cutout.getWaterfallInsets().right, findCutoutInsetForSide(
-                displayW, displayH, cutout.getBoundingRectRight(), Gravity.RIGHT));
-        int bottomInset = Math.max(cutout.getWaterfallInsets().bottom, findCutoutInsetForSide(
-                displayW, displayH, cutout.getBoundingRectBottom(), Gravity.BOTTOM));
+        int leftInset = Math.max(waterFallInsets.left, findCutoutInsetForSide(
+                displayW, displayH, bounds[BOUNDS_POSITION_LEFT], Gravity.LEFT));
+        int topInset = Math.max(waterFallInsets.top, findCutoutInsetForSide(
+                displayW, displayH, bounds[BOUNDS_POSITION_TOP], Gravity.TOP));
+        int rightInset = Math.max(waterFallInsets.right, findCutoutInsetForSide(
+                displayW, displayH, bounds[BOUNDS_POSITION_RIGHT], Gravity.RIGHT));
+        int bottomInset = Math.max(waterFallInsets.bottom, findCutoutInsetForSide(
+                displayW, displayH, bounds[BOUNDS_POSITION_BOTTOM], Gravity.BOTTOM));
 
         return new Rect(leftInset, topInset, rightInset, bottomInset);
     }
 
-    private static int findCutoutInsetForSide(int displayW, int displayH, Rect boundingRect,
-            int gravity) {
+    private static int findCutoutInsetForSide(int displayW, int displayH,
+            @NonNull Rect boundingRect, int gravity) {
         if (boundingRect.isEmpty()) {
             return 0;
         }
 
         int inset = 0;
-        switch (gravity) {
-            case Gravity.TOP:
-                return Math.max(inset, boundingRect.bottom);
-            case Gravity.BOTTOM:
-                return Math.max(inset, displayH - boundingRect.top);
-            case Gravity.LEFT:
-                return Math.max(inset, boundingRect.right);
-            case Gravity.RIGHT:
-                return Math.max(inset, displayW - boundingRect.left);
-            default:
-                throw new IllegalArgumentException("unknown gravity: " + gravity);
-        }
+        return switch (gravity) {
+            case Gravity.TOP -> Math.max(inset, boundingRect.bottom);
+            case Gravity.BOTTOM -> Math.max(inset, displayH - boundingRect.top);
+            case Gravity.LEFT -> Math.max(inset, boundingRect.right);
+            case Gravity.RIGHT -> Math.max(inset, displayW - boundingRect.left);
+            default -> throw new IllegalArgumentException("unknown gravity: " + gravity);
+        };
     }
 
     /**
@@ -1293,6 +1487,7 @@
                 out.writeInt(cutout.mCutoutPathParserInfo.getRotation());
                 out.writeFloat(cutout.mCutoutPathParserInfo.getScale());
                 out.writeFloat(cutout.mCutoutPathParserInfo.getPhysicalPixelDisplaySizeRatio());
+                out.writeIntArray(cutout.mSideOverrides);
             }
         }
 
@@ -1348,9 +1543,10 @@
             final CutoutPathParserInfo info = new CutoutPathParserInfo(
                     displayWidth, displayHeight, physicalDisplayWidth, physicalDisplayHeight,
                     density, cutoutSpec, rotation, scale, physicalPixelDisplaySizeRatio);
+            final int[] sideOverrides = in.createIntArray();
 
-            return new DisplayCutout(
-                    safeInsets, waterfallInsets, bounds, info, false /* copyArguments */);
+            return new DisplayCutout(safeInsets, waterfallInsets,
+                        new Bounds(bounds, false /* copyArguments */), info, sideOverrides);
         }
 
         public DisplayCutout get() {
@@ -1382,8 +1578,10 @@
                     mInner.mCutoutPathParserInfo.getRotation(),
                     scale,
                     mInner.mCutoutPathParserInfo.getPhysicalPixelDisplaySizeRatio());
+            final int[] sideOverrides = mInner.mSideOverrides;
 
-            mInner = new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, info);
+            mInner = new DisplayCutout(safeInsets, Insets.of(waterfallInsets), bounds, info,
+                    sideOverrides);
         }
 
         @Override
diff --git a/core/proto/android/view/displaycutout.proto b/core/proto/android/view/displaycutout.proto
index 72d5303..53155d2 100644
--- a/core/proto/android/view/displaycutout.proto
+++ b/core/proto/android/view/displaycutout.proto
@@ -32,4 +32,5 @@
     optional .android.graphics.RectProto bound_right = 5;
     optional .android.graphics.RectProto bound_bottom = 6;
     optional .android.graphics.RectProto waterfall_insets = 7;
+    repeated int32 side_overrides = 8;
 }
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 804e9ef..806be94 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4017,6 +4017,18 @@
          by shrinking the display such that it does not overlap the cutout area. -->
     <bool name="config_maskMainBuiltInDisplayCutout">false</bool>
 
+    <!-- This string array provide override side of each rotation of the given insets.
+         Array of "[rotation],[side]".
+         Undefined rotation will apply the default behavior.
+         When there are cutouts on multiple edges of the display, the override won't take any
+         effect. -->
+    <string-array name="config_mainBuiltInDisplayCutoutSideOverride" translatable="false">
+        <!-- Example:
+        <item>90,top</item>
+        <item>270,bottom</item>
+        -->
+    </string-array>
+
     <!-- Ultrasound support for Mic/speaker path -->
     <!-- Whether the default microphone audio source supports near-ultrasound frequencies
          (range of 18 - 21 kHz). -->
@@ -6373,6 +6385,8 @@
     </string>
     <bool name="config_fillSecondaryBuiltInDisplayCutout">false</bool>
     <bool name="config_maskSecondaryBuiltInDisplayCutout">false</bool>
+    <string-array name="config_secondaryBuiltInDisplayCutoutSideOverride" translatable="false">
+    </string-array>
 
     <!-- An array contains unique ids of all built-in displays and the unique id of a display can be
          obtained from {@link Display#getUniqueId}. This array should be set for multi-display
@@ -6418,6 +6432,11 @@
         <item>@string/config_secondaryBuiltInDisplayCutoutRectApproximation</item>
     </string-array>
 
+    <array name="config_displayCutoutSideOverrideArray" translatable="false">
+        <item>@array/config_mainBuiltInDisplayCutoutSideOverride</item>
+        <item>@array/config_secondaryBuiltInDisplayCutoutSideOverride</item>
+    </array>
+
     <!-- The maskBuiltInDisplayCutout config for each display in a multi-display device. -->
     <array name="config_maskBuiltInDisplayCutoutArray" translatable="false">
         <item>@bool/config_maskMainBuiltInDisplayCutout</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 9589fb0..b0a4c16 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4036,6 +4036,7 @@
   <java-symbol type="string" name="global_action_logout" />
   <java-symbol type="string" name="config_mainBuiltInDisplayCutout" />
   <java-symbol type="string" name="config_mainBuiltInDisplayCutoutRectApproximation" />
+  <java-symbol type="array" name="config_mainBuiltInDisplayCutoutSideOverride" />
   <java-symbol type="drawable" name="messaging_user" />
   <java-symbol type="bool" name="config_fillMainBuiltInDisplayCutout" />
   <java-symbol type="drawable" name="ic_logout" />
@@ -5002,9 +5003,11 @@
   <java-symbol type="string" name="config_secondaryBuiltInDisplayCutoutRectApproximation" />
   <java-symbol type="bool" name="config_fillSecondaryBuiltInDisplayCutout" />
   <java-symbol type="bool" name="config_maskSecondaryBuiltInDisplayCutout" />
+  <java-symbol type="array" name="config_secondaryBuiltInDisplayCutoutSideOverride" />
   <java-symbol type="array" name="config_displayUniqueIdArray" />
   <java-symbol type="array" name="config_displayCutoutPathArray" />
   <java-symbol type="array" name="config_displayCutoutApproximationRectArray" />
+  <java-symbol type="array" name="config_displayCutoutSideOverrideArray" />
   <java-symbol type="array" name="config_fillBuiltInDisplayCutoutArray" />
   <java-symbol type="array" name="config_maskBuiltInDisplayCutoutArray" />
   <java-symbol type="dimen" name="secondary_waterfall_display_left_edge_size" />
diff --git a/core/tests/coretests/src/android/view/DisplayCutoutTest.java b/core/tests/coretests/src/android/view/DisplayCutoutTest.java
index faeae2c..0d1dde3 100644
--- a/core/tests/coretests/src/android/view/DisplayCutoutTest.java
+++ b/core/tests/coretests/src/android/view/DisplayCutoutTest.java
@@ -16,6 +16,8 @@
 
 package android.view;
 
+import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
+import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
 import static android.view.DisplayCutout.NO_CUTOUT;
 import static android.view.DisplayCutout.extractBoundsFromList;
 import static android.view.DisplayCutout.fromSpec;
@@ -180,7 +182,7 @@
         final int displayHeight = 400;
         final float density = 1f;
         final DisplayCutout cutout = fromSpec(cutoutSpecString, displayWidth, displayHeight,
-                density, Insets.NONE);
+                density, Insets.NONE, null);
         assertThat(cutout.getCutoutPath(), notNullValue());
     }
 
@@ -191,9 +193,9 @@
         final int displayHeight = 400;
         final float density = 1f;
         final Path first = fromSpec(cutoutSpecString, displayWidth, displayHeight,
-                density, Insets.NONE).getCutoutPath();
+                density, Insets.NONE, null).getCutoutPath();
         final Path second = fromSpec(cutoutSpecString, displayWidth, displayHeight,
-                density, Insets.NONE).getCutoutPath();
+                density, Insets.NONE, null).getCutoutPath();
         assertThat(first, equalTo(second));
     }
 
@@ -203,9 +205,9 @@
         final int displayHeight = 400;
         final float density = 1f;
         final Path first = fromSpec("L1,0 L1,1 L0,1 z", displayWidth, displayHeight,
-                density, Insets.NONE).getCutoutPath();
+                density, Insets.NONE, null).getCutoutPath();
         final Path second = fromSpec("L2,0 L2,2 L0,2 z", displayWidth, displayHeight,
-                density, Insets.NONE).getCutoutPath();
+                density, Insets.NONE, null).getCutoutPath();
         assertThat(first, not(equalTo(second)));
     }
 
@@ -216,7 +218,7 @@
         final int displayHeight = 400;
         final float density = 1f;
         final DisplayCutout cutout = fromSpec(cutoutSpecString, displayWidth, displayHeight,
-                density, Insets.NONE);
+                density, Insets.NONE, null);
         assertThat(displayWidth, equalTo(cutout.getCutoutPathParserInfo().getDisplayWidth()));
         assertThat(displayHeight, equalTo(cutout.getCutoutPathParserInfo().getDisplayHeight()));
         assertThat(density, equalTo(cutout.getCutoutPathParserInfo().getDensity()));
@@ -360,63 +362,64 @@
     @Test
     public void fromSpec_caches() {
         Insets waterfallInsets = Insets.of(0, 20, 0, 20);
-        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets);
+        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets, null);
         assertThat(
-                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets),
+                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, waterfallInsets, null),
                 sameInstance(cached));
     }
 
     @Test
     public void fromSpec_wontCacheIfSpecChanges() {
-        DisplayCutout cached = fromSpec("L1,0 L1000,1000 L0,1 z", 200, 400, 1f, Insets.NONE);
+        DisplayCutout cached = fromSpec("L1,0 L1000,1000 L0,1 z", 200, 400, 1f, Insets.NONE, null);
         assertThat(
-                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE),
+                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null),
                 not(sameInstance(cached)));
     }
 
     @Test
     public void fromSpec_wontCacheIfScreenWidthChanges() {
-        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 2000, 400, 1f, Insets.NONE);
+        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 2000, 400, 1f, Insets.NONE, null);
         assertThat(
-                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE),
+                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null),
                 not(sameInstance(cached)));
     }
 
     @Test
     public void fromSpec_wontCacheIfScreenHeightChanges() {
-        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 4000, 1f, Insets.NONE);
+        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 4000, 1f, Insets.NONE, null);
         assertThat(
-                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE),
+                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null),
                 not(sameInstance(cached)));
     }
 
     @Test
     public void fromSpec_wontCacheIfDensityChanges() {
-        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE);
+        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE, null);
         assertThat(
-                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE),
+                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 1f, Insets.NONE, null),
                 not(sameInstance(cached)));
     }
 
     @Test
     public void fromSpec_wontCacheIfWaterfallInsetsChange() {
         Insets waterfallInsets = Insets.of(0, 20, 0, 20);
-        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE);
+        DisplayCutout cached = fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, Insets.NONE, null);
         assertThat(
-                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, waterfallInsets),
+                fromSpec("L1,0 L1,1 L0,1 z", 200, 400, 2f, waterfallInsets, null),
                 not(sameInstance(cached)));
     }
 
     @Test
     public void fromSpec_setsSafeInsets_top() {
-        DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z", 200, 400, 2f, Insets.NONE);
+        DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z", 200, 400, 2f,
+                Insets.NONE, null);
         assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 20, 0, 0)));
     }
 
     @Test
     public void fromSpec_setsSafeInsets_top_and_bottom() {
         DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z"
-                + "@bottom M -50,0 v -10,0 h 100 v 20 z", 200, 400, 2f, Insets.NONE);
+                + "@bottom M -50,0 v -10,0 h 100 v 20 z", 200, 400, 2f, Insets.NONE, null);
         assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 20, 0, 10)));
         assertThat(cutout.getBoundingRectsAll(), equalTo(new Rect[]{
                 ZERO_RECT, new Rect(50, 0, 150, 20),
@@ -426,33 +429,35 @@
 
     @Test
     public void fromSpec_setsSafeInsets_waterfallTopBottom() {
-        DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(0, 30, 0, 30));
+        DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(0, 30, 0, 30), null);
         assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 30, 0, 30)));
     }
 
     @Test
     public void fromSpec_setsSafeInsets_waterfallLeftRight() {
-        DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 0, 30, 0));
+        DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 0, 30, 0), null);
         assertThat(cutout.getSafeInsets(), equalTo(new Rect(30, 0, 30, 0)));
     }
 
     @Test
     public void fromSpec_setsSafeInsets_waterfall_allEdges() {
-        DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 30, 30, 30));
+        DisplayCutout cutout = fromSpec("", 200, 400, 2f, Insets.of(30, 30, 30, 30), null);
         assertThat(cutout.getSafeInsets(), equalTo(new Rect(30, 30, 30, 30)));
     }
 
     @Test
     public void fromSpec_setsSafeInsets_cutoutTopBottom_waterfallTopBottom() {
         DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z"
-                + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f, Insets.of(0, 30, 0, 30));
+                + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f,
+                Insets.of(0, 30, 0, 30), null);
         assertThat(cutout.getSafeInsets(), equalTo(new Rect(0, 30, 0, 30)));
     }
 
     @Test
     public void fromSpec_setsSafeInsets_cutoutTopBottom_waterfallLeftRight() {
         DisplayCutout cutout = fromSpec("M -50,0 v 20 h 100 v -20 z"
-                + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f, Insets.of(30, 0, 30, 0));
+                + "@bottom M -50,0 v -20,0 h 100 v 20 z", 200, 400, 2f,
+                    Insets.of(30, 0, 30, 0), null);
         assertThat(cutout.getSafeInsets(), equalTo(new Rect(30, 20, 30, 20)));
     }
 
@@ -568,7 +573,84 @@
         DisplayCutout rotated = cutout.getRotated(displayH, displayW, ROTATION_90, ROTATION_180);
         assertEquals(expected, rotated);
     }
+    @Test
+    public void testGetRotatedCutoutWithOverride_top_rot0() {
+        int displayW = 500, displayH = 1000;
+        final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM,
+                BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP};
+        DisplayCutout expected = new DisplayCutout(Insets.of(20, 100, 20, 0),
+                ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT,
+                Insets.of(20, 0, 20, 0), null, sideOverrides);
+        DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0),
+                ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT,
+                Insets.of(20, 0, 20, 0), null, sideOverrides);
+        DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_0);
+        assertEquals(expected, rotated);
+    }
 
+    @Test
+    public void testGetRotatedCutoutWithOverride_top_rot90() {
+        int displayW = 500, displayH = 1000;
+        final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM,
+                BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP};
+        DisplayCutout expected = new DisplayCutout(Insets.of(0, 20, 0, 75),
+                ZERO_RECT, ZERO_RECT, ZERO_RECT, new Rect(0, displayW - 75, 100, displayW - 50),
+                Insets.of(0, 20, 0, 20), createParserInfo(ROTATION_90), sideOverrides);
+        DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0),
+                ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT,
+                Insets.of(20, 0, 20, 0), null, sideOverrides);
+        DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_90);
+        assertEquals(expected, rotated);
+    }
+
+    @Test
+    public void testGetRotatedCutoutWithOverride_top_rot180() {
+        int displayW = 500, displayH = 1000;
+        final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM,
+                BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP};
+        DisplayCutout expected = new DisplayCutout(Insets.of(20, 0, 20, 100),
+                ZERO_RECT, ZERO_RECT, ZERO_RECT,
+                new Rect(displayW - 75, displayH - 100, displayW - 50, displayH - 0),
+                Insets.of(20, 0, 20, 0), createParserInfo(ROTATION_180), sideOverrides);
+        DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0),
+                ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT,
+                Insets.of(20, 0, 20, 0), null, sideOverrides);
+        DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_180);
+        assertEquals(expected, rotated);
+    }
+
+    @Test
+    public void testGetRotatedCutoutWithOverride_top_rot270() {
+        int displayW = 500, displayH = 1000;
+        final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM,
+                BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP};
+        DisplayCutout expected = new DisplayCutout(Insets.of(0, 75, 0, 20),
+                ZERO_RECT, new Rect(displayH - 100, 50, displayH - 0, 75), ZERO_RECT, ZERO_RECT,
+                Insets.of(0, 20, 0, 20), createParserInfo(ROTATION_270), sideOverrides);
+        DisplayCutout cutout = new DisplayCutout(Insets.of(20, 100, 20, 0),
+                ZERO_RECT, new Rect(50, 0, 75, 100), ZERO_RECT, ZERO_RECT,
+                Insets.of(20, 0, 20, 0), null, sideOverrides);
+        DisplayCutout rotated = cutout.getRotated(displayW, displayH, ROTATION_0, ROTATION_270);
+        assertEquals(expected, rotated);
+    }
+
+    @Test
+    public void testGetRotatedCutoutWithOverride_top_rot90to180() {
+        int displayW = 500, displayH = 1000;
+        final int[] sideOverrides = new int[] {BOUNDS_POSITION_TOP, BOUNDS_POSITION_BOTTOM,
+                BOUNDS_POSITION_BOTTOM, BOUNDS_POSITION_TOP};
+        DisplayCutout expected = new DisplayCutout(Insets.of(20, 0, 20, 100),
+                ZERO_RECT, ZERO_RECT, ZERO_RECT,
+                new Rect(displayW - 75, displayH - 100, displayW - 50, displayH - 0),
+                Insets.of(20, 0, 20, 0), createParserInfo(ROTATION_180),
+                sideOverrides);
+        DisplayCutout cutout = new DisplayCutout(Insets.of(0, 20, 0, 75),
+                ZERO_RECT, ZERO_RECT, ZERO_RECT, new Rect(0, displayW - 75, 100, displayW - 50),
+                Insets.of(0, 20, 0, 20), null, sideOverrides);
+        // starting from 90, so the start displayW/H are swapped:
+        DisplayCutout rotated = cutout.getRotated(displayH, displayW, ROTATION_90, ROTATION_180);
+        assertEquals(expected, rotated);
+    }
     private static DisplayCutout createCutoutTop() {
         return createCutoutWithInsets(0, 100, 0, 0);
     }