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