Reset configuration of invisible size-compat activity if display changes

Assume a size compatibility mode activity is moved to another display,
and then it is put in background. In this case, if the size or density
of display is changed, the existing override configuration (by previous
display) will be reset and also kill its process if the process state
is not important to user (also sync the state criteria to pre-N app).
So when the activity becomes the foreground again, it will have a new
process with the latest configuration.

Bug: 112288258
Fixes: 125587591
Test: atest ActivityRecordTests#testSizeCompatMode_ResetNonVisibleActivity

Change-Id: I14c90b6a7bb00eeb66d2b9f2d8e85ccfa9ef4e51
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 6ff1023..d1faa71 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+import static android.app.ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
 import static android.app.ActivityManager.TaskDescription.ATTR_TASKDESCRIPTION_PREFIX;
 import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
 import static android.app.ActivityOptions.ANIM_CUSTOM;
@@ -2726,12 +2727,27 @@
         }
 
         final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
-        if (parentAppBounds.width() < resolvedAppBounds.width()
-                || parentAppBounds.height() < resolvedAppBounds.height()) {
+        final int appWidth = resolvedAppBounds.width();
+        final int appHeight = resolvedAppBounds.height();
+        final int parentAppWidth = parentAppBounds.width();
+        final int parentAppHeight = parentAppBounds.height();
+        if (parentAppWidth < appWidth || parentAppHeight < appHeight) {
             // One side is larger than the parent.
             return true;
         }
 
+        if (info.hasFixedAspectRatio()) {
+            final float aspectRatio = (0.5f + Math.max(appWidth, appHeight))
+                    / Math.min(appWidth, appHeight);
+            final float parentAspectRatio = (0.5f + Math.max(parentAppWidth, parentAppHeight))
+                    / Math.min(parentAppWidth, parentAppHeight);
+            // Check if the parent still has available space in long side.
+            if (aspectRatio < parentAspectRatio
+                    && (aspectRatio < info.maxAspectRatio || info.minAspectRatio > 0)) {
+                return true;
+            }
+        }
+
         final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
         // If the width or height is the same as parent, it is already the best fit of the override
         // bounds, therefore this condition is considered as not size compatibility mode. Here uses
@@ -2930,8 +2946,27 @@
         }
 
         final ActivityDisplay display = getDisplay();
-        if (display != null) {
+        if (display == null) {
+            return;
+        }
+        if (visible) {
+            // It may toggle the UI for user to restart the size compatibility mode activity.
             display.handleActivitySizeCompatModeIfNeeded(this);
+        } else if (shouldUseSizeCompatMode()) {
+            // The override changes can only be obtained from display, because we don't have the
+            // difference of full configuration in each hierarchy.
+            final int displayChanges = display.getLastOverrideConfigurationChanges();
+            final int orientationChanges = CONFIG_WINDOW_CONFIGURATION
+                    | CONFIG_SCREEN_SIZE | CONFIG_ORIENTATION;
+            final boolean hasNonOrienSizeChanged = hasResizeChange(displayChanges)
+                    // Filter out the case of simple orientation change.
+                    && (displayChanges & orientationChanges) != orientationChanges;
+            // For background activity that uses size compatibility mode, if the size or density of
+            // the display is changed, then reset the override configuration and kill the activity's
+            // process if its process state is not important to user.
+            if (hasNonOrienSizeChanged || (displayChanges & ActivityInfo.CONFIG_DENSITY) != 0) {
+                restartProcessIfVisible();
+            }
         }
     }
 
