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