Ensure consistency of screenHeightDp for non-resizable activity

Originally the height of status bar (or other stable insets) is not
excluded from screenHeightDp for size compatibility mode activity
because the previous display where the activity resides may be removed
or changed then there is no available and consistent display bounds to
calculate the screen configuration.

Now we precompute and store the display insets in each rotation, so the
activity always has the original display insets to compute consistent
screen configuration.

Test: atest ActivityRecordTests
Test: atest TaskRecordTests#testComputeConfigResourceOverrides
Bug: 112288258

Change-Id: I5c769f8080fc5ae4eba966d2fa42cb650254074d
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e2253e7..6cf36d6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -88,6 +88,8 @@
 import static android.os.Build.VERSION_CODES.O;
 import static android.os.Process.SYSTEM_UID;
 import static android.view.Display.INVALID_DISPLAY;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 
 import static com.android.server.am.ActivityRecordProto.CONFIGURATION_CONTAINER;
 import static com.android.server.am.ActivityRecordProto.FRONT_OF_TASK;
@@ -195,6 +197,7 @@
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.view.AppTransitionAnimationSpec;
+import android.view.DisplayCutout;
 import android.view.IAppTransitionAnimationSpecsFuture;
 import android.view.IApplicationToken;
 import android.view.RemoteAnimationDefinition;
@@ -382,6 +385,12 @@
     private int[] mHorizontalSizeConfigurations;
     private int[] mSmallestSizeConfigurations;
 
+    /**
+     * The precomputed display insets for resolving configuration. It will be non-null if
+     * {@link #shouldUseSizeCompatMode} returns {@code true}.
+     */
+    private CompatDisplayInsets mCompatDisplayInsets;
+
     boolean pendingVoiceInteractionStart;   // Waiting for activity-invoked voice session
     IVoiceInteractionSession voiceSession;  // Voice interaction session for this activity
 
@@ -2833,6 +2842,11 @@
                 // The smallest screen width is the short side of screen bounds. Because the bounds
                 // and density won't be changed, smallestScreenWidthDp is also fixed.
                 overrideConfig.smallestScreenWidthDp = parentConfig.smallestScreenWidthDp;
+
+                final ActivityDisplay display = getDisplay();
+                if (display != null && display.mDisplayContent != null) {
+                    mCompatDisplayInsets = new CompatDisplayInsets(display.mDisplayContent);
+                }
             }
         }
         onRequestedOverrideConfigurationChanged(overrideConfig);
@@ -2849,7 +2863,7 @@
             super.resolveOverrideConfiguration(newParentConfiguration);
             if (hasOverrideBounds) {
                 task.computeConfigResourceOverrides(getResolvedOverrideConfiguration(),
-                        newParentConfiguration, true /* insideParentBounds */);
+                        newParentConfiguration);
             }
         }
 
@@ -2922,9 +2936,8 @@
             resolvedBounds.right -= resolvedAppBounds.left;
         }
 
-        // In size compatibility mode, activity is allowed to have larger bounds than its parent.
         task.computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
