1 /*
2  * Copyright (C) 2024 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.multidisplay;
18 
19 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
20 import static android.server.wm.CommandSession.ActivityCallback.ON_CONFIGURATION_CHANGED;
21 import static android.server.wm.CommandSession.ActivityCallback.ON_RESUME;
22 import static android.view.Display.DEFAULT_DISPLAY;
23 import static android.view.Display.INVALID_DISPLAY;
24 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
25 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
26 
27 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
28 
29 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
30 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
31 
32 import static org.junit.Assert.assertEquals;
33 import static org.junit.Assert.assertTrue;
34 import static org.junit.Assume.assumeTrue;
35 
36 import android.app.Activity;
37 import android.app.ActivityOptions;
38 import android.content.ComponentName;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.os.Bundle;
42 import android.platform.test.annotations.Presubmit;
43 import android.server.wm.CommandSession;
44 import android.server.wm.MockImeHelper;
45 import android.server.wm.MultiDisplayTestBase;
46 import android.server.wm.WindowManagerState.DisplayContent;
47 import android.view.Display;
48 import android.view.View;
49 import android.view.WindowManager;
50 import android.view.inputmethod.InputMethodManager;
51 import android.widget.EditText;
52 import android.widget.LinearLayout;
53 
54 import androidx.test.filters.MediumTest;
55 import androidx.test.rule.ActivityTestRule;
56 
57 import com.android.cts.mockime.ImeEventStream;
58 import com.android.cts.mockime.MockImeSession;
59 
60 import org.junit.Before;
61 import org.junit.Test;
62 
63 import java.util.concurrent.TimeUnit;
64 
65 /**
66  * Build/Install/Run:
67  *     atest CtsWindowManagerDeviceMultiDisplay:MultiDisplayClientTests
68  */
69 @Presubmit
70 @MediumTest
71 @android.server.wm.annotation.Group3
72 public class MultiDisplayClientTests extends MultiDisplayTestBase {
73 
74     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(10); // 10 seconds
75     private static final String EXTRA_SHOW_IME = "show_ime";
76 
77     @Before
78     @Override
setUp()79     public void setUp() throws Exception {
80         super.setUp();
81         assumeTrue(supportsMultiDisplay());
82     }
83 
84     @Test
testDisplayIdUpdateOnMove_RelaunchActivity()85     public void testDisplayIdUpdateOnMove_RelaunchActivity() throws Exception {
86         testDisplayIdUpdateOnMove(ClientTestActivity.class, false /* handlesConfigChange */);
87     }
88 
89     @Test
testDisplayIdUpdateOnMove_NoRelaunchActivity()90     public void testDisplayIdUpdateOnMove_NoRelaunchActivity() throws Exception {
91         testDisplayIdUpdateOnMove(NoRelaunchActivity.class, true /* handlesConfigChange */);
92     }
93 
testDisplayIdUpdateOnMove(Class<T> activityClass, boolean handlesConfigChange)94     private <T extends Activity> void testDisplayIdUpdateOnMove(Class<T> activityClass,
95             boolean handlesConfigChange) throws Exception {
96         final ActivityTestRule<T> activityTestRule = new ActivityTestRule<>(
97                 activityClass, true /* initialTouchMode */, false /* launchActivity */);
98 
99         // Launch activity display.
100         separateTestJournal();
101         Activity activity = activityTestRule.launchActivity(new Intent());
102         final ComponentName activityName = activity.getComponentName();
103         waitAndAssertResume(activityName);
104 
105         // Create new simulated display
106         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
107                 .setSimulateDisplay(true)
108                 .createDisplay();
109 
110         // Move the activity to the new secondary display.
111         separateTestJournal();
112         final ActivityOptions launchOptions = ActivityOptions.makeBasic();
113         final int displayId = newDisplay.mId;
114         launchOptions.setLaunchDisplayId(displayId);
115         final Intent newDisplayIntent = new Intent(mContext, activityClass);
116         newDisplayIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
117         getInstrumentation().getTargetContext().startActivity(newDisplayIntent,
118                 launchOptions.toBundle());
119         waitAndAssertTopResumedActivity(activityName, displayId,
120                 "Activity moved to secondary display must be focused");
121 
122         if (handlesConfigChange) {
123             // Wait for activity to receive the configuration change after move
124             waitAndAssertConfigurationChange(activityName);
125         } else {
126             // Activity will be re-created, wait for resumed state
127             waitAndAssertResume(activityName);
128             activity = activityTestRule.getActivity();
129         }
130 
131         final String suffix = " must be updated.";
132         assertEquals("Activity#getDisplayId()" + suffix, displayId, activity.getDisplayId());
133         assertEquals("Activity#getDisplay" + suffix,
134                 displayId, activity.getDisplay().getDisplayId());
135 
136         final WindowManager wm = activity.getWindowManager();
137         assertEquals("WM#getDefaultDisplay()" + suffix,
138                 displayId, wm.getDefaultDisplay().getDisplayId());
139 
140         final View view = activity.getWindow().getDecorView();
141         assertEquals("View#getDisplay()" + suffix,
142                 displayId, view.getDisplay().getDisplayId());
143     }
144 
145     @Test
testDisplayIdUpdateWhenImeMove_RelaunchActivity()146     public void testDisplayIdUpdateWhenImeMove_RelaunchActivity() throws Exception {
147         testDisplayIdUpdateWhenImeMove(ClientTestActivity.class);
148     }
149 
150     @Test
testDisplayIdUpdateWhenImeMove_NoRelaunchActivity()151     public void testDisplayIdUpdateWhenImeMove_NoRelaunchActivity() throws Exception {
152         testDisplayIdUpdateWhenImeMove(NoRelaunchActivity.class);
153     }
154 
testDisplayIdUpdateWhenImeMove(Class<? extends ImeTestActivity> activityClass)155     private void testDisplayIdUpdateWhenImeMove(Class<? extends ImeTestActivity> activityClass)
156             throws Exception {
157         assumeTrue(MSG_NO_MOCK_IME, supportsInstallableIme());
158 
159         final VirtualDisplaySession virtualDisplaySession = createManagedVirtualDisplaySession();
160         final MockImeSession mockImeSession = MockImeHelper.createManagedMockImeSession(this);
161 
162         assertImeShownAndMatchesDisplayId(
163                 activityClass, mockImeSession, DEFAULT_DISPLAY);
164 
165         final DisplayContent newDisplay = virtualDisplaySession
166                 .setSimulateDisplay(true)
167                 .setShowSystemDecorations(true)
168                 .setDisplayImePolicy(DISPLAY_IME_POLICY_LOCAL)
169                 .createDisplay();
170 
171         // Launch activity on the secondary display and make IME show.
172         assertImeShownAndMatchesDisplayId(
173                 activityClass, mockImeSession, newDisplay.mId);
174     }
175 
assertImeShownAndMatchesDisplayId(Class<? extends ImeTestActivity> activityClass, MockImeSession imeSession, int targetDisplayId)176     private  void assertImeShownAndMatchesDisplayId(Class<? extends ImeTestActivity> activityClass,
177             MockImeSession imeSession, int targetDisplayId) throws Exception {
178         final ImeEventStream stream = imeSession.openEventStream();
179 
180         final Intent intent = new Intent(mContext, activityClass)
181                 .putExtra(EXTRA_SHOW_IME, true).setFlags(FLAG_ACTIVITY_NEW_TASK);
182         separateTestJournal();
183         final ActivityOptions launchOptions = ActivityOptions.makeBasic();
184         launchOptions.setLaunchDisplayId(targetDisplayId);
185         getInstrumentation().getTargetContext().startActivity(intent, launchOptions.toBundle());
186 
187 
188         // Verify if IME is showed on the target display.
189         expectEvent(stream, event -> "showSoftInput".equals(event.getEventName()), TIMEOUT);
190         mWmState.waitAndAssertImeWindowShownOnDisplay(targetDisplayId);
191 
192         final int imeDisplayId = expectCommand(stream, imeSession.callGetDisplayId(), TIMEOUT)
193                 .getReturnIntegerValue();
194         assertEquals("IME#getDisplayId() must match when IME move.",
195                 targetDisplayId, imeDisplayId);
196     }
197 
198     @Test
testInputMethodManagerDisplayId()199     public void testInputMethodManagerDisplayId() {
200         // Create a simulated display.
201         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
202                 .setSimulateDisplay(true)
203                 .createDisplay();
204 
205         final Display display = mDm.getDisplay(newDisplay.mId);
206         final Context newDisplayContext = mContext.createDisplayContext(display);
207         final InputMethodManager imm = newDisplayContext.getSystemService(InputMethodManager.class);
208 
209         assertEquals("IMM#getDisplayId() must match.", newDisplay.mId, imm.getDisplayId());
210     }
211 
212     @Test
testViewGetDisplayOnPrimaryDisplay()213     public void testViewGetDisplayOnPrimaryDisplay() {
214         testViewGetDisplay(true /* isPrimary */);
215     }
216 
217     @Test
testViewGetDisplayOnSecondaryDisplay()218     public void testViewGetDisplayOnSecondaryDisplay() {
219         testViewGetDisplay(false /* isPrimary */);
220     }
221 
testViewGetDisplay(boolean isPrimary)222     private void testViewGetDisplay(boolean isPrimary) {
223         final TestActivitySession<ClientTestActivity> activitySession =
224                 createManagedTestActivitySession();
225         final DisplayContent newDisplay = createManagedVirtualDisplaySession()
226                 .setSimulateDisplay(true)
227                 .createDisplay();
228         final int displayId = isPrimary ? DEFAULT_DISPLAY : newDisplay.mId;
229 
230         separateTestJournal();
231         activitySession.launchTestActivityOnDisplaySync(ClientTestActivity.class, displayId);
232 
233         final Activity activity = activitySession.getActivity();
234         final ComponentName activityName = activity.getComponentName();
235 
236         waitAndAssertTopResumedActivity(activityName, displayId,
237                 "Activity launched on display:" + displayId + " must be focused");
238 
239         // Test View#getdisplay() from activity
240         final View view = activity.getWindow().getDecorView();
241         assertEquals("View#getDisplay() must match.", displayId, view.getDisplay().getDisplayId());
242 
243         final int[] resultDisplayId = { INVALID_DISPLAY };
244         activitySession.runOnMainAndAssertWithTimeout(
245                 () -> {
246                     // Test View#getdisplay() from WM#addView()
247                     final WindowManager wm = activity.getWindowManager();
248                     final View addedView = new View(activity);
249                     wm.addView(addedView, new WindowManager.LayoutParams());
250 
251                     // Get display ID from callback in case the added view has not be attached.
252                     addedView.addOnAttachStateChangeListener(
253                             new View.OnAttachStateChangeListener() {
254                                 @Override
255                                 public void onViewAttachedToWindow(View view) {
256                                     resultDisplayId[0] = view.getDisplay().getDisplayId();
257                                 }
258 
259                                 @Override
260                                 public void onViewDetachedFromWindow(View view) {}
261                             });
262 
263                     return displayId == resultDisplayId[0];
264                 }, TIMEOUT, "Display from added view must match. "
265                         + "Should be display:" + displayId
266                         + ", but was display:" + resultDisplayId[0]
267         );
268     }
269 
waitAndAssertConfigurationChange(ComponentName activityName)270     private void waitAndAssertConfigurationChange(ComponentName activityName) {
271         assertTrue("Must receive a single configuration change",
272                 mWmState.waitForWithAmState(
273                         state -> getCallbackCount(activityName, ON_CONFIGURATION_CHANGED) == 1,
274                         activityName + " receives configuration change"));
275     }
276 
waitAndAssertResume(ComponentName activityName)277     private void waitAndAssertResume(ComponentName activityName) {
278         assertTrue("Must be resumed once",
279                 mWmState.waitForWithAmState(
280                         state -> getCallbackCount(activityName, ON_RESUME) == 1,
281                         activityName + " performs resume"));
282     }
283 
getCallbackCount(ComponentName activityName, CommandSession.ActivityCallback callback)284     private static int getCallbackCount(ComponentName activityName,
285             CommandSession.ActivityCallback callback) {
286         final ActivityLifecycleCounts lifecycles = new ActivityLifecycleCounts(activityName);
287         return lifecycles.getCount(callback);
288     }
289 
290     public static class ClientTestActivity extends ImeTestActivity { }
291 
292     public static class NoRelaunchActivity extends ImeTestActivity { }
293 
294     public static class ImeTestActivity extends CommandSession.BasicTestActivity {
295         private EditText mEditText;
296         private boolean mShouldShowIme;
297 
298         @Override
onCreate(Bundle icicle)299         protected void onCreate(Bundle icicle) {
300             super.onCreate(icicle);
301             mShouldShowIme = getIntent().hasExtra(EXTRA_SHOW_IME);
302             if (mShouldShowIme) {
303                 mEditText = new EditText(this);
304                 final LinearLayout layout = new LinearLayout(this);
305                 layout.setOrientation(LinearLayout.VERTICAL);
306                 layout.addView(mEditText);
307                 setContentView(layout);
308             }
309         }
310 
311         @Override
onResume()312         protected void onResume() {
313             super.onResume();
314             if (mShouldShowIme) {
315                 getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
316                 mEditText.requestFocus();
317             }
318         }
319     }
320 }
321