blob: 2ac8a33ace03aa28b613ce35f0e28a7324750a26 [file] [log] [blame]
/*
* 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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.Process;
import android.os.SystemClock;
import android.text.TextUtils;
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.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.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.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 org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
/**
* Ensures that blocking APIs in {@link InputConnection} are working as expected.
*
* <p>TODO(b/129012881): Reduce boilerplate code.</p>
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class InputConnectionBlockingMethodTest extends EndToEndImeTestBase {
private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
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.InputConnectionBlockingMethodTest";
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 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}.
*
* <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 AtomicBoolean isTestRunning = new AtomicBoolean(true);
try {
final ImeEventStream stream = imeSession.openEventStream();
final String marker = getTestMarker();
TestActivity.startSync(activity -> {
final LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
final EditText editText = new EditText(activity) {
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
final InputConnection ic = super.onCreateInputConnection(outAttrs);
// Fall back to the original InputConnection once the test is done.
return isTestRunning.get()
? inputConnectionWrapperProvider.apply(ic) : ic;
}
};
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);
} finally {
isTestRunning.set(false);
}
}
}
/**
* 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.");
}
}
/**
* 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 String expectedResult = "89";
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
assertEquals(expectedN, n);
assertEquals(expectedFlags, 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();
assertEquals(expectedResult, result);
});
}
/**
* 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 String unexpectedResult = "89";
final BlockingMethodVerifier blocker = new BlockingMethodVerifier();
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
blocker.onMethodCalled();
return unexpectedResult;
}
}
testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> {
final ImeCommand command = session.callGetTextAfterCursor(
unexpectedResult.length(), InputConnection.GET_TEXT_WITH_STYLES);
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());
}, blocker);
}
/**
* Test {@link InputConnection#getTextAfterCursor(int, int)} fail-fasts once unbindInput() is
* issued.
*/
@Test
public void testGetTextAfterCursorFailFastAfterUnbindInput() throws Exception {
final String unexpectedResult = "89";
final AtomicBoolean methodCalled = new AtomicBoolean(false);
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getTextAfterCursor(int n, int flags) {
methodCalled.set(true);
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());
assertFalse("Once unbindInput() happened, IC#getTextAfterCursor() fails fast.",
methodCalled.get());
expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO);
});
}
/**
* 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 String expectedResult = "123";
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
assertEquals(expectedN, n);
assertEquals(expectedFlags, 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();
assertEquals(expectedResult, result);
});
}
/**
* 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 String unexpectedResult = "123";
final BlockingMethodVerifier blocker = new BlockingMethodVerifier();
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
blocker.onMethodCalled();
return unexpectedResult;
}
}
testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> {
final ImeCommand command = session.callGetTextBeforeCursor(
unexpectedResult.length(), InputConnection.GET_TEXT_WITH_STYLES);
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());
}, blocker);
}
/**
* Test {@link InputConnection#getTextBeforeCursor(int, int)} fail-fasts once unbindInput() is
* issued.
*/
@Test
public void testGetTextBeforeCursorFailFastAfterUnbindInput() throws Exception {
final String unexpectedResult = "123";
final AtomicBoolean methodCalled = new AtomicBoolean(false);
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getTextBeforeCursor(int n, int flags) {
methodCalled.set(true);
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());
assertFalse("Once unbindInput() happened, IC#getTextBeforeCursor() fails fast.",
methodCalled.get());
expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO);
});
}
/**
* Test {@link InputConnection#getSelectedText(int)} works as expected.
*/
@Test
public void testGetSelectedText() throws Exception {
final int expectedFlags = InputConnection.GET_TEXT_WITH_STYLES;
final String expectedResult = "4567";
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getSelectedText(int 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();
assertEquals(expectedResult, result);
});
}
/**
* 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 String unexpectedResult = "4567";
final BlockingMethodVerifier blocker = new BlockingMethodVerifier();
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getSelectedText(int 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());
}, blocker);
}
/**
* Test {@link InputConnection#getSelectedText(int)} fail-fasts once unbindInput() is issued.
*/
@Test
public void testGetSelectedTextFailFastAfterUnbindInput() throws Exception {
final String unexpectedResult = "4567";
final AtomicBoolean methodCalled = new AtomicBoolean(false);
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public CharSequence getSelectedText(int flags) {
methodCalled.set(true);
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());
assertFalse("Once unbindInput() happened, IC#getSelectedText() fails fast.",
methodCalled.get());
expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO);
});
}
/**
* Test {@link InputConnection#getCursorCapsMode(int)} works as expected.
*/
@Test
public void testGetCursorCapsMode() throws Exception {
final int expectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
final int expectedReqMode = TextUtils.CAP_MODE_SENTENCES | TextUtils.CAP_MODE_CHARACTERS
| TextUtils.CAP_MODE_WORDS;
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public int getCursorCapsMode(int reqModes) {
assertEquals(expectedReqMode, 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);
});
}
/**
* 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 unexpectedResult = EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
final BlockingMethodVerifier blocker = new BlockingMethodVerifier();
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public int getCursorCapsMode(int reqModes) {
blocker.onMethodCalled();
return unexpectedResult;
}
}
testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> {
final ImeCommand command = session.callGetCursorCapsMode(TextUtils.CAP_MODE_WORDS);
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());
}, 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 AtomicBoolean methodCalled = new AtomicBoolean(false);
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public int getCursorCapsMode(int reqModes) {
methodCalled.set(true);
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());
assertFalse("Once unbindInput() happened, IC#getCursorCapsMode() fails fast.",
methodCalled.get());
expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO);
});
}
/**
* 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 class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
assertEquals(expectedFlags, flags);
ExtractedTextRequestTest.assertTestInstance(request);
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);
});
}
/**
* 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 ExtractedText unexpectedResult = ExtractedTextTest.createForTest();
final BlockingMethodVerifier blocker = new BlockingMethodVerifier();
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
blocker.onMethodCalled();
return unexpectedResult;
}
}
testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> {
final ImeCommand command = session.callGetExtractedText(
ExtractedTextRequestTest.createForTest(),
InputConnection.GET_EXTRACTED_TEXT_MONITOR);
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());
}, 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 AtomicBoolean methodCalled = new AtomicBoolean(false);
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
methodCalled.set(true);
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());
assertFalse("Once unbindInput() happened, IC#getExtractedText() fails fast.",
methodCalled.get());
expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO);
});
}
/**
* 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 class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public boolean requestCursorUpdates(int 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());
});
}
/**
* 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 boolean unexpectedResult = true;
final BlockingMethodVerifier blocker = new BlockingMethodVerifier();
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public boolean requestCursorUpdates(int 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());
}, blocker);
}
/**
* Test {@link InputConnection#requestCursorUpdates(int)} fail-fasts once unbindInput() is
* issued.
*/
@Test
public void testRequestCursorUpdatesFailFastAfterUnbindInput() throws Exception {
final boolean unexpectedResult = true;
final AtomicBoolean methodCalled = new AtomicBoolean(false);
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
methodCalled.set(true);
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());
assertFalse("Once unbindInput() happened, IC#requestCursorUpdates() fails fast.",
methodCalled.get());
expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO);
});
}
/**
* 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 class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags,
Bundle opts) {
assertEquals(expectedInputContentInfo.getContentUri(),
inputContentInfo.getContentUri());
assertEquals(expectedFlags, flags);
assertEquals(expectedOpt.getInt(expectedOptKey), opts.getInt(expectedOptKey));
return expectedResult;
}
}
testInputConnection(Wrapper::new, (MockImeSession session, ImeEventStream stream) -> {
final ImeCommand command =
session.callCommitContent(expectedInputContentInfo, expectedFlags, expectedOpt);
assertTrue(expectCommand(stream, command, TIMEOUT).getReturnBooleanValue());
});
}
/**
* 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 boolean unexpectedResult = true;
final BlockingMethodVerifier blocker = new BlockingMethodVerifier();
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags,
Bundle opts) {
blocker.onMethodCalled();
return unexpectedResult;
}
}
testInputConnection(Wrapper::new, (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);
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());
}, 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 AtomicBoolean methodCalled = new AtomicBoolean(false);
final class Wrapper extends InputConnectionWrapper {
private Wrapper(InputConnection target) {
super(target, false);
}
@Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags,
Bundle opts) {
methodCalled.set(true);
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());
assertFalse("Once unbindInput() happened, IC#commitContent() fails fast.",
methodCalled.get());
expectElapseTimeLessThan(result, IMMEDIATE_TIMEOUT_NANO);
});
}
}