IMF: Add requestTextBoundsInfo CTS

Bug: 265457058
Test: atest InputConnectionEndToEndTest
Change-Id: Iedbbb4bf70f304e104b1a60bbfb07cb98bb3744c
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
index 91b227a..0c75cd0 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
@@ -36,6 +36,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
+import android.graphics.RectF;
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
 import android.os.Bundle;
@@ -86,6 +87,7 @@
 import android.view.inputmethod.SelectGesture;
 import android.view.inputmethod.SelectRangeGesture;
 import android.view.inputmethod.TextAttribute;
+import android.view.inputmethod.TextBoundsInfoResult;
 import android.widget.FrameLayout;
 import android.widget.HorizontalScrollView;
 import android.widget.ImageView;
@@ -462,6 +464,15 @@
                                 .performHandwritingGesture(gesture, Runnable::run, consumer);
                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
                     }
+                    case "requestTextBoundsInfo": {
+                        var rectF = command.getExtras().getParcelable("rectF", RectF.class);
+                        Consumer<TextBoundsInfoResult> consumer = value ->
+                                getTracer().onRequestTextBoundsInfoResult(
+                                        value, command.getId());
+                        getMemorizedOrCurrentInputConnection().requestTextBoundsInfo(
+                                rectF, mMainHandler::post, consumer);
+                        return ImeEvent.RETURN_VALUE_UNAVAILABLE;
+                    }
                     case "requestCursorUpdates": {
                         final int cursorUpdateMode = command.getExtras().getInt("cursorUpdateMode");
                         final int cursorUpdateFilter =
@@ -1715,6 +1726,14 @@
             recordEventInternal("onPerformHandwritingGestureResult", runnable, arguments);
         }
 
