Add MockSpellChecker and SpellCheckerTest.

This CL adds a minimal end-to-end test for Spell checker framework.
To this end, this CL introduces MockSpellChecker, which is inspired by
MockIme.

Bug: 166304720
Test: atest CtsInputMethodTestCases:SpellCheckerTest
Change-Id: I6d92ed0c7f7ca88011f6f5470798cfbc7ff3fa96
diff --git a/tests/inputmethod/Android.bp b/tests/inputmethod/Android.bp
index 6f07ac6..aa3be52 100644
--- a/tests/inputmethod/Android.bp
+++ b/tests/inputmethod/Android.bp
@@ -29,10 +29,13 @@
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "CtsMockInputMethodLib",
+        "CtsMockSpellCheckerLib",
         "testng",
+        "kotlin-test",
     ],
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
         "src/**/I*.aidl",
     ],
     aidl: {
diff --git a/tests/inputmethod/AndroidTest.xml b/tests/inputmethod/AndroidTest.xml
index fd2ea5d..9108a44 100644
--- a/tests/inputmethod/AndroidTest.xml
+++ b/tests/inputmethod/AndroidTest.xml
@@ -36,6 +36,15 @@
         <option name="force-install-mode" value="FULL"/>
         <option name="test-file-name" value="CtsMockInputMethod.apk" />
     </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <!--
+            MockSpellChecker always needs to be instaleld as a full package, even when CTS is
+            running for instant apps.
+        -->
+        <option name="force-install-mode" value="FULL"/>
+        <option name="test-file-name" value="CtsMockSpellChecker.apk" />
+    </target_preparer>
     <!--
         TODO(yukawa): come up with a proper way to take care of devices that do not support
         installable IMEs.  Ideally target_preparer should have an option to annotate required
diff --git a/tests/inputmethod/mockspellchecker/Android.bp b/tests/inputmethod/mockspellchecker/Android.bp
new file mode 100644
index 0000000..dce4215
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/Android.bp
@@ -0,0 +1,51 @@
+// Copyright (C) 2020 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.
+
+java_test_helper_library {
+    name: "CtsMockSpellCheckerLib",
+    sdk_version: "test_current",
+
+    srcs: [
+        "src/**/*.kt",
+        "src/**/*.proto",
+    ],
+    libs: ["junit"],
+    proto: {
+        type: "lite",
+    },
+    static_libs: [
+        "androidx.annotation_annotation",
+        "compatibility-device-util-axt",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsMockSpellChecker",
+    defaults: ["cts_defaults"],
+    optimize: {
+        enabled: false,
+    },
+    sdk_version: "current",
+    min_sdk_version: "19",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "CtsMockSpellCheckerLib",
+    ],
+}
diff --git a/tests/inputmethod/mockspellchecker/AndroidManifest.xml b/tests/inputmethod/mockspellchecker/AndroidManifest.xml
new file mode 100644
index 0000000..f076bb1
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 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="com.android.cts.mockspellchecker">
+
+    <application android:multiArch="true"
+                 android:supportsRtl="true">
+
+        <meta-data android:name="instantapps.clients.allowed"
+                   android:value="true"/>
+
+        <service android:name=".MockSpellChecker"
+                 android:label="@string/spell_checker_name"
+                 android:permission="android.permission.BIND_TEXT_SERVICE"
+                 android:exported="false">
+            <intent-filter>
+                <action android:name="android.service.textservice.SpellCheckerService"/>
+            </intent-filter>
+
+            <meta-data
+                android:name="android.view.textservice.scs"
+                android:resource="@xml/spellchecker"/>
+        </service>
+
+        <provider android:authorities="com.android.cts.mockspellchecker.provider"
+                  android:name=".SharedPrefsProvider"
+                  android:exported="true"
+                  android:visibleToInstantApps="true">
+        </provider>
+
+    </application>
+</manifest>
diff --git a/tests/inputmethod/mockspellchecker/res/values/values.xml b/tests/inputmethod/mockspellchecker/res/values/values.xml
new file mode 100644
index 0000000..4accba9
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/res/values/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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>
+    <string name="spell_checker_name">Mock Spell Checker</string>
+</resources>
diff --git a/tests/inputmethod/mockspellchecker/res/xml/spellchecker.xml b/tests/inputmethod/mockspellchecker/res/xml/spellchecker.xml
new file mode 100644
index 0000000..8820f29
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/res/xml/spellchecker.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 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.
+-->
+
+<spell-checker xmlns:android="http://schemas.android.com/apk/res/android"
+    android:label="@string/spell_checker_name">
+    <subtype
+        android:label="English"
+        android:subtypeLocale="en"
+    />
+</spell-checker>
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/Constants.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/Constants.kt
new file mode 100644
index 0000000..8653e94
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/Constants.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 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 com.android.cts.mockspellchecker
+
+const val TAG = "MockSpellChecker"
+const val PACKAGE = "com.android.cts.mockspellchecker"
+const val AUTHORITY = "com.android.cts.mockspellchecker.provider"
+
+internal const val KEY_CONFIGURATION = "configuration"
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellChecker.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellChecker.kt
new file mode 100644
index 0000000..ee16ce4
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellChecker.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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 com.android.cts.mockspellchecker
+
+import android.content.ComponentName
+import android.service.textservice.SpellCheckerService
+import android.util.Log
+import android.view.textservice.SuggestionsInfo
+import android.view.textservice.TextInfo
+import com.android.cts.mockspellchecker.MockSpellCheckerProto.MockSpellCheckerConfiguration
+import java.io.FileDescriptor
+import java.io.PrintWriter
+
+internal inline fun <T> withLog(msg: String, block: () -> T): T {
+    Log.i(TAG, msg)
+    return block()
+}
+
+/** Mock Spell checker for end-to-end tests. */
+class MockSpellChecker : SpellCheckerService() {
+
+    override fun onCreate() = withLog("MockSpellChecker.onCreate") {
+        super.onCreate()
+    }
+
+    override fun onDestroy() = withLog("MockSpellChecker.onDestroy") {
+        super.onDestroy()
+    }
+
+    override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
+        writer?.println("MockSpellChecker")
+    }
+
+    override fun createSession(): Session = withLog("MockSpellChecker.createSession") {
+        val configuration = MockSpellCheckerConfiguration.parseFrom(
+                SharedPrefsProvider.get(contentResolver, KEY_CONFIGURATION))
+        return MockSpellCheckerSession(configuration)
+    }
+
+    private inner class MockSpellCheckerSession(
+        val configuration: MockSpellCheckerConfiguration
+    ) : SpellCheckerService.Session() {
+
+        override fun onCreate() = withLog("MockSpellCheckerSession.onCreate") {
+        }
+
+        override fun onGetSuggestions(
+            textInfo: TextInfo?,
+            suggestionsLimit: Int
+        ): SuggestionsInfo = withLog(
+            "MockSpellCheckerSession.onGetSuggestions: ${textInfo?.text}") {
+            if (textInfo == null) return emptySuggestionsInfo()
+            return configuration.suggestionRulesList
+                    .find { it.match == textInfo.text }
+                    ?.let { SuggestionsInfo(it.attributes, it.suggestionsList.toTypedArray()) }
+                    ?: emptySuggestionsInfo()
+        }
+
+        private fun emptySuggestionsInfo() = SuggestionsInfo(0, arrayOf())
+    }
+
+    companion object {
+        @JvmStatic
+        fun getId(): String =
+                ComponentName(PACKAGE, MockSpellChecker::class.java.name).flattenToShortString()
+    }
+}
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellCheckerClient.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellCheckerClient.kt
new file mode 100644
index 0000000..90c9fd5
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/MockSpellCheckerClient.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 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 com.android.cts.mockspellchecker
+
+import android.content.Context
+import com.android.cts.mockspellchecker.MockSpellCheckerProto.MockSpellCheckerConfiguration
+
+/**
+ * Client interface for {@link MockSpellChecker}.
+ *
+ * <p>This class should be used by test apps.
+ */
+class MockSpellCheckerClient(
+    private val context: Context,
+    private val configuration: MockSpellCheckerConfiguration
+)
+    : AutoCloseable {
+
+    fun initialize() {
+        SharedPrefsProvider.put(
+                context.contentResolver, KEY_CONFIGURATION, configuration.toByteArray())
+    }
+
+    override fun close() {
+        SharedPrefsProvider.delete(context.contentResolver, KEY_CONFIGURATION)
+    }
+
+    companion object {
+        @JvmStatic
+        fun create(context: Context, configuration: MockSpellCheckerConfiguration):
+                MockSpellCheckerClient {
+            val client = MockSpellCheckerClient(context, configuration)
+            client.initialize()
+            return client
+        }
+    }
+}
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/SharedPrefsProvider.kt b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/SharedPrefsProvider.kt
new file mode 100644
index 0000000..7e23885
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/SharedPrefsProvider.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 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 com.android.cts.mockspellchecker
+
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Base64
+
+private const val PREFS_FILE_NAME = "prefs.xml"
+private const val COLUMN_NAME = "value"
+
+/**
+ * ContentProvider to access MockSpellChecker's shared preferences.
+ *
+ * <p>Please use the companion object methods to interact with this ContentProvider. The companion
+ * object methods can be used from other processes.
+ *
+ * <p>This class supports ByteArray value only.
+ */
+class SharedPrefsProvider : ContentProvider() {
+
+    override fun onCreate(): Boolean = withLog("SharedPrefsProvider.onCreate") { true }
+
+    override fun getType(uri: Uri): String? = null
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? = withLog("SharedPrefsProvider.query: $uri") {
+        val context = context ?: return null
+        val prefs = getSharedPreferences(context)
+        val bytes = Base64.decode(prefs.getString(uri.path, ""), Base64.DEFAULT)
+        val cursor = MatrixCursor(arrayOf(COLUMN_NAME))
+        cursor.addRow(arrayOf(bytes))
+        return cursor
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? =
+            withLog("SharedPrefsProvider.insert: $uri") { null }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int = withLog("SharedPrefsProvider.update: $uri") {
+        val context = context ?: return 0
+        if (values == null) return 0
+        val prefs = getSharedPreferences(context)
+        val bytes = values.getAsByteArray(COLUMN_NAME)
+        val str = Base64.encodeToString(bytes, Base64.DEFAULT)
+        prefs.edit().putString(uri.path, str).apply()
+        return 1
+    }
+
+    override fun delete(
+        uri: Uri,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int = withLog("SharedPrefsProvider.delete: $uri") {
+        val context = context ?: return 0
+        val prefs = getSharedPreferences(context)
+        prefs.edit().remove(uri.path).apply()
+        return 1
+    }
+
+    private fun getSharedPreferences(context: Context) =
+        context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
+
+    companion object {
+        /** Returns the data for the key. */
+        fun get(resolver: ContentResolver, key: String): ByteArray {
+            val cursor = resolver.query(uriFor(key), arrayOf(COLUMN_NAME), null, null)
+            return if (cursor != null && cursor.moveToNext()) {
+                cursor.getBlob(0)
+            } else {
+                ByteArray(0)
+            }
+        }
+
+        /** Stores the data for the key. */
+        fun put(resolver: ContentResolver, key: String, value: ByteArray) {
+            val values = ContentValues()
+            values.put(COLUMN_NAME, value)
+            resolver.update(uriFor(key), values, null)
+        }
+
+        /** Deletes the data for the key. */
+        fun delete(resolver: ContentResolver, key: String) {
+            resolver.delete(uriFor(key), null)
+        }
+
+        private fun uriFor(key: String): Uri = Uri.parse("content://$AUTHORITY/$key")
+    }
+}
diff --git a/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/mockspellchecker.proto b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/mockspellchecker.proto
new file mode 100644
index 0000000..58b127f
--- /dev/null
+++ b/tests/inputmethod/mockspellchecker/src/com/android/cts/mockspellchecker/mockspellchecker.proto
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+syntax = "proto2";
+
+package com.android.cts.mockspellchecker;
+
+option java_outer_classname = "MockSpellCheckerProto";
+
+// Represents a suggestion rule.
+// If the string matches 'match', SuggestionsInfo with attributes and suggestions are appended.
+message SuggestionRule {
+  optional string match = 1;
+  optional int32 attributes = 2;
+  repeated string suggestions = 3;
+}
+
+// Represents a MockSpellChecker configuration.
+message MockSpellCheckerConfiguration {
+  repeated SuggestionRule suggestion_rules = 1;
+};
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/SpellCheckerTest.kt b/tests/inputmethod/src/android/view/inputmethod/cts/SpellCheckerTest.kt
new file mode 100644
index 0000000..77eaa64
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/SpellCheckerTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 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.content.Context
+import android.provider.Settings
+import android.text.style.SuggestionSpan
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.inputmethod.cts.util.EndToEndImeTestBase
+import android.view.inputmethod.cts.util.InputMethodVisibilityVerifier
+import android.view.inputmethod.cts.util.TestActivity
+import android.view.inputmethod.cts.util.TestUtils
+import android.view.inputmethod.cts.util.UnlockScreenRule
+import android.view.textservice.SpellCheckerSubtype
+import android.view.textservice.SuggestionsInfo
+import android.view.textservice.TextServicesManager
+import android.widget.EditText
+import android.widget.LinearLayout
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.CtsTouchUtils
+import com.android.compatibility.common.util.SettingsStateChangerRule
+import com.android.cts.mockime.MockImeSession
+import com.android.cts.mockspellchecker.MockSpellChecker
+import com.android.cts.mockspellchecker.MockSpellCheckerClient
+import com.android.cts.mockspellchecker.MockSpellCheckerProto
+import com.android.cts.mockspellchecker.MockSpellCheckerProto.MockSpellCheckerConfiguration
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+private val TIMEOUT = TimeUnit.SECONDS.toMillis(5)
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class SpellCheckerTest : EndToEndImeTestBase() {
+
+    private val context: Context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+
+    @Rule
+    fun unlockScreenRule() = UnlockScreenRule()
+
+    @Rule
+    fun spellCheckerSettingsRule() = SettingsStateChangerRule(
+            context, Settings.Secure.SELECTED_SPELL_CHECKER, MockSpellChecker.getId())
+
+    @Rule
+    fun spellCheckerSubtypeSettingsRule() = SettingsStateChangerRule(
+            context, Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE,
+            SpellCheckerSubtype.SUBTYPE_ID_NONE.toString())
+
+    @Before
+    fun setUp() {
+        val tsm = context.getSystemService(TextServicesManager::class.java)!!
+        // Skip if spell checker is not enabled by default.
+        Assume.assumeNotNull(tsm)
+        Assume.assumeTrue(tsm.isSpellCheckerEnabled)
+    }
+
+    @Test
+    fun test() {
+        val configuration = MockSpellCheckerConfiguration.newBuilder()
+                .addSuggestionRules(
+                        MockSpellCheckerProto.SuggestionRule.newBuilder()
+                                .setMatch("match")
+                                .addSuggestions("suggestion")
+                                .setAttributes(SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
+                ).build()
+        MockImeSession.create(context).use { session ->
+            MockSpellCheckerClient.create(context, configuration).use {
+                val (_, editText) = startTestActivity()
+                CtsTouchUtils.emulateTapOnViewCenter(
+                        InstrumentationRegistry.getInstrumentation(), null, editText)
+                TestUtils.waitOnMainUntil({ editText.hasFocus() }, TIMEOUT)
+                InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT)
+                session.callCommitText("match", 1)
+                session.callCommitText(" ", 1)
+                TestUtils.waitOnMainUntil({ hasSuggestionSpan(editText) }, TIMEOUT)
+            }
+        }
+    }
+
+    private fun hasSuggestionSpan(editText: EditText): Boolean {
+        val editable = editText.text
+        val spans = editable.getSpans(0, editable.length, SuggestionSpan::class.java)
+        return spans != null && spans.isNotEmpty()
+    }
+
+    private fun startTestActivity(): Pair<TestActivity, EditText> {
+        var editText: EditText? = null
+        val activity = TestActivity.startSync { activity: TestActivity? ->
+            val layout = LinearLayout(activity)
+            editText = EditText(activity)
+            layout.addView(editText, LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT))
+            layout
+        }
+        return Pair(activity, editText!!)
+    }
+}
\ No newline at end of file