Create new module CtsInputMethodTestCases

This CL copies CTS tests related to Input Method and Input Method
Framework out from CtsViewTestCases module into its own module. After
a continuous CTS test for this new module is set up and running, the
duplicated test in CtsViewTestCases should be removed (Bug 34648531).

Bug: 7542467
Test: Manually "run cts --module CtsInputMethodTestCases" and verify
      all tests are passed.
Change-Id: I86881c0145f510b2aa9424e2b90823ca517d2989
diff --git a/tests/inputmethod/Android.mk b/tests/inputmethod/Android.mk
new file mode 100644
index 0000000..7de22d4
--- /dev/null
+++ b/tests/inputmethod/Android.mk
@@ -0,0 +1,42 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# don't include this package in any target
+LOCAL_MODULE_TAGS := tests
+# and when built explicitly put it in the data partition
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+LOCAL_MULTILIB := both
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    compatibility-device-util \
+    ctstestrunner
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsInputMethodTestCases
+
+include $(BUILD_CTS_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/inputmethod/AndroidManifest.xml b/tests/inputmethod/AndroidManifest.xml
new file mode 100644
index 0000000..11f008d
--- /dev/null
+++ b/tests/inputmethod/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.view.inputmethod.cts">
+
+    <application
+        android:label="CtsInputMethodTestCases"
+        android:multiArch="true"
+        android:supportsRtl="true">
+
+        <uses-library android:name="android.test.runner" />
+
+        <activity
+            android:name="android.view.inputmethod.cts.InputMethodCtsActivity"
+            android:label="InputMethodCtsActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:label="CTS tests of android.view.inputmethod"
+        android:targetPackage="android.view.inputmethod.cts">
+        <meta-data
+            android:name="listener"
+            android:value="com.android.cts.runner.CtsTestRunListener" />
+    </instrumentation>
+
+</manifest>
diff --git a/tests/inputmethod/AndroidTest.xml b/tests/inputmethod/AndroidTest.xml
new file mode 100644
index 0000000..ed1e6be
--- /dev/null
+++ b/tests/inputmethod/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<configuration description="Config for CTS InputMethod test cases">
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsInputMethodTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.view.inputmethod.cts" />
+        <option name="runtime-hint" value="1m0s" />
+    </test>
+</configuration>
diff --git a/tests/inputmethod/res/layout/inputmethod_edittext.xml b/tests/inputmethod/res/layout/inputmethod_edittext.xml
new file mode 100644
index 0000000..a8f442e
--- /dev/null
+++ b/tests/inputmethod/res/layout/inputmethod_edittext.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:background="@drawable/blue"
+    android:padding="10px">
+
+    <EditText
+        android:id="@+id/entry"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:background="@android:drawable/editbox_background"/>
+
+</RelativeLayout>
diff --git a/tests/inputmethod/res/values/colors.xml b/tests/inputmethod/res/values/colors.xml
new file mode 100644
index 0000000..1d87ea8
--- /dev/null
+++ b/tests/inputmethod/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ -->
+
+<resources>
+    <drawable name="blue">#770000ff</drawable>
+</resources>
diff --git a/tests/inputmethod/res/xml/keyboard.xml b/tests/inputmethod/res/xml/keyboard.xml
new file mode 100644
index 0000000..af8b23b
--- /dev/null
+++ b/tests/inputmethod/res/xml/keyboard.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
+    android:keyWidth="10%p"
+    android:horizontalGap="0px"
+    android:verticalGap="0px"
+    android:keyHeight="10px"
+    >
+
+    <Row>
+        <Key android:codes="-1" android:keyLabel="Sticky!"
+                android:isModifier="true" android:isSticky="true" />
+        <Key android:codes="120" android:keyLabel="x" />
+    </Row>
+</Keyboard>
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/BaseInputConnectionTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/BaseInputConnectionTest.java
new file mode 100644
index 0000000..8089739
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/BaseInputConnectionTest.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.content.ClipDescription;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.cts.R;
+import android.view.inputmethod.cts.util.InputConnectionTestUtils;
+import android.widget.EditText;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class BaseInputConnectionTest {
+    private Instrumentation mInstrumentation;
+    private InputMethodCtsActivity mActivity;
+    private Window mWindow;
+    private EditText mView;
+    private BaseInputConnection mConnection;
+
+    @Rule
+    public ActivityTestRule<InputMethodCtsActivity> mActivityRule =
+            new ActivityTestRule<>(InputMethodCtsActivity.class);
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mActivity = mActivityRule.getActivity();
+        PollingCheck.waitFor(mActivity::hasWindowFocus);
+        mWindow = mActivity.getWindow();
+        mView = (EditText) mWindow.findViewById(R.id.entry);
+        mConnection = new BaseInputConnection(mView, true);
+    }
+
+    @Test
+    public void testDefaultMethods() {
+        // These methods are default to return fixed result.
+
+        assertFalse(mConnection.beginBatchEdit());
+        assertFalse(mConnection.endBatchEdit());
+
+        // only fit for test default implementation of commitCompletion.
+        int completionId = 1;
+        String completionString = "commitCompletion test";
+        assertFalse(mConnection.commitCompletion(new CompletionInfo(completionId,
+                0, completionString)));
+
+        assertNull(mConnection.getExtractedText(new ExtractedTextRequest(), 0));
+
+        // only fit for test default implementation of performEditorAction.
+        int actionCode = 1;
+        int actionId = 2;
+        String action = "android.intent.action.MAIN";
+        assertTrue(mConnection.performEditorAction(actionCode));
+        assertFalse(mConnection.performContextMenuAction(actionId));
+        assertFalse(mConnection.performPrivateCommand(action, new Bundle()));
+    }
+
+    @Test
+    public void testOpComposingSpans() {
+        Spannable text = new SpannableString("Test ComposingSpans");
+        BaseInputConnection.setComposingSpans(text);
+        assertTrue(BaseInputConnection.getComposingSpanStart(text) > -1);
+        assertTrue(BaseInputConnection.getComposingSpanEnd(text) > -1);
+        BaseInputConnection.removeComposingSpans(text);
+        assertTrue(BaseInputConnection.getComposingSpanStart(text) == -1);
+        assertTrue(BaseInputConnection.getComposingSpanEnd(text) == -1);
+    }
+
+    /**
+     * getEditable: Return the target of edit operations. The default implementation
+     *              returns its own fake editable that is just used for composing text.
+     * clearMetaKeyStates: Default implementation uses
+     *              MetaKeyKeyListener#clearMetaKeyState(long, int) to clear the state.
+     *              BugId:1738511
+     * commitText: 1. Default implementation replaces any existing composing text with the given
+     *                text.
+     *             2. In addition, only if dummy mode, a key event is sent for the new text and the
+     *                current editable buffer cleared.
+     * deleteSurroundingText: The default implementation performs the deletion around the current
+     *              selection position of the editable text.
+     * getCursorCapsMode: 1. The default implementation uses TextUtils.getCapsMode to get the
+     *                  cursor caps mode for the current selection position in the editable text.
+     *                  TextUtils.getCapsMode is tested fully in TextUtilsTest#testGetCapsMode.
+     *                    2. In dummy mode in which case 0 is always returned.
+     * getTextBeforeCursor, getTextAfterCursor: The default implementation performs the deletion
+     *                          around the current selection position of the editable text.
+     * setSelection: changes the selection position in the current editable text.
+     */
+    @Test
+    public void testOpTextMethods() throws Throwable {
+        // return is an default Editable instance with empty source
+        final Editable text = mConnection.getEditable();
+        assertNotNull(text);
+        assertEquals(0, text.length());
+
+        // Test commitText, not dummy mode
+        CharSequence str = "TestCommit ";
+        Editable inputText = Editable.Factory.getInstance().newEditable(str);
+        mConnection.commitText(inputText, inputText.length());
+        final Editable text2 = mConnection.getEditable();
+        int strLength = str.length();
+        assertEquals(strLength, text2.length());
+        assertEquals(str.toString(), text2.toString());
+        assertEquals(TextUtils.CAP_MODE_WORDS,
+                mConnection.getCursorCapsMode(TextUtils.CAP_MODE_WORDS));
+        int offLength = 3;
+        CharSequence expected = str.subSequence(strLength - offLength, strLength);
+        assertEquals(expected.toString(), mConnection.getTextBeforeCursor(offLength,
+                BaseInputConnection.GET_TEXT_WITH_STYLES).toString());
+        mConnection.setSelection(0, 0);
+        expected = str.subSequence(0, offLength);
+        assertEquals(expected.toString(), mConnection.getTextAfterCursor(offLength,
+                BaseInputConnection.GET_TEXT_WITH_STYLES).toString());
+
+        mActivityRule.runOnUiThread(() -> {
+            assertTrue(mView.requestFocus());
+            assertTrue(mView.isFocused());
+        });
+
+        // dummy mode
+        BaseInputConnection dummyConnection = new BaseInputConnection(mView, false);
+        dummyConnection.commitText(inputText, inputText.length());
+        PollingCheck.waitFor(() -> text2.toString().equals(mView.getText().toString()));
+        assertEquals(0, dummyConnection.getCursorCapsMode(TextUtils.CAP_MODE_WORDS));
+
+        // Test deleteSurroundingText
+        int end = text2.length();
+        mConnection.setSelection(end, end);
+        // Delete the ending space
+        assertTrue(mConnection.deleteSurroundingText(1, 2));
+        Editable text3 = mConnection.getEditable();
+        assertEquals(strLength - 1, text3.length());
+        String expectedDelString = "TestCommit";
+        assertEquals(expectedDelString, text3.toString());
+    }
+
+    /**
+     * finishComposingText: 1. The default implementation removes the composing state from the
+     *                         current editable text.
+     *                      2. In addition, only if dummy mode, a key event is sent for the new
+     *                         text and the current editable buffer cleared.
+     * setComposingText: The default implementation places the given text into the editable,
+     *                  replacing any existing composing text
+     */
+    @Test
+    public void testFinishComposingText() throws Throwable {
+        CharSequence str = "TestFinish";
+        Editable inputText = Editable.Factory.getInstance().newEditable(str);
+        mConnection.commitText(inputText, inputText.length());
+        final Editable text = mConnection.getEditable();
+        // Test finishComposingText, not dummy mode
+        BaseInputConnection.setComposingSpans(text);
+        assertTrue(BaseInputConnection.getComposingSpanStart(text) > -1);
+        assertTrue(BaseInputConnection.getComposingSpanEnd(text) > -1);
+        mConnection.finishComposingText();
+        assertTrue(BaseInputConnection.getComposingSpanStart(text) == -1);
+        assertTrue(BaseInputConnection.getComposingSpanEnd(text) == -1);
+
+        mActivityRule.runOnUiThread(() -> {
+            assertTrue(mView.requestFocus());
+            assertTrue(mView.isFocused());
+        });
+
+        // dummy mode
+        BaseInputConnection dummyConnection = new BaseInputConnection(mView, false);
+        dummyConnection.setComposingText(str, str.length());
+        dummyConnection.finishComposingText();
+        PollingCheck.waitFor(() -> text.toString().equals(mView.getText().toString()));
+    }
+
+    /**
+     * Provides standard implementation for sending a key event to the window
+     * attached to the input connection's view
+     */
+    @Test
+    public void testSendKeyEvent() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            assertTrue(mView.requestFocus());
+            assertTrue(mView.isFocused());
+        });
+
+        // 12-key support
+        KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+        if (keymap.getKeyboardType() == KeyCharacterMap.NUMERIC) {
+            // 'Q' in case of 12-key(NUMERIC) keyboard
+            mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_7));
+            mConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_7));
+        } else {
+            mInstrumentation.sendStringSync("q");
+            mInstrumentation.waitForIdleSync();
+        }
+        PollingCheck.waitFor(() -> "q".equals(mView.getText().toString()));
+    }
+
+    /**
+     * Updates InputMethodManager with the current fullscreen mode.
+     */
+    @Test
+    public void testReportFullscreenMode() {
+        InputMethodManager imManager = (InputMethodManager) mInstrumentation.getTargetContext()
+                .getSystemService(Context.INPUT_METHOD_SERVICE);
+        mConnection.reportFullscreenMode(false);
+        assertFalse(imManager.isFullscreenMode());
+        mConnection.reportFullscreenMode(true);
+        // Only IMEs are allowed to report full-screen mode.  Calling this method from the
+        // application should have no effect.
+        assertFalse(imManager.isFullscreenMode());
+    }
+
+    /**
+     * An utility method to create an instance of {@link BaseInputConnection} in dummy mode with
+     * an initial text and selection range.
+     * @param view the {@link View} to be associated with the {@link BaseInputConnection}.
+     * @param source the initial text.
+     * @return {@link BaseInputConnection} instantiated in dummy mode with {@code source} and
+     * selection range from {@code selectionStart} to {@code selectionEnd}
+     */
+    private static BaseInputConnection createDummyConnectionWithSelection(
+            final View view, final CharSequence source) {
+        final int selectionStart = Selection.getSelectionStart(source);
+        final int selectionEnd = Selection.getSelectionEnd(source);
+        final Editable editable = Editable.Factory.getInstance().newEditable(source);
+        Selection.setSelection(editable, selectionStart, selectionEnd);
+        return new BaseInputConnection(view, false) {
+            @Override
+            public Editable getEditable() {
+                return editable;
+            }
+        };
+    }
+
+    private void verifyDeleteSurroundingTextMain(final String initialState,
+            final int deleteBefore, final int deleteAfter, final String expectedState) {
+        final CharSequence source = InputConnectionTestUtils.formatString(initialState);
+        final BaseInputConnection ic = createDummyConnectionWithSelection(mView, source);
+        ic.deleteSurroundingText(deleteBefore, deleteAfter);
+
+        final CharSequence expectedString = InputConnectionTestUtils.formatString(expectedState);
+        final int expectedSelectionStart = Selection.getSelectionStart(expectedString);
+        final int expectedSelectionEnd = Selection.getSelectionEnd(expectedString);
+
+        // It is sufficient to check the surrounding text up to source.length() characters, because
+        // InputConnection.deleteSurroundingText() is not supposed to increase the text length.
+        final int retrievalLength = source.length();
+        if (expectedSelectionStart == 0) {
+            assertTrue(TextUtils.isEmpty(ic.getTextBeforeCursor(retrievalLength, 0)));
+        } else {
+            assertEquals(expectedString.subSequence(0, expectedSelectionStart).toString(),
+                    ic.getTextBeforeCursor(retrievalLength, 0).toString());
+        }
+        if (expectedSelectionStart == expectedSelectionEnd) {
+            assertTrue(TextUtils.isEmpty(ic.getSelectedText(0)));  // null is allowed.
+        } else {
+            assertEquals(expectedString.subSequence(expectedSelectionStart,
+                    expectedSelectionEnd).toString(), ic.getSelectedText(0).toString());
+        }
+        if (expectedSelectionEnd == expectedString.length()) {
+            assertTrue(TextUtils.isEmpty(ic.getTextAfterCursor(retrievalLength, 0)));
+        } else {
+            assertEquals(expectedString.subSequence(expectedSelectionEnd,
+                    expectedString.length()).toString(),
+                    ic.getTextAfterCursor(retrievalLength, 0).toString());
+        }
+    }
+
+    /**
+     * Tests {@link BaseInputConnection#deleteSurroundingText(int, int)} comprehensively.
+     */
+    @Test
+    public void testDeleteSurroundingText() throws Throwable {
+        verifyDeleteSurroundingTextMain("012[]3456789", 0, 0, "012[]3456789");
+        verifyDeleteSurroundingTextMain("012[]3456789", -1, -1, "012[]3456789");
+        verifyDeleteSurroundingTextMain("012[]3456789", 1, 2, "01[]56789");
+        verifyDeleteSurroundingTextMain("012[]3456789", 10, 1, "[]456789");
+        verifyDeleteSurroundingTextMain("012[]3456789", 1, 10, "01[]");
+        verifyDeleteSurroundingTextMain("[]0123456789", 3, 3, "[]3456789");
+        verifyDeleteSurroundingTextMain("0123456789[]", 3, 3, "0123456[]");
+        verifyDeleteSurroundingTextMain("012[345]6789", 0, 0, "012[345]6789");
+        verifyDeleteSurroundingTextMain("012[345]6789", -1, -1, "012[345]6789");
+        verifyDeleteSurroundingTextMain("012[345]6789", 1, 2, "01[345]89");
+        verifyDeleteSurroundingTextMain("012[345]6789", 10, 1, "[345]789");
+        verifyDeleteSurroundingTextMain("012[345]6789", 1, 10, "01[345]");
+        verifyDeleteSurroundingTextMain("[012]3456789", 3, 3, "[012]6789");
+        verifyDeleteSurroundingTextMain("0123456[789]", 3, 3, "0123[789]");
+        verifyDeleteSurroundingTextMain("[0123456789]", 0, 0, "[0123456789]");
+        verifyDeleteSurroundingTextMain("[0123456789]", 1, 1, "[0123456789]");
+
+        // Surrogate characters do not have any special meanings.  Validating the character sequence
+        // is beyond the goal of this API.
+        verifyDeleteSurroundingTextMain("0<>[]3456789", 1, 0, "0<[]3456789");
+        verifyDeleteSurroundingTextMain("0<>[]3456789", 2, 0, "0[]3456789");
+        verifyDeleteSurroundingTextMain("0<>[]3456789", 3, 0, "[]3456789");
+        verifyDeleteSurroundingTextMain("012[]<>56789", 0, 1, "012[]>56789");
+        verifyDeleteSurroundingTextMain("012[]<>56789", 0, 2, "012[]56789");
+        verifyDeleteSurroundingTextMain("012[]<>56789", 0, 3, "012[]6789");
+        verifyDeleteSurroundingTextMain("0<<[]3456789", 1, 0, "0<[]3456789");
+        verifyDeleteSurroundingTextMain("0<<[]3456789", 2, 0, "0[]3456789");
+        verifyDeleteSurroundingTextMain("0<<[]3456789", 3, 0, "[]3456789");
+        verifyDeleteSurroundingTextMain("012[]<<56789", 0, 1, "012[]<56789");
+        verifyDeleteSurroundingTextMain("012[]<<56789", 0, 2, "012[]56789");
+        verifyDeleteSurroundingTextMain("012[]<<56789", 0, 3, "012[]6789");
+        verifyDeleteSurroundingTextMain("0>>[]3456789", 1, 0, "0>[]3456789");
+        verifyDeleteSurroundingTextMain("0>>[]3456789", 2, 0, "0[]3456789");
+        verifyDeleteSurroundingTextMain("0>>[]3456789", 3, 0, "[]3456789");
+        verifyDeleteSurroundingTextMain("012[]>>56789", 0, 1, "012[]>56789");
+        verifyDeleteSurroundingTextMain("012[]>>56789", 0, 2, "012[]56789");
+        verifyDeleteSurroundingTextMain("012[]>>56789", 0, 3, "012[]6789");
+    }
+
+    private void verifyDeleteSurroundingTextInCodePointsMain(final String initialState,
+            final int deleteBeforeInCodePoints, final int deleteAfterInCodePoints,
+            final String expectedState) {
+        final CharSequence source = InputConnectionTestUtils.formatString(initialState);
+        final BaseInputConnection ic = createDummyConnectionWithSelection(mView, source);
+        ic.deleteSurroundingTextInCodePoints(deleteBeforeInCodePoints, deleteAfterInCodePoints);
+
+        final CharSequence expectedString = InputConnectionTestUtils.formatString(expectedState);
+        final int expectedSelectionStart = Selection.getSelectionStart(expectedString);
+        final int expectedSelectionEnd = Selection.getSelectionEnd(expectedString);
+
+        // It is sufficient to check the surrounding text up to source.length() characters, because
+        // InputConnection.deleteSurroundingTextInCodePoints() is not supposed to increase the text
+        // length.
+        final int retrievalLength = source.length();
+        if (expectedSelectionStart == 0) {
+            assertTrue(TextUtils.isEmpty(ic.getTextBeforeCursor(retrievalLength, 0)));
+        } else {
+            assertEquals(expectedString.subSequence(0, expectedSelectionStart).toString(),
+                    ic.getTextBeforeCursor(retrievalLength, 0).toString());
+        }
+        if (expectedSelectionStart == expectedSelectionEnd) {
+            assertTrue(TextUtils.isEmpty(ic.getSelectedText(0)));  // null is allowed.
+        } else {
+            assertEquals(expectedString.subSequence(expectedSelectionStart,
+                    expectedSelectionEnd).toString(), ic.getSelectedText(0).toString());
+        }
+        if (expectedSelectionEnd == expectedString.length()) {
+            assertTrue(TextUtils.isEmpty(ic.getTextAfterCursor(retrievalLength, 0)));
+        } else {
+            assertEquals(expectedString.subSequence(expectedSelectionEnd,
+                    expectedString.length()).toString(),
+                    ic.getTextAfterCursor(retrievalLength, 0).toString());
+        }
+    }
+
+    /**
+     * Tests {@link BaseInputConnection#deleteSurroundingTextInCodePoints(int, int)}
+     * comprehensively.
+     */
+    @Test
+    public void testDeleteSurroundingTextInCodePoints() throws Throwable {
+        verifyDeleteSurroundingTextInCodePointsMain("012[]3456789", 0, 0, "012[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]3456789", -1, -1, "012[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]3456789", 1, 2, "01[]56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]3456789", 10, 1, "[]456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]3456789", 1, 10, "01[]");
+        verifyDeleteSurroundingTextInCodePointsMain("[]0123456789", 3, 3, "[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0123456789[]", 3, 3, "0123456[]");
+        verifyDeleteSurroundingTextInCodePointsMain("012[345]6789", 0, 0, "012[345]6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[345]6789", -1, -1, "012[345]6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[345]6789", 1, 2, "01[345]89");
+        verifyDeleteSurroundingTextInCodePointsMain("012[345]6789", 10, 1, "[345]789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[345]6789", 1, 10, "01[345]");
+        verifyDeleteSurroundingTextInCodePointsMain("[012]3456789", 3, 3, "[012]6789");
+        verifyDeleteSurroundingTextInCodePointsMain("0123456[789]", 3, 3, "0123[789]");
+        verifyDeleteSurroundingTextInCodePointsMain("[0123456789]", 0, 0, "[0123456789]");
+        verifyDeleteSurroundingTextInCodePointsMain("[0123456789]", 1, 1, "[0123456789]");
+
+        verifyDeleteSurroundingTextInCodePointsMain("0<>[]3456789", 1, 0, "0[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0<>[]3456789", 2, 0, "[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0<>[]3456789", 3, 0, "[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<>56789", 0, 1, "012[]56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<>56789", 0, 2, "012[]6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<>56789", 0, 3, "012[]789");
+
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 0, "[]<><><><><>");
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 1, "[]<><><><>");
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 2, "[]<><><>");
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 3, "[]<><>");
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 4, "[]<>");
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 5, "[]");
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 6, "[]");
+        verifyDeleteSurroundingTextInCodePointsMain("[]<><><><><>", 0, 1000, "[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 0, 0, "<><><><><>[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 1, 0, "<><><><>[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 2, 0, "<><><>[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 3, 0, "<><>[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 4, 0, "<>[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 5, 0, "[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 6, 0, "[]");
+        verifyDeleteSurroundingTextInCodePointsMain("<><><><><>[]", 1000, 0, "[]");
+
+        verifyDeleteSurroundingTextInCodePointsMain("0<<[]3456789", 1, 0, "0<<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0<<[]3456789", 2, 0, "0<<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0<<[]3456789", 3, 0, "0<<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<<56789", 0, 1, "012[]<<56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<<56789", 0, 2, "012[]<<56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<<56789", 0, 3, "012[]<<56789");
+        verifyDeleteSurroundingTextInCodePointsMain("0>>[]3456789", 1, 0, "0>>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0>>[]3456789", 2, 0, "0>>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0>>[]3456789", 3, 0, "0>>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]>>56789", 0, 1, "012[]>>56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]>>56789", 0, 2, "012[]>>56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]>>56789", 0, 3, "012[]>>56789");
+        verifyDeleteSurroundingTextInCodePointsMain("01<[]>456789", 1, 0, "01<[]>456789");
+        verifyDeleteSurroundingTextInCodePointsMain("01<[]>456789", 0, 1, "01<[]>456789");
+        verifyDeleteSurroundingTextInCodePointsMain("<12[]3456789", 1, 0, "<1[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("<12[]3456789", 2, 0, "<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("<12[]3456789", 3, 0, "<12[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("<<>[]3456789", 1, 0, "<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("<<>[]3456789", 2, 0, "<<>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("<<>[]3456789", 3, 0, "<<>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]34>6789", 0, 1, "012[]4>6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]34>6789", 0, 2, "012[]>6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]34>6789", 0, 3, "012[]34>6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<>>6789", 0, 1, "012[]>6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<>>6789", 0, 2, "012[]<>>6789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<>>6789", 0, 3, "012[]<>>6789");
+
+        // Atomicity test.
+        verifyDeleteSurroundingTextInCodePointsMain("0<<[]3456789", 1, 1, "0<<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0<<[]3456789", 2, 1, "0<<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0<<[]3456789", 3, 1, "0<<[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<<56789", 1, 1, "012[]<<56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<<56789", 1, 2, "012[]<<56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]<<56789", 1, 3, "012[]<<56789");
+        verifyDeleteSurroundingTextInCodePointsMain("0>>[]3456789", 1, 1, "0>>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0>>[]3456789", 2, 1, "0>>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("0>>[]3456789", 3, 1, "0>>[]3456789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]>>56789", 1, 1, "012[]>>56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]>>56789", 1, 2, "012[]>>56789");
+        verifyDeleteSurroundingTextInCodePointsMain("012[]>>56789", 1, 3, "012[]>>56789");
+        verifyDeleteSurroundingTextInCodePointsMain("01<[]>456789", 1, 1, "01<[]>456789");
+
+        // Do not verify the character sequences in the selected region.
+        verifyDeleteSurroundingTextInCodePointsMain("01[><]456789", 1, 0, "0[><]456789");
+        verifyDeleteSurroundingTextInCodePointsMain("01[><]456789", 0, 1, "01[><]56789");
+        verifyDeleteSurroundingTextInCodePointsMain("01[><]456789", 1, 1, "0[><]56789");
+    }
+
+    @Test
+    public void testCloseConnection() {
+        final CharSequence source = "0123456789";
+        mConnection.commitText(source, source.length());
+        final Editable text = mConnection.getEditable();
+        BaseInputConnection.setComposingSpans(text, 2, 5);
+        assertEquals(2, BaseInputConnection.getComposingSpanStart(text));
+        assertEquals(5, BaseInputConnection.getComposingSpanEnd(text));
+
+        // BaseInputConnection#closeConnection() must clear the on-going composition.
+        mConnection.closeConnection();
+        assertEquals(-1, BaseInputConnection.getComposingSpanStart(text));
+        assertEquals(-1, BaseInputConnection.getComposingSpanEnd(text));
+    }
+
+    @Test
+    public void testGetHandler() {
+        // BaseInputConnection must not implement getHandler().
+        assertNull(mConnection.getHandler());
+    }
+
+    @Test
+    public void testCommitContent() {
+        final InputContentInfo inputContentInfo = new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("https://example.com"));
+        // The default implementation should do nothing and just return false.
+        assertFalse(mConnection.commitContent(inputContentInfo, 0 /* flags */, null /* opts */));
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/CompletionInfoTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/CompletionInfoTest.java
new file mode 100644
index 0000000..9a8d206
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/CompletionInfoTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.inputmethod.CompletionInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CompletionInfoTest {
+    private static final int ID = 1;
+    private static final int POSITION = 1;
+    private static final String TEXT = "CompletionInfoText";
+    private static final String LABEL = "CompletionInfoLabel";
+
+    @Test
+    public void testCompletionInfo() {
+        new CompletionInfo(ID, POSITION, TEXT);
+        CompletionInfo info = new CompletionInfo(ID, POSITION, TEXT, LABEL);
+        assertCompletionInfo(info);
+
+        assertEquals(0, info.describeContents());
+        assertNotNull(info.toString());
+
+        Parcel p = Parcel.obtain();
+        info.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        CompletionInfo targetInfo = CompletionInfo.CREATOR.createFromParcel(p);
+        p.recycle();
+        assertCompletionInfo(targetInfo);
+    }
+
+    private void assertCompletionInfo(CompletionInfo info) {
+        assertEquals(ID, info.getId());
+        assertEquals(POSITION, info.getPosition());
+        assertEquals(TEXT, info.getText().toString());
+        assertEquals(LABEL, info.getLabel().toString());
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/EditorInfoTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/EditorInfoTest.java
new file mode 100644
index 0000000..1557511
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/EditorInfoTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.MoreAsserts;
+import android.text.TextUtils;
+import android.util.Printer;
+import android.view.inputmethod.EditorInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EditorInfoTest {
+    @Test
+    public void testEditorInfo() {
+        EditorInfo info = new EditorInfo();
+
+        info.actionId = 1;
+        info.actionLabel = "actionLabel";
+        info.fieldId = 2;
+        info.fieldName = "fieldName";
+        info.hintText = "hintText";
+        info.imeOptions = EditorInfo.IME_FLAG_NO_ENTER_ACTION;
+        info.initialCapsMode = TextUtils.CAP_MODE_CHARACTERS;
+        info.initialSelEnd = 10;
+        info.initialSelStart = 0;
+        info.inputType = EditorInfo.TYPE_MASK_CLASS;
+        info.label = "label";
+        info.packageName = "android.view.cts";
+        info.privateImeOptions = "privateIme";
+        Bundle b = new Bundle();
+        String key = "bundleKey";
+        String value = "bundleValue";
+        b.putString(key, value);
+        info.extras = b;
+        info.hintLocales = LocaleList.forLanguageTags("en-PH,en-US");
+        info.contentMimeTypes = new String[]{"image/gif", "image/png"};
+
+        assertEquals(0, info.describeContents());
+
+        Parcel p = Parcel.obtain();
+        info.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        EditorInfo targetInfo = EditorInfo.CREATOR.createFromParcel(p);
+        p.recycle();
+        assertEquals(info.actionId, targetInfo.actionId);
+        assertEquals(info.fieldId, targetInfo.fieldId);
+        assertEquals(info.fieldName, targetInfo.fieldName);
+        assertEquals(info.imeOptions, targetInfo.imeOptions);
+        assertEquals(info.initialCapsMode, targetInfo.initialCapsMode);
+        assertEquals(info.initialSelEnd, targetInfo.initialSelEnd);
+        assertEquals(info.initialSelStart, targetInfo.initialSelStart);
+        assertEquals(info.inputType, targetInfo.inputType);
+        assertEquals(info.packageName, targetInfo.packageName);
+        assertEquals(info.privateImeOptions, targetInfo.privateImeOptions);
+        assertEquals(info.hintText.toString(), targetInfo.hintText.toString());
+        assertEquals(info.actionLabel.toString(), targetInfo.actionLabel.toString());
+        assertEquals(info.label.toString(), targetInfo.label.toString());
+        assertEquals(info.extras.getString(key), targetInfo.extras.getString(key));
+        assertEquals(info.hintLocales, targetInfo.hintLocales);
+        MoreAsserts.assertEquals(info.contentMimeTypes, targetInfo.contentMimeTypes);
+
+        Printer printer = mock(Printer.class);
+        String prefix = "TestEditorInfo";
+        info.dump(printer, prefix);
+        verify(printer, atLeastOnce()).println(anyString());
+    }
+
+    @Test
+    public void testNullHintLocals() {
+        EditorInfo info = new EditorInfo();
+        info.hintLocales = null;
+        Parcel p = Parcel.obtain();
+        info.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        EditorInfo targetInfo = EditorInfo.CREATOR.createFromParcel(p);
+        p.recycle();
+        assertNull(targetInfo.hintLocales);
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/ExtractedTextRequestTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/ExtractedTextRequestTest.java
new file mode 100644
index 0000000..3e12579
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/ExtractedTextRequestTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.inputmethod.ExtractedTextRequest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ExtractedTextRequestTest {
+    @Test
+    public void testExtractedTextRequest() {
+        ExtractedTextRequest request = new ExtractedTextRequest();
+        request.flags = 1;
+        request.hintMaxChars = 100;
+        request.hintMaxLines = 10;
+        request.token = 2;
+
+        assertEquals(0, request.describeContents());
+
+        Parcel p = Parcel.obtain();
+        request.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        ExtractedTextRequest target = ExtractedTextRequest.CREATOR.createFromParcel(p);
+        p.recycle();
+        assertEquals(request.flags, target.flags);
+        assertEquals(request.hintMaxChars, request.hintMaxChars);
+        assertEquals(request.hintMaxLines, target.hintMaxLines);
+        assertEquals(request.token, target.token);
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/ExtractedTextTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/ExtractedTextTest.java
new file mode 100644
index 0000000..41e3efa
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/ExtractedTextTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.inputmethod.ExtractedText;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ExtractedTextTest {
+    @Test
+    public void testWriteToParcel() {
+        ExtractedText extractedText = new ExtractedText();
+        extractedText.flags = 1;
+        extractedText.selectionEnd = 11;
+        extractedText.selectionStart = 2;
+        extractedText.startOffset = 1;
+        CharSequence text = "test";
+        extractedText.text = text;
+        Parcel p = Parcel.obtain();
+        extractedText.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        ExtractedText target = ExtractedText.CREATOR.createFromParcel(p);
+        assertEquals(extractedText.flags, target.flags);
+        assertEquals(extractedText.selectionEnd, target.selectionEnd);
+        assertEquals(extractedText.selectionStart, target.selectionStart);
+        assertEquals(extractedText.startOffset, target.startOffset);
+        assertEquals(extractedText.partialStartOffset, target.partialStartOffset);
+        assertEquals(extractedText.partialEndOffset, target.partialEndOffset);
+        assertEquals(extractedText.text.toString(), target.text.toString());
+
+        assertEquals(0, extractedText.describeContents());
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputBindingTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputBindingTest.java
new file mode 100644
index 0000000..faaff3d
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputBindingTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+import android.os.Binder;
+import android.os.Parcel;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.InputBinding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputBindingTest {
+    @Test
+    public void testInputBinding() {
+        View view = new View(InstrumentationRegistry.getTargetContext());
+        BaseInputConnection bic = new BaseInputConnection(view, false);
+        Binder binder = new Binder();
+        int uid = 1;
+        int pid = 2;
+        InputBinding inputBinding = new InputBinding(bic, binder, uid, pid);
+        new InputBinding(bic, inputBinding);
+        assertSame(bic, inputBinding.getConnection());
+        assertSame(binder, inputBinding.getConnectionToken());
+        assertEquals(uid, inputBinding.getUid());
+        assertEquals(pid, inputBinding.getPid());
+
+        assertNotNull(inputBinding.toString());
+        assertEquals(0, inputBinding.describeContents());
+
+        Parcel p = Parcel.obtain();
+        inputBinding.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        InputBinding target = InputBinding.CREATOR.createFromParcel(p);
+        assertEquals(uid, target.getUid());
+        assertEquals(pid, target.getPid());
+        assertSame(binder, target.getConnectionToken());
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionWrapperTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionWrapperTest.java
new file mode 100644
index 0000000..71abacc
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionWrapperTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static com.android.compatibility.common.util.WidgetTestUtils.sameCharSequence;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+import android.view.inputmethod.InputContentInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputConnectionWrapperTest {
+    @Test
+    public void testInputConnectionWrapper() {
+        InputConnection inputConnection = mock(InputConnection.class);
+        doReturn(true).when(inputConnection).commitContent(any(InputContentInfo.class),
+                anyInt(), any(Bundle.class));
+        InputConnectionWrapper wrapper = new InputConnectionWrapper(null, true);
+        try {
+            wrapper.beginBatchEdit();
+            fail("Failed to throw NullPointerException!");
+        } catch (NullPointerException e) {
+            // expected
+        }
+        wrapper.setTarget(inputConnection);
+
+        wrapper.beginBatchEdit();
+        verify(inputConnection, times(1)).beginBatchEdit();
+
+        wrapper.clearMetaKeyStates(KeyEvent.META_ALT_ON);
+        verify(inputConnection, times(1)).clearMetaKeyStates(KeyEvent.META_ALT_ON);
+
+        wrapper.commitCompletion(new CompletionInfo(1, 1, "testText"));
+        ArgumentCaptor<CompletionInfo> completionInfoCaptor =
+                ArgumentCaptor.forClass(CompletionInfo.class);
+        verify(inputConnection, times(1)).commitCompletion(completionInfoCaptor.capture());
+        assertEquals(1, completionInfoCaptor.getValue().getId());
+        assertEquals(1, completionInfoCaptor.getValue().getPosition());
+        assertEquals("testText", completionInfoCaptor.getValue().getText());
+
+        wrapper.commitCorrection(new CorrectionInfo(0, "oldText", "newText"));
+        ArgumentCaptor<CorrectionInfo> correctionInfoCaptor =
+                ArgumentCaptor.forClass(CorrectionInfo.class);
+        verify(inputConnection, times(1)).commitCorrection(correctionInfoCaptor.capture());
+        assertEquals(0, correctionInfoCaptor.getValue().getOffset());
+        assertEquals("oldText", correctionInfoCaptor.getValue().getOldText());
+        assertEquals("newText", correctionInfoCaptor.getValue().getNewText());
+
+        wrapper.commitText("Text", 1);
+        verify(inputConnection, times(1)).commitText(sameCharSequence("Text"), eq(1));
+
+        wrapper.deleteSurroundingText(10, 100);
+        verify(inputConnection, times(1)).deleteSurroundingText(10, 100);
+
+        wrapper.deleteSurroundingTextInCodePoints(10, 100);
+        verify(inputConnection, times(1)).deleteSurroundingTextInCodePoints(10, 100);
+
+        wrapper.endBatchEdit();
+        verify(inputConnection, times(1)).endBatchEdit();
+
+        wrapper.finishComposingText();
+        verify(inputConnection, times(1)).finishComposingText();
+
+        wrapper.getCursorCapsMode(TextUtils.CAP_MODE_CHARACTERS);
+        verify(inputConnection, times(1)).getCursorCapsMode(TextUtils.CAP_MODE_CHARACTERS);
+
+        wrapper.getExtractedText(new ExtractedTextRequest(), 0);
+        verify(inputConnection, times(1)).getExtractedText(any(ExtractedTextRequest.class), eq(0));
+
+        wrapper.getTextAfterCursor(5, 0);
+        verify(inputConnection, times(1)).getTextAfterCursor(5, 0);
+
+        wrapper.getTextBeforeCursor(3, 0);
+        verify(inputConnection, times(1)).getTextBeforeCursor(3, 0);
+
+        wrapper.performContextMenuAction(1);
+        verify(inputConnection, times(1)).performContextMenuAction(1);
+
+        wrapper.performEditorAction(EditorInfo.IME_ACTION_GO);
+        verify(inputConnection, times(1)).performEditorAction(EditorInfo.IME_ACTION_GO);
+
+        wrapper.performPrivateCommand("com.android.action.MAIN", new Bundle());
+        verify(inputConnection, times(1)).performPrivateCommand(eq("com.android.action.MAIN"),
+                any(Bundle.class));
+
+        wrapper.reportFullscreenMode(true);
+        verify(inputConnection, times(1)).reportFullscreenMode(true);
+
+        wrapper.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0));
+        ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+        verify(inputConnection, times(1)).sendKeyEvent(keyEventCaptor.capture());
+        assertEquals(KeyEvent.ACTION_DOWN, keyEventCaptor.getValue().getAction());
+        assertEquals(KeyEvent.KEYCODE_0, keyEventCaptor.getValue().getKeyCode());
+
+        wrapper.setComposingText("Text", 1);
+        verify(inputConnection, times(1)).setComposingText("Text", 1);
+
+        wrapper.setSelection(0, 10);
+        verify(inputConnection, times(1)).setSelection(0, 10);
+
+        wrapper.getSelectedText(0);
+        verify(inputConnection, times(1)).getSelectedText(0);
+
+        wrapper.setComposingRegion(0, 3);
+        verify(inputConnection, times(1)).setComposingRegion(0, 3);
+
+        wrapper.requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE);
+        verify(inputConnection, times(1))
+                .requestCursorUpdates(InputConnection.CURSOR_UPDATE_IMMEDIATE);
+
+        wrapper.closeConnection();
+        verify(inputConnection, times(1)).closeConnection();
+
+        verify(inputConnection, never()).getHandler();
+        assertNull(wrapper.getHandler());
+        verify(inputConnection, times(1)).getHandler();
+
+        verify(inputConnection, never()).commitContent(any(InputContentInfo.class), anyInt(),
+                any(Bundle.class));
+
+        final InputContentInfo inputContentInfo = new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("https://example.com"));
+        wrapper.commitContent(inputContentInfo, 0 /* flags */, null /* opt */);
+        verify(inputConnection, times(1)).commitContent(inputContentInfo, 0, null);
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputContentInfoTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputContentInfoTest.java
new file mode 100644
index 0000000..777b6d8
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputContentInfoTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.inputmethod.InputContentInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.InvalidParameterException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputContentInfoTest {
+    @Test
+    public void testInputContentInfo() {
+        InputContentInfo info = new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("https://example.com"));
+
+        assertEquals(Uri.parse("content://com.example/path"), info.getContentUri());
+        assertEquals(1, info.getDescription().getMimeTypeCount());
+        assertEquals("image/png", info.getDescription().getMimeType(0));
+        assertEquals("sample content", info.getDescription().getLabel());
+        assertEquals(Uri.parse("https://example.com"), info.getLinkUri());
+        assertEquals(0, info.describeContents());
+
+        Parcel p = Parcel.obtain();
+        info.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        InputContentInfo targetInfo = InputContentInfo.CREATOR.createFromParcel(p);
+        p.recycle();
+
+        assertEquals(info.getContentUri(), targetInfo.getContentUri());
+        assertEquals(info.getDescription().getMimeTypeCount(),
+                targetInfo.getDescription().getMimeTypeCount());
+        assertEquals(info.getDescription().getMimeType(0),
+                targetInfo.getDescription().getMimeType(0));
+        assertEquals(info.getDescription().getLabel(), targetInfo.getDescription().getLabel());
+        assertEquals(info.getLinkUri(), targetInfo.getLinkUri());
+        assertEquals(info.describeContents(), targetInfo.describeContents());
+    }
+
+    @Test
+    public void testOptionalConstructorParam() {
+        InputContentInfo info = new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}));
+
+        assertEquals(Uri.parse("content://com.example/path"), info.getContentUri());
+        assertEquals(1, info.getDescription().getMimeTypeCount());
+        assertEquals("image/png", info.getDescription().getMimeType(0));
+        assertEquals("sample content", info.getDescription().getLabel());
+        assertNull(info.getLinkUri());
+        assertEquals(0, info.describeContents());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testContentUriNullContentUri() {
+        new InputContentInfo(
+                null, new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("https://example.com"));
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testContentUriInvalidContentUri() {
+        new InputContentInfo(
+                Uri.parse("https://example.com"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("https://example.com"));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testMimeTypeNulLDescription() {
+        new InputContentInfo(
+                Uri.parse("content://com.example/path"), null,
+                Uri.parse("https://example.com"));
+    }
+
+    @Test
+    public void testLinkUri() {
+        // Test that we accept null link Uri
+        new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                null);
+
+        // Test that we accept http link Uri
+        new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("http://example.com/path"));
+
+        // Test that we accept https link Uri
+        new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("https://example.com/path"));
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testLinkUriFtpLinkUri() {
+        // InputContentInfo must accept http and https link Uri only
+        new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("ftp://example.com/path"));
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testLinkUriContentLinkUri() {
+        // InputContentInfo must accept http and https link Uri only
+        new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("content://com.example/path"));
+    }
+
+    @Test
+    public void testRequestAndReleasePermission() {
+        InputContentInfo info = new InputContentInfo(
+                Uri.parse("content://com.example/path"),
+                new ClipDescription("sample content", new String[]{"image/png"}),
+                Uri.parse("https://example.com"));
+
+        // Here we only assert that {request, release}Permission() do not crash, because ensuring
+        // the entire functionality of these methods requires end-to-end IME test environment, which
+        // we do not have yet in CTS.
+        // Note it is actually intentional that calling these methods here has no effect.  Those
+        // methods would have effect only after the object is passed from the IME process to the
+        // application process.
+        // TODO: Create an end-to-end CTS test for this functionality.
+        info.requestPermission();
+        info.releasePermission();
+        info.requestPermission();
+        info.releasePermission();
+    }
+
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodCtsActivity.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodCtsActivity.java
new file mode 100644
index 0000000..9501d44
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodCtsActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.inputmethod.cts.R;
+
+public class InputMethodCtsActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.inputmethod_edittext);
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodInfoTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodInfoTest.java
new file mode 100644
index 0000000..b34673b
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodInfoTest.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.TextUtils;
+import android.util.Printer;
+import android.view.inputmethod.InputMethod;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputMethodInfoTest {
+    private Context mContext;
+
+    private InputMethodInfo mInputMethodInfo;
+    private String mPackageName;
+    private String mClassName;
+    private CharSequence mLabel;
+    private String mSettingsActivity;
+
+    private int mSubtypeNameResId;
+    private int mSubtypeIconResId;
+    private String mSubtypeLocale;
+    private String mSubtypeMode;
+    private String mSubtypeExtraValue_key;
+    private String mSubtypeExtraValue_value;
+    private String mSubtypeExtraValue;
+    private boolean mSubtypeIsAuxiliary;
+    private boolean mSubtypeOverridesImplicitlyEnabledSubtype;
+    private int mSubtypeId;
+    private InputMethodSubtype mInputMethodSubtype;
+
+    @Before
+    public void setup() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mPackageName = mContext.getPackageName();
+        mClassName = InputMethodSettingsActivityStub.class.getName();
+        mLabel = "test";
+        mSettingsActivity = "android.view.inputmethod.cts.InputMethodSettingsActivityStub";
+        mInputMethodInfo = new InputMethodInfo(mPackageName, mClassName, mLabel, mSettingsActivity);
+
+        mSubtypeNameResId = 0;
+        mSubtypeIconResId = 0;
+        mSubtypeLocale = "en_US";
+        mSubtypeMode = "keyboard";
+        mSubtypeExtraValue_key = "key1";
+        mSubtypeExtraValue_value = "value1";
+        mSubtypeExtraValue = "tag," + mSubtypeExtraValue_key + "=" + mSubtypeExtraValue_value;
+        mSubtypeIsAuxiliary = false;
+        mSubtypeOverridesImplicitlyEnabledSubtype = false;
+        mSubtypeId = 99;
+        mInputMethodSubtype = new InputMethodSubtype(mSubtypeNameResId, mSubtypeIconResId,
+                mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue, mSubtypeIsAuxiliary,
+                mSubtypeOverridesImplicitlyEnabledSubtype, mSubtypeId);
+    }
+
+    @Test
+    public void testInputMethodInfoProperties() throws XmlPullParserException, IOException {
+        assertEquals(0, mInputMethodInfo.describeContents());
+        assertNotNull(mInputMethodInfo.toString());
+
+        assertInfo(mInputMethodInfo);
+        assertEquals(0, mInputMethodInfo.getIsDefaultResourceId());
+
+        Intent intent = new Intent(InputMethod.SERVICE_INTERFACE);
+        intent.setClass(mContext, InputMethodSettingsActivityStub.class);
+        PackageManager pm = mContext.getPackageManager();
+        List<ResolveInfo> ris = pm.queryIntentServices(intent, PackageManager.GET_META_DATA);
+        for (int i = 0; i < ris.size(); i++) {
+            ResolveInfo resolveInfo = ris.get(i);
+            mInputMethodInfo = new InputMethodInfo(mContext, resolveInfo);
+            assertService(resolveInfo.serviceInfo, mInputMethodInfo.getServiceInfo());
+            assertInfo(mInputMethodInfo);
+        }
+    }
+
+    @Test
+    public void testInputMethodSubtypeProperties() {
+        // TODO: Test InputMethodSubtype.getDisplayName()
+        assertEquals(mSubtypeNameResId, mInputMethodSubtype.getNameResId());
+        assertEquals(mSubtypeIconResId, mInputMethodSubtype.getIconResId());
+        assertEquals(mSubtypeLocale, mInputMethodSubtype.getLocale());
+        assertEquals(mSubtypeMode, mInputMethodSubtype.getMode());
+        assertEquals(mSubtypeExtraValue, mInputMethodSubtype.getExtraValue());
+        assertTrue(mInputMethodSubtype.containsExtraValueKey(mSubtypeExtraValue_key));
+        assertEquals(mSubtypeExtraValue_value,
+                mInputMethodSubtype.getExtraValueOf(mSubtypeExtraValue_key));
+        assertEquals(mSubtypeIsAuxiliary, mInputMethodSubtype.isAuxiliary());
+        assertEquals(mSubtypeOverridesImplicitlyEnabledSubtype,
+                mInputMethodSubtype.overridesImplicitlyEnabledSubtype());
+        assertEquals(mSubtypeId, mInputMethodSubtype.hashCode());
+    }
+
+    private void assertService(ServiceInfo expected, ServiceInfo actual) {
+        assertEquals(expected.getIconResource(), actual.getIconResource());
+        assertEquals(expected.labelRes, actual.labelRes);
+        assertEquals(expected.nonLocalizedLabel, actual.nonLocalizedLabel);
+        assertEquals(expected.icon, actual.icon);
+        assertEquals(expected.permission, actual.permission);
+    }
+
+    private void assertInfo(InputMethodInfo info) {
+        assertEquals(mPackageName, info.getPackageName());
+        assertEquals(mSettingsActivity, info.getSettingsActivity());
+        ComponentName component = info.getComponent();
+        assertEquals(mClassName, component.getClassName());
+        String expectedId = component.flattenToShortString();
+        assertEquals(expectedId, info.getId());
+        assertEquals(mClassName, info.getServiceName());
+    }
+
+    @Test
+    public void testDump() {
+        Printer printer = mock(Printer.class);
+        String prefix = "test";
+        mInputMethodInfo.dump(printer, prefix);
+        verify(printer, atLeastOnce()).println(anyString());
+    }
+
+    @Test
+    public void testLoadIcon() {
+        PackageManager pm = mContext.getPackageManager();
+        assertNotNull(mInputMethodInfo.loadIcon(pm));
+    }
+
+    @Test
+    public void testEquals() {
+        InputMethodInfo inputMethodInfo = new InputMethodInfo(mPackageName, mClassName, mLabel,
+                mSettingsActivity);
+        assertTrue(inputMethodInfo.equals(mInputMethodInfo));
+    }
+
+    @Test
+    public void testLoadLabel() {
+        CharSequence expected = "test";
+        PackageManager pm = mContext.getPackageManager();
+        assertEquals(expected.toString(), mInputMethodInfo.loadLabel(pm).toString());
+    }
+
+    @Test
+    public void testInputMethodInfoWriteToParcel() {
+        final Parcel p = Parcel.obtain();
+        mInputMethodInfo.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        final InputMethodInfo imi = InputMethodInfo.CREATOR.createFromParcel(p);
+        p.recycle();
+
+        assertEquals(mInputMethodInfo.getPackageName(), imi.getPackageName());
+        assertEquals(mInputMethodInfo.getServiceName(), imi.getServiceName());
+        assertEquals(mInputMethodInfo.getSettingsActivity(), imi.getSettingsActivity());
+        assertEquals(mInputMethodInfo.getId(), imi.getId());
+        assertEquals(mInputMethodInfo.getIsDefaultResourceId(), imi.getIsDefaultResourceId());
+        assertService(mInputMethodInfo.getServiceInfo(), imi.getServiceInfo());
+    }
+
+    @Test
+    public void testInputMethodSubtypeWriteToParcel() {
+        final Parcel p = Parcel.obtain();
+        mInputMethodSubtype.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        final InputMethodSubtype subtype = InputMethodSubtype.CREATOR.createFromParcel(p);
+        p.recycle();
+
+        assertEquals(mInputMethodSubtype.containsExtraValueKey(mSubtypeExtraValue_key),
+                subtype.containsExtraValueKey(mSubtypeExtraValue_key));
+        assertEquals(mInputMethodSubtype.getExtraValue(), subtype.getExtraValue());
+        assertEquals(mInputMethodSubtype.getExtraValueOf(mSubtypeExtraValue_key),
+                subtype.getExtraValueOf(mSubtypeExtraValue_key));
+        assertEquals(mInputMethodSubtype.getIconResId(), subtype.getIconResId());
+        assertEquals(mInputMethodSubtype.getLocale(), subtype.getLocale());
+        assertEquals(mInputMethodSubtype.getMode(), subtype.getMode());
+        assertEquals(mInputMethodSubtype.getNameResId(), subtype.getNameResId());
+        assertEquals(mInputMethodSubtype.hashCode(), subtype.hashCode());
+        assertEquals(mInputMethodSubtype.isAuxiliary(), subtype.isAuxiliary());
+        assertEquals(mInputMethodSubtype.overridesImplicitlyEnabledSubtype(),
+                subtype.overridesImplicitlyEnabledSubtype());
+    }
+
+    @Test
+    public void testInputMethodSubtypesOfSystemImes() {
+        if (!mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_INPUT_METHODS)) {
+            return;
+        }
+
+        final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+        final List<InputMethodInfo> imis = imm.getInputMethodList();
+        final ArrayList<String> localeList = new ArrayList<>(Arrays.asList(
+                Resources.getSystem().getAssets().getLocales()));
+        boolean foundEnabledSystemImeSubtypeWithValidLanguage = false;
+        for (InputMethodInfo imi : imis) {
+            if ((imi.getServiceInfo().applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+                continue;
+            }
+            final int subtypeCount = imi.getSubtypeCount();
+            // System IME must have one subtype at least.
+            assertTrue(subtypeCount > 0);
+            if (foundEnabledSystemImeSubtypeWithValidLanguage) {
+                continue;
+            }
+            final List<InputMethodSubtype> enabledSubtypes =
+                    imm.getEnabledInputMethodSubtypeList(imi, true);
+            SUBTYPE_LOOP:
+            for (InputMethodSubtype subtype : enabledSubtypes) {
+                final String subtypeLocale = subtype.getLocale();
+                if (subtypeLocale.length() < 2) {
+                    continue;
+                }
+                // TODO: Detect language more strictly.
+                final String subtypeLanguage = subtypeLocale.substring(0, 2);
+                for (final String locale : localeList) {
+                    if (locale.startsWith(subtypeLanguage)) {
+                        foundEnabledSystemImeSubtypeWithValidLanguage = true;
+                        break SUBTYPE_LOOP;
+                    }
+                }
+            }
+        }
+        assertTrue(foundEnabledSystemImeSubtypeWithValidLanguage);
+    }
+
+    @Test
+    public void testAtLeastOneEncryptionAwareInputMethodIsAvailable() {
+        if (!mContext.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_INPUT_METHODS)) {
+            return;
+        }
+
+        if (!TextUtils.equals("native", getFbeMode())) {
+            // Skip the test unless the device is in native FBE mode.
+            return;
+        }
+
+        final InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+        final List<InputMethodInfo> imis = imm.getInputMethodList();
+        boolean hasEncryptionAwareInputMethod = false;
+        for (final InputMethodInfo imi : imis) {
+            final ServiceInfo serviceInfo = imi.getServiceInfo();
+            if (serviceInfo == null) {
+                continue;
+            }
+            if ((serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) !=
+                    ApplicationInfo.FLAG_SYSTEM) {
+                continue;
+            }
+            if (serviceInfo.encryptionAware) {
+                hasEncryptionAwareInputMethod = true;
+                break;
+            }
+        }
+        assertTrue(hasEncryptionAwareInputMethod);
+    }
+
+    private String getFbeMode() {
+        try (ParcelFileDescriptor.AutoCloseInputStream in =
+                     new ParcelFileDescriptor.AutoCloseInputStream(InstrumentationRegistry
+                             .getInstrumentation()
+                             .getUiAutomation()
+                             .executeShellCommand("sm get-fbe-mode"))) {
+            try (BufferedReader br =
+                         new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
+                // Assume that the output of "sm get-fbe-mode" is always one-line.
+                final String line = br.readLine();
+                return line != null ? line.trim() : "";
+            }
+        } catch (IOException e) {
+            return "";
+        }
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodManagerTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodManagerTest.java
new file mode 100644
index 0000000..16eaad0
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodManagerTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.KeyEvent;
+import android.view.Window;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.cts.R;
+import android.widget.EditText;
+
+import com.android.compatibility.common.util.PollingCheck;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class InputMethodManagerTest {
+    private Instrumentation mInstrumentation;
+    private InputMethodCtsActivity mActivity;
+
+    @Rule
+    public ActivityTestRule<InputMethodCtsActivity> mActivityRule =
+            new ActivityTestRule<>(InputMethodCtsActivity.class);
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mActivity = mActivityRule.getActivity();
+    }
+
+    @After
+    public void teardown() {
+        mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+    }
+
+    @Test
+    public void testInputMethodManager() throws Throwable {
+        if (!mActivity.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_INPUT_METHODS)) {
+            return;
+        }
+
+        Window window = mActivity.getWindow();
+        final EditText view = (EditText) window.findViewById(R.id.entry);
+
+        PollingCheck.waitFor(1000, view::hasWindowFocus);
+
+        mActivityRule.runOnUiThread(view::requestFocus);
+        mInstrumentation.waitForIdleSync();
+        assertTrue(view.isFocused());
+
+        BaseInputConnection connection = new BaseInputConnection(view, false);
+        Context context = mInstrumentation.getTargetContext();
+        final InputMethodManager imManager = (InputMethodManager) context
+                .getSystemService(Context.INPUT_METHOD_SERVICE);
+
+        PollingCheck.waitFor(imManager::isActive);
+
+        assertTrue(imManager.isAcceptingText());
+        assertTrue(imManager.isActive(view));
+
+        assertFalse(imManager.isFullscreenMode());
+        connection.reportFullscreenMode(true);
+        // Only IMEs are allowed to report full-screen mode.  Calling this method from the
+        // application should have no effect.
+        assertFalse(imManager.isFullscreenMode());
+
+        mActivityRule.runOnUiThread(() -> {
+            IBinder token = view.getWindowToken();
+
+            // Show and hide input method.
+            assertTrue(imManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT));
+            assertTrue(imManager.hideSoftInputFromWindow(token, 0));
+
+            Handler handler = new Handler();
+            ResultReceiver receiver = new ResultReceiver(handler);
+            assertTrue(imManager.showSoftInput(view, 0, receiver));
+            receiver = new ResultReceiver(handler);
+            assertTrue(imManager.hideSoftInputFromWindow(token, 0, receiver));
+
+            imManager.showSoftInputFromInputMethod(token, InputMethodManager.SHOW_FORCED);
+            imManager.hideSoftInputFromInputMethod(token, InputMethodManager.HIDE_NOT_ALWAYS);
+
+            // status: hide to show to hide
+            imManager.toggleSoftInputFromWindow(token, 0, InputMethodManager.HIDE_NOT_ALWAYS);
+            imManager.toggleSoftInputFromWindow(token, 0, InputMethodManager.HIDE_NOT_ALWAYS);
+
+            List<InputMethodInfo> enabledImList = imManager.getEnabledInputMethodList();
+            if (enabledImList != null && enabledImList.size() > 0) {
+                imManager.setInputMethod(token, enabledImList.get(0).getId());
+                // cannot test whether setting was successful
+            }
+
+            List<InputMethodInfo> imList = imManager.getInputMethodList();
+            if (imList != null && enabledImList != null) {
+                assertTrue(imList.size() >= enabledImList.size());
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodSettingsActivityStub.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodSettingsActivityStub.java
new file mode 100644
index 0000000..58aa364
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodSettingsActivityStub.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import android.preference.PreferenceActivity;
+
+public class InputMethodSettingsActivityStub extends PreferenceActivity {
+
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/KeyboardTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/KeyboardTest.java
new file mode 100644
index 0000000..3f61093
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/KeyboardTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.inputmethodservice.Keyboard;
+import android.inputmethodservice.Keyboard.Key;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.inputmethod.cts.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KeyboardTest {
+    @Test
+    public void testKeyOnPressedAndReleased() {
+        Key nonStickyKey = null;
+        Key stickyKey = null;
+        // Indirectly instantiate Keyboard.Key with XML resources.
+        final Keyboard keyboard =
+                new Keyboard(InstrumentationRegistry.getTargetContext(), R.xml.keyboard);
+        for (final Key key : keyboard.getKeys()) {
+            if (!key.sticky) {
+                nonStickyKey = key;
+                break;
+            }
+        }
+        for (final Key key : keyboard.getModifierKeys()) {
+            if (key.sticky) {
+                stickyKey = key;
+                break;
+            }
+        }
+
+        // Asserting existences of following keys is not the goal of this test, but this should work
+        // anyway.
+        assertNotNull(nonStickyKey);
+        assertNotNull(stickyKey);
+
+        // At first, both "pressed" and "on" must be false.
+        assertFalse(nonStickyKey.pressed);
+        assertFalse(stickyKey.pressed);
+        assertFalse(nonStickyKey.on);
+        assertFalse(stickyKey.on);
+
+        // Pressing the key must flip the "pressed" state only.
+        nonStickyKey.onPressed();
+        stickyKey.onPressed();
+        assertTrue(nonStickyKey.pressed);
+        assertTrue(stickyKey.pressed);
+        assertFalse(nonStickyKey.on);
+        assertFalse(stickyKey.on);
+
+        // Releasing the key inside the key area must flip the "pressed" state and toggle the "on"
+        // state if the key is marked as sticky.
+        nonStickyKey.onReleased(true /* inside */);
+        stickyKey.onReleased(true /* inside */);
+        assertFalse(nonStickyKey.pressed);
+        assertFalse(stickyKey.pressed);
+        assertFalse(nonStickyKey.on);
+        assertTrue(stickyKey.on);   // The key state is toggled.
+
+        // Pressing the key again must flip the "pressed" state only.
+        nonStickyKey.onPressed();
+        stickyKey.onPressed();
+        assertTrue(nonStickyKey.pressed);
+        assertTrue(stickyKey.pressed);
+        assertFalse(nonStickyKey.on);
+        assertTrue(stickyKey.on);
+
+        // Releasing the key inside the key area must flip the "pressed" state and toggle the "on"
+        // state if the key is marked as sticky hence we will be back to the initial state.
+        nonStickyKey.onReleased(true /* inside */);
+        stickyKey.onReleased(true /* inside */);
+        assertFalse(nonStickyKey.pressed);
+        assertFalse(stickyKey.pressed);
+        assertFalse(nonStickyKey.on);
+        assertFalse(stickyKey.on);
+
+        // Pressing then releasing the key outside the key area must not affect the "on" state.
+        nonStickyKey.onPressed();
+        stickyKey.onPressed();
+        nonStickyKey.onReleased(false /* inside */);
+        stickyKey.onReleased(false /* inside */);
+        assertFalse(nonStickyKey.pressed);
+        assertFalse(stickyKey.pressed);
+        assertFalse(nonStickyKey.on);
+        assertFalse(stickyKey.on);
+    }
+}
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/util/InputConnectionTestUtils.java b/tests/inputmethod/src/android/view/inputmethod/cts/util/InputConnectionTestUtils.java
new file mode 100644
index 0000000..3735c33
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/util/InputConnectionTestUtils.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod.cts.util;
+
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+
+public final class InputConnectionTestUtils {
+
+    /**
+     * A utility function to generate test string for input method APIs.  There are several
+     * pre-defined meta characters that are useful for unit tests.
+     *
+     * <p>Pre-defined meta characters:</p>
+     * <dl>
+     *     <dl>{@code [}</dl><dd>The text selection starts from here.</dd>
+     *     <dl>{@code ]}</dl><dd>The text selection ends at here.</dd>
+     *     <dl>{@code <}</dl><dd>Represents a high surrogate character.</dd>
+     *     <dl>{@code >}</dl><dd>Represents a low surrogate character.</dd>
+     * </ul>
+     *
+     * <p>Examples: {@code "012[3<>67]89"} will be converted to {@ode "0123HL6789"}, where
+     * {@code "H"} and {@code "L"} indicate certain high and low surrogate characters, respectively,
+     * with selecting {@code "3HL67"}.</p>
+     *
+     * @param formatString
+     * @return A {@link CharSequence} object with text selection specified by the meta characters.
+     */
+    public static CharSequence formatString(final String formatString) {
+        final String U1F427 = "\uD83D\uDC27";
+        final SpannableStringBuilder builder = new SpannableStringBuilder();
+        int selectionStart = -1;
+        int selectionEnd = -1;
+        for (int i = 0; i < formatString.length(); ++i) {
+            final Character c = formatString.charAt(i);
+            switch (c) {
+                case '[':
+                    selectionStart = builder.length();
+                    break;
+                case ']':
+                    selectionEnd = builder.length();
+                    break;
+                case '<':
+                    builder.append(U1F427.charAt(0));  // High surrogate
+                    break;
+                case '>':
+                    builder.append(U1F427.charAt(1));  // Low surrogate
+                    break;
+                default:
+                    builder.append(c);
+                    break;
+            }
+        }
+        if (selectionStart < 0) {
+            throw new UnsupportedOperationException("Selection marker '[' must be specified.");
+        }
+        if (selectionEnd < 0) {
+            throw new UnsupportedOperationException("Selection marker ']' must be specified.");
+        }
+        Selection.setSelection(builder, selectionStart, selectionEnd);
+        return builder;
+    }
+}