xref: /aosp_15_r20/cts/tests/framework/base/windowmanager/src/android/server/wm/ime/MultiDisplayImeTests.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.server.wm.ime;
18 
19 import static android.server.wm.InputMethodVisibilityVerifier.expectImeInvisible;
20 import static android.server.wm.InputMethodVisibilityVerifier.expectImeVisible;
21 import static android.server.wm.MockImeHelper.createManagedMockImeSession;
22 import static android.server.wm.UiDeviceUtils.pressBackButton;
23 import static android.server.wm.WindowManagerState.STATE_RESUMED;
24 import static android.view.Display.DEFAULT_DISPLAY;
25 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
26 import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
27 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
28 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
29 
30 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
31 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
32 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
33 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEventWithKeyValue;
34 import static com.android.cts.mockime.ImeEventStreamTestUtils.hideSoftInputMatcher;
35 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
36 
37 import static com.google.common.truth.Truth.assertThat;
38 import static com.google.common.truth.Truth.assertWithMessage;
39 
40 import static org.junit.Assert.assertEquals;
41 import static org.junit.Assert.assertFalse;
42 import static org.junit.Assert.assertTrue;
43 import static org.junit.Assume.assumeFalse;
44 import static org.junit.Assume.assumeTrue;
45 
46 import android.app.Activity;
47 import android.content.ComponentName;
48 import android.content.Context;
49 import android.content.ContextWrapper;
50 import android.content.Intent;
51 import android.content.res.Configuration;
52 import android.graphics.Rect;
53 import android.os.Bundle;
54 import android.os.SystemClock;
55 import android.platform.test.annotations.Presubmit;
56 import android.platform.test.annotations.RequiresFlagsDisabled;
57 import android.platform.test.flag.junit.CheckFlagsRule;
58 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
59 import android.server.wm.MultiDisplayTestBase;
60 import android.server.wm.WindowManagerState;
61 import android.server.wm.WindowManagerState.DisplayContent;
62 import android.server.wm.WindowManagerState.WindowState;
63 import android.server.wm.intent.Activities;
64 import android.text.TextUtils;
65 import android.view.View;
66 import android.view.Window;
67 import android.view.WindowManager;
68 import android.view.inputmethod.EditorInfo;
69 import android.view.inputmethod.InputConnection;
70 import android.view.inputmethod.InputMethodManager;
71 import android.widget.EditText;
72 import android.widget.LinearLayout;
73 
74 import com.android.compatibility.common.util.PollingCheck;
75 import com.android.compatibility.common.util.SystemUtil;
76 import com.android.cts.mockime.ImeCommand;
77 import com.android.cts.mockime.ImeEventStream;
78 import com.android.cts.mockime.MockImeSession;
79 
80 import org.junit.Before;
81 import org.junit.Rule;
82 import org.junit.Test;
83 
84 import java.util.List;
85 import java.util.concurrent.TimeUnit;
86 
87 /**
88  * Build/Install/Run:
89  *     atest CtsWindowManagerDeviceIme:MultiDisplayImeTests
90  */
91 @Presubmit
92 @android.server.wm.annotation.Group3
93 public class MultiDisplayImeTests extends MultiDisplayTestBase {
94 
95     @Rule
96     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
97 
98     static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
99     static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
100 
101     @Before
102     @Override
setUp()103     public void setUp() throws Exception {
104         assumeRunNotOnVisibleBackgroundNonProfileUser("On visible background users, having the"
105                 + "keyboard in one display and the app that consumes the key events in another "
106                 + "virtual display, is not supported");
107 
108         super.setUp();
109 
110         assumeTrue(supportsMultiDisplay());
111         assumeTrue(MSG_NO_MOCK_IME, supportsInstallableIme());
112     }
113 
114     @Test
testImeWindowCanSwitchToDifferentDisplays()115     public void testImeWindowCanSwitchToDifferentDisplays() throws Exception {
116         final MockImeSession mockImeSession = createManagedMockImeSession(this);
117         final TestActivitySession<ImeTestActivity> imeTestActivitySession =
118                 createManagedTestActivitySession();
119         final TestActivitySession<ImeTestActivity2> imeTestActivitySession2 =
120                 createManagedTestActivitySession();
121 
122         // Create a virtual display and launch an activity on it.
123         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
124                 .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
125                 .setSimulateDisplay(true)
126                 .createDisplay();
127 
128         final ImeEventStream stream = mockImeSession.openEventStream();
129 
130         imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
131                 newDisplay.mId);
132 
133         expectEvent(stream, editorMatcher("onStartInput",
134                 imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
135 
136         // Make the activity to show soft input.
137         showSoftInputAndAssertImeShownOnDisplay(newDisplay.mId, imeTestActivitySession, stream);
138 
139         // Assert the configuration of the IME window is the same as the configuration of the
140         // virtual display.
141         assertImeWindowAndDisplayConfiguration(mWmState.getImeWindowState(), newDisplay);
142 
143         // Launch another activity on the main display of the user. When the test runs on the
144         // current user, the display will be the default display.
145         imeTestActivitySession2.launchTestActivityOnDisplaySync(
146                 ImeTestActivity2.class, getMainDisplayId());
147         expectEvent(stream, editorMatcher("onStartInput",
148                 imeTestActivitySession2.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
149 
150         // Make the activity to show soft input.
151         showSoftInputAndAssertImeShownOnDisplay(getMainDisplayId(), imeTestActivitySession2,
152                 stream);
153 
154         // Assert the configuration of the IME window is the same as the configuration of the
155         // main display of the user.
156         assertImeWindowAndDisplayConfiguration(mWmState.getImeWindowState(),
157                 mWmState.getDisplay(getMainDisplayId()));
158     }
159 
160     /**
161      * This checks that calling showSoftInput on the incorrect display, requiring the fallback IMM,
162      * will not drop the statsToken tracking the show request.
163      */
164     @Test
165     @RequiresFlagsDisabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
testFallbackImmMaintainsParameters()166     public void testFallbackImmMaintainsParameters() throws Exception {
167         try (var mockImeSession = createManagedMockImeSession(this);
168                 TestActivitySession<ImeTestActivity> imeTestActivitySession =
169                         createManagedTestActivitySession();
170                 var displaySession = createManagedVirtualDisplaySession()) {
171             final var newDisplay = displaySession.setSimulateDisplay(true).createDisplay();
172 
173             imeTestActivitySession.launchTestActivityOnDisplaySync(
174                     ImeTestActivity.class, newDisplay.mId);
175             final var activity = imeTestActivitySession.getActivity();
176             final var stream = mockImeSession.openEventStream();
177 
178             expectEvent(stream, editorMatcher("onStartInput",
179                     activity.mEditText.getPrivateImeOptions()), TIMEOUT);
180 
181             imeTestActivitySession.runOnMainSyncAndWait(() -> {
182                 final var imm = activity.getApplicationContext()
183                         .getSystemService(InputMethodManager.class);
184                 imm.showSoftInput(activity.mEditText, 0 /* flags */);
185             });
186 
187             expectImeVisible(TIMEOUT);
188             PollingCheck.waitFor(() -> !mockImeSession.hasPendingImeVisibilityRequests(),
189                     "No pending requests should remain after the IME is visible");
190         }
191     }
192 
193     @Test
testImeApiForBug118341760()194     public void testImeApiForBug118341760() throws Exception {
195         final MockImeSession mockImeSession = createManagedMockImeSession(this);
196         final TestActivitySession<ImeTestActivityWithBrokenContextWrapper> imeTestActivitySession =
197                 createManagedTestActivitySession();
198         // Create a virtual display and launch an activity on it.
199         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
200                 .setSimulateDisplay(true)
201                 .createDisplay();
202         imeTestActivitySession.launchTestActivityOnDisplaySync(
203                 ImeTestActivityWithBrokenContextWrapper.class, newDisplay.mId);
204 
205         final ImeTestActivityWithBrokenContextWrapper activity =
206                 imeTestActivitySession.getActivity();
207         final ImeEventStream stream = mockImeSession.openEventStream();
208         final String privateImeOption = activity.getEditText().getPrivateImeOptions();
209         expectEvent(stream, event -> {
210             if (!TextUtils.equals("onStartInput", event.getEventName())) {
211                 return false;
212             }
213             final EditorInfo editorInfo = event.getArguments().getParcelable("editorInfo");
214             return TextUtils.equals(editorInfo.packageName, mContext.getPackageName())
215                     && TextUtils.equals(editorInfo.privateImeOptions, privateImeOption);
216         }, TIMEOUT);
217 
218         imeTestActivitySession.runOnMainSyncAndWait(() -> {
219             final InputMethodManager imm = activity.getSystemService(InputMethodManager.class);
220             assertTrue("InputMethodManager.isActive() should work",
221                     imm.isActive(activity.getEditText()));
222         });
223     }
224 
225     @Test
testImeWindowCanSwitchWhenTopFocusedDisplayChange()226     public void testImeWindowCanSwitchWhenTopFocusedDisplayChange() throws Exception {
227         // If config_perDisplayFocusEnabled, the focus will not move even if touching on
228         // the Activity in the different display.
229         assumeFalse(perDisplayFocusEnabled());
230 
231         final MockImeSession mockImeSession = createManagedMockImeSession(this);
232         final TestActivitySession<ImeTestActivity> imeTestActivitySession =
233                 createManagedTestActivitySession();
234         final TestActivitySession<ImeTestActivity2> imeTestActivitySession2 =
235                 createManagedTestActivitySession();
236 
237         // Create a virtual display and launch an activity on virtual & default display.
238         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
239                 .setSimulateDisplay(true)
240                 .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
241                 .createDisplay();
242         imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
243                 DEFAULT_DISPLAY);
244         imeTestActivitySession2.launchTestActivityOnDisplaySync(ImeTestActivity2.class,
245                 newDisplay.mId);
246 
247         final DisplayContent defDisplay = mWmState.getDisplay(DEFAULT_DISPLAY);
248         final ImeEventStream stream = mockImeSession.openEventStream();
249 
250         // Tap on the imeTestActivity task center instead of the display center because
251         // the activity might not be spanning the entire display
252         WindowManagerState.Task imeTestActivityTask = mWmState
253                 .getTaskByActivity(imeTestActivitySession.getActivity().getComponentName());
254         tapOnTaskCenter(imeTestActivityTask);
255         expectEvent(stream, editorMatcher("onStartInput",
256                 imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
257         showSoftInputAndAssertImeShownOnDisplay(defDisplay.mId, imeTestActivitySession, stream);
258 
259         // Tap virtual display as top focused display & request focus on EditText to show
260         // soft input.
261         touchAndCancelOnDisplayCenterSync(newDisplay.mId);
262         expectEvent(stream, editorMatcher("onStartInput",
263                 imeTestActivitySession2.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
264         showSoftInputAndAssertImeShownOnDisplay(newDisplay.mId, imeTestActivitySession2, stream);
265 
266         // Tap on the imeTestActivity task center instead of the display center because
267         // the activity might not be spanning the entire display
268         imeTestActivityTask = mWmState
269                 .getTaskByActivity(imeTestActivitySession.getActivity().getComponentName());
270         tapOnTaskCenter(imeTestActivityTask);
271         expectEvent(stream, editorMatcher("onStartInput",
272                 imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
273         showSoftInputAndAssertImeShownOnDisplay(defDisplay.mId, imeTestActivitySession, stream);
274     }
275 
276     /**
277      * Test that the IME can be shown in a different display (actually the default display) than
278      * the display on which the target IME application is shown.  Then test several basic operations
279      * in {@link InputConnection}.
280      */
281     @Test
testCrossDisplayBasicImeOperations()282     public void testCrossDisplayBasicImeOperations() throws Exception {
283         final MockImeSession mockImeSession = createManagedMockImeSession(this);
284         final TestActivitySession<ImeTestActivity> imeTestActivitySession =
285                 createManagedTestActivitySession();
286 
287         // Create a virtual display by app and assume the display should not show IME window.
288         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
289                 .setPublicDisplay(true)
290                 .createDisplay();
291         SystemUtil.runWithShellPermissionIdentity(
292                 () -> assertTrue("Display should not support showing IME window",
293                         mTargetContext.getSystemService(WindowManager.class)
294                                 .getDisplayImePolicy(newDisplay.mId)
295                                 == DISPLAY_IME_POLICY_FALLBACK_DISPLAY));
296 
297         // Launch Ime test activity in virtual display.
298         imeTestActivitySession.launchTestActivityOnDisplay(ImeTestActivity.class,
299                 newDisplay.mId);
300         final ImeEventStream stream = mockImeSession.openEventStream();
301 
302         // Expect onStartInput would be executed when user tapping on the
303         // non-system created display intentionally.
304         tapAndAssertEditorFocusedOnImeActivity(imeTestActivitySession, newDisplay.mId);
305         expectEvent(stream, editorMatcher("onStartInput",
306                 imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
307 
308         // Verify the activity to show soft input on the default display.
309         showSoftInputAndAssertImeShownOnDisplay(DEFAULT_DISPLAY, imeTestActivitySession, stream);
310 
311         // Commit text & make sure the input texts should be delivered to focused EditText on
312         // virtual display.
313         final EditText editText = imeTestActivitySession.getActivity().mEditText;
314         final String commitText = "test commit";
315         expectCommand(stream, mockImeSession.callCommitText(commitText, 1), TIMEOUT);
316         imeTestActivitySession.runOnMainAndAssertWithTimeout(
317                 () -> TextUtils.equals(commitText, editText.getText()), TIMEOUT,
318                 "The input text should be delivered");
319 
320         // Since the IME and the IME target app are running in different displays,
321         // InputConnection#requestCursorUpdates() is not supported and it should return false.
322         // See InputMethodServiceTest#testOnUpdateCursorAnchorInfo() for the normal scenario.
323         final ImeCommand callCursorUpdates = mockImeSession.callRequestCursorUpdates(
324                 InputConnection.CURSOR_UPDATE_IMMEDIATE);
325         assertFalse(expectCommand(stream, callCursorUpdates, TIMEOUT).getReturnBooleanValue());
326     }
327 
328     /**
329      * Test that the IME can be hidden with the {@link WindowManager#DISPLAY_IME_POLICY_HIDE} flag.
330      */
331     @Test
testDisplayPolicyImeHideImeOperation()332     public void testDisplayPolicyImeHideImeOperation() throws Exception {
333         final MockImeSession mockImeSession = createManagedMockImeSession(this);
334         final TestActivitySession<ImeTestActivity> imeTestActivitySession =
335                 createManagedTestActivitySession();
336 
337         // Create a virtual display and launch an activity on virtual display.
338         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
339                 .setDisplayImePolicy(DISPLAY_IME_POLICY_HIDE)
340                 .setSimulateDisplay(true)
341                 .createDisplay();
342 
343         // Launch Ime test activity and initial the editor focus on virtual display.
344         imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
345                 newDisplay.mId);
346 
347         // Verify the activity is launched to the secondary display.
348         final ComponentName imeTestActivityName =
349                 imeTestActivitySession.getActivity().getComponentName();
350         assertThat(mWmState.hasActivityInDisplay(newDisplay.mId, imeTestActivityName)).isTrue();
351 
352         // Verify invoking showSoftInput will be ignored when the display with the HIDE policy.
353         final ImeEventStream stream = mockImeSession.openEventStream();
354         imeTestActivitySession.runOnMainSyncAndWait(
355                 imeTestActivitySession.getActivity()::showSoftInput);
356         notExpectEvent(stream, editorMatcher("showSoftInput",
357                 imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
358                 NOT_EXPECT_TIMEOUT);
359     }
360 
361     /**
362      * A regression test for Bug 273630528.
363      *
364      * Test that the IME on the editor activity with embedded in virtual display will be hidden
365      * after pressing the back key.
366      */
367     @Test
testHideImeWhenImeTargetOnEmbeddedVirtualDisplay()368     public void testHideImeWhenImeTargetOnEmbeddedVirtualDisplay() throws Exception {
369         final VirtualDisplaySession session = createManagedVirtualDisplaySession();
370         final MockImeSession imeSession = createManagedMockImeSession(this);
371         final TestActivitySession<ImeTestActivity> imeActivitySession =
372                 createManagedTestActivitySession();
373 
374         // Setup a virtual display embedded on an activity.
375         final DisplayContent dc = session
376                 .setPublicDisplay(true)
377                 .setSupportsTouch(true)
378                 .createDisplay();
379 
380         // Launch a test activity on that virtual display and show IME by tapping the editor.
381         imeActivitySession.launchTestActivityOnDisplay(ImeTestActivity.class, dc.mId);
382         tapAndAssertEditorFocusedOnImeActivity(imeActivitySession, dc.mId);
383         final ImeEventStream stream = imeSession.openEventStream();
384         final String marker = imeActivitySession.getActivity().mEditText.getPrivateImeOptions();
385         expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
386 
387         // Expect soft-keyboard becomes visible after requesting show IME.
388         showSoftInputAndAssertImeShownOnDisplay(DEFAULT_DISPLAY, imeActivitySession, stream);
389         expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible",
390                 View.VISIBLE, TIMEOUT);
391         expectImeVisible(TIMEOUT);
392 
393         // Pressing back key, expect soft-keyboard will become invisible.
394         pressBackButton();
395         expectEvent(stream, hideSoftInputMatcher(), TIMEOUT);
396         expectEventWithKeyValue(stream, "onWindowVisibilityChanged", "visible",
397                 View.GONE, TIMEOUT);
398         expectImeInvisible(TIMEOUT);
399     }
400 
401     @Test
testImeWindowCanShownWhenActivityMovedToDisplay()402     public void testImeWindowCanShownWhenActivityMovedToDisplay() throws Exception {
403         // If config_perDisplayFocusEnabled, the focus will not move even if touching on
404         // the Activity in the different display.
405         assumeFalse(perDisplayFocusEnabled());
406 
407         // Launch a regular activity on default display at the test beginning to prevent the test
408         // may mis-touch the launcher icon that breaks the test expectation.
409         final TestActivitySession<Activities.RegularActivity> testActivitySession =
410                 createManagedTestActivitySession();
411         testActivitySession.launchTestActivityOnDisplaySync(Activities.RegularActivity.class,
412                 DEFAULT_DISPLAY);
413 
414         // Create a virtual display and launch an activity on virtual display.
415         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
416                 .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
417                 .setSimulateDisplay(true)
418                 .createDisplay();
419 
420         // Leverage MockImeSession to ensure at least an IME exists as default.
421         final MockImeSession mockImeSession = createManagedMockImeSession(this);
422         final TestActivitySession<ImeTestActivity> imeTestActivitySession =
423                 createManagedTestActivitySession();
424         // Launch Ime test activity and initial the editor focus on virtual display.
425         imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
426                 newDisplay.mId);
427 
428         // Verify the activity is launched to the secondary display.
429         final ComponentName imeTestActivityName =
430                 imeTestActivitySession.getActivity().getComponentName();
431         assertThat(mWmState.hasActivityInDisplay(newDisplay.mId, imeTestActivityName)).isTrue();
432 
433         // Tap default display, assume a pointer-out-side event will happened to change the top
434         // display.
435         final DisplayContent defDisplay = mWmState.getDisplay(DEFAULT_DISPLAY);
436         tapOnDisplayCenter(defDisplay.mId);
437         mWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
438         mWmState.assertValidity();
439 
440         // Reparent ImeTestActivity from virtual display to default display.
441         getLaunchActivityBuilder()
442                 .setUseInstrumentation()
443                 .setTargetActivity(imeTestActivitySession.getActivity().getComponentName())
444                 .setIntentFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
445                 .allowMultipleInstances(false)
446                 .setDisplayId(DEFAULT_DISPLAY).execute();
447         waitAndAssertResumedAndFocusedActivityOnDisplay(
448                 imeTestActivitySession.getActivity().getComponentName(), DEFAULT_DISPLAY,
449                 "Activity launched on default display and on top");
450 
451         // Activity is no longer on the secondary display
452         assertThat(mWmState.hasActivityInDisplay(newDisplay.mId, imeTestActivityName)).isFalse();
453 
454         // Tap on the imeTestActivity task center instead of the display center because
455         // the activity might not be spanning the entire display
456         final ImeEventStream stream = mockImeSession.openEventStream();
457         final WindowManagerState.Task testActivityTask = mWmState
458                 .getTaskByActivity(imeTestActivitySession.getActivity().getComponentName());
459         tapOnTaskCenter(testActivityTask);
460         expectEvent(stream, editorMatcher("onStartInput",
461                 imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
462 
463         // Verify the activity shows soft input on the default display.
464         showSoftInputAndAssertImeShownOnDisplay(DEFAULT_DISPLAY, imeTestActivitySession, stream);
465     }
466 
467     @Test
testNoConfigurationChangedWhenSwitchBetweenTwoIdenticalDisplays()468     public void testNoConfigurationChangedWhenSwitchBetweenTwoIdenticalDisplays() throws Exception {
469         // If config_perDisplayFocusEnabled, the focus will not move even if touching on
470         // the Activity in the different display.
471         assumeFalse(perDisplayFocusEnabled());
472 
473         // Create two displays with the same display metrics
474         final List<DisplayContent> newDisplays = createManagedVirtualDisplaySession()
475                 .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
476                 .setOwnContentOnly(true)
477                 .setSimulateDisplay(true)
478                 .setResizeDisplay(false)
479                 .createDisplays(2);
480         final DisplayContent firstDisplay = newDisplays.get(0);
481         final DisplayContent secondDisplay = newDisplays.get(1);
482 
483         // Skip if the test environment somehow didn't create 2 displays with identical size.
484         assumeTrue("Skip the test if the size of the created displays aren't identical",
485                 firstDisplay.getDisplayRect().equals(secondDisplay.getDisplayRect()));
486 
487         final TestActivitySession<ImeTestActivity2> imeTestActivitySession2 =
488                 createManagedTestActivitySession();
489         imeTestActivitySession2.launchTestActivityOnDisplaySync(
490                 ImeTestActivity2.class, secondDisplay.mId);
491 
492         // Make firstDisplay the top focus display.
493         touchAndCancelOnDisplayCenterSync(firstDisplay.mId);
494 
495         mWmState.waitForWithAmState(state -> state.getFocusedDisplayId() == firstDisplay.mId,
496                 "First display must be top focused.");
497 
498         // Initialize IME test environment
499         final MockImeSession mockImeSession = createManagedMockImeSession(this);
500         final TestActivitySession<ImeTestActivity> imeTestActivitySession =
501                 createManagedTestActivitySession();
502         ImeEventStream stream = mockImeSession.openEventStream();
503         // Filter out onConfigurationChanged events in case that IME is moved from the default
504         // display to the firstDisplay.
505         ImeEventStream configChangeVerifyStream = clearOnConfigurationChangedFromStream(stream);
506 
507         imeTestActivitySession.launchTestActivityOnDisplaySync(ImeTestActivity.class,
508                 firstDisplay.mId);
509         // Wait until IME is ready for the IME client to call showSoftInput().
510         expectEvent(stream, editorMatcher("onStartInput",
511                 imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
512 
513         int imeDisplayId = expectCommand(stream, mockImeSession.callGetDisplayId(),
514                 TIMEOUT).getReturnIntegerValue();
515         assertThat(imeDisplayId).isEqualTo(firstDisplay.mId);
516 
517         imeTestActivitySession.runOnMainSyncAndWait(
518                 imeTestActivitySession.getActivity()::showSoftInput);
519         waitOrderedImeEventsThenAssertImeShown(stream, firstDisplay.mId,
520                 event -> "showSoftInput".equals(event.getEventName()));
521         try {
522             // Launch Ime must not lead to screen size changes.
523             waitAndAssertImeNoScreenSizeChanged(configChangeVerifyStream);
524 
525             final Rect currentBoundsOnFirstDisplay = expectCommand(stream,
526                     mockImeSession.callGetCurrentWindowMetricsBounds(), TIMEOUT)
527                     .getReturnParcelableValue();
528 
529             // Clear onConfigurationChanged events before IME moves to the secondary display to
530             // prevent flaky because IME may receive configuration updates which we don't care
531             // about. An example is CONFIG_KEYBOARD_HIDDEN.
532             configChangeVerifyStream = clearOnConfigurationChangedFromStream(stream);
533 
534             // Tap secondDisplay to change it to the top focused display.
535             touchAndCancelOnDisplayCenterSync(firstDisplay.mId);
536 
537             // Move ImeTestActivity from firstDisplay to secondDisplay.
538             getLaunchActivityBuilder()
539                     .setUseInstrumentation()
540                     .setTargetActivity(imeTestActivitySession.getActivity().getComponentName())
541                     .setIntentFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
542                     .allowMultipleInstances(false)
543                     .setDisplayId(secondDisplay.mId).execute();
544 
545             // Make sure ImeTestActivity is move from the firstDisplay to the secondDisplay
546             waitAndAssertResumedAndFocusedActivityOnDisplay(
547                     imeTestActivitySession.getActivity().getComponentName(), secondDisplay.mId,
548                     "ImeTestActivity must be top-resumed on display#" + secondDisplay.mId);
549             assertThat(mWmState.hasActivityInDisplay(firstDisplay.mId,
550                     imeTestActivitySession.getActivity().getComponentName())).isFalse();
551             // Wait until IME is ready for the IME client to call showSoftInput().
552             expectEvent(stream, editorMatcher("onStartInput",
553                     imeTestActivitySession.getActivity().mEditText.getPrivateImeOptions()),
554                     TIMEOUT);
555             imeDisplayId = expectCommand(stream, mockImeSession.callGetDisplayId(),
556                     TIMEOUT).getReturnIntegerValue();
557             assertThat(imeDisplayId).isEqualTo(secondDisplay.mId);
558 
559             // With the refactor, the additional show is not needed, as we already verified that
560             // the IME is showing
561             if (!android.view.inputmethod.Flags.refactorInsetsController()) {
562                 // Show soft input again to trigger IME movement.
563                 imeTestActivitySession.runOnMainSyncAndWait(
564                         imeTestActivitySession.getActivity()::showSoftInput);
565                 waitOrderedImeEventsThenAssertImeShown(stream, secondDisplay.mId,
566                         event -> "showSoftInput".equals(event.getEventName()));
567             }
568 
569             // Moving IME to the display with the same display metrics must not lead to
570             // screen size changes.
571             waitAndAssertImeNoScreenSizeChanged(configChangeVerifyStream);
572 
573             final Rect currentBoundsOnSecondDisplay = expectCommand(stream,
574                     mockImeSession.callGetCurrentWindowMetricsBounds(), TIMEOUT)
575                     .getReturnParcelableValue();
576 
577             assertWithMessage("The current WindowMetrics bounds of IME must not be changed.")
578                     .that(currentBoundsOnFirstDisplay).isEqualTo(currentBoundsOnSecondDisplay);
579         } catch (AssertionError e) {
580             mWmState.computeState();
581             final Rect displayRect1 = mWmState.getDisplay(firstDisplay.mId).getDisplayRect();
582             final Rect displayRect2 = mWmState.getDisplay(secondDisplay.mId).getDisplayRect();
583             assumeTrue("Skip test since the size of one or both displays happens unexpected change",
584                     displayRect1.equals(displayRect2));
585             throw e;
586         }
587     }
588 
589     public static class ImeTestActivity extends Activity {
590         EditText mEditText;
591 
592         @Override
onCreate(Bundle icicle)593         protected void onCreate(Bundle icicle) {
594             super.onCreate(icicle);
595             mEditText = new EditText(this);
596             // Set private IME option for editorMatcher to identify which TextView received
597             // onStartInput event.
598             resetPrivateImeOptionsIdentifier();
599             final LinearLayout layout = new LinearLayout(this);
600             layout.setOrientation(LinearLayout.VERTICAL);
601             layout.addView(mEditText);
602             mEditText.requestFocus();
603             // SOFT_INPUT_STATE_UNSPECIFIED may produced unexpected behavior for CTS. To make tests
604             // deterministic, using SOFT_INPUT_STATE_UNCHANGED instead.
605             setUnchangedSoftInputState();
606             setContentView(layout);
607         }
608 
showSoftInput()609         void showSoftInput() {
610             final InputMethodManager imm = getSystemService(InputMethodManager.class);
611             imm.showSoftInput(mEditText, 0);
612         }
613 
resetPrivateImeOptionsIdentifier()614         void resetPrivateImeOptionsIdentifier() {
615             mEditText.setPrivateImeOptions(
616                     getClass().getName() + "/" + Long.toString(SystemClock.elapsedRealtimeNanos()));
617         }
618 
setUnchangedSoftInputState()619         private void setUnchangedSoftInputState() {
620             final Window window = getWindow();
621             final int currentSoftInputMode = window.getAttributes().softInputMode;
622             final int newSoftInputMode =
623                     (currentSoftInputMode & ~WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE)
624                             | SOFT_INPUT_STATE_UNCHANGED;
625             window.setSoftInputMode(newSoftInputMode);
626         }
627     }
628 
629     public static class ImeTestActivity2 extends ImeTestActivity { }
630 
631     public static final class ImeTestActivityWithBrokenContextWrapper extends Activity {
632         private EditText mEditText;
633 
634         /**
635          * Emulates the behavior of certain {@link ContextWrapper} subclasses we found in the wild.
636          *
637          * <p> Certain {@link ContextWrapper} subclass in the wild delegate method calls to
638          * ApplicationContext except for {@link #getSystemService(String)}.</p>
639          *
640          **/
641         private static final class Bug118341760ContextWrapper extends ContextWrapper {
642             private final Context mOriginalContext;
643 
Bug118341760ContextWrapper(Context base)644             Bug118341760ContextWrapper(Context base) {
645                 super(base.getApplicationContext());
646                 mOriginalContext = base;
647             }
648 
649             /**
650              * Emulates the behavior of {@link ContextWrapper#getSystemService(String)} of certain
651              * {@link ContextWrapper} subclasses we found in the wild.
652              *
653              * @param name The name of the desired service.
654              * @return The service or {@code null} if the name does not exist.
655              */
656             @Override
getSystemService(String name)657             public Object getSystemService(String name) {
658                 return mOriginalContext.getSystemService(name);
659             }
660         }
661 
662         @Override
onCreate(Bundle icicle)663         protected void onCreate(Bundle icicle) {
664             super.onCreate(icicle);
665             mEditText = new EditText(new Bug118341760ContextWrapper(this));
666             // Use SystemClock.elapsedRealtimeNanos()) as a unique ID of this edit text.
667             mEditText.setPrivateImeOptions(Long.toString(SystemClock.elapsedRealtimeNanos()));
668             final LinearLayout layout = new LinearLayout(this);
669             layout.setOrientation(LinearLayout.VERTICAL);
670             layout.addView(mEditText);
671             mEditText.requestFocus();
672             setContentView(layout);
673         }
674 
getEditText()675         EditText getEditText() {
676             return mEditText;
677         }
678     }
679 
assertImeWindowAndDisplayConfiguration( WindowState imeWinState, DisplayContent display)680     private void assertImeWindowAndDisplayConfiguration(
681             WindowState imeWinState, DisplayContent display) {
682         // The IME window should inherit the configuration from the IME DisplayArea.
683         final WindowManagerState.DisplayArea imeContainerDisplayArea = display.getImeContainer();
684         final Configuration configurationForIme = imeWinState.getMergedOverrideConfiguration();
685         final Configuration configurationForImeContainer =
686                 imeContainerDisplayArea.getMergedOverrideConfiguration();
687         final int displayDensityDpiForIme = configurationForIme.densityDpi;
688         final int displayDensityDpiForImeContainer = configurationForImeContainer.densityDpi;
689         final Rect displayBoundsForIme = configurationForIme.windowConfiguration.getBounds();
690         final Rect displayBoundsForImeContainer =
691                 configurationForImeContainer.windowConfiguration.getBounds();
692 
693         assertEquals("Display density not the same",
694                 displayDensityDpiForImeContainer, displayDensityDpiForIme);
695         assertEquals("Display bounds not the same",
696                 displayBoundsForImeContainer, displayBoundsForIme);
697     }
698 
tapAndAssertEditorFocusedOnImeActivity( TestActivitySession<? extends ImeTestActivity> activitySession, int expectDisplayId)699     private void tapAndAssertEditorFocusedOnImeActivity(
700             TestActivitySession<? extends ImeTestActivity> activitySession, int expectDisplayId) {
701         final int[] location = new int[2];
702         waitAndAssertActivityStateOnDisplay(activitySession.getActivity().getComponentName(),
703                 STATE_RESUMED, expectDisplayId,
704                 "ImeActivity failed to appear on display#" + expectDisplayId);
705         activitySession.runOnMainSyncAndWait(() -> {
706             final EditText editText = activitySession.getActivity().mEditText;
707             editText.getLocationOnScreen(location);
708         });
709         final ComponentName expectComponent = activitySession.getActivity().getComponentName();
710         tapOnDisplaySync(location[0], location[1], expectDisplayId);
711         mWmState.computeState(activitySession.getActivity().getComponentName());
712         mWmState.assertFocusedAppOnDisplay("Activity not focus on the display", expectComponent,
713                 expectDisplayId);
714     }
715 
showSoftInputAndAssertImeShownOnDisplay(int displayId, TestActivitySession<? extends ImeTestActivity> activitySession, ImeEventStream stream)716     private void showSoftInputAndAssertImeShownOnDisplay(int displayId,
717             TestActivitySession<? extends ImeTestActivity> activitySession, ImeEventStream stream)
718             throws Exception {
719         activitySession.runOnMainSyncAndWait(
720                 activitySession.getActivity()::showSoftInput);
721         expectEvent(stream, editorMatcher("onStartInputView",
722                 activitySession.getActivity().mEditText.getPrivateImeOptions()), TIMEOUT);
723         // Assert the IME is shown on the expected display.
724         mWmState.waitAndAssertImeWindowShownOnDisplay(displayId);
725     }
726 }
727