1 /* 2 * 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 17 package android.view.inputmethod.cts; 18 19 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE; 20 import static android.view.inputmethod.InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS; 21 22 import static com.android.cts.mocka11yime.MockA11yImeEventStreamUtils.editorMatcherForA11yIme; 23 import static com.android.cts.mocka11yime.MockA11yImeEventStreamUtils.expectA11yImeCommand; 24 import static com.android.cts.mocka11yime.MockA11yImeEventStreamUtils.expectA11yImeEvent; 25 import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; 26 import static com.android.cts.mockime.ImeEventStreamTestUtils.eventMatcher; 27 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput; 28 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand; 29 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; 30 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent; 31 import static com.android.cts.mockime.ImeEventStreamTestUtils.withDescription; 32 33 import static com.google.common.truth.Truth.assertThat; 34 35 import static org.junit.Assert.assertEquals; 36 import static org.junit.Assert.assertFalse; 37 import static org.junit.Assert.assertNotNull; 38 import static org.junit.Assert.assertNull; 39 import static org.junit.Assert.assertTrue; 40 import static org.junit.Assert.fail; 41 42 import android.app.Instrumentation; 43 import android.app.UiAutomation; 44 import android.content.ClipDescription; 45 import android.graphics.PointF; 46 import android.graphics.RectF; 47 import android.net.Uri; 48 import android.os.Bundle; 49 import android.os.CancellationSignal; 50 import android.os.Handler; 51 import android.os.Process; 52 import android.os.SystemClock; 53 import android.platform.test.annotations.AppModeSdkSandbox; 54 import android.text.Annotation; 55 import android.text.SpannableStringBuilder; 56 import android.text.Spanned; 57 import android.text.TextUtils; 58 import android.util.Pair; 59 import android.view.KeyEvent; 60 import android.view.inputmethod.CompletionInfo; 61 import android.view.inputmethod.CorrectionInfo; 62 import android.view.inputmethod.DeleteGesture; 63 import android.view.inputmethod.DeleteRangeGesture; 64 import android.view.inputmethod.EditorInfo; 65 import android.view.inputmethod.ExtractedText; 66 import android.view.inputmethod.ExtractedTextRequest; 67 import android.view.inputmethod.HandwritingGesture; 68 import android.view.inputmethod.InputConnection; 69 import android.view.inputmethod.InputConnectionWrapper; 70 import android.view.inputmethod.InputContentInfo; 71 import android.view.inputmethod.InsertGesture; 72 import android.view.inputmethod.InsertModeGesture; 73 import android.view.inputmethod.JoinOrSplitGesture; 74 import android.view.inputmethod.PreviewableHandwritingGesture; 75 import android.view.inputmethod.RemoveSpaceGesture; 76 import android.view.inputmethod.SelectGesture; 77 import android.view.inputmethod.SelectRangeGesture; 78 import android.view.inputmethod.SurroundingText; 79 import android.view.inputmethod.TextAttribute; 80 import android.view.inputmethod.TextBoundsInfo; 81 import android.view.inputmethod.TextBoundsInfoResult; 82 import android.view.inputmethod.TextSnapshot; 83 import android.view.inputmethod.cts.util.EndToEndImeTestBase; 84 import android.view.inputmethod.cts.util.MockTestActivityUtil; 85 import android.view.inputmethod.cts.util.TestActivity; 86 import android.widget.EditText; 87 import android.widget.LinearLayout; 88 89 import androidx.annotation.AnyThread; 90 import androidx.annotation.NonNull; 91 import androidx.annotation.Nullable; 92 import androidx.test.ext.junit.runners.AndroidJUnit4; 93 import androidx.test.filters.FlakyTest; 94 import androidx.test.filters.LargeTest; 95 import androidx.test.platform.app.InstrumentationRegistry; 96 97 import com.android.compatibility.common.util.ApiTest; 98 import com.android.cts.inputmethod.LegacyImeClientTestUtils; 99 import com.android.cts.mocka11yime.MockA11yImeEventStream; 100 import com.android.cts.mocka11yime.MockA11yImeSession; 101 import com.android.cts.mocka11yime.MockA11yImeSettings; 102 import com.android.cts.mockime.ImeCommand; 103 import com.android.cts.mockime.ImeEvent; 104 import com.android.cts.mockime.ImeEventStream; 105 import com.android.cts.mockime.ImeEventStreamTestUtils.DescribedPredicate; 106 import com.android.cts.mockime.ImeSettings; 107 import com.android.cts.mockime.MockImeSession; 108 109 import com.google.common.truth.Correspondence; 110 111 import org.junit.Rule; 112 import org.junit.Test; 113 import org.junit.rules.ErrorCollector; 114 import org.junit.runner.RunWith; 115 116 import java.util.ArrayList; 117 import java.util.Arrays; 118 import java.util.Collections; 119 import java.util.List; 120 import java.util.Objects; 121 import java.util.concurrent.CopyOnWriteArrayList; 122 import java.util.concurrent.CountDownLatch; 123 import java.util.concurrent.Executor; 124 import java.util.concurrent.TimeUnit; 125 import java.util.concurrent.atomic.AtomicInteger; 126 import java.util.concurrent.atomic.AtomicReference; 127 import java.util.function.Consumer; 128 import java.util.function.Function; 129 import java.util.function.IntConsumer; 130 131 /** 132 * Provides basic tests for APIs defined in {@link InputConnection}. 133 * 134 * <p>TODO(b/193535269): Clean up boilerplate code around mocking InputConnection.</p> 135 */ 136 @LargeTest 137 @RunWith(AndroidJUnit4.class) 138 @AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") 139 public class InputConnectionEndToEndTest extends EndToEndImeTestBase { 140 private static final long TIME_SLICE = TimeUnit.MILLISECONDS.toMillis(125); 141 private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); 142 private static final long EXPECTED_NOT_CALLED_TIMEOUT = TimeUnit.SECONDS.toMillis(1); 143 private static final long LONG_TIMEOUT = TimeUnit.SECONDS.toMillis(30); 144 private static final long IMMEDIATE_TIMEOUT_NANO = TimeUnit.MILLISECONDS.toNanos(200); 145 146 @Rule 147 public final ErrorCollector mErrorCollector = new ErrorCollector(); 148 149 /** 150 * A utility method to verify a method is called within a certain timeout period then block 151 * it by {@link BlockingMethodVerifier#close()} is called. 152 */ 153 private static final class BlockingMethodVerifier implements AutoCloseable { 154 private final CountDownLatch mWaitUntilMethodCalled = new CountDownLatch(1); 155 private final CountDownLatch mWaitUntilTestFinished = new CountDownLatch(1); 156 157 /** 158 * Used to notify when a method to be tested is called. 159 */ onMethodCalled()160 void onMethodCalled() { 161 try { 162 mWaitUntilMethodCalled.countDown(); 163 mWaitUntilTestFinished.await(); 164 } catch (InterruptedException e) { 165 } 166 } 167 168 /** 169 * Ensures that the method to be tested is called within {@param timeout}. 170 * 171 * @param message Message to be shown when the method is not called despite the expectation. 172 * @param timeout Timeout in milliseconds. 173 */ expectMethodCalled(@onNull String message, long timeout)174 void expectMethodCalled(@NonNull String message, long timeout) { 175 try { 176 assertTrue(message, mWaitUntilMethodCalled.await(timeout, TimeUnit.MILLISECONDS)); 177 } catch (InterruptedException e) { 178 fail(message + e); 179 } 180 } 181 182 /** 183 * Unblock the method to be tested to avoid the test from being blocked forever. 184 */ 185 @Override close()186 public void close() throws Exception { 187 mWaitUntilTestFinished.countDown(); 188 } 189 } 190 191 /** 192 * A utility method to verify that a method is called with a certain set of parameters. 193 */ 194 private static final class MethodCallVerifier { 195 private final AtomicReference<Bundle> mArgs = new AtomicReference<>(); 196 private final AtomicInteger mCallCount = new AtomicInteger(0); 197 198 @AnyThread reset()199 void reset() { 200 mArgs.set(null); 201 mCallCount.set(0); 202 } 203 204 /** 205 * Used to record when a method to be tested is called. 206 * 207 * @param argumentsRecorder a {@link Consumer} to capture method parameters. 208 */ onMethodCalled(@onNull Consumer<Bundle> argumentsRecorder)209 void onMethodCalled(@NonNull Consumer<Bundle> argumentsRecorder) { 210 final Bundle bundle = new Bundle(); 211 argumentsRecorder.accept(bundle); 212 mArgs.set(bundle); 213 mCallCount.incrementAndGet(); 214 } 215 216 /** 217 * Used to assert captured parameters later. 218 * 219 * @param argumentsVerifier a {@link Consumer} to verify method arguments. 220 * @throws AssertionError when {@link #onMethodCalled(Consumer)} was not called only once. 221 */ assertCalledOnce(@onNull Consumer<Bundle> argumentsVerifier)222 void assertCalledOnce(@NonNull Consumer<Bundle> argumentsVerifier) { 223 assertEquals(1, mCallCount.get()); 224 final Bundle bundle = mArgs.get(); 225 assertNotNull(bundle); 226 argumentsVerifier.accept(bundle); 227 } 228 229 /** 230 * Ensures that the method to be tested is called within {@param timeout}. 231 * 232 * @param argumentsVerifier a {@link Consumer} to verify method arguments. 233 * @param timeout timeout in millisecond 234 * @throws AssertionError when {@link #onMethodCalled(Consumer)} was not called only once. 235 */ expectCalledOnce(@onNull Consumer<Bundle> argumentsVerifier, long timeout)236 void expectCalledOnce(@NonNull Consumer<Bundle> argumentsVerifier, long timeout) { 237 // Currently using busy-wait because CountDownLatch is not compatible with reset(). 238 // TODO: Consider using other more efficient operation. 239 long remainingTime = timeout; 240 while (mCallCount.get() == 0) { 241 if (remainingTime < 0) { 242 fail("The method must be called, but was not within" + timeout + " msec."); 243 } 244 SystemClock.sleep(TIME_SLICE); 245 remainingTime -= TIME_SLICE; 246 } 247 assertEquals(1, mCallCount.get()); 248 final Bundle bundle = mArgs.get(); 249 assertNotNull(bundle); 250 argumentsVerifier.accept(bundle); 251 } 252 253 /** 254 * Used to assert that {@link #onMethodCalled(Consumer)} was never called. 255 * 256 * @param callCountVerificationMessage A message to be used when the assertion fails. 257 */ assertNotCalled(@ullable String callCountVerificationMessage)258 void assertNotCalled(@Nullable String callCountVerificationMessage) { 259 if (callCountVerificationMessage != null) { 260 assertEquals(callCountVerificationMessage, 0, mCallCount.get()); 261 } else { 262 assertEquals(0, mCallCount.get()); 263 } 264 } 265 266 /** 267 * Ensures that the method to be tested is not called within {@param timeout}. 268 * 269 * @param callCountVerificationMessage A message to be used when the assertion fails. 270 * @param timeout timeout in millisecond 271 */ expectNotCalled(@ullable String callCountVerificationMessage, long timeout)272 void expectNotCalled(@Nullable String callCountVerificationMessage, long timeout) { 273 // Currently using busy-wait because CountDownLatch is not compatible with reset(). 274 // TODO: Consider using other more efficient operation. 275 long remainingTime = timeout; 276 while (true) { 277 if (mCallCount.get() != 0) { 278 fail("The method must not be called. params=" + evaluateBundle(mArgs.get())); 279 } 280 if (remainingTime < 0) { 281 break; // This is indeed an expected scenario, not an error. 282 } 283 SystemClock.sleep(TIME_SLICE); 284 remainingTime -= TIME_SLICE; 285 } 286 if (callCountVerificationMessage != null) { 287 assertEquals(callCountVerificationMessage, 0, mCallCount.get()); 288 } else { 289 assertEquals(0, mCallCount.get()); 290 } 291 } 292 293 /** 294 * Recursively evaluate {@link Bundle} so that {@link Bundle#toString()} can print all the 295 * nested {@link Bundle} objects. 296 * 297 * @param bundle {@link Bundle} to recursively evaluate. 298 * @return the {@code bundle} object passed. 299 */ 300 @Nullable evaluateBundle(@ullable Bundle bundle)301 private static Bundle evaluateBundle(@Nullable Bundle bundle) { 302 if (bundle != null) { 303 for (String key : bundle.keySet()) { 304 final Object value = bundle.get(key); 305 if (value instanceof Bundle) { 306 evaluateBundle((Bundle) value); 307 } 308 } 309 } 310 return bundle; 311 } 312 } 313 314 /** 315 * A test procedure definition for 316 * {@link #testInputConnection(Function, TestProcedure, AutoCloseable)}. 317 */ 318 @FunctionalInterface 319 interface TestProcedure { 320 /** 321 * The test body of {@link #testInputConnection(Function, TestProcedure, AutoCloseable)} 322 * 323 * @param session {@link MockImeSession} to be used during this test. 324 * @param stream {@link ImeEventStream} associated with {@code session}. 325 */ run(@onNull MockImeSession session, @NonNull ImeEventStream stream)326 void run(@NonNull MockImeSession session, @NonNull ImeEventStream stream) throws Exception; 327 } 328 329 /** 330 * A test procedure definition for 331 * {@link #testA11yInputConnection(Function, TestProcedureForAccessibilityIme)} 332 */ 333 @FunctionalInterface 334 interface TestProcedureForAccessibilityIme { 335 /** 336 * The test body of {@link #testInputConnection(Function, TestProcedure, AutoCloseable)} 337 * 338 * @param a11yImeSession {@link MockA11yImeSession} to be used during this test. 339 * @param stream {@link MockA11yImeEventStream} associated with {@code session}. 340 */ run(@onNull MockA11yImeSession a11yImeSession, @NonNull MockA11yImeEventStream stream)341 void run(@NonNull MockA11yImeSession a11yImeSession, @NonNull MockA11yImeEventStream stream) 342 throws Exception; 343 } 344 345 /** 346 * A test procedure definition for 347 * {@link #testInputConnection(Function, TestProcedureForMixedImes, AutoCloseable)}. 348 */ 349 @FunctionalInterface 350 interface TestProcedureForMixedImes { 351 /** 352 * The test body of {@link #testInputConnection(Function, TestProcedure, AutoCloseable)} 353 * 354 * @param imeSession {@link MockImeSession} to be used during this test. 355 * @param imeStream {@link ImeEventStream} associated with {@code session}. 356 * @param a11yImeSession {@link MockA11yImeSession} to be used during this test. 357 * @param a11yImeStream {@link MockA11yImeEventStream} associated with {@code session}. 358 */ run(@onNull MockImeSession imeSession, @NonNull ImeEventStream imeStream, @NonNull MockA11yImeSession a11yImeSession, @NonNull MockA11yImeEventStream a11yImeStream)359 void run(@NonNull MockImeSession imeSession, @NonNull ImeEventStream imeStream, 360 @NonNull MockA11yImeSession a11yImeSession, 361 @NonNull MockA11yImeEventStream a11yImeStream) 362 throws Exception; 363 } 364 365 /** 366 * Tries to trigger {@link com.android.cts.mockime.MockIme#onUnbindInput()} by showing another 367 * Activity in a different process. 368 */ triggerUnbindInput()369 private void triggerUnbindInput() { 370 final boolean isInstant = InstrumentationRegistry.getInstrumentation().getTargetContext() 371 .getPackageManager().isInstantApp(); 372 MockTestActivityUtil.launchSync(isInstant, TIMEOUT); 373 } 374 375 /** 376 * A utility method to run a unit test for {@link InputConnection}. 377 * 378 * <p>This utility method enables you to avoid boilerplate code when writing unit tests for 379 * {@link InputConnection}.</p> 380 * 381 * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the 382 * original {@link InputConnection}. 383 * @param testProcedure Test body. 384 */ testInputConnection( Function<InputConnection, InputConnection> inputConnectionWrapperProvider, TestProcedure testProcedure)385 private void testInputConnection( 386 Function<InputConnection, InputConnection> inputConnectionWrapperProvider, 387 TestProcedure testProcedure) throws Exception { 388 testInputConnection(inputConnectionWrapperProvider, testProcedure, null); 389 } 390 391 /** 392 * A utility method to run a unit test for {@link InputConnection}. 393 * 394 * <p>This utility method enables you to avoid boilerplate code when writing unit tests for 395 * {@link InputConnection}.</p> 396 * 397 * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the 398 * original {@link InputConnection}. 399 * @param testProcedure Test body. 400 */ testInputConnection( Function<InputConnection, InputConnection> inputConnectionWrapperProvider, TestProcedureForMixedImes testProcedure)401 private void testInputConnection( 402 Function<InputConnection, InputConnection> inputConnectionWrapperProvider, 403 TestProcedureForMixedImes testProcedure) throws Exception { 404 testInputConnection(inputConnectionWrapperProvider, testProcedure, null); 405 } 406 407 /** 408 * A utility method to run a unit test for {@link InputConnection} with 409 * {@link android.accessibilityservice.InputMethod}. 410 * 411 * <p>This utility method enables you to avoid boilerplate code when writing unit tests for 412 * {@link InputConnection}.</p> 413 * 414 * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the 415 * original {@link InputConnection}. 416 * @param testProcedure Test body. 417 */ testA11yInputConnection( Function<InputConnection, InputConnection> inputConnectionWrapperProvider, TestProcedureForAccessibilityIme testProcedure)418 private void testA11yInputConnection( 419 Function<InputConnection, InputConnection> inputConnectionWrapperProvider, 420 TestProcedureForAccessibilityIme testProcedure) throws Exception { 421 testInputConnection(inputConnectionWrapperProvider, 422 (imeSession, imeStream, a11ySession, a11yStream) 423 -> testProcedure.run(a11ySession, a11yStream), null); 424 } 425 426 /** 427 * A utility method to run a unit test for {@link InputConnection} with 428 * {@link android.accessibilityservice.InputMethod}. 429 * 430 * <p>This utility method enables you to avoid boilerplate code when writing unit tests for 431 * {@link InputConnection}.</p> 432 * 433 * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the 434 * original {@link InputConnection}. 435 * @param testProcedure Test body. 436 * @param closeable {@link AutoCloseable} object to be cleaned up after running test. 437 **/ testA11yInputConnection( Function<InputConnection, InputConnection> inputConnectionWrapperProvider, TestProcedureForAccessibilityIme testProcedure, @Nullable AutoCloseable closeable)438 private void testA11yInputConnection( 439 Function<InputConnection, InputConnection> inputConnectionWrapperProvider, 440 TestProcedureForAccessibilityIme testProcedure, 441 @Nullable AutoCloseable closeable) throws Exception { 442 testInputConnection(inputConnectionWrapperProvider, 443 (imeSession, imeStream, a11ySession, a11yStream) 444 -> testProcedure.run(a11ySession, a11yStream), closeable); 445 } 446 447 /** 448 * A utility method to run a unit test for {@link InputConnection} that is as-if built with 449 * {@link android.os.Build.VERSION_CODES#CUPCAKE} SDK. 450 * 451 * <p>This helps you to test the situation where IMEs' calling newly added 452 * {@link InputConnection} APIs would be fallen back to its default interface method or could be 453 * causing {@link java.lang.AbstractMethodError} unless specially handled. 454 * 455 * @param testProcedure Test body. 456 */ testMinimallyImplementedInputConnection(TestProcedure testProcedure)457 private void testMinimallyImplementedInputConnection(TestProcedure testProcedure) 458 throws Exception { 459 testInputConnection( 460 ic -> LegacyImeClientTestUtils.createMinimallyImplementedNoOpInputConnection(), 461 testProcedure, null); 462 } 463 464 /** 465 * A utility method to run a unit test for {@link InputConnection} that is as-if built with 466 * {@link android.os.Build.VERSION_CODES#CUPCAKE} SDK. 467 * 468 * <p>This helps you to test the situation where IMEs' calling newly added 469 * {@link InputConnection} APIs would be fallen back to its default interface method or could be 470 * causing {@link java.lang.AbstractMethodError} unless specially handled. 471 * 472 * @param testProcedure Test body. 473 */ testMinimallyImplementedInputConnectionForA11y( TestProcedureForAccessibilityIme testProcedure)474 private void testMinimallyImplementedInputConnectionForA11y( 475 TestProcedureForAccessibilityIme testProcedure) 476 throws Exception { 477 testA11yInputConnection( 478 ic -> LegacyImeClientTestUtils.createMinimallyImplementedNoOpInputConnection(), 479 testProcedure); 480 } 481 482 /** 483 * A utility method to run a unit test for {@link InputConnection}. 484 * 485 * <p>This utility method enables you to avoid boilerplate code when writing unit tests for 486 * {@link InputConnection}.</p> 487 * 488 * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the 489 * original {@link InputConnection}. 490 * @param testProcedure Test body. 491 * @param closeable {@link AutoCloseable} object to be cleaned up after running test. 492 */ testInputConnection( Function<InputConnection, InputConnection> inputConnectionWrapperProvider, TestProcedure testProcedure, @Nullable AutoCloseable closeable)493 private void testInputConnection( 494 Function<InputConnection, InputConnection> inputConnectionWrapperProvider, 495 TestProcedure testProcedure, @Nullable AutoCloseable closeable) throws Exception { 496 try (AutoCloseable closeableHolder = closeable; 497 MockImeSession imeSession = MockImeSession.create( 498 InstrumentationRegistry.getInstrumentation().getContext(), 499 InstrumentationRegistry.getInstrumentation().getUiAutomation(), 500 new ImeSettings.Builder())) { 501 final ImeEventStream stream = imeSession.openEventStream(); 502 503 final String marker = getTestMarker(); 504 TestActivity.startSync(activity -> { 505 final LinearLayout layout = new LinearLayout(activity); 506 layout.setOrientation(LinearLayout.VERTICAL); 507 508 // Just to be conservative, we explicitly check MockImeSession#isActive() here when 509 // injecting our custom InputConnection implementation. 510 final EditText editText = new EditText(activity) { 511 @Override 512 public boolean onCheckIsTextEditor() { 513 return imeSession.isActive(); 514 } 515 516 @Override 517 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 518 if (imeSession.isActive()) { 519 final InputConnection ic = super.onCreateInputConnection(outAttrs); 520 return inputConnectionWrapperProvider.apply(ic); 521 } 522 return null; 523 } 524 }; 525 526 editText.setPrivateImeOptions(marker); 527 editText.setHint("editText"); 528 editText.requestFocus(); 529 530 layout.addView(editText); 531 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 532 return layout; 533 }); 534 535 // Wait until the MockIme gets bound to the TestActivity. 536 expectBindInput(stream, Process.myPid(), TIMEOUT); 537 538 // Wait until "onStartInput" gets called for the EditText. 539 expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); 540 541 testProcedure.run(imeSession, stream); 542 } 543 } 544 545 /** 546 * A utility method to run a unit test for {@link InputConnection}. 547 * 548 * <p>This utility method enables you to avoid boilerplate code when writing unit tests for 549 * {@link InputConnection}.</p> 550 * 551 * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the 552 * original {@link InputConnection}. 553 * @param testProcedure Test body. 554 * @param closeable {@link AutoCloseable} object to be cleaned up after running test. 555 */ testInputConnection( Function<InputConnection, InputConnection> inputConnectionWrapperProvider, TestProcedureForMixedImes testProcedure, @Nullable AutoCloseable closeable)556 private void testInputConnection( 557 Function<InputConnection, InputConnection> inputConnectionWrapperProvider, 558 TestProcedureForMixedImes testProcedure, 559 @Nullable AutoCloseable closeable) throws Exception { 560 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 561 final UiAutomation uiAutomation = instrumentation.getUiAutomation( 562 UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES); 563 try (AutoCloseable closeableHolder = closeable; 564 MockImeSession imeSession = MockImeSession.create(instrumentation.getContext(), 565 uiAutomation, new ImeSettings.Builder())) { 566 final ImeEventStream imeStream = imeSession.openEventStream(); 567 568 final String marker = getTestMarker(); 569 570 TestActivity.startSync(activity -> { 571 final LinearLayout layout = new LinearLayout(activity); 572 layout.setOrientation(LinearLayout.VERTICAL); 573 574 // Just to be conservative, we explicitly check MockImeSession#isActive() here when 575 // injecting our custom InputConnection implementation. 576 final EditText editText = new EditText(activity) { 577 @Override 578 public boolean onCheckIsTextEditor() { 579 return imeSession.isActive(); 580 } 581 582 @Override 583 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 584 if (imeSession.isActive()) { 585 final InputConnection ic = super.onCreateInputConnection(outAttrs); 586 return inputConnectionWrapperProvider.apply(ic); 587 } 588 return null; 589 } 590 }; 591 592 editText.setPrivateImeOptions(marker); 593 editText.setHint("editText"); 594 editText.requestFocus(); 595 596 layout.addView(editText); 597 activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE); 598 return layout; 599 }); 600 601 // Wait until "onStartInput" gets called for the EditText. 602 expectEvent(imeStream, editorMatcher("onStartInput", marker), TIMEOUT); 603 604 try (MockA11yImeSession a11yImeSession = MockA11yImeSession.create( 605 instrumentation.getContext(), uiAutomation, MockA11yImeSettings.DEFAULT, 606 TIMEOUT)) { 607 final MockA11yImeEventStream a11yImeEventStream = a11yImeSession.openEventStream(); 608 609 // Wait until "onStartInput" gets called for the EditText. 610 expectA11yImeEvent(a11yImeEventStream, 611 editorMatcherForA11yIme("onStartInput", marker), TIMEOUT); 612 613 // Now everything is stable and ready to start testing. 614 testProcedure.run(imeSession, imeStream, a11yImeSession, a11yImeEventStream); 615 } 616 } 617 } 618 619 /** 620 * Ensures that {@code event}'s elapse time is less than the given threshold. 621 * 622 * @param event {@link ImeEvent} to be tested. 623 * @param elapseNanoTimeThreshold threshold in nano sec. 624 */ expectElapseTimeLessThan(@onNull ImeEvent event, long elapseNanoTimeThreshold)625 private static void expectElapseTimeLessThan(@NonNull ImeEvent event, 626 long elapseNanoTimeThreshold) { 627 final long elapseNanoTime = event.getExitTimestamp() - event.getEnterTimestamp(); 628 if (elapseNanoTime > elapseNanoTimeThreshold) { 629 fail(event.getEventName() + " took " + elapseNanoTime + " nsec," 630 + " which must be less than" + elapseNanoTimeThreshold + " nsec."); 631 } 632 } 633 634 @Nullable createTestCharSequence(@ullable String text, @Nullable Annotation annotation)635 private static CharSequence createTestCharSequence(@Nullable String text, 636 @Nullable Annotation annotation) { 637 if (text == null) { 638 return null; 639 } 640 final SpannableStringBuilder sb = new SpannableStringBuilder(text); 641 if (annotation != null) { 642 sb.setSpan(annotation, 0, sb.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); 643 } 644 return sb; 645 } 646 assertEqualsForTestCharSequence(@ullable CharSequence expected, @Nullable CharSequence actual)647 private static void assertEqualsForTestCharSequence(@Nullable CharSequence expected, 648 @Nullable CharSequence actual) { 649 assertEquals(Objects.toString(expected), Objects.toString(actual)); 650 final Function<CharSequence, List<Annotation>> toAnnotations = cs -> { 651 if (cs instanceof Spanned) { 652 final Spanned spanned = (Spanned) cs; 653 return Arrays.asList(spanned.getSpans(0, cs.length(), Annotation.class)); 654 } 655 return Collections.emptyList(); 656 }; 657 assertThat(toAnnotations.apply(actual)).comparingElementsUsing(Correspondence.transforming( 658 (Annotation annotation) -> Pair.create(annotation.getKey(), annotation.getValue()), 659 (Annotation annotation) -> Pair.create(annotation.getKey(), annotation.getValue()), 660 "has the same Key/Value as")) 661 .containsExactlyElementsIn(toAnnotations.apply(expected)); 662 } 663 664 /** 665 * Test {@link InputConnection#getTextAfterCursor(int, int)} works as expected. 666 */ 667 @Test testGetTextAfterCursor()668 public void testGetTextAfterCursor() throws Exception { 669 final int expectedN = 3; 670 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 671 final CharSequence expectedResult = 672 createTestCharSequence("89", new Annotation("command", "getTextAfterCursor")); 673 674 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 675 676 final class Wrapper extends InputConnectionWrapper { 677 private Wrapper(InputConnection target) { 678 super(target, false); 679 } 680 681 @Override 682 public CharSequence getTextAfterCursor(int n, int flags) { 683 methodCallVerifier.onMethodCalled(args -> { 684 args.putInt("n", n); 685 args.putInt("flags", flags); 686 }); 687 return expectedResult; 688 } 689 } 690 691 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 692 final ImeCommand command = session.callGetTextAfterCursor(expectedN, expectedFlags); 693 final CharSequence result = 694 expectCommand(stream, command, TIMEOUT).getReturnCharSequenceValue(); 695 assertEqualsForTestCharSequence(expectedResult, result); 696 methodCallVerifier.assertCalledOnce(args -> { 697 assertEquals(expectedN, args.get("n")); 698 assertEquals(expectedFlags, args.get("flags")); 699 }); 700 }); 701 } 702 703 /** 704 * Test {@link InputConnection#getTextAfterCursor(int, int)} fails when a negative 705 * {@code length} is passed. See Bug 169114026 for background. 706 */ 707 @Test testGetTextAfterCursorFailWithNegativeLength()708 public void testGetTextAfterCursorFailWithNegativeLength() throws Exception { 709 final String unexpectedResult = "123"; 710 711 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 712 713 final class Wrapper extends InputConnectionWrapper { 714 private Wrapper(InputConnection target) { 715 super(target, false); 716 } 717 718 @Override 719 public CharSequence getTextAfterCursor(int n, int flags) { 720 methodCallVerifier.onMethodCalled(args -> { 721 args.putInt("n", n); 722 args.putInt("flags", flags); 723 }); 724 return unexpectedResult; 725 } 726 } 727 728 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 729 final ImeCommand command = session.callGetTextAfterCursor(-1, 0); 730 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 731 assertTrue("IC#getTextAfterCursor() returns null for a negative length.", 732 result.isNullReturnValue()); 733 methodCallVerifier.expectNotCalled( 734 "IC#getTextAfterCursor() will not be triggered with a negative length.", 735 EXPECTED_NOT_CALLED_TIMEOUT); 736 }); 737 } 738 739 /** 740 * Test {@link InputConnection#getTextAfterCursor(int, int)} fails after a system-defined 741 * time-out even if the target app does not respond. 742 */ 743 @Test testGetTextAfterCursorFailWithTimeout()744 public void testGetTextAfterCursorFailWithTimeout() throws Exception { 745 final int expectedN = 3; 746 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 747 final String unexpectedResult = "89"; 748 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 749 750 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 751 752 final class Wrapper extends InputConnectionWrapper { 753 private Wrapper(InputConnection target) { 754 super(target, false); 755 } 756 757 @Override 758 public CharSequence getTextAfterCursor(int n, int flags) { 759 methodCallVerifier.onMethodCalled(args -> { 760 args.putInt("n", n); 761 args.putInt("flags", flags); 762 }); 763 blocker.onMethodCalled(); 764 return unexpectedResult; 765 } 766 } 767 768 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 769 final ImeCommand command = session.callGetTextAfterCursor(expectedN, expectedFlags); 770 blocker.expectMethodCalled("IC#getTextAfterCursor() must be called back", TIMEOUT); 771 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 772 assertTrue("When timeout happens, IC#getTextAfterCursor() returns null", 773 result.isNullReturnValue()); 774 methodCallVerifier.assertCalledOnce(args -> { 775 assertEquals(expectedN, args.get("n")); 776 assertEquals(expectedFlags, args.get("flags")); 777 }); 778 }, blocker); 779 } 780 781 /** 782 * Test {@link InputConnection#getTextAfterCursor(int, int)} fail-fasts once unbindInput() is 783 * issued. 784 */ 785 @Test testGetTextAfterCursorFailFastAfterUnbindInput()786 public void testGetTextAfterCursorFailFastAfterUnbindInput() throws Exception { 787 final String unexpectedResult = "89"; 788 789 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 790 791 final class Wrapper extends InputConnectionWrapper { 792 private Wrapper(InputConnection target) { 793 super(target, false); 794 } 795 796 @Override 797 public CharSequence getTextAfterCursor(int n, int flags) { 798 methodCallVerifier.onMethodCalled(args -> { 799 args.putInt("n", n); 800 args.putInt("flags", flags); 801 }); 802 return unexpectedResult; 803 } 804 } 805 806 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 807 // Memorize the current InputConnection. 808 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 809 810 // Let unbindInput happen. 811 triggerUnbindInput(); 812 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 813 814 // Now IC#getTextAfterCursor() for the memorized IC should fail fast. 815 final ImeEvent result = expectCommand(stream, session.callGetTextAfterCursor( 816 unexpectedResult.length(), InputConnection.GET_TEXT_WITH_STYLES), TIMEOUT); 817 assertTrue("Once unbindInput() happened, IC#getTextAfterCursor() returns null", 818 result.isNullReturnValue()); 819 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 820 methodCallVerifier.assertNotCalled( 821 "Once unbindInput() happened, IC#getTextAfterCursor() fails fast."); 822 }); 823 } 824 825 /** 826 * Test {@link InputConnection#getTextBeforeCursor(int, int)} works as expected. 827 */ 828 @Test testGetTextBeforeCursor()829 public void testGetTextBeforeCursor() throws Exception { 830 final int expectedN = 3; 831 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 832 final CharSequence expectedResult = 833 createTestCharSequence("123", new Annotation("command", "getTextBeforeCursor")); 834 835 836 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 837 838 final class Wrapper extends InputConnectionWrapper { 839 private Wrapper(InputConnection target) { 840 super(target, false); 841 } 842 843 @Override 844 public CharSequence getTextBeforeCursor(int n, int flags) { 845 methodCallVerifier.onMethodCalled(args -> { 846 args.putInt("n", n); 847 args.putInt("flags", flags); 848 }); 849 return expectedResult; 850 } 851 } 852 853 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 854 final ImeCommand command = session.callGetTextBeforeCursor(expectedN, expectedFlags); 855 final CharSequence result = 856 expectCommand(stream, command, TIMEOUT).getReturnCharSequenceValue(); 857 assertEqualsForTestCharSequence(expectedResult, result); 858 methodCallVerifier.assertCalledOnce(args -> { 859 assertEquals(expectedN, args.get("n")); 860 assertEquals(expectedFlags, args.get("flags")); 861 }); 862 }); 863 } 864 865 /** 866 * Test {@link InputConnection#getTextBeforeCursor(int, int)} fails when a negative 867 * {@code length} is passed. See Bug 169114026 for background. 868 */ 869 @Test testGetTextBeforeCursorFailWithNegativeLength()870 public void testGetTextBeforeCursorFailWithNegativeLength() throws Exception { 871 final String unexpectedResult = "123"; 872 873 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 874 875 final class Wrapper extends InputConnectionWrapper { 876 private Wrapper(InputConnection target) { 877 super(target, false); 878 } 879 880 @Override 881 public CharSequence getTextBeforeCursor(int n, int flags) { 882 methodCallVerifier.onMethodCalled(args -> { 883 args.putInt("n", n); 884 args.putInt("flags", flags); 885 }); 886 return unexpectedResult; 887 } 888 } 889 890 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 891 final ImeCommand command = session.callGetTextBeforeCursor(-1, 0); 892 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 893 assertTrue("IC#getTextBeforeCursor() returns null for a negative length.", 894 result.isNullReturnValue()); 895 methodCallVerifier.expectNotCalled( 896 "IC#getTextBeforeCursor() will not be triggered with a negative length.", 897 EXPECTED_NOT_CALLED_TIMEOUT); 898 }); 899 } 900 901 /** 902 * Test {@link InputConnection#getTextBeforeCursor(int, int)} fails after a system-defined 903 * time-out even if the target app does not respond. 904 */ 905 @Test testGetTextBeforeCursorFailWithTimeout()906 public void testGetTextBeforeCursorFailWithTimeout() throws Exception { 907 final int expectedN = 3; 908 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 909 final String unexpectedResult = "123"; 910 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 911 912 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 913 914 final class Wrapper extends InputConnectionWrapper { 915 private Wrapper(InputConnection target) { 916 super(target, false); 917 } 918 919 @Override 920 public CharSequence getTextBeforeCursor(int n, int flags) { 921 methodCallVerifier.onMethodCalled(args -> { 922 args.putInt("n", n); 923 args.putInt("flags", flags); 924 }); 925 blocker.onMethodCalled(); 926 return unexpectedResult; 927 } 928 } 929 930 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 931 final ImeCommand command = session.callGetTextBeforeCursor(expectedN, expectedFlags); 932 blocker.expectMethodCalled("IC#getTextBeforeCursor() must be called back", TIMEOUT); 933 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 934 assertTrue("When timeout happens, IC#getTextBeforeCursor() returns null", 935 result.isNullReturnValue()); 936 methodCallVerifier.assertCalledOnce(args -> { 937 assertEquals(expectedN, args.get("n")); 938 assertEquals(expectedFlags, args.get("flags")); 939 }); 940 }, blocker); 941 } 942 943 /** 944 * Test {@link InputConnection#getTextBeforeCursor(int, int)} fail-fasts once unbindInput() is 945 * issued. 946 */ 947 @Test testGetTextBeforeCursorFailFastAfterUnbindInput()948 public void testGetTextBeforeCursorFailFastAfterUnbindInput() throws Exception { 949 final String unexpectedResult = "123"; 950 951 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 952 953 final class Wrapper extends InputConnectionWrapper { 954 private Wrapper(InputConnection target) { 955 super(target, false); 956 } 957 958 @Override 959 public CharSequence getTextBeforeCursor(int n, int flags) { 960 methodCallVerifier.onMethodCalled(args -> { 961 args.putInt("n", n); 962 args.putInt("flags", flags); 963 }); 964 return unexpectedResult; 965 } 966 } 967 968 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 969 // Memorize the current InputConnection. 970 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 971 972 // Let unbindInput happen. 973 triggerUnbindInput(); 974 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 975 976 // Now IC#getTextBeforeCursor() for the memorized IC should fail fast. 977 final ImeEvent result = expectCommand(stream, session.callGetTextBeforeCursor( 978 unexpectedResult.length(), InputConnection.GET_TEXT_WITH_STYLES), TIMEOUT); 979 assertTrue("Once unbindInput() happened, IC#getTextBeforeCursor() returns null", 980 result.isNullReturnValue()); 981 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 982 methodCallVerifier.assertNotCalled( 983 "Once unbindInput() happened, IC#getTextBeforeCursor() fails fast."); 984 }); 985 } 986 987 /** 988 * Test {@link InputConnection#getSelectedText(int)} works as expected. 989 */ 990 @Test testGetSelectedText()991 public void testGetSelectedText() throws Exception { 992 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 993 final CharSequence expectedResult = 994 createTestCharSequence("4567", new Annotation("command", "getSelectedText")); 995 996 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 997 998 final class Wrapper extends InputConnectionWrapper { 999 private Wrapper(InputConnection target) { 1000 super(target, false); 1001 } 1002 1003 @Override 1004 public CharSequence getSelectedText(int flags) { 1005 methodCallVerifier.onMethodCalled(args -> { 1006 args.putInt("flags", flags); 1007 }); 1008 assertEquals(expectedFlags, flags); 1009 return expectedResult; 1010 } 1011 } 1012 1013 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1014 final ImeCommand command = session.callGetSelectedText(expectedFlags); 1015 final CharSequence result = 1016 expectCommand(stream, command, TIMEOUT).getReturnCharSequenceValue(); 1017 assertEqualsForTestCharSequence(expectedResult, result); 1018 methodCallVerifier.assertCalledOnce(args -> { 1019 assertEquals(expectedFlags, args.get("flags")); 1020 }); 1021 }); 1022 } 1023 1024 /** 1025 * Test {@link InputConnection#getSelectedText(int)} fails after a system-defined time-out even 1026 * if the target app does not respond. 1027 */ 1028 @Test testGetSelectedTextFailWithTimeout()1029 public void testGetSelectedTextFailWithTimeout() throws Exception { 1030 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 1031 final String unexpectedResult = "4567"; 1032 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 1033 1034 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1035 1036 final class Wrapper extends InputConnectionWrapper { 1037 private Wrapper(InputConnection target) { 1038 super(target, false); 1039 } 1040 1041 @Override 1042 public CharSequence getSelectedText(int flags) { 1043 methodCallVerifier.onMethodCalled(args -> { 1044 args.putInt("flags", flags); 1045 }); 1046 blocker.onMethodCalled(); 1047 return unexpectedResult; 1048 } 1049 } 1050 1051 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1052 final ImeCommand command = 1053 session.callGetSelectedText(InputConnection.GET_TEXT_WITH_STYLES); 1054 blocker.expectMethodCalled("IC#getSelectedText() must be called back", TIMEOUT); 1055 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 1056 assertTrue("When timeout happens, IC#getSelectedText() returns null", 1057 result.isNullReturnValue()); 1058 methodCallVerifier.assertCalledOnce(args -> { 1059 assertEquals(expectedFlags, args.get("flags")); 1060 }); 1061 }, blocker); 1062 } 1063 1064 /** 1065 * Test {@link InputConnection#getSelectedText(int)} fail-fasts once unbindInput() is issued. 1066 */ 1067 @Test testGetSelectedTextFailFastAfterUnbindInput()1068 public void testGetSelectedTextFailFastAfterUnbindInput() throws Exception { 1069 final String unexpectedResult = "4567"; 1070 1071 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1072 1073 final class Wrapper extends InputConnectionWrapper { 1074 private Wrapper(InputConnection target) { 1075 super(target, false); 1076 } 1077 1078 @Override 1079 public CharSequence getSelectedText(int flags) { 1080 methodCallVerifier.onMethodCalled(args -> { 1081 args.putInt("flags", flags); 1082 }); 1083 return unexpectedResult; 1084 } 1085 } 1086 1087 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1088 // Memorize the current InputConnection. 1089 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 1090 1091 // Let unbindInput happen. 1092 triggerUnbindInput(); 1093 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 1094 1095 // Now IC#getSelectedText() for the memorized IC should fail fast. 1096 final ImeEvent result = expectCommand(stream, session.callGetSelectedText( 1097 InputConnection.GET_TEXT_WITH_STYLES), TIMEOUT); 1098 assertTrue("Once unbindInput() happened, IC#getSelectedText() returns null", 1099 result.isNullReturnValue()); 1100 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 1101 methodCallVerifier.assertNotCalled( 1102 "Once unbindInput() happened, IC#getSelectedText() fails fast."); 1103 }); 1104 } 1105 1106 /** 1107 * Verify that {@link InputConnection#getSelectedText(int)} returns {@code null} when the target 1108 * app does not implement it. This can happen if the app was built before 1109 * {@link android.os.Build.VERSION_CODES#GINGERBREAD}. 1110 */ 1111 @Test testGetSelectedTextFailWithMethodMissing()1112 public void testGetSelectedTextFailWithMethodMissing() throws Exception { 1113 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 1114 final ImeCommand command = session.callGetSelectedText(0); 1115 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 1116 assertTrue("Currently getSelectedText() returns null when the target app does not" 1117 + " implement it.", result.isNullReturnValue()); 1118 }); 1119 } 1120 1121 @Test 1122 @ApiTest(apis = {"android.view.inputmethod.InputConnection#requestTextBoundsInfo"}) testRequestTextBoundsInfo()1123 public void testRequestTextBoundsInfo() throws Exception { 1124 final var methodCallVerifier = new MethodCallVerifier(); 1125 final var tbiResult = new TextBoundsInfoResult(TextBoundsInfoResult.CODE_FAILED, null); 1126 1127 final class Wrapper extends InputConnectionWrapper { 1128 private Wrapper(InputConnection target) { 1129 super(target, false); 1130 } 1131 1132 @Override 1133 public void requestTextBoundsInfo(RectF rectF, Executor executor, 1134 Consumer<TextBoundsInfoResult> consumer) { 1135 mErrorCollector.checkSucceeds(() -> { 1136 methodCallVerifier.onMethodCalled(args -> { 1137 args.putParcelable("rectF", rectF); 1138 }); 1139 1140 var called = new boolean[1]; 1141 executor.execute(() -> { 1142 called[0] = true; 1143 consumer.accept(tbiResult); 1144 }); 1145 assertTrue("editor-side executor must be Runnable::run", called[0]); 1146 return null; 1147 }); 1148 } 1149 } 1150 1151 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1152 final RectF rectF = new RectF(1f, 2f, 3f, 4f); 1153 final ImeCommand command = session.callRequestTextBoundsInfo(rectF); 1154 methodCallVerifier.expectCalledOnce(args -> { 1155 assertEquals(rectF, args.getParcelable("rectF", RectF.class)); 1156 }, TIMEOUT); 1157 expectCommand(stream, command, TIMEOUT); 1158 var event = expectEvent(stream, onRequestTextBoundsInfoResultMatcher(command.getId()), 1159 TIMEOUT); 1160 var actualResultCode = event.getArguments().getInt("resultCode"); 1161 var actualBoundsInfo = event.getArguments().getParcelable("boundsInfo", 1162 TextBoundsInfo.class); 1163 1164 assertEquals(TextBoundsInfoResult.CODE_FAILED, actualResultCode); 1165 assertNull(actualBoundsInfo); 1166 }); 1167 } 1168 1169 @Test testRequestTextBoundsInfo_unimplemented()1170 public void testRequestTextBoundsInfo_unimplemented() throws Exception { 1171 testMinimallyImplementedInputConnection((session, stream) -> { 1172 final RectF rectF = new RectF(1f, 2f, 3f, 4f); 1173 final ImeCommand command = session.callRequestTextBoundsInfo(rectF); 1174 expectCommand(stream, command, TIMEOUT); 1175 var event = expectEvent(stream, onRequestTextBoundsInfoResultMatcher(command.getId()), 1176 TIMEOUT); 1177 var actualResultCode = event.getArguments().getInt("resultCode"); 1178 var actualBoundsInfo = event.getArguments().getParcelable("boundsInfo", 1179 TextBoundsInfo.class); 1180 1181 assertEquals(TextBoundsInfoResult.CODE_UNSUPPORTED, actualResultCode); 1182 assertNull(actualBoundsInfo); 1183 }); 1184 } 1185 1186 /** 1187 * Test {@link InputConnection#getSurroundingText(int, int, int)} works as expected. 1188 */ 1189 @Test testGetSurroundingText()1190 public void testGetSurroundingText() throws Exception { 1191 final int expectedBeforeLength = 3; 1192 final int expectedAfterLength = 4; 1193 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 1194 final CharSequence expectedText = 1195 createTestCharSequence("012345", new Annotation("command", "getSurroundingText")); 1196 final SurroundingText expectedResult = new SurroundingText(expectedText, 1, 2, 0); 1197 1198 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1199 1200 final class Wrapper extends InputConnectionWrapper { 1201 private Wrapper(InputConnection target) { 1202 super(target, false); 1203 } 1204 1205 @Override 1206 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1207 int flags) { 1208 methodCallVerifier.onMethodCalled(args -> { 1209 args.putInt("beforeLength", beforeLength); 1210 args.putInt("afterLength", afterLength); 1211 args.putInt("flags", flags); 1212 }); 1213 return expectedResult; 1214 } 1215 } 1216 1217 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1218 final ImeCommand command = session.callGetSurroundingText(expectedBeforeLength, 1219 expectedAfterLength, expectedFlags); 1220 final SurroundingText result = 1221 expectCommand(stream, command, TIMEOUT).getReturnParcelableValue(); 1222 assertEqualsForTestCharSequence(expectedResult.getText(), result.getText()); 1223 assertEquals(expectedResult.getSelectionStart(), result.getSelectionStart()); 1224 assertEquals(expectedResult.getSelectionEnd(), result.getSelectionEnd()); 1225 assertEquals(expectedResult.getOffset(), result.getOffset()); 1226 methodCallVerifier.assertCalledOnce(args -> { 1227 assertEquals(expectedBeforeLength, args.get("beforeLength")); 1228 assertEquals(expectedAfterLength, args.get("afterLength")); 1229 assertEquals(expectedFlags, args.get("flags")); 1230 }); 1231 }); 1232 } 1233 1234 /** 1235 * Test {@link InputConnection#getSurroundingText(int, int, int)} fails when a nagative 1236 * {@code afterLength} is passed. See Bug 169114026 for background. 1237 */ 1238 @Test testGetSurroundingTextFailWithNegativeAfterLength()1239 public void testGetSurroundingTextFailWithNegativeAfterLength() throws Exception { 1240 final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); 1241 1242 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1243 1244 final class Wrapper extends InputConnectionWrapper { 1245 private Wrapper(InputConnection target) { 1246 super(target, false); 1247 } 1248 1249 @Override 1250 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1251 int flags) { 1252 methodCallVerifier.onMethodCalled(args -> { 1253 args.putInt("beforeLength", beforeLength); 1254 args.putInt("afterLength", afterLength); 1255 args.putInt("flags", flags); 1256 }); 1257 return unexpectedResult; 1258 } 1259 } 1260 1261 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1262 final ImeCommand command = session.callGetSurroundingText(1, -1, 0); 1263 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 1264 assertTrue("IC#getSurroundingText() returns null for a negative afterLength.", 1265 result.isNullReturnValue()); 1266 methodCallVerifier.expectNotCalled( 1267 "IC#getSurroundingText() will not be triggered with a negative afterLength.", 1268 EXPECTED_NOT_CALLED_TIMEOUT); 1269 }); 1270 } 1271 1272 /** 1273 * Test {@link InputConnection#getSurroundingText(int, int, int)} fails when a negative 1274 * {@code beforeLength} is passed. See Bug 169114026 for background. 1275 */ 1276 @Test testGetSurroundingTextFailWithNegativeBeforeLength()1277 public void testGetSurroundingTextFailWithNegativeBeforeLength() throws Exception { 1278 final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); 1279 1280 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1281 1282 final class Wrapper extends InputConnectionWrapper { 1283 private Wrapper(InputConnection target) { 1284 super(target, false); 1285 } 1286 1287 @Override 1288 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1289 int flags) { 1290 methodCallVerifier.onMethodCalled(args -> { 1291 args.putInt("beforeLength", beforeLength); 1292 args.putInt("afterLength", afterLength); 1293 args.putInt("flags", flags); 1294 }); 1295 return unexpectedResult; 1296 } 1297 } 1298 1299 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1300 final ImeCommand command = session.callGetSurroundingText(-1, 1, 0); 1301 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 1302 assertTrue("IC#getSurroundingText() returns null for a negative beforeLength.", 1303 result.isNullReturnValue()); 1304 methodCallVerifier.expectNotCalled( 1305 "IC#getSurroundingText() will not be triggered with a negative beforeLength.", 1306 EXPECTED_NOT_CALLED_TIMEOUT); 1307 }); 1308 } 1309 1310 /** 1311 * Test {@link InputConnection#getSurroundingText(int, int, int)} fails after a system-defined 1312 * time-out even if the target app does not respond. 1313 */ 1314 @Test testGetSurroundingTextFailWithTimeout()1315 public void testGetSurroundingTextFailWithTimeout() throws Exception { 1316 final int expectedBeforeLength = 3; 1317 final int expectedAfterLength = 4; 1318 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 1319 final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); 1320 1321 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 1322 1323 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1324 1325 final class Wrapper extends InputConnectionWrapper { 1326 private Wrapper(InputConnection target) { 1327 super(target, false); 1328 } 1329 1330 @Override 1331 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1332 int flags) { 1333 methodCallVerifier.onMethodCalled(args -> { 1334 args.putInt("beforeLength", beforeLength); 1335 args.putInt("afterLength", afterLength); 1336 args.putInt("flags", flags); 1337 }); 1338 blocker.onMethodCalled(); 1339 return unexpectedResult; 1340 } 1341 } 1342 1343 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1344 final ImeCommand command = session.callGetSurroundingText(expectedBeforeLength, 1345 expectedAfterLength, expectedFlags); 1346 blocker.expectMethodCalled("IC#getSurroundingText() must be called back", TIMEOUT); 1347 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 1348 assertTrue("When timeout happens, IC#getSurroundingText() returns null", 1349 result.isNullReturnValue()); 1350 methodCallVerifier.assertCalledOnce(args -> { 1351 assertEquals(expectedBeforeLength, args.get("beforeLength")); 1352 assertEquals(expectedAfterLength, args.get("afterLength")); 1353 assertEquals(expectedFlags, args.get("flags")); 1354 }); 1355 }, blocker); 1356 } 1357 1358 /** 1359 * Test {@link InputConnection#getSurroundingText(int, int, int)} fail-fasts once unbindInput() 1360 * is issued. 1361 */ 1362 @Test testGetSurroundingTextFailFastAfterUnbindInput()1363 public void testGetSurroundingTextFailFastAfterUnbindInput() throws Exception { 1364 final int beforeLength = 3; 1365 final int afterLength = 4; 1366 final int flags = InputConnection.GET_TEXT_WITH_STYLES; 1367 final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); 1368 1369 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1370 1371 final class Wrapper extends InputConnectionWrapper { 1372 private Wrapper(InputConnection target) { 1373 super(target, false); 1374 } 1375 1376 @Override 1377 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1378 int flags) { 1379 methodCallVerifier.onMethodCalled(args -> { 1380 args.putInt("beforeLength", beforeLength); 1381 args.putInt("afterLength", afterLength); 1382 args.putInt("flags", flags); 1383 }); 1384 return unexpectedResult; 1385 } 1386 } 1387 1388 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1389 // Memorize the current InputConnection. 1390 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 1391 1392 // Let unbindInput happen. 1393 triggerUnbindInput(); 1394 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 1395 1396 // Now IC#getTextBeforeCursor() for the memorized IC should fail fast. 1397 final ImeEvent result = expectCommand(stream, session.callGetSurroundingText( 1398 beforeLength, afterLength, flags), TIMEOUT); 1399 assertTrue("Once unbindInput() happened, IC#getSurroundingText() returns null", 1400 result.isNullReturnValue()); 1401 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 1402 methodCallVerifier.assertNotCalled( 1403 "Once unbindInput() happened, IC#getSurroundingText() fails fast."); 1404 }); 1405 } 1406 1407 /** 1408 * Verify that the default implementation of 1409 * {@link InputConnection#getSurroundingText(int, int, int)} returns {@code null} without any 1410 * crash even when the target app does not override it . 1411 */ 1412 @Test testGetSurroundingTextDefaultMethod()1413 public void testGetSurroundingTextDefaultMethod() throws Exception { 1414 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 1415 final ImeCommand command = session.callGetSurroundingText(1, 2, 0); 1416 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 1417 assertTrue("Default IC#getSurroundingText() returns null.", 1418 result.isNullReturnValue()); 1419 }); 1420 } 1421 1422 /** 1423 * Test {@link InputConnection#getSurroundingText(int, int, int)} works as expected for 1424 * {@link android.accessibilityservice.InputMethod}. 1425 */ 1426 @Test testGetSurroundingTextForA11y()1427 public void testGetSurroundingTextForA11y() throws Exception { 1428 final int expectedBeforeLength = 3; 1429 final int expectedAfterLength = 4; 1430 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 1431 final CharSequence expectedText = 1432 createTestCharSequence("012345", new Annotation("command", "getSurroundingText")); 1433 final SurroundingText expectedResult = new SurroundingText(expectedText, 1, 2, 0); 1434 1435 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1436 1437 final class Wrapper extends InputConnectionWrapper { 1438 private Wrapper(InputConnection target) { 1439 super(target, false); 1440 } 1441 1442 @Override 1443 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1444 int flags) { 1445 methodCallVerifier.onMethodCalled(args -> { 1446 args.putInt("beforeLength", beforeLength); 1447 args.putInt("afterLength", afterLength); 1448 args.putInt("flags", flags); 1449 }); 1450 return expectedResult; 1451 } 1452 } 1453 1454 testA11yInputConnection(Wrapper::new, (session, stream) -> { 1455 final var command = session.callGetSurroundingText(expectedBeforeLength, 1456 expectedAfterLength, expectedFlags); 1457 final var result = expectA11yImeCommand(stream, command, TIMEOUT) 1458 .<SurroundingText>getReturnParcelableValue(); 1459 assertEqualsForTestCharSequence(expectedResult.getText(), result.getText()); 1460 assertEquals(expectedResult.getSelectionStart(), result.getSelectionStart()); 1461 assertEquals(expectedResult.getSelectionEnd(), result.getSelectionEnd()); 1462 assertEquals(expectedResult.getOffset(), result.getOffset()); 1463 methodCallVerifier.assertCalledOnce(args -> { 1464 assertEquals(expectedBeforeLength, args.get("beforeLength")); 1465 assertEquals(expectedAfterLength, args.get("afterLength")); 1466 assertEquals(expectedFlags, args.get("flags")); 1467 }); 1468 }); 1469 } 1470 1471 /** 1472 * Test {@link InputConnection#getSurroundingText(int, int, int)} fails when a negative 1473 * {@code afterLength} is passed for {@link android.accessibilityservice.InputMethod}. 1474 * See Bug 169114026 for background. 1475 */ 1476 @Test testGetSurroundingTextFailWithNegativeAfterLengthForA11y()1477 public void testGetSurroundingTextFailWithNegativeAfterLengthForA11y() throws Exception { 1478 final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); 1479 1480 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1481 1482 final class Wrapper extends InputConnectionWrapper { 1483 private Wrapper(InputConnection target) { 1484 super(target, false); 1485 } 1486 1487 @Override 1488 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1489 int flags) { 1490 methodCallVerifier.onMethodCalled(args -> { 1491 args.putInt("beforeLength", beforeLength); 1492 args.putInt("afterLength", afterLength); 1493 args.putInt("flags", flags); 1494 }); 1495 return unexpectedResult; 1496 } 1497 } 1498 1499 testA11yInputConnection(Wrapper::new, (session, stream) -> { 1500 final var command = session.callGetSurroundingText(1, -1, 0); 1501 final var result = expectA11yImeCommand(stream, command, TIMEOUT); 1502 assertTrue("IC#getSurroundingText() returns null for a negative afterLength.", 1503 result.isNullReturnValue()); 1504 methodCallVerifier.expectNotCalled( 1505 "IC#getSurroundingText() will not be triggered with a negative afterLength.", 1506 EXPECTED_NOT_CALLED_TIMEOUT); 1507 }); 1508 } 1509 1510 /** 1511 * Test {@link InputConnection#getSurroundingText(int, int, int)} fails when a negative 1512 * {@code beforeLength} is passed for {@link android.accessibilityservice.InputMethod}. 1513 * See Bug 169114026 for background. 1514 */ 1515 @Test testGetSurroundingTextFailWithNegativeBeforeLengthForA11y()1516 public void testGetSurroundingTextFailWithNegativeBeforeLengthForA11y() throws Exception { 1517 final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); 1518 1519 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1520 1521 final class Wrapper extends InputConnectionWrapper { 1522 private Wrapper(InputConnection target) { 1523 super(target, false); 1524 } 1525 1526 @Override 1527 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1528 int flags) { 1529 methodCallVerifier.onMethodCalled(args -> { 1530 args.putInt("beforeLength", beforeLength); 1531 args.putInt("afterLength", afterLength); 1532 args.putInt("flags", flags); 1533 }); 1534 return unexpectedResult; 1535 } 1536 } 1537 1538 testA11yInputConnection(Wrapper::new, (session, stream) -> { 1539 final var command = session.callGetSurroundingText(-1, 1, 0); 1540 final var result = expectA11yImeCommand(stream, command, TIMEOUT); 1541 assertTrue("IC#getSurroundingText() returns null for a negative beforeLength.", 1542 result.isNullReturnValue()); 1543 methodCallVerifier.expectNotCalled( 1544 "IC#getSurroundingText() will not be triggered with a negative beforeLength.", 1545 EXPECTED_NOT_CALLED_TIMEOUT); 1546 }); 1547 } 1548 1549 /** 1550 * Test {@link InputConnection#getSurroundingText(int, int, int)} fails for 1551 * {@link android.accessibilityservice.InputMethod} after a system-defined time-out even if the 1552 * target app does not respond. 1553 */ 1554 @Test testGetSurroundingTextFailWithTimeoutForA11y()1555 public void testGetSurroundingTextFailWithTimeoutForA11y() throws Exception { 1556 final int expectedBeforeLength = 3; 1557 final int expectedAfterLength = 4; 1558 final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; 1559 final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); 1560 1561 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 1562 1563 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1564 1565 final class Wrapper extends InputConnectionWrapper { 1566 private Wrapper(InputConnection target) { 1567 super(target, false); 1568 } 1569 1570 @Override 1571 public SurroundingText getSurroundingText(int beforeLength, int afterLength, 1572 int flags) { 1573 methodCallVerifier.onMethodCalled(args -> { 1574 args.putInt("beforeLength", beforeLength); 1575 args.putInt("afterLength", afterLength); 1576 args.putInt("flags", flags); 1577 }); 1578 blocker.onMethodCalled(); 1579 return unexpectedResult; 1580 } 1581 } 1582 1583 testA11yInputConnection(Wrapper::new, (session, stream) -> { 1584 final var command = session.callGetSurroundingText(expectedBeforeLength, 1585 expectedAfterLength, expectedFlags); 1586 blocker.expectMethodCalled("IC#getSurroundingText() must be called back", TIMEOUT); 1587 final var result = expectA11yImeCommand(stream, command, TIMEOUT); 1588 assertTrue("When timeout happens, IC#getSurroundingText() returns null", 1589 result.isNullReturnValue()); 1590 methodCallVerifier.assertCalledOnce(args -> { 1591 assertEquals(expectedBeforeLength, args.get("beforeLength")); 1592 assertEquals(expectedAfterLength, args.get("afterLength")); 1593 assertEquals(expectedFlags, args.get("flags")); 1594 }); 1595 }, blocker); 1596 } 1597 1598 /** 1599 * Verify that the default implementation of 1600 * {@link InputConnection#getSurroundingText(int, int, int)} returns {@code null} without any 1601 * crash even when the target app does not override it for 1602 * {@link android.accessibilityservice.InputMethod}. 1603 */ 1604 @Test testGetSurroundingTextDefaultMethodForA11y()1605 public void testGetSurroundingTextDefaultMethodForA11y() throws Exception { 1606 testMinimallyImplementedInputConnectionForA11y((session, stream) -> { 1607 final var command = session.callGetSurroundingText(1, 2, 0); 1608 final var result = expectA11yImeCommand(stream, command, TIMEOUT); 1609 assertTrue("Default IC#getSurroundingText() returns null.", 1610 result.isNullReturnValue()); 1611 }); 1612 } 1613 1614 /** 1615 * Test 1616 * {@link InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)} 1617 * works as expected for {@link SelectGesture}. 1618 */ 1619 @Test 1620 @ApiTest(apis = {"android.view.inputmethod.SelectGesture.Builder#setGranularity", 1621 "android.view.inputmethod.SelectGesture.Builder#setSelectionArea", 1622 "android.view.inputmethod.SelectGesture.Builder#setGranularity", 1623 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingSelectGesture()1624 public void testPerformHandwritingSelectGesture() throws Exception { 1625 SelectGesture.Builder builder = new SelectGesture.Builder(); 1626 testPerformHandwritingGesture( 1627 builder.setGranularity(HandwritingGesture.GRANULARITY_WORD) 1628 .setSelectionArea(new RectF(1, 2, 3, 4)) 1629 .setFallbackText("").build(), 1630 HANDWRITING_GESTURE_RESULT_SUCCESS); 1631 } 1632 1633 /** 1634 * Test 1635 * {@link InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)} 1636 * works as expected for {@link SelectRangeGesture}. 1637 */ 1638 @Test 1639 @ApiTest(apis = {"android.view.inputmethod.SelectRangeGesture.Builder#setGranularity", 1640 "android.view.inputmethod.SelectRangeGesture.Builder#setSelectionArea", 1641 "android.view.inputmethod.SelectRangeGesture.Builder#setGranularity", 1642 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingSelectRangeGesture()1643 public void testPerformHandwritingSelectRangeGesture() throws Exception { 1644 SelectRangeGesture.Builder builder = new SelectRangeGesture.Builder(); 1645 testPerformHandwritingGesture( 1646 builder.setGranularity(HandwritingGesture.GRANULARITY_WORD) 1647 .setSelectionStartArea(new RectF(1, 2, 3, 4)) 1648 .setSelectionEndArea(new RectF(5, 6, 7, 8)) 1649 .setFallbackText("").build(), 1650 HANDWRITING_GESTURE_RESULT_SUCCESS); 1651 } 1652 1653 /** 1654 * Test 1655 * {@link InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)} 1656 * works as expected for {@link SelectGesture} by returning 1657 * {@link InputConnection#HANDWRITING_GESTURE_RESULT_FAILED}. 1658 */ 1659 @Test 1660 @ApiTest(apis = {"android.view.inputmethod.SelectGesture.Builder#setGranularity", 1661 "android.view.inputmethod.SelectGesture.Builder#setSelectionArea", 1662 "android.view.inputmethod.SelectGesture.Builder#setFallbackText", 1663 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingSelectGesture_failed()1664 public void testPerformHandwritingSelectGesture_failed() throws Exception { 1665 SelectGesture.Builder builder = new SelectGesture.Builder(); 1666 testPerformHandwritingGesture( 1667 builder.setGranularity(HandwritingGesture.GRANULARITY_WORD) 1668 .setSelectionArea(new RectF(1, 2, 3, 4)) 1669 .setFallbackText("").build(), 1670 InputConnection.HANDWRITING_GESTURE_RESULT_FAILED); 1671 } 1672 1673 /** 1674 * Test 1675 * InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)} 1676 * works as expected for {@link InsertGesture}. 1677 */ 1678 @Test 1679 @ApiTest(apis = {"android.view.inputmethod.InsertGesture.Builder#setInsertionPoint", 1680 "android.view.inputmethod.InsertGesture.Builder#setFallbackText", 1681 "android.view.inputmethod.InsertGesture.Builder#setTextToInsert", 1682 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingInsertGesture()1683 public void testPerformHandwritingInsertGesture() throws Exception { 1684 InsertGesture.Builder builder = new InsertGesture.Builder(); 1685 testPerformHandwritingGesture(builder.setTextToInsert("text") 1686 .setInsertionPoint(new PointF(1, 1)).setFallbackText("").build(), 1687 HANDWRITING_GESTURE_RESULT_SUCCESS); 1688 } 1689 1690 /** 1691 * Test 1692 * InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)} 1693 * works as expected for {@link InsertGesture}. 1694 */ 1695 @Test 1696 @ApiTest(apis = {"android.view.inputmethod.InsertGesture.Builder#setInsertionPoint", 1697 "android.view.inputmethod.InsertGesture.Builder#setFallbackText", 1698 "android.view.inputmethod.InsertGesture.Builder#setTextToInsert", 1699 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingInsertGesture_emptyText()1700 public void testPerformHandwritingInsertGesture_emptyText() throws Exception { 1701 InsertGesture.Builder builder = new InsertGesture.Builder(); 1702 testPerformHandwritingGesture(builder.setTextToInsert("") 1703 .setInsertionPoint(new PointF(1, 1)).setFallbackText("").build(), 1704 HANDWRITING_GESTURE_RESULT_SUCCESS); 1705 } 1706 1707 /** 1708 * Test 1709 * InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)} 1710 * works as expected for {@link InsertGesture}. 1711 */ 1712 @Test 1713 @ApiTest(apis = {"android.view.inputmethod.InsertModeGesture.Builder#setInsertionPoint", 1714 "android.view.inputmethod.InsertGesture.Builder#setFallbackText", 1715 "android.view.inputmethod.InsertGesture.Builder#setCancellationSignal", 1716 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingInsertModeGesture()1717 public void testPerformHandwritingInsertModeGesture() throws Exception { 1718 InsertModeGesture.Builder builder = new InsertModeGesture.Builder(); 1719 testPerformHandwritingGesture(builder 1720 .setCancellationSignal(new CancellationSignal()) 1721 .setInsertionPoint(new PointF(1, 1)).setFallbackText("").build(), 1722 HANDWRITING_GESTURE_RESULT_SUCCESS); 1723 } 1724 1725 @Test 1726 @ApiTest(apis = {"android.view.inputmethod.InsertModeGesture.Builder#setInsertionPoint", 1727 "android.view.inputmethod.InsertGesture.Builder#setFallbackText", 1728 "android.view.inputmethod.InsertGesture.Builder#setCancellationSignal", 1729 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingInsertModeGesture_ongoingGestureCancellation()1730 public void testPerformHandwritingInsertModeGesture_ongoingGestureCancellation() 1731 throws Exception { 1732 InsertModeGesture.Builder builder = new InsertModeGesture.Builder(); 1733 testInsertModeGestureOngoingCancellation( 1734 builder.setCancellationSignal(new CancellationSignal()) 1735 .setInsertionPoint(new PointF(1, 1)) 1736 .setFallbackText("").build()); 1737 } 1738 1739 /** 1740 * Test 1741 * InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}} 1742 * works as expected for {@link DeleteGesture}. 1743 */ 1744 @Test 1745 @ApiTest(apis = {"android.view.inputmethod.DeleteGesture.Builder#setGranularity", 1746 "android.view.inputmethod.DeleteGesture.Builder#setSelectionArea", 1747 "android.view.inputmethod.DeleteGesture.Builder#setFallbackText", 1748 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingDeleteGesture()1749 public void testPerformHandwritingDeleteGesture() throws Exception { 1750 DeleteGesture.Builder builder = new DeleteGesture.Builder(); 1751 testPerformHandwritingGesture( 1752 builder.setGranularity(HandwritingGesture.GRANULARITY_WORD) 1753 .setDeletionArea(new RectF(1, 2, 3, 4)) 1754 .setFallbackText("").build(), 1755 HANDWRITING_GESTURE_RESULT_SUCCESS); 1756 } 1757 1758 /** 1759 * Test 1760 * InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}} 1761 * works as expected for {@link DeleteRangeGesture}. 1762 */ 1763 @Test 1764 @ApiTest(apis = {"android.view.inputmethod.DeleteRangeGesture.Builder#setGranularity", 1765 "android.view.inputmethod.DeleteRangeGesture.Builder#setSelectionArea", 1766 "android.view.inputmethod.DeleteRangeGesture.Builder#setFallbackText", 1767 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingDeleteRangeGesture()1768 public void testPerformHandwritingDeleteRangeGesture() throws Exception { 1769 DeleteRangeGesture.Builder builder = new DeleteRangeGesture.Builder(); 1770 testPerformHandwritingGesture( 1771 builder.setGranularity(HandwritingGesture.GRANULARITY_WORD) 1772 .setDeletionStartArea(new RectF(1, 2, 3, 4)) 1773 .setDeletionEndArea(new RectF(5, 6, 7, 8)) 1774 .setFallbackText("").build(), 1775 HANDWRITING_GESTURE_RESULT_SUCCESS); 1776 } 1777 1778 /** 1779 * Test InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}} 1780 * works as expected for {@link RemoveSpaceGesture}. 1781 */ 1782 @Test 1783 @ApiTest(apis = {"android.view.inputmethod.RemoveSpaceGesture.Builder#setPoints", 1784 "android.view.inputmethod.RemoveSpaceGesture.Builder#setFallbackText", 1785 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingRemoveSpaceGesture()1786 public void testPerformHandwritingRemoveSpaceGesture() throws Exception { 1787 testPerformHandwritingGesture( 1788 new RemoveSpaceGesture.Builder() 1789 .setPoints(new PointF(1f, 2f), new PointF(3f, 4f)) 1790 .setFallbackText("") 1791 .build(), 1792 HANDWRITING_GESTURE_RESULT_SUCCESS); 1793 } 1794 1795 /** 1796 * Test InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}} 1797 * works as expected for {@link JoinOrSplitGesture}. 1798 */ 1799 @Test 1800 @ApiTest(apis = {"android.view.inputmethod.JoinOrSplitGesture.Builder#setJoinOrSplitPoint", 1801 "android.view.inputmethod.JoinOrSplitGesture.Builder#setFallbackText", 1802 "android.view.inputmethod.InputConnection#performHandwritingGesture"}) testPerformHandwritingJoinOrSplitGesture()1803 public void testPerformHandwritingJoinOrSplitGesture() throws Exception { 1804 testPerformHandwritingGesture( 1805 new JoinOrSplitGesture.Builder() 1806 .setJoinOrSplitPoint(new PointF(1f, 2f)) 1807 .setFallbackText("") 1808 .build(), 1809 HANDWRITING_GESTURE_RESULT_SUCCESS); 1810 } 1811 testPerformHandwritingGesture( T gesture, int returnResult)1812 private <T extends HandwritingGesture> void testPerformHandwritingGesture( 1813 T gesture, int returnResult) throws Exception { 1814 final int expectedResult = returnResult; 1815 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1816 1817 final class Wrapper extends InputConnectionWrapper { 1818 private Wrapper(InputConnection target) { 1819 super(target, false); 1820 } 1821 1822 @Override 1823 public void performHandwritingGesture( 1824 HandwritingGesture gesture, Executor executor, IntConsumer consumer) { 1825 assertNotNull(executor); 1826 assertNotNull(consumer); 1827 if (returnResult > InputConnection.HANDWRITING_GESTURE_RESULT_UNKNOWN) { 1828 consumer.accept(returnResult); 1829 // Intentionally try to send second time. This update should be ignored 1830 // and never sent back to IME. 1831 consumer.accept(InputConnection.HANDWRITING_GESTURE_RESULT_UNSUPPORTED); 1832 } 1833 methodCallVerifier.onMethodCalled(args -> { 1834 args.putByteArray("gesture", gesture.toByteArray()); 1835 }); 1836 } 1837 } 1838 1839 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1840 ImeCommand command = session.callPerformHandwritingGesture( 1841 gesture, false /* useDelayedCancellation */); 1842 expectCommand(stream, command, TIMEOUT); 1843 1844 long requestId = command.getId(); 1845 ImeEvent callbackEvent = expectEvent( 1846 stream, onPerformHandwritingGestureResultMatcher(requestId), TIMEOUT); 1847 assertEquals(expectedResult, callbackEvent.getArguments().getInt("result")); 1848 1849 methodCallVerifier.assertCalledOnce(args -> { 1850 byte[] bytes = args.getByteArray("gesture"); 1851 HandwritingGesture gesture1 = HandwritingGesture.fromByteArray(bytes); 1852 assertEquals(gesture, gesture1); 1853 }); 1854 1855 // Verify that the second callback was filtered out. 1856 notExpectEvent(stream, onPerformHandwritingGestureResultMatcher(requestId), 1857 EXPECTED_NOT_CALLED_TIMEOUT); 1858 }); 1859 } 1860 testInsertModeGestureOngoingCancellation( InsertModeGesture gesture1)1861 private void testInsertModeGestureOngoingCancellation( 1862 InsertModeGesture gesture1) throws Exception { 1863 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1864 final CountDownLatch latch = new CountDownLatch(1); 1865 final int resultCode = HANDWRITING_GESTURE_RESULT_SUCCESS; 1866 final class Wrapper extends InputConnectionWrapper { 1867 private Wrapper(InputConnection target) { 1868 super(target, false); 1869 } 1870 1871 @Override 1872 public void performHandwritingGesture( 1873 HandwritingGesture gesture, Executor executor, IntConsumer consumer) { 1874 CancellationSignal cs = ((InsertModeGesture) gesture).getCancellationSignal(); 1875 assertNotNull(cs); 1876 1877 methodCallVerifier.onMethodCalled(args -> {}); 1878 cs.setOnCancelListener(() -> latch.countDown()); 1879 consumer.accept(resultCode); 1880 } 1881 } 1882 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1883 ImeCommand command = session.callPerformHandwritingGesture( 1884 gesture1, true /* useDelayedCancellation */); 1885 1886 expectCommand(stream, command, TIMEOUT); 1887 methodCallVerifier.assertCalledOnce(args -> {}); 1888 1889 long requestId = command.getId(); 1890 ImeEvent callbackEvent = expectEvent( 1891 stream, onPerformHandwritingGestureResultMatcher(requestId), TIMEOUT); 1892 assertEquals(resultCode, callbackEvent.getArguments().getInt("result")); 1893 }); 1894 1895 latch.await(3, TimeUnit.SECONDS); 1896 assertEquals(0, latch.getCount()); 1897 1898 } 1899 onPerformHandwritingGestureResultMatcher( long requestId)1900 private static DescribedPredicate<ImeEvent> onPerformHandwritingGestureResultMatcher( 1901 long requestId) { 1902 return withDescription("onPerformHandwritingGestureResult(" + requestId + ")", event -> { 1903 if (!TextUtils.equals("onPerformHandwritingGestureResult", event.getEventName())) { 1904 return false; 1905 } 1906 return event.getArguments().getLong("requestId") == requestId; 1907 }); 1908 } 1909 1910 private static DescribedPredicate<ImeEvent> onRequestTextBoundsInfoResultMatcher( 1911 long requestId) { 1912 return withDescription("onRequestTextBoundsInfoResult(" + requestId + ")", event -> { 1913 if (!TextUtils.equals("onRequestTextBoundsInfoResult", event.getEventName())) { 1914 return false; 1915 } 1916 return event.getArguments().getLong("requestId") == requestId; 1917 }); 1918 } 1919 1920 /** 1921 * Test 1922 * {@link InputConnection#previewHandwritingGesture(PreviewableHandwritingGesture, 1923 * CancellationSignal)} works as expected for {@link SelectGesture}. 1924 */ 1925 @Test 1926 @ApiTest(apis = {"android.view.inputmethod.SelectGesture.Builder#setGranularity", 1927 "android.view.inputmethod.SelectGesture.Builder#setSelectionArea", 1928 "android.view.inputmethod.SelectGesture.Builder#setGranularity", 1929 "android.view.inputmethod.InputConnection#previewHandwritingGesture"}) 1930 @FlakyTest(bugId = 324566416) 1931 public void testPreviewHandwritingSelectGesture() throws Exception { 1932 SelectGesture.Builder builder = new SelectGesture.Builder(); 1933 testPreviewHandwritingGesture( 1934 builder.setGranularity(HandwritingGesture.GRANULARITY_WORD) 1935 .setSelectionArea(new RectF(1, 2, 3, 4)) 1936 .build()); 1937 } 1938 1939 @Test 1940 @ApiTest(apis = {"android.view.inputmethod.SelectGesture.Builder#setGranularity", 1941 "android.view.inputmethod.SelectGesture.Builder#setSelectionArea", 1942 "android.view.inputmethod.SelectGesture.Builder#setGranularity", 1943 "android.view.inputmethod.InputConnection#previewHandwritingGesture"}) 1944 @FlakyTest(bugId = 338754377) 1945 public void testPreviewHandwritingSelectGesture_ongoingGestureCancellation() 1946 throws Exception { 1947 SelectGesture.Builder builder = new SelectGesture.Builder(); 1948 testPreviewHandwritingGestureOngoingCancellation( 1949 builder.setGranularity(HandwritingGesture.GRANULARITY_WORD) 1950 .setSelectionArea(new RectF(1, 2, 3, 4)) 1951 .build()); 1952 } 1953 1954 private <T extends PreviewableHandwritingGesture> void testPreviewHandwritingGesture(T gesture) 1955 throws Exception { 1956 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1957 1958 final class Wrapper extends InputConnectionWrapper { 1959 private Wrapper(InputConnection target) { 1960 super(target, false); 1961 } 1962 1963 @Override 1964 public boolean previewHandwritingGesture( 1965 PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { 1966 assertNotNull(gesture); 1967 1968 methodCallVerifier.onMethodCalled(args -> 1969 args.putByteArray("gesture", gesture.toByteArray())); 1970 1971 return true; 1972 } 1973 } 1974 1975 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 1976 ImeCommand command = 1977 session.callPreviewHandwritingGesture( 1978 gesture, false /* useDelayedCancellation */); 1979 1980 expectCommand(stream, command, TIMEOUT); 1981 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 1982 methodCallVerifier.assertCalledOnce( 1983 args -> assertEquals(gesture, 1984 HandwritingGesture.fromByteArray(args.getByteArray("gesture")))); 1985 }); 1986 } 1987 1988 private <T extends PreviewableHandwritingGesture> void 1989 testPreviewHandwritingGestureOngoingCancellation(T gesture) throws Exception { 1990 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 1991 final CountDownLatch latch = new CountDownLatch(1); 1992 1993 final class Wrapper extends InputConnectionWrapper { 1994 private Wrapper(InputConnection target) { 1995 super(target, false); 1996 } 1997 1998 @Override 1999 public boolean previewHandwritingGesture( 2000 PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { 2001 assertNotNull(gesture); 2002 assertNotNull(cancellationSignal); 2003 methodCallVerifier.onMethodCalled(args ->{}); 2004 cancellationSignal.setOnCancelListener(() -> latch.countDown()); 2005 2006 return true; 2007 } 2008 } 2009 2010 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2011 ImeCommand command = 2012 session.callPreviewHandwritingGesture( 2013 gesture, true /* useDelayedCancellation */); 2014 2015 expectCommand(stream, command, TIMEOUT); 2016 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 2017 methodCallVerifier.assertCalledOnce(args -> {}); 2018 }); 2019 2020 latch.await(3, TimeUnit.SECONDS); 2021 assertEquals(0, latch.getCount()); 2022 } 2023 2024 /** 2025 * Test {@link InputConnection#getCursorCapsMode(int)} works as expected. 2026 */ 2027 @Test 2028 public void testGetCursorCapsMode() throws Exception { 2029 final int expectedReqMode = TextUtils.CAP_MODE_SENTENCES | TextUtils.CAP_MODE_CHARACTERS 2030 | TextUtils.CAP_MODE_WORDS; 2031 final int expectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; 2032 2033 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2034 2035 final class Wrapper extends InputConnectionWrapper { 2036 private Wrapper(InputConnection target) { 2037 super(target, false); 2038 } 2039 2040 @Override 2041 public int getCursorCapsMode(int reqModes) { 2042 methodCallVerifier.onMethodCalled(args -> { 2043 args.putInt("reqModes", reqModes); 2044 }); 2045 return expectedResult; 2046 } 2047 } 2048 2049 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2050 final ImeCommand command = session.callGetCursorCapsMode(expectedReqMode); 2051 final int result = expectCommand(stream, command, TIMEOUT).getReturnIntegerValue(); 2052 assertEquals(expectedResult, result); 2053 methodCallVerifier.assertCalledOnce(args -> { 2054 assertEquals(expectedReqMode, args.getInt("reqModes")); 2055 }); 2056 }); 2057 } 2058 2059 /** 2060 * Test {@link InputConnection#getCursorCapsMode(int)} fails after a system-defined time-out 2061 * even if the target app does not respond. 2062 */ 2063 @Test 2064 public void testGetCursorCapsModeFailWithTimeout() throws Exception { 2065 final int expectedReqMode = TextUtils.CAP_MODE_SENTENCES | TextUtils.CAP_MODE_CHARACTERS 2066 | TextUtils.CAP_MODE_WORDS; 2067 final int unexpectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; 2068 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 2069 2070 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2071 2072 final class Wrapper extends InputConnectionWrapper { 2073 private Wrapper(InputConnection target) { 2074 super(target, false); 2075 } 2076 2077 @Override 2078 public int getCursorCapsMode(int reqModes) { 2079 methodCallVerifier.onMethodCalled(args -> { 2080 args.putInt("reqModes", reqModes); 2081 }); 2082 blocker.onMethodCalled(); 2083 return unexpectedResult; 2084 } 2085 } 2086 2087 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2088 final ImeCommand command = session.callGetCursorCapsMode(expectedReqMode); 2089 blocker.expectMethodCalled("IC#getCursorCapsMode() must be called back", TIMEOUT); 2090 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 2091 assertEquals("When timeout happens, IC#getCursorCapsMode() returns 0", 2092 0, result.getReturnIntegerValue()); 2093 methodCallVerifier.assertCalledOnce(args -> { 2094 assertEquals(expectedReqMode, args.getInt("reqModes")); 2095 }); 2096 }, blocker); 2097 } 2098 2099 /** 2100 * Test {@link InputConnection#getCursorCapsMode(int)} fail-fasts once unbindInput() is issued. 2101 */ 2102 @Test 2103 public void testGetCursorCapsModeFailFastAfterUnbindInput() throws Exception { 2104 final int unexpectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; 2105 2106 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2107 2108 final class Wrapper extends InputConnectionWrapper { 2109 private Wrapper(InputConnection target) { 2110 super(target, false); 2111 } 2112 2113 @Override 2114 public int getCursorCapsMode(int reqModes) { 2115 methodCallVerifier.onMethodCalled(args -> { 2116 args.putInt("reqModes", reqModes); 2117 }); 2118 return unexpectedResult; 2119 } 2120 } 2121 2122 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2123 // Memorize the current InputConnection. 2124 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 2125 2126 // Let unbindInput happen. 2127 triggerUnbindInput(); 2128 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 2129 2130 // Now IC#getCursorCapsMode() for the memorized IC should fail fast. 2131 final ImeEvent result = expectCommand(stream, 2132 session.callGetCursorCapsMode(TextUtils.CAP_MODE_WORDS), TIMEOUT); 2133 assertEquals("Once unbindInput() happened, IC#getCursorCapsMode() returns 0", 2134 0, result.getReturnIntegerValue()); 2135 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 2136 methodCallVerifier.assertNotCalled( 2137 "Once unbindInput() happened, IC#getCursorCapsMode() fails fast."); 2138 }); 2139 } 2140 2141 /** 2142 * Test {@link InputConnection#getCursorCapsMode(int)} works as expected for 2143 * {@link android.accessibilityservice.InputMethod}. 2144 */ 2145 @Test 2146 public void testGetCursorCapsModeForA11y() throws Exception { 2147 final int expectedReqMode = TextUtils.CAP_MODE_SENTENCES | TextUtils.CAP_MODE_CHARACTERS 2148 | TextUtils.CAP_MODE_WORDS; 2149 final int expectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; 2150 2151 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2152 2153 final class Wrapper extends InputConnectionWrapper { 2154 private Wrapper(InputConnection target) { 2155 super(target, false); 2156 } 2157 2158 @Override 2159 public int getCursorCapsMode(int reqModes) { 2160 methodCallVerifier.onMethodCalled(args -> { 2161 args.putInt("reqModes", reqModes); 2162 }); 2163 return expectedResult; 2164 } 2165 } 2166 2167 testA11yInputConnection(Wrapper::new, (session, stream) -> { 2168 final var command = session.callGetCursorCapsMode(expectedReqMode); 2169 final int result = expectA11yImeCommand(stream, command, TIMEOUT) 2170 .getReturnIntegerValue(); 2171 assertEquals(expectedResult, result); 2172 methodCallVerifier.assertCalledOnce(args -> { 2173 assertEquals(expectedReqMode, args.getInt("reqModes")); 2174 }); 2175 }); 2176 } 2177 2178 /** 2179 * Test {@link InputConnection#getCursorCapsMode(int)} fails for 2180 * {@link android.accessibilityservice.InputMethod} after a system-defined time-out even if the 2181 * target app does not respond. 2182 */ 2183 @Test 2184 public void testGetCursorCapsModeFailWithTimeoutForA11y() throws Exception { 2185 final int expectedReqMode = TextUtils.CAP_MODE_SENTENCES | TextUtils.CAP_MODE_CHARACTERS 2186 | TextUtils.CAP_MODE_WORDS; 2187 final int unexpectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; 2188 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 2189 2190 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2191 2192 final class Wrapper extends InputConnectionWrapper { 2193 private Wrapper(InputConnection target) { 2194 super(target, false); 2195 } 2196 2197 @Override 2198 public int getCursorCapsMode(int reqModes) { 2199 methodCallVerifier.onMethodCalled(args -> { 2200 args.putInt("reqModes", reqModes); 2201 }); 2202 blocker.onMethodCalled(); 2203 return unexpectedResult; 2204 } 2205 } 2206 2207 testA11yInputConnection(Wrapper::new, (session, stream) -> { 2208 final var command = session.callGetCursorCapsMode(expectedReqMode); 2209 blocker.expectMethodCalled("IC#getCursorCapsMode() must be called back", TIMEOUT); 2210 final var result = expectA11yImeCommand(stream, command, LONG_TIMEOUT); 2211 assertEquals("When timeout happens, IC#getCursorCapsMode() returns 0", 2212 0, result.getReturnIntegerValue()); 2213 methodCallVerifier.assertCalledOnce(args -> { 2214 assertEquals(expectedReqMode, args.getInt("reqModes")); 2215 }); 2216 }, blocker); 2217 } 2218 2219 /** 2220 * Test {@link InputConnection#getExtractedText(ExtractedTextRequest, int)} works as expected. 2221 */ 2222 @Test 2223 public void testGetExtractedText() throws Exception { 2224 final ExtractedTextRequest expectedRequest = ExtractedTextRequestTest.createForTest(); 2225 final int expectedFlags = InputConnection.GET_EXTRACTED_TEXT_MONITOR; 2226 final ExtractedText expectedResult = ExtractedTextTest.createForTest(); 2227 2228 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2229 2230 final class Wrapper extends InputConnectionWrapper { 2231 private Wrapper(InputConnection target) { 2232 super(target, false); 2233 } 2234 2235 @Override 2236 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 2237 methodCallVerifier.onMethodCalled(args -> { 2238 args.putParcelable("request", request); 2239 args.putInt("flags", flags); 2240 }); 2241 return expectedResult; 2242 } 2243 } 2244 2245 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2246 final ImeCommand command = session.callGetExtractedText(expectedRequest, expectedFlags); 2247 final ExtractedText result = 2248 expectCommand(stream, command, TIMEOUT).getReturnParcelableValue(); 2249 ExtractedTextTest.assertTestInstance(result); 2250 methodCallVerifier.assertCalledOnce(args -> { 2251 ExtractedTextRequestTest.assertTestInstance(args.getParcelable("request")); 2252 assertEquals(expectedFlags, args.getInt("flags")); 2253 }); 2254 }); 2255 } 2256 2257 /** 2258 * Test {@link InputConnection#getExtractedText(ExtractedTextRequest, int)} fails after a 2259 * system-defined time-out even if the target app does not respond. 2260 */ 2261 @Test 2262 public void testGetExtractedTextFailWithTimeout() throws Exception { 2263 final ExtractedTextRequest expectedRequest = ExtractedTextRequestTest.createForTest(); 2264 final int expectedFlags = InputConnection.GET_EXTRACTED_TEXT_MONITOR; 2265 final ExtractedText unexpectedResult = ExtractedTextTest.createForTest(); 2266 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 2267 2268 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2269 2270 final class Wrapper extends InputConnectionWrapper { 2271 private Wrapper(InputConnection target) { 2272 super(target, false); 2273 } 2274 2275 @Override 2276 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 2277 methodCallVerifier.onMethodCalled(args -> { 2278 args.putParcelable("request", request); 2279 args.putInt("flags", flags); 2280 }); 2281 blocker.onMethodCalled(); 2282 return unexpectedResult; 2283 } 2284 } 2285 2286 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2287 final ImeCommand command = session.callGetExtractedText(expectedRequest, expectedFlags); 2288 blocker.expectMethodCalled("IC#getExtractedText() must be called back", TIMEOUT); 2289 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 2290 assertTrue("When timeout happens, IC#getExtractedText() returns null", 2291 result.isNullReturnValue()); 2292 methodCallVerifier.assertCalledOnce(args -> { 2293 ExtractedTextRequestTest.assertTestInstance(args.getParcelable("request")); 2294 assertEquals(expectedFlags, args.getInt("flags")); 2295 }); 2296 }, blocker); 2297 } 2298 2299 /** 2300 * Test {@link InputConnection#getExtractedText(ExtractedTextRequest, int)} fail-fasts once 2301 * unbindInput() is issued. 2302 */ 2303 @Test 2304 public void testGetExtractedTextFailFastAfterUnbindInput() throws Exception { 2305 final ExtractedText unexpectedResult = ExtractedTextTest.createForTest(); 2306 2307 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2308 2309 final class Wrapper extends InputConnectionWrapper { 2310 private Wrapper(InputConnection target) { 2311 super(target, false); 2312 } 2313 2314 @Override 2315 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 2316 methodCallVerifier.onMethodCalled(args -> { 2317 args.putParcelable("request", request); 2318 args.putInt("flags", flags); 2319 }); 2320 return unexpectedResult; 2321 } 2322 } 2323 2324 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2325 // Memorize the current InputConnection. 2326 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 2327 2328 // Let unbindInput happen. 2329 triggerUnbindInput(); 2330 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 2331 2332 // Now IC#getExtractedText() for the memorized IC should fail fast. 2333 final ImeEvent result = expectCommand(stream, session.callGetExtractedText( 2334 ExtractedTextRequestTest.createForTest(), 2335 InputConnection.GET_EXTRACTED_TEXT_MONITOR), TIMEOUT); 2336 assertTrue("Once unbindInput() happened, IC#getExtractedText() returns null", 2337 result.isNullReturnValue()); 2338 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 2339 methodCallVerifier.assertNotCalled( 2340 "Once unbindInput() happened, IC#getExtractedText() fails fast."); 2341 }); 2342 } 2343 2344 /** 2345 * Test {@link InputConnection#requestCursorUpdates(int)} works as expected. 2346 */ 2347 @Test 2348 public void testRequestCursorUpdates() throws Exception { 2349 final int expectedFlags = InputConnection.CURSOR_UPDATE_IMMEDIATE; 2350 final boolean expectedResult = true; 2351 2352 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2353 2354 final class Wrapper extends InputConnectionWrapper { 2355 private Wrapper(InputConnection target) { 2356 super(target, false); 2357 } 2358 2359 @Override 2360 public boolean requestCursorUpdates(int cursorUpdateMode, int cursorUpdateFilter) { 2361 methodCallVerifier.onMethodCalled(args -> { 2362 args.putInt("cursorUpdateMode", cursorUpdateMode); 2363 args.putInt("cursorUpdateFilter", cursorUpdateFilter); 2364 }); 2365 assertEquals(expectedFlags, cursorUpdateMode); 2366 assertEquals(0, cursorUpdateFilter); 2367 return expectedResult; 2368 } 2369 } 2370 2371 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2372 final ImeCommand command = session.callRequestCursorUpdates(expectedFlags); 2373 assertTrue(expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 2374 methodCallVerifier.assertCalledOnce(args -> { 2375 assertEquals(expectedFlags, args.getInt("cursorUpdateMode")); 2376 }); 2377 }); 2378 } 2379 2380 /** 2381 * Test {@link InputConnection#requestCursorUpdates(int, int)} works as expected. 2382 */ 2383 @Test 2384 public void testRequestCursorUpdatesWithFilter() throws Exception { 2385 final int expectedUpdateFlags = InputConnection.CURSOR_UPDATE_IMMEDIATE; 2386 final int expectedFilterFlags = InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS; 2387 final boolean expectedResult = true; 2388 2389 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2390 2391 final class Wrapper extends InputConnectionWrapper { 2392 private Wrapper(InputConnection target) { 2393 super(target, false); 2394 } 2395 2396 @Override 2397 public boolean requestCursorUpdates(int cursorUpdateMode, int cursorUpdateFilter) { 2398 methodCallVerifier.onMethodCalled(args -> { 2399 args.putInt("cursorUpdateMode", cursorUpdateMode); 2400 args.putInt("cursorUpdateFilter", cursorUpdateFilter); 2401 }); 2402 assertEquals(expectedUpdateFlags, cursorUpdateMode); 2403 assertEquals(expectedFilterFlags, cursorUpdateFilter); 2404 return expectedResult; 2405 } 2406 } 2407 2408 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2409 final ImeCommand command = 2410 session.callRequestCursorUpdates(expectedUpdateFlags, expectedFilterFlags); 2411 assertTrue(expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 2412 methodCallVerifier.assertCalledOnce(args -> { 2413 assertEquals(expectedUpdateFlags, args.getInt("cursorUpdateMode")); 2414 assertEquals(expectedFilterFlags, args.getInt("cursorUpdateFilter")); 2415 }); 2416 }); 2417 } 2418 2419 /** 2420 * Test {@link InputConnection#requestCursorUpdates(int)} fails after a system-defined time-out 2421 * even if the target app does not respond. 2422 */ 2423 @Test 2424 public void testRequestCursorUpdatesFailWithTimeout() throws Exception { 2425 final int expectedFlags = InputConnection.CURSOR_UPDATE_IMMEDIATE; 2426 final boolean unexpectedResult = true; 2427 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 2428 2429 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2430 2431 final class Wrapper extends InputConnectionWrapper { 2432 private Wrapper(InputConnection target) { 2433 super(target, false); 2434 } 2435 2436 @Override 2437 public boolean requestCursorUpdates(int cursorUpdateMode, int cursorUpdateFilter) { 2438 methodCallVerifier.onMethodCalled(args -> { 2439 args.putInt("cursorUpdateMode", cursorUpdateMode); 2440 args.putInt("cursorUpdateFilter", cursorUpdateFilter); 2441 }); 2442 blocker.onMethodCalled(); 2443 return unexpectedResult; 2444 } 2445 } 2446 2447 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2448 final ImeCommand command = session.callRequestCursorUpdates( 2449 InputConnection.CURSOR_UPDATE_IMMEDIATE); 2450 blocker.expectMethodCalled("IC#requestCursorUpdates() must be called back", TIMEOUT); 2451 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 2452 assertFalse("When timeout happens, IC#requestCursorUpdates() returns false", 2453 result.getReturnBooleanValue()); 2454 methodCallVerifier.assertCalledOnce(args -> { 2455 assertEquals(expectedFlags, args.getInt("cursorUpdateMode")); 2456 }); 2457 }, blocker); 2458 } 2459 2460 /** 2461 * Test {@link InputConnection#requestCursorUpdates(int)} fail-fasts once unbindInput() is 2462 * issued. 2463 */ 2464 @Test 2465 public void testRequestCursorUpdatesFailFastAfterUnbindInput() throws Exception { 2466 final boolean unexpectedResult = true; 2467 2468 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2469 2470 final class Wrapper extends InputConnectionWrapper { 2471 private Wrapper(InputConnection target) { 2472 super(target, false); 2473 } 2474 2475 @Override 2476 public boolean requestCursorUpdates(int cursorUpdateMode) { 2477 methodCallVerifier.onMethodCalled(args -> { 2478 args.putInt("cursorUpdateMode", cursorUpdateMode); 2479 }); 2480 return unexpectedResult; 2481 } 2482 } 2483 2484 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2485 // Memorize the current InputConnection. 2486 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 2487 2488 // Let unbindInput happen. 2489 triggerUnbindInput(); 2490 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 2491 2492 // Now IC#requestCursorUpdates() for the memorized IC should fail fast. 2493 final ImeEvent result = expectCommand(stream, session.callRequestCursorUpdates( 2494 InputConnection.CURSOR_UPDATE_IMMEDIATE), TIMEOUT); 2495 assertFalse("Once unbindInput() happened, IC#requestCursorUpdates() returns false", 2496 result.getReturnBooleanValue()); 2497 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 2498 methodCallVerifier.assertNotCalled( 2499 "Once unbindInput() happened, IC#requestCursorUpdates() fails fast."); 2500 }); 2501 } 2502 2503 /** 2504 * Verify that {@link InputConnection#requestCursorUpdates(int)} fails when the target app does 2505 * not implement it. This can happen if the app was built before 2506 * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. 2507 */ 2508 @Test 2509 public void testRequestCursorUpdatesFailWithMethodMissing() throws Exception { 2510 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 2511 final ImeCommand command = session.callRequestCursorUpdates( 2512 InputConnection.CURSOR_UPDATE_IMMEDIATE); 2513 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 2514 assertFalse("IC#requestCursorUpdates() returns false when the target app does not " 2515 + " implement it.", result.getReturnBooleanValue()); 2516 }); 2517 } 2518 2519 /** 2520 * Test {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} works as expected. 2521 */ 2522 @Test 2523 public void testCommitContent() throws Exception { 2524 final InputContentInfo expectedInputContentInfo = new InputContentInfo( 2525 Uri.parse("content://com.example/path"), 2526 new ClipDescription("sample content", new String[]{"image/png"}), 2527 Uri.parse("https://example.com")); 2528 final Bundle expectedOpt = new Bundle(); 2529 final String expectedOptKey = "testKey"; 2530 final int expectedOptValue = 42; 2531 expectedOpt.putInt(expectedOptKey, expectedOptValue); 2532 final int expectedFlags = InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION; 2533 final boolean expectedResult = true; 2534 2535 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2536 2537 final class Wrapper extends InputConnectionWrapper { 2538 private Wrapper(InputConnection target) { 2539 super(target, false); 2540 } 2541 2542 @Override 2543 public boolean commitContent(InputContentInfo inputContentInfo, int flags, 2544 Bundle opts) { 2545 methodCallVerifier.onMethodCalled(args -> { 2546 args.putParcelable("inputContentInfo", inputContentInfo); 2547 args.putInt("flags", flags); 2548 args.putBundle("opts", opts); 2549 }); 2550 return expectedResult; 2551 } 2552 } 2553 2554 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2555 final ImeCommand command = 2556 session.callCommitContent(expectedInputContentInfo, expectedFlags, expectedOpt); 2557 assertTrue(expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 2558 methodCallVerifier.assertCalledOnce(args -> { 2559 final InputContentInfo inputContentInfo = args.getParcelable("inputContentInfo"); 2560 final Bundle opts = args.getBundle("opts"); 2561 assertNotNull(inputContentInfo); 2562 assertEquals(expectedInputContentInfo.getContentUri(), 2563 inputContentInfo.getContentUri()); 2564 assertEquals(expectedFlags, args.getInt("flags")); 2565 assertNotNull(opts); 2566 assertEquals(expectedOpt.getInt(expectedOptKey), opts.getInt(expectedOptKey)); 2567 }); 2568 }); 2569 } 2570 2571 /** 2572 * Test {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} fails after a 2573 * system-defined time-out even if the target app does not respond. 2574 */ 2575 @Test 2576 public void testCommitContentFailWithTimeout() throws Exception { 2577 final InputContentInfo expectedInputContentInfo = new InputContentInfo( 2578 Uri.parse("content://com.example/path"), 2579 new ClipDescription("sample content", new String[]{"image/png"}), 2580 Uri.parse("https://example.com")); 2581 final Bundle expectedOpt = new Bundle(); 2582 final String expectedOptKey = "testKey"; 2583 final int expectedOptValue = 42; 2584 expectedOpt.putInt(expectedOptKey, expectedOptValue); 2585 final int expectedFlags = InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION; 2586 final boolean unexpectedResult = true; 2587 final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); 2588 2589 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2590 2591 final class Wrapper extends InputConnectionWrapper { 2592 private Wrapper(InputConnection target) { 2593 super(target, false); 2594 } 2595 2596 @Override 2597 public boolean commitContent(InputContentInfo inputContentInfo, int flags, 2598 Bundle opts) { 2599 methodCallVerifier.onMethodCalled(args -> { 2600 args.putParcelable("inputContentInfo", inputContentInfo); 2601 args.putInt("flags", flags); 2602 args.putBundle("opts", opts); 2603 }); 2604 blocker.onMethodCalled(); 2605 return unexpectedResult; 2606 } 2607 } 2608 2609 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2610 final ImeCommand command = 2611 session.callCommitContent(expectedInputContentInfo, expectedFlags, expectedOpt); 2612 blocker.expectMethodCalled("IC#commitContent() must be called back", TIMEOUT); 2613 final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); 2614 assertFalse("When timeout happens, IC#commitContent() returns false", 2615 result.getReturnBooleanValue()); 2616 methodCallVerifier.assertCalledOnce(args -> { 2617 final InputContentInfo inputContentInfo = args.getParcelable("inputContentInfo"); 2618 final Bundle opts = args.getBundle("opts"); 2619 assertNotNull(inputContentInfo); 2620 assertEquals(expectedInputContentInfo.getContentUri(), 2621 inputContentInfo.getContentUri()); 2622 assertEquals(expectedFlags, args.getInt("flags")); 2623 assertNotNull(opts); 2624 assertEquals(expectedOpt.getInt(expectedOptKey), opts.getInt(expectedOptKey)); 2625 }); 2626 }, blocker); 2627 } 2628 2629 /** 2630 * Test {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} fail-fasts once 2631 * unbindInput() is issued. 2632 */ 2633 @Test 2634 public void testCommitContentFailFastAfterUnbindInput() throws Exception { 2635 final boolean unexpectedResult = true; 2636 2637 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2638 2639 final class Wrapper extends InputConnectionWrapper { 2640 private Wrapper(InputConnection target) { 2641 super(target, false); 2642 } 2643 2644 @Override 2645 public boolean commitContent(InputContentInfo inputContentInfo, int flags, 2646 Bundle opts) { 2647 methodCallVerifier.onMethodCalled(args -> { 2648 args.putParcelable("inputContentInfo", inputContentInfo); 2649 args.putInt("flags", flags); 2650 args.putBundle("opts", opts); 2651 }); 2652 return unexpectedResult; 2653 } 2654 } 2655 2656 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2657 // Memorize the current InputConnection. 2658 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 2659 2660 // Let unbindInput happen. 2661 triggerUnbindInput(); 2662 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 2663 2664 // Now IC#getTextAfterCursor() for the memorized IC should fail fast. 2665 final ImeEvent result = expectCommand(stream, session.callCommitContent( 2666 new InputContentInfo(Uri.parse("content://com.example/path"), 2667 new ClipDescription("sample content", new String[]{"image/png"}), 2668 Uri.parse("https://example.com")), 0, null), TIMEOUT); 2669 assertFalse("Once unbindInput() happened, IC#commitContent() returns false", 2670 result.getReturnBooleanValue()); 2671 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 2672 methodCallVerifier.assertNotCalled( 2673 "Once unbindInput() happened, IC#commitContent() fails fast."); 2674 }); 2675 } 2676 2677 /** 2678 * Verify that {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} fails when 2679 * the target app does not implement it. This can happen if the app was built before 2680 * {@link android.os.Build.VERSION_CODES#N_MR1}. 2681 */ 2682 @Test 2683 public void testCommitContentFailWithMethodMissing() throws Exception { 2684 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 2685 final ImeCommand command = session.callCommitContent( 2686 new InputContentInfo(Uri.parse("content://com.example/path"), 2687 new ClipDescription("sample content", new String[]{"image/png"}), 2688 Uri.parse("https://example.com")), 0, null); 2689 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 2690 // CAVEAT: this behavior is a bit questionable and may change in a future version. 2691 assertFalse("Currently IC#commitContent() returns false when the target app does not" 2692 + " implement it.", result.getReturnBooleanValue()); 2693 }); 2694 } 2695 2696 /** 2697 * Test {@link InputConnection#deleteSurroundingText(int, int)} works as expected. 2698 */ 2699 @Test 2700 public void testDeleteSurroundingText() throws Exception { 2701 final int expectedBeforeLength = 5; 2702 final int expectedAfterLength = 4; 2703 // Intentionally let the app return "false" to confirm that IME still receives "true". 2704 final boolean returnedResult = false; 2705 2706 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2707 2708 final class Wrapper extends InputConnectionWrapper { 2709 private Wrapper(InputConnection target) { 2710 super(target, false); 2711 } 2712 2713 @Override 2714 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 2715 methodCallVerifier.onMethodCalled(args -> { 2716 args.putInt("beforeLength", beforeLength); 2717 args.putInt("afterLength", afterLength); 2718 }); 2719 return returnedResult; 2720 } 2721 } 2722 2723 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2724 final ImeCommand command = 2725 session.callDeleteSurroundingText(expectedBeforeLength, expectedAfterLength); 2726 assertTrue("deleteSurroundingText() always returns true unless RemoteException is" 2727 + " thrown", expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 2728 methodCallVerifier.expectCalledOnce(args -> { 2729 assertEquals(expectedBeforeLength, args.getInt("beforeLength")); 2730 assertEquals(expectedAfterLength, args.getInt("afterLength")); 2731 }, TIMEOUT); 2732 }); 2733 } 2734 2735 /** 2736 * Test {@link InputConnection#deleteSurroundingText(int, int)} fails fast once 2737 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 2738 */ 2739 @Test 2740 public void testDeleteSurroundingTextAfterUnbindInput() throws Exception { 2741 final boolean returnedResult = true; 2742 2743 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2744 2745 final class Wrapper extends InputConnectionWrapper { 2746 private Wrapper(InputConnection target) { 2747 super(target, false); 2748 } 2749 2750 @Override 2751 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 2752 methodCallVerifier.onMethodCalled(args -> { 2753 args.putInt("beforeLength", beforeLength); 2754 args.putInt("afterLength", afterLength); 2755 }); 2756 return returnedResult; 2757 } 2758 } 2759 2760 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2761 // Memorize the current InputConnection. 2762 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 2763 2764 // Let unbindInput happen. 2765 triggerUnbindInput(); 2766 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 2767 2768 // Now IC#deleteSurroundingText() for the memorized IC should fail fast. 2769 final ImeCommand command = session.callDeleteSurroundingText(3, 4); 2770 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 2771 // CAVEAT: this behavior is a bit questionable and may change in a future version. 2772 assertTrue("Currently IC#deleteSurroundingText() still returns true even after" 2773 + " unbindInput().", result.getReturnBooleanValue()); 2774 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 2775 2776 // Make sure that the app does not receive the call (for a while). 2777 methodCallVerifier.expectNotCalled( 2778 "Once unbindInput() happened, IC#deleteSurroundingText() fails fast.", 2779 EXPECTED_NOT_CALLED_TIMEOUT); 2780 }); 2781 } 2782 2783 /** 2784 * Test {@link InputConnection#deleteSurroundingText(int, int)} works as expected for 2785 * {@link android.accessibilityservice.InputMethod}. 2786 */ 2787 @Test 2788 public void testDeleteSurroundingTextForA11y() throws Exception { 2789 final int expectedBeforeLength = 5; 2790 final int expectedAfterLength = 4; 2791 // Intentionally let the app return "false" to confirm that IME still receives "true". 2792 final boolean returnedResult = false; 2793 2794 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2795 2796 final class Wrapper extends InputConnectionWrapper { 2797 private Wrapper(InputConnection target) { 2798 super(target, false); 2799 } 2800 2801 @Override 2802 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 2803 methodCallVerifier.onMethodCalled(args -> { 2804 args.putInt("beforeLength", beforeLength); 2805 args.putInt("afterLength", afterLength); 2806 }); 2807 return returnedResult; 2808 } 2809 } 2810 2811 testA11yInputConnection(Wrapper::new, (session, stream) -> { 2812 final var command = 2813 session.callDeleteSurroundingText(expectedBeforeLength, expectedAfterLength); 2814 expectA11yImeCommand(stream, command, TIMEOUT); 2815 methodCallVerifier.expectCalledOnce(args -> { 2816 assertEquals(expectedBeforeLength, args.getInt("beforeLength")); 2817 assertEquals(expectedAfterLength, args.getInt("afterLength")); 2818 }, TIMEOUT); 2819 }); 2820 } 2821 2822 /** 2823 * Test {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)} works as expected. 2824 */ 2825 @Test 2826 public void testDeleteSurroundingTextInCodePoints() throws Exception { 2827 final int expectedBeforeLength = 5; 2828 final int expectedAfterLength = 4; 2829 // Intentionally let the app return "false" to confirm that IME still receives "true". 2830 final boolean returnedResult = false; 2831 2832 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2833 2834 final class Wrapper extends InputConnectionWrapper { 2835 private Wrapper(InputConnection target) { 2836 super(target, false); 2837 } 2838 2839 @Override 2840 public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { 2841 methodCallVerifier.onMethodCalled(args -> { 2842 args.putInt("beforeLength", beforeLength); 2843 args.putInt("afterLength", afterLength); 2844 }); 2845 return returnedResult; 2846 } 2847 } 2848 2849 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2850 final ImeCommand command = session.callDeleteSurroundingTextInCodePoints( 2851 expectedBeforeLength, expectedAfterLength); 2852 assertTrue("deleteSurroundingText() always returns true unless RemoteException is" 2853 + " thrown", expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 2854 methodCallVerifier.expectCalledOnce(args -> { 2855 assertEquals(expectedBeforeLength, args.getInt("beforeLength")); 2856 assertEquals(expectedAfterLength, args.getInt("afterLength")); 2857 }, TIMEOUT); 2858 }); 2859 } 2860 2861 /** 2862 * Test {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)} fails fast once 2863 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 2864 */ 2865 @Test 2866 public void testDeleteSurroundingTextInCodePointsAfterUnbindInput() throws Exception { 2867 final boolean returnedResult = true; 2868 2869 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2870 2871 final class Wrapper extends InputConnectionWrapper { 2872 private Wrapper(InputConnection target) { 2873 super(target, false); 2874 } 2875 2876 @Override 2877 public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { 2878 methodCallVerifier.onMethodCalled(args -> { 2879 args.putInt("beforeLength", beforeLength); 2880 args.putInt("afterLength", afterLength); 2881 }); 2882 return returnedResult; 2883 } 2884 } 2885 2886 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2887 // Memorize the current InputConnection. 2888 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 2889 2890 // Let unbindInput happen. 2891 triggerUnbindInput(); 2892 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 2893 2894 // Now IC#deleteSurroundingTextInCodePoints() for the memorized IC should fail fast. 2895 final ImeCommand command = session.callDeleteSurroundingTextInCodePoints(3, 4); 2896 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 2897 // CAVEAT: this behavior is a bit questionable and may change in a future version. 2898 assertTrue("Currently IC#deleteSurroundingTextInCodePoints() still returns true even" 2899 + " after unbindInput().", result.getReturnBooleanValue()); 2900 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 2901 2902 // Make sure that the app does not receive the call (for a while). 2903 methodCallVerifier.expectNotCalled( 2904 "Once unbindInput() happened, IC#deleteSurroundingTextInCodePoints() fails" 2905 + " fast.", EXPECTED_NOT_CALLED_TIMEOUT); 2906 }); 2907 } 2908 2909 /** 2910 * Verify that the app does not crash even if it does not implement 2911 * {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}, which can happen if the 2912 * app was built before {@link android.os.Build.VERSION_CODES#N}. 2913 */ 2914 @Test 2915 public void testDeleteSurroundingTextInCodePointsFailWithMethodMissing() throws Exception { 2916 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 2917 final ImeCommand command = session.callDeleteSurroundingTextInCodePoints(1, 2); 2918 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 2919 assertTrue("IC#deleteSurroundingTextInCodePoints() returns true even when the target" 2920 + " app does not implement it.", result.getReturnBooleanValue()); 2921 }); 2922 } 2923 2924 /** 2925 * Test {@link InputConnection#commitText(CharSequence, int)} works as expected. 2926 */ 2927 @Test 2928 public void testCommitText() throws Exception { 2929 final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); 2930 final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); 2931 final int expectedNewCursorPosition = 123; 2932 // Intentionally let the app return "false" to confirm that IME still receives "true". 2933 final boolean returnedResult = false; 2934 2935 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2936 2937 final class Wrapper extends InputConnectionWrapper { 2938 private Wrapper(InputConnection target) { 2939 super(target, false); 2940 } 2941 2942 @Override 2943 public boolean commitText(CharSequence text, int newCursorPosition) { 2944 methodCallVerifier.onMethodCalled(args -> { 2945 args.putCharSequence("text", text); 2946 args.putInt("newCursorPosition", newCursorPosition); 2947 }); 2948 2949 return returnedResult; 2950 } 2951 } 2952 2953 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2954 final ImeCommand command = 2955 session.callCommitText(expectedText, expectedNewCursorPosition); 2956 assertTrue("commitText() always returns true unless RemoteException is thrown", 2957 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 2958 methodCallVerifier.expectCalledOnce(args -> { 2959 assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); 2960 assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); 2961 }, TIMEOUT); 2962 }); 2963 } 2964 2965 /** 2966 * Test {@link InputConnection#commitText(CharSequence, int)} fails fast once 2967 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 2968 */ 2969 @Test 2970 public void testCommitTextAfterUnbindInput() throws Exception { 2971 final boolean returnedResult = true; 2972 2973 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 2974 2975 final class Wrapper extends InputConnectionWrapper { 2976 private Wrapper(InputConnection target) { 2977 super(target, false); 2978 } 2979 2980 @Override 2981 public boolean commitText(CharSequence text, int newCursorPosition) { 2982 methodCallVerifier.onMethodCalled(args -> { 2983 args.putCharSequence("text", text); 2984 args.putInt("newCursorPosition", newCursorPosition); 2985 }); 2986 return returnedResult; 2987 } 2988 } 2989 2990 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 2991 // Memorize the current InputConnection. 2992 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 2993 2994 // Let unbindInput happen. 2995 triggerUnbindInput(); 2996 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 2997 2998 // Now IC#getTextAfterCursor() for the memorized IC should fail fast. 2999 final ImeEvent result = expectCommand(stream, 3000 session.callCommitText("text", 1), TIMEOUT); 3001 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3002 assertTrue("Currently IC#commitText() still returns true even after unbindInput().", 3003 result.getReturnBooleanValue()); 3004 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3005 3006 // Make sure that the app does not receive the call (for a while). 3007 methodCallVerifier.expectNotCalled( 3008 "Once unbindInput() happened, IC#commitText() fails fast.", 3009 EXPECTED_NOT_CALLED_TIMEOUT); 3010 }); 3011 } 3012 3013 /** 3014 * Test {@link InputConnection#commitText(CharSequence, int, TextAttribute)} works as expected. 3015 */ 3016 @Test 3017 public void testCommitTextWithTextAttribute() throws Exception { 3018 final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); 3019 final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); 3020 final int expectedNewCursorPosition = 123; 3021 final ArrayList<String> expectedSuggestions = new ArrayList<>(); 3022 expectedSuggestions.add("test"); 3023 final TextAttribute expectedTextAttribute = new TextAttribute.Builder() 3024 .setTextConversionSuggestions(expectedSuggestions).build(); 3025 // Intentionally let the app return "false" to confirm that IME still receives "true". 3026 final boolean returnedResult = false; 3027 3028 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3029 3030 final class Wrapper extends InputConnectionWrapper { 3031 private Wrapper(InputConnection target) { 3032 super(target, false); 3033 } 3034 3035 @Override 3036 public boolean commitText( 3037 CharSequence text, int newCursorPosition, TextAttribute textAttribute) { 3038 methodCallVerifier.onMethodCalled(args -> { 3039 args.putCharSequence("text", text); 3040 args.putInt("newCursorPosition", newCursorPosition); 3041 args.putParcelable("textAttribute", textAttribute); 3042 }); 3043 3044 return returnedResult; 3045 } 3046 } 3047 3048 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3049 final ImeCommand command = session.callCommitText( 3050 expectedText, expectedNewCursorPosition, expectedTextAttribute); 3051 assertTrue("commitText() always returns true unless RemoteException is thrown", 3052 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3053 methodCallVerifier.expectCalledOnce(args -> { 3054 assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); 3055 assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); 3056 final TextAttribute textAttribute = args.getParcelable("textAttribute"); 3057 assertThat(textAttribute).isNotNull(); 3058 assertThat(textAttribute.getTextConversionSuggestions()) 3059 .containsExactlyElementsIn(expectedSuggestions); 3060 }, TIMEOUT); 3061 }); 3062 } 3063 3064 /** 3065 * Test {@link InputConnection#commitText(CharSequence, int, TextAttribute)} fails fast once 3066 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3067 */ 3068 @Test 3069 public void testCommitTextAfterUnbindInputWithTextAttribute() throws Exception { 3070 final boolean returnedResult = true; 3071 3072 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3073 3074 final class Wrapper extends InputConnectionWrapper { 3075 private Wrapper(InputConnection target) { 3076 super(target, false); 3077 } 3078 3079 @Override 3080 public boolean commitText( 3081 CharSequence text, int newCursorPosition, TextAttribute textAttribute) { 3082 methodCallVerifier.onMethodCalled(args -> { 3083 args.putCharSequence("text", text); 3084 args.putInt("newCursorPosition", newCursorPosition); 3085 args.putParcelable("textAttribute", textAttribute); 3086 }); 3087 return returnedResult; 3088 } 3089 } 3090 3091 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3092 // Memorize the current InputConnection. 3093 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3094 3095 // Let unbindInput happen. 3096 triggerUnbindInput(); 3097 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3098 3099 // Now IC#getTextAfterCursor() for the memorized IC should fail fast. 3100 final ImeEvent result = expectCommand(stream, 3101 session.callCommitText("text", 1, 3102 new TextAttribute.Builder().setTextConversionSuggestions( 3103 Collections.singletonList("test")).build()), 3104 TIMEOUT); 3105 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3106 assertTrue("Currently IC#commitText() still returns true even after unbindInput().", 3107 result.getReturnBooleanValue()); 3108 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3109 3110 // Make sure that the app does not receive the call (for a while). 3111 methodCallVerifier.expectNotCalled( 3112 "Once unbindInput() happened, IC#commitText() fails fast.", 3113 EXPECTED_NOT_CALLED_TIMEOUT); 3114 }); 3115 } 3116 3117 /** 3118 * Test {@link InputConnection#commitText(CharSequence, int, TextAttribute)} works as expected 3119 * for {@link android.accessibilityservice.InputMethod}. 3120 */ 3121 @Test 3122 public void testCommitTextWithTextAttributeForA11y() throws Exception { 3123 final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); 3124 final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); 3125 final int expectedNewCursorPosition = 123; 3126 final ArrayList<String> expectedSuggestions = new ArrayList<>(); 3127 expectedSuggestions.add("test"); 3128 final TextAttribute expectedTextAttribute = new TextAttribute.Builder() 3129 .setTextConversionSuggestions(expectedSuggestions).build(); 3130 // Intentionally let the app return "false" to confirm that IME still receives "true". 3131 final boolean returnedResult = false; 3132 3133 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3134 3135 final class Wrapper extends InputConnectionWrapper { 3136 private Wrapper(InputConnection target) { 3137 super(target, false); 3138 } 3139 3140 @Override 3141 public boolean commitText( 3142 CharSequence text, int newCursorPosition, TextAttribute textAttribute) { 3143 methodCallVerifier.onMethodCalled(args -> { 3144 args.putCharSequence("text", text); 3145 args.putInt("newCursorPosition", newCursorPosition); 3146 args.putParcelable("textAttribute", textAttribute); 3147 }); 3148 3149 return returnedResult; 3150 } 3151 } 3152 3153 testA11yInputConnection(Wrapper::new, (session, stream) -> { 3154 final var command = session.callCommitText( 3155 expectedText, expectedNewCursorPosition, expectedTextAttribute); 3156 expectA11yImeCommand(stream, command, TIMEOUT); 3157 methodCallVerifier.expectCalledOnce(args -> { 3158 assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); 3159 assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); 3160 final var textAttribute = args.getParcelable("textAttribute", TextAttribute.class); 3161 assertThat(textAttribute).isNotNull(); 3162 assertThat(textAttribute.getTextConversionSuggestions()) 3163 .containsExactlyElementsIn(expectedSuggestions); 3164 }, TIMEOUT); 3165 }); 3166 } 3167 3168 /** 3169 * Test {@link android.accessibilityservice.InputMethod.AccessibilityInputConnection#commitText( 3170 * CharSequence, int, TextAttribute)} finishes any existing composing text. 3171 */ 3172 @Test 3173 public void testCommitTextFromA11yFinishesExistingComposition() throws Exception { 3174 final MethodCallVerifier endBatchEditVerifier = new MethodCallVerifier(); 3175 final CopyOnWriteArrayList<String> callHistory = new CopyOnWriteArrayList<>(); 3176 3177 final class Wrapper extends InputConnectionWrapper { 3178 private int mBatchEditCount = 0; 3179 3180 private Wrapper(InputConnection target) { 3181 super(target, false); 3182 } 3183 3184 @Override 3185 public boolean setComposingText(CharSequence text, int newCursorPosition, 3186 TextAttribute textAttribute) { 3187 callHistory.add("setComposingText"); 3188 return true; 3189 } 3190 3191 @Override 3192 public boolean beginBatchEdit() { 3193 callHistory.add("beginBatchEdit"); 3194 ++mBatchEditCount; 3195 return true; 3196 } 3197 3198 @Override 3199 public boolean finishComposingText() { 3200 callHistory.add("finishComposingText"); 3201 return true; 3202 } 3203 3204 @Override 3205 public boolean commitText( 3206 CharSequence text, int newCursorPosition, TextAttribute textAttribute) { 3207 callHistory.add("commitText"); 3208 return true; 3209 } 3210 3211 @Override 3212 public boolean endBatchEdit() { 3213 callHistory.add("endBatchEdit"); 3214 --mBatchEditCount; 3215 final boolean batchEditStillInProgress = mBatchEditCount > 0; 3216 if (!batchEditStillInProgress) { 3217 endBatchEditVerifier.onMethodCalled(args -> { }); 3218 } 3219 return batchEditStillInProgress; 3220 } 3221 } 3222 3223 testInputConnection(Wrapper::new, (imeSession, imeStream, a11ySession, a11yStream) -> { 3224 expectCommand(imeStream, imeSession.callSetComposingText("fromIme", 1, null), TIMEOUT); 3225 expectA11yImeCommand(a11yStream, a11ySession.callCommitText("fromA11y", 1, null), 3226 TIMEOUT); 3227 endBatchEditVerifier.expectCalledOnce(args -> { }, TIMEOUT); 3228 assertThat(callHistory).containsExactly( 3229 "setComposingText", 3230 "beginBatchEdit", 3231 "finishComposingText", 3232 "commitText", 3233 "endBatchEdit").inOrder(); 3234 }); 3235 } 3236 3237 /** 3238 * Test {@link InputConnection#setComposingText(CharSequence, int)} works as expected. 3239 */ 3240 @Test 3241 public void testSetComposingText() throws Exception { 3242 final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); 3243 final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); 3244 final int expectedNewCursorPosition = 123; 3245 // Intentionally let the app return "false" to confirm that IME still receives "true". 3246 final boolean returnedResult = false; 3247 3248 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3249 3250 final class Wrapper extends InputConnectionWrapper { 3251 private Wrapper(InputConnection target) { 3252 super(target, false); 3253 } 3254 3255 @Override 3256 public boolean setComposingText(CharSequence text, int newCursorPosition) { 3257 methodCallVerifier.onMethodCalled(args -> { 3258 args.putCharSequence("text", text); 3259 args.putInt("newCursorPosition", newCursorPosition); 3260 }); 3261 return returnedResult; 3262 } 3263 } 3264 3265 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3266 final ImeCommand command = 3267 session.callSetComposingText(expectedText, expectedNewCursorPosition); 3268 assertTrue("setComposingText() always returns true unless RemoteException is thrown", 3269 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3270 methodCallVerifier.expectCalledOnce(args -> { 3271 assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); 3272 assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); 3273 }, TIMEOUT); 3274 }); 3275 } 3276 3277 /** 3278 * Test {@link InputConnection#setComposingText(CharSequence, int)} fails fast once 3279 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3280 */ 3281 @Test 3282 public void testSetComposingTextAfterUnbindInput() throws Exception { 3283 final boolean returnedResult = true; 3284 3285 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3286 3287 final class Wrapper extends InputConnectionWrapper { 3288 private Wrapper(InputConnection target) { 3289 super(target, false); 3290 } 3291 3292 @Override 3293 public boolean setComposingText(CharSequence text, int newCursorPosition) { 3294 methodCallVerifier.onMethodCalled(args -> { 3295 args.putCharSequence("text", text); 3296 args.putInt("newCursorPosition", newCursorPosition); 3297 }); 3298 return returnedResult; 3299 } 3300 } 3301 3302 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3303 // Memorize the current InputConnection. 3304 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3305 3306 // Let unbindInput happen. 3307 triggerUnbindInput(); 3308 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3309 3310 // Now this API call on the memorized IC should fail fast. 3311 final ImeCommand command = session.callSetComposingText("text", 1); 3312 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3313 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3314 assertTrue("Currently IC#setComposingText() still returns true even after " 3315 + "unbindInput().", result.getReturnBooleanValue()); 3316 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3317 3318 // Make sure that the app does not receive the call (for a while). 3319 methodCallVerifier.expectNotCalled( 3320 "Once unbindInput() happened, IC#setComposingText() fails fast.", 3321 EXPECTED_NOT_CALLED_TIMEOUT); 3322 }); 3323 } 3324 3325 /** 3326 * Test {@link InputConnection#setComposingText(CharSequence, int, TextAttribute)} 3327 * works as expected. 3328 */ 3329 @Test 3330 public void testSetComposingTextWithTextAttribute() throws Exception { 3331 final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); 3332 final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); 3333 final int expectedNewCursorPosition = 123; 3334 final ArrayList<String> expectedSuggestions = new ArrayList<>(); 3335 expectedSuggestions.add("test"); 3336 final TextAttribute expectedTextAttribute = new TextAttribute.Builder() 3337 .setTextConversionSuggestions(expectedSuggestions).build(); 3338 // Intentionally let the app return "false" to confirm that IME still receives "true". 3339 final boolean returnedResult = false; 3340 3341 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3342 3343 final class Wrapper extends InputConnectionWrapper { 3344 private Wrapper(InputConnection target) { 3345 super(target, false); 3346 } 3347 3348 @Override 3349 public boolean setComposingText(CharSequence text, int newCursorPosition, 3350 TextAttribute textAttribute) { 3351 methodCallVerifier.onMethodCalled(args -> { 3352 args.putCharSequence("text", text); 3353 args.putInt("newCursorPosition", newCursorPosition); 3354 args.putParcelable("textAttribute", textAttribute); 3355 }); 3356 return returnedResult; 3357 } 3358 } 3359 3360 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3361 final ImeCommand command = session.callSetComposingText( 3362 expectedText, expectedNewCursorPosition, expectedTextAttribute); 3363 assertTrue("testSetComposingTextWithTextAttribute() always returns true unless" 3364 + " RemoteException is thrown", 3365 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3366 methodCallVerifier.expectCalledOnce(args -> { 3367 assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); 3368 assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); 3369 final TextAttribute textAttribute = args.getParcelable("textAttribute"); 3370 assertThat(textAttribute).isNotNull(); 3371 assertThat(textAttribute.getTextConversionSuggestions()) 3372 .containsExactlyElementsIn(expectedSuggestions); 3373 }, TIMEOUT); 3374 }); 3375 } 3376 3377 /** 3378 * Test {@link InputConnection#setComposingText(CharSequence, int, TextAttribute)} fails fast 3379 * once {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3380 */ 3381 @Test 3382 public void testSetComposingTextAfterUnbindInputWithTextAttribute() throws Exception { 3383 final boolean returnedResult = true; 3384 3385 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3386 3387 final class Wrapper extends InputConnectionWrapper { 3388 private Wrapper(InputConnection target) { 3389 super(target, false); 3390 } 3391 3392 @Override 3393 public boolean setComposingText(CharSequence text, int newCursorPosition, 3394 TextAttribute textAttribute) { 3395 methodCallVerifier.onMethodCalled(args -> { 3396 args.putCharSequence("text", text); 3397 args.putInt("newCursorPosition", newCursorPosition); 3398 args.putParcelable("textAttribute", textAttribute); 3399 }); 3400 return returnedResult; 3401 } 3402 } 3403 3404 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3405 // Memorize the current InputConnection. 3406 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3407 3408 // Let unbindInput happen. 3409 triggerUnbindInput(); 3410 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3411 3412 // Now this API call on the memorized IC should fail fast. 3413 final ImeCommand command = session.callSetComposingText( 3414 "text", 1, new TextAttribute.Builder() 3415 .setTextConversionSuggestions(Collections.singletonList("test")) 3416 .build()); 3417 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3418 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3419 assertTrue("Currently IC#setComposingText() still returns true even after " 3420 + "unbindInput().", result.getReturnBooleanValue()); 3421 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3422 3423 // Make sure that the app does not receive the call (for a while). 3424 methodCallVerifier.expectNotCalled( 3425 "Once unbindInput() happened, IC#setComposingText() fails fast.", 3426 EXPECTED_NOT_CALLED_TIMEOUT); 3427 }); 3428 } 3429 3430 /** 3431 * Test {@link InputConnection#setComposingRegion(int, int)} works as expected. 3432 */ 3433 @Test 3434 public void testSetComposingRegion() throws Exception { 3435 final int expectedStart = 3; 3436 final int expectedEnd = 17; 3437 // Intentionally let the app return "false" to confirm that IME still receives "true". 3438 final boolean returnedResult = false; 3439 3440 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3441 3442 final class Wrapper extends InputConnectionWrapper { 3443 private Wrapper(InputConnection target) { 3444 super(target, false); 3445 } 3446 3447 @Override 3448 public boolean setComposingRegion(int start, int end) { 3449 methodCallVerifier.onMethodCalled(args -> { 3450 args.putInt("start", start); 3451 args.putInt("end", end); 3452 }); 3453 return returnedResult; 3454 } 3455 } 3456 3457 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3458 final ImeCommand command = session.callSetComposingRegion(expectedStart, expectedEnd); 3459 assertTrue("setComposingRegion() always returns true unless RemoteException is thrown", 3460 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3461 methodCallVerifier.expectCalledOnce(args -> { 3462 assertEquals(expectedStart, args.getInt("start")); 3463 assertEquals(expectedEnd, args.getInt("end")); 3464 }, TIMEOUT); 3465 }); 3466 } 3467 3468 /** 3469 * Test {@link InputConnection#setComposingRegion(int, int)} fails fast once 3470 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3471 */ 3472 @Test 3473 public void testSetComposingRegionTextAfterUnbindInput() throws Exception { 3474 final boolean returnedResult = true; 3475 3476 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3477 3478 final class Wrapper extends InputConnectionWrapper { 3479 private Wrapper(InputConnection target) { 3480 super(target, false); 3481 } 3482 3483 @Override 3484 public boolean setComposingRegion(int start, int end) { 3485 methodCallVerifier.onMethodCalled(args -> { 3486 args.putInt("start", start); 3487 args.putInt("end", end); 3488 }); 3489 return returnedResult; 3490 } 3491 } 3492 3493 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3494 // Memorize the current InputConnection. 3495 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3496 3497 // Let unbindInput happen. 3498 triggerUnbindInput(); 3499 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3500 3501 // Now this API call on the memorized IC should fail fast. 3502 final ImeCommand command = session.callSetComposingRegion(1, 23); 3503 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3504 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3505 assertTrue("Currently IC#setComposingRegion() still returns true even after" 3506 + " unbindInput().", result.getReturnBooleanValue()); 3507 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3508 3509 // Make sure that the app does not receive the call (for a while). 3510 methodCallVerifier.expectNotCalled( 3511 "Once unbindInput() happened, IC#setComposingRegion() fails fast.", 3512 EXPECTED_NOT_CALLED_TIMEOUT); 3513 }); 3514 } 3515 3516 /** 3517 * Verify that the app does not crash even if it does not implement 3518 * {@link InputConnection#setComposingRegion(int, int)}, which can happen if the app was built 3519 * before {@link android.os.Build.VERSION_CODES#GINGERBREAD}. 3520 */ 3521 @Test 3522 public void testSetComposingRegionFailWithMethodMissing() throws Exception { 3523 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 3524 final ImeCommand command = session.callSetComposingRegion(1, 23); 3525 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3526 assertTrue("IC#setComposingRegion() returns true even when the target app does not" 3527 + " implement it.", result.getReturnBooleanValue()); 3528 }); 3529 } 3530 3531 /** 3532 * Test {@link InputConnection#setComposingRegion} works as expected. 3533 */ 3534 @Test 3535 public void testSetComposingRegionWithTextAttribute() throws Exception { 3536 final int expectedStart = 3; 3537 final int expectedEnd = 17; 3538 final ArrayList<String> expectedSuggestions = new ArrayList<>(); 3539 expectedSuggestions.add("test"); 3540 final TextAttribute expectedTextAttribute = new TextAttribute.Builder() 3541 .setTextConversionSuggestions(expectedSuggestions).build(); 3542 // Intentionally let the app return "false" to confirm that IME still receives "true". 3543 final boolean returnedResult = false; 3544 3545 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3546 3547 final class Wrapper extends InputConnectionWrapper { 3548 private Wrapper(InputConnection target) { 3549 super(target, false); 3550 } 3551 3552 @Override 3553 public boolean setComposingRegion( 3554 int start, int end, TextAttribute textAttribute) { 3555 methodCallVerifier.onMethodCalled(args -> { 3556 args.putInt("start", start); 3557 args.putInt("end", end); 3558 args.putParcelable("textAttribute", textAttribute); 3559 }); 3560 return returnedResult; 3561 } 3562 } 3563 3564 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3565 final ImeCommand command = session.callSetComposingRegion( 3566 expectedStart, expectedEnd, expectedTextAttribute); 3567 assertTrue("setComposingRegion() always returns true unless RemoteException is thrown", 3568 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3569 methodCallVerifier.expectCalledOnce(args -> { 3570 assertEquals(expectedStart, args.getInt("start")); 3571 assertEquals(expectedEnd, args.getInt("end")); 3572 final TextAttribute textAttribute = args.getParcelable("textAttribute"); 3573 assertThat(textAttribute).isNotNull(); 3574 assertThat(textAttribute.getTextConversionSuggestions()) 3575 .containsExactlyElementsIn(expectedSuggestions); 3576 }, TIMEOUT); 3577 }); 3578 } 3579 3580 /** 3581 * Test {@link InputConnection#setComposingRegion(int, int, TextAttribute)} fails fast once 3582 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3583 */ 3584 @Test 3585 public void testSetComposingRegionTextAfterUnbindInputWithTextAttribute() throws Exception { 3586 final boolean returnedResult = true; 3587 3588 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3589 3590 final class Wrapper extends InputConnectionWrapper { 3591 private Wrapper(InputConnection target) { 3592 super(target, false); 3593 } 3594 3595 @Override 3596 public boolean setComposingRegion(int start, int end, TextAttribute textAttribute) { 3597 methodCallVerifier.onMethodCalled(args -> { 3598 args.putInt("start", start); 3599 args.putInt("end", end); 3600 args.putParcelable("textAttribute", textAttribute); 3601 }); 3602 return returnedResult; 3603 } 3604 } 3605 3606 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3607 // Memorize the current InputConnection. 3608 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3609 3610 // Let unbindInput happen. 3611 triggerUnbindInput(); 3612 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3613 3614 // Now this API call on the memorized IC should fail fast. 3615 final ImeCommand command = session.callSetComposingRegion(1, 23, 3616 new TextAttribute.Builder().setTextConversionSuggestions( 3617 Collections.singletonList("test")).build()); 3618 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3619 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3620 assertTrue("Currently IC#setComposingRegion() still returns true even after" 3621 + " unbindInput().", result.getReturnBooleanValue()); 3622 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3623 3624 // Make sure that the app does not receive the call (for a while). 3625 methodCallVerifier.expectNotCalled( 3626 "Once unbindInput() happened, IC#setComposingRegion() fails fast.", 3627 EXPECTED_NOT_CALLED_TIMEOUT); 3628 }); 3629 } 3630 3631 /** 3632 * Test {@link InputConnection#finishComposingText()} works as expected. 3633 */ 3634 @Test 3635 public void testFinishComposingText() throws Exception { 3636 // Intentionally let the app return "false" to confirm that IME still receives "true". 3637 final boolean returnedResult = false; 3638 3639 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3640 3641 final class Wrapper extends InputConnectionWrapper { 3642 private Wrapper(InputConnection target) { 3643 super(target, false); 3644 } 3645 3646 @Override 3647 public boolean finishComposingText() { 3648 methodCallVerifier.onMethodCalled(bundle -> { }); 3649 return returnedResult; 3650 } 3651 } 3652 3653 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3654 final ImeCommand command = session.callFinishComposingText(); 3655 assertTrue("finishComposingText() always returns true unless RemoteException is thrown", 3656 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3657 methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); 3658 }); 3659 } 3660 3661 /** 3662 * Test {@link InputConnection#finishComposingText()} fails fast once 3663 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3664 */ 3665 @Test 3666 public void testFinishComposingTextAfterUnbindInput() throws Exception { 3667 final boolean returnedResult = true; 3668 3669 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3670 3671 final class Wrapper extends InputConnectionWrapper { 3672 private Wrapper(InputConnection target) { 3673 super(target, false); 3674 } 3675 3676 @Override 3677 public boolean finishComposingText() { 3678 methodCallVerifier.onMethodCalled(bundle -> { }); 3679 return returnedResult; 3680 } 3681 } 3682 3683 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3684 // Memorize the current InputConnection. 3685 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3686 3687 // Let unbindInput happen. 3688 triggerUnbindInput(); 3689 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3690 3691 // The system internally calls "finishComposingText". So wait for a while then reset 3692 // the verifier before our calling "finishComposingText". 3693 SystemClock.sleep(TIMEOUT); 3694 methodCallVerifier.reset(); 3695 3696 // Now this API call on the memorized IC should fail fast. 3697 final ImeCommand command = session.callFinishComposingText(); 3698 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3699 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3700 assertTrue("Currently IC#finishComposingText() still returns true even after" 3701 + " unbindInput().", result.getReturnBooleanValue()); 3702 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3703 3704 // Make sure that the app does not receive the call (for a while). 3705 methodCallVerifier.expectNotCalled( 3706 "Once unbindInput() happened, IC#finishComposingText() fails fast.", 3707 EXPECTED_NOT_CALLED_TIMEOUT); 3708 }); 3709 } 3710 3711 /** 3712 * Test {@link InputConnection#commitCompletion(CompletionInfo)} works as expected. 3713 */ 3714 @Test 3715 public void testCommitCompletion() throws Exception { 3716 final CompletionInfo expectedCompletionInfo = new CompletionInfo(0x12345678, 0x87654321, 3717 createTestCharSequence("testText", new Annotation("param", "text")), 3718 createTestCharSequence("testLabel", new Annotation("param", "label"))); 3719 // Intentionally let the app return "false" to confirm that IME still receives "true". 3720 final boolean returnedResult = false; 3721 3722 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3723 3724 final class Wrapper extends InputConnectionWrapper { 3725 private Wrapper(InputConnection target) { 3726 super(target, false); 3727 } 3728 3729 @Override 3730 public boolean commitCompletion(CompletionInfo text) { 3731 methodCallVerifier.onMethodCalled(bundle -> { 3732 bundle.putParcelable("text", text); 3733 }); 3734 return returnedResult; 3735 } 3736 } 3737 3738 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3739 final ImeCommand command = session.callCommitCompletion(expectedCompletionInfo); 3740 assertTrue("commitCompletion() always returns true unless RemoteException is thrown", 3741 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3742 methodCallVerifier.expectCalledOnce(args -> { 3743 final CompletionInfo actualCompletionInfo = args.getParcelable("text"); 3744 assertNotNull(actualCompletionInfo); 3745 assertEquals(expectedCompletionInfo.getId(), actualCompletionInfo.getId()); 3746 assertEquals(expectedCompletionInfo.getPosition(), 3747 actualCompletionInfo.getPosition()); 3748 assertEqualsForTestCharSequence(expectedCompletionInfo.getText(), 3749 actualCompletionInfo.getText()); 3750 assertEqualsForTestCharSequence(expectedCompletionInfo.getLabel(), 3751 actualCompletionInfo.getLabel()); 3752 }, TIMEOUT); 3753 }); 3754 } 3755 3756 /** 3757 * Test {@link InputConnection#commitCompletion(CompletionInfo)} fails fast once 3758 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3759 */ 3760 @Test 3761 public void testCommitCompletionAfterUnbindInput() throws Exception { 3762 final boolean returnedResult = true; 3763 3764 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3765 3766 final class Wrapper extends InputConnectionWrapper { 3767 private Wrapper(InputConnection target) { 3768 super(target, false); 3769 } 3770 3771 @Override 3772 public boolean commitCompletion(CompletionInfo text) { 3773 methodCallVerifier.onMethodCalled(bundle -> { 3774 bundle.putParcelable("text", text); 3775 }); 3776 return returnedResult; 3777 } 3778 } 3779 3780 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3781 // Memorize the current InputConnection. 3782 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3783 3784 // Let unbindInput happen. 3785 triggerUnbindInput(); 3786 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3787 3788 // Now this API call on the memorized IC should fail fast. 3789 final ImeCommand command = session.callCommitCompletion(new CompletionInfo( 3790 0x12345678, 0x87654321, 3791 createTestCharSequence("testText", new Annotation("param", "text")), 3792 createTestCharSequence("testLabel", new Annotation("param", "label")))); 3793 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3794 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3795 assertTrue("Currently IC#commitCompletion() still returns true even after" 3796 + " unbindInput().", result.getReturnBooleanValue()); 3797 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3798 3799 // Make sure that the app does not receive the call (for a while). 3800 methodCallVerifier.expectNotCalled( 3801 "Once unbindInput() happened, IC#commitCompletion() fails fast.", 3802 EXPECTED_NOT_CALLED_TIMEOUT); 3803 }); 3804 } 3805 3806 /** 3807 * Test {@link InputConnection#commitCorrection(CorrectionInfo)} works as expected. 3808 */ 3809 @Test 3810 public void testCommitCorrection() throws Exception { 3811 final CorrectionInfo expectedCorrectionInfo = new CorrectionInfo(0x11111111, 3812 createTestCharSequence("testOldText", new Annotation("param", "oldText")), 3813 createTestCharSequence("testNewText", new Annotation("param", "newText"))); 3814 // Intentionally let the app return "false" to confirm that IME still receives "true". 3815 final boolean returnedResult = false; 3816 3817 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3818 3819 final class Wrapper extends InputConnectionWrapper { 3820 private Wrapper(InputConnection target) { 3821 super(target, false); 3822 } 3823 3824 @Override 3825 public boolean commitCorrection(CorrectionInfo correctionInfo) { 3826 methodCallVerifier.onMethodCalled(bundle -> { 3827 bundle.putParcelable("correctionInfo", correctionInfo); 3828 }); 3829 return returnedResult; 3830 } 3831 } 3832 3833 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3834 final ImeCommand command = session.callCommitCorrection(expectedCorrectionInfo); 3835 assertTrue("commitCorrection() always returns true unless RemoteException is thrown", 3836 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3837 methodCallVerifier.expectCalledOnce(args -> { 3838 final CorrectionInfo actualCorrectionInfo = args.getParcelable("correctionInfo"); 3839 assertNotNull(actualCorrectionInfo); 3840 assertEquals(expectedCorrectionInfo.getOffset(), 3841 actualCorrectionInfo.getOffset()); 3842 assertEqualsForTestCharSequence(expectedCorrectionInfo.getOldText(), 3843 actualCorrectionInfo.getOldText()); 3844 assertEqualsForTestCharSequence(expectedCorrectionInfo.getNewText(), 3845 actualCorrectionInfo.getNewText()); 3846 }, TIMEOUT); 3847 }); 3848 } 3849 3850 /** 3851 * Test {@link InputConnection#commitCorrection(CorrectionInfo)} fails fast once 3852 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3853 */ 3854 @Test 3855 public void testCommitCorrectionAfterUnbindInput() throws Exception { 3856 final boolean returnedResult = true; 3857 3858 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3859 3860 final class Wrapper extends InputConnectionWrapper { 3861 private Wrapper(InputConnection target) { 3862 super(target, false); 3863 } 3864 3865 @Override 3866 public boolean commitCorrection(CorrectionInfo correctionInfo) { 3867 methodCallVerifier.onMethodCalled(bundle -> { 3868 bundle.putParcelable("correctionInfo", correctionInfo); 3869 }); 3870 return returnedResult; 3871 } 3872 } 3873 3874 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3875 // Memorize the current InputConnection. 3876 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3877 3878 // Let unbindInput happen. 3879 triggerUnbindInput(); 3880 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3881 3882 // Now this API call on the memorized IC should fail fast. 3883 final ImeCommand command = session.callCommitCorrection(new CorrectionInfo(0x11111111, 3884 createTestCharSequence("testOldText", new Annotation("param", "oldText")), 3885 createTestCharSequence("testNewText", new Annotation("param", "newText")))); 3886 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3887 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3888 assertTrue("Currently IC#commitCorrection() still returns true even after" 3889 + " unbindInput().", result.getReturnBooleanValue()); 3890 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3891 3892 // Make sure that the app does not receive the call (for a while). 3893 methodCallVerifier.expectNotCalled( 3894 "Once unbindInput() happened, IC#commitCorrection() fails fast.", 3895 EXPECTED_NOT_CALLED_TIMEOUT); 3896 }); 3897 } 3898 3899 /** 3900 * Verify that the app does not crash even if it does not implement 3901 * {@link InputConnection#commitCorrection(CorrectionInfo)}, which can happen if the app was 3902 * built before {@link android.os.Build.VERSION_CODES#HONEYCOMB}. 3903 */ 3904 @Test 3905 public void testCommitCorrectionFailWithMethodMissing() throws Exception { 3906 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 3907 final ImeCommand command = session.callCommitCorrection(new CorrectionInfo(0x11111111, 3908 createTestCharSequence("testOldText", new Annotation("param", "oldText")), 3909 createTestCharSequence("testNewText", new Annotation("param", "newText")))); 3910 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3911 assertTrue("IC#commitCorrection() returns true even when the target app does not" 3912 + " implement it.", result.getReturnBooleanValue()); 3913 }); 3914 } 3915 3916 /** 3917 * Test {@link InputConnection#setSelection(int, int)} works as expected. 3918 */ 3919 @Test 3920 public void testSetSelection() throws Exception { 3921 final int expectedStart = 123; 3922 final int expectedEnd = 456; 3923 // Intentionally let the app return "false" to confirm that IME still receives "true". 3924 final boolean returnedResult = false; 3925 3926 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3927 3928 final class Wrapper extends InputConnectionWrapper { 3929 private Wrapper(InputConnection target) { 3930 super(target, false); 3931 } 3932 3933 @Override 3934 public boolean setSelection(int start, int end) { 3935 methodCallVerifier.onMethodCalled(args -> { 3936 args.putInt("start", start); 3937 args.putInt("end", end); 3938 }); 3939 return returnedResult; 3940 } 3941 } 3942 3943 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3944 final ImeCommand command = session.callSetSelection(expectedStart, expectedEnd); 3945 assertTrue("setSelection() always returns true unless RemoteException is thrown", 3946 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 3947 methodCallVerifier.expectCalledOnce(args -> { 3948 assertEquals(expectedStart, args.getInt("start")); 3949 assertEquals(expectedEnd, args.getInt("end")); 3950 }, TIMEOUT); 3951 }); 3952 } 3953 3954 /** 3955 * Test {@link InputConnection#setSelection(int, int)} fails fast once 3956 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 3957 */ 3958 @Test 3959 public void testSetSelectionTextAfterUnbindInput() throws Exception { 3960 final boolean returnedResult = true; 3961 3962 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 3963 3964 final class Wrapper extends InputConnectionWrapper { 3965 private Wrapper(InputConnection target) { 3966 super(target, false); 3967 } 3968 3969 @Override 3970 public boolean setSelection(int start, int end) { 3971 methodCallVerifier.onMethodCalled(args -> { 3972 args.putInt("start", start); 3973 args.putInt("end", end); 3974 }); 3975 return returnedResult; 3976 } 3977 } 3978 3979 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 3980 // Memorize the current InputConnection. 3981 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 3982 3983 // Let unbindInput happen. 3984 triggerUnbindInput(); 3985 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 3986 3987 // Now this API call on the memorized IC should fail fast. 3988 final ImeCommand command = session.callSetSelection(123, 456); 3989 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 3990 // CAVEAT: this behavior is a bit questionable and may change in a future version. 3991 assertTrue("Currently IC#setSelection() still returns true even after unbindInput().", 3992 result.getReturnBooleanValue()); 3993 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 3994 3995 // Make sure that the app does not receive the call (for a while). 3996 methodCallVerifier.expectNotCalled( 3997 "Once unbindInput() happened, IC#setSelection() fails fast.", 3998 EXPECTED_NOT_CALLED_TIMEOUT); 3999 }); 4000 } 4001 4002 /** 4003 * Test {@link InputConnection#setSelection(int, int)} works as expected for 4004 * {@link android.accessibilityservice.InputMethod}. 4005 */ 4006 @Test 4007 public void testSetSelectionForA11y() throws Exception { 4008 final int expectedStart = 123; 4009 final int expectedEnd = 456; 4010 // Intentionally let the app return "false" to confirm that IME still receives "true". 4011 final boolean returnedResult = false; 4012 4013 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4014 4015 final class Wrapper extends InputConnectionWrapper { 4016 private Wrapper(InputConnection target) { 4017 super(target, false); 4018 } 4019 4020 @Override 4021 public boolean setSelection(int start, int end) { 4022 methodCallVerifier.onMethodCalled(args -> { 4023 args.putInt("start", start); 4024 args.putInt("end", end); 4025 }); 4026 return returnedResult; 4027 } 4028 } 4029 4030 testA11yInputConnection(Wrapper::new, (session, stream) -> { 4031 final var command = session.callSetSelection(expectedStart, expectedEnd); 4032 expectA11yImeCommand(stream, command, TIMEOUT); 4033 methodCallVerifier.expectCalledOnce(args -> { 4034 assertEquals(expectedStart, args.getInt("start")); 4035 assertEquals(expectedEnd, args.getInt("end")); 4036 }, TIMEOUT); 4037 }); 4038 } 4039 4040 /** 4041 * Test {@link InputConnection#performEditorAction(int)} works as expected. 4042 */ 4043 @Test 4044 public void testPerformEditorAction() throws Exception { 4045 final int expectedEditorAction = EditorInfo.IME_ACTION_GO; 4046 // Intentionally let the app return "false" to confirm that IME still receives "true". 4047 final boolean returnedResult = false; 4048 4049 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4050 4051 final class Wrapper extends InputConnectionWrapper { 4052 private Wrapper(InputConnection target) { 4053 super(target, false); 4054 } 4055 4056 @Override 4057 public boolean performEditorAction(int editorAction) { 4058 methodCallVerifier.onMethodCalled(args -> { 4059 args.putInt("editorAction", editorAction); 4060 }); 4061 return returnedResult; 4062 } 4063 } 4064 4065 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4066 final ImeCommand command = session.callPerformEditorAction(expectedEditorAction); 4067 assertTrue("performEditorAction() always returns true unless RemoteException is thrown", 4068 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4069 methodCallVerifier.expectCalledOnce(args -> { 4070 assertEquals(expectedEditorAction, args.getInt("editorAction")); 4071 }, TIMEOUT); 4072 }); 4073 } 4074 4075 /** 4076 * Test {@link InputConnection#performEditorAction(int)} fails fast once 4077 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4078 */ 4079 @Test 4080 public void testPerformEditorActionAfterUnbindInput() throws Exception { 4081 final boolean returnedResult = true; 4082 4083 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4084 4085 final class Wrapper extends InputConnectionWrapper { 4086 private Wrapper(InputConnection target) { 4087 super(target, false); 4088 } 4089 4090 @Override 4091 public boolean performEditorAction(int editorAction) { 4092 methodCallVerifier.onMethodCalled(args -> { 4093 args.putInt("editorAction", editorAction); 4094 }); 4095 return returnedResult; 4096 } 4097 } 4098 4099 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4100 // Memorize the current InputConnection. 4101 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4102 4103 // Let unbindInput happen. 4104 triggerUnbindInput(); 4105 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4106 4107 // Now this API call on the memorized IC should fail fast. 4108 final ImeCommand command = session.callPerformEditorAction(EditorInfo.IME_ACTION_GO); 4109 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4110 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4111 assertTrue("Currently IC#performEditorAction() still returns true even after " 4112 + "unbindInput().", result.getReturnBooleanValue()); 4113 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4114 4115 // Make sure that the app does not receive the call (for a while). 4116 methodCallVerifier.expectNotCalled( 4117 "Once unbindInput() happened, IC#performEditorAction() fails fast.", 4118 EXPECTED_NOT_CALLED_TIMEOUT); 4119 }); 4120 } 4121 4122 /** 4123 * Test {@link InputConnection#performEditorAction(int)} works as expected for 4124 * {@link android.accessibilityservice.InputMethod}. 4125 */ 4126 @Test 4127 public void testPerformEditorActionForA11y() throws Exception { 4128 final int expectedEditorAction = EditorInfo.IME_ACTION_GO; 4129 // Intentionally let the app return "false" to confirm that IME still receives "true". 4130 final boolean returnedResult = false; 4131 4132 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4133 4134 final class Wrapper extends InputConnectionWrapper { 4135 private Wrapper(InputConnection target) { 4136 super(target, false); 4137 } 4138 4139 @Override 4140 public boolean performEditorAction(int editorAction) { 4141 methodCallVerifier.onMethodCalled(args -> { 4142 args.putInt("editorAction", editorAction); 4143 }); 4144 return returnedResult; 4145 } 4146 } 4147 4148 testA11yInputConnection(Wrapper::new, (session, stream) -> { 4149 final var command = session.callPerformEditorAction(expectedEditorAction); 4150 expectA11yImeCommand(stream, command, TIMEOUT); 4151 methodCallVerifier.expectCalledOnce(args -> { 4152 assertEquals(expectedEditorAction, args.getInt("editorAction")); 4153 }, TIMEOUT); 4154 }); 4155 } 4156 4157 /** 4158 * Test {@link InputConnection#performContextMenuAction(int)} works as expected. 4159 */ 4160 @Test 4161 public void testPerformContextMenuAction() throws Exception { 4162 final int expectedId = android.R.id.selectAll; 4163 // Intentionally let the app return "false" to confirm that IME still receives "true". 4164 final boolean returnedResult = false; 4165 4166 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4167 4168 final class Wrapper extends InputConnectionWrapper { 4169 private Wrapper(InputConnection target) { 4170 super(target, false); 4171 } 4172 4173 @Override 4174 public boolean performContextMenuAction(int id) { 4175 methodCallVerifier.onMethodCalled(args -> { 4176 args.putInt("id", id); 4177 }); 4178 return returnedResult; 4179 } 4180 } 4181 4182 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4183 final ImeCommand command = session.callPerformContextMenuAction(expectedId); 4184 assertTrue("performContextMenuAction() always returns true unless RemoteException is " 4185 + "thrown", 4186 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4187 methodCallVerifier.expectCalledOnce(args -> { 4188 assertEquals(expectedId, args.getInt("id")); 4189 }, TIMEOUT); 4190 }); 4191 } 4192 4193 /** 4194 * Test {@link InputConnection#performContextMenuAction(int)} fails fast once 4195 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4196 */ 4197 @Test 4198 public void testPerformContextMenuActionAfterUnbindInput() throws Exception { 4199 final boolean returnedResult = true; 4200 4201 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4202 4203 final class Wrapper extends InputConnectionWrapper { 4204 private Wrapper(InputConnection target) { 4205 super(target, false); 4206 } 4207 4208 @Override 4209 public boolean performContextMenuAction(int id) { 4210 methodCallVerifier.onMethodCalled(args -> { 4211 args.putInt("id", id); 4212 }); 4213 return returnedResult; 4214 } 4215 } 4216 4217 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4218 // Memorize the current InputConnection. 4219 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4220 4221 // Let unbindInput happen. 4222 triggerUnbindInput(); 4223 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4224 4225 // Now this API call on the memorized IC should fail fast. 4226 final ImeCommand command = session.callPerformEditorAction(EditorInfo.IME_ACTION_GO); 4227 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4228 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4229 assertTrue("Currently IC#performContextMenuAction() still returns true even after " 4230 + "unbindInput().", result.getReturnBooleanValue()); 4231 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4232 4233 // Make sure that the app does not receive the call (for a while). 4234 methodCallVerifier.expectNotCalled( 4235 "Once unbindInput() happened, IC#performContextMenuAction() fails fast.", 4236 EXPECTED_NOT_CALLED_TIMEOUT); 4237 }); 4238 } 4239 4240 /** 4241 * Test {@link InputConnection#performContextMenuAction(int)} works as expected 4242 * for {@link android.accessibilityservice.InputMethod}. 4243 */ 4244 @Test 4245 public void testPerformContextMenuActionForA11y() throws Exception { 4246 final int expectedId = android.R.id.selectAll; 4247 // Intentionally let the app return "false" to confirm that IME still receives "true". 4248 final boolean returnedResult = false; 4249 4250 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4251 4252 final class Wrapper extends InputConnectionWrapper { 4253 private Wrapper(InputConnection target) { 4254 super(target, false); 4255 } 4256 4257 @Override 4258 public boolean performContextMenuAction(int id) { 4259 methodCallVerifier.onMethodCalled(args -> { 4260 args.putInt("id", id); 4261 }); 4262 return returnedResult; 4263 } 4264 } 4265 4266 testA11yInputConnection(Wrapper::new, (session, stream) -> { 4267 final var command = session.callPerformContextMenuAction(expectedId); 4268 expectA11yImeCommand(stream, command, TIMEOUT); 4269 methodCallVerifier.expectCalledOnce(args -> { 4270 assertEquals(expectedId, args.getInt("id")); 4271 }, TIMEOUT); 4272 }); 4273 } 4274 4275 /** 4276 * Test {@link InputConnection#beginBatchEdit()} works as expected. 4277 */ 4278 @Test 4279 public void testBeginBatchEdit() throws Exception { 4280 // Intentionally let the app return "false" to confirm that IME still receives "true". 4281 final boolean returnedResult = false; 4282 4283 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4284 4285 final class Wrapper extends InputConnectionWrapper { 4286 private Wrapper(InputConnection target) { 4287 super(target, false); 4288 } 4289 4290 @Override 4291 public boolean beginBatchEdit() { 4292 methodCallVerifier.onMethodCalled(args -> { }); 4293 return returnedResult; 4294 } 4295 } 4296 4297 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4298 final ImeCommand command = session.callBeginBatchEdit(); 4299 assertTrue("beginBatchEdit() always returns true unless RemoteException is thrown", 4300 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4301 methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); 4302 }); 4303 } 4304 4305 /** 4306 * Test {@link InputConnection#beginBatchEdit()} fails fast once 4307 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4308 */ 4309 @Test 4310 public void testBeginBatchEditAfterUnbindInput() throws Exception { 4311 final boolean returnedResult = true; 4312 4313 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4314 4315 final class Wrapper extends InputConnectionWrapper { 4316 private Wrapper(InputConnection target) { 4317 super(target, false); 4318 } 4319 4320 @Override 4321 public boolean beginBatchEdit() { 4322 methodCallVerifier.onMethodCalled(args -> { }); 4323 return returnedResult; 4324 } 4325 } 4326 4327 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4328 // Memorize the current InputConnection. 4329 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4330 4331 // Let unbindInput happen. 4332 triggerUnbindInput(); 4333 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4334 4335 // Now this API call on the memorized IC should fail fast. 4336 final ImeCommand command = session.callBeginBatchEdit(); 4337 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4338 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4339 assertTrue("Currently IC#beginBatchEdit() still returns true even after unbindInput().", 4340 result.getReturnBooleanValue()); 4341 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4342 4343 // Make sure that the app does not receive the call (for a while). 4344 methodCallVerifier.expectNotCalled( 4345 "Once unbindInput() happened, IC#beginBatchEdit() fails fast.", 4346 EXPECTED_NOT_CALLED_TIMEOUT); 4347 }); 4348 } 4349 4350 /** 4351 * Test {@link InputConnection#endBatchEdit()} works as expected. 4352 */ 4353 @Test 4354 public void testEndBatchEdit() throws Exception { 4355 // Intentionally let the app return "false" to confirm that IME still receives "true". 4356 final boolean returnedResult = false; 4357 4358 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4359 4360 final class Wrapper extends InputConnectionWrapper { 4361 private Wrapper(InputConnection target) { 4362 super(target, false); 4363 } 4364 4365 @Override 4366 public boolean endBatchEdit() { 4367 methodCallVerifier.onMethodCalled(args -> { }); 4368 return returnedResult; 4369 } 4370 } 4371 4372 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4373 final ImeCommand command = session.callEndBatchEdit(); 4374 assertTrue("endBatchEdit() always returns true unless RemoteException is thrown", 4375 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4376 methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); 4377 }); 4378 } 4379 4380 /** 4381 * Test {@link InputConnection#endBatchEdit()} fails fast once 4382 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4383 */ 4384 @Test 4385 public void testEndBatchEditAfterUnbindInput() throws Exception { 4386 final boolean returnedResult = true; 4387 4388 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4389 4390 final class Wrapper extends InputConnectionWrapper { 4391 private Wrapper(InputConnection target) { 4392 super(target, false); 4393 } 4394 4395 @Override 4396 public boolean endBatchEdit() { 4397 methodCallVerifier.onMethodCalled(args -> { }); 4398 return returnedResult; 4399 } 4400 } 4401 4402 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4403 // Memorize the current InputConnection. 4404 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4405 4406 // Let unbindInput happen. 4407 triggerUnbindInput(); 4408 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4409 4410 // Now this API call on the memorized IC should fail fast. 4411 final ImeCommand command = session.callEndBatchEdit(); 4412 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4413 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4414 assertTrue("Currently IC#endBatchEdit() still returns true even after unbindInput().", 4415 result.getReturnBooleanValue()); 4416 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4417 4418 // Make sure that the app does not receive the call (for a while). 4419 methodCallVerifier.expectNotCalled( 4420 "Once unbindInput() happened, IC#endBatchEdit() fails fast.", 4421 EXPECTED_NOT_CALLED_TIMEOUT); 4422 }); 4423 } 4424 4425 /** 4426 * Test {@link InputConnection#sendKeyEvent(KeyEvent)} works as expected. 4427 */ 4428 @Test 4429 public void testSendKeyEvent() throws Exception { 4430 final KeyEvent expectedKeyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X); 4431 // Intentionally let the app return "false" to confirm that IME still receives "true". 4432 final boolean returnedResult = false; 4433 4434 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4435 4436 final class Wrapper extends InputConnectionWrapper { 4437 private Wrapper(InputConnection target) { 4438 super(target, false); 4439 } 4440 4441 @Override 4442 public boolean sendKeyEvent(KeyEvent event) { 4443 methodCallVerifier.onMethodCalled(args -> { 4444 args.putParcelable("event", event); 4445 }); 4446 return returnedResult; 4447 } 4448 } 4449 4450 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4451 final ImeCommand command = session.callSendKeyEvent(expectedKeyEvent); 4452 assertTrue("sendKeyEvent() always returns true unless RemoteException is thrown", 4453 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4454 methodCallVerifier.expectCalledOnce(args -> { 4455 final KeyEvent actualKeyEvent = args.getParcelable("event"); 4456 assertNotNull(actualKeyEvent); 4457 assertEquals(expectedKeyEvent.getAction(), actualKeyEvent.getAction()); 4458 assertEquals(expectedKeyEvent.getKeyCode(), actualKeyEvent.getKeyCode()); 4459 }, TIMEOUT); 4460 }); 4461 } 4462 4463 /** 4464 * Test {@link InputConnection#sendKeyEvent(KeyEvent)} fails fast once 4465 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4466 */ 4467 @Test 4468 public void testSendKeyEventAfterUnbindInput() throws Exception { 4469 final boolean returnedResult = true; 4470 4471 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4472 4473 final class Wrapper extends InputConnectionWrapper { 4474 private Wrapper(InputConnection target) { 4475 super(target, false); 4476 } 4477 4478 @Override 4479 public boolean sendKeyEvent(KeyEvent event) { 4480 methodCallVerifier.onMethodCalled(args -> { 4481 args.putParcelable("event", event); 4482 }); 4483 return returnedResult; 4484 } 4485 } 4486 4487 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4488 // Memorize the current InputConnection. 4489 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4490 4491 // Let unbindInput happen. 4492 triggerUnbindInput(); 4493 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4494 4495 // Now this API call on the memorized IC should fail fast. 4496 final ImeCommand command = session.callSendKeyEvent( 4497 new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X)); 4498 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4499 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4500 assertTrue("Currently IC#sendKeyEvent() still returns true even after unbindInput().", 4501 result.getReturnBooleanValue()); 4502 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4503 4504 // Make sure that the app does not receive the call (for a while). 4505 methodCallVerifier.expectNotCalled( 4506 "Once unbindInput() happened, IC#sendKeyEvent() fails fast.", 4507 EXPECTED_NOT_CALLED_TIMEOUT); 4508 }); 4509 } 4510 4511 /** 4512 * Test {@link InputConnection#sendKeyEvent(KeyEvent)} works as expected for 4513 * {@link android.accessibilityservice.InputMethod}. 4514 */ 4515 @Test 4516 public void testSendKeyEventForA11y() throws Exception { 4517 final KeyEvent expectedKeyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X); 4518 // Intentionally let the app return "false" to confirm that IME still receives "true". 4519 final boolean returnedResult = false; 4520 4521 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4522 4523 final class Wrapper extends InputConnectionWrapper { 4524 private Wrapper(InputConnection target) { 4525 super(target, false); 4526 } 4527 4528 @Override 4529 public boolean sendKeyEvent(KeyEvent event) { 4530 methodCallVerifier.onMethodCalled(args -> { 4531 args.putParcelable("event", event); 4532 }); 4533 return returnedResult; 4534 } 4535 } 4536 4537 testA11yInputConnection(Wrapper::new, (session, stream) -> { 4538 final var command = session.callSendKeyEvent(expectedKeyEvent); 4539 expectA11yImeCommand(stream, command, TIMEOUT); 4540 methodCallVerifier.expectCalledOnce(args -> { 4541 final KeyEvent actualKeyEvent = args.getParcelable("event"); 4542 assertNotNull(actualKeyEvent); 4543 assertEquals(expectedKeyEvent.getAction(), actualKeyEvent.getAction()); 4544 assertEquals(expectedKeyEvent.getKeyCode(), actualKeyEvent.getKeyCode()); 4545 }, TIMEOUT); 4546 }); 4547 } 4548 4549 /** 4550 * Test {@link InputConnection#clearMetaKeyStates(int)} works as expected. 4551 */ 4552 @Test 4553 public void testClearMetaKeyStates() throws Exception { 4554 final int expectedStates = KeyEvent.META_ALT_MASK; 4555 // Intentionally let the app return "false" to confirm that IME still receives "true". 4556 final boolean returnedResult = false; 4557 4558 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4559 4560 final class Wrapper extends InputConnectionWrapper { 4561 private Wrapper(InputConnection target) { 4562 super(target, false); 4563 } 4564 4565 @Override 4566 public boolean clearMetaKeyStates(int states) { 4567 methodCallVerifier.onMethodCalled(args -> { 4568 args.putInt("states", states); 4569 }); 4570 return returnedResult; 4571 } 4572 } 4573 4574 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4575 final ImeCommand command = session.callClearMetaKeyStates(expectedStates); 4576 assertTrue("clearMetaKeyStates() always returns true unless RemoteException is thrown", 4577 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4578 methodCallVerifier.expectCalledOnce(args -> { 4579 final int actualStates = args.getInt("states"); 4580 assertEquals(expectedStates, actualStates); 4581 }, TIMEOUT); 4582 }); 4583 } 4584 4585 /** 4586 * Test {@link InputConnection#clearMetaKeyStates(int)} fails fast once 4587 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4588 */ 4589 @Test 4590 public void testClearMetaKeyStatesAfterUnbindInput() throws Exception { 4591 final boolean returnedResult = true; 4592 4593 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4594 4595 final class Wrapper extends InputConnectionWrapper { 4596 private Wrapper(InputConnection target) { 4597 super(target, false); 4598 } 4599 4600 @Override 4601 public boolean clearMetaKeyStates(int states) { 4602 methodCallVerifier.onMethodCalled(args -> { 4603 args.putInt("states", states); 4604 }); 4605 return returnedResult; 4606 } 4607 } 4608 4609 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4610 // Memorize the current InputConnection. 4611 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4612 4613 // Let unbindInput happen. 4614 triggerUnbindInput(); 4615 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4616 4617 // Now this API call on the memorized IC should fail fast. 4618 final ImeCommand command = session.callClearMetaKeyStates(KeyEvent.META_ALT_MASK); 4619 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4620 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4621 assertTrue("Currently IC#clearMetaKeyStates() still returns true even after " 4622 + "unbindInput().", result.getReturnBooleanValue()); 4623 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4624 4625 // Make sure that the app does not receive the call (for a while). 4626 methodCallVerifier.expectNotCalled( 4627 "Once unbindInput() happened, IC#clearMetaKeyStates() fails fast.", 4628 EXPECTED_NOT_CALLED_TIMEOUT); 4629 }); 4630 } 4631 4632 /** 4633 * Test {@link InputConnection#clearMetaKeyStates(int)} works as expected for 4634 * {@link android.accessibilityservice.InputMethod}. 4635 */ 4636 @Test 4637 public void testClearMetaKeyStatesForA11y() throws Exception { 4638 final int expectedStates = KeyEvent.META_ALT_MASK; 4639 // Intentionally let the app return "false" to confirm that IME still receives "true". 4640 final boolean returnedResult = false; 4641 4642 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4643 4644 final class Wrapper extends InputConnectionWrapper { 4645 private Wrapper(InputConnection target) { 4646 super(target, false); 4647 } 4648 4649 @Override 4650 public boolean clearMetaKeyStates(int states) { 4651 methodCallVerifier.onMethodCalled(args -> { 4652 args.putInt("states", states); 4653 }); 4654 return returnedResult; 4655 } 4656 } 4657 4658 testA11yInputConnection(Wrapper::new, (session, stream) -> { 4659 final var command = session.callClearMetaKeyStates(expectedStates); 4660 expectA11yImeCommand(stream, command, TIMEOUT); 4661 methodCallVerifier.expectCalledOnce(args -> { 4662 final int actualStates = args.getInt("states"); 4663 assertEquals(expectedStates, actualStates); 4664 }, TIMEOUT); 4665 }); 4666 } 4667 4668 /** 4669 * Test {@link InputConnection#reportFullscreenMode(boolean)} is ignored as expected. 4670 */ 4671 @Test 4672 public void testReportFullscreenMode() throws Exception { 4673 // Intentionally let the app return "false" to confirm that IME still receives "true". 4674 final boolean returnedResult = false; 4675 4676 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4677 4678 final class Wrapper extends InputConnectionWrapper { 4679 private Wrapper(InputConnection target) { 4680 super(target, false); 4681 } 4682 4683 @Override 4684 public boolean reportFullscreenMode(boolean enabled) { 4685 methodCallVerifier.onMethodCalled(args -> { 4686 args.putBoolean("enabled", enabled); 4687 }); 4688 return returnedResult; 4689 } 4690 } 4691 4692 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4693 final ImeCommand command = session.callReportFullscreenMode(true); 4694 assertFalse("reportFullscreenMode() always returns false on API 26+", 4695 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4696 4697 // Make sure that the app does not receive the call (for a while). 4698 methodCallVerifier.expectNotCalled( 4699 "IC#reportFullscreenMode() must be ignored on API 26+", 4700 EXPECTED_NOT_CALLED_TIMEOUT); 4701 }); 4702 } 4703 4704 /** 4705 * Test {@link InputConnection#reportFullscreenMode(boolean)} is ignored as expected even after 4706 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4707 */ 4708 @Test 4709 public void testReportFullscreenModeAfterUnbindInput() throws Exception { 4710 final boolean returnedResult = true; 4711 4712 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4713 4714 final class Wrapper extends InputConnectionWrapper { 4715 private Wrapper(InputConnection target) { 4716 super(target, false); 4717 } 4718 4719 @Override 4720 public boolean reportFullscreenMode(boolean enabled) { 4721 methodCallVerifier.onMethodCalled(args -> { 4722 args.putBoolean("enabled", enabled); 4723 }); 4724 return returnedResult; 4725 } 4726 } 4727 4728 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4729 // Memorize the current InputConnection. 4730 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4731 4732 // Let unbindInput happen. 4733 triggerUnbindInput(); 4734 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4735 4736 // Now this API call on the memorized IC should fail fast. 4737 final ImeCommand command = session.callReportFullscreenMode(true); 4738 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4739 assertFalse("reportFullscreenMode() always returns false on API 26+", 4740 result.getReturnBooleanValue()); 4741 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4742 4743 // Make sure that the app does not receive the call (for a while). 4744 methodCallVerifier.expectNotCalled("IC#reportFullscreenMode() must be ignored on " 4745 + "API 26+ even after unbindInput().", EXPECTED_NOT_CALLED_TIMEOUT); 4746 }); 4747 } 4748 4749 /** 4750 * Test {@link InputConnection#performSpellCheck()} works as expected. 4751 */ 4752 @Test 4753 public void testPerformSpellCheck() throws Exception { 4754 // Intentionally let the app return "false" to confirm that IME still receives "true". 4755 final boolean returnedResult = false; 4756 4757 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4758 4759 final class Wrapper extends InputConnectionWrapper { 4760 private Wrapper(InputConnection target) { 4761 super(target, false); 4762 } 4763 4764 @Override 4765 public boolean performSpellCheck() { 4766 methodCallVerifier.onMethodCalled(args -> { }); 4767 return returnedResult; 4768 } 4769 } 4770 4771 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4772 final ImeCommand command = session.callPerformSpellCheck(); 4773 assertTrue("performSpellCheck() always returns true unless RemoteException is thrown", 4774 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4775 methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); 4776 }); 4777 } 4778 4779 /** 4780 * Test {@link InputConnection#performSpellCheck()} fails fast once 4781 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4782 */ 4783 @Test 4784 public void testPerformSpellCheckAfterUnbindInput() throws Exception { 4785 final boolean returnedResult = true; 4786 4787 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4788 4789 final class Wrapper extends InputConnectionWrapper { 4790 private Wrapper(InputConnection target) { 4791 super(target, false); 4792 } 4793 4794 @Override 4795 public boolean performSpellCheck() { 4796 methodCallVerifier.onMethodCalled(args -> { }); 4797 return returnedResult; 4798 } 4799 } 4800 4801 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4802 // Memorize the current InputConnection. 4803 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4804 4805 // Let unbindInput happen. 4806 triggerUnbindInput(); 4807 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4808 4809 // Now this API call on the memorized IC should fail fast. 4810 final ImeCommand command = session.callPerformSpellCheck(); 4811 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4812 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4813 assertTrue("Currently IC#performSpellCheck() still returns true even after " 4814 + "unbindInput().", result.getReturnBooleanValue()); 4815 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4816 4817 // Make sure that the app does not receive the call (for a while). 4818 methodCallVerifier.expectNotCalled( 4819 "Once unbindInput() happened, IC#performSpellCheck() fails fast.", 4820 EXPECTED_NOT_CALLED_TIMEOUT); 4821 }); 4822 } 4823 4824 /** 4825 * Verify that the default implementation of {@link InputConnection#performSpellCheck()} 4826 * returns {@code true} without any crash even when the target app does not override it. 4827 */ 4828 @Test 4829 public void testPerformSpellCheckDefaultMethod() throws Exception { 4830 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 4831 final ImeCommand command = session.callPerformSpellCheck(); 4832 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4833 assertTrue("IC#performSpellCheck() still returns true even when the target " 4834 + "application does not implement it.", result.getReturnBooleanValue()); 4835 }); 4836 } 4837 4838 /** 4839 * Test {@link InputConnection#performPrivateCommand(String, Bundle)} works as expected. 4840 */ 4841 @Test 4842 public void testPerformPrivateCommand() throws Exception { 4843 final String expectedAction = "myAction"; 4844 final Bundle expectedData = new Bundle(); 4845 final String expectedDataKey = "testKey"; 4846 final int expectedDataValue = 42; 4847 expectedData.putInt(expectedDataKey, expectedDataValue); 4848 // Intentionally let the app return "false" to confirm that IME still receives "true". 4849 final boolean returnedResult = false; 4850 4851 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4852 4853 final class Wrapper extends InputConnectionWrapper { 4854 private Wrapper(InputConnection target) { 4855 super(target, false); 4856 } 4857 4858 @Override 4859 public boolean performPrivateCommand(String action, Bundle data) { 4860 methodCallVerifier.onMethodCalled(args -> { 4861 args.putString("action", action); 4862 args.putBundle("data", data); 4863 }); 4864 return returnedResult; 4865 } 4866 } 4867 4868 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4869 final ImeCommand command = 4870 session.callPerformPrivateCommand(expectedAction, expectedData); 4871 assertTrue("performPrivateCommand() always returns true unless RemoteException is " 4872 + "thrown", expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 4873 methodCallVerifier.expectCalledOnce(args -> { 4874 final String actualAction = args.getString("action"); 4875 final Bundle actualData = args.getBundle("data"); 4876 assertEquals(expectedAction, actualAction); 4877 assertNotNull(actualData); 4878 assertEquals(expectedData.get(expectedDataKey), actualData.getInt(expectedDataKey)); 4879 }, TIMEOUT); 4880 }); 4881 } 4882 4883 /** 4884 * Test {@link InputConnection#performPrivateCommand(String, Bundle)} fails fast once 4885 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4886 */ 4887 @Test 4888 public void testPerformPrivateCommandAfterUnbindInput() throws Exception { 4889 final boolean returnedResult = true; 4890 4891 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4892 4893 final class Wrapper extends InputConnectionWrapper { 4894 private Wrapper(InputConnection target) { 4895 super(target, false); 4896 } 4897 4898 @Override 4899 public boolean performPrivateCommand(String action, Bundle data) { 4900 methodCallVerifier.onMethodCalled(args -> { 4901 args.putString("action", action); 4902 args.putBundle("data", data); 4903 }); 4904 return returnedResult; 4905 } 4906 } 4907 4908 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4909 // Memorize the current InputConnection. 4910 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4911 4912 // Let unbindInput happen. 4913 triggerUnbindInput(); 4914 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4915 4916 // Now this API call on the memorized IC should fail fast. 4917 final ImeCommand command = session.callPerformPrivateCommand("myAction", null); 4918 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 4919 // CAVEAT: this behavior is a bit questionable and may change in a future version. 4920 assertTrue("Currently IC#performPrivateCommand() still returns true even after " 4921 + "unbindInput().", result.getReturnBooleanValue()); 4922 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 4923 4924 // Make sure that the app does not receive the call (for a while). 4925 methodCallVerifier.expectNotCalled( 4926 "Once unbindInput() happened, IC#performPrivateCommand() fails fast.", 4927 EXPECTED_NOT_CALLED_TIMEOUT); 4928 }); 4929 } 4930 4931 /** 4932 * Test {@link InputConnection#getHandler()} is ignored as expected. 4933 */ 4934 @Test 4935 public void testGetHandler() throws Exception { 4936 final Handler returnedResult = null; 4937 4938 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4939 4940 final class Wrapper extends InputConnectionWrapper { 4941 private Wrapper(InputConnection target) { 4942 super(target, false); 4943 } 4944 4945 @Override 4946 public Handler getHandler() { 4947 methodCallVerifier.onMethodCalled(args -> { }); 4948 return returnedResult; 4949 } 4950 } 4951 4952 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4953 // The system internally calls "getHandler". So reset the verifier before our calling 4954 // "callGetHandler". 4955 methodCallVerifier.reset(); 4956 final ImeCommand command = session.callGetHandler(); 4957 assertTrue("getHandler() always returns null", 4958 expectCommand(stream, command, TIMEOUT).isNullReturnValue()); 4959 4960 // Make sure that the app does not receive the call (for a while). 4961 methodCallVerifier.expectNotCalled("IC#getHandler() must be ignored.", 4962 EXPECTED_NOT_CALLED_TIMEOUT); 4963 }); 4964 } 4965 4966 /** 4967 * Test {@link InputConnection#getHandler()} is ignored as expected even after 4968 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 4969 */ 4970 @Test 4971 public void testGetHandlerAfterUnbindInput() throws Exception { 4972 final Handler returnedResult = null; 4973 4974 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 4975 4976 final class Wrapper extends InputConnectionWrapper { 4977 private Wrapper(InputConnection target) { 4978 super(target, false); 4979 } 4980 4981 @Override 4982 public Handler getHandler() { 4983 methodCallVerifier.onMethodCalled(args -> { }); 4984 return returnedResult; 4985 } 4986 } 4987 4988 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 4989 // Memorize the current InputConnection. 4990 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 4991 4992 // Let unbindInput happen. 4993 triggerUnbindInput(); 4994 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 4995 4996 // The system internally calls "getHandler". So reset the verifier before our calling 4997 // "callGetHandler". 4998 methodCallVerifier.reset(); 4999 // Now this API call on the memorized IC should fail fast. 5000 final ImeCommand command = session.callGetHandler(); 5001 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 5002 assertTrue("getHandler() always returns null", result.isNullReturnValue()); 5003 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 5004 5005 // Make sure that the app does not receive the call (for a while). 5006 methodCallVerifier.expectNotCalled( 5007 "IC#getHandler() must be ignored even after unbindInput().", 5008 EXPECTED_NOT_CALLED_TIMEOUT); 5009 }); 5010 } 5011 5012 /** 5013 * Verify that applications that do not implement {@link InputConnection#getHandler()} will not 5014 * crash. This can happen if the app was built before {@link android.os.Build.VERSION_CODES#N}. 5015 */ 5016 @Test 5017 public void testGetHandlerWithMethodMissing() throws Exception { 5018 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 5019 final ImeCommand command = session.callGetHandler(); 5020 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 5021 assertTrue("IC#getHandler() still returns null even when the target app does not" 5022 + " implement it.", result.isNullReturnValue()); 5023 }); 5024 } 5025 5026 /** 5027 * Test {@link InputConnection#closeConnection()} is ignored as expected. 5028 */ 5029 @Test 5030 public void testCloseConnection() throws Exception { 5031 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 5032 5033 final class Wrapper extends InputConnectionWrapper { 5034 private Wrapper(InputConnection target) { 5035 super(target, false); 5036 } 5037 5038 @Override 5039 public void closeConnection() { 5040 methodCallVerifier.onMethodCalled(args -> { }); 5041 } 5042 } 5043 5044 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 5045 final ImeCommand command = session.callCloseConnection(); 5046 expectCommand(stream, command, TIMEOUT); 5047 5048 // Make sure that the app does not receive the call (for a while). 5049 methodCallVerifier.expectNotCalled("IC#getHandler() must be ignored.", 5050 EXPECTED_NOT_CALLED_TIMEOUT); 5051 }); 5052 } 5053 5054 /** 5055 * Test {@link InputConnection#closeConnection()} is ignored as expected even after 5056 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 5057 */ 5058 @Test 5059 public void testCloseConnectionAfterUnbindInput() throws Exception { 5060 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 5061 final CountDownLatch latch = new CountDownLatch(1); 5062 5063 final class Wrapper extends InputConnectionWrapper { 5064 private Wrapper(InputConnection target) { 5065 super(target, false); 5066 } 5067 5068 @Override 5069 public void closeConnection() { 5070 methodCallVerifier.onMethodCalled(args -> { }); 5071 latch.countDown(); 5072 } 5073 } 5074 5075 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 5076 // Memorize the current InputConnection. 5077 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 5078 5079 // Let unbindInput happen. 5080 triggerUnbindInput(); 5081 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 5082 5083 // The system internally calls "closeConnection". So wait for it to happen then reset 5084 // the verifier before our calling "closeConnection". 5085 assertTrue("closeConnection() must be called by the system.", 5086 latch.await(TIMEOUT, TimeUnit.MILLISECONDS)); 5087 methodCallVerifier.reset(); 5088 5089 // Now this API call on the memorized IC should fail fast. 5090 final ImeCommand command = session.callCloseConnection(); 5091 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 5092 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 5093 5094 // Make sure that the app does not receive the call (for a while). 5095 methodCallVerifier.expectNotCalled( 5096 "IC#closeConnection() must be ignored even after unbindInput().", 5097 EXPECTED_NOT_CALLED_TIMEOUT); 5098 }); 5099 } 5100 5101 /** 5102 * Verify that applications that do not implement {@link InputConnection#closeConnection()} 5103 * will not crash. This can happen if the app was built before 5104 * {@link android.os.Build.VERSION_CODES#N}. 5105 */ 5106 @Test 5107 public void testCloseConnectionWithMethodMissing() throws Exception { 5108 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 5109 final ImeCommand command = session.callCloseConnection(); 5110 expectCommand(stream, command, TIMEOUT); 5111 }); 5112 } 5113 5114 /** 5115 * Test {@link InputConnection#setImeConsumesInput(boolean)} works as expected. 5116 */ 5117 @Test 5118 public void testSetImeConsumesInput() throws Exception { 5119 final boolean expectedImeConsumesInput = true; 5120 // Intentionally let the app return "false" to confirm that IME still receives "true". 5121 final boolean returnedResult = false; 5122 5123 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 5124 5125 final class Wrapper extends InputConnectionWrapper { 5126 private Wrapper(InputConnection target) { 5127 super(target, false); 5128 } 5129 5130 @Override 5131 public boolean setImeConsumesInput(boolean imeConsumesInput) { 5132 methodCallVerifier.onMethodCalled(args -> { 5133 args.putBoolean("imeConsumesInput", imeConsumesInput); 5134 }); 5135 return returnedResult; 5136 } 5137 } 5138 5139 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 5140 final ImeCommand command = session.callSetImeConsumesInput(expectedImeConsumesInput); 5141 assertTrue("setImeConsumesInput() always returns true unless RemoteException is thrown", 5142 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 5143 methodCallVerifier.expectCalledOnce(args -> { 5144 final boolean actualImeConsumesInput = args.getBoolean("imeConsumesInput"); 5145 assertEquals(expectedImeConsumesInput, actualImeConsumesInput); 5146 }, TIMEOUT); 5147 }); 5148 } 5149 5150 /** 5151 * Test {@link InputConnection#setImeConsumesInput(boolean)} fails fast once 5152 * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 5153 */ 5154 @Test 5155 public void testSetImeConsumesInputAfterUnbindInput() throws Exception { 5156 final boolean returnedResult = true; 5157 5158 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 5159 5160 final class Wrapper extends InputConnectionWrapper { 5161 private Wrapper(InputConnection target) { 5162 super(target, false); 5163 } 5164 5165 @Override 5166 public boolean setImeConsumesInput(boolean imeConsumesInput) { 5167 methodCallVerifier.onMethodCalled(args -> { 5168 args.putBoolean("imeConsumesInput", imeConsumesInput); 5169 }); 5170 return returnedResult; 5171 } 5172 } 5173 5174 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 5175 // Memorize the current InputConnection. 5176 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 5177 5178 // Let unbindInput happen. 5179 triggerUnbindInput(); 5180 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 5181 5182 // Now this API call on the memorized IC should fail fast. 5183 final ImeCommand command = session.callSetImeConsumesInput(true); 5184 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 5185 // CAVEAT: this behavior is a bit questionable and may change in a future version. 5186 assertTrue("Currently IC#setImeConsumesInput() still returns true even after " 5187 + "unbindInput().", result.getReturnBooleanValue()); 5188 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 5189 5190 // Make sure that the app does not receive the call (for a while). 5191 methodCallVerifier.expectNotCalled( 5192 "Once unbindInput() happened, IC#setImeConsumesInput() fails fast.", 5193 EXPECTED_NOT_CALLED_TIMEOUT); 5194 }); 5195 } 5196 5197 /** 5198 * Verify that the default implementation of 5199 * {@link InputConnection#setImeConsumesInput(boolean)} returns {@code true} without any crash 5200 * even when the target app does not override it. 5201 */ 5202 @Test 5203 public void testSetImeConsumesInputDefaultMethod() throws Exception { 5204 testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { 5205 final ImeCommand command = session.callSetImeConsumesInput(true); 5206 final ImeEvent result = expectCommand(stream, command, TIMEOUT); 5207 assertTrue("IC#setImeConsumesInput() still returns true even when the target " 5208 + "application does not implement it.", result.getReturnBooleanValue()); 5209 }); 5210 } 5211 5212 /** 5213 * Test {@link InputConnection#takeSnapshot()} is ignored as expected. 5214 */ 5215 @Test 5216 public void testTakeSnapshot() throws Exception { 5217 final TextSnapshot returnedTextSnapshot = new TextSnapshot( 5218 new SurroundingText("test", 4, 4, 0), -1, -1, 0); 5219 final class Wrapper extends InputConnectionWrapper { 5220 private Wrapper(InputConnection target) { 5221 super(target, false); 5222 } 5223 5224 @Override 5225 public TextSnapshot takeSnapshot() { 5226 return returnedTextSnapshot; 5227 } 5228 } 5229 5230 testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { 5231 final ImeCommand command = session.callTakeSnapshot(); 5232 assertTrue("takeSnapshot() always returns null", 5233 expectCommand(stream, command, TIMEOUT).isNullReturnValue()); 5234 }); 5235 } 5236 5237 /** 5238 * Test {@link InputConnection#replaceText(int, int, CharSequence, int, TextAttribute)} works as 5239 * expected. 5240 */ 5241 @Test 5242 public void testReplaceText() throws Exception { 5243 final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); 5244 final int expectedStart = 0; 5245 final int expectedEnd = 5; 5246 final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); 5247 final int expectedNewCursorPosition = 123; 5248 final ArrayList<String> expectedSuggestions = new ArrayList<>(); 5249 expectedSuggestions.add("test"); 5250 final TextAttribute expectedTextAttribute = 5251 new TextAttribute.Builder() 5252 .setTextConversionSuggestions(expectedSuggestions) 5253 .build(); 5254 // Intentionally let the app return "false" to confirm that IME still receives "true". 5255 final boolean returnedResult = false; 5256 5257 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 5258 5259 final class Wrapper extends InputConnectionWrapper { 5260 private Wrapper(InputConnection target) { 5261 super(target, false); 5262 } 5263 5264 @Override 5265 public boolean replaceText( 5266 int start, 5267 int end, 5268 CharSequence text, 5269 int newCursorPosition, 5270 TextAttribute textAttribute) { 5271 methodCallVerifier.onMethodCalled( 5272 args -> { 5273 args.putInt("start", start); 5274 args.putInt("end", end); 5275 args.putCharSequence("text", text); 5276 args.putInt("newCursorPosition", newCursorPosition); 5277 args.putParcelable("textAttribute", textAttribute); 5278 }); 5279 5280 return returnedResult; 5281 } 5282 } 5283 5284 testInputConnection( 5285 Wrapper::new, 5286 (MockImeSession session, ImeEventStream stream) -> { 5287 final ImeCommand command = 5288 session.callReplaceText( 5289 expectedStart, 5290 expectedEnd, 5291 expectedText, 5292 expectedNewCursorPosition, 5293 expectedTextAttribute); 5294 assertTrue( 5295 "replaceText() always returns true unless RemoteException is thrown", 5296 expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); 5297 methodCallVerifier.expectCalledOnce( 5298 args -> { 5299 assertEquals(expectedStart, args.getInt("start")); 5300 assertEquals(expectedEnd, args.getInt("end")); 5301 assertEqualsForTestCharSequence( 5302 expectedText, args.getCharSequence("text")); 5303 assertEquals( 5304 expectedNewCursorPosition, 5305 args.getInt("newCursorPosition")); 5306 final TextAttribute textAttribute = 5307 args.getParcelable("textAttribute"); 5308 assertThat(textAttribute).isNotNull(); 5309 assertThat(textAttribute.getTextConversionSuggestions()) 5310 .containsExactlyElementsIn(expectedSuggestions); 5311 }, 5312 TIMEOUT); 5313 }); 5314 } 5315 5316 /** 5317 * Test {@link InputConnection#replaceText(int, int, CharSequence, int, TextAttribute)} fails 5318 * fast once {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. 5319 */ 5320 @Test 5321 public void testReplaceTextAfterUnbindInput() throws Exception { 5322 final boolean returnedResult = true; 5323 5324 final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); 5325 5326 final class Wrapper extends InputConnectionWrapper { 5327 private Wrapper(InputConnection target) { 5328 super(target, false); 5329 } 5330 5331 @Override 5332 public boolean replaceText( 5333 int start, 5334 int end, 5335 CharSequence text, 5336 int newCursorPosition, 5337 TextAttribute textAttribute) { 5338 methodCallVerifier.onMethodCalled( 5339 args -> { 5340 args.putInt("start", start); 5341 args.putInt("end", end); 5342 args.putCharSequence("text", text); 5343 args.putInt("newCursorPosition", newCursorPosition); 5344 args.putParcelable("textAttribute", textAttribute); 5345 }); 5346 5347 return returnedResult; 5348 } 5349 } 5350 5351 testInputConnection( 5352 Wrapper::new, 5353 (MockImeSession session, ImeEventStream stream) -> { 5354 // Memorize the current InputConnection. 5355 expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); 5356 5357 // Let unbindInput happen. 5358 triggerUnbindInput(); 5359 expectEvent(stream, eventMatcher("unbindInput"), TIMEOUT); 5360 5361 // Now IC#getTextAfterCursor() for the memorized IC should fail fast. 5362 final ImeEvent result = 5363 expectCommand( 5364 stream, 5365 session.callReplaceText(0, 5, "text", 1, null), 5366 TIMEOUT); 5367 // CAVEAT: this behavior is a bit questionable and may change in a future 5368 // version. 5369 assertTrue( 5370 "Currently IC#replaceText() still returns true even after" 5371 + " unbindInput().", 5372 result.getReturnBooleanValue()); 5373 expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); 5374 5375 // Make sure that the app does not receive the call (for a while). 5376 methodCallVerifier.expectNotCalled( 5377 "Once unbindInput() happened, IC#replaceText() fails fast.", 5378 EXPECTED_NOT_CALLED_TIMEOUT); 5379 }); 5380 } 5381 } 5382