Always contain window frames in window bounds

Previously, if the window bounds and the safe area of display cutout
don't intersect, we might get invalid window frames.

This CL uses a new method to intersect rectangles which makes sure the
result frame is contained by the window bounds.

Fix: 367461591
Flag: EXEMPT bugfix
Test: atest WindowLayoutTests
Change-Id: I63d28e5dcdcea9f4206f5026c00905a5cb413bd3
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index dda3993..d5ccca9 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -157,10 +157,10 @@
             // which prevents overlap with the DisplayCutout.
             if (!attachedInParent && !floatingInScreenWindow) {
                 mTempRect.set(outParentFrame);
-                outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+                intersectOrClamp(outParentFrame, displayCutoutSafeExceptMaybeBars);
                 frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame);
             }
-            outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
+            intersectOrClamp(outDisplayFrame, displayCutoutSafeExceptMaybeBars);
         }
 
         final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
@@ -283,6 +283,19 @@
                 + " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes));
     }
 
+    /**
+     * If both rectangles intersect, set inOutRect to that intersection. Otherwise, clamp inOutRect
+     * to the side (or the corner) that the other rectangle is away from.
+     * Unlike {@link Rect#intersectUnchecked(Rect)}, this method guarantees that the new rectangle
+     * is valid and contained in inOutRect if rectangles involved are valid.
+     */
+    private static void intersectOrClamp(Rect inOutRect, Rect other) {
+        inOutRect.left = Math.min(Math.max(inOutRect.left, other.left), inOutRect.right);
+        inOutRect.top = Math.min(Math.max(inOutRect.top, other.top), inOutRect.bottom);
+        inOutRect.right = Math.max(Math.min(inOutRect.right, other.right), inOutRect.left);
+        inOutRect.bottom = Math.max(Math.min(inOutRect.bottom, other.bottom), inOutRect.top);
+    }
+
     public static void extendFrameByCutout(Rect displayCutoutSafe,
             Rect displayFrame, Rect inOutFrame, Rect tempRect) {
         if (displayCutoutSafe.contains(inOutFrame)) {
diff --git a/core/tests/coretests/src/android/view/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java
index 5cac98d..d4693e6 100644
--- a/core/tests/coretests/src/android/view/WindowLayoutTests.java
+++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java
@@ -413,4 +413,19 @@
         assertInsetByTopBottom(0, 0, mFrames.parentFrame);
         assertInsetByTopBottom(0, 0, mFrames.frame);
     }
+
+    @Test
+    public void windowBoundsOutsideDisplayCutoutSafe() {
+        addDisplayCutout();
+        mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
+        mWindowBounds.set(0, -1000, DISPLAY_WIDTH, 0);
+        computeFrames();
+
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.displayFrame);
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.parentFrame);
+        assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0,
+                mFrames.frame);
+    }
 }