Test IMM#invalidateInput()

This CL adds test cases for IMM#invalidateInput() [1].

 [1]: I3161755779080f98bcef0e47dd0c5247d8a3a256

Bug: 203086369
Test: atest -c CtsInputMethodTestCases:InputMethodStartInputLifecycleTest
Change-Id: If4ec69f7688e3fdd3f5427f8543a4b2bc8d649b4
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java
index 4eb6573..6df2a54 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputMethodStartInputLifecycleTest.java
@@ -27,6 +27,8 @@
 import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
 import static com.android.cts.mockime.ImeEventStreamTestUtils.notExpectEvent;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
@@ -34,14 +36,23 @@
 import android.app.Instrumentation;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.graphics.Color;
 import android.inputmethodservice.InputMethodService;
 import android.os.IBinder;
 import android.os.Process;
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Selection;
 import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.TextSnapshot;
 import android.view.inputmethod.cts.util.DisableScreenDozeRule;
 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
 import android.view.inputmethod.cts.util.RequireImeCompatFlagRule;
@@ -52,6 +63,7 @@
 import android.widget.EditText;
 import android.widget.LinearLayout;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -84,6 +96,14 @@
             FINISH_INPUT_NO_FALLBACK_CONNECTION, true);
 
     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+    private static final long NOT_EXPECT_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
+
+    private static final String TEST_MARKER_PREFIX =
+            "android.view.inputmethod.cts.FocusHandlingTest";
+
+    private static String getTestMarker() {
+        return TEST_MARKER_PREFIX + "/"  + SystemClock.elapsedRealtimeNanos();
+    }
 
     @AppModeFull(reason = "KeyguardManager is not accessible from instant apps")
     @Test
