1 /* <lambda>null2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.view.inputmethod.cts 17 18 import android.app.Instrumentation 19 import android.app.UiAutomation 20 import android.content.Context 21 import android.os.Bundle 22 import android.os.Looper 23 import android.provider.Settings 24 import android.text.style.SuggestionSpan 25 import android.text.style.SuggestionSpan.FLAG_GRAMMAR_ERROR 26 import android.text.style.SuggestionSpan.FLAG_MISSPELLED 27 import android.text.style.SuggestionSpan.SUGGESTIONS_MAX_SIZE 28 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 29 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 30 import android.view.inputmethod.InputMethodInfo 31 import android.view.inputmethod.InputMethodManager 32 import android.view.inputmethod.cts.util.EndToEndImeTestBase 33 import android.view.inputmethod.cts.util.InputMethodVisibilityVerifier 34 import android.view.inputmethod.cts.util.TestActivity 35 import android.view.inputmethod.cts.util.TestUtils.runOnMainSync 36 import android.view.inputmethod.cts.util.TestUtils.waitOnMainUntil 37 import android.view.inputmethod.cts.util.UnlockScreenRule 38 import android.view.textservice.SentenceSuggestionsInfo 39 import android.view.textservice.SpellCheckerSession 40 import android.view.textservice.SpellCheckerSubtype 41 import android.view.textservice.SuggestionsInfo 42 import android.view.textservice.SuggestionsInfo.RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS 43 import android.view.textservice.SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY 44 import android.view.textservice.SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR 45 import android.view.textservice.SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO 46 import android.view.textservice.TextInfo 47 import android.view.textservice.TextServicesManager 48 import android.widget.EditText 49 import android.widget.LinearLayout 50 import androidx.annotation.UiThread 51 import androidx.test.filters.MediumTest 52 import androidx.test.platform.app.InstrumentationRegistry 53 import androidx.test.runner.AndroidJUnit4 54 import androidx.test.uiautomator.By 55 import androidx.test.uiautomator.UiDevice 56 import androidx.test.uiautomator.Until 57 import com.android.compatibility.common.util.CtsTouchUtils 58 import com.android.compatibility.common.util.PollingCheck 59 import com.android.compatibility.common.util.SettingsStateChangerRule 60 import com.android.compatibility.common.util.SystemUtil 61 import com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand 62 import com.android.cts.mockime.MockImeSession 63 import com.android.cts.mockspellchecker.EXTRAS_KEY_PREFIX 64 import com.android.cts.mockspellchecker.MockSpellChecker 65 import com.android.cts.mockspellchecker.MockSpellCheckerClient 66 import com.android.cts.mockspellchecker.MockSpellCheckerProto 67 import com.android.cts.mockspellchecker.MockSpellCheckerProto.MockSpellCheckerConfiguration 68 import com.google.common.truth.Truth.assertThat 69 import org.junit.Assert.assertThrows 70 import org.junit.Assert.fail 71 import org.junit.Assume 72 import org.junit.Before 73 import org.junit.Rule 74 import org.junit.Test 75 import org.junit.runner.RunWith 76 import java.lang.IllegalArgumentException 77 import java.util.Locale 78 import java.util.concurrent.Executor 79 import java.util.concurrent.TimeUnit 80 import java.util.concurrent.TimeoutException 81 import kotlin.collections.ArrayList 82 83 @MediumTest 84 @RunWith(AndroidJUnit4::class) 85 class SpellCheckerTest : EndToEndImeTestBase() { 86 87 private val TAG = "SpellCheckerTest" 88 private val SPELL_CHECKING_IME_ID = "com.android.cts.spellcheckingime/.SpellCheckingIme" 89 private val TIMEOUT = TimeUnit.SECONDS.toMillis(5) 90 91 private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() 92 private val context: Context = instrumentation.getTargetContext() 93 private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) 94 private val uiAutomation: UiAutomation = instrumentation.uiAutomation 95 96 @Rule 97 fun unlockScreenRule() = UnlockScreenRule() 98 99 @Rule 100 fun spellCheckerSettingsRule() = SettingsStateChangerRule( 101 context, Settings.Secure.SELECTED_SPELL_CHECKER, MockSpellChecker.getId()) 102 103 @Rule 104 fun spellCheckerSubtypeSettingsRule() = SettingsStateChangerRule( 105 context, Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, 106 SpellCheckerSubtype.SUBTYPE_ID_NONE.toString()) 107 108 @Before 109 fun setUp() { 110 val tsm = context.getSystemService(TextServicesManager::class.java)!! 111 // Skip if spell checker is not enabled by default. 112 Assume.assumeNotNull(tsm) 113 Assume.assumeTrue(tsm.isSpellCheckerEnabled) 114 } 115 116 @Test 117 fun misspelled_easyCorrect() { 118 val uniqueSuggestion = "s618397" // "s" + a random number 119 val configuration = MockSpellCheckerConfiguration.newBuilder() 120 .addSuggestionRules( 121 MockSpellCheckerProto.SuggestionRule.newBuilder() 122 .setMatch("match") 123 .addSuggestions(uniqueSuggestion) 124 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 125 ).build() 126 MockImeSession.create(context).use { session -> 127 MockSpellCheckerClient.create(context, configuration).use { 128 val (_, editText) = startTestActivity() 129 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 130 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 131 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 132 session.callCommitText("match", 1) 133 session.callCommitText(" ", 1) 134 waitOnMainUntil({ 135 findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null 136 }, TIMEOUT) 137 // Tap inside 'match'. 138 emulateTapAtOffset(editText, 2) 139 // Wait until the cursor moves inside 'match'. 140 waitOnMainUntil({ isCursorInside(editText, 1, 4) }, TIMEOUT) 141 // Wait for the suggestion to come up, and click it. 142 uiDevice.wait(Until.findObject(By.text(uniqueSuggestion)), TIMEOUT).also { 143 assertThat(it).isNotNull() 144 }.click() 145 // Verify that the text ('match') is replaced with the suggestion. 146 waitOnMainUntil({ "$uniqueSuggestion " == editText.text.toString() }, TIMEOUT) 147 // The SuggestionSpan should be removed. 148 waitOnMainUntil({ 149 findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) == null 150 }, TIMEOUT) 151 } 152 } 153 } 154 155 @Test 156 fun misspelled_noEasyCorrect() { 157 val uniqueSuggestion = "s974355" // "s" + a random number 158 val configuration = MockSpellCheckerConfiguration.newBuilder() 159 .addSuggestionRules( 160 MockSpellCheckerProto.SuggestionRule.newBuilder() 161 .setMatch("match") 162 .addSuggestions(uniqueSuggestion) 163 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO 164 or RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS) 165 ).build() 166 MockImeSession.create(context).use { session -> 167 MockSpellCheckerClient.create(context, configuration).use { 168 val (_, editText) = startTestActivity() 169 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 170 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 171 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 172 session.callCommitText("match", 1) 173 session.callCommitText(" ", 1) 174 waitOnMainUntil({ 175 findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null 176 }, TIMEOUT) 177 // Tap inside 'match'. 178 emulateTapAtOffset(editText, 2) 179 // Wait until the cursor moves inside 'match'. 180 waitOnMainUntil({ isCursorInside(editText, 1, 4) }, TIMEOUT) 181 // Verify that the suggestion is not shown. 182 assertThat(uiDevice.wait(Until.gone(By.text(uniqueSuggestion)), TIMEOUT)).isTrue() 183 } 184 } 185 } 186 187 @Test 188 fun grammarError() { 189 val configuration = MockSpellCheckerConfiguration.newBuilder() 190 .addSuggestionRules( 191 MockSpellCheckerProto.SuggestionRule.newBuilder() 192 .setMatch("match") 193 .addSuggestions("suggestion") 194 .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR) 195 ).build() 196 MockImeSession.create(context).use { session -> 197 MockSpellCheckerClient.create(context, configuration).use { 198 val (_, editText) = startTestActivity() 199 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 200 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 201 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 202 session.callCommitText("match", 1) 203 session.callCommitText(" ", 1) 204 waitOnMainUntil({ 205 findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null 206 }, TIMEOUT) 207 } 208 } 209 } 210 211 @Test 212 fun performSpellCheck() { 213 val configuration = MockSpellCheckerConfiguration.newBuilder() 214 .addSuggestionRules( 215 MockSpellCheckerProto.SuggestionRule.newBuilder() 216 .setMatch("match") 217 .addSuggestions("suggestion") 218 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 219 ).build() 220 MockImeSession.create(context).use { session -> 221 MockSpellCheckerClient.create(context, configuration).use { client -> 222 val stream = session.openEventStream() 223 val (_, editText) = startTestActivity() 224 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 225 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 226 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 227 session.callCommitText("match", 1) 228 session.callCommitText(" ", 1) 229 waitOnMainUntil({ 230 findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null 231 }, TIMEOUT) 232 // The word is now in dictionary. The next spell check should remove the misspelled 233 // SuggestionSpan. 234 client.updateConfiguration(MockSpellCheckerConfiguration.newBuilder() 235 .addSuggestionRules( 236 MockSpellCheckerProto.SuggestionRule.newBuilder() 237 .setMatch("match") 238 .setAttributes(RESULT_ATTR_IN_THE_DICTIONARY) 239 ).build()) 240 val command = session.callPerformSpellCheck() 241 expectCommand(stream, command, TIMEOUT) 242 waitOnMainUntil({ 243 findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) == null 244 }, TIMEOUT) 245 } 246 } 247 } 248 249 @Test 250 fun textServicesManagerApi() { 251 val tsm = context.getSystemService(TextServicesManager::class.java)!! 252 assertThat(tsm).isNotNull() 253 assertThat(tsm!!.isSpellCheckerEnabled()).isTrue() 254 val spellCheckerInfo = tsm.getCurrentSpellCheckerInfo() 255 assertThat(spellCheckerInfo).isNotNull() 256 assertThat(spellCheckerInfo!!.getPackageName()).isEqualTo( 257 "com.android.cts.mockspellchecker") 258 assertThat(spellCheckerInfo!!.getSubtypeCount()).isEqualTo(1) 259 assertThat(tsm.getEnabledSpellCheckerInfos()!!.size).isAtLeast(1) 260 assertThat(tsm.getEnabledSpellCheckerInfos()!!.map { it.getPackageName() }) 261 .contains("com.android.cts.mockspellchecker") 262 } 263 264 @Test 265 fun newSpellCheckerSession() { 266 val configuration = MockSpellCheckerConfiguration.newBuilder() 267 .addSuggestionRules( 268 MockSpellCheckerProto.SuggestionRule.newBuilder() 269 .setMatch("match") 270 .addSuggestions("suggestion") 271 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 272 ).build() 273 MockSpellCheckerClient.create(context, configuration).use { 274 val tsm = context.getSystemService(TextServicesManager::class.java) 275 assertThat(tsm).isNotNull() 276 val fakeListener = FakeSpellCheckerSessionListener() 277 val fakeExecutor = FakeExecutor() 278 val params = SpellCheckerSession.SpellCheckerSessionParams.Builder() 279 .setLocale(Locale.US) 280 .setSupportedAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 281 .build() 282 val session: SpellCheckerSession? = tsm?.newSpellCheckerSession( 283 params, fakeExecutor, fakeListener) 284 assertThat(session).isNotNull() 285 session?.getSentenceSuggestions(arrayOf(TextInfo("match")), 5) 286 waitOnMainUntil({ fakeExecutor.runnables.size == 1 }, TIMEOUT) 287 fakeExecutor.runnables[0].run() 288 289 assertThat(fakeListener.getSentenceSuggestionsResults).hasSize(1) 290 assertThat(fakeListener.getSentenceSuggestionsResults[0]).hasLength(1) 291 val sentenceSuggestionsInfo = fakeListener.getSentenceSuggestionsResults[0]!![0] 292 assertThat(sentenceSuggestionsInfo.suggestionsCount).isEqualTo(1) 293 assertThat(sentenceSuggestionsInfo.getOffsetAt(0)).isEqualTo(0) 294 assertThat(sentenceSuggestionsInfo.getLengthAt(0)).isEqualTo("match".length) 295 val suggestionsInfo = sentenceSuggestionsInfo.getSuggestionsInfoAt(0) 296 assertThat(suggestionsInfo.suggestionsCount).isEqualTo(1) 297 assertThat(suggestionsInfo.getSuggestionAt(0)).isEqualTo("suggestion") 298 299 assertThat(fakeListener.getSentenceSuggestionsResults).hasSize(1) 300 assertThat(fakeListener.getSentenceSuggestionsCallingThreads).hasSize(1) 301 assertThat(fakeListener.getSentenceSuggestionsCallingThreads[0]) 302 .isEqualTo(Thread.currentThread()) 303 } 304 } 305 306 @Test 307 fun newSpellCheckerSession_implicitExecutor() { 308 val configuration = MockSpellCheckerConfiguration.newBuilder() 309 .addSuggestionRules( 310 MockSpellCheckerProto.SuggestionRule.newBuilder() 311 .setMatch("match") 312 .addSuggestions("suggestion") 313 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 314 ).build() 315 MockSpellCheckerClient.create(context, configuration).use { 316 val tsm = context.getSystemService(TextServicesManager::class.java) 317 assertThat(tsm).isNotNull() 318 val fakeListener = FakeSpellCheckerSessionListener() 319 var session: SpellCheckerSession? = null 320 runOnMainSync { 321 session = tsm?.newSpellCheckerSession(null /* bundle */, Locale.US, 322 fakeListener, false /* referToSpellCheckerLanguageSettings */) 323 } 324 assertThat(session).isNotNull() 325 session?.getSentenceSuggestions(arrayOf(TextInfo("match")), 5) 326 waitOnMainUntil({ 327 fakeListener.getSentenceSuggestionsCallingThreads.size > 0 328 }, TIMEOUT) 329 runOnMainSync { 330 assertThat(fakeListener.getSentenceSuggestionsCallingThreads).hasSize(1) 331 assertThat(fakeListener.getSentenceSuggestionsCallingThreads[0]) 332 .isEqualTo(Looper.getMainLooper().thread) 333 } 334 } 335 } 336 337 @Test 338 fun newSpellCheckerSession_extras() { 339 val configuration = MockSpellCheckerConfiguration.newBuilder() 340 .addSuggestionRules( 341 MockSpellCheckerProto.SuggestionRule.newBuilder() 342 .setMatch("match") 343 .addSuggestions("suggestion") 344 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 345 ).build() 346 MockSpellCheckerClient.create(context, configuration).use { 347 val tsm = context.getSystemService(TextServicesManager::class.java) 348 assertThat(tsm).isNotNull() 349 val fakeListener = FakeSpellCheckerSessionListener() 350 val fakeExecutor = FakeExecutor() 351 // Set a prefix. MockSpellChecker will add "test_" to the spell check result. 352 val extras = Bundle() 353 extras.putString(EXTRAS_KEY_PREFIX, "test_") 354 val params = SpellCheckerSession.SpellCheckerSessionParams.Builder() 355 .setLocale(Locale.US) 356 .setSupportedAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 357 .setExtras(extras) 358 .build() 359 val session: SpellCheckerSession? = tsm?.newSpellCheckerSession( 360 params, fakeExecutor, fakeListener) 361 assertThat(session).isNotNull() 362 session?.getSentenceSuggestions(arrayOf(TextInfo("match")), 5) 363 waitOnMainUntil({ fakeExecutor.runnables.size == 1 }, TIMEOUT) 364 fakeExecutor.runnables[0].run() 365 366 assertThat(fakeListener.getSentenceSuggestionsResults).hasSize(1) 367 assertThat(fakeListener.getSentenceSuggestionsResults[0]).hasLength(1) 368 val sentenceSuggestionsInfo = fakeListener.getSentenceSuggestionsResults[0]!![0] 369 assertThat(sentenceSuggestionsInfo.suggestionsCount).isEqualTo(1) 370 val suggestionsInfo = sentenceSuggestionsInfo.getSuggestionsInfoAt(0) 371 assertThat(suggestionsInfo.suggestionsCount).isEqualTo(1) 372 assertThat(suggestionsInfo.getSuggestionAt(0)).isEqualTo("test_suggestion") 373 } 374 } 375 376 @Test 377 fun spellCheckerSessionParamsBuilder() { 378 // Locale or shouldReferToSpellCheckerLanguageSettings should be set. 379 assertThrows(IllegalArgumentException::class.java) { 380 SpellCheckerSession.SpellCheckerSessionParams.Builder().build() 381 } 382 383 // Test defaults. 384 val localeOnly = SpellCheckerSession.SpellCheckerSessionParams.Builder() 385 .setLocale(Locale.US) 386 .build() 387 assertThat(localeOnly.locale).isEqualTo(Locale.US) 388 assertThat(localeOnly.shouldReferToSpellCheckerLanguageSettings()).isFalse() 389 assertThat(localeOnly.supportedAttributes).isEqualTo(0) 390 assertThat(localeOnly.extras).isNotNull() 391 assertThat(localeOnly.extras.size()).isEqualTo(0) 392 393 // Test setters. 394 val extras = Bundle() 395 extras.putString("key", "value") 396 val params = SpellCheckerSession.SpellCheckerSessionParams.Builder() 397 .setLocale(Locale.CANADA) 398 .setShouldReferToSpellCheckerLanguageSettings(true) 399 .setSupportedAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 400 .setExtras(extras) 401 .build() 402 assertThat(params.locale).isEqualTo(Locale.CANADA) 403 assertThat(params.shouldReferToSpellCheckerLanguageSettings()).isTrue() 404 assertThat(params.supportedAttributes).isEqualTo(RESULT_ATTR_LOOKS_LIKE_TYPO) 405 // Bundle does not implement equals. 406 assertThat(params.extras).isNotNull() 407 assertThat(params.extras.size()).isEqualTo(1) 408 assertThat(params.extras.getString("key")).isEqualTo("value") 409 } 410 411 @Test 412 fun suppressesSpellChecker() { 413 val configuration = MockSpellCheckerConfiguration.newBuilder() 414 .addSuggestionRules( 415 MockSpellCheckerProto.SuggestionRule.newBuilder() 416 .setMatch("match") 417 .addSuggestions("suggestion") 418 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 419 ).build() 420 // SpellCheckingIme should have android:suppressesSpellChecker="true" 421 ImeSession(SPELL_CHECKING_IME_ID).use { 422 assertThat(getCurrentInputMethodInfo().suppressesSpellChecker()).isTrue() 423 424 MockSpellCheckerClient.create(context, configuration).use { 425 val (activity, editText) = startTestActivity() 426 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 427 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 428 val imm = activity.getSystemService(InputMethodManager::class.java) 429 assertThat(imm?.isInputMethodSuppressingSpellChecker).isTrue() 430 431 // SpellCheckerSession should return empty results if suppressed. 432 val tsm = activity.getSystemService(TextServicesManager::class.java) 433 val listener = FakeSpellCheckerSessionListener() 434 var session: SpellCheckerSession? = null 435 runOnMainSync { 436 session = tsm?.newSpellCheckerSession(null, Locale.US, listener, false) 437 } 438 assertThat(session).isNotNull() 439 val suggestions: Array<SentenceSuggestionsInfo>? = 440 getSentenceSuggestions(session!!, listener, "match") 441 assertThat(suggestions).isNotNull() 442 assertThat(suggestions!!.size).isEqualTo(0) 443 } 444 } 445 } 446 447 @Test 448 fun suppressesSpellChecker_false() { 449 MockImeSession.create(context).use { 450 assertThat(getCurrentInputMethodInfo().suppressesSpellChecker()).isFalse() 451 452 val (activity, editText) = startTestActivity() 453 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 454 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 455 val imm = activity.getSystemService(InputMethodManager::class.java) 456 assertThat(imm?.isInputMethodSuppressingSpellChecker).isFalse() 457 } 458 } 459 460 @Test 461 fun trailingPunctuation() { 462 // Set up a rule that matches the sentence "match?" and marks it as grammar error. 463 val configuration = MockSpellCheckerConfiguration.newBuilder() 464 .setMatchSentence(true) 465 .addSuggestionRules( 466 MockSpellCheckerProto.SuggestionRule.newBuilder() 467 .setMatch("match?") 468 .addSuggestions("suggestion.") 469 .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR) 470 ).build() 471 MockImeSession.create(context).use { session -> 472 MockSpellCheckerClient.create(context, configuration).use { client -> 473 val (_, editText) = startTestActivity() 474 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 475 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 476 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 477 session.callCommitText("match", 1) 478 // The trailing punctuation "?" is also sent in the next spell check, and the 479 // sentence "match?" will be marked as FLAG_GRAMMAR_ERROR according to the 480 // configuration. 481 session.callCommitText("?", 1) 482 waitOnMainUntil({ 483 findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null 484 }, TIMEOUT) 485 } 486 } 487 } 488 489 @Test 490 fun newSpellCheckerSession_processPurePunctuationRequest() { 491 val configuration = MockSpellCheckerConfiguration.newBuilder() 492 .addSuggestionRules( 493 MockSpellCheckerProto.SuggestionRule.newBuilder() 494 .setMatch("foo") 495 .addSuggestions("suggestion") 496 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 497 ).build() 498 MockSpellCheckerClient.create(context, configuration).use { 499 val tsm = context.getSystemService(TextServicesManager::class.java) 500 assertThat(tsm).isNotNull() 501 val fakeListener = FakeSpellCheckerSessionListener() 502 val fakeExecutor = FakeExecutor() 503 val params = SpellCheckerSession.SpellCheckerSessionParams.Builder() 504 .setLocale(Locale.US) 505 .setSupportedAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 506 .build() 507 var session: SpellCheckerSession? = tsm?.newSpellCheckerSession( 508 params, fakeExecutor, fakeListener) 509 assertThat(session).isNotNull() 510 session?.getSentenceSuggestions(arrayOf(TextInfo(". ")), 5) 511 waitOnMainUntil({ fakeExecutor.runnables.size == 1 }, TIMEOUT) 512 fakeExecutor.runnables[0].run() 513 assertThat(fakeListener.getSentenceSuggestionsResults).hasSize(1) 514 assertThat(fakeListener.getSentenceSuggestionsResults[0]).hasLength(1) 515 assertThat(fakeListener.getSentenceSuggestionsResults[0]!![0]).isNull() 516 } 517 } 518 519 @Test 520 fun respectSentenceBoundary() { 521 // Set up two rules: 522 // - Matches the sentence "Preceding text?" and marks it as grammar error. 523 // - Matches the sentence "match?" and marks it as misspelled. 524 val configuration = MockSpellCheckerConfiguration.newBuilder() 525 .setMatchSentence(true) 526 .addSuggestionRules( 527 MockSpellCheckerProto.SuggestionRule.newBuilder() 528 .setMatch("Preceding text?") 529 .addSuggestions("suggestion.") 530 .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR) 531 ).addSuggestionRules( 532 MockSpellCheckerProto.SuggestionRule.newBuilder() 533 .setMatch("match?") 534 .addSuggestions("suggestion.") 535 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 536 ).build() 537 MockImeSession.create(context).use { session -> 538 MockSpellCheckerClient.create(context, configuration).use { client -> 539 val (_, editText) = startTestActivity() 540 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 541 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 542 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 543 session.callCommitText("Preceding text", 1) 544 session.callCommitText("?", 1) 545 waitOnMainUntil({ 546 findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null 547 }, TIMEOUT) 548 // The next spell check only contains the text after "Preceding text?". According 549 // to our configuration, the sentence "match?" will be marked as FLAG_MISSPELLED. 550 session.callCommitText("match", 1) 551 session.callCommitText("?", 1) 552 waitOnMainUntil({ 553 findSuggestionSpanWithFlags(editText, FLAG_MISSPELLED) != null 554 }, TIMEOUT) 555 } 556 } 557 } 558 559 @Test 560 fun removePreviousSuggestion() { 561 // Set up two rules: 562 // - Matches the sentence "Wrong context word?" and marks "word" as grammar error. 563 // - Matches the sentence "Correct context word?" and marks "word" as in-vocabulary. 564 val configuration = MockSpellCheckerConfiguration.newBuilder() 565 .setMatchSentence(true) 566 .addSuggestionRules( 567 MockSpellCheckerProto.SuggestionRule.newBuilder() 568 .setMatch("Wrong context word?") 569 .addSuggestions("suggestion") 570 .setStartOffset(14) 571 .setLength(4) 572 .setAttributes(RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR) 573 ).addSuggestionRules( 574 MockSpellCheckerProto.SuggestionRule.newBuilder() 575 .setMatch("Correct context word?") 576 .setStartOffset(16) 577 .setLength(4) 578 .setAttributes(RESULT_ATTR_IN_THE_DICTIONARY) 579 ).build() 580 MockImeSession.create(context).use { session -> 581 MockSpellCheckerClient.create(context, configuration).use { client -> 582 val (_, editText) = startTestActivity() 583 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 584 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 585 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 586 session.callCommitText("Wrong context word", 1) 587 session.callCommitText("?", 1) 588 waitOnMainUntil({ 589 findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) != null 590 }, TIMEOUT) 591 // Change "Wrong" to "Correct" and then trigger spell check. 592 session.callSetSelection(0, 5) // Select "Wrong" 593 session.callCommitText("Correct", 1) 594 session.callPerformSpellCheck() 595 waitOnMainUntil({ 596 findSuggestionSpanWithFlags(editText, FLAG_GRAMMAR_ERROR) == null 597 }, TIMEOUT) 598 } 599 } 600 } 601 602 @Test 603 fun ignoreInvalidSuggestions() { 604 // Set up a wrong rule: 605 // - Matches the sentence "Context word" and marks "word" as grammar error. 606 val configuration = MockSpellCheckerConfiguration.newBuilder() 607 .setMatchSentence(true) 608 .addSuggestionRules( 609 MockSpellCheckerProto.SuggestionRule.newBuilder() 610 .setMatch("Context word") 611 .addSuggestions("suggestion") 612 .setStartOffset(8) 613 .setLength(5) // Should be 4 614 .setAttributes(RESULT_ATTR_LOOKS_LIKE_TYPO) 615 ).build() 616 MockImeSession.create(context).use { session -> 617 MockSpellCheckerClient.create(context, configuration).use { client -> 618 val (_, editText) = startTestActivity() 619 CtsTouchUtils.emulateTapOnViewCenter(instrumentation, null, editText) 620 waitOnMainUntil({ editText.hasFocus() }, TIMEOUT) 621 InputMethodVisibilityVerifier.expectImeVisible(TIMEOUT) 622 session.callCommitText("Context word", 1) 623 session.callPerformSpellCheck() 624 try { 625 waitOnMainUntil({ 626 findSuggestionSpanWithFlags(editText, RESULT_ATTR_LOOKS_LIKE_TYPO) != null 627 }, TIMEOUT) 628 fail("Invalid suggestions should be ignored") 629 } catch (e: TimeoutException) { 630 // Expected. 631 } 632 } 633 } 634 } 635 636 private fun findSuggestionSpanWithFlags(editText: EditText, flags: Int): SuggestionSpan? = 637 getSuggestionSpans(editText).find { (it.flags and flags) == flags } 638 639 private fun getSuggestionSpans(editText: EditText): Array<SuggestionSpan> { 640 val editable = editText.text 641 val spans = editable.getSpans(0, editable.length, SuggestionSpan::class.java) 642 return spans 643 } 644 645 private fun emulateTapAtOffset(editText: EditText, offset: Int) { 646 var x = 0 647 var y = 0 648 runOnMainSync { 649 x = editText.layout.getPrimaryHorizontal(offset).toInt() 650 val line = editText.layout.getLineForOffset(offset) 651 y = (editText.layout.getLineTop(line) + editText.layout.getLineBottom(line)) / 2 652 } 653 CtsTouchUtils.emulateTapOnView(instrumentation, null, editText, x, y) 654 } 655 656 @UiThread 657 private fun isCursorInside(editText: EditText, start: Int, end: Int): Boolean = 658 start <= editText.selectionStart && editText.selectionEnd <= end 659 660 private fun startTestActivity(): Pair<TestActivity, EditText> { 661 var editText: EditText? = null 662 val activity = TestActivity.startSync { activity: TestActivity? -> 663 val layout = LinearLayout(activity) 664 editText = EditText(activity) 665 layout.addView(editText, LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)) 666 layout 667 } 668 return Pair(activity, editText!!) 669 } 670 671 private fun getCurrentInputMethodInfo(): InputMethodInfo { 672 val curId = Settings.Secure.getString(context.getContentResolver(), 673 Settings.Secure.DEFAULT_INPUT_METHOD) 674 val imm = context.getSystemService(InputMethodManager::class.java) 675 val info = imm?.inputMethodList?.find { it.id == curId } 676 assertThat(info).isNotNull() 677 return info!! 678 } 679 680 private fun getSentenceSuggestions( 681 session: SpellCheckerSession, 682 listener: FakeSpellCheckerSessionListener, 683 text: String 684 ): Array<SentenceSuggestionsInfo>? { 685 val prevSize = listener.getSentenceSuggestionsResults.size 686 session.getSentenceSuggestions(arrayOf(TextInfo(text)), SUGGESTIONS_MAX_SIZE) 687 waitOnMainUntil({ 688 listener.getSentenceSuggestionsResults.size == prevSize + 1 689 }, TIMEOUT) 690 return listener.getSentenceSuggestionsResults[prevSize] 691 } 692 693 private inner class ImeSession(val imeId: String) : AutoCloseable { 694 695 init { 696 SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime reset") 697 SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime enable $imeId") 698 SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime set $imeId") 699 PollingCheck.check("Make sure that $imeId is selected", TIMEOUT) { 700 getCurrentInputMethodInfo().id == imeId 701 } 702 } 703 704 override fun close() { 705 SystemUtil.runCommandAndPrintOnLogcat(TAG, "ime reset") 706 } 707 } 708 709 private class FakeSpellCheckerSessionListener : 710 SpellCheckerSession.SpellCheckerSessionListener { 711 val getSentenceSuggestionsResults = ArrayList<Array<SentenceSuggestionsInfo>?>() 712 val getSentenceSuggestionsCallingThreads = ArrayList<Thread>() 713 714 override fun onGetSuggestions(results: Array<SuggestionsInfo>?) { 715 fail("Not expected") 716 } 717 718 override fun onGetSentenceSuggestions(results: Array<SentenceSuggestionsInfo>?) { 719 getSentenceSuggestionsResults.add(results) 720 getSentenceSuggestionsCallingThreads.add(Thread.currentThread()) 721 } 722 } 723 724 private class FakeExecutor : Executor { 725 @get:Synchronized 726 val runnables = ArrayList<Runnable>() 727 728 @Synchronized 729 override fun execute(r: Runnable) { 730 runnables.add(r) 731 } 732 } 733 } 734