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);
}