1 /* 2 * Copyright (C) 2022 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 com.android.inputmethodservice; 18 19 import static android.view.WindowInsets.Type.captionBar; 20 21 import static com.android.compatibility.common.util.SystemUtil.eventually; 22 23 import static com.google.common.truth.Truth.assertThat; 24 25 import static org.junit.Assert.assertEquals; 26 import static org.junit.Assert.fail; 27 import static org.junit.Assume.assumeFalse; 28 29 import android.app.Instrumentation; 30 import android.content.Context; 31 import android.content.res.Configuration; 32 import android.graphics.Insets; 33 import android.os.RemoteException; 34 import android.provider.Settings; 35 import android.support.test.uiautomator.By; 36 import android.support.test.uiautomator.UiDevice; 37 import android.support.test.uiautomator.UiObject2; 38 import android.support.test.uiautomator.Until; 39 import android.util.Log; 40 import android.view.WindowManagerGlobal; 41 import android.view.inputmethod.EditorInfo; 42 import android.view.inputmethod.InputMethodManager; 43 44 import androidx.test.ext.junit.runners.AndroidJUnit4; 45 import androidx.test.filters.MediumTest; 46 import androidx.test.platform.app.InstrumentationRegistry; 47 48 import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper; 49 import com.android.apps.inputmethod.simpleime.testing.TestActivity; 50 51 import org.junit.After; 52 import org.junit.Before; 53 import org.junit.Test; 54 import org.junit.runner.RunWith; 55 56 import java.io.IOException; 57 import java.util.concurrent.CountDownLatch; 58 import java.util.concurrent.TimeUnit; 59 60 @RunWith(AndroidJUnit4.class) 61 @MediumTest 62 public class InputMethodServiceTest { 63 private static final String TAG = "SimpleIMSTest"; 64 private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService"; 65 private static final String EDIT_TEXT_DESC = "Input box"; 66 private static final long TIMEOUT_IN_SECONDS = 3; 67 private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = 68 "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1"; 69 private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = 70 "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0"; 71 72 private Instrumentation mInstrumentation; 73 private UiDevice mUiDevice; 74 private Context mContext; 75 private String mTargetPackageName; 76 private TestActivity mActivity; 77 private InputMethodServiceWrapper mInputMethodService; 78 private String mInputMethodId; 79 private boolean mShowImeWithHardKeyboardEnabled; 80 81 @Before setUp()82 public void setUp() throws Exception { 83 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 84 mUiDevice = UiDevice.getInstance(mInstrumentation); 85 mContext = mInstrumentation.getContext(); 86 mTargetPackageName = mInstrumentation.getTargetContext().getPackageName(); 87 mInputMethodId = getInputMethodId(); 88 prepareIme(); 89 prepareEditor(); 90 mInstrumentation.waitForIdleSync(); 91 mUiDevice.freezeRotation(); 92 mUiDevice.setOrientationNatural(); 93 // Waits for input binding ready. 94 eventually(() -> { 95 mInputMethodService = 96 InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting(); 97 assertThat(mInputMethodService).isNotNull(); 98 99 // The editor won't bring up keyboard by default. 100 assertThat(mInputMethodService.getCurrentInputStarted()).isTrue(); 101 assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse(); 102 }); 103 // Save the original value of show_ime_with_hard_keyboard from Settings. 104 mShowImeWithHardKeyboardEnabled = Settings.Secure.getInt( 105 mInputMethodService.getContentResolver(), 106 Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0; 107 } 108 109 @After tearDown()110 public void tearDown() throws Exception { 111 mUiDevice.unfreezeRotation(); 112 executeShellCommand("ime disable " + mInputMethodId); 113 // Change back the original value of show_ime_with_hard_keyboard in Settings. 114 executeShellCommand(mShowImeWithHardKeyboardEnabled 115 ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD 116 : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); 117 } 118 119 /** 120 * This checks that the IME can be shown and hidden by user actions 121 * (i.e. tapping on an EditText, tapping the Home button). 122 */ 123 @Test testShowHideKeyboard_byUserAction()124 public void testShowHideKeyboard_byUserAction() throws Exception { 125 setShowImeWithHardKeyboard(true /* enabled */); 126 127 // Performs click on editor box to bring up the soft keyboard. 128 Log.i(TAG, "Click on EditText."); 129 verifyInputViewStatus( 130 () -> clickOnEditorText(), 131 true /* expected */, 132 true /* inputViewStarted */); 133 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 134 135 // Press home key to hide soft keyboard. 136 Log.i(TAG, "Press home"); 137 verifyInputViewStatus( 138 () -> assertThat(mUiDevice.pressHome()).isTrue(), 139 true /* expected */, 140 false /* inputViewStarted */); 141 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 142 } 143 144 /** 145 * This checks that the IME can be shown and hidden using the WindowInsetsController APIs. 146 */ 147 @Test testShowHideKeyboard_byApi()148 public void testShowHideKeyboard_byApi() throws Exception { 149 setShowImeWithHardKeyboard(true /* enabled */); 150 151 // Triggers to show IME via public API. 152 verifyInputViewStatus( 153 () -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(), 154 true /* expected */, 155 true /* inputViewStarted */); 156 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 157 158 // Triggers to hide IME via public API. 159 verifyInputViewStatusOnMainSync( 160 () -> assertThat(mActivity.hideImeWithWindowInsetsController()).isTrue(), 161 true /* expected */, 162 false /* inputViewStarted */); 163 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 164 } 165 166 /** 167 * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf. 168 */ 169 @Test testShowHideSelf()170 public void testShowHideSelf() throws Exception { 171 setShowImeWithHardKeyboard(true /* enabled */); 172 173 // IME request to show itself without any flags, expect shown. 174 Log.i(TAG, "Call IMS#requestShowSelf(0)"); 175 verifyInputViewStatusOnMainSync( 176 () -> mInputMethodService.requestShowSelf(0 /* flags */), 177 true /* expected */, 178 true /* inputViewStarted */); 179 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 180 181 // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown). 182 Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); 183 verifyInputViewStatusOnMainSync( 184 () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY), 185 false /* expected */, 186 true /* inputViewStarted */); 187 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 188 189 // IME request to hide itself without any flags, expect hidden. 190 Log.i(TAG, "Call IMS#requestHideSelf(0)"); 191 verifyInputViewStatusOnMainSync( 192 () -> mInputMethodService.requestHideSelf(0 /* flags */), 193 true /* expected */, 194 false /* inputViewStarted */); 195 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 196 197 // IME request to show itself with flag SHOW_IMPLICIT, expect shown. 198 Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)"); 199 verifyInputViewStatusOnMainSync( 200 () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), 201 true /* expected */, 202 true /* inputViewStarted */); 203 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 204 205 // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden. 206 Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); 207 verifyInputViewStatusOnMainSync( 208 () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY), 209 true /* expected */, 210 false /* inputViewStarted */); 211 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 212 } 213 214 /** 215 * This checks the return value of IMS#onEvaluateInputViewShown, 216 * when show_ime_with_hard_keyboard is enabled. 217 */ 218 @Test testOnEvaluateInputViewShown_showImeWithHardKeyboard()219 public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() throws Exception { 220 setShowImeWithHardKeyboard(true /* enabled */); 221 222 mInputMethodService.getResources().getConfiguration().keyboard = 223 Configuration.KEYBOARD_QWERTY; 224 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 225 Configuration.HARDKEYBOARDHIDDEN_NO; 226 eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue()); 227 228 mInputMethodService.getResources().getConfiguration().keyboard = 229 Configuration.KEYBOARD_NOKEYS; 230 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 231 Configuration.HARDKEYBOARDHIDDEN_NO; 232 eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue()); 233 234 mInputMethodService.getResources().getConfiguration().keyboard = 235 Configuration.KEYBOARD_QWERTY; 236 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 237 Configuration.HARDKEYBOARDHIDDEN_YES; 238 eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue()); 239 } 240 241 /** 242 * This checks the return value of IMSonEvaluateInputViewShown, 243 * when show_ime_with_hard_keyboard is disabled. 244 */ 245 @Test testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard()246 public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() throws Exception { 247 setShowImeWithHardKeyboard(false /* enabled */); 248 249 mInputMethodService.getResources().getConfiguration().keyboard = 250 Configuration.KEYBOARD_QWERTY; 251 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 252 Configuration.HARDKEYBOARDHIDDEN_NO; 253 eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isFalse()); 254 255 mInputMethodService.getResources().getConfiguration().keyboard = 256 Configuration.KEYBOARD_NOKEYS; 257 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 258 Configuration.HARDKEYBOARDHIDDEN_NO; 259 eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue()); 260 261 mInputMethodService.getResources().getConfiguration().keyboard = 262 Configuration.KEYBOARD_QWERTY; 263 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 264 Configuration.HARDKEYBOARDHIDDEN_YES; 265 eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue()); 266 } 267 268 /** 269 * This checks that any (implicit or explicit) show request, 270 * when IMS#onEvaluateInputViewShown returns false, results in the IME not being shown. 271 */ 272 @Test testShowSoftInput_disableShowImeWithHardKeyboard()273 public void testShowSoftInput_disableShowImeWithHardKeyboard() throws Exception { 274 setShowImeWithHardKeyboard(false /* enabled */); 275 276 // Simulate connecting a hard keyboard. 277 mInputMethodService.getResources().getConfiguration().keyboard = 278 Configuration.KEYBOARD_QWERTY; 279 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 280 Configuration.HARDKEYBOARDHIDDEN_NO; 281 282 // When InputMethodService#onEvaluateInputViewShown() returns false, the Ime should not be 283 // shown no matter what the show flag is. 284 verifyInputViewStatusOnMainSync(() -> assertThat( 285 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), 286 false /* expected */, 287 false /* inputViewStarted */); 288 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 289 290 verifyInputViewStatusOnMainSync( 291 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), 292 false /* expected */, 293 false /* inputViewStarted */); 294 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 295 } 296 297 /** 298 * This checks that an explicit show request results in the IME being shown. 299 */ 300 @Test testShowSoftInputExplicitly()301 public void testShowSoftInputExplicitly() throws Exception { 302 setShowImeWithHardKeyboard(true /* enabled */); 303 304 // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the 305 // Ime should be shown. 306 verifyInputViewStatusOnMainSync( 307 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), 308 true /* expected */, 309 true /* inputViewStarted */); 310 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 311 } 312 313 /** 314 * This checks that an implicit show request results in the IME being shown. 315 */ 316 @Test testShowSoftInputImplicitly()317 public void testShowSoftInputImplicitly() throws Exception { 318 setShowImeWithHardKeyboard(true /* enabled */); 319 320 // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT, 321 // the IME should be shown. 322 verifyInputViewStatusOnMainSync(() -> assertThat( 323 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), 324 true /* expected */, 325 true /* inputViewStarted */); 326 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 327 } 328 329 /** 330 * This checks that an explicit show request when the IME is not previously shown, 331 * and it should be shown in fullscreen mode, results in the IME being shown. 332 */ 333 @Test testShowSoftInputExplicitly_fullScreenMode()334 public void testShowSoftInputExplicitly_fullScreenMode() throws Exception { 335 setShowImeWithHardKeyboard(true /* enabled */); 336 337 // Set orientation landscape to enable fullscreen mode. 338 setOrientation(2); 339 eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse()); 340 // Wait for the TestActivity to be recreated. 341 eventually(() -> 342 assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); 343 // Get the new TestActivity. 344 mActivity = TestActivity.getLastCreatedInstance(); 345 assertThat(mActivity).isNotNull(); 346 InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 347 // Wait for the new EditText to be served by InputMethodManager. 348 eventually(() -> assertThat( 349 imm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); 350 351 verifyInputViewStatusOnMainSync(() -> assertThat( 352 mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), 353 true /* expected */, 354 true /* inputViewStarted */); 355 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 356 } 357 358 /** 359 * This checks that an implicit show request when the IME is not previously shown, 360 * and it should be shown in fullscreen mode, results in the IME not being shown. 361 */ 362 @Test testShowSoftInputImplicitly_fullScreenMode()363 public void testShowSoftInputImplicitly_fullScreenMode() throws Exception { 364 setShowImeWithHardKeyboard(true /* enabled */); 365 366 // Set orientation landscape to enable fullscreen mode. 367 setOrientation(2); 368 eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse()); 369 // Wait for the TestActivity to be recreated. 370 eventually(() -> 371 assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); 372 // Get the new TestActivity. 373 mActivity = TestActivity.getLastCreatedInstance(); 374 assertThat(mActivity).isNotNull(); 375 InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); 376 // Wait for the new EditText to be served by InputMethodManager. 377 eventually(() -> assertThat( 378 imm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); 379 380 verifyInputViewStatusOnMainSync(() -> assertThat( 381 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), 382 false /* expected */, 383 false /* inputViewStarted */); 384 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 385 } 386 387 /** 388 * This checks that an explicit show request when a hard keyboard is connected, 389 * results in the IME being shown. 390 */ 391 @Test testShowSoftInputExplicitly_withHardKeyboard()392 public void testShowSoftInputExplicitly_withHardKeyboard() throws Exception { 393 setShowImeWithHardKeyboard(false /* enabled */); 394 395 // Simulate connecting a hard keyboard. 396 mInputMethodService.getResources().getConfiguration().keyboard = 397 Configuration.KEYBOARD_QWERTY; 398 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 399 Configuration.HARDKEYBOARDHIDDEN_YES; 400 401 verifyInputViewStatusOnMainSync(() -> assertThat( 402 mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), 403 true /* expected */, 404 true /* inputViewStarted */); 405 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 406 } 407 408 /** 409 * This checks that an implicit show request when a hard keyboard is connected, 410 * results in the IME not being shown. 411 */ 412 @Test testShowSoftInputImplicitly_withHardKeyboard()413 public void testShowSoftInputImplicitly_withHardKeyboard() throws Exception { 414 setShowImeWithHardKeyboard(false /* enabled */); 415 416 // Simulate connecting a hard keyboard. 417 mInputMethodService.getResources().getConfiguration().keyboard = 418 Configuration.KEYBOARD_QWERTY; 419 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 420 Configuration.HARDKEYBOARDHIDDEN_YES; 421 422 verifyInputViewStatusOnMainSync(() -> assertThat( 423 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), 424 false /* expected */, 425 false /* inputViewStarted */); 426 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 427 } 428 429 /** 430 * This checks that an explicit show request followed by connecting a hard keyboard 431 * and a configuration change, still results in the IME being shown. 432 */ 433 @Test testShowSoftInputExplicitly_thenConfigurationChanged()434 public void testShowSoftInputExplicitly_thenConfigurationChanged() throws Exception { 435 setShowImeWithHardKeyboard(false /* enabled */); 436 437 // Start with no hard keyboard. 438 mInputMethodService.getResources().getConfiguration().keyboard = 439 Configuration.KEYBOARD_NOKEYS; 440 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 441 Configuration.HARDKEYBOARDHIDDEN_YES; 442 443 verifyInputViewStatusOnMainSync( 444 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), 445 true /* expected */, 446 true /* inputViewStarted */); 447 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 448 449 // Simulate connecting a hard keyboard. 450 mInputMethodService.getResources().getConfiguration().keyboard = 451 Configuration.KEYBOARD_QWERTY; 452 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 453 Configuration.HARDKEYBOARDHIDDEN_YES; 454 455 // Simulate a fake configuration change to avoid triggering the recreation of TestActivity. 456 mInputMethodService.getResources().getConfiguration().orientation = 457 Configuration.ORIENTATION_LANDSCAPE; 458 459 verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged( 460 mInputMethodService.getResources().getConfiguration()), 461 true /* expected */, 462 true /* inputViewStarted */); 463 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 464 } 465 466 /** 467 * This checks that an implicit show request followed by connecting a hard keyboard 468 * and a configuration change, does not trigger IMS#onFinishInputView, 469 * but results in the IME being hidden. 470 */ 471 @Test testShowSoftInputImplicitly_thenConfigurationChanged()472 public void testShowSoftInputImplicitly_thenConfigurationChanged() throws Exception { 473 setShowImeWithHardKeyboard(false /* enabled */); 474 475 // Start with no hard keyboard. 476 mInputMethodService.getResources().getConfiguration().keyboard = 477 Configuration.KEYBOARD_NOKEYS; 478 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 479 Configuration.HARDKEYBOARDHIDDEN_YES; 480 481 verifyInputViewStatusOnMainSync(() -> assertThat( 482 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), 483 true /* expected */, 484 true /* inputViewStarted */); 485 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 486 487 // Simulate connecting a hard keyboard. 488 mInputMethodService.getResources().getConfiguration().keyboard = 489 Configuration.KEYBOARD_QWERTY; 490 mInputMethodService.getResources().getConfiguration().keyboard = 491 Configuration.HARDKEYBOARDHIDDEN_YES; 492 493 // Simulate a fake configuration change to avoid triggering the recreation of TestActivity. 494 mInputMethodService.getResources().getConfiguration().orientation = 495 Configuration.ORIENTATION_LANDSCAPE; 496 497 // Normally, IMS#onFinishInputView will be called when finishing the input view by the user. 498 // But if IMS#hideWindow is called when receiving a new configuration change, we don't 499 // expect that it's user-driven to finish the lifecycle of input view with 500 // IMS#onFinishInputView, because the input view will be re-initialized according to the 501 // last #mShowInputRequested state. So in this case we treat the input view as still alive. 502 verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged( 503 mInputMethodService.getResources().getConfiguration()), 504 true /* expected */, 505 true /* inputViewStarted */); 506 assertThat(mInputMethodService.isInputViewShown()).isFalse(); 507 } 508 509 /** 510 * This checks that an explicit show request directly followed by an implicit show request, 511 * while a hardware keyboard is connected, still results in the IME being shown 512 * (i.e. the implicit show request is treated as explicit). 513 */ 514 @Test testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard()515 public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard() 516 throws Exception { 517 setShowImeWithHardKeyboard(false /* enabled */); 518 519 // Simulate connecting a hard keyboard. 520 mInputMethodService.getResources().getConfiguration().keyboard = 521 Configuration.KEYBOARD_QWERTY; 522 mInputMethodService.getResources().getConfiguration().hardKeyboardHidden = 523 Configuration.HARDKEYBOARDHIDDEN_YES; 524 525 // Explicit show request. 526 verifyInputViewStatusOnMainSync(() -> assertThat( 527 mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), 528 true /* expected */, 529 true /* inputViewStarted */); 530 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 531 532 // Implicit show request. 533 verifyInputViewStatusOnMainSync(() -> assertThat( 534 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), 535 false /* expected */, 536 true /* inputViewStarted */); 537 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 538 539 // Simulate a fake configuration change to avoid triggering the recreation of TestActivity. 540 // This should now consider the implicit show request, but keep the state from the 541 // explicit show request, and thus not hide the keyboard. 542 verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged( 543 mInputMethodService.getResources().getConfiguration()), 544 true /* expected */, 545 true /* inputViewStarted */); 546 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 547 } 548 549 /** 550 * This checks that a forced show request directly followed by an explicit show request, 551 * and then a hide not always request, still results in the IME being shown 552 * (i.e. the explicit show request retains the forced state). 553 */ 554 @Test testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways()555 public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways() 556 throws Exception { 557 setShowImeWithHardKeyboard(true /* enabled */); 558 559 verifyInputViewStatusOnMainSync(() -> assertThat( 560 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(), 561 true /* expected */, 562 true /* inputViewStarted */); 563 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 564 565 verifyInputViewStatusOnMainSync(() -> assertThat( 566 mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), 567 false /* expected */, 568 true /* inputViewStarted */); 569 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 570 571 verifyInputViewStatusOnMainSync(() -> 572 mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS), 573 false /* expected */, 574 true /* inputViewStarted */); 575 assertThat(mInputMethodService.isInputViewShown()).isTrue(); 576 } 577 578 /** 579 * This checks that the IME fullscreen mode state is updated after changing orientation. 580 */ 581 @Test testFullScreenMode()582 public void testFullScreenMode() throws Exception { 583 setShowImeWithHardKeyboard(true /* enabled */); 584 585 Log.i(TAG, "Set orientation natural"); 586 verifyFullscreenMode(() -> setOrientation(0), 587 false /* expected */, 588 true /* orientationPortrait */); 589 590 Log.i(TAG, "Set orientation left"); 591 verifyFullscreenMode(() -> setOrientation(1), 592 true /* expected */, 593 false /* orientationPortrait */); 594 595 Log.i(TAG, "Set orientation right"); 596 verifyFullscreenMode(() -> setOrientation(2), 597 false /* expected */, 598 false /* orientationPortrait */); 599 } 600 601 /** 602 * This checks that when the system navigation bar is not created (e.g. emulator), 603 * then the IME caption bar is also not created. 604 */ 605 @Test testNoNavigationBar_thenImeNoCaptionBar()606 public void testNoNavigationBar_thenImeNoCaptionBar() throws Exception { 607 boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService() 608 .hasNavigationBar(mInputMethodService.getDisplayId()); 609 assumeFalse("Must not have a navigation bar", hasNavigationBar); 610 611 assertEquals(Insets.NONE, mInputMethodService.getWindow().getWindow().getDecorView() 612 .getRootWindowInsets().getInsetsIgnoringVisibility(captionBar())); 613 } 614 verifyInputViewStatus( Runnable runnable, boolean expected, boolean inputViewStarted)615 private void verifyInputViewStatus( 616 Runnable runnable, boolean expected, boolean inputViewStarted) 617 throws InterruptedException { 618 verifyInputViewStatusInternal(runnable, expected, inputViewStarted, 619 false /* runOnMainSync */); 620 } 621 verifyInputViewStatusOnMainSync( Runnable runnable, boolean expected, boolean inputViewStarted)622 private void verifyInputViewStatusOnMainSync( 623 Runnable runnable, boolean expected, boolean inputViewStarted) 624 throws InterruptedException { 625 verifyInputViewStatusInternal(runnable, expected, inputViewStarted, 626 true /* runOnMainSync */); 627 } 628 629 /** 630 * Verifies the status of the Input View after executing the given runnable. 631 * 632 * @param runnable the runnable to execute for showing or hiding the IME. 633 * @param expected whether the runnable is expected to trigger the signal. 634 * @param inputViewStarted the expected state of the Input View after executing the runnable. 635 * @param runOnMainSync whether to execute the runnable on the main thread. 636 */ verifyInputViewStatusInternal( Runnable runnable, boolean expected, boolean inputViewStarted, boolean runOnMainSync)637 private void verifyInputViewStatusInternal( 638 Runnable runnable, boolean expected, boolean inputViewStarted, boolean runOnMainSync) 639 throws InterruptedException { 640 CountDownLatch signal = new CountDownLatch(1); 641 mInputMethodService.setCountDownLatchForTesting(signal); 642 // Runnable to trigger onStartInputView() / onFinishInputView() / onConfigurationChanged() 643 if (runOnMainSync) { 644 mInstrumentation.runOnMainSync(runnable); 645 } else { 646 runnable.run(); 647 } 648 mInstrumentation.waitForIdleSync(); 649 boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); 650 if (expected && !completed) { 651 fail("Timed out waiting for" 652 + " onStartInputView() / onFinishInputView() / onConfigurationChanged()"); 653 } else if (!expected && completed) { 654 fail("Unexpected call" 655 + " onStartInputView() / onFinishInputView() / onConfigurationChanged()"); 656 } 657 // Input is not finished. 658 assertThat(mInputMethodService.getCurrentInputStarted()).isTrue(); 659 assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted); 660 } 661 setOrientation(int orientation)662 private void setOrientation(int orientation) { 663 // Simple wrapper for catching RemoteException. 664 try { 665 switch (orientation) { 666 case 1: 667 mUiDevice.setOrientationLeft(); 668 break; 669 case 2: 670 mUiDevice.setOrientationRight(); 671 break; 672 default: 673 mUiDevice.setOrientationNatural(); 674 } 675 } catch (RemoteException e) { 676 throw new RuntimeException(e); 677 } 678 } 679 680 /** 681 * Verifies the IME fullscreen mode state after executing the given runnable. 682 * 683 * @param runnable the runnable to execute for setting the orientation. 684 * @param expected whether the runnable is expected to trigger the signal. 685 * @param orientationPortrait whether the orientation is expected to be portrait. 686 */ verifyFullscreenMode( Runnable runnable, boolean expected, boolean orientationPortrait)687 private void verifyFullscreenMode( 688 Runnable runnable, boolean expected, boolean orientationPortrait) 689 throws InterruptedException { 690 CountDownLatch signal = new CountDownLatch(1); 691 mInputMethodService.setCountDownLatchForTesting(signal); 692 693 // Runnable to trigger onConfigurationChanged() 694 try { 695 runnable.run(); 696 } catch (Exception e) { 697 throw new RuntimeException(e); 698 } 699 // Waits for onConfigurationChanged() to finish. 700 mInstrumentation.waitForIdleSync(); 701 boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); 702 if (expected && !completed) { 703 fail("Timed out waiting for onConfigurationChanged()"); 704 } else if (!expected && completed) { 705 fail("Unexpected call onConfigurationChanged()"); 706 } 707 708 clickOnEditorText(); 709 eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue()); 710 711 assertThat(mInputMethodService.getResources().getConfiguration().orientation) 712 .isEqualTo( 713 orientationPortrait 714 ? Configuration.ORIENTATION_PORTRAIT 715 : Configuration.ORIENTATION_LANDSCAPE); 716 EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); 717 assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN).isEqualTo(0); 718 assertThat(editorInfo.internalImeOptions & EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT) 719 .isEqualTo( 720 orientationPortrait ? EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT : 0); 721 assertThat(mInputMethodService.onEvaluateFullscreenMode()).isEqualTo(!orientationPortrait); 722 assertThat(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait); 723 724 mUiDevice.pressBack(); 725 } 726 prepareIme()727 private void prepareIme() throws Exception { 728 executeShellCommand("ime enable " + mInputMethodId); 729 executeShellCommand("ime set " + mInputMethodId); 730 mInstrumentation.waitForIdleSync(); 731 Log.i(TAG, "Finish preparing IME"); 732 } 733 prepareEditor()734 private void prepareEditor() { 735 mActivity = TestActivity.start(mInstrumentation); 736 Log.i(TAG, "Finish preparing activity with editor."); 737 } 738 getInputMethodId()739 private String getInputMethodId() { 740 return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME; 741 } 742 743 /** 744 * Sets the value of show_ime_with_hard_keyboard, only if it is different to the default value. 745 * 746 * @param enabled the value to be set. 747 */ setShowImeWithHardKeyboard(boolean enabled)748 private void setShowImeWithHardKeyboard(boolean enabled) throws IOException { 749 if (mShowImeWithHardKeyboardEnabled != enabled) { 750 executeShellCommand(enabled 751 ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD 752 : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); 753 mInstrumentation.waitForIdleSync(); 754 } 755 } 756 executeShellCommand(String cmd)757 private String executeShellCommand(String cmd) throws IOException { 758 Log.i(TAG, "Run command: " + cmd); 759 return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 760 .executeShellCommand(cmd); 761 } 762 clickOnEditorText()763 private void clickOnEditorText() { 764 // Find the editText and click it. 765 UiObject2 editTextUiObject = 766 mUiDevice.wait( 767 Until.findObject(By.desc(EDIT_TEXT_DESC)), 768 TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS)); 769 assertThat(editTextUiObject).isNotNull(); 770 editTextUiObject.click(); 771 mInstrumentation.waitForIdleSync(); 772 } 773 } 774