-                false /* insideParentBounds */);
+                mCompatDisplayInsets);
         // Use parent orientation if it cannot be decided by bounds, so the activity can fit inside
         // the parent bounds appropriately.
         if (resolvedConfig.screenWidthDp == resolvedConfig.screenHeightDp) {
@@ -3450,6 +3463,7 @@
         // configuration.
         getRequestedOverrideConfiguration().setToDefaults();
         getResolvedOverrideConfiguration().setToDefaults();
+        mCompatDisplayInsets = null;
         if (visible) {
             // Configuration will be ensured when becoming visible, so if it is already visible,
             // then the manual update is needed.
@@ -3796,4 +3810,46 @@
         writeToProto(proto);
         proto.end(token);
     }
+
+    /**
+     * The precomputed insets of the display in each rotation. This is used to make the size
+     * compatibility mode activity compute the configuration without relying on its current display.
+     */
+    static class CompatDisplayInsets {
+        final int mDisplayWidth;
+        final int mDisplayHeight;
+
+        /** The nonDecorInsets for each rotation. Includes the navigation bar and cutout insets. */
+        final Rect[] mNonDecorInsets = new Rect[4];
+        /**
+         * The stableInsets for each rotation. Includes the status bar inset and the
+         * nonDecorInsets. It is used to compute {@link Configuration#screenWidthDp} and
+         * {@link Configuration#screenHeightDp}.
+         */
+        final Rect[] mStableInsets = new Rect[4];
+
+        CompatDisplayInsets(DisplayContent display) {
+            mDisplayWidth = display.mBaseDisplayWidth;
+            mDisplayHeight = display.mBaseDisplayHeight;
+            final DisplayPolicy policy = display.getDisplayPolicy();
+            final DisplayCutout cutout = display.getDisplayInfo().displayCutout;
+            for (int rotation = 0; rotation < 4; rotation++) {
+                mNonDecorInsets[rotation] = new Rect();
+                mStableInsets[rotation] = new Rect();
+                final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+                final int dw = rotated ? mDisplayHeight : mDisplayWidth;
+                final int dh = rotated ? mDisplayWidth : mDisplayHeight;
+                policy.getNonDecorInsetsLw(rotation, dw, dh, cutout, mNonDecorInsets[rotation]);
+                mStableInsets[rotation].set(mNonDecorInsets[rotation]);
+                policy.convertNonDecorInsetsToStableInsets(mStableInsets[rotation], rotation);
+            }
+        }
+
+        void getDisplayBounds(Rect outBounds, int rotation) {
+            final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+            final int dw = rotated ? mDisplayHeight : mDisplayWidth;
+            final int dh = rotated ? mDisplayWidth : mDisplayHeight;
+            outBounds.set(0, 0, dw, dh);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 6ae7720..1934e25 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2775,6 +2775,16 @@
     }
 
     /**
+     * Calculates the stable insets if we already have the non-decor insets.
+     *
+     * @param inOutInsets The known non-decor insets. It will be modified to stable insets.
+     * @param rotation The current display rotation.
+     */
+    void convertNonDecorInsetsToStableInsets(Rect inOutInsets, int rotation) {
+        inOutInsets.top = Math.max(inOutInsets.top, mStatusBarHeightForRotation[rotation]);
+    }
+
+    /**
      * Calculates the stable insets without running a layout.
      *
      * @param displayRotation the current display rotation
@@ -2789,7 +2799,7 @@
 
         // Navigation bar and status bar.
         getNonDecorInsetsLw(displayRotation, displayWidth, displayHeight, displayCutout, outInsets);
-        outInsets.top = Math.max(outInsets.top, mStatusBarHeightForRotation[displayRotation]);
+        convertNonDecorInsetsToStableInsets(outInsets, displayRotation);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index 9713bef..15060e1 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -22,6 +22,7 @@
 import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -2048,15 +2049,12 @@
         }
         mTmpBounds.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight);
 
-        policy.getStableInsetsLw(displayInfo.rotation,
-                displayInfo.logicalWidth, displayInfo.logicalHeight, displayInfo.displayCutout,
-                mTmpInsets);
-        intersectWithInsetsIfFits(outStableBounds, mTmpBounds, mTmpInsets);
-
-        policy.getNonDecorInsetsLw(displayInfo.rotation,
-                displayInfo.logicalWidth, displayInfo.logicalHeight, displayInfo.displayCutout,
-                mTmpInsets);
+        policy.getNonDecorInsetsLw(displayInfo.rotation, displayInfo.logicalWidth,
+                displayInfo.logicalHeight, displayInfo.displayCutout, mTmpInsets);
         intersectWithInsetsIfFits(outNonDecorBounds, mTmpBounds, mTmpInsets);
+
+        policy.convertNonDecorInsetsToStableInsets(mTmpInsets, displayInfo.rotation);
+        intersectWithInsetsIfFits(outStableBounds, mTmpBounds, mTmpInsets);
     }
 
     /**
@@ -2073,7 +2071,7 @@
 
     void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
             @NonNull Configuration parentConfig) {
-        computeConfigResourceOverrides(inOutConfig, parentConfig, true /* insideParentBounds */);
+        computeConfigResourceOverrides(inOutConfig, parentConfig, null /* compatInsets */);
     }
 
     /**
@@ -2085,7 +2083,8 @@
      * just be inherited from the parent configuration.
      **/
     void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
-            @NonNull Configuration parentConfig, boolean insideParentBounds) {
+            @NonNull Configuration parentConfig,
+            @Nullable ActivityRecord.CompatDisplayInsets compatInsets) {
         int windowingMode = inOutConfig.windowConfiguration.getWindowingMode();
         if (windowingMode == WINDOWING_MODE_UNDEFINED) {
             windowingMode = parentConfig.windowConfiguration.getWindowingMode();
@@ -2103,6 +2102,9 @@
             inOutConfig.windowConfiguration.setAppBounds(bounds);
             outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
         }
+        // Non-null compatibility insets means the activity prefers to keep its original size, so
+        // the out bounds doesn't need to be restricted by the parent.
+        final boolean insideParentBounds = compatInsets == null;
         if (insideParentBounds && windowingMode != WINDOWING_MODE_FREEFORM) {
             final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
             if (parentAppBounds != null && !parentAppBounds.isEmpty()) {
@@ -2125,6 +2127,17 @@
                 // Set to app bounds because it excludes decor insets.
                 mTmpNonDecorBounds.set(outAppBounds);
                 mTmpStableBounds.set(outAppBounds);
+
+                // Apply the given non-decor and stable insets to calculate the corresponding bounds
+                // for screen size of configuration.
+                final int rotation = parentConfig.windowConfiguration.getRotation();
+                if (rotation != ROTATION_UNDEFINED && compatInsets != null) {
+                    compatInsets.getDisplayBounds(mTmpBounds, rotation);
+                    intersectWithInsetsIfFits(mTmpNonDecorBounds, mTmpBounds,
+                            compatInsets.mNonDecorInsets[rotation]);
+                    intersectWithInsetsIfFits(mTmpStableBounds, mTmpBounds,
+                            compatInsets.mStableInsets[rotation]);
+                }
             }
 
             if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {