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