Compatible behavior for non-resizable activity (1/N)

Introduce a fixed screen mode for non-resizable activity. If the
activity has fixed of orientation or aspect ratio, it can keep the
original configuration when the size of container is changed.

The original bounds and screen related attributes are set to the
override configuration. So when the activity meet:
 - Move to another display
 - Display size changed
The activity won't be relaunch or receive unexpected config changes.

The next CL will utilize this information to scale and offset the
windows of the non-resizable activity into the corresponding place
according to different use cases.

Bug: 112288258
Test: atest ActivityRecordTests
Change-Id: I5050f180e5d6c6c05514f0dc79102c0668c75c23
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 4faf910..0251efb 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -75,12 +75,12 @@
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
 import static android.content.pm.ActivityInfo.isFixedOrientationPortrait;
 import static android.content.res.Configuration.EMPTY;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
 import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
 import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET;
 import static android.os.Build.VERSION_CODES.HONEYCOMB;
@@ -2610,10 +2610,6 @@
         }
     }
 
-    int getRequestedOrientation() {
-        return getOrientation();
-    }
-
     void setRequestedOrientation(int requestedOrientation) {
         setOrientation(requestedOrientation, mayFreezeScreenLocked(app));
         mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged(
@@ -2641,7 +2637,7 @@
 
     int getOrientation() {
         if (mAppWindowToken == null) {
-            return SCREEN_ORIENTATION_UNSPECIFIED;
+            return info.screenOrientation;
         }
 
         return mAppWindowToken.getOrientationIgnoreVisibility();
@@ -2677,25 +2673,92 @@
         mLastReportedConfiguration.setConfiguration(global, override);
     }
 
+    /**
+     * Get the configuration orientation by the requested screen orientation
+     * ({@link ActivityInfo.ScreenOrientation}) of this activity.
+     *
+     * @return orientation in ({@link #ORIENTATION_LANDSCAPE}, {@link #ORIENTATION_PORTRAIT},
+     *         {@link #ORIENTATION_UNDEFINED}).
+     */
+    int getRequestedConfigurationOrientation() {
+        final int screenOrientation = getOrientation();
+        if (screenOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
+            // NOSENSOR means the display's "natural" orientation, so return that.
+            final ActivityDisplay display = getDisplay();
+            if (display != null && display.mDisplayContent != null) {
+                return display.mDisplayContent.getNaturalOrientation();
+            }
+        } else if (screenOrientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED) {
+            // LOCKED means the activity's orientation remains unchanged, so return existing value.
+            return getConfiguration().orientation;
+        } else if (isFixedOrientationLandscape(screenOrientation)) {
+            return ORIENTATION_LANDSCAPE;
+        } else if (isFixedOrientationPortrait(screenOrientation)) {
+            return ORIENTATION_PORTRAIT;
+        }
+        return ORIENTATION_UNDEFINED;
+    }
+
+    /**
+     * Indicates the activity will keep the bounds and screen configuration when it was first
+     * launched, no matter how its parent changes.
+     *
+     * @return {@code true} if this activity is declared as non-resizable and fixed orientation or
+     *         aspect ratio.
+     */
+    private boolean inSizeCompatMode() {
+        return !isResizeable() && (info.isFixedOrientation() || info.hasFixedAspectRatio())
+                // The configuration of non-standard type should be enforced by system.
+                && isActivityTypeStandard()
+                && !mAtmService.mForceResizableActivities;
+    }
+
     // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer.
     private void updateOverrideConfiguration() {
+        final boolean inSizeCompatMode = inSizeCompatMode();
+        if (inSizeCompatMode) {
+            if (!matchParentBounds()) {
+                // The override configuration is set only once in size compatible mode.
+                return;
+            }
+            if (!hasProcess() && !isConfigurationCompatible(task.getConfiguration())) {
+                // Don't compute when launching in fullscreen and the fixed orientation is not the
+                // current orientation. It is more accurately to compute the override bounds from
+                // the updated configuration after the fixed orientation is applied.
+                return;
+            }
+        }
+
         computeBounds(mTmpBounds);
 
+        if (inSizeCompatMode && mTmpBounds.isEmpty()) {
+            mTmpBounds.set(task.getWindowConfiguration().getBounds());
+        }
         if (mTmpBounds.equals(getRequestedOverrideBounds())) {
+            // The bounds is not changed or the activity is resizable (both the 2 bounds are empty).
             return;
         }
 
-        setBounds(mTmpBounds);
-
-        // Bounds changed...update configuration to match.
-        if (!matchParentBounds()) {
-            mTmpConfig.setTo(getRequestedOverrideConfiguration());
-            task.computeConfigResourceOverrides(mTmpConfig, task.getParent().getConfiguration());
-        } else {
-            mTmpConfig.unset();
+        final Configuration overrideConfig = mTmpConfig;
+        overrideConfig.unset();
+        if (!mTmpBounds.isEmpty()) {
+            overrideConfig.windowConfiguration.setBounds(mTmpBounds);
+            if (inSizeCompatMode) {
+                // Ensure the screen related fields are set. It is used to prevent activity relaunch
+                // when moving between displays. For screenWidthDp and screenWidthDp, because they
+                // are relative to bounds and density, they will be calculated in
+                // {@link TaskRecord#computeConfigResourceOverrides} and the result will also be
+                // relatively fixed.
+                final Configuration srcConfig = task.getConfiguration();
+                overrideConfig.colorMode = srcConfig.colorMode;
+                overrideConfig.densityDpi = srcConfig.densityDpi;
+                overrideConfig.screenLayout = srcConfig.screenLayout;
+                // 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 = srcConfig.smallestScreenWidthDp;
+            }
         }
-
-        onRequestedOverrideConfigurationChanged(mTmpConfig);
+        onRequestedOverrideConfigurationChanged(overrideConfig);
     }
 
     @Override
@@ -2707,6 +2770,86 @@
         // layout traversals.
         mConfigurationSeq = Math.max(++mConfigurationSeq, 1);
         getResolvedOverrideConfiguration().seq = mConfigurationSeq;
+
+        if (matchParentBounds()) {
+            return;
+        }
+
+        final Configuration resolvedConfig = getResolvedOverrideConfiguration();
+        if (!inSizeCompatMode()) {
+            computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
+                    ORIENTATION_UNDEFINED, true /* insideParentBounds */);
+            return;
+        }
+
+        final Configuration displayConfig = getDisplay().getConfiguration();
+        int orientation = getConfiguration().orientation;
+        if (orientation != displayConfig.orientation && isConfigurationCompatible(displayConfig)) {
+            // The activity is compatible to apply the orientation change or it requests different
+            // fixed orientation.
+            orientation = displayConfig.orientation;
+        } else {
+            if (resolvedConfig.windowConfiguration.getAppBounds() != null) {
+                // Keep the computed resolved override configuration.
+                return;
+            }
+            final int requestedOrientation = getRequestedConfigurationOrientation();
+            if (requestedOrientation != ORIENTATION_UNDEFINED) {
+                orientation = requestedOrientation;
+            }
+        }
+
+        // Adjust the bounds to match the current orientation.
+        if (orientation != ORIENTATION_UNDEFINED) {
+            final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
+            final int longSide = Math.max(resolvedBounds.height(), resolvedBounds.width());
+            final int shortSide = Math.min(resolvedBounds.height(), resolvedBounds.width());
+            final boolean toBeLandscape = orientation == ORIENTATION_LANDSCAPE;
+            final int width = toBeLandscape ? longSide : shortSide;
+            final int height = toBeLandscape ? shortSide : longSide;
+            // Assume the bounds is always started from zero because the size may be bigger than its
+            // parent (task ~ display). The actual letterboxing will be done by surface offset.
+            resolvedBounds.set(0, 0, width, height);
+        }
+
+        // In size compatible mode, activity is allowed to have larger bounds than its parent.
+        computeConfigResourceOverrides(resolvedConfig, newParentConfiguration, orientation,
+                false /* insideParentBounds */);
+    }
+
+    private void computeConfigResourceOverrides(Configuration inOutConfig,
+            Configuration parentConfig, int orientation, boolean insideParentBounds) {
+        // Set the real orientation or undefined value to ensure the output orientation won't be the
+        // old value. Also reset app bounds so it will be updated according to bounds.
+        inOutConfig.orientation = orientation;
+        final Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+        if (outAppBounds != null) {
+            outAppBounds.setEmpty();
+        }
+
+        // TODO(b/112288258): Remove below calculation because the position information in bounds
+        // will be replaced by the offset of surface.
+        final Rect appBounds = parentConfig.windowConfiguration.getAppBounds();
+        if (appBounds != null) {
+            final Rect outBounds = inOutConfig.windowConfiguration.getBounds();
+            final int activityWidth = outBounds.width();
+            final int navBarPosition = mAtmService.mWindowManager.getNavBarPosition(getDisplayId());
+            if (navBarPosition == NAV_BAR_LEFT) {
+                // Position the activity frame on the opposite side of the nav bar.
+                outBounds.left = appBounds.right - activityWidth;
+                outBounds.right = appBounds.right;
+            } else if (navBarPosition == NAV_BAR_RIGHT) {
+                // Position the activity frame on the opposite side of the nav bar.
+                outBounds.left = 0;
+                outBounds.right = activityWidth + appBounds.left;
+            } else if (appBounds.width() > activityWidth) {
+                // Horizontally center the frame.
+                outBounds.left = appBounds.left + (appBounds.width() - activityWidth) / 2;
+                outBounds.right = outBounds.left + activityWidth;
+            }
+        }
+
+        task.computeConfigResourceOverrides(inOutConfig, parentConfig, insideParentBounds);
     }
 
     @Override
@@ -2739,8 +2882,7 @@
 
     /** Returns true if the configuration is compatible with this activity. */
     boolean isConfigurationCompatible(Configuration config) {
-        final int orientation = mAppWindowToken != null
-                ? getOrientation() : info.screenOrientation;
+        final int orientation = getOrientation();
         if (isFixedOrientationPortrait(orientation)
                 && config.orientation != ORIENTATION_PORTRAIT) {
             return false;
@@ -2822,21 +2964,6 @@
         // away later in StackWindowController.adjustConfigurationForBounds(). Otherwise, the app
         // bounds would end up too small.
         outBounds.set(0, 0, activityWidth + appBounds.left, activityHeight + appBounds.top);
-
-        final int navBarPosition = mAtmService.mWindowManager.getNavBarPosition(getDisplayId());
-        if (navBarPosition == NAV_BAR_LEFT) {
-            // Position the activity frame on the opposite side of the nav bar.
-            outBounds.left = appBounds.right - activityWidth;
-            outBounds.right = appBounds.right;
-        } else if (navBarPosition == NAV_BAR_RIGHT) {
-            // Position the activity frame on the opposite side of the nav bar.
-            outBounds.left = 0;
-            outBounds.right = activityWidth + appBounds.left;
-        } else {
-            // Horizontally center the frame.
-            outBounds.left = appBounds.left + (containingAppWidth - activityWidth) / 2;
-            outBounds.right = outBounds.left + activityWidth;
-        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 875fc4e..ce01b82 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1720,7 +1720,7 @@
             if (r == null) {
                 return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
             }
-            return r.getRequestedOrientation();
+            return r.getOrientation();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index 69f0012..fe42de1 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -1288,22 +1288,7 @@
                 || top == null) {
             return getRequestedOverrideConfiguration().orientation;
         }
-        int screenOrientation = top.getOrientation();
-        if (screenOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
-            // NOSENSOR means the display's "natural" orientation, so return that.
-            ActivityDisplay display = mStack != null ? mStack.getDisplay() : null;
-            if (display != null && display.mDisplayContent != null) {
-                return mStack.getDisplay().mDisplayContent.getNaturalOrientation();
-            }
-        } else if (screenOrientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED) {
-            // LOCKED means the activity's orientation remains unchanged, so return existing value.
-            return top.getConfiguration().orientation;
-        } else if (ActivityInfo.isFixedOrientationLandscape(screenOrientation)) {
-            return ORIENTATION_LANDSCAPE;
-        } else if (ActivityInfo.isFixedOrientationPortrait(screenOrientation)) {
-            return ORIENTATION_PORTRAIT;
-        }
-        return ORIENTATION_UNDEFINED;
+        return top.getRequestedConfigurationOrientation();
     }
 
     /**
@@ -2084,6 +2069,11 @@
         return Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
     }
 
+    void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
+            @NonNull Configuration parentConfig) {
+        computeConfigResourceOverrides(inOutConfig, parentConfig, true /* insideParentBounds */);
+    }
+
     /**
      * Calculates configuration values used by the client to get resources. This should be run
      * using app-facing bounds (bounds unmodified by animations or transient interactions).
@@ -2093,7 +2083,7 @@
      * just be inherited from the parent configuration.
      **/
     void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