+        public void onRequestTextBoundsInfoResult(TextBoundsInfoResult result, long requestId) {
+            final Bundle arguments = new Bundle();
+            arguments.putInt("resultCode", result.getResultCode());
+            arguments.putParcelable("boundsInfo", result.getTextBoundsInfo());
+            arguments.putLong("requestId", requestId);
+            recordEventInternal("onRequestTextBoundsInfoResult", () -> {}, arguments);
+        }
+
         void getWindowLayoutInfo(@NonNull WindowLayoutInfo windowLayoutInfo,
                 @NonNull Runnable runnable) {
             final Bundle arguments = new Bundle();
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
index 762e813..8031dc2 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
@@ -30,6 +30,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.graphics.RectF;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -1257,9 +1258,8 @@
      * {@link InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}
      * with the given parameters.
      *
-     * <p>Use {@link ImeEvent#getReturnIntegerValue()} for {@link ImeEvent} returned from
-     * {@link ImeEventStreamTestUtils#expectCommand(ImeEventStream, ImeCommand, long)} to see the
-     * value returned from the API.</p>
+     * <p>The result callback will be recorded as an {@code onPerformHandwritingGestureResult}
+     * event.
      *
      * <p>This can be affected by {@link #memorizeCurrentInputConnection()}.</p>
      *
@@ -1277,6 +1277,25 @@
     }
 
     /**
+     * Lets {@link MockIme} to call {@link InputConnection#requestTextBoundsInfo}.
+     *
+     * <p>The result callback will be recorded as an {@code onRequestTextBoundsInfoResult} event.
+     *
+     * <p>This can be affected by {@link #memorizeCurrentInputConnection()}.</p>
+     *
+     * @param gesture {@link SelectGesture} or {@link InsertGesture} or {@link DeleteGesture}.
+     * @return {@link ImeCommand} object that can be passed to
+     *         {@link ImeEventStreamTestUtils#expectCommand(ImeEventStream, ImeCommand, long)} to
+     *         wait until this event is handled by {@link MockIme}.
+     */
+    @NonNull
+    public ImeCommand callRequestTextBoundsInfo(RectF rectF) {
+        final Bundle params = new Bundle();
+        params.putParcelable("rectF", rectF);
+        return callCommandInternal("requestTextBoundsInfo", params);
+    }
+
+    /**
      * Lets {@link MockIme} to call
      * {@link InputConnection#previewHandwritingGesture(PreviewableHandwritingGesture,
      *  CancellationSignal)} with the given parameters.
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionEndToEndTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionEndToEndTest.java
index 39c506a..8a88c2c 100644
--- a/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionEndToEndTest.java
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/InputConnectionEndToEndTest.java
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -79,6 +80,8 @@
 import android.view.inputmethod.SelectRangeGesture;
 import android.view.inputmethod.SurroundingText;
 import android.view.inputmethod.TextAttribute;
+import android.view.inputmethod.TextBoundsInfo;
+import android.view.inputmethod.TextBoundsInfoResult;
 import android.view.inputmethod.TextSnapshot;
 import android.view.inputmethod.cts.util.EndToEndImeTestBase;
 import android.view.inputmethod.cts.util.MockTestActivityUtil;
@@ -106,7 +109,9 @@
 
 import com.google.common.truth.Correspondence;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ErrorCollector;
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
@@ -146,6 +151,9 @@
         return TEST_MARKER_PREFIX + "/"  + SystemClock.elapsedRealtimeNanos();
     }
 
+    @Rule
+    public final ErrorCollector mErrorCollector = new ErrorCollector();
+
     /**
      * A utility method to verify a method is called within a certain timeout period then block
      * it by {@link BlockingMethodVerifier#close()} is called.
@@ -1118,6 +1126,71 @@
         });
     }
 
+    @Test
+    @ApiTest(apis = {"android.view.inputmethod.InputConnection#requestTextBoundsInfo"})
+    public void testRequestTextBoundsInfo() throws Exception {
+        final var methodCallVerifier = new MethodCallVerifier();
+        final var tbiResult = new TextBoundsInfoResult(TextBoundsInfoResult.CODE_FAILED, null);
+
+        final class Wrapper extends InputConnectionWrapper {
+            private Wrapper(InputConnection target) {
+                super(target, false);
+            }
+
+            @Override
+            public void requestTextBoundsInfo(RectF rectF, Executor executor,
+                    Consumer<TextBoundsInfoResult> consumer) {
+                mErrorCollector.checkSucceeds(() -> {
+                    methodCallVerifier.onMethodCalled(args -> {
+                        args.putParcelable("rectF", rectF);
+                    });
+
+                    var called = new boolean[1];
+                    executor.execute(() -> {
+                        called[0] = true;
+                        consumer.accept(tbiResult);
+                    });
+                    assertTrue("editor-side executor must be Runnable::run", called[0]);
+                    return null;
+                });
+            }
+        }
+
+        testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> {
+            final RectF rectF = new RectF(1f, 2f, 3f, 4f);
+            final ImeCommand command = session.callRequestTextBoundsInfo(rectF);
+            methodCallVerifier.expectCalledOnce(args -> {
+                assertEquals(rectF, args.getParcelable("rectF", RectF.class));
+            }, TIMEOUT);
+            expectCommand(stream, command, TIMEOUT);
+            var event = expectEvent(stream, onRequestTextBoundsInfoResultMatcher(command.getId()),
+                    TIMEOUT);
+            var actualResultCode = event.getArguments().getInt("resultCode");
+            var actualBoundsInfo = event.getArguments().getParcelable("boundsInfo",
+                    TextBoundsInfo.class);
+
+            assertEquals(TextBoundsInfoResult.CODE_FAILED, actualResultCode);
+            assertNull(actualBoundsInfo);
+        });
+    }
+
+    @Test
+    public void testRequestTextBoundsInfo_unimplemented() throws Exception {
+        testMinimallyImplementedInputConnection((session, stream) -> {
+            final RectF rectF = new RectF(1f, 2f, 3f, 4f);
+            final ImeCommand command = session.callRequestTextBoundsInfo(rectF);
+            expectCommand(stream, command, TIMEOUT);
+            var event = expectEvent(stream, onRequestTextBoundsInfoResultMatcher(command.getId()),
+                    TIMEOUT);
+            var actualResultCode = event.getArguments().getInt("resultCode");
+            var actualBoundsInfo = event.getArguments().getParcelable("boundsInfo",
+                    TextBoundsInfo.class);
+
+            assertEquals(TextBoundsInfoResult.CODE_UNSUPPORTED, actualResultCode);
+            assertNull(actualBoundsInfo);
+        });
+    }
+
     /**
      * Test {@link InputConnection#getSurroundingText(int, int, int)} works as expected.
      */
@@ -1796,6 +1869,16 @@
         };
     }
 
+    private static Predicate<ImeEvent> onRequestTextBoundsInfoResultMatcher(
+            long requestId) {
+        return withDescription("onRequestTextBoundsInfoResult(" + requestId + ")", event -> {
+            if (!TextUtils.equals("onRequestTextBoundsInfoResult", event.getEventName())) {
+                return false;
+            }
+            return event.getArguments().getLong("requestId") == requestId;
+        });
+    }
+
     /**
      * Test
      * {@link InputConnection#previewHandwritingGesture(HandwritingGesture, CancellationSignal)}