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