-            @NonNull Configuration parentConfig) {
+            @NonNull Configuration parentConfig, boolean insideParentBounds) {
         int windowingMode = inOutConfig.windowConfiguration.getWindowingMode();
         if (windowingMode == WINDOWING_MODE_UNDEFINED) {
             windowingMode = parentConfig.windowConfiguration.getWindowingMode();
@@ -2111,7 +2101,7 @@
             inOutConfig.windowConfiguration.setAppBounds(bounds);
             outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
         }
-        if (windowingMode != WINDOWING_MODE_FREEFORM) {
+        if (insideParentBounds && windowingMode != WINDOWING_MODE_FREEFORM) {
             final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
             if (parentAppBounds != null && !parentAppBounds.isEmpty()) {
                 outAppBounds.intersect(parentAppBounds);
@@ -2120,7 +2110,7 @@
 
         if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED
                 || inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
-            if (mStack != null) {
+            if (insideParentBounds && mStack != null) {
                 final DisplayInfo di = new DisplayInfo();
                 mStack.getDisplay().mDisplay.getDisplayInfo(di);
 
@@ -2135,12 +2125,16 @@
             }
 
             if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
-                inOutConfig.screenWidthDp = Math.min((int) (mTmpStableBounds.width() / density),
-                        parentConfig.screenWidthDp);
+                final int overrideScreenWidthDp = (int) (mTmpStableBounds.width() / density);
+                inOutConfig.screenWidthDp = insideParentBounds
+                        ? Math.min(overrideScreenWidthDp, parentConfig.screenWidthDp)
+                        : overrideScreenWidthDp;
             }
             if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
-                inOutConfig.screenHeightDp = Math.min((int) (mTmpStableBounds.height() / density),
-                        parentConfig.screenHeightDp);
+                final int overrideScreenHeightDp = (int) (mTmpStableBounds.height() / density);
+                inOutConfig.screenHeightDp = insideParentBounds
+                        ? Math.min(overrideScreenHeightDp, parentConfig.screenWidthDp)
+                        : overrideScreenHeightDp;
             }
 
             if (inOutConfig.smallestScreenWidthDp
@@ -2162,7 +2156,7 @@
 
         if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
             inOutConfig.orientation = (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
-                    ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
+                    ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
         }
         if (inOutConfig.screenLayout == Configuration.SCREENLAYOUT_UNDEFINED) {
             // For calculating screen layout, we need to use the non-decor inset screen area for the
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 4073ff1..8c36905 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -183,11 +183,14 @@
                 .thenReturn(navBarPosition);
         mTask.getConfiguration().windowConfiguration.setAppBounds(taskBounds);
         mActivity.info.maxAspectRatio = aspectRatio;
-        mActivity.ensureActivityConfiguration(
-                0 /* globalChanges */, false /* preserveWindow */);
+        ensureActivityConfiguration();
         assertEquals(expectedActivityBounds, mActivity.getBounds());
     }
 
+    private void ensureActivityConfiguration() {
+        mActivity.ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
+    }
+
     @Test
     public void testCanBeLaunchedOnDisplay() {
         mService.mSupportsMultiWindow = true;
@@ -281,7 +284,7 @@
 
         mActivity.mRelaunchReason = ActivityTaskManagerService.RELAUNCH_REASON_NONE;
 
-        mActivity.ensureActivityConfiguration(0, false, false);
+        ensureActivityConfiguration();
 
         assertEquals(ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE,
                 mActivity.mRelaunchReason);
@@ -305,7 +308,7 @@
 
         mActivity.mRelaunchReason = ActivityTaskManagerService.RELAUNCH_REASON_NONE;
 
-        mActivity.ensureActivityConfiguration(0, false, false);
+        ensureActivityConfiguration();
 
         assertEquals(ActivityTaskManagerService.RELAUNCH_REASON_FREE_RESIZE,
                 mActivity.mRelaunchReason);
@@ -327,7 +330,7 @@
         mActivity.mRelaunchReason =
                 ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
 
-        mActivity.ensureActivityConfiguration(0, false, false);
+        ensureActivityConfiguration();
 
         assertEquals(ActivityTaskManagerService.RELAUNCH_REASON_NONE,
                 mActivity.mRelaunchReason);
@@ -433,4 +436,46 @@
             stack.getDisplay().removeChild(stack);
         }
     }
+
+    @Test
+    public void testFixedScreenConfigurationWhenMovingToDisplay() {
+        // Initialize different bounds on a new display.
+        final ActivityDisplay newDisplay = addNewActivityDisplayAt(ActivityDisplay.POSITION_TOP);
+        newDisplay.setBounds(0, 0, 1000, 2000);
+        newDisplay.getConfiguration().densityDpi = 300;
+
+        mTask.getWindowConfiguration().setAppBounds(mStack.getDisplay().getBounds());
+        mTask.getConfiguration().densityDpi = 200;
+        when(mActivity.mAppWindowToken.getOrientationIgnoreVisibility()).thenReturn(
+                ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+        mActivity.info.maxAspectRatio = 1.5f;
+        ensureActivityConfiguration();
+        final Rect originalBounds = new Rect(mActivity.getBounds());
+        final int originalDpi = mActivity.getConfiguration().densityDpi;
+
+        // Move the non-resizable activity to the new display.
+        mStack.reparent(newDisplay, true /* onTop */, false /* displayRemoved */);
+        ensureActivityConfiguration();
+
+        assertEquals(originalBounds, mActivity.getBounds());
+        assertEquals(originalDpi, mActivity.getConfiguration().densityDpi);
+    }
+
+    @Test
+    public void testFixedScreenBoundsWhenDisplaySizeChanged() {
+        when(mActivity.mAppWindowToken.getOrientationIgnoreVisibility()).thenReturn(
+                ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+        mTask.getWindowConfiguration().setAppBounds(mStack.getDisplay().getBounds());
+        mActivity.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+        ensureActivityConfiguration();
+        final Rect originalBounds = new Rect(mActivity.getBounds());
+
+        // Change the size of current display.
+        mStack.getDisplay().setBounds(0, 0, 1000, 2000);
+        ensureActivityConfiguration();
+
+        assertEquals(originalBounds, mActivity.getBounds());
+    }
 }