// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser.input;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Color;
import android.os.SystemClock;
import android.text.InputType;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.UnderlineSpan;
import android.view.KeyEvent;
import android.view.ViewConfiguration;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;

import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.ContentJUnit4ClassRunner;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.JavaScriptUtils;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.base.ime.TextInputType;

import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;

/**
 * IME (input method editor) and text input tests.
 */
@RunWith(ContentJUnit4ClassRunner.class)
@CommandLineFlags.Add({"expose-internals-for-testing"})
@Batch(ImeTest.IME_BATCH)
public class ImeTest {
    /* package */ static final String IME_BATCH = "ImeTestBatch";

    // TODO(https://crbug.com/989569): Find a way to re-use the content shell
    // across tests?
    @Rule
    public ImeActivityTestRule mRule = new ImeActivityTestRule();
    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Before
    public void setUp() throws Exception {
        mRule.setUpForUrl(ImeActivityTestRule.INPUT_FORM_HTML);
    }

    @After
    public void tearDown() throws Exception {
        mRule.getActivity().finish();
    }

    @Test
    @MediumTest
    @Feature({"TextInput", "Main"})
    public void testKeyboardDismissedWhenNavigating() throws Throwable {
        mRule.assertWaitForKeyboardStatus(true);

        // Hide keyboard when loading a new Url.
        mRule.fullyLoadUrl(UrlUtils.getIsolatedTestFileUrl(ImeActivityTestRule.INPUT_FORM_HTML));
        mRule.assertWaitForKeyboardStatus(false);

        DOMUtils.clickNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);