@@ -99,8 +119,7 @@
                 context, instrumentation.getUiAutomation(), new ImeSettings.Builder())) {
             final ImeEventStream stream = imeSession.openEventStream();
 
-            final String marker = InputMethodManagerTest.class.getName() + "/"
-                    + SystemClock.elapsedRealtimeNanos();
+            final String marker = getTestMarker();
             final AtomicInteger screenStateCallbackRef = new AtomicInteger(-1);
             TestActivity.startSync(activity -> {
                 final LinearLayout layout = new LinearLayout(activity);
@@ -187,8 +206,7 @@
                 new ImeSettings.Builder())) {
             final ImeEventStream stream = imeSession.openEventStream();
 
-            final String marker = InputMethodStartInputLifecycleTest.class.getName() + "/"
-                    + SystemClock.elapsedRealtimeNanos();
+            final String marker = getTestMarker();
             final EditText editText = launchTestActivity(marker);
             TestUtils.runOnMainSync(() -> editText.requestFocus());
 
@@ -243,6 +261,184 @@
         return editTextRef.get();
     }
 
+    /**
+     * A mostly-minimum implementation of {@link View} that can be used to test custom
+     * implementations of {@link View#onCreateInputConnection(EditorInfo)}.
+     */
+    static class TestEditor extends View {
+        TestEditor(@NonNull Context context) {
+            super(context);
+            setBackgroundColor(Color.YELLOW);
+            setFocusableInTouchMode(true);
+            setFocusable(true);
+            setLayoutParams(new ViewGroup.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT, 10 /* height */));
+        }
+    }
+
+    /**
+     * {@link InputMethodManager#invalidateInput(View)} is a lightweight version of
+     * {@link InputMethodManager#restartInput(View)} that reuses existing {@link InputConnection}
+     * by using {@link InputConnection#takeSnapshot()}.
+     */
+    @Test
+    public void testInterruptInputWithTakeSnapshotSupport() throws Exception {
+        testInterruptInputMain(true /* supportTakeSnapshot */);
+    }
+
+    /**
+     * {@link InputMethodManager#invalidateInput(View)} falls back into
+     * {@link InputMethodManager#restartInput(View)} when {@link InputConnection#takeSnapshot()}
+     * returns {@code null}.
+     */
+    @Test
+    public void testInterruptInputWithoutTakeSnapshotSupport() throws Exception {
+        testInterruptInputMain(false /* supportTakeSnapshot */);
+    }
+
+    private void testInterruptInputMain(boolean supportTakeSnapshot) throws Exception {
+        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        try (MockImeSession imeSession = MockImeSession.create(
+                instrumentation.getContext(),
+                instrumentation.getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            final String marker = getTestMarker();
+            final int initialSelStart = 3;
+            final int initialSelEnd = 7;
+            final int initialCapsMode = TextUtils.CAP_MODE_SENTENCES;
+
+            final AtomicInteger onCreateConnectionCount = new AtomicInteger(0);
+            class MyTestEditor extends TestEditor {
+                final Editable mEditable;
+
+                MyTestEditor(Context context, @NonNull Editable editable) {
+                    super(context);
+                    mEditable = editable;
+                }
+
+                @Override
+                public boolean onCheckIsTextEditor() {
+                    return true;
+                }
+
+                @Override
+                public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+                    onCreateConnectionCount.incrementAndGet();
+                    outAttrs.inputType =
+                            InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+                    outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
+                    outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
+                    outAttrs.initialCapsMode = initialCapsMode;
+                    outAttrs.privateImeOptions = marker;
+                    outAttrs.setInitialSurroundingText(mEditable);
+                    return new BaseInputConnection(this, true) {
+                        @Override
+                        public Editable getEditable() {
+                            return mEditable;
+                        }
+
+                        @Override
+                        public TextSnapshot takeSnapshot() {
+                            return supportTakeSnapshot ? super.takeSnapshot() : null;
+                        }
+                    };
+                }
+            }
+
+            final AtomicReference<MyTestEditor> myEditorRef = new AtomicReference<>();
+            TestActivity.startSync(activity -> {
+                final LinearLayout layout = new LinearLayout(activity);
+                layout.setOrientation(LinearLayout.VERTICAL);
+
+                final Editable editable =
+                        Editable.Factory.getInstance().newEditable("0123456789");
+                Selection.setSelection(editable, initialSelStart, initialSelEnd);
+
+                final MyTestEditor editor = new MyTestEditor(activity, editable);
+                editor.requestFocus();
+                myEditorRef.set(editor);
+
+                layout.addView(editor);
+                return layout;
+            });
+            final MyTestEditor myEditor = myEditorRef.get();
+
+            // Wait until the MockIme gets bound to the TestActivity.
+            expectBindInput(stream, Process.myPid(), TIMEOUT);
+
+            {
+                final ImeEvent startInputEvent =
+                        expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+                final EditorInfo editorInfo =
+                        startInputEvent.getArguments().getParcelable("editorInfo");
+                assertThat(editorInfo).isNotNull();
+                assertThat(editorInfo.initialSelStart).isEqualTo(initialSelStart);
+                assertThat(editorInfo.initialSelEnd).isEqualTo(initialSelEnd);
+                assertThat(editorInfo.getInitialSelectedText(0).toString()).isEqualTo("3456");
+            }
+
+            stream.skipAll();
+            final ImeEventStream forkedStream = stream.copy();
+
+            final int prevOnCreateInputConnectionCount = onCreateConnectionCount.get();
+
+            final int newSelStart = 1;
+            final int newSelEnd = 3;
+            TestUtils.runOnMainSync(() -> {
+                Selection.setSelection(myEditor.mEditable, newSelStart, newSelEnd);
+                final InputMethodManager imm = myEditor.getContext().getSystemService(
+                        InputMethodManager.class);
+                imm.invalidateInput(myEditor);
+            });
+
+            // Verify that InputMethodService#onStartInput() is triggered as if IMM#restartInput()
+            // was called.
+            {
+                final ImeEvent startInputEvent =
+                        expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+                final boolean restarting = startInputEvent.getArguments().getBoolean("restarting");
+                assertThat(restarting).isTrue();
+                final EditorInfo editorInfo =
+                        startInputEvent.getArguments().getParcelable("editorInfo");
+                assertThat(editorInfo).isNotNull();
+                assertThat(editorInfo.initialSelStart).isEqualTo(newSelStart);
+                assertThat(editorInfo.initialSelEnd).isEqualTo(newSelEnd);
+                assertThat(editorInfo.getInitialSelectedText(0).toString()).isEqualTo("12");
+            }
+
+            if (supportTakeSnapshot) {
+                // Make sure that InputMethodManager#interruptInput() does not trigger
+                // View#onCreateInputConnection() if InputConnection#takeSnapshot() is supported.
+                assertThat(onCreateConnectionCount.get()).isEqualTo(
+                        prevOnCreateInputConnectionCount);
+            } else {
+                // Make sure that InputMethodManager#interruptInput() does trigger
+                // View#onCreateInputConnection() if InputConnection#takeSnapshot() is not
+                // supported.
+                assertThat(onCreateConnectionCount.get()).isGreaterThan(
+                        prevOnCreateInputConnectionCount);
+            }
+
+            // For historical reasons, InputMethodService#onFinishInput() will not be triggered when
+            // restarting an input connection.
+            assertThat(forkedStream.findFirst(onFinishInputMatcher()).isPresent()).isFalse();
+
+            // Make sure that InputMethodManager#updateSelection() will be ignored when there is
+            // no change from the last call of InputMethodManager#interruptInput().
+            TestUtils.runOnMainSync(() -> {
+                Selection.setSelection(myEditor.mEditable, newSelStart, newSelEnd);
+                final InputMethodManager imm = myEditor.getContext().getSystemService(
+                        InputMethodManager.class);
+                imm.updateSelection(myEditor, newSelStart, newSelEnd, -1, -1);
+            });
+
+            notExpectEvent(stream, event -> "onUpdateSelection".equals(event.getEventName()),
+                    NOT_EXPECT_TIMEOUT);
+        }
+    }
+
     private static Predicate<ImeEvent> onFinishInputMatcher() {
         return event -> TextUtils.equals("onFinishInput", event.getEventName());
     }