@@ -3398,7 +3433,8 @@
     void restartProcessIfVisible() {
         Slog.i(TAG, "Request to restart process of " + this);
 
-        // Reset the existing override configuration to the latest configuration.
+        // Reset the existing override configuration so it can be updated according to the latest
+        // configuration.
         getRequestedOverrideConfiguration().setToDefaults();
         getResolvedOverrideConfiguration().setToDefaults();
         if (visible) {
@@ -3418,8 +3454,17 @@
             // Kill its process immediately because the activity should be in background.
             // The activity state will be update to {@link #DESTROYED} in
             // {@link ActivityStack#cleanUpActivityLocked} when handling process died.
-            mAtmService.mH.post(() -> mAtmService.mAmInternal.killProcess(
-                    app.mName, app.mUid, "restartActivityProcess"));
+            mAtmService.mH.post(() -> {
+                final WindowProcessController wpc;
+                synchronized (mAtmService.mGlobalLock) {
+                    if (!hasProcess()
+                            || app.getReportedProcState() <= PROCESS_STATE_IMPORTANT_FOREGROUND) {
+                        return;
+                    }
+                    wpc = app;
+                }
+                mAtmService.mAmInternal.killProcess(wpc.mName, wpc.mUid, "resetConfig");
+            });
             return;
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 118eb5b..b3b4cbe 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5209,7 +5209,7 @@
                 // the ATMS lock held.
                 final Message msg = PooledLambda.obtainMessage(
                         ActivityManagerInternal::killAllBackgroundProcessesExcept, mAmInternal,
-                        N, ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
+                        N, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
                 mH.sendMessage(msg);
             }
         }
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 5c528c7..3886ae1 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -77,6 +77,9 @@
      */
     private Configuration mFullConfiguration = new Configuration();
 
+    /** The bit mask of the last override fields of full configuration. */
+    private int mLastOverrideConfigurationChanges;
+
     /**
      * Contains merged override configuration settings from the top of the hierarchy down to this
      * particular instance. It is different from {@link #mFullConfiguration} because it starts from
@@ -108,6 +111,11 @@
         return mFullConfiguration;
     }
 
+    /** Returns the last changes from applying override configuration. */
+    int getLastOverrideConfigurationChanges() {
+        return mLastOverrideConfigurationChanges;
+    }
+
     /**
      * Notify that parent config changed and we need to update full configuration.
      * @see #mFullConfiguration
@@ -116,7 +124,8 @@
         mTmpConfig.setTo(mResolvedOverrideConfiguration);
         resolveOverrideConfiguration(newParentConfig);
         mFullConfiguration.setTo(newParentConfig);
-        mFullConfiguration.updateFrom(mResolvedOverrideConfiguration);
+        mLastOverrideConfigurationChanges =
+                mFullConfiguration.updateFrom(mResolvedOverrideConfiguration);
         if (!mTmpConfig.equals(mResolvedOverrideConfiguration)) {
             onMergedOverrideConfigurationChanged();
             // This depends on the assumption that change-listeners don't do
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 2a0dc27..d580557 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -43,7 +43,10 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
 
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
 import android.app.ActivityOptions;
 import android.app.servertransaction.ActivityConfigurationChangeItem;
 import android.app.servertransaction.ClientTransaction;
@@ -61,6 +64,8 @@
 import org.junit.Test;
 import org.mockito.invocation.InvocationOnMock;
 
+import java.util.concurrent.TimeUnit;
+
 /**
  * Tests for the {@link ActivityRecord} class.
  *
@@ -175,13 +180,11 @@
     @Test
     public void testRestartProcessIfVisible() {
         doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
-        mTask.getWindowConfiguration().setAppBounds(0, 0, 500, 1000);
         mActivity.visible = true;
         mActivity.haveState = false;
-        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
-        mActivity.info.maxAspectRatio = 1.5f;
         mActivity.setState(ActivityStack.ActivityState.RESUMED, "testRestart");
-        ensureActivityConfiguration();
+        prepareFixedAspectRatioUnresizableActivity();
+
         final Rect originalOverrideBounds = new Rect(mActivity.getBounds());
         mTask.getWindowConfiguration().setAppBounds(0, 0, 600, 1200);
         // The visible activity should recompute configuration according to the last parent bounds.
@@ -459,13 +462,9 @@
         newDisplay.getWindowConfiguration().setAppBounds(new Rect(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();
+        prepareFixedAspectRatioUnresizableActivity();
+
         final Rect originalBounds = new Rect(mActivity.getBounds());
         final int originalDpi = mActivity.getConfiguration().densityDpi;
 
@@ -475,14 +474,16 @@
 
         assertEquals(originalBounds, mActivity.getBounds());
         assertEquals(originalDpi, mActivity.getConfiguration().densityDpi);
+        assertTrue(mActivity.inSizeCompatMode());
     }
 
     @Test
     public void testSizeCompatMode_FixedScreenBoundsWhenDisplaySizeChanged() {
         when(mActivity.mAppWindowToken.getOrientationIgnoreVisibility()).thenReturn(
-                ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+                ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         mTask.getWindowConfiguration().setAppBounds(mStack.getDisplay().getBounds());
-        mActivity.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        mTask.getConfiguration().orientation = Configuration.ORIENTATION_PORTRAIT;
+        mActivity.info.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
         mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
         ensureActivityConfiguration();
         final Rect originalBounds = new Rect(mActivity.getBounds());
@@ -492,6 +493,7 @@
         ensureActivityConfiguration();
 
         assertEquals(originalBounds, mActivity.getBounds());
+        assertTrue(mActivity.inSizeCompatMode());
     }
 
     @Test
@@ -500,10 +502,7 @@
                 | Configuration.SCREENLAYOUT_SIZE_NORMAL;
         mTask.getConfiguration().screenLayout = fixedScreenLayout
                 | Configuration.SCREENLAYOUT_LAYOUTDIR_LTR;
-        mTask.getWindowConfiguration().setAppBounds(mStack.getDisplay().getBounds());
-        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
-        mActivity.info.maxAspectRatio = 1.5f;
-        ensureActivityConfiguration();
+        prepareFixedAspectRatioUnresizableActivity();
 
         // The initial configuration should inherit from parent.
         assertEquals(mTask.getConfiguration().screenLayout,
@@ -517,4 +516,46 @@
         assertEquals(fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_RTL,
                 mActivity.getConfiguration().screenLayout);
     }
+
+    @Test
+    public void testSizeCompatMode_ResetNonVisibleActivity() {
+        final ActivityDisplay display = mStack.getDisplay();
+        spyOn(display);
+
+        prepareFixedAspectRatioUnresizableActivity();
+        mActivity.setState(STOPPED, "testSizeCompatMode");
+        mActivity.visible = false;
+        mActivity.app.setReportedProcState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY);
+        // Make the parent bounds to be different so the activity is in size compatibility mode.
+        mTask.getWindowConfiguration().setAppBounds(new Rect(0, 0, 600, 1200));
+
+        // Simulate the display changes orientation.
+        doReturn(ActivityInfo.CONFIG_SCREEN_SIZE | ActivityInfo.CONFIG_ORIENTATION
+                | ActivityInfo.CONFIG_WINDOW_CONFIGURATION)
+                        .when(display).getLastOverrideConfigurationChanges();
+        mActivity.onConfigurationChanged(mTask.getConfiguration());
+        // The override configuration should not change so it is still in size compatibility mode.
+        assertTrue(mActivity.inSizeCompatMode());
+
+        // Simulate the display changes density.
+        doReturn(ActivityInfo.CONFIG_DENSITY).when(display).getLastOverrideConfigurationChanges();
+        mService.mAmInternal = mock(ActivityManagerInternal.class);
+        mActivity.onConfigurationChanged(mTask.getConfiguration());
+        // The override configuration should be reset and the activity's process will be killed.
+        assertFalse(mActivity.inSizeCompatMode());
+        verify(mActivity).restartProcessIfVisible();
+        mService.mH.runWithScissors(() -> { }, TimeUnit.SECONDS.toMillis(3));
+        verify(mService.mAmInternal).killProcess(
+                eq(mActivity.app.mName), eq(mActivity.app.mUid), anyString());
+    }
+
+    /** Setup {@link #mActivity} as a size-compat-mode-able activity without fixed orientation. */
+    private void prepareFixedAspectRatioUnresizableActivity() {
+        when(mActivity.mAppWindowToken.getOrientationIgnoreVisibility()).thenReturn(
+                ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+        mTask.getWindowConfiguration().setAppBounds(mStack.getDisplay().getBounds());
+        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+        mActivity.info.maxAspectRatio = 1.5f;
+        ensureActivityConfiguration();
+    }
 }