Verify onConfigurationChanged behavior for IME

Bug: 149463653
Test: atest InputmethodServiceTest MultiDisplaySystemDecorationTests

Change-Id: Ic29767d1d0d75d65e7b7cc71cce48cc29b074d3a
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java
index 8dbc12c..a4148f0 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplaySystemDecorationTests.java
@@ -396,6 +396,7 @@
         final ImeEventStream stream = mockImeSession.openEventStream();
         imeTestActivitySession.runOnMainSyncAndWait(
                 imeTestActivitySession.getActivity()::showSoftInput);
+        ImeEventStream configChangeVerifyStream = stream.copy();
         waitOrderedImeEventsThenAssertImeShown(stream, newDisplay.mId,
                 editorMatcher("onStartInput",
                         imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
@@ -403,6 +404,8 @@
 
         // Assert the configuration of the IME window is the same as the configuration of the
         // virtual display.
+        waitAndAssertImeConfigurationChanged(configChangeVerifyStream);
+        configChangeVerifyStream = clearOnConfigurationChangedFromStream(configChangeVerifyStream);
         assertImeWindowAndDisplayConfiguration(mWmState.getImeWindowState(), newDisplay);
 
         // Launch another activity on the default display.
@@ -419,6 +422,7 @@
 
         // Assert the configuration of the IME window is the same as the configuration of the
         // default display.
+        waitAndAssertImeConfigurationChanged(configChangeVerifyStream);
         assertImeWindowAndDisplayConfiguration(mWmState.getImeWindowState(),
                 mWmState.getDisplay(DEFAULT_DISPLAY));
     }
@@ -492,6 +496,7 @@
         tapOnDisplayCenter(defDisplay.mId);
         imeTestActivitySession.runOnMainSyncAndWait(
                 imeTestActivitySession.getActivity()::showSoftInput);
+        ImeEventStream configChangeVerifyStream = stream.copy();
         waitOrderedImeEventsThenAssertImeShown(stream, defDisplay.mId,
                 editorMatcher("onStartInput",
                         imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
@@ -507,6 +512,9 @@
                         imeTestActivitySession2.getActivity().mEditText.getPrivateImeOptions()),
                 event -> "showSoftInput".equals(event.getEventName()));
 
+        waitAndAssertImeConfigurationChanged(configChangeVerifyStream);
+        configChangeVerifyStream = clearOnConfigurationChangedFromStream(configChangeVerifyStream);
+
         // Tap default display again to make sure the IME window will come back.
         tapOnDisplayCenter(defDisplay.mId);
         imeTestActivitySession.runOnMainSyncAndWait(
@@ -515,6 +523,8 @@
                 editorMatcher("onStartInput",
                         imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
                 event -> "showSoftInput".equals(event.getEventName()));
+
+        waitAndAssertImeConfigurationChanged(configChangeVerifyStream);
     }
 
     /**
@@ -746,6 +756,73 @@
                 event -> "showSoftInput".equals(event.getEventName()));
     }
 
+    @Test
+    public void testNoConfigurationChangedWhenSwitchBetweenTwoIdenticalDisplays() throws Exception {
+        // If config_perDisplayFocusEnabled, the focus will not move even if touching on
+        // the Activity in the different display.
+        assumeFalse(perDisplayFocusEnabled());
+        assumeTrue(MSG_NO_MOCK_IME, supportsInstallableIme());
+
+        // Create two displays with the same display metrics
+        final List<DisplayContent> newDisplays = createManagedVirtualDisplaySession()
+                .setShowSystemDecorations(true)
+                .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
+                .setSimulateDisplay(true)
+                .createDisplays(2);
+        final DisplayContent firstDisplay = newDisplays.get(0);
+        final DisplayContent secondDisplay = newDisplays.get(1);
+
+        // Initialize IME test environment
+        final MockImeSession mockImeSession = createManagedMockImeSession(this);
+        final TestActivitySession<ImeTestActivity> imeTestActivitySession =
+                createManagedTestActivitySession();
+        ImeEventStream stream = mockImeSession.openEventStream();
+
+        // Make firstDisplay the top focus display.
+        tapOnDisplayCenter(firstDisplay.mId);
+        // Filter out onConfigurationChanged events in case that IME is moved from the default
+        // display to the firstDisplay.
+        ImeEventStream configChangeVerifyStream = clearOnConfigurationChangedFromStream(stream);
+        imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
+                firstDisplay.mId);
+        imeTestActivitySession.runOnMainSyncAndWait(
+                imeTestActivitySession.getActivity()::showSoftInput);
+
+        waitOrderedImeEventsThenAssertImeShown(stream, firstDisplay.mId,
+                editorMatcher("onStartInput",
+                        imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
+                event -> "showSoftInput".equals(event.getEventName()));
+        // Launch Ime must not lead to configuration changes.
+        waitAndAssertNoImeConfigurationChanged(configChangeVerifyStream);
+
+        // Move ImeTestActivity from firstDisplay to secondDisplay.
+        getLaunchActivityBuilder()
+                .setUseInstrumentation()
+                .setTargetActivity(imeTestActivitySession.getActivity().getComponentName())
+                .setIntentFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .allowMultipleInstances(false)
+                .setDisplayId(secondDisplay.mId).execute();
+
+        // Make sure ImeTestActivity is move from the firstDisplay to the secondDisplay
+        waitAndAssertTopResumedActivity(imeTestActivitySession.getActivity().getComponentName(),
+                secondDisplay.mId, "ImeTestActivity must be top-resumed on display#"
+                + secondDisplay.mId);
+        assertThat(mWmState.hasActivityInDisplay(firstDisplay.mId,
+                imeTestActivitySession.getActivity().getComponentName())).isFalse();
+
+        // Show soft input again to trigger IME movement.
+        imeTestActivitySession.runOnMainSyncAndWait(
+                imeTestActivitySession.getActivity()::showSoftInput);
+
+        waitOrderedImeEventsThenAssertImeShown(stream, secondDisplay.mId,
+                editorMatcher("onStartInput",
+                        imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
+                event -> "showSoftInput".equals(event.getEventName()));
+        // Moving IME to the display with the same display metrics must not trigger
+        // onConfigurationChanged callback.
+        waitAndAssertNoImeConfigurationChanged(configChangeVerifyStream);
+    }
+
     public static class ImeTestActivity extends Activity {
         ImeAwareEditText mEditText;
 
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java
index e145c52..fdd22d3 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/MultiDisplayTestBase.java
@@ -41,7 +41,9 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.cts.mockime.ImeEventStreamTestUtils.clearAllEvents;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasSize;
@@ -53,6 +55,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.inputmethodservice.InputMethodService;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.server.wm.CommandSession.ActivitySession;
@@ -69,6 +72,7 @@
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.cts.mockime.ImeEvent;
 import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.ImeEventStreamTestUtils;
 
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -491,7 +495,7 @@
         @NonNull
         List<DisplayContent> createDisplays(int count) {
             if (mSimulateDisplay) {
-                return simulateDisplay();
+                return simulateDisplays(count);
             } else {
                 return createVirtualDisplays(count);
             }
@@ -525,13 +529,10 @@
          * </pre>
          * @return {@link DisplayContent} of newly created display.
          */
-        private List<DisplayContent> simulateDisplay() {
+        private List<DisplayContent> simulateDisplays(int count) {
             mOverlayDisplayDeviceSession = new OverlayDisplayDevicesSession(mContext);
-            mOverlayDisplayDeviceSession.createDisplay(
-                    mSimulationDisplaySize,
-                    mDensityDpi,
-                    mOwnContentOnly,
-                    mShowSystemDecorations);
+            mOverlayDisplayDeviceSession.createDisplays(mSimulationDisplaySize, mDensityDpi,
+                    mOwnContentOnly, mShowSystemDecorations, count);
             mOverlayDisplayDeviceSession.configureDisplays(mDisplayImePolicy /* imePolicy */);
             return mOverlayDisplayDeviceSession.getCreatedDisplays();
         }
@@ -692,16 +693,24 @@
         }
 
         /** Creates overlay display with custom density dpi, specified size, and test flags. */
-        void createDisplay(Size displaySize, int densityDpi, boolean ownContentOnly,
-                boolean shouldShowSystemDecorations) {
-            String displaySettingsEntry = displaySize + "/" + densityDpi;
-            if (ownContentOnly) {
-                displaySettingsEntry += OVERLAY_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+        void createDisplays(Size displaySize, int densityDpi, boolean ownContentOnly,
+                boolean shouldShowSystemDecorations, int count) {
+            final StringBuilder builder = new StringBuilder();
+            for (int i = 0; i < count; i++) {
+                String displaySettingsEntry = displaySize + "/" + densityDpi;
+                if (ownContentOnly) {
+                    displaySettingsEntry += OVERLAY_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+                }
+                if (shouldShowSystemDecorations) {
+                    displaySettingsEntry += OVERLAY_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
+                }
+                builder.append(displaySettingsEntry);
+                // Creating n displays needs (n - 1) ';'.
+                if (i < count - 1) {
+                    builder.append(';');
+                }
             }
-            if (shouldShowSystemDecorations) {
-                displaySettingsEntry += OVERLAY_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
-            }
-            set(displaySettingsEntry);
+            set(builder.toString());
         }
 
         void configureDisplays(int imePolicy) {
@@ -853,6 +862,26 @@
         mWmState.waitAndAssertImeWindowShownOnDisplay(displayId);
     }
 
+    protected void waitAndAssertImeConfigurationChanged(ImeEventStream stream) throws Exception {
+        expectEvent(stream, event -> "onConfigurationChanged".equals(event.getEventName()),
+                TimeUnit.SECONDS.toMillis(5) /* eventTimeout */);
+    }
+
+    protected void waitAndAssertNoImeConfigurationChanged(ImeEventStream stream) {
+        notExpectEvent(stream, event -> "onConfigurationChanged".equals(event.getEventName()),
+                TimeUnit.SECONDS.toMillis(1) /* eventTimeout */);
+    }
+
+    /**
+     * Clears all {@link InputMethodService#onConfigurationChanged(Configuration)} events from the
+     * given {@code stream} and returns a forked {@link ImeEventStream}.
+     *
+     * @see ImeEventStreamTestUtils#clearAllEvents(ImeEventStream, String)
+     */
+    protected ImeEventStream clearOnConfigurationChangedFromStream(ImeEventStream stream) {
+        return clearAllEvents(stream, "onConfigurationChanged");
+    }
+
     /**
      * This class is used when you need to test virtual display created by a privileged app.
      *
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
index e5344b8..b2955b2 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
@@ -994,6 +994,11 @@
         });
     }
 
+    @Override
+    public void onConfigurationChanged(Configuration configuration) {
+        getTracer().onConfigurationChanged(() -> {}, configuration);
+    }
+
     /**
      * Event tracing helper class for {@link MockIme}.
      */
@@ -1271,5 +1276,11 @@
             final Bundle arguments = new Bundle();
             recordEventInternal("onInlineSuggestionLongClickedEvent", runnable, arguments);
         }
+
+        void onConfigurationChanged(@NonNull Runnable runnable, Configuration configuration) {
+            final Bundle arguments = new Bundle();
+            arguments.putParcelable("Configuration", configuration);
+            recordEventInternal("onConfigurationChanged", runnable, arguments);
+        }
     }
 }
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
index 1dc5148..6eeb1da 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodServiceTest.java
@@ -713,6 +713,23 @@
         }
     }
 
+    @Test
+    public void testNoConfigurationChangedOnStartInput() throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                mInstrumentation.getContext(), mInstrumentation.getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            createTestActivity(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+
+            final ImeEventStream forkedStream = stream.copy();
+            expectEvent(stream, event -> "onStartInput".equals(event.getEventName()), TIMEOUT);
+            // Verify if InputMethodService#isUiContext returns true
+            notExpectEvent(forkedStream, event -> "onConfigurationChanged".equals(
+                    event.getEventName()), EXPECTED_TIMEOUT);
+        }
+    }
+
     /** Test case for committing and setting composing region after cursor. */
     private static UpdateSelectionTest getCommitAndSetComposingRegionTest(
             long timeout, String makerPrefix) throws Exception {