| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view.inputmethod.cts; |
| |
| import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE; |
| |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.expectBindInput; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand; |
| import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import android.content.ClipDescription; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Process; |
| import android.os.SystemClock; |
| import android.text.Annotation; |
| import android.text.SpannableStringBuilder; |
| import android.text.Spanned; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| import android.view.KeyEvent; |
| import android.view.inputmethod.CompletionInfo; |
| import android.view.inputmethod.CorrectionInfo; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.ExtractedText; |
| import android.view.inputmethod.ExtractedTextRequest; |
| import android.view.inputmethod.InputConnection; |
| import android.view.inputmethod.InputConnectionWrapper; |
| import android.view.inputmethod.InputContentInfo; |
| import android.view.inputmethod.SurroundingText; |
| import android.view.inputmethod.TextAttribute; |
| import android.view.inputmethod.TextSnapshot; |
| import android.view.inputmethod.cts.util.EndToEndImeTestBase; |
| import android.view.inputmethod.cts.util.MockTestActivityUtil; |
| import android.view.inputmethod.cts.util.TestActivity; |
| import android.widget.EditText; |
| import android.widget.LinearLayout; |
| |
| import androidx.annotation.AnyThread; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.test.filters.LargeTest; |
| import androidx.test.platform.app.InstrumentationRegistry; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.cts.inputmethod.LegacyImeClientTestUtils; |
| import com.android.cts.mockime.ImeCommand; |
| import com.android.cts.mockime.ImeEvent; |
| import com.android.cts.mockime.ImeEventStream; |
| import com.android.cts.mockime.ImeSettings; |
| import com.android.cts.mockime.MockImeSession; |
| |
| import com.google.common.truth.Correspondence; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| |
| /** |
| * Provides basic tests for APIs defined in {@link InputConnection}. |
| * |
| * <p>TODO(b/193535269): Clean up boilerplate code around mocking InputConnection.</p> |
| */ |
| @LargeTest |
| @RunWith(AndroidJUnit4.class) |
| public class InputConnectionEndToEndTest extends EndToEndImeTestBase { |
| private static final long TIME_SLICE = TimeUnit.MILLISECONDS.toMillis(125); |
| private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5); |
| private static final long EXPECTED_NOT_CALLED_TIMEOUT = TimeUnit.SECONDS.toMillis(1); |
| private static final long LONG_TIMEOUT = TimeUnit.SECONDS.toMillis(30); |
| private static final long IMMEDIATE_TIMEOUT_NANO = TimeUnit.MILLISECONDS.toNanos(200); |
| |
| private static final String TEST_MARKER_PREFIX = |
| "android.view.inputmethod.cts.InputConnectionEndToEndTest"; |
| |
| private static String getTestMarker() { |
| return TEST_MARKER_PREFIX + "/" + SystemClock.elapsedRealtimeNanos(); |
| } |
| |
| /** |
| * A utility method to verify a method is called within a certain timeout period then block |
| * it by {@link BlockingMethodVerifier#close()} is called. |
| */ |
| private static final class BlockingMethodVerifier implements AutoCloseable { |
| private final CountDownLatch mWaitUntilMethodCalled = new CountDownLatch(1); |
| private final CountDownLatch mWaitUntilTestFinished = new CountDownLatch(1); |
| |
| /** |
| * Used to notify when a method to be tested is called. |
| */ |
| void onMethodCalled() { |
| try { |
| mWaitUntilMethodCalled.countDown(); |
| mWaitUntilTestFinished.await(); |
| } catch (InterruptedException e) { |
| } |
| } |
| |
| /** |
| * Ensures that the method to be tested is called within {@param timeout}. |
| * |
| * @param message Message to be shown when the method is not called despite the expectation. |
| * @param timeout Timeout in milliseconds. |
| */ |
| void expectMethodCalled(@NonNull String message, long timeout) { |
| try { |
| assertTrue(message, mWaitUntilMethodCalled.await(timeout, TimeUnit.MILLISECONDS)); |
| } catch (InterruptedException e) { |
| fail(message + e); |
| } |
| } |
| |
| /** |
| * Unblock the method to be tested to avoid the test from being blocked forever. |
| */ |
| @Override |
| public void close() throws Exception { |
| mWaitUntilTestFinished.countDown(); |
| } |
| } |
| |
| /** |
| * A utility method to verify that a method is called with a certain set of parameters. |
| */ |
| private static final class MethodCallVerifier { |
| private final AtomicReference<Bundle> mArgs = new AtomicReference<>(); |
| private final AtomicInteger mCallCount = new AtomicInteger(0); |
| |
| @AnyThread |
| void reset() { |
| mArgs.set(null); |
| mCallCount.set(0); |
| } |
| |
| /** |
| * Used to record when a method to be tested is called. |
| * |
| * @param argumentsRecorder a {@link Consumer} to capture method parameters. |
| */ |
| void onMethodCalled(@NonNull Consumer<Bundle> argumentsRecorder) { |
| final Bundle bundle = new Bundle(); |
| argumentsRecorder.accept(bundle); |
| mArgs.set(bundle); |
| mCallCount.incrementAndGet(); |
| } |
| |
| /** |
| * Used to assert captured parameters later. |
| * |
| * @param argumentsVerifier a {@link Consumer} to verify method arguments. |
| * @throws AssertionError when {@link #onMethodCalled(Consumer)} was not called only once. |
| */ |
| void assertCalledOnce(@NonNull Consumer<Bundle> argumentsVerifier) { |
| assertEquals(1, mCallCount.get()); |
| final Bundle bundle = mArgs.get(); |
| assertNotNull(bundle); |
| argumentsVerifier.accept(bundle); |
| } |
| |
| /** |
| * Ensures that the method to be tested is called within {@param timeout}. |
| * |
| * @param argumentsVerifier a {@link Consumer} to verify method arguments. |
| * @param timeout timeout in millisecond |
| * @throws AssertionError when {@link #onMethodCalled(Consumer)} was not called only once. |
| */ |
| void expectCalledOnce(@NonNull Consumer<Bundle> argumentsVerifier, long timeout) { |
| // Currently using busy-wait because CountDownLatch is not compatible with reset(). |
| // TODO: Consider using other more efficient operation. |
| long remainingTime = timeout; |
| while (mCallCount.get() == 0) { |
| if (remainingTime < 0) { |
| fail("The method must be called, but was not within" + timeout + " msec."); |
| } |
| SystemClock.sleep(TIME_SLICE); |
| remainingTime -= TIME_SLICE; |
| } |
| assertEquals(1, mCallCount.get()); |
| final Bundle bundle = mArgs.get(); |
| assertNotNull(bundle); |
| argumentsVerifier.accept(bundle); |
| } |
| |
| /** |
| * Used to assert that {@link #onMethodCalled(Consumer)} was never called. |
| * |
| * @param callCountVerificationMessage A message to be used when the assertion fails. |
| */ |
| void assertNotCalled(@Nullable String callCountVerificationMessage) { |
| if (callCountVerificationMessage != null) { |
| assertEquals(callCountVerificationMessage, 0, mCallCount.get()); |
| } else { |
| assertEquals(0, mCallCount.get()); |
| } |
| } |
| |
| /** |
| * Ensures that the method to be tested is not called within {@param timeout}. |
| * |
| * @param callCountVerificationMessage A message to be used when the assertion fails. |
| * @param timeout timeout in millisecond |
| */ |
| void expectNotCalled(@Nullable String callCountVerificationMessage, long timeout) { |
| // Currently using busy-wait because CountDownLatch is not compatible with reset(). |
| // TODO: Consider using other more efficient operation. |
| long remainingTime = timeout; |
| while (true) { |
| if (mCallCount.get() != 0) { |
| fail("The method must not be called. params=" + evaluateBundle(mArgs.get())); |
| } |
| if (remainingTime < 0) { |
| break; // This is indeed an expected scenario, not an error. |
| } |
| SystemClock.sleep(TIME_SLICE); |
| remainingTime -= TIME_SLICE; |
| } |
| if (callCountVerificationMessage != null) { |
| assertEquals(callCountVerificationMessage, 0, mCallCount.get()); |
| } else { |
| assertEquals(0, mCallCount.get()); |
| } |
| } |
| |
| /** |
| * Recursively evaluate {@link Bundle} so that {@link Bundle#toString()} can print all the |
| * nested {@link Bundle} objects. |
| * |
| * @param bundle {@link Bundle} to recursively evaluate. |
| * @return the {@code bundle} object passed. |
| */ |
| @Nullable |
| private static Bundle evaluateBundle(@Nullable Bundle bundle) { |
| if (bundle != null) { |
| for (String key : bundle.keySet()) { |
| final Object value = bundle.get(key); |
| if (value instanceof Bundle) { |
| evaluateBundle((Bundle) value); |
| } |
| } |
| } |
| return bundle; |
| } |
| } |
| |
| /** |
| * A test procedure definition for |
| * {@link #testInputConnection(Function, TestProcedure, AutoCloseable)}. |
| */ |
| @FunctionalInterface |
| interface TestProcedure { |
| /** |
| * The test body of {@link #testInputConnection(Function, TestProcedure, AutoCloseable)} |
| * |
| * @param session {@link MockImeSession} to be used during this test. |
| * @param stream {@link ImeEventStream} associated with {@code session}. |
| */ |
| void run(@NonNull MockImeSession session, @NonNull ImeEventStream stream) throws Exception; |
| } |
| |
| /** |
| * Tries to trigger {@link com.android.cts.mockime.MockIme#onUnbindInput()} by showing another |
| * Activity in a different process. |
| */ |
| private void triggerUnbindInput() { |
| final boolean isInstant = InstrumentationRegistry.getInstrumentation().getTargetContext() |
| .getPackageManager().isInstantApp(); |
| MockTestActivityUtil.launchSync(isInstant, TIMEOUT); |
| } |
| |
| /** |
| * A utility method to run a unit test for {@link InputConnection}. |
| * |
| * <p>This utility method enables you to avoid boilerplate code when writing unit tests for |
| * {@link InputConnection}.</p> |
| * |
| * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the |
| * original {@link InputConnection}. |
| * @param testProcedure Test body. |
| */ |
| private void testInputConnection( |
| Function<InputConnection, InputConnection> inputConnectionWrapperProvider, |
| TestProcedure testProcedure) throws Exception { |
| testInputConnection(inputConnectionWrapperProvider, testProcedure, null); |
| } |
| |
| /** |
| * A utility method to run a unit test for {@link InputConnection} that is as-if built with |
| * {@link android.os.Build.VERSION_CODES#CUPCAKE} SDK. |
| * |
| * <p>This helps you to test the situation where IMEs' calling newly added |
| * {@link InputConnection} APIs would be fallen back to its default interface method or could be |
| * causing {@link java.lang.AbstractMethodError} unless specially handled. |
| * |
| * @param testProcedure Test body. |
| */ |
| private void testMinimallyImplementedInputConnection(TestProcedure testProcedure) |
| throws Exception { |
| testInputConnection( |
| ic -> LegacyImeClientTestUtils.createMinimallyImplementedNoOpInputConnection(), |
| testProcedure, null); |
| } |
| |
| /** |
| * A utility method to run a unit test for {@link InputConnection}. |
| * |
| * <p>This utility method enables you to avoid boilerplate code when writing unit tests for |
| * {@link InputConnection}.</p> |
| * |
| * @param inputConnectionWrapperProvider {@link Function} to install custom hooks to the |
| * original {@link InputConnection}. |
| * @param testProcedure Test body. |
| * @param closeable {@link AutoCloseable} object to be cleaned up after running test. |
| */ |
| private void testInputConnection( |
| Function<InputConnection, InputConnection> inputConnectionWrapperProvider, |
| TestProcedure testProcedure, @Nullable AutoCloseable closeable) throws Exception { |
| try (AutoCloseable closeableHolder = closeable; |
| MockImeSession imeSession = MockImeSession.create( |
| InstrumentationRegistry.getInstrumentation().getContext(), |
| InstrumentationRegistry.getInstrumentation().getUiAutomation(), |
| new ImeSettings.Builder())) { |
| final ImeEventStream stream = imeSession.openEventStream(); |
| |
| final String marker = getTestMarker(); |
| TestActivity.startSync(activity -> { |
| final LinearLayout layout = new LinearLayout(activity); |
| layout.setOrientation(LinearLayout.VERTICAL); |
| |
| // Just to be conservative, we explicitly check MockImeSession#isActive() here when |
| // injecting our custom InputConnection implementation. |
| final EditText editText = new EditText(activity) { |
| @Override |
| public boolean onCheckIsTextEditor() { |
| return imeSession.isActive(); |
| } |
| |
| @Override |
| public InputConnection onCreateInputConnection(EditorInfo outAttrs) { |
| if (imeSession.isActive()) { |
| final InputConnection ic = super.onCreateInputConnection(outAttrs); |
| return inputConnectionWrapperProvider.apply(ic); |
| } |
| return null; |
| } |
| }; |
| |
| editText.setPrivateImeOptions(marker); |
| editText.setHint("editText"); |
| editText.requestFocus(); |
| |
| layout.addView(editText); |
| activity.getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE); |
| return layout; |
| }); |
| |
| // Wait until the MockIme gets bound to the TestActivity. |
| expectBindInput(stream, Process.myPid(), TIMEOUT); |
| |
| // Wait until "onStartInput" gets called for the EditText. |
| expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT); |
| |
| testProcedure.run(imeSession, stream); |
| } |
| } |
| |
| /** |
| * Ensures that {@code event}'s elapse time is less than the given threshold. |
| * |
| * @param event {@link ImeEvent} to be tested. |
| * @param elapseNanoTimeThreshold threshold in nano sec. |
| */ |
| private static void expectElapseTimeLessThan(@NonNull ImeEvent event, |
| long elapseNanoTimeThreshold) { |
| final long elapseNanoTime = event.getExitTimestamp() - event.getEnterTimestamp(); |
| if (elapseNanoTime > elapseNanoTimeThreshold) { |
| fail(event.getEventName() + " took " + elapseNanoTime + " nsec," |
| + " which must be less than" + elapseNanoTimeThreshold + " nsec."); |
| } |
| } |
| |
| @Nullable |
| private static CharSequence createTestCharSequence(@Nullable String text, |
| @Nullable Annotation annotation) { |
| if (text == null) { |
| return null; |
| } |
| final SpannableStringBuilder sb = new SpannableStringBuilder(text); |
| if (annotation != null) { |
| sb.setSpan(annotation, 0, sb.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); |
| } |
| return sb; |
| } |
| |
| private static void assertEqualsForTestCharSequence(@Nullable CharSequence expected, |
| @Nullable CharSequence actual) { |
| assertEquals(Objects.toString(expected), Objects.toString(actual)); |
| final Function<CharSequence, List<Annotation>> toAnnotations = cs -> { |
| if (cs instanceof Spanned) { |
| final Spanned spanned = (Spanned) cs; |
| return Arrays.asList(spanned.getSpans(0, cs.length(), Annotation.class)); |
| } |
| return Collections.emptyList(); |
| }; |
| assertThat(toAnnotations.apply(actual)).comparingElementsUsing(Correspondence.transforming( |
| (Annotation annotation) -> Pair.create(annotation.getKey(), annotation.getValue()), |
| (Annotation annotation) -> Pair.create(annotation.getKey(), annotation.getValue()), |
| "has the same Key/Value as")) |
| .containsExactlyElementsIn(toAnnotations.apply(expected)); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextAfterCursor(int, int)} works as expected. |
| */ |
| @Test |
| public void testGetTextAfterCursor() throws Exception { |
| final int expectedN = 3; |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final CharSequence expectedResult = |
| createTestCharSequence("89", new Annotation("command", "getTextAfterCursor")); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextAfterCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetTextAfterCursor(expectedN, expectedFlags); |
| final CharSequence result = |
| expectCommand(stream, command, TIMEOUT).getReturnCharSequenceValue(); |
| assertEqualsForTestCharSequence(expectedResult, result); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedN, args.get("n")); |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextAfterCursor(int, int)} fails when a negative |
| * {@code length} is passed. See Bug 169114026 for background. |
| */ |
| @Test |
| public void testGetTextAfterCursorFailWithNegativeLength() throws Exception { |
| final String unexpectedResult = "123"; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextAfterCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetTextAfterCursor(-1, 0); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("IC#getTextAfterCursor() returns null for a negative length.", |
| result.isNullReturnValue()); |
| methodCallVerifier.expectNotCalled( |
| "IC#getTextAfterCursor() will not be triggered with a negative length.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextAfterCursor(int, int)} fails after a system-defined |
| * time-out even if the target app does not respond. |
| */ |
| @Test |
| public void testGetTextAfterCursorFailWithTimeout() throws Exception { |
| final int expectedN = 3; |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final String unexpectedResult = "89"; |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextAfterCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetTextAfterCursor(expectedN, expectedFlags); |
| blocker.expectMethodCalled("IC#getTextAfterCursor() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("When timeout happens, IC#getTextAfterCursor() returns null", |
| result.isNullReturnValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedN, args.get("n")); |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextAfterCursor(int, int)} fail-fasts once unbindInput() is |
| * issued. |
| */ |
| @Test |
| public void testGetTextAfterCursorFailFastAfterUnbindInput() throws Exception { |
| final String unexpectedResult = "89"; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextAfterCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getTextAfterCursor() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, session.callGetTextAfterCursor( |
| unexpectedResult.length(), InputConnection.GET_TEXT_WITH_STYLES), TIMEOUT); |
| assertTrue("Once unbindInput() happened, IC#getTextAfterCursor() returns null", |
| result.isNullReturnValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#getTextAfterCursor() fails fast."); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextBeforeCursor(int, int)} works as expected. |
| */ |
| @Test |
| public void testGetTextBeforeCursor() throws Exception { |
| final int expectedN = 3; |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final CharSequence expectedResult = |
| createTestCharSequence("123", new Annotation("command", "getTextBeforeCursor")); |
| |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextBeforeCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetTextBeforeCursor(expectedN, expectedFlags); |
| final CharSequence result = |
| expectCommand(stream, command, TIMEOUT).getReturnCharSequenceValue(); |
| assertEqualsForTestCharSequence(expectedResult, result); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedN, args.get("n")); |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextBeforeCursor(int, int)} fails when a negative |
| * {@code length} is passed. See Bug 169114026 for background. |
| */ |
| @Test |
| public void testGetTextBeforeCursorFailWithNegativeLength() throws Exception { |
| final String unexpectedResult = "123"; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextBeforeCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetTextBeforeCursor(-1, 0); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("IC#getTextBeforeCursor() returns null for a negative length.", |
| result.isNullReturnValue()); |
| methodCallVerifier.expectNotCalled( |
| "IC#getTextBeforeCursor() will not be triggered with a negative length.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextBeforeCursor(int, int)} fails after a system-defined |
| * time-out even if the target app does not respond. |
| */ |
| @Test |
| public void testGetTextBeforeCursorFailWithTimeout() throws Exception { |
| final int expectedN = 3; |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final String unexpectedResult = "123"; |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextBeforeCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetTextBeforeCursor(expectedN, expectedFlags); |
| blocker.expectMethodCalled("IC#getTextBeforeCursor() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("When timeout happens, IC#getTextBeforeCursor() returns null", |
| result.isNullReturnValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedN, args.get("n")); |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#getTextBeforeCursor(int, int)} fail-fasts once unbindInput() is |
| * issued. |
| */ |
| @Test |
| public void testGetTextBeforeCursorFailFastAfterUnbindInput() throws Exception { |
| final String unexpectedResult = "123"; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getTextBeforeCursor(int n, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("n", n); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getTextBeforeCursor() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, session.callGetTextBeforeCursor( |
| unexpectedResult.length(), InputConnection.GET_TEXT_WITH_STYLES), TIMEOUT); |
| assertTrue("Once unbindInput() happened, IC#getTextBeforeCursor() returns null", |
| result.isNullReturnValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#getTextBeforeCursor() fails fast."); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSelectedText(int)} works as expected. |
| */ |
| @Test |
| public void testGetSelectedText() throws Exception { |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final CharSequence expectedResult = |
| createTestCharSequence("4567", new Annotation("command", "getSelectedText")); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getSelectedText(int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("flags", flags); |
| }); |
| assertEquals(expectedFlags, flags); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetSelectedText(expectedFlags); |
| final CharSequence result = |
| expectCommand(stream, command, TIMEOUT).getReturnCharSequenceValue(); |
| assertEqualsForTestCharSequence(expectedResult, result); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSelectedText(int)} fails after a system-defined time-out even |
| * if the target app does not respond. |
| */ |
| @Test |
| public void testGetSelectedTextFailWithTimeout() throws Exception { |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final String unexpectedResult = "4567"; |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getSelectedText(int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("flags", flags); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = |
| session.callGetSelectedText(InputConnection.GET_TEXT_WITH_STYLES); |
| blocker.expectMethodCalled("IC#getSelectedText() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("When timeout happens, IC#getSelectedText() returns null", |
| result.isNullReturnValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSelectedText(int)} fail-fasts once unbindInput() is issued. |
| */ |
| @Test |
| public void testGetSelectedTextFailFastAfterUnbindInput() throws Exception { |
| final String unexpectedResult = "4567"; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public CharSequence getSelectedText(int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getSelectedText() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, session.callGetSelectedText( |
| InputConnection.GET_TEXT_WITH_STYLES), TIMEOUT); |
| assertTrue("Once unbindInput() happened, IC#getSelectedText() returns null", |
| result.isNullReturnValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#getSelectedText() fails fast."); |
| }); |
| } |
| |
| /** |
| * Verify that {@link InputConnection#getSelectedText(int)} returns {@code null} when the target |
| * app does not implement it. This can happen if the app was built before |
| * {@link android.os.Build.VERSION_CODES#GINGERBREAD}. |
| */ |
| @Test |
| public void testGetSelectedTextFailWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetSelectedText(0); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("Currently getSelectedText() returns null when the target app does not" |
| + " implement it.", result.isNullReturnValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSurroundingText(int, int, int)} works as expected. |
| */ |
| @Test |
| public void testGetSurroundingText() throws Exception { |
| final int expectedBeforeLength = 3; |
| final int expectedAfterLength = 4; |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final CharSequence expectedText = |
| createTestCharSequence("012345", new Annotation("command", "getSurroundingText")); |
| final SurroundingText expectedResult = new SurroundingText(expectedText, 1, 2, 0); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public SurroundingText getSurroundingText(int beforeLength, int afterLength, |
| int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| args.putInt("flags", flags); |
| }); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetSurroundingText(expectedBeforeLength, |
| expectedAfterLength, expectedFlags); |
| final SurroundingText result = |
| expectCommand(stream, command, TIMEOUT).getReturnParcelableValue(); |
| assertEqualsForTestCharSequence(expectedResult.getText(), result.getText()); |
| assertEquals(expectedResult.getSelectionStart(), result.getSelectionStart()); |
| assertEquals(expectedResult.getSelectionEnd(), result.getSelectionEnd()); |
| assertEquals(expectedResult.getOffset(), result.getOffset()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedBeforeLength, args.get("beforeLength")); |
| assertEquals(expectedAfterLength, args.get("afterLength")); |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSurroundingText(int, int, int)} fails when a nagative |
| * {@code afterLength} is passed. See Bug 169114026 for background. |
| */ |
| @Test |
| public void testGetSurroundingTextFailWithNegativeAfterLength() throws Exception { |
| final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public SurroundingText getSurroundingText(int beforeLength, int afterLength, |
| int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetSurroundingText(1, -1, 0); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("IC#getSurroundingText() returns null for a negative afterLength.", |
| result.isNullReturnValue()); |
| methodCallVerifier.expectNotCalled( |
| "IC#getSurroundingText() will not be triggered with a negative afterLength.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSurroundingText(int, int, int)} fails when a negative |
| * {@code beforeLength} is passed. See Bug 169114026 for background. |
| */ |
| @Test |
| public void testGetSurroundingTextFailWithNegativeBeforeLength() throws Exception { |
| final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public SurroundingText getSurroundingText(int beforeLength, int afterLength, |
| int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetSurroundingText(-1, 1, 0); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("IC#getSurroundingText() returns null for a negative beforeLength.", |
| result.isNullReturnValue()); |
| methodCallVerifier.expectNotCalled( |
| "IC#getSurroundingText() will not be triggered with a negative beforeLength.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSurroundingText(int, int, int)} fails after a system-defined |
| * time-out even if the target app does not respond. |
| */ |
| @Test |
| public void testGetSurroundingTextFailWithTimeout() throws Exception { |
| final int expectedBeforeLength = 3; |
| final int expectedAfterLength = 4; |
| final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES; |
| final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); |
| |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public SurroundingText getSurroundingText(int beforeLength, int afterLength, |
| int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| args.putInt("flags", flags); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetSurroundingText(expectedBeforeLength, |
| expectedAfterLength, expectedFlags); |
| blocker.expectMethodCalled("IC#getSurroundingText() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("When timeout happens, IC#getSurroundingText() returns null", |
| result.isNullReturnValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedBeforeLength, args.get("beforeLength")); |
| assertEquals(expectedAfterLength, args.get("afterLength")); |
| assertEquals(expectedFlags, args.get("flags")); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#getSurroundingText(int, int, int)} fail-fasts once unbindInput() |
| * is issued. |
| */ |
| @Test |
| public void testGetSurroundingTextFailFastAfterUnbindInput() throws Exception { |
| final int beforeLength = 3; |
| final int afterLength = 4; |
| final int flags = InputConnection.GET_TEXT_WITH_STYLES; |
| final SurroundingText unexpectedResult = new SurroundingText("012345", 1, 2, 0); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public SurroundingText getSurroundingText(int beforeLength, int afterLength, |
| int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getTextBeforeCursor() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, session.callGetSurroundingText( |
| beforeLength, afterLength, flags), TIMEOUT); |
| assertTrue("Once unbindInput() happened, IC#getSurroundingText() returns null", |
| result.isNullReturnValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#getSurroundingText() fails fast."); |
| }); |
| } |
| |
| /** |
| * Verify that the default implementation of |
| * {@link InputConnection#getSurroundingText(int, int, int)} returns {@code null} without any |
| * crash even when the target app does not override it . |
| */ |
| @Test |
| public void testGetSurroundingTextDefaultMethod() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetSurroundingText(1, 2, 0); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("Default IC#getSurroundingText() returns null.", |
| result.isNullReturnValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getCursorCapsMode(int)} works as expected. |
| */ |
| @Test |
| public void testGetCursorCapsMode() throws Exception { |
| final int expectedReqMode = TextUtils.CAP_MODE_SENTENCES | TextUtils.CAP_MODE_CHARACTERS |
| | TextUtils.CAP_MODE_WORDS; |
| final int expectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public int getCursorCapsMode(int reqModes) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("reqModes", reqModes); |
| }); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetCursorCapsMode(expectedReqMode); |
| final int result = expectCommand(stream, command, TIMEOUT).getReturnIntegerValue(); |
| assertEquals(expectedResult, result); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedReqMode, args.getInt("reqModes")); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getCursorCapsMode(int)} fails after a system-defined time-out |
| * even if the target app does not respond. |
| */ |
| @Test |
| public void testGetCursorCapsModeFailWithTimeout() throws Exception { |
| final int expectedReqMode = TextUtils.CAP_MODE_SENTENCES | TextUtils.CAP_MODE_CHARACTERS |
| | TextUtils.CAP_MODE_WORDS; |
| final int unexpectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public int getCursorCapsMode(int reqModes) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("reqModes", reqModes); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetCursorCapsMode(expectedReqMode); |
| blocker.expectMethodCalled("IC#getCursorCapsMode() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertEquals("When timeout happens, IC#getCursorCapsMode() returns 0", |
| 0, result.getReturnIntegerValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedReqMode, args.getInt("reqModes")); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#getCursorCapsMode(int)} fail-fasts once unbindInput() is issued. |
| */ |
| @Test |
| public void testGetCursorCapsModeFailFastAfterUnbindInput() throws Exception { |
| final int unexpectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public int getCursorCapsMode(int reqModes) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("reqModes", reqModes); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getCursorCapsMode() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, |
| session.callGetCursorCapsMode(TextUtils.CAP_MODE_WORDS), TIMEOUT); |
| assertEquals("Once unbindInput() happened, IC#getCursorCapsMode() returns 0", |
| 0, result.getReturnIntegerValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#getCursorCapsMode() fails fast."); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getExtractedText(ExtractedTextRequest, int)} works as expected. |
| */ |
| @Test |
| public void testGetExtractedText() throws Exception { |
| final ExtractedTextRequest expectedRequest = ExtractedTextRequestTest.createForTest(); |
| final int expectedFlags = InputConnection.GET_EXTRACTED_TEXT_MONITOR; |
| final ExtractedText expectedResult = ExtractedTextTest.createForTest(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("request", request); |
| args.putInt("flags", flags); |
| }); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetExtractedText(expectedRequest, expectedFlags); |
| final ExtractedText result = |
| expectCommand(stream, command, TIMEOUT).getReturnParcelableValue(); |
| ExtractedTextTest.assertTestInstance(result); |
| methodCallVerifier.assertCalledOnce(args -> { |
| ExtractedTextRequestTest.assertTestInstance(args.getParcelable("request")); |
| assertEquals(expectedFlags, args.getInt("flags")); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getExtractedText(ExtractedTextRequest, int)} fails after a |
| * system-defined time-out even if the target app does not respond. |
| */ |
| @Test |
| public void testGetExtractedTextFailWithTimeout() throws Exception { |
| final ExtractedTextRequest expectedRequest = ExtractedTextRequestTest.createForTest(); |
| final int expectedFlags = InputConnection.GET_EXTRACTED_TEXT_MONITOR; |
| final ExtractedText unexpectedResult = ExtractedTextTest.createForTest(); |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("request", request); |
| args.putInt("flags", flags); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetExtractedText(expectedRequest, expectedFlags); |
| blocker.expectMethodCalled("IC#getExtractedText() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertTrue("When timeout happens, IC#getExtractedText() returns null", |
| result.isNullReturnValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| ExtractedTextRequestTest.assertTestInstance(args.getParcelable("request")); |
| assertEquals(expectedFlags, args.getInt("flags")); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#getExtractedText(ExtractedTextRequest, int)} fail-fasts once |
| * unbindInput() is issued. |
| */ |
| @Test |
| public void testGetExtractedTextFailFastAfterUnbindInput() throws Exception { |
| final ExtractedText unexpectedResult = ExtractedTextTest.createForTest(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("request", request); |
| args.putInt("flags", flags); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getExtractedText() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, session.callGetExtractedText( |
| ExtractedTextRequestTest.createForTest(), |
| InputConnection.GET_EXTRACTED_TEXT_MONITOR), TIMEOUT); |
| assertTrue("Once unbindInput() happened, IC#getExtractedText() returns null", |
| result.isNullReturnValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#getExtractedText() fails fast."); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#requestCursorUpdates(int)} works as expected. |
| */ |
| @Test |
| public void testRequestCursorUpdates() throws Exception { |
| final int expectedFlags = InputConnection.CURSOR_UPDATE_IMMEDIATE; |
| final boolean expectedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean requestCursorUpdates(int cursorUpdateMode) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("cursorUpdateMode", cursorUpdateMode); |
| }); |
| assertEquals(expectedFlags, cursorUpdateMode); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callRequestCursorUpdates(expectedFlags); |
| assertTrue(expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedFlags, args.getInt("cursorUpdateMode")); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#requestCursorUpdates(int)} fails after a system-defined time-out |
| * even if the target app does not respond. |
| */ |
| @Test |
| public void testRequestCursorUpdatesFailWithTimeout() throws Exception { |
| final int expectedFlags = InputConnection.CURSOR_UPDATE_IMMEDIATE; |
| final boolean unexpectedResult = true; |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean requestCursorUpdates(int cursorUpdateMode) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("cursorUpdateMode", cursorUpdateMode); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callRequestCursorUpdates( |
| InputConnection.CURSOR_UPDATE_IMMEDIATE); |
| blocker.expectMethodCalled("IC#requestCursorUpdates() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertFalse("When timeout happens, IC#requestCursorUpdates() returns false", |
| result.getReturnBooleanValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| assertEquals(expectedFlags, args.getInt("cursorUpdateMode")); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#requestCursorUpdates(int)} fail-fasts once unbindInput() is |
| * issued. |
| */ |
| @Test |
| public void testRequestCursorUpdatesFailFastAfterUnbindInput() throws Exception { |
| final boolean unexpectedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean requestCursorUpdates(int cursorUpdateMode) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("cursorUpdateMode", cursorUpdateMode); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#requestCursorUpdates() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, session.callRequestCursorUpdates( |
| InputConnection.CURSOR_UPDATE_IMMEDIATE), TIMEOUT); |
| assertFalse("Once unbindInput() happened, IC#requestCursorUpdates() returns false", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#requestCursorUpdates() fails fast."); |
| }); |
| } |
| |
| /** |
| * Verify that {@link InputConnection#requestCursorUpdates(int)} fails when the target app does |
| * not implement it. This can happen if the app was built before |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. |
| */ |
| @Test |
| public void testRequestCursorUpdatesFailWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callRequestCursorUpdates( |
| InputConnection.CURSOR_UPDATE_IMMEDIATE); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertFalse("IC#requestCursorUpdates() returns false when the target app does not " |
| + " implement it.", result.getReturnBooleanValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} works as expected. |
| */ |
| @Test |
| public void testCommitContent() throws Exception { |
| final InputContentInfo expectedInputContentInfo = new InputContentInfo( |
| Uri.parse("content://com.example/path"), |
| new ClipDescription("sample content", new String[]{"image/png"}), |
| Uri.parse("https://example.com")); |
| final Bundle expectedOpt = new Bundle(); |
| final String expectedOptKey = "testKey"; |
| final int expectedOptValue = 42; |
| expectedOpt.putInt(expectedOptKey, expectedOptValue); |
| final int expectedFlags = InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION; |
| final boolean expectedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitContent(InputContentInfo inputContentInfo, int flags, |
| Bundle opts) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("inputContentInfo", inputContentInfo); |
| args.putInt("flags", flags); |
| args.putBundle("opts", opts); |
| }); |
| return expectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = |
| session.callCommitContent(expectedInputContentInfo, expectedFlags, expectedOpt); |
| assertTrue(expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| final InputContentInfo inputContentInfo = args.getParcelable("inputContentInfo"); |
| final Bundle opts = args.getBundle("opts"); |
| assertNotNull(inputContentInfo); |
| assertEquals(expectedInputContentInfo.getContentUri(), |
| inputContentInfo.getContentUri()); |
| assertEquals(expectedFlags, args.getInt("flags")); |
| assertNotNull(opts); |
| assertEquals(expectedOpt.getInt(expectedOptKey), opts.getInt(expectedOptKey)); |
| }); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} fails after a |
| * system-defined time-out even if the target app does not respond. |
| */ |
| @Test |
| public void testCommitContentFailWithTimeout() throws Exception { |
| final InputContentInfo expectedInputContentInfo = new InputContentInfo( |
| Uri.parse("content://com.example/path"), |
| new ClipDescription("sample content", new String[]{"image/png"}), |
| Uri.parse("https://example.com")); |
| final Bundle expectedOpt = new Bundle(); |
| final String expectedOptKey = "testKey"; |
| final int expectedOptValue = 42; |
| expectedOpt.putInt(expectedOptKey, expectedOptValue); |
| final int expectedFlags = InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION; |
| final boolean unexpectedResult = true; |
| final BlockingMethodVerifier blocker = new BlockingMethodVerifier(); |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitContent(InputContentInfo inputContentInfo, int flags, |
| Bundle opts) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("inputContentInfo", inputContentInfo); |
| args.putInt("flags", flags); |
| args.putBundle("opts", opts); |
| }); |
| blocker.onMethodCalled(); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = |
| session.callCommitContent(expectedInputContentInfo, expectedFlags, expectedOpt); |
| blocker.expectMethodCalled("IC#commitContent() must be called back", TIMEOUT); |
| final ImeEvent result = expectCommand(stream, command, LONG_TIMEOUT); |
| assertFalse("When timeout happens, IC#commitContent() returns false", |
| result.getReturnBooleanValue()); |
| methodCallVerifier.assertCalledOnce(args -> { |
| final InputContentInfo inputContentInfo = args.getParcelable("inputContentInfo"); |
| final Bundle opts = args.getBundle("opts"); |
| assertNotNull(inputContentInfo); |
| assertEquals(expectedInputContentInfo.getContentUri(), |
| inputContentInfo.getContentUri()); |
| assertEquals(expectedFlags, args.getInt("flags")); |
| assertNotNull(opts); |
| assertEquals(expectedOpt.getInt(expectedOptKey), opts.getInt(expectedOptKey)); |
| }); |
| }, blocker); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} fail-fasts once |
| * unbindInput() is issued. |
| */ |
| @Test |
| public void testCommitContentFailFastAfterUnbindInput() throws Exception { |
| final boolean unexpectedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitContent(InputContentInfo inputContentInfo, int flags, |
| Bundle opts) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("inputContentInfo", inputContentInfo); |
| args.putInt("flags", flags); |
| args.putBundle("opts", opts); |
| }); |
| return unexpectedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getTextAfterCursor() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, session.callCommitContent( |
| new InputContentInfo(Uri.parse("content://com.example/path"), |
| new ClipDescription("sample content", new String[]{"image/png"}), |
| Uri.parse("https://example.com")), 0, null), TIMEOUT); |
| assertFalse("Once unbindInput() happened, IC#commitContent() returns false", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| methodCallVerifier.assertNotCalled( |
| "Once unbindInput() happened, IC#commitContent() fails fast."); |
| }); |
| } |
| |
| /** |
| * Verify that {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} fails when |
| * the target app does not implement it. This can happen if the app was built before |
| * {@link android.os.Build.VERSION_CODES#N_MR1}. |
| */ |
| @Test |
| public void testCommitContentFailWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callCommitContent( |
| new InputContentInfo(Uri.parse("content://com.example/path"), |
| new ClipDescription("sample content", new String[]{"image/png"}), |
| Uri.parse("https://example.com")), 0, null); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertFalse("Currently IC#commitContent() returns false when the target app does not" |
| + " implement it.", result.getReturnBooleanValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#deleteSurroundingText(int, int)} works as expected. |
| */ |
| @Test |
| public void testDeleteSurroundingText() throws Exception { |
| final int expectedBeforeLength = 5; |
| final int expectedAfterLength = 4; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean deleteSurroundingText(int beforeLength, int afterLength) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = |
| session.callDeleteSurroundingText(expectedBeforeLength, expectedAfterLength); |
| assertTrue("deleteSurroundingText() always returns true unless RemoteException is" |
| + " thrown", expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEquals(expectedBeforeLength, args.getInt("beforeLength")); |
| assertEquals(expectedAfterLength, args.getInt("afterLength")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#deleteSurroundingText(int, int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testDeleteSurroundingTextAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean deleteSurroundingText(int beforeLength, int afterLength) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#deleteSurroundingText() for the memorized IC should fail fast. |
| final ImeCommand command = session.callDeleteSurroundingText(3, 4); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#deleteSurroundingText() still returns true even after" |
| + " unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#deleteSurroundingText() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)} works as expected. |
| */ |
| @Test |
| public void testDeleteSurroundingTextInCodePoints() throws Exception { |
| final int expectedBeforeLength = 5; |
| final int expectedAfterLength = 4; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callDeleteSurroundingTextInCodePoints( |
| expectedBeforeLength, expectedAfterLength); |
| assertTrue("deleteSurroundingText() always returns true unless RemoteException is" |
| + " thrown", expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEquals(expectedBeforeLength, args.getInt("beforeLength")); |
| assertEquals(expectedAfterLength, args.getInt("afterLength")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testDeleteSurroundingTextInCodePointsAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("beforeLength", beforeLength); |
| args.putInt("afterLength", afterLength); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#deleteSurroundingTextInCodePoints() for the memorized IC should fail fast. |
| final ImeCommand command = session.callDeleteSurroundingTextInCodePoints(3, 4); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#deleteSurroundingTextInCodePoints() still returns true even" |
| + " after unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#deleteSurroundingTextInCodePoints() fails" |
| + " fast.", EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Verify that the app does not crash even if it does not implement |
| * {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}, which can happen if the |
| * app was built before {@link android.os.Build.VERSION_CODES#N}. |
| */ |
| @Test |
| public void testDeleteSurroundingTextInCodePointsFailWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callDeleteSurroundingTextInCodePoints(1, 2); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("IC#deleteSurroundingTextInCodePoints() returns true even when the target" |
| + " app does not implement it.", result.getReturnBooleanValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitText(CharSequence, int)} works as expected. |
| */ |
| @Test |
| public void testCommitText() throws Exception { |
| final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); |
| final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); |
| final int expectedNewCursorPosition = 123; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitText(CharSequence text, int newCursorPosition) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| }); |
| |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = |
| session.callCommitText(expectedText, expectedNewCursorPosition); |
| assertTrue("commitText() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); |
| assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitText(CharSequence, int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testCommitTextAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitText(CharSequence text, int newCursorPosition) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getTextAfterCursor() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, |
| session.callCommitText("text", 1), TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#commitText() still returns true even after unbindInput().", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#commitText() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitText(CharSequence, int, TextAttribute)} works as expected. |
| */ |
| @Test |
| public void testCommitTextWithTextAttribute() throws Exception { |
| final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); |
| final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); |
| final int expectedNewCursorPosition = 123; |
| final ArrayList<String> expectedSuggestions = new ArrayList<>(); |
| expectedSuggestions.add("test"); |
| final TextAttribute expectedTextAttribute = new TextAttribute.Builder() |
| .setTextConversionSuggestions(expectedSuggestions).build(); |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitText( |
| CharSequence text, int newCursorPosition, TextAttribute textAttribute) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| args.putParcelable("textAttribute", textAttribute); |
| }); |
| |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callCommitText( |
| expectedText, expectedNewCursorPosition, expectedTextAttribute); |
| assertTrue("commitText() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); |
| assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); |
| final TextAttribute textAttribute = args.getParcelable("textAttribute"); |
| assertThat(textAttribute).isNotNull(); |
| assertThat(textAttribute.getTextConversionSuggestions()) |
| .containsExactlyElementsIn(expectedSuggestions); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitText(CharSequence, int, TextAttribute)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testCommitTextAfterUnbindInputWithTextAttribute() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitText( |
| CharSequence text, int newCursorPosition, TextAttribute textAttribute) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| args.putParcelable("textAttribute", textAttribute); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now IC#getTextAfterCursor() for the memorized IC should fail fast. |
| final ImeEvent result = expectCommand(stream, |
| session.callCommitText("text", 1, |
| new TextAttribute.Builder().setTextConversionSuggestions( |
| Collections.singletonList("test")).build()), |
| TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#commitText() still returns true even after unbindInput().", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#commitText() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingText(CharSequence, int)} works as expected. |
| */ |
| @Test |
| public void testSetComposingText() throws Exception { |
| final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); |
| final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); |
| final int expectedNewCursorPosition = 123; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingText(CharSequence text, int newCursorPosition) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = |
| session.callSetComposingText(expectedText, expectedNewCursorPosition); |
| assertTrue("setComposingText() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); |
| assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingText(CharSequence, int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testSetComposingTextAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingText(CharSequence text, int newCursorPosition) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callSetComposingText("text", 1); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#setComposingText() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#setComposingText() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingText(CharSequence, int, TextAttribute)} |
| * works as expected. |
| */ |
| @Test |
| public void testSetComposingTextWithTextAttribute() throws Exception { |
| final Annotation expectedSpan = new Annotation("expectedKey", "expectedValue"); |
| final CharSequence expectedText = createTestCharSequence("expectedText", expectedSpan); |
| final int expectedNewCursorPosition = 123; |
| final ArrayList<String> expectedSuggestions = new ArrayList<>(); |
| expectedSuggestions.add("test"); |
| final TextAttribute expectedTextAttribute = new TextAttribute.Builder() |
| .setTextConversionSuggestions(expectedSuggestions).build(); |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingText(CharSequence text, int newCursorPosition, |
| TextAttribute textAttribute) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| args.putParcelable("textAttribute", textAttribute); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSetComposingText( |
| expectedText, expectedNewCursorPosition, expectedTextAttribute); |
| assertTrue("testSetComposingTextWithTextAttribute() always returns true unless" |
| + " RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEqualsForTestCharSequence(expectedText, args.getCharSequence("text")); |
| assertEquals(expectedNewCursorPosition, args.getInt("newCursorPosition")); |
| final TextAttribute textAttribute = args.getParcelable("textAttribute"); |
| assertThat(textAttribute).isNotNull(); |
| assertThat(textAttribute.getTextConversionSuggestions()) |
| .containsExactlyElementsIn(expectedSuggestions); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingText(CharSequence, int, TextAttribute)} fails fast |
| * once {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testSetComposingTextAfterUnbindInputWithTextAttribute() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingText(CharSequence text, int newCursorPosition, |
| TextAttribute textAttribute) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putCharSequence("text", text); |
| args.putInt("newCursorPosition", newCursorPosition); |
| args.putParcelable("textAttribute", textAttribute); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callSetComposingText( |
| "text", 1, new TextAttribute.Builder() |
| .setTextConversionSuggestions(Collections.singletonList("test")) |
| .build()); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#setComposingText() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#setComposingText() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingRegion(int, int)} works as expected. |
| */ |
| @Test |
| public void testSetComposingRegion() throws Exception { |
| final int expectedStart = 3; |
| final int expectedEnd = 17; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingRegion(int start, int end) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("start", start); |
| args.putInt("end", end); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSetComposingRegion(expectedStart, expectedEnd); |
| assertTrue("setComposingRegion() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEquals(expectedStart, args.getInt("start")); |
| assertEquals(expectedEnd, args.getInt("end")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingRegion(int, int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testSetComposingRegionTextAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingRegion(int start, int end) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("start", start); |
| args.putInt("end", end); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callSetComposingRegion(1, 23); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#setComposingRegion() still returns true even after" |
| + " unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#setComposingRegion() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Verify that the app does not crash even if it does not implement |
| * {@link InputConnection#setComposingRegion(int, int)}, which can happen if the app was built |
| * before {@link android.os.Build.VERSION_CODES#GINGERBREAD}. |
| */ |
| @Test |
| public void testSetComposingRegionFailWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSetComposingRegion(1, 23); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("IC#setComposingRegion() returns true even when the target app does not" |
| + " implement it.", result.getReturnBooleanValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingRegion} works as expected. |
| */ |
| @Test |
| public void testSetComposingRegionWithTextAttribute() throws Exception { |
| final int expectedStart = 3; |
| final int expectedEnd = 17; |
| final ArrayList<String> expectedSuggestions = new ArrayList<>(); |
| expectedSuggestions.add("test"); |
| final TextAttribute expectedTextAttribute = new TextAttribute.Builder() |
| .setTextConversionSuggestions(expectedSuggestions).build(); |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingRegion( |
| int start, int end, TextAttribute textAttribute) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("start", start); |
| args.putInt("end", end); |
| args.putParcelable("textAttribute", textAttribute); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSetComposingRegion( |
| expectedStart, expectedEnd, expectedTextAttribute); |
| assertTrue("setComposingRegion() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEquals(expectedStart, args.getInt("start")); |
| assertEquals(expectedEnd, args.getInt("end")); |
| final TextAttribute textAttribute = args.getParcelable("textAttribute"); |
| assertThat(textAttribute).isNotNull(); |
| assertThat(textAttribute.getTextConversionSuggestions()) |
| .containsExactlyElementsIn(expectedSuggestions); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setComposingRegion(int, int, TextAttribute)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testSetComposingRegionTextAfterUnbindInputWithTextAttribute() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setComposingRegion(int start, int end, TextAttribute textAttribute) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("start", start); |
| args.putInt("end", end); |
| args.putParcelable("textAttribute", textAttribute); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callSetComposingRegion(1, 23, |
| new TextAttribute.Builder().setTextConversionSuggestions( |
| Collections.singletonList("test")).build()); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#setComposingRegion() still returns true even after" |
| + " unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#setComposingRegion() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#finishComposingText()} works as expected. |
| */ |
| @Test |
| public void testFinishComposingText() throws Exception { |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean finishComposingText() { |
| methodCallVerifier.onMethodCalled(bundle -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callFinishComposingText(); |
| assertTrue("finishComposingText() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#finishComposingText()} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testFinishComposingTextAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean finishComposingText() { |
| methodCallVerifier.onMethodCalled(bundle -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // The system internally calls "finishComposingText". So wait for a while then reset |
| // the verifier before our calling "finishComposingText". |
| SystemClock.sleep(TIMEOUT); |
| methodCallVerifier.reset(); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callFinishComposingText(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#finishComposingText() still returns true even after" |
| + " unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#finishComposingText() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitCompletion(CompletionInfo)} works as expected. |
| */ |
| @Test |
| public void testCommitCompletion() throws Exception { |
| final CompletionInfo expectedCompletionInfo = new CompletionInfo(0x12345678, 0x87654321, |
| createTestCharSequence("testText", new Annotation("param", "text")), |
| createTestCharSequence("testLabel", new Annotation("param", "label"))); |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitCompletion(CompletionInfo text) { |
| methodCallVerifier.onMethodCalled(bundle -> { |
| bundle.putParcelable("text", text); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callCommitCompletion(expectedCompletionInfo); |
| assertTrue("commitCompletion() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| final CompletionInfo actualCompletionInfo = args.getParcelable("text"); |
| assertNotNull(actualCompletionInfo); |
| assertEquals(expectedCompletionInfo.getId(), actualCompletionInfo.getId()); |
| assertEquals(expectedCompletionInfo.getPosition(), |
| actualCompletionInfo.getPosition()); |
| assertEqualsForTestCharSequence(expectedCompletionInfo.getText(), |
| actualCompletionInfo.getText()); |
| assertEqualsForTestCharSequence(expectedCompletionInfo.getLabel(), |
| actualCompletionInfo.getLabel()); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitCompletion(CompletionInfo)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testCommitCompletionAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitCompletion(CompletionInfo text) { |
| methodCallVerifier.onMethodCalled(bundle -> { |
| bundle.putParcelable("text", text); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callCommitCompletion(new CompletionInfo( |
| 0x12345678, 0x87654321, |
| createTestCharSequence("testText", new Annotation("param", "text")), |
| createTestCharSequence("testLabel", new Annotation("param", "label")))); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#commitCompletion() still returns true even after" |
| + " unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#commitCompletion() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitCorrection(CorrectionInfo)} works as expected. |
| */ |
| @Test |
| public void testCommitCorrection() throws Exception { |
| final CorrectionInfo expectedCorrectionInfo = new CorrectionInfo(0x11111111, |
| createTestCharSequence("testOldText", new Annotation("param", "oldText")), |
| createTestCharSequence("testNewText", new Annotation("param", "newText"))); |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitCorrection(CorrectionInfo correctionInfo) { |
| methodCallVerifier.onMethodCalled(bundle -> { |
| bundle.putParcelable("correctionInfo", correctionInfo); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callCommitCorrection(expectedCorrectionInfo); |
| assertTrue("commitCorrection() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| final CorrectionInfo actualCorrectionInfo = args.getParcelable("correctionInfo"); |
| assertNotNull(actualCorrectionInfo); |
| assertEquals(expectedCorrectionInfo.getOffset(), |
| actualCorrectionInfo.getOffset()); |
| assertEqualsForTestCharSequence(expectedCorrectionInfo.getOldText(), |
| actualCorrectionInfo.getOldText()); |
| assertEqualsForTestCharSequence(expectedCorrectionInfo.getNewText(), |
| actualCorrectionInfo.getNewText()); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#commitCorrection(CorrectionInfo)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testCommitCorrectionAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean commitCorrection(CorrectionInfo correctionInfo) { |
| methodCallVerifier.onMethodCalled(bundle -> { |
| bundle.putParcelable("correctionInfo", correctionInfo); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callCommitCorrection(new CorrectionInfo(0x11111111, |
| createTestCharSequence("testOldText", new Annotation("param", "oldText")), |
| createTestCharSequence("testNewText", new Annotation("param", "newText")))); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#commitCorrection() still returns true even after" |
| + " unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#commitCorrection() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Verify that the app does not crash even if it does not implement |
| * {@link InputConnection#commitCorrection(CorrectionInfo)}, which can happen if the app was |
| * built before {@link android.os.Build.VERSION_CODES#HONEYCOMB}. |
| */ |
| @Test |
| public void testCommitCorrectionFailWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callCommitCorrection(new CorrectionInfo(0x11111111, |
| createTestCharSequence("testOldText", new Annotation("param", "oldText")), |
| createTestCharSequence("testNewText", new Annotation("param", "newText")))); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("IC#commitCorrection() returns true even when the target app does not" |
| + " implement it.", result.getReturnBooleanValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setSelection(int, int)} works as expected. |
| */ |
| @Test |
| public void testSetSelection() throws Exception { |
| final int expectedStart = 123; |
| final int expectedEnd = 456; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setSelection(int start, int end) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("start", start); |
| args.putInt("end", end); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSetSelection(expectedStart, expectedEnd); |
| assertTrue("setSelection() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEquals(expectedStart, args.getInt("start")); |
| assertEquals(expectedEnd, args.getInt("end")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setSelection(int, int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testSetSelectionTextAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setSelection(int start, int end) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("start", start); |
| args.putInt("end", end); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callSetSelection(123, 456); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#setSelection() still returns true even after unbindInput().", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#setSelection() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performEditorAction(int)} works as expected. |
| */ |
| @Test |
| public void testPerformEditorAction() throws Exception { |
| final int expectedEditorAction = EditorInfo.IME_ACTION_GO; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performEditorAction(int editorAction) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("editorAction", editorAction); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callPerformEditorAction(expectedEditorAction); |
| assertTrue("performEditorAction() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEquals(expectedEditorAction, args.getInt("editorAction")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performEditorAction(int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testPerformEditorActionAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performEditorAction(int editorAction) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("editorAction", editorAction); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callPerformEditorAction(EditorInfo.IME_ACTION_GO); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#performEditorAction() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#performEditorAction() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performContextMenuAction(int)} works as expected. |
| */ |
| @Test |
| public void testPerformContextMenuAction() throws Exception { |
| final int expectedId = android.R.id.selectAll; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performContextMenuAction(int id) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("id", id); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callPerformContextMenuAction(expectedId); |
| assertTrue("performContextMenuAction() always returns true unless RemoteException is " |
| + "thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| assertEquals(expectedId, args.getInt("id")); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performContextMenuAction(int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testPerformContextMenuActionAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performContextMenuAction(int id) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("id", id); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callPerformEditorAction(EditorInfo.IME_ACTION_GO); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#performContextMenuAction() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#performContextMenuAction() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#beginBatchEdit()} works as expected. |
| */ |
| @Test |
| public void testBeginBatchEdit() throws Exception { |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean beginBatchEdit() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callBeginBatchEdit(); |
| assertTrue("beginBatchEdit() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#beginBatchEdit()} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testBeginBatchEditAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean beginBatchEdit() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callBeginBatchEdit(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#beginBatchEdit() still returns true even after unbindInput().", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#beginBatchEdit() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#endBatchEdit()} works as expected. |
| */ |
| @Test |
| public void testEndBatchEdit() throws Exception { |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean endBatchEdit() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callEndBatchEdit(); |
| assertTrue("endBatchEdit() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#endBatchEdit()} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testEndBatchEditAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean endBatchEdit() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callEndBatchEdit(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#endBatchEdit() still returns true even after unbindInput().", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#endBatchEdit() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#sendKeyEvent(KeyEvent)} works as expected. |
| */ |
| @Test |
| public void testSendKeyEvent() throws Exception { |
| final KeyEvent expectedKeyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X); |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean sendKeyEvent(KeyEvent event) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("event", event); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSendKeyEvent(expectedKeyEvent); |
| assertTrue("sendKeyEvent() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| final KeyEvent actualKeyEvent = args.getParcelable("event"); |
| assertNotNull(actualKeyEvent); |
| assertEquals(expectedKeyEvent.getAction(), actualKeyEvent.getAction()); |
| assertEquals(expectedKeyEvent.getKeyCode(), actualKeyEvent.getKeyCode()); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#sendKeyEvent(KeyEvent)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testSendKeyEventAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean sendKeyEvent(KeyEvent event) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putParcelable("event", event); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callSendKeyEvent( |
| new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X)); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#sendKeyEvent() still returns true even after unbindInput().", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#sendKeyEvent() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#clearMetaKeyStates(int)} works as expected. |
| */ |
| @Test |
| public void testClearMetaKeyStates() throws Exception { |
| final int expectedStates = KeyEvent.META_ALT_MASK; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean clearMetaKeyStates(int states) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("states", states); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callClearMetaKeyStates(expectedStates); |
| assertTrue("clearMetaKeyStates() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| final int actualStates = args.getInt("states"); |
| assertEquals(expectedStates, actualStates); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#clearMetaKeyStates(int)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testClearMetaKeyStatesAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean clearMetaKeyStates(int states) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putInt("states", states); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callClearMetaKeyStates(KeyEvent.META_ALT_MASK); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#clearMetaKeyStates() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#clearMetaKeyStates() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#reportFullscreenMode(boolean)} is ignored as expected. |
| */ |
| @Test |
| public void testReportFullscreenMode() throws Exception { |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean reportFullscreenMode(boolean enabled) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putBoolean("enabled", enabled); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callReportFullscreenMode(true); |
| assertFalse("reportFullscreenMode() always returns false on API 26+", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "IC#reportFullscreenMode() must be ignored on API 26+", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#reportFullscreenMode(boolean)} is ignored as expected even after |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testReportFullscreenModeAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean reportFullscreenMode(boolean enabled) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putBoolean("enabled", enabled); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callReportFullscreenMode(true); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertFalse("reportFullscreenMode() always returns false on API 26+", |
| result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled("IC#reportFullscreenMode() must be ignored on " |
| + "API 26+ even after unbindInput().", EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performSpellCheck()} works as expected. |
| */ |
| @Test |
| public void testPerformSpellCheck() throws Exception { |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performSpellCheck() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callPerformSpellCheck(); |
| assertTrue("performSpellCheck() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performSpellCheck()} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testPerformSpellCheckAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performSpellCheck() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callPerformSpellCheck(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#performSpellCheck() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#performSpellCheck() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Verify that the default implementation of {@link InputConnection#performSpellCheck()} |
| * returns {@code true} without any crash even when the target app does not override it. |
| */ |
| @Test |
| public void testPerformSpellCheckDefaultMethod() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callPerformSpellCheck(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("IC#performSpellCheck() still returns true even when the target " |
| + "application does not implement it.", result.getReturnBooleanValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performPrivateCommand(String, Bundle)} works as expected. |
| */ |
| @Test |
| public void testPerformPrivateCommand() throws Exception { |
| final String expectedAction = "myAction"; |
| final Bundle expectedData = new Bundle(); |
| final String expectedDataKey = "testKey"; |
| final int expectedDataValue = 42; |
| expectedData.putInt(expectedDataKey, expectedDataValue); |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performPrivateCommand(String action, Bundle data) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putString("action", action); |
| args.putBundle("data", data); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = |
| session.callPerformPrivateCommand(expectedAction, expectedData); |
| assertTrue("performPrivateCommand() always returns true unless RemoteException is " |
| + "thrown", expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| final String actualAction = args.getString("action"); |
| final Bundle actualData = args.getBundle("data"); |
| assertEquals(expectedAction, actualAction); |
| assertNotNull(actualData); |
| assertEquals(expectedData.get(expectedDataKey), actualData.getInt(expectedDataKey)); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#performPrivateCommand(String, Bundle)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testPerformPrivateCommandAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean performPrivateCommand(String action, Bundle data) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putString("action", action); |
| args.putBundle("data", data); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callPerformPrivateCommand("myAction", null); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#performPrivateCommand() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#performPrivateCommand() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getHandler()} is ignored as expected. |
| */ |
| @Test |
| public void testGetHandler() throws Exception { |
| final Handler returnedResult = null; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public Handler getHandler() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // The system internally calls "getHandler". So reset the verifier before our calling |
| // "callGetHandler". |
| methodCallVerifier.reset(); |
| final ImeCommand command = session.callGetHandler(); |
| assertTrue("getHandler() always returns null", |
| expectCommand(stream, command, TIMEOUT).isNullReturnValue()); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled("IC#getHandler() must be ignored.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#getHandler()} is ignored as expected even after |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testGetHandlerAfterUnbindInput() throws Exception { |
| final Handler returnedResult = null; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public Handler getHandler() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // The system internally calls "getHandler". So reset the verifier before our calling |
| // "callGetHandler". |
| methodCallVerifier.reset(); |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callGetHandler(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("getHandler() always returns null", result.isNullReturnValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "IC#getHandler() must be ignored even after unbindInput().", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Verify that applications that do not implement {@link InputConnection#getHandler()} will not |
| * crash. This can happen if the app was built before {@link android.os.Build.VERSION_CODES#N}. |
| */ |
| @Test |
| public void testGetHandlerWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callGetHandler(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("IC#getHandler() still returns null even when the target app does not" |
| + " implement it.", result.isNullReturnValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#closeConnection()} is ignored as expected. |
| */ |
| @Test |
| public void testCloseConnection() throws Exception { |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public void closeConnection() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callCloseConnection(); |
| expectCommand(stream, command, TIMEOUT); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled("IC#getHandler() must be ignored.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#closeConnection()} is ignored as expected even after |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testCloseConnectionAfterUnbindInput() throws Exception { |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| final CountDownLatch latch = new CountDownLatch(1); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public void closeConnection() { |
| methodCallVerifier.onMethodCalled(args -> { }); |
| latch.countDown(); |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // The system internally calls "closeConnection". So wait for it to happen then reset |
| // the verifier before our calling "closeConnection". |
| assertTrue("closeConnection() must be called by the system.", |
| latch.await(TIMEOUT, TimeUnit.MILLISECONDS)); |
| methodCallVerifier.reset(); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callCloseConnection(); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "IC#closeConnection() must be ignored even after unbindInput().", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Verify that applications that do not implement {@link InputConnection#closeConnection()} |
| * will not crash. This can happen if the app was built before |
| * {@link android.os.Build.VERSION_CODES#N}. |
| */ |
| @Test |
| public void testCloseConnectionWithMethodMissing() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callCloseConnection(); |
| expectCommand(stream, command, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setImeConsumesInput(boolean)} works as expected. |
| */ |
| @Test |
| public void testSetImeConsumesInput() throws Exception { |
| final boolean expectedImeConsumesInput = true; |
| // Intentionally let the app return "false" to confirm that IME still receives "true". |
| final boolean returnedResult = false; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setImeConsumesInput(boolean imeConsumesInput) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putBoolean("imeConsumesInput", imeConsumesInput); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSetImeConsumesInput(expectedImeConsumesInput); |
| assertTrue("setImeConsumesInput() always returns true unless RemoteException is thrown", |
| expectCommand(stream, command, TIMEOUT).getReturnBooleanValue()); |
| methodCallVerifier.expectCalledOnce(args -> { |
| final boolean actualImeConsumesInput = args.getBoolean("imeConsumesInput"); |
| assertEquals(expectedImeConsumesInput, actualImeConsumesInput); |
| }, TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#setImeConsumesInput(boolean)} fails fast once |
| * {@link android.view.inputmethod.InputMethod#unbindInput()} is issued. |
| */ |
| @Test |
| public void testSetImeConsumesInputAfterUnbindInput() throws Exception { |
| final boolean returnedResult = true; |
| |
| final MethodCallVerifier methodCallVerifier = new MethodCallVerifier(); |
| |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public boolean setImeConsumesInput(boolean imeConsumesInput) { |
| methodCallVerifier.onMethodCalled(args -> { |
| args.putBoolean("imeConsumesInput", imeConsumesInput); |
| }); |
| return returnedResult; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| // Memorize the current InputConnection. |
| expectCommand(stream, session.memorizeCurrentInputConnection(), TIMEOUT); |
| |
| // Let unbindInput happen. |
| triggerUnbindInput(); |
| expectEvent(stream, event -> "unbindInput".equals(event.getEventName()), TIMEOUT); |
| |
| // Now this API call on the memorized IC should fail fast. |
| final ImeCommand command = session.callSetImeConsumesInput(true); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| // CAVEAT: this behavior is a bit questionable and may change in a future version. |
| assertTrue("Currently IC#setImeConsumesInput() still returns true even after " |
| + "unbindInput().", result.getReturnBooleanValue()); |
| expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO); |
| |
| // Make sure that the app does not receive the call (for a while). |
| methodCallVerifier.expectNotCalled( |
| "Once unbindInput() happened, IC#setImeConsumesInput() fails fast.", |
| EXPECTED_NOT_CALLED_TIMEOUT); |
| }); |
| } |
| |
| /** |
| * Verify that the default implementation of |
| * {@link InputConnection#setImeConsumesInput(boolean)} returns {@code true} without any crash |
| * even when the target app does not override it. |
| */ |
| @Test |
| public void testSetImeConsumesInputDefaultMethod() throws Exception { |
| testMinimallyImplementedInputConnection((MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callSetImeConsumesInput(true); |
| final ImeEvent result = expectCommand(stream, command, TIMEOUT); |
| assertTrue("IC#setImeConsumesInput() still returns true even when the target " |
| + "application does not implement it.", result.getReturnBooleanValue()); |
| }); |
| } |
| |
| /** |
| * Test {@link InputConnection#takeSnapshot()} is ignored as expected. |
| */ |
| @Test |
| public void testTakeSnapshot() throws Exception { |
| final TextSnapshot returnedTextSnapshot = new TextSnapshot( |
| new SurroundingText("test", 4, 4, 0), -1, -1, 0); |
| final class Wrapper extends InputConnectionWrapper { |
| private Wrapper(InputConnection target) { |
| super(target, false); |
| } |
| |
| @Override |
| public TextSnapshot takeSnapshot() { |
| return returnedTextSnapshot; |
| } |
| } |
| |
| testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> { |
| final ImeCommand command = session.callTakeSnapshot(); |
| assertTrue("takeSnapshot() always returns null", |
| expectCommand(stream, command, TIMEOUT).isNullReturnValue()); |
| }); |
| } |
| |
| } |