        // Hide keyboard when navigating.
        final String code = "document.getElementById(\"link\").click()";
        JavaScriptUtils.executeJavaScriptAndWaitForResult(mRule.getWebContents(), code);
        mRule.assertWaitForKeyboardStatus(false);
    }

    @Test
    @MediumTest
    @Feature({"TextInput", "Main"})
    public void testKeyboardDismissedAfterClickingGo() throws Throwable {
        mRule.setComposingText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, 0, 5);

        mRule.performGo(mRule.getTestCallBackHelperContainer());

        mRule.assertWaitForKeyboardStatus(false);
    }

    @Test
    @MediumTest
    @Feature({"TextInput", "Main"})
    public void testDoesNotHang_getTextAfterKeyboardHides() throws Throwable {
        mRule.setComposingText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, 0, 5);

        mRule.performGo(mRule.getTestCallBackHelperContainer());

        // This may time out if we do not get the information on time.
        // TODO(changwan): find a way to remove the loop.
        for (int i = 0; i < 100; ++i) {
            mRule.getTextBeforeCursor(10, 0);
        }

        mRule.assertWaitForKeyboardStatus(false);
    }

    // crbug.com/643519
    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testCompositionWithNullTextNotCrash() throws Throwable {
        mRule.commitText(null, 1);
        mRule.assertTextsAroundCursor("", null, "");

        mRule.setComposingText(null, 1);
        mRule.assertTextsAroundCursor("", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testDeleteSurroundingTextWithRangeSelection() throws Throwable {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        mRule.setSelection(1, 4);
        mRule.waitAndVerifyUpdateSelection(1, 1, 4, -1, -1);

        mRule.deleteSurroundingText(0, 0);
        mRule.assertTextsAroundCursor("h", "ell", "o");

        mRule.deleteSurroundingText(1, 1);
        mRule.assertTextsAroundCursor("", "ell", "");

        mRule.deleteSurroundingText(1, 0);
        mRule.assertTextsAroundCursor("", "ell", "");

        mRule.deleteSurroundingText(0, 1);
        mRule.assertTextsAroundCursor("", "ell", "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testDeleteSurroundingTextWithCursorSelection() throws Throwable {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        mRule.setSelection(2, 2);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);

        mRule.deleteSurroundingText(0, 0);
        mRule.assertTextsAroundCursor("he", null, "llo");

        mRule.deleteSurroundingText(1, 1);
        mRule.assertTextsAroundCursor("h", null, "lo");

        mRule.deleteSurroundingText(1, 0);
        mRule.assertTextsAroundCursor("", null, "lo");

        mRule.deleteSurroundingText(0, 10);
        mRule.assertTextsAroundCursor("", null, "");

        mRule.deleteSurroundingText(10, 10);
        mRule.assertTextsAroundCursor("", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testKeyboardAppFinishesCompositionOnUnexpectedSelectionChange() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea2");
        mRule.commitText("12345", 1);
        mRule.setSelection(3, 3);
        mRule.setComposingRegion(2, 3);

        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.waitAndVerifyUpdateSelection(1, 3, 3, -1, -1);
        mRule.waitAndVerifyUpdateSelection(2, 3, 3, 2, 3);

        // Unexpected selection change occurs, e.g., the user clicks on an area.
        // There was already one click during test setup; we have to wait out the double-tap
        // timeout or the test will be flaky.
        Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
        DOMUtils.clickNode(mRule.getWebContents(), "textarea2");
        mRule.waitAndVerifyUpdateSelection(3, 5, 5, 2, 3);
        // Keyboard app finishes composition. We emulate this in TestInputMethodManagerWrapper.
        mRule.waitAndVerifyUpdateSelection(4, 5, 5, -1, -1);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    @DisabledTest(message = "https://crbug.com/1223357")
    public void testDeleteSurroundingTextInCodePointsWithRangeSelection() throws Throwable {
        final String trophy = "\uD83C\uDFC6";
        mRule.commitText("ab" + trophy + "cdef" + trophy + "gh", 1);
        mRule.waitAndVerifyUpdateSelection(0, 12, 12, -1, -1);

        mRule.setSelection(6, 8);
        mRule.waitAndVerifyUpdateSelection(1, 6, 8, -1, -1);
        mRule.assertTextsAroundCursor("ab" + trophy + "cd", "ef", trophy + "gh");

        mRule.deleteSurroundingTextInCodePoints(2, 2);
        mRule.waitAndVerifyUpdateSelection(2, 4, 6, -1, -1);
        mRule.assertTextsAroundCursor("ab" + trophy, "ef", "h");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    @DisabledTest(message = "https://crbug.com/1222977")
    public void testDeleteSurroundingTextInCodePointsWithCursorSelection() throws Throwable {
        final String trophy = "\uD83C\uDFC6";
        mRule.commitText("ab" + trophy + "cd" + trophy, 1);
        mRule.waitAndVerifyUpdateSelection(0, 8, 8, -1, -1);

        mRule.setSelection(4, 4);
        mRule.waitAndVerifyUpdateSelection(1, 4, 4, -1, -1);
        mRule.assertTextsAroundCursor("ab" + trophy, null, "cd" + trophy);

        mRule.deleteSurroundingTextInCodePoints(2, 2);
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, -1, -1);
        mRule.assertTextsAroundCursor("a", null, trophy);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testSetComposingTextForNewCursorPositions() throws Throwable {
        // Cursor is on the right of composing text when newCursorPosition > 0.
        mRule.setComposingText("ab", 1);
        mRule.waitAndVerifyUpdateSelection(0, 2, 2, 0, 2);

        mRule.finishComposingText();
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);

        // Cursor exceeds the left boundary.
        mRule.setComposingText("cdef", -100);
        mRule.waitAndVerifyUpdateSelection(2, 0, 0, 2, 6);

        // Cursor is on the left boundary.
        mRule.getInputMethodManagerWrapper().expectsSelectionOutsideComposition();
        mRule.setComposingText("cd", -2);
        mRule.waitAndVerifyUpdateSelection(3, 0, 0, 2, 4);

        mRule.getInputMethodManagerWrapper().expectsSelectionOutsideComposition();
        // Cursor is between the left boundary and the composing text.
        mRule.setComposingText("cd", -1);
        mRule.waitAndVerifyUpdateSelection(4, 1, 1, 2, 4);

        // Cursor is on the left of composing text.
        mRule.setComposingText("cd", 0);
        mRule.waitAndVerifyUpdateSelection(5, 2, 2, 2, 4);

        mRule.finishComposingText();
        mRule.waitAndVerifyUpdateSelection(6, 2, 2, -1, -1);

        // Cursor is on the right of composing text.
        mRule.setComposingText("ef", 1);
        mRule.waitAndVerifyUpdateSelection(7, 4, 4, 2, 4);

        mRule.getInputMethodManagerWrapper().expectsSelectionOutsideComposition();
        // Cursor is between the composing text and the right boundary.
        mRule.setComposingText("ef", 2);
        mRule.waitAndVerifyUpdateSelection(8, 5, 5, 2, 4);

        mRule.getInputMethodManagerWrapper().expectsSelectionOutsideComposition();
        // Cursor is on the right boundary.
        mRule.setComposingText("ef", 3);
        mRule.waitAndVerifyUpdateSelection(9, 6, 6, 2, 4);

        mRule.getInputMethodManagerWrapper().expectsSelectionOutsideComposition();
        // Cursor exceeds the right boundary.
        mRule.setComposingText("efgh", 100);
        mRule.waitAndVerifyUpdateSelection(10, 8, 8, 2, 6);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testCommitTextForNewCursorPositions() throws Throwable {
        // Cursor is on the left of committing text.
        mRule.commitText("ab", 0);
        mRule.waitAndVerifyUpdateSelection(0, 0, 0, -1, -1);

        // Cursor is on the right of committing text.
        mRule.commitText("cd", 1);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);

        // Cursor is between the committing text and the right boundary.
        mRule.commitText("ef", 2);
        mRule.waitAndVerifyUpdateSelection(2, 5, 5, -1, -1);

        // Cursor is between the left boundary and the committing text.
        mRule.commitText("gh", -3);
        mRule.waitAndVerifyUpdateSelection(3, 2, 2, -1, -1);

        // Cursor is on the right boundary.
        mRule.commitText("ij", 7);
        mRule.waitAndVerifyUpdateSelection(4, 10, 10, -1, -1);

        // Cursor is on the left boundary.
        mRule.commitText("kl", -10);
        mRule.waitAndVerifyUpdateSelection(5, 0, 0, -1, -1);

        // Cursor exceeds the right boundary.
        mRule.commitText("mn", 100);
        mRule.waitAndVerifyUpdateSelection(6, 14, 14, -1, -1);

        // Cursor exceeds the left boundary.
        mRule.commitText("op", -100);
        mRule.waitAndVerifyUpdateSelection(7, 0, 0, -1, -1);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testSetComposingTextWithEmptyText() throws Throwable {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        mRule.setComposingText("AB", 1);
        mRule.waitAndVerifyUpdateSelection(1, 7, 7, 5, 7);

        // With previous composition.
        mRule.setComposingText("", -3);
        mRule.waitAndVerifyUpdateSelection(2, 2, 2, -1, -1);
        mRule.assertTextsAroundCursor("he", null, "llo");

        // Without previous composition.
        mRule.setComposingText("", 3);
        mRule.waitAndVerifyUpdateSelection(3, 4, 4, -1, -1);
        mRule.assertTextsAroundCursor("hell", null, "o");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testCommitTextWithEmptyText() throws Throwable {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.setSelection(2, 2);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);

        mRule.setComposingText("world", 1);
        mRule.waitAndVerifyUpdateSelection(2, 7, 7, 2, 7);
        // With previous composition.
        mRule.commitText("", 2);
        mRule.waitAndVerifyUpdateSelection(3, 3, 3, -1, -1);

        // Without previous composition.
        mRule.commitText("", -1);
        mRule.waitAndVerifyUpdateSelection(4, 2, 2, -1, -1);

        // Although it is not documented in the spec, commitText() also removes existing selection.
        mRule.setSelection(2, 5);
        mRule.commitText("", 1);
        mRule.waitAndVerifyUpdateSelection(5, 2, 5, -1, -1);
        mRule.waitAndVerifyUpdateSelection(6, 2, 2, -1, -1);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testCommitWhileComposingText() throws Throwable {
        mRule.setComposingText("h", 1);
        mRule.waitAndVerifyUpdateSelection(0, 1, 1, 0, 1);

        mRule.setComposingText("he", 1);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, 0, 2);

        mRule.setComposingText("hel", 1);
        mRule.waitAndVerifyUpdateSelection(2, 3, 3, 0, 3);

        mRule.commitText("hel", 1);
        mRule.waitAndVerifyUpdateSelection(3, 3, 3, -1, -1);

        mRule.setComposingText("lo", 1);
        mRule.waitAndVerifyUpdateSelection(4, 5, 5, 3, 5);

        mRule.commitText("", 1);
        mRule.waitAndVerifyUpdateSelection(5, 3, 3, -1, -1);

        mRule.assertTextsAroundCursor("hel", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testCommitEnterKeyWhileComposingText() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");

        mRule.setComposingText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, 0, 5);

        // Cancel the current composition and replace it with enter.
        mRule.commitText("\n", 1);
        mRule.waitAndVerifyUpdateSelection(1, 1, 1, -1, -1);
        // Blink internal editor has <div>\n<br></div> where <br> is a placeholder
        // to place caret after the newline.
        mRule.assertTextsAroundCursor("\n", null, "");

        mRule.commitText("world", 1);
        mRule.waitAndVerifyUpdateSelection(2, 6, 6, -1, -1);
        mRule.assertTextsAroundCursor("\nworld", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeCopy() throws Exception {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        mRule.setSelection(2, 5);
        mRule.waitAndVerifyUpdateSelection(1, 2, 5, -1, -1);

        mRule.copy();
        mRule.assertClipboardContents(mRule.getActivity(), "llo");
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testEnterTextAndRefocus() throws Exception {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.restartInput();
        DOMUtils.clickNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);

        Assert.assertEquals(5, mRule.getConnectionFactory().getOutAttrs().initialSelStart);
        Assert.assertEquals(5, mRule.getConnectionFactory().getOutAttrs().initialSelEnd);
    }

    private static int getImeAction(EditorInfo editorInfo) {
        return editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testAdvanceFocusNextAndPrevious() throws Exception {
        mRule.focusElement("textarea");
        // Forward direction focus. Excessive focus advance should be ignored.
        for (int i = 0; i < 10; ++i) {
            // Forward direction focus.
            mRule.performEditorAction(EditorInfo.IME_ACTION_NEXT);
        }
        mRule.waitForKeyboardStates(7, 0, 7,
                new Integer[] {TextInputType.TEXT_AREA, TextInputType.TEXT_AREA,
                        TextInputType.NUMBER, TextInputType.NUMBER, TextInputType.CONTENT_EDITABLE,
                        TextInputType.SEARCH, TextInputType.TEXT});
        ArrayList<EditorInfo> editorInfoList =
                mRule.getInputMethodManagerWrapper().getEditorInfoList();
        Assert.assertEquals(7, editorInfoList.size());
        // textarea.
        Assert.assertEquals(EditorInfo.IME_ACTION_NONE, getImeAction(editorInfoList.get(0)));
        // textarea2.
        Assert.assertEquals(EditorInfo.IME_ACTION_NONE, getImeAction(editorInfoList.get(1)));
        // input_number1.
        Assert.assertEquals(EditorInfo.IME_ACTION_NEXT, getImeAction(editorInfoList.get(2)));
        // input_number2.
        Assert.assertEquals(EditorInfo.IME_ACTION_NEXT, getImeAction(editorInfoList.get(3)));
        // content_editable1.
        Assert.assertEquals(EditorInfo.IME_ACTION_NONE, getImeAction(editorInfoList.get(4)));
        // search1.
        Assert.assertEquals(EditorInfo.IME_ACTION_SEARCH, getImeAction(editorInfoList.get(5)));
        // input_text3.
        Assert.assertEquals(EditorInfo.IME_ACTION_GO, getImeAction(editorInfoList.get(6)));

        mRule.resetAllStates();

        // Backward direction focus. Excessive focus advance should be ignored.
        for (int i = 0; i < 10; ++i) {
            // Backward direction focus.
            mRule.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
        }
        mRule.waitForKeyboardStates(6, 0, 6,
                new Integer[] {TextInputType.SEARCH, TextInputType.CONTENT_EDITABLE,
                        TextInputType.NUMBER, TextInputType.NUMBER, TextInputType.TEXT_AREA,
                        TextInputType.TEXT_AREA});
        editorInfoList = mRule.getInputMethodManagerWrapper().getEditorInfoList();
        Assert.assertEquals(6, editorInfoList.size());
        // search1.
        Assert.assertEquals(EditorInfo.IME_ACTION_SEARCH, getImeAction(editorInfoList.get(0)));
        // content_editable1.
        Assert.assertEquals(EditorInfo.IME_ACTION_NONE, getImeAction(editorInfoList.get(1)));
        // input_number2.
        Assert.assertEquals(EditorInfo.IME_ACTION_NEXT, getImeAction(editorInfoList.get(2)));
        // input_number1.
        Assert.assertEquals(EditorInfo.IME_ACTION_NEXT, getImeAction(editorInfoList.get(3)));
        // textarea2.
        Assert.assertEquals(EditorInfo.IME_ACTION_NONE, getImeAction(editorInfoList.get(4)));
        // textarea.
        Assert.assertEquals(EditorInfo.IME_ACTION_NONE, getImeAction(editorInfoList.get(5)));
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testShowAndHideSoftInput() throws Exception {
        mRule.focusElement("input_radio", false);

        // hideSoftKeyboard(), mRule.restartInput()
        mRule.waitForKeyboardStates(0, 1, 1, new Integer[] {});

        // When input connection is null, we still need to set flags to prevent InputMethodService
        // from entering fullscreen mode and from opening custom UI.
        CriteriaHelper.pollUiThread(
                () -> Criteria.checkThat(mRule.getInputConnection(), Matchers.nullValue()));
        Assert.assertTrue(
                (mRule.getConnectionFactory().getOutAttrs().imeOptions
                        & (EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI))
                != 0);

        // showSoftInput(), mRule.restartInput()
        mRule.focusElement("input_number1");
        mRule.waitForKeyboardStates(1, 1, 2, new Integer[] {TextInputType.NUMBER});
        Assert.assertNotNull(mRule.getInputMethodManagerWrapper().getInputConnection());

        mRule.focusElement("input_number2");
        // Hide should never be called here. Otherwise we will see a flicker. Restarted to
        // reset internal states to handle the new input form.
        mRule.waitForKeyboardStates(
                2, 1, 3, new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER});

        mRule.focusElement("input_text");
        // showSoftInput() on input_text. mRule.restartInput() on input_number1 due to focus change,
        // and mRule.restartInput() on input_text later.
        mRule.waitForKeyboardStates(3, 1, 4,
                new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER, TextInputType.TEXT});

        mRule.setComposingText("a", 1);
        mRule.waitAndVerifyUpdateSelection(0, 0, 0, -1, -1);
        mRule.waitAndVerifyUpdateSelection(1, 1, 1, 0, 1);
        mRule.resetUpdateSelectionList();

        // JavaScript changes focus.
        String code = "(function() { "
                + "var textarea = document.getElementById('textarea');"
                + "textarea.focus();"
                + "})();";
        JavaScriptUtils.executeJavaScriptAndWaitForResult(mRule.getWebContents(), code);
        mRule.waitAndVerifyUpdateSelection(0, 0, 0, -1, -1);
        mRule.resetUpdateSelectionList();

        mRule.waitForKeyboardStates(4, 1, 5,
                new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER, TextInputType.TEXT,
                        TextInputType.TEXT_AREA});
        Assert.assertEquals(0, mRule.getConnectionFactory().getOutAttrs().initialSelStart);
        Assert.assertEquals(0, mRule.getConnectionFactory().getOutAttrs().initialSelEnd);

        mRule.setComposingText("aa", 1);
        mRule.waitAndVerifyUpdateSelection(0, 2, 2, 0, 2);

        mRule.focusElement("input_text");
        mRule.waitForKeyboardStates(5, 1, 6,
                new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER, TextInputType.TEXT,
                        TextInputType.TEXT_AREA, TextInputType.TEXT});
        Assert.assertEquals(1, mRule.getConnectionFactory().getOutAttrs().initialSelStart);
        Assert.assertEquals(1, mRule.getConnectionFactory().getOutAttrs().initialSelEnd);

        mRule.focusElement("input_radio", false);
        // hideSoftInput(), mRule.restartInput()
        mRule.waitForKeyboardStates(5, 2, 7,
                new Integer[] {TextInputType.NUMBER, TextInputType.NUMBER, TextInputType.TEXT,
                        TextInputType.TEXT_AREA, TextInputType.TEXT});
    }
    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testKeyboardNotDismissedAfterCopySelection() throws Exception {
        mRule.commitText("Sample_Text", 1);
        mRule.waitAndVerifyUpdateSelection(0, 11, 11, -1, -1);

        // Select 'text' part.
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");

        mRule.assertWaitForSelectActionBarStatus(true);

        mRule.selectAll();
        mRule.copy();
        mRule.assertClipboardContents(mRule.getActivity(), "Sample_Text");
        Assert.assertEquals(11, mRule.getInputMethodManagerWrapper().getSelection().end());
        mRule.assertWaitForKeyboardStatus(true);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeNotDismissedAfterCutSelection() throws Exception {
        mRule.commitText("Sample_Text", 1);
        mRule.waitAndVerifyUpdateSelection(0, 11, 11, -1, -1);
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForSelectActionBarStatus(true);
        mRule.assertWaitForKeyboardStatus(true);
        mRule.cut();
        mRule.assertWaitForKeyboardStatus(true);
        mRule.assertWaitForSelectActionBarStatus(false);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeNotShownOnLongPressingEmptyInput() throws Exception {
        DOMUtils.focusNode(mRule.getWebContents(), "input_radio");
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(false);
        mRule.commitText("Sample_Text", 1);
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testSelectActionBarShownOnLongPressingInput() throws Exception {
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForSelectActionBarStatus(false);
        mRule.commitText("Sample_Text", 1);
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForSelectActionBarStatus(true);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testLongPressInputWhileComposingText() throws Exception {
        mRule.assertWaitForSelectActionBarStatus(false);
        mRule.setComposingText("SampleTextThatIsVeryLong Test", 1);
        mRule.waitAndVerifyUpdateSelection(0, 29, 29, 0, 29);
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");

        mRule.assertWaitForSelectActionBarStatus(true);

        // Long press will first change selection region, and then trigger IME app to show up.
        // See RenderFrameImpl::didChangeSelection() and RenderWidget::didHandleGestureEvent().
        mRule.waitAndVerifyUpdateSelection(1, 0, 24, 0, 29);

        // Now IME app wants to finish composing text because an external selection
        // change has been detected. At least Google Latin IME and Samsung IME
        // behave this way.
        mRule.finishComposingText();
        mRule.waitAndVerifyUpdateSelection(2, 0, 24, -1, -1);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeShownWhenLongPressOnAlreadySelectedText() throws Exception {
        mRule.assertWaitForSelectActionBarStatus(false);
        mRule.commitText("Sample_Text", 1);

        int showCount = mRule.getInputMethodManagerWrapper().getShowSoftInputCounter();
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForSelectActionBarStatus(true);
        Assert.assertEquals(
                showCount + 1, mRule.getInputMethodManagerWrapper().getShowSoftInputCounter());

        // Now long press again. Selection region remains the same, but the logic
        // should trigger IME to show up. Note that Android does not provide show /
        // hide status of IME, so we will just check whether showIme() has been triggered.
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        final int newCount = showCount + 2;
        CriteriaHelper.pollUiThread(() -> {
            Criteria.checkThat(mRule.getInputMethodManagerWrapper().getShowSoftInputCounter(),
                    Matchers.is(newCount));
        });
    }

    private void reloadPage() throws Exception {
        // Reload the page, then focus will be lost and keyboard should be hidden.
        mRule.fullyLoadUrl(mRule.getWebContents().getLastCommittedUrl().getSpec());
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    @SuppressWarnings("TryFailThrowable") // TODO(tedchoc): Remove after fixing timeout.
    public void testPhysicalKeyboard_AttachDetach() throws Throwable {
        mRule.attachPhysicalKeyboard();
        // We still call showSoftKeyboard, which will be ignored by physical keyboard.
        mRule.waitForKeyboardStates(1, 0, 1, new Integer[] {TextInputType.TEXT});
        mRule.setComposingText("a", 1);
        mRule.waitForKeyboardStates(1, 0, 1, new Integer[] {TextInputType.TEXT});
        mRule.detachPhysicalKeyboard();
        mRule.assertWaitForKeyboardStatus(true);
        // Now we really show soft keyboard. We also call mRule.restartInput when configuration
        // changes.
        mRule.waitForKeyboardStates(
                2, 0, 2, new Integer[] {TextInputType.TEXT, TextInputType.TEXT});

        reloadPage();

        // Depending on the timing, hideSoftInput and mRule.restartInput call counts may vary here
        // because render widget gets restarted. But the end result should be the same.
        mRule.assertWaitForKeyboardStatus(false);

        mRule.detachPhysicalKeyboard();

        // We should not show soft keyboard here because focus has been lost.
        thrown.expect(AssertionError.class);
        CriteriaHelper.pollUiThread(
                () -> mRule.getInputMethodManagerWrapper().isShowWithoutHideOutstanding());
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testSelectActionBarClearedOnTappingInput() throws Exception {
        mRule.commitText("Sample_Text", 1);
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);
        mRule.assertWaitForSelectActionBarStatus(true);
        DOMUtils.clickNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForSelectActionBarStatus(false);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testSelectActionBarClearedOnTappingOutsideInput() throws Exception {
        mRule.commitText("Sample_Text", 1);
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);
        mRule.assertWaitForSelectActionBarStatus(true);
        DOMUtils.clickNode(mRule.getWebContents(), "plain_text");
        mRule.assertWaitForKeyboardStatus(false);
        mRule.assertWaitForSelectActionBarStatus(false);

        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);
        mRule.assertWaitForSelectActionBarStatus(true);
        DOMUtils.clickNode(mRule.getWebContents(), "input_radio");
        mRule.assertWaitForKeyboardStatus(false);
        mRule.assertWaitForSelectActionBarStatus(false);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeNotShownOnLongPressingDifferentEmptyInputs() throws Exception {
        DOMUtils.focusNode(mRule.getWebContents(), "input_radio");
        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(false);
        DOMUtils.longPressNode(mRule.getWebContents(), "textarea");
        mRule.assertWaitForKeyboardStatus(false);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeStaysOnLongPressingDifferentNonEmptyInputs() throws Exception {
        DOMUtils.focusNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);

        mRule.commitText("Sample_Text", 1);
        // We should wait to avoid race condition.
        mRule.waitAndVerifyUpdateSelection(0, 11, 11, -1, -1);

        DOMUtils.focusNode(mRule.getWebContents(), "textarea");
        mRule.waitAndVerifyUpdateSelection(1, 0, 0, -1, -1);

        mRule.commitText("Sample_Text", 1);
        mRule.waitAndVerifyUpdateSelection(2, 11, 11, -1, -1);

        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);
        mRule.assertWaitForSelectActionBarStatus(true);

        DOMUtils.longPressNode(mRule.getWebContents(), "textarea");
        mRule.assertWaitForKeyboardStatus(true);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeCut() throws Exception {
        mRule.commitText("snarful", 1);
        mRule.waitAndVerifyUpdateSelection(0, 7, 7, -1, -1);

        mRule.setSelection(1, 5);
        mRule.waitAndVerifyUpdateSelection(1, 1, 5, -1, -1);

        mRule.cut();
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, -1, -1);
        mRule.assertTextsAroundCursor("s", null, "ul");
        mRule.assertClipboardContents(mRule.getActivity(), "narf");
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImePaste() throws Exception {
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            ClipboardManager clipboardManager =
                    (ClipboardManager) mRule.getActivity().getSystemService(
                            Context.CLIPBOARD_SERVICE);
            clipboardManager.setPrimaryClip(ClipData.newPlainText("blarg", "blarg"));
        });

        mRule.paste();
        // Paste is a two step process when there is a non-zero selection.
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.assertTextsAroundCursor("blarg", null, "");

        mRule.setSelection(3, 5);
        mRule.waitAndVerifyUpdateSelection(1, 3, 5, -1, -1);
        mRule.assertTextsAroundCursor("bla", "rg", "");

        mRule.paste();
        // Paste is a two step process when there is a non-zero selection.
        mRule.waitAndVerifyUpdateSelection(2, 8, 8, -1, -1);
        mRule.assertTextsAroundCursor("blablarg", null, "");

        mRule.paste();
        mRule.waitAndVerifyUpdateSelection(3, 13, 13, -1, -1);
        mRule.assertTextsAroundCursor("blablargblarg", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testImeSelectAndCollapseSelection() throws Exception {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        mRule.selectAll();
        mRule.waitAndVerifyUpdateSelection(1, 0, 5, -1, -1);

        mRule.collapseSelection();
        mRule.waitAndVerifyUpdateSelection(2, 5, 5, -1, -1);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testShowVirtualKeyboardIfEnabled() throws Throwable {
        // ShowVirtualKeyboardIfEnabled() is now implicitly called by the updated focus
        // heuristic so no need to call explicitly. http://crbug.com/371927
        DOMUtils.focusNode(mRule.getWebContents(), "input_radio");
        mRule.assertWaitForKeyboardStatus(false);

        DOMUtils.focusNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForKeyboardStatus(true);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testFinishComposingText() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");

        mRule.commitText("hllo", 1);
        mRule.waitAndVerifyUpdateSelection(0, 4, 4, -1, -1);

        mRule.commitText(" ", 1);
        mRule.waitAndVerifyUpdateSelection(1, 5, 5, -1, -1);

        mRule.setSelection(1, 1);
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, -1, -1);
        mRule.assertTextsAroundCursor("h", null, "llo ");

        mRule.setComposingRegion(0, 4);
        mRule.waitAndVerifyUpdateSelection(3, 1, 1, 0, 4);

        mRule.finishComposingText();
        mRule.waitAndVerifyUpdateSelection(4, 1, 1, -1, -1);

        mRule.commitText("\n", 1);
        mRule.waitAndVerifyUpdateSelection(5, 2, 2, -1, -1);
        mRule.assertTextsAroundCursor("h\n", null, "llo ");
    }

    // http://crbug.com/445499
    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testDeleteText() throws Throwable {
        mRule.focusElement("textarea");

        // The calls below are a reflection of what the stock Google Keyboard (Andr
        // when the noted key is touched on screen.
        // H
        mRule.setComposingText("h", 1);
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));

        // A
        mRule.setComposingText("ha", 1);
        Assert.assertEquals("ha", mRule.getTextBeforeCursor(9, 0));

        mRule.setComposingText("h", 1);
        mRule.setComposingRegion(0, 1);
        mRule.setComposingText("h", 1);
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));

        // I
        mRule.setComposingText("hi", 1);
        Assert.assertEquals("hi", mRule.getTextBeforeCursor(9, 0));

        // SPACE
        mRule.commitText("hi", 1);
        mRule.commitText(" ", 1);
        Assert.assertEquals("hi ", mRule.getTextBeforeCursor(9, 0));

        // DEL
        mRule.deleteSurroundingText(1, 0);
        mRule.setComposingRegion(0, 2);
        Assert.assertEquals("hi", mRule.getTextBeforeCursor(9, 0));

        mRule.setComposingText("h", 1);
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));

        mRule.commitText("", 1);
        Assert.assertEquals("", mRule.getTextBeforeCursor(9, 0));

        // DEL (on empty input)
        mRule.deleteSurroundingText(1, 0); // DEL on empty still sends 1,0
        Assert.assertEquals("", mRule.getTextBeforeCursor(9, 0));
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testSwipingText() throws Throwable {
        mRule.focusElement("textarea");

        // The calls below are a reflection of what the stock Google Keyboard (Android 4.4) sends
        // when the word is swiped on the soft keyboard.  Exercise care when altering to make sure
        // that the test reflects reality.  If this test breaks, it's possible that code has
        // changed and different calls need to be made instead.
        // "three"
        mRule.setComposingText("three", 1);
        Assert.assertEquals("three", mRule.getTextBeforeCursor(99, 0));

        // "word"
        mRule.commitText("three", 1);
        mRule.commitText(" ", 1);
        mRule.setComposingText("word", 1);
        Assert.assertEquals("three word", mRule.getTextBeforeCursor(99, 0));

        // "test"
        mRule.commitText("word", 1);
        mRule.commitText(" ", 1);
        mRule.setComposingText("test", 1);
        Assert.assertEquals("three word test", mRule.getTextBeforeCursor(99, 0));
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    @DisabledTest(message = "https://crbug.com/1222342")
    public void testDeleteMultiCharacterCodepoint() throws Throwable {
        // This smiley is a multi character codepoint.
        final String smiley = "\uD83D\uDE0A";

        mRule.commitText(smiley, 1);
        mRule.waitAndVerifyUpdateSelection(0, 2, 2, -1, -1);
        mRule.assertTextsAroundCursor(smiley, null, "");

        // DEL, sent via mRule.dispatchKeyEvent like it is in Android WebView or a physical
        // keyboard.
        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0));

        mRule.waitAndVerifyUpdateSelection(1, 0, 0, -1, -1);

        // Make sure that we accept further typing after deleting the smiley.
        mRule.setComposingText("s", 1);
        mRule.setComposingText("sm", 1);
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, 0, 1);
        mRule.waitAndVerifyUpdateSelection(3, 2, 2, 0, 2);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testBackspaceKeycode() throws Throwable {
        mRule.focusElement("textarea");

        // H
        mRule.commitText("h", 1);
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));

        // A
        mRule.commitText("a", 1);
        Assert.assertEquals("ha", mRule.getTextBeforeCursor(9, 0));

        // DEL, sent via mRule.dispatchKeyEvent like it is in Android WebView or a physical
        // keyboard.
        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0));

        // DEL
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testRepeatBackspaceKeycode() throws Throwable {
        mRule.focusElement("textarea");

        // H
        mRule.commitText("h", 1);
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));

        // A
        mRule.commitText("a", 1);
        Assert.assertEquals("ha", mRule.getTextBeforeCursor(9, 0));

        // Multiple keydowns should each delete one character (this is for physical keyboard
        // key-repeat).
        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0));

        // DEL
        Assert.assertEquals("", mRule.getTextBeforeCursor(9, 0));
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testPhysicalKeyboard() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");

        // Type 'a' using a physical keyboard.
        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0));
        mRule.waitAndVerifyUpdateSelection(0, 1, 1, -1, -1);

        // Type 'enter' key.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0));
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);
        mRule.assertTextsAroundCursor("a\n", null, "");

        // Type 'b'.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_B, 0));
        mRule.waitAndVerifyUpdateSelection(2, 3, 3, -1, -1);
        mRule.assertTextsAroundCursor("a\nb", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testPhysicalKeyboard_AccentKeyCodes() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");
        int index = 0;

        // h
        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_H, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_H, 0));
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 1, 1, -1, -1);

        // ALT-i  (circumflex accent key on virtual keyboard). Accent should not appear until the
        // next letter is entered.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("h", mRule.getTextBeforeCursor(9, 0));

        // finishComposingText() should not prevent the accent from being joined.
        mRule.finishComposingText();

        // o
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_O, 0));
        Assert.assertEquals("hô", mRule.getTextBeforeCursor(9, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_O, 0));
        Assert.assertEquals("hô", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 2, 2, -1, -1);

        // o again. Should not have accent mark this time.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_O, 0));
        Assert.assertEquals("hôo", mRule.getTextBeforeCursor(9, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_O, 0));
        Assert.assertEquals("hôo", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 3, 3, -1, -1);

        // ALT-i. Should not display anything until the next key is pressed.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("hôo", mRule.getTextBeforeCursor(9, 0));

        // ALT-i again should commit the caret this time.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("hôoˆ", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 4, 4, -1, -1);

        // b (cannot be accented, should just appear after)
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B, 0));
        Assert.assertEquals("hôoˆb", mRule.getTextBeforeCursor(9, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_B, 0));
        Assert.assertEquals("hôoˆb", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 5, 5, -1, -1);

        // ALT-i. Should not display anything.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("hôoˆb", mRule.getTextBeforeCursor(9, 0));

        // Backspace. Should delete the b even though we have a pending accent.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0));
        Assert.assertEquals("hôoˆ", mRule.getTextBeforeCursor(9, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL, 0));
        Assert.assertEquals("hôoˆ", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 4, 4, -1, -1);

        // Alt-i. Should not display anything (the pending accent should have been cleared by the
        // backspace).
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("hôoˆ", mRule.getTextBeforeCursor(9, 0));

        // Space. Should display the pending accent.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE, 0));
        Assert.assertEquals("hôoˆˆ", mRule.getTextBeforeCursor(9, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE, 0));
        Assert.assertEquals("hôoˆˆ", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 5, 5, -1, -1);

        // Alt-i. Should not display anything but should set a circumflex as the pending accent.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_I, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("hôoˆˆ", mRule.getTextBeforeCursor(9, 0));

        // Alt-e. Should output the circumflex and set an acute accent as the pending accent.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                KeyEvent.KEYCODE_E, 0, KeyEvent.META_ALT_ON));
        mRule.dispatchKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                KeyEvent.KEYCODE_E, 0, KeyEvent.META_ALT_ON));
        Assert.assertEquals("hôoˆˆˆ", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 6, 6, -1, -1);

        // e. Should output an e with an acute accent.
        eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_E, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_E, 0));
        Assert.assertEquals("hôoˆˆˆé", mRule.getTextBeforeCursor(9, 0));
        mRule.waitAndVerifyUpdateSelection(index++, 7, 7, -1, -1);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testSetComposingRegionOutOfBounds() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");
        mRule.setComposingText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, 0, 5);

        mRule.setComposingRegion(0, 0);
        mRule.waitAndVerifyUpdateSelection(1, 5, 5, -1, -1);
        mRule.setComposingRegion(0, 9);
        mRule.waitAndVerifyUpdateSelection(2, 5, 5, 0, 5);
        mRule.setComposingRegion(9, 1);
        mRule.waitAndVerifyUpdateSelection(3, 5, 5, 1, 5);
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testEnterKey_AfterCommitText() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");

        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0));
        mRule.waitAndVerifyUpdateSelection(1, 6, 6, -1, -1);
        mRule.assertTextsAroundCursor("hello\n", null, "");

        mRule.commitText("world", 1);
        mRule.waitAndVerifyUpdateSelection(2, 11, 11, -1, -1);
        mRule.assertTextsAroundCursor("hello\nworld", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testEnterKey_WhileComposingText() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");

        mRule.setComposingText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, 0, 5);

        // IME app should call this, otherwise enter key should clear the current composition.
        mRule.finishComposingText();
        mRule.waitAndVerifyUpdateSelection(1, 5, 5, -1, -1);

        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0));
        mRule.dispatchKeyEvent(
                new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0));

        // The second new line is not a user visible/editable one, it is a side-effect of Blink
        // using <br> internally. This only happens when \n is at the end.
        mRule.waitAndVerifyUpdateSelection(2, 6, 6, -1, -1);

        mRule.commitText("world", 1);
        mRule.waitAndVerifyUpdateSelection(3, 11, 11, -1, -1);
        mRule.assertTextsAroundCursor("hello\nworld", null, "");
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testDpadKeyCodesWhileSwipingText() throws Throwable {
        int showCount = mRule.getInputMethodManagerWrapper().getShowSoftInputCounter();
        mRule.focusElement("textarea");

        // focusElement() calls showSoftInput().
        CriteriaHelper.pollUiThread(() -> {
            Criteria.checkThat(mRule.getInputMethodManagerWrapper().getShowSoftInputCounter(),
                    Matchers.is(showCount + 1));
        });

        // DPAD_CENTER should cause keyboard to appear on keyup.
        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0));

        // Should not have called showSoftInput() on keydown.
        CriteriaHelper.pollUiThread(() -> {
            Criteria.checkThat(mRule.getInputMethodManagerWrapper().getShowSoftInputCounter(),
                    Matchers.is(showCount + 1));
        });

        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0));

        // Should have called showSoftInput() on keyup.
        CriteriaHelper.pollUiThread(() -> {
            Criteria.checkThat(mRule.getInputMethodManagerWrapper().getShowSoftInputCounter(),
                    Matchers.is(showCount + 2));
        });
    }

    @Test
    @SmallTest
    @Feature({"TextInput", "Main"})
    public void testNavigateTextWithDpadKeyCodes() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("textarea");

        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, 0));
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_LEFT, 0));

        mRule.assertTextsAroundCursor("hell", null, "o");
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testPastePopupShowAndHide() throws Throwable {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);

        mRule.selectAll();
        mRule.waitAndVerifyUpdateSelection(1, 0, 5, -1, -1);
        mRule.assertTextsAroundCursor("", "hello", "");

        mRule.cut();
        mRule.waitAndVerifyUpdateSelection(2, 0, 0, -1, -1);
        mRule.assertTextsAroundCursor("", null, "");

        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        CriteriaHelper.pollUiThread(() -> {
            Criteria.checkThat(
                    mRule.getSelectionPopupController().isPastePopupShowing(), Matchers.is(true));
            Criteria.checkThat(
                    mRule.getSelectionPopupController().isInsertionForTesting(), Matchers.is(true));
        });

        mRule.setComposingText("h", 1);
        CriteriaHelper.pollUiThread(() -> {
            Criteria.checkThat(
                    mRule.getSelectionPopupController().isPastePopupShowing(), Matchers.is(false));
        });
        Assert.assertFalse(mRule.getSelectionPopupController().isInsertionForTesting());
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testSelectionClearedOnKeyEvent() throws Throwable {
        mRule.commitText("Sample_Text", 1);
        mRule.waitAndVerifyUpdateSelection(0, 11, 11, -1, -1);

        DOMUtils.longPressNode(mRule.getWebContents(), "input_text");
        mRule.assertWaitForSelectActionBarStatus(true);

        mRule.setComposingText("h", 1);
        mRule.assertWaitForSelectActionBarStatus(false);
        Assert.assertFalse(mRule.getSelectionPopupController().hasSelection());
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testTextHandlesPreservedWithDpadNavigation() throws Throwable {
        DOMUtils.longPressNode(mRule.getWebContents(), "plain_text");
        mRule.assertWaitForSelectActionBarStatus(true);
        Assert.assertTrue(mRule.getSelectionPopupController().hasSelection());

        long eventTime = SystemClock.uptimeMillis();
        mRule.dispatchKeyEvent(new KeyEvent(
                eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, 0));
        mRule.assertWaitForSelectActionBarStatus(true);
        Assert.assertTrue(mRule.getSelectionPopupController().hasSelection());
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testRestartInputWhileComposingText() throws Throwable {
        mRule.setComposingText("abc", 1);
        mRule.waitAndVerifyUpdateSelection(0, 3, 3, 0, 3);
        mRule.restartInput();
        // We don't do anything when input gets restarted. But we depend on Android's
        // InputMethodManager and/or input methods to call mRule.finishComposingText() in setting
        // current input connection as active or finishing the current input connection.
        Thread.sleep(1000);
        Assert.assertEquals(
                1, mRule.getInputMethodManagerWrapper().getUpdateSelectionList().size());
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testRestartInputKeepsTextAndCursor() throws Exception {
        mRule.commitText("ab", 2);
        mRule.restartInput();
        Assert.assertEquals("ab", mRule.getTextBeforeCursor(10, 0));
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testContentEditableEvents_ComposingText() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("contenteditable_event");
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.setComposingText("a", 1);
        mRule.waitAndVerifyUpdateSelection(0, 1, 1, 0, 1);
        mRule.waitForEventLogs(
                "keydown(229),compositionstart(),compositionupdate(a),input(a),keyup(229),"
                + "selectionchange");
        mRule.clearEventLogs();

        mRule.finishComposingText();
        mRule.waitAndVerifyUpdateSelection(1, 1, 1, -1, -1);
        mRule.waitForEventLogs("compositionend(a)");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testInputTextEvents_ComposingText() throws Throwable {
        mRule.setComposingText("a", 1);
        mRule.waitAndVerifyUpdateSelection(0, 1, 1, 0, 1);
        mRule.waitForEventLogs("keydown(229),compositionstart(),compositionupdate(a),"
                + "input(a),keyup(229),selectionchange");
        mRule.clearEventLogs();

        mRule.finishComposingText();
        mRule.waitAndVerifyUpdateSelection(1, 1, 1, -1, -1);
        mRule.waitForEventLogs("compositionend(a)");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testContentEditableEvents_CommitText() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("contenteditable_event");
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.commitText("a", 1);
        mRule.waitAndVerifyUpdateSelection(0, 1, 1, -1, -1);

        mRule.waitForEventLogs("keydown(229),input(a),keyup(229),selectionchange");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testInputTextEvents_CommitText() throws Throwable {
        mRule.commitText("a", 1);
        mRule.waitAndVerifyUpdateSelection(0, 1, 1, -1, -1);

        mRule.waitForEventLogs("keydown(229),input(a),keyup(229),selectionchange");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testContentEditableEvents_DeleteSurroundingText() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("contenteditable_event");
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.waitForEventLogs("keydown(229),input(hello),keyup(229),selectionchange");
        mRule.clearEventLogs();

        mRule.setSelection(2, 2);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.deleteSurroundingText(1, 1);
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, -1, -1);

        // TODO(yabinh): It should only fire 1 input and 1 selectionchange events.
        mRule.waitForEventLogs(
                "keydown(229),input,input,keyup(229),selectionchange,selectionchange");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testInputTextEvents_DeleteSurroundingText() throws Throwable {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.waitForEventLogs("keydown(229),input(hello),keyup(229),selectionchange");
        mRule.clearEventLogs();

        mRule.setSelection(2, 2);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.deleteSurroundingText(1, 1);
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, -1, -1);
        // TODO(yabinh): It should only fire 1 input and 1 selectionchange events.
        mRule.waitForEventLogs(
                "keydown(229),input,input,keyup(229),selectionchange,selectionchange");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testContentEditableEvents_DeleteSurroundingTextInCodePoints() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("contenteditable_event");
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.waitForEventLogs("keydown(229),input(hello),keyup(229),selectionchange");
        mRule.clearEventLogs();

        mRule.setSelection(2, 2);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.deleteSurroundingTextInCodePoints(1, 1);
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, -1, -1);
        // TODO(yabinh): It should only fire 1 input and 1 selectionchange events.
        mRule.waitForEventLogs(
                "keydown(229),input,input,keyup(229),selectionchange,selectionchange");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testInputTextEvents_DeleteSurroundingTextInCodePoints() throws Throwable {
        mRule.commitText("hello", 1);
        mRule.waitAndVerifyUpdateSelection(0, 5, 5, -1, -1);
        mRule.waitForEventLogs("keydown(229),input(hello),keyup(229),selectionchange");
        mRule.clearEventLogs();

        mRule.setSelection(2, 2);
        mRule.waitAndVerifyUpdateSelection(1, 2, 2, -1, -1);
        mRule.waitForEventLogs("selectionchange");
        mRule.clearEventLogs();

        mRule.deleteSurroundingTextInCodePoints(1, 1);
        mRule.waitAndVerifyUpdateSelection(2, 1, 1, -1, -1);
        // TODO(yabinh): It should only fire 1 input and 1 selectionchange events.
        mRule.waitForEventLogs(
                "keydown(229),input,input,keyup(229),selectionchange,selectionchange");
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testGetCursorCapsMode() throws Throwable {
        mRule.focusElementAndWaitForStateUpdate("contenteditable_event");
        mRule.commitText("Hello World", 1);
        mRule.waitAndVerifyUpdateSelection(0, 11, 11, -1, -1);
        Assert.assertEquals(0, mRule.getCursorCapsMode(InputType.TYPE_TEXT_FLAG_CAP_WORDS));
        mRule.setSelection(6, 6);
        mRule.waitAndVerifyUpdateSelection(1, 6, 6, -1, -1);
        Assert.assertEquals(InputType.TYPE_TEXT_FLAG_CAP_WORDS,
                mRule.getCursorCapsMode(InputType.TYPE_TEXT_FLAG_CAP_WORDS));
        mRule.commitText("\n", 1);
        Assert.assertEquals(InputType.TYPE_TEXT_FLAG_CAP_WORDS,
                mRule.getCursorCapsMode(InputType.TYPE_TEXT_FLAG_CAP_WORDS));
    }

    // https://crbug.com/604675
    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testAlertInKeyUpListenerDoesNotCrash() throws Exception {
        // Call 'alert()' when 'keyup' event occurs. Since we are in contentshell,
        // this does not actually pops up the alert window.
        String code = "(function() { "
                + "var editor = document.getElementById('input_text');"
                + "editor.addEventListener('keyup', function(e) { alert('keyup') });"
                + "})();";
        JavaScriptUtils.executeJavaScriptAndWaitForResult(mRule.getWebContents(), code);
        mRule.setComposingText("ab", 1);
        mRule.finishComposingText();
        Assert.assertEquals("ab", mRule.getTextBeforeCursor(10, 0));
    }

    // https://crbug.com/616334
    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testCastToBaseInputConnection() throws Exception {
        mRule.commitText("a", 1);
        final BaseInputConnection baseInputConnection = (BaseInputConnection) mRule.getConnection();
        Assert.assertEquals("a", mRule.runBlockingOnImeThread(new Callable<CharSequence>() {
            @Override
            public CharSequence call() {
                return baseInputConnection.getTextBeforeCursor(10, 0);
            }
        }));
    }

    // Tests that the method call order is kept.
    // See crbug.com/601707 for details.
    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testSetSelectionCommitTextOrder() throws Exception {
        final ChromiumBaseInputConnection connection = mRule.getConnection();
        mRule.runBlockingOnImeThread(new Callable<Void>() {
            @Override
            public Void call() {
                connection.beginBatchEdit();
                connection.commitText("hello world", 1);
                connection.setSelection(6, 6);
                connection.deleteSurroundingText(0, 5);
                connection.commitText("'", 1);
                connection.commitText("world", 1);
                connection.setSelection(7, 7);
                connection.setComposingText("", 1);
                connection.endBatchEdit();
                return null;
            }
        });
        mRule.waitAndVerifyUpdateSelection(0, 7, 7, -1, -1);
    }

    // crbug.com/643477
    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testUiThreadAccess() throws Exception {
        final ChromiumBaseInputConnection connection = mRule.getConnection();
        TestThreadUtils.runOnUiThreadBlocking(() -> {
            // We allow UI thread access for most functions, except for
            // beginBatchEdit(), endBatchEdit(), and get* methods().
            Assert.assertTrue(connection.commitText("a", 1));
            Assert.assertTrue(connection.setComposingText("b", 1));
            Assert.assertTrue(connection.setComposingText("bc", 1));
            Assert.assertTrue(connection.finishComposingText());
        });
        Assert.assertEquals("abc", mRule.runBlockingOnImeThread(new Callable<CharSequence>() {
            @Override
            public CharSequence call() {
                return connection.getTextBeforeCursor(5, 0);
            }
        }));
    }

    @Test
    @MediumTest
    @Feature({"TextInput"})
    public void testBackgroundAndUnderlineSpans() throws Throwable {
        mRule.fullyLoadUrl("data:text/html, <div contenteditable id=\"div\" />");

        WebContents webContents = mRule.getWebContents();
        DOMUtils.focusNode(webContents, "div");

        SpannableString textToCommit = new SpannableString("hello world");
        BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(Color.MAGENTA);
        UnderlineSpan underlineSpan = new UnderlineSpan();
        textToCommit.setSpan(backgroundColorSpan, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        textToCommit.setSpan(underlineSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        mRule.commitText(textToCommit, 1);

        // Wait for renderer to acknowledge commitText(). ImeActivityTestRule.commitText() blocks
        // and waits for the IME thread to finish, but the communication between the IME thread and
        // the renderer is asynchronous, so if we try to run JavaScript right away, the text won't
        // necessarily have been committed yet.
        CriteriaHelper.pollInstrumentationThread(() -> {
            try {
                Criteria.checkThat(
                        DOMUtils.getNodeContents(webContents, "div"), Matchers.is("hello world"));
            } catch (TimeoutException e) {
                throw new CriteriaNotSatisfiedException(e);
            }
        });

        Assert.assertEquals("2",
                JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                        "internals.markerCountForNode("
                                + "  document.getElementById('div').firstChild, "
                                + "  'composition')"));

        // Colors come back as ARGB.
        Assert.assertEquals(0xFFFF00FFL,
                (long) Double.parseDouble(
                        JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                                "internals.markerBackgroundColorForNode("
                                        + "  document.getElementById('div').firstChild, "
                                        + "  'composition', 0)")));

        Assert.assertEquals(0x0000000L,
                (long) Double.parseDouble(
                        JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                                "internals.markerBackgroundColorForNode("
                                        + "  document.getElementById('div').firstChild, "
                                        + "  'composition', 1)")));

        Assert.assertEquals(0x00000000L,
                (long) Double.parseDouble(
                        JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                                "internals.markerUnderlineColorForNode("
                                        + "  document.getElementById('div').firstChild, "
                                        + "  'composition', 0)")));

        Assert.assertEquals(0x00000000L,
                (long) Double.parseDouble(
                        JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                                "internals.markerUnderlineColorForNode("
                                        + "  document.getElementById('div').firstChild, "
                                        + "  'composition', 1)")));

        Assert.assertEquals("0",
                JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                        "internals.markerRangeForNode("
                                + "  document.getElementById('div').firstChild, "
                                + "  'composition', 0).startOffset"));

        Assert.assertEquals("5",
                JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                        "internals.markerRangeForNode("
                                + "  document.getElementById('div').firstChild, "
                                + "  'composition', 0).endOffset"));

        Assert.assertEquals("6",
                JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                        "internals.markerRangeForNode("
                                + "  document.getElementById('div').firstChild, "
                                + "  'composition', 1).startOffset"));

        Assert.assertEquals("11",
                JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
                        "internals.markerRangeForNode("
                                + "  document.getElementById('div').firstChild, "
                                + "  'composition', 1).endOffset"));
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testAutocorrectAttribute() throws Exception {
        // Autocorrect should be on for a text field that doesn't have an autocorrect attribute.
        mRule.focusElement("input_text");
        Assert.assertEquals(EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT,
                mRule.getConnectionFactory().getOutAttrs().inputType
                        & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT);

        // Autocorrect should be on for a text field that has autocorrect="on" set.
        mRule.focusElement("autocorrect_on");
        Assert.assertEquals(EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT,
                mRule.getConnectionFactory().getOutAttrs().inputType
                        & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT);

        // Autocorrect should be off for a text field that has autocorrect="off" set.
        mRule.focusElement("autocorrect_off");
        Assert.assertEquals(0,
                mRule.getConnectionFactory().getOutAttrs().inputType
                        & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT);
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testLastText() throws Exception {
        // Hide the keyboard first.
        DOMUtils.focusNode(mRule.getWebContents(), "input_radio");
        mRule.assertWaitForKeyboardStatus(false);
        mRule.verifyNoUpdateSelection();

        // Focus on input_text1 which has 'sometext' in it.
        DOMUtils.focusNode(mRule.getWebContents(), "input_text1");

        mRule.assertWaitForKeyboardStatus(true);

        // By the time the keyboard is shown, we should have the correct last text to pass to
        // EditorInfo in onCreateInputConnection(...).
        Assert.assertArrayEquals(new String[] {"sometext"}, mRule.getLastTextHistory());

        // Hide the keyboard again.
        DOMUtils.focusNode(mRule.getWebContents(), "input_radio");
        mRule.assertWaitForKeyboardStatus(false);

        // Focus on input_text2 which has 'othertext' in it.
        DOMUtils.focusNode(mRule.getWebContents(), "input_text2");

        mRule.assertWaitForKeyboardStatus(true);

        // By the time the keyboard is shown, we should have the correct last text to pass to
        // EditorInfo in onCreateInputConnection(...).
        Assert.assertArrayEquals(
                new String[] {"sometext", "othertext"}, mRule.getLastTextHistory());
    }

    private void assertHasEditableContentValues(
            int notEditableSample, int contentEditableSample, int textAreaSample, int inputSample) {
        Assert.assertEquals(notEditableSample,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.Input.EditableContentTypes", /* sample=Not editable */ 0));
        Assert.assertEquals(contentEditableSample,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.Input.EditableContentTypes", /* sample=Content editable */ 1));
        Assert.assertEquals(textAreaSample,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.Input.EditableContentTypes", /* sample=Text area */ 2));
        Assert.assertEquals(inputSample,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.Input.EditableContentTypes", /* sample=Input */ 3));
    }

    @Test
    @SmallTest
    @Feature({"TextInput"})
    public void testLogsHasEditableContent() throws Exception {
        final int notEditableStartingValue = RecordHistogram.getHistogramValueCountForTesting(
                "Android.Input.EditableContentTypes", /* sample=Not editable */ 0);
        final int contentEditableStartingValue = RecordHistogram.getHistogramValueCountForTesting(
                "Android.Input.EditableContentTypes", /* sample=Content editable */ 1);
        final int textAreaStartingValue = RecordHistogram.getHistogramValueCountForTesting(
                "Android.Input.EditableContentTypes", /* sample=Text area */ 2);
        final int inputStartingValue = RecordHistogram.getHistogramValueCountForTesting(
                "Android.Input.EditableContentTypes", /* sample=Input */ 3);

        DOMUtils.focusNode(mRule.getWebContents(), "link");
        mRule.assertWaitForKeyboardStatus(false);
        assertHasEditableContentValues(notEditableStartingValue + 1, contentEditableStartingValue,
                textAreaStartingValue, inputStartingValue);
        DOMUtils.focusNode(mRule.getWebContents(), "contenteditable1");
        mRule.assertWaitForKeyboardStatus(true);
        assertHasEditableContentValues(notEditableStartingValue + 1,
                contentEditableStartingValue + 1, textAreaStartingValue, inputStartingValue);
        DOMUtils.focusNode(mRule.getWebContents(), "textarea");
        mRule.assertWaitForKeyboardStatus(true);
        assertHasEditableContentValues(notEditableStartingValue + 1,
                contentEditableStartingValue + 1, textAreaStartingValue + 1, inputStartingValue);
        DOMUtils.focusNode(mRule.getWebContents(), "input_text1");
        mRule.assertWaitForKeyboardStatus(true);
        assertHasEditableContentValues(notEditableStartingValue + 1,
                contentEditableStartingValue + 1, textAreaStartingValue + 1,
                inputStartingValue + 1);
    }
}
