1 // Copyright 2017 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.content.browser.input; 6 7 import android.annotation.TargetApi; 8 import android.app.Activity; 9 import android.content.ClipData; 10 import android.content.ClipboardManager; 11 import android.content.Context; 12 import android.content.res.Configuration; 13 import android.os.Handler; 14 import android.util.Pair; 15 import android.view.KeyEvent; 16 import android.view.View; 17 import android.view.inputmethod.EditorInfo; 18 import android.view.inputmethod.InputConnection; 19 20 import org.hamcrest.Matchers; 21 import org.junit.Assert; 22 23 import org.chromium.base.test.util.CallbackHelper; 24 import org.chromium.base.test.util.Criteria; 25 import org.chromium.base.test.util.CriteriaHelper; 26 import org.chromium.base.test.util.CriteriaNotSatisfiedException; 27 import org.chromium.content.browser.ViewEventSinkImpl; 28 import org.chromium.content.browser.selection.SelectionPopupControllerImpl; 29 import org.chromium.content.browser.webcontents.WebContentsImpl; 30 import org.chromium.content_public.browser.ImeAdapter; 31 import org.chromium.content_public.browser.test.RenderFrameHostTestExt; 32 import org.chromium.content_public.browser.test.util.DOMUtils; 33 import org.chromium.content_public.browser.test.util.JavaScriptUtils; 34 import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer; 35 import org.chromium.content_public.browser.test.util.TestInputMethodManagerWrapper; 36 import org.chromium.content_public.browser.test.util.TestInputMethodManagerWrapper.InputConnectionProvider; 37 import org.chromium.content_public.browser.test.util.TestThreadUtils; 38 import org.chromium.content_shell_apk.ContentShellActivityTestRule; 39 import org.chromium.ui.base.ime.TextInputType; 40 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.List; 44 import java.util.concurrent.Callable; 45 import java.util.concurrent.ExecutionException; 46 import java.util.concurrent.TimeoutException; 47 48 /** 49 * Integration tests for text input for Android L (or above) features. 50 */ 51 class ImeActivityTestRule extends ContentShellActivityTestRule { 52 private ChromiumBaseInputConnection mConnection; 53 private TestInputConnectionFactory mConnectionFactory; 54 private ImeAdapterImpl mImeAdapter; 55 56 static final String INPUT_FORM_HTML = "content/test/data/android/input/input_forms.html"; 57 static final String PASSWORD_FORM_HTML = "content/test/data/android/input/password_form.html"; 58 static final String INPUT_MODE_HTML = "content/test/data/android/input/input_mode.html"; 59 static final String INPUT_ACTION_HTML = "content/test/data/android/input/input_action.html"; 60 static final String INPUT_VK_API_HTML = 61 "content/test/data/android/input/virtual_keyboard_api.html"; 62 63 private SelectionPopupControllerImpl mSelectionPopupController; 64 private TestCallbackHelperContainer mCallbackContainer; 65 private TestInputMethodManagerWrapper mInputMethodManagerWrapper; 66 setUpForUrl(String url)67 public void setUpForUrl(String url) throws Exception { 68 launchContentShellWithUrlSync(url); 69 mSelectionPopupController = getSelectionPopupController(); 70 71 final ImeAdapter imeAdapter = getImeAdapter(); 72 InputConnectionProvider provider = 73 TestInputMethodManagerWrapper.defaultInputConnectionProvider(imeAdapter); 74 mInputMethodManagerWrapper = new TestInputMethodManagerWrapper(provider) { 75 private boolean mExpectsSelectionOutsideComposition; 76 77 @Override 78 public void expectsSelectionOutsideComposition() { 79 mExpectsSelectionOutsideComposition = true; 80 } 81 82 @Override 83 public void onUpdateSelection( 84 Range oldSel, Range oldComp, Range newSel, Range newComp) { 85 // We expect that selection will be outside composition in some cases. Keyboard 86 // app will not finish composition in this case. 87 if (mExpectsSelectionOutsideComposition) { 88 mExpectsSelectionOutsideComposition = false; 89 return; 90 } 91 if (oldComp == null || oldComp.start() == oldComp.end() 92 || newComp.start() == newComp.end()) { 93 return; 94 } 95 // This emulates keyboard app's behavior that finishes composition when 96 // selection is outside composition. 97 if (!newSel.intersects(newComp)) { 98 try { 99 finishComposingText(); 100 } catch (Exception e) { 101 e.printStackTrace(); 102 Assert.fail(); 103 } 104 } 105 } 106 }; 107 getImeAdapter().setInputMethodManagerWrapper(mInputMethodManagerWrapper); 108 Assert.assertEquals(0, mInputMethodManagerWrapper.getShowSoftInputCounter()); 109 mConnectionFactory = 110 new TestInputConnectionFactory(getImeAdapter().getInputConnectionFactoryForTest()); 111 getImeAdapter().setInputConnectionFactory(mConnectionFactory); 112 113 WebContentsImpl webContents = (WebContentsImpl) getWebContents(); 114 mCallbackContainer = new TestCallbackHelperContainer(webContents); 115 DOMUtils.waitForNonZeroNodeBounds(webContents, "input_text"); 116 boolean result = DOMUtils.clickNode(webContents, "input_text"); 117 118 Assert.assertEquals("Failed to dispatch touch event.", true, result); 119 assertWaitForKeyboardStatus(true); 120 121 mConnection = getInputConnection(); 122 mImeAdapter = getImeAdapter(); 123 124 waitForKeyboardStates(1, 0, 1, new Integer[] {TextInputType.TEXT}); 125 Assert.assertEquals(0, mConnectionFactory.getOutAttrs().initialSelStart); 126 Assert.assertEquals(0, mConnectionFactory.getOutAttrs().initialSelEnd); 127 128 waitForEventLogs("selectionchange"); 129 clearEventLogs(); 130 131 waitAndVerifyUpdateSelection(0, 0, 0, -1, -1); 132 resetAllStates(); 133 } 134 getTestCallBackHelperContainer()135 TestCallbackHelperContainer getTestCallBackHelperContainer() { 136 return mCallbackContainer; 137 } 138 getConnection()139 ChromiumBaseInputConnection getConnection() { 140 return mConnection; 141 } 142 getInputMethodManagerWrapper()143 TestInputMethodManagerWrapper getInputMethodManagerWrapper() { 144 return mInputMethodManagerWrapper; 145 } 146 getConnectionFactory()147 TestInputConnectionFactory getConnectionFactory() { 148 return mConnectionFactory; 149 } 150 fullyLoadUrl(final String url)151 void fullyLoadUrl(final String url) throws Exception { 152 CallbackHelper done = mCallbackContainer.getOnFirstVisuallyNonEmptyPaintHelper(); 153 int currentCallCount = done.getCallCount(); 154 TestThreadUtils.runOnUiThreadBlocking( 155 () -> { getActivity().getActiveShell().loadUrl(url); }); 156 waitForActiveShellToBeDoneLoading(); 157 done.waitForCallback(currentCallCount); 158 } 159 clearEventLogs()160 void clearEventLogs() throws Exception { 161 final String code = "clearEventLogs()"; 162 JavaScriptUtils.executeJavaScriptAndWaitForResult(getWebContents(), code); 163 } 164 waitForEventLogs(String expectedLogs)165 void waitForEventLogs(String expectedLogs) throws Exception { 166 final String code = "getEventLogs()"; 167 final String sanitizedExpectedLogs = "\"" + expectedLogs + "\""; 168 Assert.assertEquals(sanitizedExpectedLogs, 169 JavaScriptUtils.executeJavaScriptAndWaitForResult(getWebContents(), code)); 170 } 171 waitForEventLogState(String expectedLogs)172 void waitForEventLogState(String expectedLogs) { 173 final String code = "getEventLogs()"; 174 final String sanitizedExpectedLogs = "\"" + expectedLogs + "\""; 175 CriteriaHelper.pollInstrumentationThread(() -> { 176 try { 177 Criteria.checkThat( 178 JavaScriptUtils.executeJavaScriptAndWaitForResult(getWebContents(), code), 179 Matchers.is(sanitizedExpectedLogs)); 180 } catch (TimeoutException ex) { 181 throw new CriteriaNotSatisfiedException(ex); 182 } 183 }); 184 } 185 waitForFocusedElement(String id)186 void waitForFocusedElement(String id) { 187 CriteriaHelper.pollInstrumentationThread(() -> { 188 try { 189 Criteria.checkThat(DOMUtils.getFocusedNode(getWebContents()), Matchers.is(id)); 190 } catch (TimeoutException ex) { 191 throw new CriteriaNotSatisfiedException(ex); 192 } 193 }); 194 } 195 assertTextsAroundCursor(CharSequence before, CharSequence selected, CharSequence after)196 void assertTextsAroundCursor(CharSequence before, CharSequence selected, CharSequence after) 197 throws Exception { 198 Assert.assertEquals(before, getTextBeforeCursor(100, 0)); 199 Assert.assertEquals(selected, getSelectedText(0)); 200 Assert.assertEquals(after, getTextAfterCursor(100, 0)); 201 } 202 waitForKeyboardStates(int show, int hide, int restart, Integer[] textInputTypeHistory)203 void waitForKeyboardStates(int show, int hide, int restart, Integer[] textInputTypeHistory) { 204 final String expected = 205 stringifyKeyboardStates(show, hide, restart, textInputTypeHistory, null, null); 206 CriteriaHelper.pollUiThread(() -> { 207 Criteria.checkThat(getKeyboardStates(false, false), Matchers.is(expected)); 208 }); 209 } 210 waitForKeyboardStates(int show, int hide, int restart, Integer[] textInputTypeHistory, Integer[] textInputModeHistory)211 void waitForKeyboardStates(int show, int hide, int restart, Integer[] textInputTypeHistory, 212 Integer[] textInputModeHistory) { 213 final String expected = stringifyKeyboardStates( 214 show, hide, restart, textInputTypeHistory, textInputModeHistory, null); 215 CriteriaHelper.pollUiThread(() -> { 216 Criteria.checkThat(getKeyboardStates(true, false), Matchers.is(expected)); 217 }); 218 } 219 waitForKeyboardInputActionStates(int show, int hide, int restart, Integer[] textInputTypeHistory, Integer[] textInputActionHistory)220 void waitForKeyboardInputActionStates(int show, int hide, int restart, 221 Integer[] textInputTypeHistory, Integer[] textInputActionHistory) { 222 final String expected = stringifyKeyboardStates( 223 show, hide, restart, textInputTypeHistory, null, textInputActionHistory); 224 CriteriaHelper.pollUiThread(() -> { 225 Criteria.checkThat(getKeyboardStates(false, true), Matchers.is(expected)); 226 }); 227 } 228 resetAllStates()229 void resetAllStates() { 230 mInputMethodManagerWrapper.reset(); 231 mConnectionFactory.resetAllStates(); 232 } 233 getKeyboardStates(boolean includeInputMode, boolean includeInputAction)234 String getKeyboardStates(boolean includeInputMode, boolean includeInputAction) { 235 int showCount = mInputMethodManagerWrapper.getShowSoftInputCounter(); 236 int hideCount = mInputMethodManagerWrapper.getHideSoftInputCounter(); 237 int restartCount = mInputMethodManagerWrapper.getRestartInputCounter(); 238 Integer[] textInputTypeHistory = mConnectionFactory.getTextInputTypeHistory(); 239 Integer[] textInputModeHistory = null; 240 Integer[] textInputActionHistory = null; 241 if (includeInputMode) textInputModeHistory = mConnectionFactory.getTextInputModeHistory(); 242 if (includeInputAction) { 243 textInputActionHistory = mConnectionFactory.getTextInputActionHistory(); 244 } 245 return stringifyKeyboardStates(showCount, hideCount, restartCount, textInputTypeHistory, 246 textInputModeHistory, textInputActionHistory); 247 } 248 stringifyKeyboardStates(int show, int hide, int restart, Integer[] inputTypeHistory, Integer[] inputModeHistory, Integer[] inputActionHistory)249 String stringifyKeyboardStates(int show, int hide, int restart, Integer[] inputTypeHistory, 250 Integer[] inputModeHistory, Integer[] inputActionHistory) { 251 return "show count: " + show + ", hide count: " + hide + ", restart count: " + restart 252 + ", input type history: " + Arrays.deepToString(inputTypeHistory) 253 + ", input mode history: " + Arrays.deepToString(inputModeHistory) 254 + ", input action history: " + Arrays.deepToString(inputActionHistory); 255 } 256 getLastTextHistory()257 String[] getLastTextHistory() { 258 return mConnectionFactory.getTextInputLastTextHistory(); 259 } 260 waitForEditorAction(final int expectedAction)261 void waitForEditorAction(final int expectedAction) { 262 CriteriaHelper.pollUiThread(() -> { 263 EditorInfo editorInfo = mConnectionFactory.getOutAttrs(); 264 int actualAction = editorInfo.actionId != 0 265 ? editorInfo.actionId 266 : editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION; 267 Criteria.checkThat(actualAction, Matchers.is(expectedAction)); 268 }); 269 } 270 performEditorAction(final int action)271 void performEditorAction(final int action) { 272 mConnection.performEditorAction(action); 273 } 274 performGo(TestCallbackHelperContainer testCallbackHelperContainer)275 void performGo(TestCallbackHelperContainer testCallbackHelperContainer) throws Throwable { 276 final InputConnection inputConnection = mConnection; 277 final Callable<Void> callable = new Callable<Void>() { 278 @Override 279 public Void call() { 280 inputConnection.performEditorAction(EditorInfo.IME_ACTION_GO); 281 return null; 282 } 283 }; 284 285 handleBlockingCallbackAction( 286 testCallbackHelperContainer.getOnPageFinishedHelper(), new Runnable() { 287 @Override 288 public void run() { 289 try { 290 runBlockingOnImeThread(callable); 291 } catch (Exception e) { 292 e.printStackTrace(); 293 Assert.fail(); 294 } 295 } 296 }); 297 } 298 assertWaitForKeyboardStatus(final boolean show)299 void assertWaitForKeyboardStatus(final boolean show) { 300 CriteriaHelper.pollUiThread(() -> { 301 if (show) { 302 Criteria.checkThat(getInputConnection(), Matchers.notNullValue()); 303 } 304 Criteria.checkThat( 305 mInputMethodManagerWrapper.isShowWithoutHideOutstanding(), Matchers.is(show)); 306 }); 307 } 308 assertWaitForSelectActionBarStatus(final boolean show)309 void assertWaitForSelectActionBarStatus(final boolean show) { 310 CriteriaHelper.pollUiThread(() -> { 311 Criteria.checkThat( 312 mSelectionPopupController.isSelectActionBarShowing(), Matchers.is(show)); 313 }); 314 } 315 verifyNoUpdateSelection()316 void verifyNoUpdateSelection() { 317 final List<Pair<Range, Range>> states = mInputMethodManagerWrapper.getUpdateSelectionList(); 318 Assert.assertEquals(0, states.size()); 319 } 320 waitAndVerifyUpdateSelection(final int index, final int selectionStart, final int selectionEnd, final int compositionStart, final int compositionEnd)321 void waitAndVerifyUpdateSelection(final int index, final int selectionStart, 322 final int selectionEnd, final int compositionStart, final int compositionEnd) { 323 final List<Pair<Range, Range>> states = mInputMethodManagerWrapper.getUpdateSelectionList(); 324 CriteriaHelper.pollUiThread( 325 () -> Criteria.checkThat(states.size(), Matchers.greaterThan(index))); 326 Pair<Range, Range> selection = states.get(index); 327 Assert.assertEquals("Mismatched selection start", selectionStart, selection.first.start()); 328 Assert.assertEquals("Mismatched selection end", selectionEnd, selection.first.end()); 329 Assert.assertEquals( 330 "Mismatched composition start", compositionStart, selection.second.start()); 331 Assert.assertEquals("Mismatched composition end", compositionEnd, selection.second.end()); 332 } 333 resetUpdateSelectionList()334 void resetUpdateSelectionList() { 335 mInputMethodManagerWrapper.getUpdateSelectionList().clear(); 336 } 337 assertClipboardContents(final Activity activity, final String expectedContents)338 void assertClipboardContents(final Activity activity, final String expectedContents) { 339 CriteriaHelper.pollUiThread(() -> { 340 ClipboardManager clipboardManager = 341 (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); 342 ClipData clip = clipboardManager.getPrimaryClip(); 343 Criteria.checkThat(clip, Matchers.notNullValue()); 344 Criteria.checkThat(clip.getItemCount(), Matchers.is(1)); 345 Criteria.checkThat(clip.getItemAt(0).getText(), Matchers.is(expectedContents)); 346 }); 347 } 348 getInputConnection()349 ChromiumBaseInputConnection getInputConnection() { 350 try { 351 return TestThreadUtils.runOnUiThreadBlocking( 352 new Callable<ChromiumBaseInputConnection>() { 353 @Override 354 public ChromiumBaseInputConnection call() { 355 return (ChromiumBaseInputConnection) getImeAdapter() 356 .getInputConnectionForTest(); 357 } 358 }); 359 } catch (ExecutionException e) { 360 e.printStackTrace(); 361 Assert.fail(); 362 return null; 363 } 364 } 365 366 void restartInput() { 367 TestThreadUtils.runOnUiThreadBlocking(() -> { mImeAdapter.restartInput(); }); 368 } 369 370 // After calling this method, we should call assertClipboardContents() to wait for the clipboard 371 // to get updated. See cubug.com/621046 372 void copy() { 373 final WebContentsImpl webContents = (WebContentsImpl) getWebContents(); 374 TestThreadUtils.runOnUiThreadBlocking(() -> { webContents.copy(); }); 375 } 376 377 void cut() { 378 final WebContentsImpl webContents = (WebContentsImpl) getWebContents(); 379 TestThreadUtils.runOnUiThreadBlocking(() -> { webContents.cut(); }); 380 } 381 382 void notifyVirtualKeyboardOverlayRect(int x, int y, int width, int height) { 383 final WebContentsImpl webContents = (WebContentsImpl) getWebContents(); 384 RenderFrameHostTestExt rfh = TestThreadUtils.runOnUiThreadBlockingNoException( 385 () -> new RenderFrameHostTestExt(webContents.getMainFrame())); 386 Assert.assertTrue("Did not get a focused frame", rfh != null); 387 TestThreadUtils.runOnUiThreadBlocking( 388 () -> { rfh.notifyVirtualKeyboardOverlayRect(x, y, width, height); }); 389 } 390 391 void setClip(final CharSequence text) { 392 TestThreadUtils.runOnUiThreadBlocking(() -> { 393 final ClipboardManager clipboardManager = 394 (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); 395 clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); 396 }); 397 } 398 399 void paste() { 400 final WebContentsImpl webContents = (WebContentsImpl) getWebContents(); 401 TestThreadUtils.runOnUiThreadBlocking(() -> { webContents.paste(); }); 402 } 403 404 void selectAll() { 405 final WebContentsImpl webContents = (WebContentsImpl) getWebContents(); 406 TestThreadUtils.runOnUiThreadBlocking(() -> { webContents.selectAll(); }); 407 } 408 409 void collapseSelection() { 410 final WebContentsImpl webContents = (WebContentsImpl) getWebContents(); 411 TestThreadUtils.runOnUiThreadBlocking(() -> { webContents.collapseSelection(); }); 412 } 413 414 /** 415 * Run the {@Callable} on IME thread (or UI thread if not applicable). 416 * @param c The callable 417 * @return The result from running the callable. 418 */ 419 <T> T runBlockingOnImeThread(Callable<T> c) throws Exception { 420 return ImeTestUtils.runBlockingOnHandler(mConnectionFactory.getHandler(), c); 421 } 422 423 boolean beginBatchEdit() throws Exception { 424 final ChromiumBaseInputConnection connection = mConnection; 425 return runBlockingOnImeThread(new Callable<Boolean>() { 426 @Override 427 public Boolean call() { 428 return connection.beginBatchEdit(); 429 } 430 }); 431 } 432 433 boolean endBatchEdit() throws Exception { 434 final ChromiumBaseInputConnection connection = mConnection; 435 return runBlockingOnImeThread(new Callable<Boolean>() { 436 @Override 437 public Boolean call() { 438 return connection.endBatchEdit(); 439 } 440 }); 441 } 442 443 boolean commitText(final CharSequence text, final int newCursorPosition) throws Exception { 444 final ChromiumBaseInputConnection connection = mConnection; 445 return runBlockingOnImeThread(new Callable<Boolean>() { 446 @Override 447 public Boolean call() { 448 return connection.commitText(text, newCursorPosition); 449 } 450 }); 451 } 452 453 boolean setSelection(final int start, final int end) throws Exception { 454 final ChromiumBaseInputConnection connection = mConnection; 455 return runBlockingOnImeThread(new Callable<Boolean>() { 456 @Override 457 public Boolean call() { 458 return connection.setSelection(start, end); 459 } 460 }); 461 } 462 463 boolean setComposingRegion(final int start, final int end) throws Exception { 464 final ChromiumBaseInputConnection connection = mConnection; 465 return runBlockingOnImeThread(new Callable<Boolean>() { 466 @Override 467 public Boolean call() { 468 return connection.setComposingRegion(start, end); 469 } 470 }); 471 } 472 473 protected boolean setComposingText(final CharSequence text, final int newCursorPosition) 474 throws Exception { 475 final ChromiumBaseInputConnection connection = mConnection; 476 return runBlockingOnImeThread(new Callable<Boolean>() { 477 @Override 478 public Boolean call() { 479 return connection.setComposingText(text, newCursorPosition); 480 } 481 }); 482 } 483 484 boolean finishComposingText() throws Exception { 485 final ChromiumBaseInputConnection connection = mConnection; 486 return runBlockingOnImeThread(new Callable<Boolean>() { 487 @Override 488 public Boolean call() { 489 return connection.finishComposingText(); 490 } 491 }); 492 } 493 494 boolean deleteSurroundingText(final int before, final int after) throws Exception { 495 final ChromiumBaseInputConnection connection = mConnection; 496 return runBlockingOnImeThread(new Callable<Boolean>() { 497 @Override 498 public Boolean call() { 499 return connection.deleteSurroundingText(before, after); 500 } 501 }); 502 } 503 504 // Note that deleteSurroundingTextInCodePoints() was introduced in Android N (Api level 24), but 505 // the Android repository used in Chrome is behind that (level 23). So this function can't be 506 // called by keyboard apps currently. 507 @TargetApi(24) 508 boolean deleteSurroundingTextInCodePoints(final int before, final int after) throws Exception { 509 final ThreadedInputConnection connection = (ThreadedInputConnection) mConnection; 510 return runBlockingOnImeThread(new Callable<Boolean>() { 511 @Override 512 public Boolean call() { 513 return connection.deleteSurroundingTextInCodePoints(before, after); 514 } 515 }); 516 } 517 518 CharSequence getTextBeforeCursor(final int length, final int flags) throws Exception { 519 final ChromiumBaseInputConnection connection = mConnection; 520 return runBlockingOnImeThread(new Callable<CharSequence>() { 521 @Override 522 public CharSequence call() { 523 return connection.getTextBeforeCursor(length, flags); 524 } 525 }); 526 } 527 528 CharSequence getSelectedText(final int flags) throws Exception { 529 final ChromiumBaseInputConnection connection = mConnection; 530 return runBlockingOnImeThread(new Callable<CharSequence>() { 531 @Override 532 public CharSequence call() { 533 return connection.getSelectedText(flags); 534 } 535 }); 536 } 537 538 CharSequence getTextAfterCursor(final int length, final int flags) throws Exception { 539 final ChromiumBaseInputConnection connection = mConnection; 540 return runBlockingOnImeThread(new Callable<CharSequence>() { 541 @Override 542 public CharSequence call() { 543 return connection.getTextAfterCursor(length, flags); 544 } 545 }); 546 } 547 548 int getCursorCapsMode(final int reqModes) throws Throwable { 549 final ChromiumBaseInputConnection connection = mConnection; 550 return runBlockingOnImeThread(new Callable<Integer>() { 551 @Override 552 public Integer call() { 553 return connection.getCursorCapsMode(reqModes); 554 } 555 }); 556 } 557 558 void dispatchKeyEvent(final KeyEvent event) { 559 TestThreadUtils.runOnUiThreadBlocking(() -> { mImeAdapter.dispatchKeyEvent(event); }); 560 } 561 562 void attachPhysicalKeyboard() { 563 Configuration hardKeyboardConfig = 564 new Configuration(getActivity().getResources().getConfiguration()); 565 hardKeyboardConfig.keyboard = Configuration.KEYBOARD_QWERTY; 566 hardKeyboardConfig.keyboardHidden = Configuration.KEYBOARDHIDDEN_YES; 567 hardKeyboardConfig.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; 568 onConfigurationChanged(hardKeyboardConfig); 569 } 570 571 void detachPhysicalKeyboard() { 572 Configuration softKeyboardConfig = 573 new Configuration(getActivity().getResources().getConfiguration()); 574 softKeyboardConfig.keyboard = Configuration.KEYBOARD_NOKEYS; 575 softKeyboardConfig.keyboardHidden = Configuration.KEYBOARDHIDDEN_NO; 576 softKeyboardConfig.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; 577 onConfigurationChanged(softKeyboardConfig); 578 } 579 580 private void onConfigurationChanged(final Configuration config) { 581 TestThreadUtils.runOnUiThreadBlocking( 582 () -> { ViewEventSinkImpl.from(getWebContents()).onConfigurationChanged(config); }); 583 } 584 585 /** 586 * Focus element, wait for a single state update, reset state update list. 587 * @param id ID of the element to focus. 588 */ 589 void focusElementAndWaitForStateUpdate(String id) throws TimeoutException { 590 resetAllStates(); 591 focusElement(id); 592 waitAndVerifyUpdateSelection(0, 0, 0, -1, -1); 593 resetAllStates(); 594 } 595 596 void focusElement(final String id) throws TimeoutException { 597 focusElement(id, true); 598 } 599 600 void focusElement(final String id, boolean shouldShowKeyboard) throws TimeoutException { 601 DOMUtils.focusNode(getWebContents(), id); 602 assertWaitForKeyboardStatus(shouldShowKeyboard); 603 waitForFocusedElement(id); 604 // When we focus another element, the connection may be recreated. 605 mConnection = getInputConnection(); 606 } 607 608 static class TestInputConnectionFactory implements ChromiumBaseInputConnection.Factory { 609 private final ChromiumBaseInputConnection.Factory mFactory; 610 611 private final List<Integer> mTextInputTypeList = new ArrayList<>(); 612 private final List<Integer> mTextInputModeList = new ArrayList<>(); 613 private final List<Integer> mTextInputActionList = new ArrayList<>(); 614 private final List<String> mTextInputLastTextList = new ArrayList<>(); 615 private EditorInfo mOutAttrs; 616 617 public TestInputConnectionFactory(ChromiumBaseInputConnection.Factory factory) { 618 mFactory = factory; 619 } 620 621 @Override 622 public ChromiumBaseInputConnection initializeAndGet(View view, ImeAdapterImpl imeAdapter, 623 int inputType, int inputFlags, int inputMode, int inputAction, int selectionStart, 624 int selectionEnd, String lastText, EditorInfo outAttrs) { 625 mTextInputTypeList.add(inputType); 626 mTextInputModeList.add(inputMode); 627 mTextInputActionList.add(inputAction); 628 mTextInputLastTextList.add(lastText); 629 mOutAttrs = outAttrs; 630 return mFactory.initializeAndGet(view, imeAdapter, inputType, inputFlags, inputMode, 631 inputAction, selectionStart, selectionEnd, lastText, outAttrs); 632 } 633 634 @Override 635 public Handler getHandler() { 636 return mFactory.getHandler(); 637 } 638 639 public Integer[] getTextInputTypeHistory() { 640 Integer[] result = new Integer[mTextInputTypeList.size()]; 641 mTextInputTypeList.toArray(result); 642 return result; 643 } 644 645 public void resetAllStates() { 646 mTextInputTypeList.clear(); 647 mTextInputModeList.clear(); 648 mTextInputActionList.clear(); 649 mTextInputLastTextList.clear(); 650 } 651 652 public Integer[] getTextInputModeHistory() { 653 Integer[] result = new Integer[mTextInputModeList.size()]; 654 mTextInputModeList.toArray(result); 655 return result; 656 } 657 658 public Integer[] getTextInputActionHistory() { 659 Integer[] result = new Integer[mTextInputActionList.size()]; 660 mTextInputActionList.toArray(result); 661 return result; 662 } 663 664 public String[] getTextInputLastTextHistory() { 665 String[] result = new String[mTextInputLastTextList.size()]; 666 mTextInputLastTextList.toArray(result); 667 return result; 668 } 669 670 public EditorInfo getOutAttrs() { 671 return mOutAttrs; 672 } 673 674 @Override 675 public void onWindowFocusChanged(boolean gainFocus) { 676 mFactory.onWindowFocusChanged(gainFocus); 677 } 678 679 @Override 680 public void onViewFocusChanged(boolean gainFocus) { 681 mFactory.onViewFocusChanged(gainFocus); 682 } 683 684 @Override 685 public void onViewAttachedToWindow() { 686 mFactory.onViewAttachedToWindow(); 687 } 688 689 @Override 690 public void onViewDetachedFromWindow() { 691 mFactory.onViewDetachedFromWindow(); 692 } 693 694 @Override 695 public void setTriggerDelayedOnCreateInputConnection(boolean trigger) { 696 mFactory.setTriggerDelayedOnCreateInputConnection(trigger); 697 } 698 } 699 } 700