• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.view.inputmethod.cts;
18 
19 import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
20 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
21 import static android.content.pm.PackageManager.FEATURE_INPUT_METHODS;
22 import static android.view.inputmethod.cts.util.TestUtils.getOnMainSync;
23 import static android.view.inputmethod.cts.util.TestUtils.isInputMethodPickerShown;
24 import static android.view.inputmethod.cts.util.TestUtils.waitOnMainUntil;
25 
26 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
27 import static com.android.sts.common.SystemUtil.withSetting;
28 
29 import static com.google.common.truth.Truth.assertThat;
30 import static com.google.common.truth.Truth.assertWithMessage;
31 
32 import static org.junit.Assert.assertEquals;
33 import static org.junit.Assert.assertFalse;
34 import static org.junit.Assert.assertNotEquals;
35 import static org.junit.Assert.assertNotNull;
36 import static org.junit.Assert.assertThrows;
37 import static org.junit.Assert.assertTrue;
38 import static org.junit.Assert.fail;
39 import static org.junit.Assume.assumeFalse;
40 import static org.junit.Assume.assumeTrue;
41 
42 import android.app.Instrumentation;
43 import android.app.KeyguardManager;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.pm.PackageManager;
47 import android.os.Debug;
48 import android.os.SystemClock;
49 import android.os.UserHandle;
50 import android.platform.test.annotations.AppModeFull;
51 import android.platform.test.annotations.AppModeSdkSandbox;
52 import android.platform.test.annotations.RequiresFlagsEnabled;
53 import android.platform.test.annotations.SecurityTest;
54 import android.platform.test.flag.junit.CheckFlagsRule;
55 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
56 import android.provider.Settings;
57 import android.server.wm.LockScreenSession;
58 import android.server.wm.WindowManagerStateHelper;
59 import android.text.TextUtils;
60 import android.view.View;
61 import android.view.inputmethod.EditorInfo;
62 import android.view.inputmethod.Flags;
63 import android.view.inputmethod.InputConnection;
64 import android.view.inputmethod.InputMethodInfo;
65 import android.view.inputmethod.InputMethodManager;
66 import android.view.inputmethod.InputMethodSubtype;
67 import android.view.inputmethod.cts.util.TestActivity;
68 import android.widget.EditText;
69 import android.widget.LinearLayout;
70 import android.widget.LinearLayout.LayoutParams;
71 
72 import androidx.annotation.NonNull;
73 import androidx.test.filters.MediumTest;
74 import androidx.test.platform.app.InstrumentationRegistry;
75 import androidx.test.uiautomator.By;
76 import androidx.test.uiautomator.Direction;
77 import androidx.test.uiautomator.UiDevice;
78 import androidx.test.uiautomator.Until;
79 
80 import com.android.compatibility.common.util.PollingCheck;
81 import com.android.cts.mockime.ImeSettings;
82 import com.android.cts.mockime.MockImeSession;
83 
84 import org.junit.After;
85 import org.junit.Before;
86 import org.junit.Rule;
87 import org.junit.Test;
88 
89 import java.io.File;
90 import java.io.IOException;
91 import java.lang.ref.Cleaner;
92 import java.lang.reflect.Field;
93 import java.util.List;
94 import java.util.concurrent.CountDownLatch;
95 import java.util.concurrent.TimeUnit;
96 import java.util.concurrent.atomic.AtomicReference;
97 import java.util.stream.Collectors;
98 
99 @MediumTest
100 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).")
101 public final class InputMethodManagerTest {
102     private static final String MOCK_IME_ID = "com.android.cts.mockime/.MockIme";
103     private static final String MOCK_IME_LABEL = "Mock IME";
104     private static final String HIDDEN_FROM_PICKER_IME_ID =
105             "com.android.cts.hiddenfrompickerime/.HiddenFromPickerIme";
106     private static final String HIDDEN_FROM_PICKER_IME_LABEL = "Hidden From Picker IME";
107     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
108     // TODO(b/371520375): Remove after UiAutomator scroll waits for animation to finish.
109     private static final long SCROLL_TIMEOUT_MS = 500;
110 
111     private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
112 
113     @Rule
114     public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
115 
116     private final WindowManagerStateHelper mWmStateHelper = new WindowManagerStateHelper();
117 
118     private Instrumentation mInstrumentation;
119     private Context mContext;
120     private InputMethodManager mImManager;
121     private boolean mNeedsImeReset = false;
122 
123     @Before
setup()124     public void setup() {
125         mInstrumentation = InstrumentationRegistry.getInstrumentation();
126         mContext = mInstrumentation.getTargetContext();
127         mImManager = mContext.getSystemService(InputMethodManager.class);
128     }
129 
130     @After
resetImes()131     public void resetImes() {
132         if (mNeedsImeReset) {
133             runShellCommandOrThrow("ime reset --user " + UserHandle.myUserId());
134             mNeedsImeReset = false;
135         }
136     }
137 
138     /**
139      * Verifies that the test API {@link InputMethodManager#isInputMethodPickerShown()} is properly
140      * protected with some permission.
141      *
142      * <p>This is a regression test for Bug 237317525.</p>
143      */
144     @SecurityTest(minPatchLevel = "unknown")
145     @Test
testIsInputMethodPickerShownProtection()146     public void testIsInputMethodPickerShownProtection() {
147         assumeTrue(mContext.getPackageManager().hasSystemFeature(FEATURE_INPUT_METHODS));
148         assertThrows("InputMethodManager#isInputMethodPickerShown() must not be accessible to "
149                 + "normal apps.", SecurityException.class, mImManager::isInputMethodPickerShown);
150     }
151 
152     /**
153      * Verifies that the test API {@link InputMethodManager#addVirtualStylusIdForTestSession()} is
154      * properly protected with some permission.
155      */
156     @Test
testAddVirtualStylusIdForTestSessionProtection()157     public void testAddVirtualStylusIdForTestSessionProtection() {
158         assumeTrue(mContext.getPackageManager().hasSystemFeature(FEATURE_INPUT_METHODS));
159         assertThrows("InputMethodManager#addVirtualStylusIdForTestSession() must not be accessible "
160                 + "to normal apps.", SecurityException.class,
161                 mImManager::addVirtualStylusIdForTestSession);
162     }
163 
164     /**
165      * Verifies that the test API {@link InputMethodManager#setStylusWindowIdleTimeoutForTest(long)}
166      * is properly protected with some permission.
167      */
168     @Test
testSetStylusWindowIdleTimeoutForTestProtection()169     public void testSetStylusWindowIdleTimeoutForTestProtection() {
170         assumeTrue(mContext.getPackageManager().hasSystemFeature(FEATURE_INPUT_METHODS));
171 
172         assertThrows("InputMethodManager#setStylusWindowIdleTimeoutForTest(long) must not"
173                         + " be accessible to normal apps.", SecurityException.class,
174                 () -> mImManager.setStylusWindowIdleTimeoutForTest(0));
175     }
176 
177     @Test
testIsActive()178     public void testIsActive() throws Throwable {
179         final AtomicReference<EditText> focusedEditTextRef = new AtomicReference<>();
180         final AtomicReference<EditText> nonFocusedEditTextRef = new AtomicReference<>();
181         TestActivity.startSync(activity -> {
182             final LinearLayout layout = new LinearLayout(activity);
183             layout.setOrientation(LinearLayout.VERTICAL);
184 
185             final EditText focusedEditText = new EditText(activity);
186             layout.addView(focusedEditText);
187             focusedEditTextRef.set(focusedEditText);
188             focusedEditText.requestFocus();
189 
190             final EditText nonFocusedEditText = new EditText(activity);
191             layout.addView(nonFocusedEditText);
192             nonFocusedEditTextRef.set(nonFocusedEditText);
193 
194             return layout;
195         });
196         final View focusedEditText = focusedEditTextRef.get();
197         waitOnMainUntil(() -> mImManager.hasActiveInputConnection(focusedEditText), TIMEOUT);
198         assertTrue(getOnMainSync(() -> mImManager.isActive(focusedEditText)));
199         assertFalse(getOnMainSync(() -> mImManager.isActive(nonFocusedEditTextRef.get())));
200     }
201 
202     @Test
testIsAcceptingText()203     public void testIsAcceptingText() throws Throwable {
204         final AtomicReference<EditText> focusedFakeEditTextRef = new AtomicReference<>();
205         final CountDownLatch latch = new CountDownLatch(1);
206         TestActivity.startSync(activity -> {
207             final LinearLayout layout = new LinearLayout(activity);
208             layout.setOrientation(LinearLayout.VERTICAL);
209 
210             final EditText focusedFakeEditText = new EditText(activity) {
211                 @Override
212                 public InputConnection onCreateInputConnection(EditorInfo info) {
213                     super.onCreateInputConnection(info);
214                     latch.countDown();
215                     return null;
216                 }
217             };
218             layout.addView(focusedFakeEditText);
219             focusedFakeEditTextRef.set(focusedFakeEditText);
220             focusedFakeEditText.requestFocus();
221             return layout;
222         });
223         assertTrue(latch.await(TIMEOUT, TimeUnit.MILLISECONDS));
224         assertFalse("InputMethodManager#isAcceptingText() must return false "
225                 + "if target View returns null from onCreateInputConnection().",
226                 getOnMainSync(() -> mImManager.isAcceptingText()));
227     }
228 
229     @Test
testGetInputMethodList()230     public void testGetInputMethodList() {
231         final List<InputMethodInfo> enabledImes = mImManager.getEnabledInputMethodList();
232         assertNotNull(enabledImes);
233         final List<InputMethodInfo> imes = mImManager.getInputMethodList();
234         assertNotNull(imes);
235 
236         // Make sure that IMM#getEnabledInputMethodList() is a subset of IMM#getInputMethodList().
237         // TODO: Consider moving this to hostside test to test more realistic and useful scenario.
238         if (!imes.containsAll(enabledImes)) {
239             fail("Enabled IMEs must be a subset of all the IMEs.\n"
240                     + "all=" + dumpInputMethodInfoList(imes) + "\n"
241                     + "enabled=" + dumpInputMethodInfoList(enabledImes));
242         }
243     }
244 
245     @Test
testGetEnabledInputMethodList()246     public void testGetEnabledInputMethodList() {
247         enableImes(HIDDEN_FROM_PICKER_IME_ID);
248         final List<InputMethodInfo> enabledImes = mImManager.getEnabledInputMethodList();
249         assertThat(enabledImes).isNotNull();
250         final List<String> enabledImeIds =
251                 enabledImes.stream().map(InputMethodInfo::getId).collect(Collectors.toList());
252         assertThat(enabledImeIds).contains(HIDDEN_FROM_PICKER_IME_ID);
253     }
254 
dumpInputMethodInfoList(@onNull List<InputMethodInfo> imiList)255     private static String dumpInputMethodInfoList(@NonNull List<InputMethodInfo> imiList) {
256         return "[" + imiList.stream().map(imi -> {
257             final StringBuilder sb = new StringBuilder();
258             final int subtypeCount = imi.getSubtypeCount();
259             sb.append("InputMethodInfo{id=").append(imi.getId())
260                     .append(", subtypeCount=").append(subtypeCount)
261                     .append(", subtypes=[");
262             for (int i = 0; i < subtypeCount; ++i) {
263                 if (i != 0) {
264                     sb.append(",");
265                 }
266                 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
267                 sb.append("{id=0x").append(Integer.toHexString(subtype.hashCode()));
268                 if (!TextUtils.isEmpty(subtype.getMode())) {
269                     sb.append(",mode=").append(subtype.getMode());
270                 }
271                 if (!TextUtils.isEmpty(subtype.getLocale())) {
272                     sb.append(",locale=").append(subtype.getLocale());
273                 }
274                 if (!TextUtils.isEmpty(subtype.getLanguageTag())) {
275                     sb.append(",languageTag=").append(subtype.getLanguageTag());
276                 }
277                 sb.append("}");
278             }
279             sb.append("]");
280             return sb.toString();
281         }).collect(Collectors.joining(", ")) + "]";
282     }
283 
284     /**
285      * Shows the Input Method Picker menu and verifies Mock IME is shown,
286      * but Hidden from picker IME is not.
287      */
288     @AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
289     @Test
testInputMethodPickerShownItems()290     public void testInputMethodPickerShownItems() throws Exception {
291         assumeFalse(mContext.getPackageManager().hasSystemFeature(
292                 PackageManager.FEATURE_AUTOMOTIVE));
293         assumeTrue(mContext.getPackageManager().hasSystemFeature(
294                 PackageManager.FEATURE_INPUT_METHODS));
295         enableImes(MOCK_IME_ID, HIDDEN_FROM_PICKER_IME_ID);
296 
297         startActivityAndShowInputMethodPicker(false /* showWhenLocked */);
298 
299         final UiDevice uiDevice = getUiDevice();
300         if (mFlagsValueProvider.getBoolean(Flags.FLAG_IME_SWITCHER_REVAMP)) {
301             final var list = uiDevice.wait(Until.findObject(By.res("android:id/list")), TIMEOUT);
302             assertNotNull("List view should be found.", list);
303 
304             // Make sure the list starts at the top.
305             list.scroll(Direction.UP, 100f);
306             final var hasMockIme = list.scrollUntil(Direction.DOWN,
307                     Until.hasObject(By.text(MOCK_IME_LABEL)));
308             assertWithMessage("Mock IME should be found")
309                     .that(hasMockIme).isTrue();
310 
311             // Reset the list to the top.
312             list.scroll(Direction.UP, 100f);
313             final var hasHiddenFromPickerIme = list.scrollUntil(Direction.DOWN,
314                     Until.hasObject(By.text(HIDDEN_FROM_PICKER_IME_LABEL)));
315             assertWithMessage("Hidden from picker IME should not be found")
316                     .that(hasHiddenFromPickerIme).isFalse();
317         } else {
318             final var hasMockIme = uiDevice.wait(
319                     Until.hasObject(By.text(MOCK_IME_LABEL)), TIMEOUT);
320             assertWithMessage("Mock IME should be found")
321                     .that(hasMockIme).isTrue();
322 
323             final var hasHiddenFromPickerIme = uiDevice.wait(
324                     Until.hasObject(By.text(HIDDEN_FROM_PICKER_IME_ID)), TIMEOUT);
325             assertWithMessage("Hidden from picker IME should not be found")
326                     .that(hasHiddenFromPickerIme).isFalse();
327         }
328 
329         // Make sure that InputMethodPicker can be closed with ACTION_CLOSE_SYSTEM_DIALOGS
330         mContext.sendBroadcast(
331                 new Intent(ACTION_CLOSE_SYSTEM_DIALOGS).setFlags(FLAG_RECEIVER_FOREGROUND));
332         waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
333                 "InputMethod picker should be closed");
334     }
335 
336     /**
337      * Shows the Input Method Picker menu and verifies switching to Mock IME by tapping on the item.
338      */
339     @AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
340     @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
341     @Test
testInputMethodPickerSwitchIme()342     public void testInputMethodPickerSwitchIme() throws Exception {
343         assumeFalse(mContext.getPackageManager().hasSystemFeature(
344                 PackageManager.FEATURE_AUTOMOTIVE));
345         assumeTrue(mContext.getPackageManager().hasSystemFeature(
346                 PackageManager.FEATURE_INPUT_METHODS));
347         // Initialize MockIME (without setting it as current IME) before selecting it from the menu.
348         try (var ignored = MockImeSession.create(
349                 mInstrumentation.getContext(),
350                 mInstrumentation.getUiAutomation(),
351                 new ImeSettings.Builder()
352                         .setSuppressSetIme(true))) {
353             startActivityAndShowInputMethodPicker(false /* showWhenLocked */);
354 
355             final var info = mImManager.getCurrentInputMethodInfo();
356             assertNotEquals(MOCK_IME_ID, info != null ? info.getId() : null);
357 
358             final UiDevice uiDevice = getUiDevice();
359             final var list = uiDevice.wait(Until.findObject(By.res("android:id/list")), TIMEOUT);
360             assertNotNull("List view should be found.", list);
361 
362             // Make sure the list starts at the top.
363             list.scroll(Direction.UP, 100f);
364             final var mockImeUiObject = list.scrollUntil(Direction.DOWN,
365                     Until.findObject(By.res("android:id/list_item")
366                             .hasDescendant(By.text(MOCK_IME_LABEL))));
367             assertNotNull("Mock IME should be found", mockImeUiObject);
368 
369             // TODO(b/371520375): Remove after UiAutomator scroll waits for animation to finish.
370             SystemClock.sleep(SCROLL_TIMEOUT_MS);
371 
372             // Tapping on a menu item should dismiss the menu.
373             mockImeUiObject.click();
374             waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
375                     "InputMethod picker should be closed");
376 
377             final var newInfo = mImManager.getCurrentInputMethodInfo();
378             assertNotEquals(info, newInfo);
379             assertEquals(MOCK_IME_ID, newInfo != null ? newInfo.getId() : null);
380         }
381     }
382 
383     /**
384      * Shows the Input Method Picker menu and verifies opening the IME Language Settings activity
385      * by tapping on the button, when the device is provisioned.
386      */
387     @AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
388     @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
389     @Test
testInputMethodPickerOpenLanguageSettings()390     public void testInputMethodPickerOpenLanguageSettings() throws Exception {
391         assumeFalse(mContext.getPackageManager().hasSystemFeature(
392                 PackageManager.FEATURE_AUTOMOTIVE));
393         assumeTrue(mContext.getPackageManager().hasSystemFeature(
394                 PackageManager.FEATURE_INPUT_METHODS));
395         try (var ignored1 = withSetting(mInstrumentation, "global",
396                 Settings.Global.DEVICE_PROVISIONED, "1");
397                 var ignored2 = MockImeSession.create(
398                         mInstrumentation.getContext(),
399                         mInstrumentation.getUiAutomation(),
400                     new ImeSettings.Builder())) {
401             final var activity = startActivityAndShowInputMethodPicker(false /* showWhenLocked */);
402 
403             final var info = mImManager.getCurrentInputMethodInfo();
404             assertEquals(MOCK_IME_ID, info != null ? info.getId() : null);
405 
406             final UiDevice uiDevice = getUiDevice();
407 
408             final var container = uiDevice.wait(Until.findObject(By.res("android:id/container")),
409                     TIMEOUT);
410             assertNotNull("Container view should be found.", container);
411 
412             // Make sure the container starts at the top.
413             container.scroll(Direction.UP, 100f);
414             final var languageSettingsButtonUiObject = container.scrollUntil(Direction.DOWN,
415                     Until.findObject(By.res("android:id/button1")));
416             assertNotNull("Language settings button should be found",
417                     languageSettingsButtonUiObject);
418 
419             // TODO(b/371520375): Remove after UiAutomator scroll waits for animation to finish.
420             SystemClock.sleep(SCROLL_TIMEOUT_MS);
421 
422             languageSettingsButtonUiObject.click();
423 
424             // Tapping on the language settings button should dismiss the menu.
425             waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
426                     "InputMethod picker should be closed");
427 
428             waitOnMainUntil(() -> !activity.hasWindowFocus(), TIMEOUT,
429                     "Test activity shouldn't be focused");
430         }
431     }
432 
433     /**
434      * Shows the Input Method Picker menu and verifies the IME Language Settings button is not
435      * visible when the screen is locked.
436      */
437     @AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
438     @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
439     @Test
testInputMethodPickerNoLanguageSettingsWhenScreenLocked()440     public void testInputMethodPickerNoLanguageSettingsWhenScreenLocked() throws Exception {
441         assumeFalse(mContext.getPackageManager().hasSystemFeature(
442                 PackageManager.FEATURE_AUTOMOTIVE));
443         assumeFalse(mContext.getPackageManager().hasSystemFeature(
444                 PackageManager.FEATURE_LEANBACK));
445         assumeTrue(mContext.getPackageManager().hasSystemFeature(
446                 PackageManager.FEATURE_INPUT_METHODS));
447 
448         try (var lockScreenSession = new LockScreenSession(mInstrumentation, mWmStateHelper);
449                 var ignored = MockImeSession.create(
450                         mInstrumentation.getContext(),
451                         mInstrumentation.getUiAutomation(),
452                         new ImeSettings.Builder())) {
453 
454             lockScreenSession.setLockCredential().gotoKeyguard();
455 
456             final var km = mContext.getSystemService(KeyguardManager.class);
457             assertNotNull("KeyguardManager must be found", km);
458             assertTrue("keyguard is locked", km.isKeyguardLocked());
459             assertTrue("keyguard is secure", km.isKeyguardSecure());
460 
461             startActivityAndShowInputMethodPicker(true /* showWhenLocked */);
462 
463             final UiDevice uiDevice = getUiDevice();
464 
465             final var container = uiDevice.wait(Until.findObject(By.res("android:id/container")),
466                     TIMEOUT);
467             assertNotNull("Container view should be found.", container);
468 
469             // Make sure the container starts at the top.
470             container.scroll(Direction.UP, 100f);
471             final boolean hasButton = container.scrollUntil(Direction.DOWN,
472                     Until.hasObject(By.res("android:id/button1")));
473             assertFalse("Language settings button should not be found", hasButton);
474 
475             mContext.sendBroadcast(
476                     new Intent(ACTION_CLOSE_SYSTEM_DIALOGS).setFlags(FLAG_RECEIVER_FOREGROUND));
477             waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
478                     "InputMethod picker should be closed");
479         }
480     }
481 
482     /**
483      * Shows the Input Method Picker menu and verifies the IME Language Settings button is not
484      * visible when the device is not provisioned.
485      */
486     @AppModeFull(reason = "Instant apps cannot rely on ACTION_CLOSE_SYSTEM_DIALOGS")
487     @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
488     @Test
testInputMethodPickerNoLanguageSettingsWhenDeviceNotProvisioned()489     public void testInputMethodPickerNoLanguageSettingsWhenDeviceNotProvisioned() throws Exception {
490         assumeFalse(mContext.getPackageManager().hasSystemFeature(
491                 PackageManager.FEATURE_AUTOMOTIVE));
492         assumeTrue(mContext.getPackageManager().hasSystemFeature(
493                 PackageManager.FEATURE_INPUT_METHODS));
494         try (var ignored1 = withSetting(mInstrumentation, "global",
495                 Settings.Global.DEVICE_PROVISIONED, "0");
496                 var ignored2 = MockImeSession.create(
497                         mInstrumentation.getContext(),
498                         mInstrumentation.getUiAutomation(),
499                      new ImeSettings.Builder())) {
500             startActivityAndShowInputMethodPicker(false /* showWhenLocked */);
501 
502             final UiDevice uiDevice = getUiDevice();
503 
504             final var container = uiDevice.wait(Until.findObject(By.res("android:id/container")),
505                     TIMEOUT);
506             assertNotNull("Container view should be found.", container);
507 
508             // Make sure the container starts at the top.
509             container.scroll(Direction.UP, 100f);
510             final boolean hasButton = container.scrollUntil(Direction.DOWN,
511                     Until.hasObject(By.res("android:id/button1")));
512             assertFalse("Language settings button should not be found", hasButton);
513 
514             mContext.sendBroadcast(
515                     new Intent(ACTION_CLOSE_SYSTEM_DIALOGS).setFlags(FLAG_RECEIVER_FOREGROUND));
516             waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
517                     "InputMethod picker should be closed");
518         }
519     }
520 
521     @Test
testNoStrongServedViewReferenceAfterWindowDetached()522     public void testNoStrongServedViewReferenceAfterWindowDetached() throws IOException {
523         var receivedSignalCleaned = new CountDownLatch(1);
524         Runnable r = () -> {
525             var viewRef = new View[1];
526             TestActivity testActivity = TestActivity.startSync(activity -> {
527                 viewRef[0] = new EditText(activity);
528                 viewRef[0].setLayoutParams(new LayoutParams(
529                         LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
530                 viewRef[0].requestFocus();
531                 return viewRef[0];
532             });
533             // wait until editText becomes active
534             final InputMethodManager imm = testActivity.getSystemService(InputMethodManager.class);
535             PollingCheck.waitFor(() -> imm.hasActiveInputConnection(viewRef[0]));
536 
537             Cleaner.create().register(viewRef[0], receivedSignalCleaned::countDown);
538             viewRef[0] = null;
539 
540             // finishing the activity should destroy the reference inside IMM
541             testActivity.finish();
542         };
543         r.run();
544 
545         waitForWithGc(() -> receivedSignalCleaned.getCount() == 0);
546     }
547 
548     /**
549      * Creates the test activity and waits for it to start, and then shows the IME Switcher menu.
550      *
551      * @param showWhenLocked whether the test activity should be shown when the screen is locked.
552      * @return the started test activity.
553      */
554     @NonNull
startActivityAndShowInputMethodPicker(boolean showWhenLocked)555     private TestActivity startActivityAndShowInputMethodPicker(boolean showWhenLocked)
556             throws Exception {
557         final var testActivity = TestActivity.startSync(activity -> {
558             final View view = new View(activity);
559             view.setLayoutParams(new LayoutParams(
560                     LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
561             activity.setShowWhenLocked(showWhenLocked);
562             return view;
563         });
564         waitOnMainUntil(testActivity::hasWindowFocus, TIMEOUT, "TestActivity should be focused");
565 
566         // Make sure that InputMethodPicker is not shown in the initial state.
567         mContext.sendBroadcast(
568                 new Intent(ACTION_CLOSE_SYSTEM_DIALOGS).setFlags(FLAG_RECEIVER_FOREGROUND));
569         waitOnMainUntil(() -> !isInputMethodPickerShown(mImManager), TIMEOUT,
570                 "InputMethod picker should be closed");
571 
572         // Test InputMethodManager#showInputMethodPicker() works as expected.
573         mImManager.showInputMethodPicker();
574         waitOnMainUntil(() -> isInputMethodPickerShown(mImManager), TIMEOUT,
575                 "InputMethod picker should be shown");
576 
577         return testActivity;
578     }
579 
580     @NonNull
getUiDevice()581     private UiDevice getUiDevice() {
582         // UiDevice.getInstance(Instrumentation) may return a cached instance if it's already called
583         // in this process and for some unknown reasons it fails to detect MOCK_IME_LABEL.
584         // As a quick workaround, here we clear its internal singleton value.
585         // TODO(b/230698095): Fix this in UiDevice or stop using UiDevice.
586         try {
587             final Field field = UiDevice.class.getDeclaredField("sInstance");
588             field.setAccessible(true);
589             field.set(null, null);
590         } catch (NoSuchFieldException | SecurityException | IllegalArgumentException
591                  | IllegalAccessException e) {
592             // We don't treat this as an error as it's an implementation detail of UiDevice.
593         }
594         return UiDevice.getInstance(mInstrumentation);
595     }
596 
waitForWithGc(PollingCheck.PollingCheckCondition condition)597     private void waitForWithGc(PollingCheck.PollingCheckCondition condition) throws IOException {
598         try {
599             PollingCheck.waitFor(() -> {
600                 Runtime.getRuntime().gc();
601                 return condition.canProceed();
602             });
603         } catch (AssertionError e) {
604             var dir = new File("/sdcard/DumpOnFailure");
605             if (!dir.exists()) {
606                 assertTrue("Unable to create " + dir, dir.mkdir());
607             }
608             File heap = new File(dir, "inputmethod-dump.hprof");
609             Debug.dumpHprofData(heap.getAbsolutePath());
610             throw new AssertionError("Dumped heap in device at " + heap.getAbsolutePath(), e);
611         }
612     }
613 
enableImes(String... ids)614     private void enableImes(String... ids) {
615         for (String id : ids) {
616             runShellCommandOrThrow("ime enable --user " + UserHandle.myUserId() + " " + id);
617         }
618         mNeedsImeReset = true;
619     }
620 }
621