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