blob: 9aceba38841ce69e492d5b7d29bd4d363d8d03a5 [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.widget.cts;
import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
import static android.view.ContentInfo.SOURCE_AUTOFILL;
import static android.view.ContentInfo.SOURCE_CLIPBOARD;
import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
import static android.view.ContentInfo.SOURCE_INPUT_METHOD;
import static android.view.ContentInfo.SOURCE_PROCESS_TEXT;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.QwertyKeyListener;
import android.text.method.TextKeyListener.Capitalize;
import android.text.style.UnderlineSpan;
import android.view.ContentInfo;
import android.view.DragEvent;
import android.view.OnReceiveContentListener;
import android.view.View.MeasureSpec;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
import android.widget.TextView;
import android.widget.TextView.BufferType;
import android.widget.TextViewOnReceiveContentListener;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.WindowUtil;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import java.util.Objects;
/**
* Tests for {@link TextView#performReceiveContent} and related code.
*/
@MediumTest
@RunWith(AndroidJUnit4.class)
public class TextViewReceiveContentTest {
public static final Uri SAMPLE_CONTENT_URI = Uri.parse("content://com.example/path");
@Rule
public ActivityTestRule<TextViewCtsActivity> mActivityRule =
new ActivityTestRule<>(TextViewCtsActivity.class);
private Activity mActivity;
private TextView mTextView;
private OnReceiveContentListener mDefaultReceiver;
private OnReceiveContentListener mMockReceiver;
private ClipboardManager mClipboardManager;
@Before
public void before() {
mActivity = mActivityRule.getActivity();
WindowUtil.waitForFocus(mActivity);
mTextView = mActivity.findViewById(R.id.textview_text);
mDefaultReceiver = new TextViewOnReceiveContentListener();
mMockReceiver = Mockito.mock(OnReceiveContentListener.class);
when(mMockReceiver.onReceiveContent(any(), any())).thenReturn(null);
mClipboardManager = mActivity.getSystemService(ClipboardManager.class);
mClipboardManager.clearPrimaryClip();
configureAppTargetSdkToS();
}
@After
public void after() {
resetTargetSdk();
}
// ============================================================================================
// Tests to verify TextView APIs/accessors/defaults related to OnReceiveContentListener.
// ============================================================================================
@UiThreadTest
@Test
public void testTextView_onCreateInputConnection_nullEditorInfo() throws Exception {
initTextViewForEditing("xz", 1);
try {
mTextView.onCreateInputConnection(null);
Assert.fail("Expected exception");
} catch (NullPointerException expected) {
}
}
@UiThreadTest
@Test
public void testTextView_onCreateInputConnection_noCustomReceiver() throws Exception {
initTextViewForEditing("xz", 1);
// Call onCreateInputConnection() and assert that contentMimeTypes is not set when there is
// no custom receiver configured.
EditorInfo editorInfo = new EditorInfo();
InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
assertThat(ic).isNotNull();
assertThat(editorInfo.contentMimeTypes).isNull();
}
@UiThreadTest
@Test
public void testTextView_onCreateInputConnection_customReceiver() throws Exception {
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/plain", "image/png", "video/mp4"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Call onCreateInputConnection() and assert that contentMimeTypes is set from the receiver.
EditorInfo editorInfo = new EditorInfo();
InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
assertThat(ic).isNotNull();
assertThat(editorInfo.contentMimeTypes).isEqualTo(receiverMimeTypes);
}
@UiThreadTest
@Test
public void testTextView_onCreateInputConnection_customReceiver_oldTargetSdk()
throws Exception {
configureAppTargetSdkToR();
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/plain", "image/png", "video/mp4"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Call onCreateInputConnection() and assert that contentMimeTypes is set from the receiver.
EditorInfo editorInfo = new EditorInfo();
InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
assertThat(ic).isNotNull();
assertThat(editorInfo.contentMimeTypes).isEqualTo(receiverMimeTypes);
}
// ============================================================================================
// Tests to verify the behavior of TextViewOnReceiveContentListener.
// ============================================================================================
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_text() throws Exception {
initTextViewForEditing("xz", 1);
ClipData clip = ClipData.newPlainText("test", "y");
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
assertTextAndCursorPosition("xyz", 2);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_styledText() throws Exception {
initTextViewForEditing("xz", 1);
UnderlineSpan underlineSpan = new UnderlineSpan();
SpannableStringBuilder ssb = new SpannableStringBuilder("hi world");
ssb.setSpan(underlineSpan, 3, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ClipData clip = ClipData.newPlainText("test", ssb);
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
assertTextAndCursorPosition("xhi worldz", 9);
int spanStart = mTextView.getEditableText().getSpanStart(underlineSpan);
assertThat(spanStart).isEqualTo(4);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_text_convertToPlainText() throws Exception {
initTextViewForEditing("xz", 1);
ClipData clip = ClipData.newPlainText("test", "y");
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT);
assertTextAndCursorPosition("xyz", 2);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_styledText_convertToPlainText() throws Exception {
initTextViewForEditing("xz", 1);
UnderlineSpan underlineSpan = new UnderlineSpan();
SpannableStringBuilder ssb = new SpannableStringBuilder("hi world");
ssb.setSpan(underlineSpan, 3, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ClipData clip = ClipData.newPlainText("test", ssb);
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT);
assertTextAndCursorPosition("xhi worldz", 9);
int spanStart = mTextView.getEditableText().getSpanStart(underlineSpan);
assertThat(spanStart).isEqualTo(-1);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_html() throws Exception {
initTextViewForEditing("xz", 1);
ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
assertTextAndCursorPosition("xyz", 2);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_html_convertToPlainText() throws Exception {
initTextViewForEditing("xz", 1);
ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT);
assertTextAndCursorPosition("x*y*z", 4);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_unsupportedMimeType() throws Exception {
initTextViewForEditing("xz", 1);
ClipData clip = new ClipData("test", new String[]{"video/mp4"},
new ClipData.Item("text", "html", null, SAMPLE_CONTENT_URI));
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
assertTextAndCursorPosition("xhtmlz", 5);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_unsupportedMimeType_convertToPlainText()
throws Exception {
initTextViewForEditing("xz", 1);
ClipData clip = new ClipData("test", new String[]{"video/mp4"},
new ClipData.Item("text", "html", null, SAMPLE_CONTENT_URI));
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD,
FLAG_CONVERT_TO_PLAIN_TEXT);
assertTextAndCursorPosition("xtextz", 5);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_multipleItemsInClipData() throws Exception {
initTextViewForEditing("xz", 1);
ClipData clip = ClipData.newPlainText("test", "ONE");
clip.addItem(new ClipData.Item("TWO"));
clip.addItem(new ClipData.Item("THREE"));
// Verify the resulting text when pasting a clip that contains multiple text items.
String expectedText = "xONE\nTWO\nTHREEz";
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
assertTextAndCursorPosition(expectedText, 14);
// Verify the resulting text when inserting the same clip via drag-and-drop. The result
// should be the same as when pasting.
initTextViewForEditing("xz", 1);
onReceive(mDefaultReceiver, clip, SOURCE_DRAG_AND_DROP, 0);
assertTextAndCursorPosition(expectedText, 14);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_noSelectionPriorToPaste() throws Exception {
// Set the text and then clear the selection (ie, ensure that nothing is selected and
// that the cursor is not present).
initTextViewForEditing("xz", 0);
Selection.removeSelection(mTextView.getEditableText());
assertTextAndCursorPosition("xz", -1);
// Pasting should still work (should just insert the text at the beginning).
ClipData clip = ClipData.newPlainText("test", "y");
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
assertTextAndCursorPosition("yxz", 1);
}
@UiThreadTest
@Test
public void testDefaultReceiver_onReceive_selectionStartAndEndSwapped() throws Exception {
initTextViewForEditing("", 0);
// Set the selection such that "end" is before "start".
mTextView.setText("hey", BufferType.EDITABLE);
Selection.setSelection(mTextView.getEditableText(), 3, 1);
assertTextAndSelection("hey", 3, 1);
// Pasting should still work (should still successfully overwrite the selection).
ClipData clip = ClipData.newPlainText("test", "i");
onReceive(mDefaultReceiver, clip, SOURCE_CLIPBOARD, 0);
assertTextAndCursorPosition("hi", 2);
}
// ============================================================================================
// Tests to verify that the OnReceiveContentListener is invoked for all the appropriate user
// interactions:
// * Paste from clipboard ("Paste" and "Paste as plain text" actions)
// * Content insertion from IME
// * Drag and drop
// * Autofill
// * Process text (Intent.ACTION_PROCESS_TEXT)
// ============================================================================================
@UiThreadTest
@Test
public void testPaste_noCustomReceiver() throws Exception {
// Setup: Populate the text field.
initTextViewForEditing("xz", 1);
// Setup: Copy text to the clipboard.
ClipData clip = ClipData.newPlainText("test", "y");
copyToClipboard(clip);
// Trigger the "Paste" action. This should execute the default receiver.
boolean result = triggerContextMenuAction(android.R.id.paste);
assertThat(result).isTrue();
assertTextAndCursorPosition("xyz", 2);
}
@UiThreadTest
@Test
public void testPaste_customReceiver() throws Exception {
// Setup: Populate the text field.
initTextViewForEditing("xz", 1);
// Setup: Copy text to the clipboard.
ClipData clip = ClipData.newPlainText("test", "y");
copyToClipboard(clip);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/plain"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the "Paste" action and assert that the custom receiver was executed.
triggerContextMenuAction(android.R.id.paste);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_CLIPBOARD, 0));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testPaste_customReceiver_unsupportedMimeType() throws Exception {
// Setup: Populate the text field.
initTextViewForEditing("xz", 1);
// Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
ClipData clip = new ClipData("test", new String[]{"video/mp4"},
new ClipData.Item("y", null, SAMPLE_CONTENT_URI));
copyToClipboard(clip);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/plain", "video/avi"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the "Paste" action and assert that the custom receiver was executed.
triggerContextMenuAction(android.R.id.paste);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_CLIPBOARD, 0));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testPasteAsPlainText_noCustomReceiver() throws Exception {
// Setup: Populate the text field.
initTextViewForEditing("xz", 1);
// Setup: Copy HTML to the clipboard.
ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
copyToClipboard(clip);
// Trigger the "Paste as plain text" action. This should execute the platform paste
// handling, so the content should be inserted according to whatever behavior is implemented
// in the OS version that's running.
boolean result = triggerContextMenuAction(android.R.id.pasteAsPlainText);
assertThat(result).isTrue();
assertTextAndCursorPosition("x*y*z", 4);
}
@UiThreadTest
@Test
public void testPasteAsPlainText_customReceiver() throws Exception {
// Setup: Populate the text field.
initTextViewForEditing("xz", 1);
// Setup: Copy text to the clipboard.
ClipData clip = ClipData.newPlainText("test", "y");
copyToClipboard(clip);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/plain"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the "Paste as plain text" action and assert that the custom receiver was
// executed.
triggerContextMenuAction(android.R.id.pasteAsPlainText);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView),
contentEq(clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testImeCommitContent_noCustomReceiver() throws Exception {
initTextViewForEditing("xz", 1);
// Trigger the IME's commitContent() call and assert its outcome.
boolean result = triggerImeCommitContent("image/png");
assertThat(result).isFalse();
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testImeCommitContent_customReceiver() throws Exception {
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the IME's commitContent() call and assert that the custom receiver was executed.
triggerImeCommitContent("image/png");
ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_INPUT_METHOD, 0));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testImeCommitContent_customReceiver_unsupportedMimeType() throws Exception {
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the IME's commitContent() call and assert that the custom receiver was executed.
triggerImeCommitContent("video/mp4");
ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_INPUT_METHOD, 0));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testImeCommitContent_customReceiver_oldTargetSdk() throws Exception {
configureAppTargetSdkToR();
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the IME's commitContent() call and assert that the custom receiver was executed.
triggerImeCommitContent("image/png");
ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_INPUT_METHOD, 0));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testImeCommitContent_linkUri() throws Exception {
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the IME's commitContent() call with a linkUri and assert receiver extras.
Uri sampleLinkUri = Uri.parse("http://example.com");
triggerImeCommitContent("image/png", sampleLinkUri, null);
ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView),
contentEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, null));
}
@UiThreadTest
@Test
public void testImeCommitContent_opts() throws Exception {
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the IME's commitContent() call with opts and assert receiver extras.
String sampleOptValue = "sampleOptValue";
triggerImeCommitContent("image/png", null, sampleOptValue);
ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView),
contentEq(clip, SOURCE_INPUT_METHOD, 0, null, sampleOptValue));
}
@UiThreadTest
@Test
public void testImeCommitContent_linkUriAndOpts() throws Exception {
initTextViewForEditing("xz", 1);
// Setup: Configure the receiver to a mock impl.
String[] receiverMimeTypes = new String[] {"text/*", "image/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger the IME's commitContent() call with a linkUri & opts and assert receiver extras.
Uri sampleLinkUri = Uri.parse("http://example.com");
String sampleOptValue = "sampleOptValue";
triggerImeCommitContent("image/png", sampleLinkUri, sampleOptValue);
ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView),
contentEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, sampleOptValue));
}
@UiThreadTest
@Test
public void testDragAndDrop_noCustomReceiver() throws Exception {
initTextViewForEditing("xz", 2);
// Trigger drop event. This should execute the default receiver.
ClipData clip = ClipData.newPlainText("test", "y");
triggerDropEvent(clip);
assertTextAndCursorPosition("yxz", 1);
}
@UiThreadTest
@Test
public void testDragAndDrop_customReceiver() throws Exception {
initTextViewForEditing("xz", 2);
String[] receiverMimeTypes = new String[] {"text/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger drop event and assert that the custom receiver was executed.
ClipData clip = ClipData.newPlainText("test", "y");
triggerDropEvent(clip);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
verifyNoMoreInteractions(mMockReceiver);
// Note: The cursor is moved to the location of the drop before calling the receiver.
assertTextAndCursorPosition("xz", 0);
}
@UiThreadTest
@Test
public void testDragAndDrop_customReceiver_nonEditableTextView() throws Exception {
// Initialize the view and assert preconditions.
mTextView.setText("Hello");
assertTextAndSelection("Hello", -1, -1);
assertThat(mTextView.isTextSelectable()).isFalse();
assertThat(mTextView.getText()).isNotInstanceOf(Editable.class);
// Configure the listener.
String[] receiverMimeTypes = new String[] {"text/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger drop event and assert that the custom receiver was executed.
ClipData clip = ClipData.newPlainText("test", "y");
triggerDropEvent(clip);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
verifyNoMoreInteractions(mMockReceiver);
// Note: The cursor/selection should not change since the view is not editable.
assertTextAndSelection("Hello", -1, -1);
}
/**
* This test checks the edge case where a {@link TextView} starts as non-editable and becomes
* editable during dragging. The test simulates this scenario by setting up an editable
* {@link TextView}, clearing its focus and then injecting an
* {@link DragEvent#ACTION_DRAG_LOCATION} event without a prior
* {@link DragEvent#ACTION_DRAG_STARTED} or {@link DragEvent#ACTION_DRAG_ENTERED} event.
*/
@UiThreadTest
@Test
public void testDragAndDrop_nonEditableTextViewChangedToEditable_actionDragLocation()
throws Exception {
// Setup an editable TextView and assert that its insertion controller is enabled.
initTextViewForEditing("Test drag and drop", 4);
assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
// Focus on another view and assert that the TextView we are going to test doesn't have
// focus (but still has its insertion controller enabled).
TextView anotherTextView = mActivity.findViewById(R.id.textview_singleLine);
anotherTextView.setTextIsSelectable(true);
anotherTextView.requestFocus();
assertThat(mTextView.hasFocus()).isFalse();
assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
// Trigger an ACTION_DRAG_LOCATION event without any prior drag events. The TextView should
// still gracefully handle the event and update its cursor position for the event's
// location.
DragEvent dragEvent = createDragEvent(DragEvent.ACTION_DRAG_LOCATION, mTextView.getX(),
mTextView.getY(), null);
assertThat(mTextView.onDragEvent(dragEvent)).isTrue();
assertTextAndCursorPosition("Test drag and drop", 0);
}
/**
* This test checks the edge case where a {@link TextView} starts as non-editable and becomes
* editable during dragging. The test simulates this scenario by setting up an editable
* {@link TextView}, clearing its focus and then injecting an
* {@link DragEvent#ACTION_DROP} event without a prior
* {@link DragEvent#ACTION_DRAG_STARTED} or {@link DragEvent#ACTION_DRAG_ENTERED} or
* {@link DragEvent#ACTION_DRAG_LOCATION} event.
*/
@UiThreadTest
@Test
public void testDragAndDrop_nonEditableTextViewChangedToEditable_actionDrop() throws Exception {
// Setup an editable TextView and assert that its insertion controller is enabled.
initTextViewForEditing("Test drag and drop", 4);
assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
// Focus on another view and assert that the TextView we are going to test doesn't have
// focus (but still has its insertion controller enabled).
TextView anotherTextView = mActivity.findViewById(R.id.textview_singleLine);
anotherTextView.setTextIsSelectable(true);
anotherTextView.requestFocus();
assertThat(mTextView.hasFocus()).isFalse();
assertThat(mTextView.getEditorForTesting().getInsertionController()).isNotNull();
// Trigger an ACTION_DROP event without any prior drag events. The TextView should still
// gracefully handle the event and accept the drop.
ClipData clip = ClipData.newPlainText("test", "Hi ");
DragEvent dragEvent = createDragEvent(DragEvent.ACTION_DROP, mTextView.getX(),
mTextView.getY(), clip);
assertThat(mTextView.onDragEvent(dragEvent)).isTrue();
assertTextAndCursorPosition("Hi Test drag and drop", 3);
}
@UiThreadTest
@Test
public void testDragAndDrop_customReceiver_unsupportedMimeType() throws Exception {
initTextViewForEditing("xz", 2);
String[] receiverMimeTypes = new String[] {"text/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger drop event and assert that the custom receiver was executed.
ClipData clip = new ClipData("test", new String[]{"video/mp4"},
new ClipData.Item("y", null, SAMPLE_CONTENT_URI));
triggerDropEvent(clip);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_DRAG_AND_DROP, 0));
verifyNoMoreInteractions(mMockReceiver);
// Note: The cursor is moved to the location of the drop before calling the receiver.
assertTextAndCursorPosition("xz", 0);
}
@UiThreadTest
@Test
public void testAutofill_noCustomReceiver() throws Exception {
initTextViewForEditing("xz", 1);
// Trigger autofill. This should execute the default receiver.
triggerAutofill("y");
assertTextAndCursorPosition("y", 1);
}
@UiThreadTest
@Test
public void testAutofill_customReceiver() throws Exception {
initTextViewForEditing("xz", 1);
String[] receiverMimeTypes = new String[] {"text/*"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
// Trigger autofill and assert that the custom receiver was executed.
triggerAutofill("y");
ClipData clip = ClipData.newPlainText("", "y");
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_AUTOFILL, 0));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndCursorPosition("xz", 1);
}
@UiThreadTest
@Test
public void testProcessText_noCustomReceiver() throws Exception {
initTextViewForEditing("Original text", 0);
Selection.setSelection(mTextView.getEditableText(), 0, mTextView.getText().length());
String newText = "Replacement text";
triggerProcessTextOnActivityResult(newText);
assertTextAndCursorPosition(newText, newText.length());
}
@UiThreadTest
@Test
public void testProcessText_customReceiver() throws Exception {
String originalText = "Original text";
initTextViewForEditing(originalText, 0);
Selection.setSelection(mTextView.getEditableText(), 0, originalText.length());
assertTextAndSelection(originalText, 0, originalText.length());
String[] receiverMimeTypes = new String[] {"text/plain"};
mTextView.setOnReceiveContentListener(receiverMimeTypes, mMockReceiver);
String newText = "Replacement text";
triggerProcessTextOnActivityResult(newText);
ClipData clip = ClipData.newPlainText("", newText);
verify(mMockReceiver, times(1)).onReceiveContent(
eq(mTextView), contentEq(clip, SOURCE_PROCESS_TEXT, 0));
verifyNoMoreInteractions(mMockReceiver);
assertTextAndSelection(originalText, 0, originalText.length());
}
private void initTextViewForEditing(final String text, final int cursorPosition) {
mTextView.setKeyListener(QwertyKeyListener.getInstance(false, Capitalize.NONE));
mTextView.setTextIsSelectable(true);
mTextView.requestFocus();
SpannableStringBuilder ssb = new SpannableStringBuilder(text);
mTextView.setText(ssb, BufferType.EDITABLE);
mTextView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
Selection.setSelection(mTextView.getEditableText(), cursorPosition);
assertWithMessage("TextView should have focus").that(mTextView.hasFocus()).isTrue();
assertTextAndCursorPosition(text, cursorPosition);
}
private void assertTextAndCursorPosition(String expectedText, int cursorPosition) {
assertTextAndSelection(expectedText, cursorPosition, cursorPosition);
}
private void assertTextAndSelection(String expectedText, int start, int end) {
assertThat(mTextView.getText().toString()).isEqualTo(expectedText);
int[] expected = new int[]{start, end};
int[] actual = new int[]{mTextView.getSelectionStart(), mTextView.getSelectionEnd()};
assertWithMessage("Unexpected selection start/end indexes")
.that(actual).isEqualTo(expected);
}
private void onReceive(final OnReceiveContentListener receiver,
final ClipData clip, final int source, final int flags) {
ContentInfo payload =
new ContentInfo.Builder(clip, source)
.setFlags(flags)
.build();
receiver.onReceiveContent(mTextView, payload);
}
private void resetTargetSdk() {
mActivity.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
}
private void configureAppTargetSdkToR() {
mActivity.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.R;
}
private void configureAppTargetSdkToS() {
mActivity.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.S;
}
private void copyToClipboard(ClipData clip) {
mClipboardManager.setPrimaryClip(clip);
}
private boolean triggerContextMenuAction(final int actionId) {
return mTextView.onTextContextMenuItem(actionId);
}
private boolean triggerImeCommitContent(String mimeType) {
return triggerImeCommitContent(mimeType, null, null);
}
private boolean triggerImeCommitContent(String mimeType, Uri linkUri, String extra) {
final InputContentInfo contentInfo = new InputContentInfo(
SAMPLE_CONTENT_URI,
new ClipDescription("from test", new String[]{mimeType}),
linkUri);
final Bundle opts;
if (extra == null) {
opts = null;
} else {
opts = new Bundle();
opts.putString(ContentInfoArgumentMatcher.EXTRA_KEY, extra);
}
EditorInfo editorInfo = new EditorInfo();
InputConnection ic = mTextView.onCreateInputConnection(editorInfo);
return ic.commitContent(contentInfo, 0, opts);
}
private void triggerAutofill(CharSequence text) {
mTextView.autofill(AutofillValue.forText(text));
}
private boolean triggerDropEvent(ClipData clip) {
DragEvent dropEvent = createDragEvent(DragEvent.ACTION_DROP, mTextView.getX(),
mTextView.getY(), clip);
return mTextView.onDragEvent(dropEvent);
}
private static DragEvent createDragEvent(int action, float x, float y, ClipData clip) {
DragEvent dragEvent = mock(DragEvent.class);
when(dragEvent.getAction()).thenReturn(action);
when(dragEvent.getX()).thenReturn(x);
when(dragEvent.getY()).thenReturn(y);
when(dragEvent.getClipData()).thenReturn(clip);
return dragEvent;
}
private void triggerProcessTextOnActivityResult(CharSequence replacementText) {
Intent data = new Intent();
data.putExtra(Intent.EXTRA_PROCESS_TEXT, replacementText);
mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, data);
}
private static ContentInfo contentEq(@NonNull ClipData clip, int source, int flags) {
return argThat(new ContentInfoArgumentMatcher(clip, source, flags, null, null));
}
private static ContentInfo contentEq(@NonNull ClipData clip, int source, int flags,
Uri linkUri, String extra) {
return argThat(new ContentInfoArgumentMatcher(clip, source, flags, linkUri, extra));
}
private static class ContentInfoArgumentMatcher implements ArgumentMatcher<ContentInfo> {
public static final String EXTRA_KEY = "testExtra";
@NonNull private final ClipData mClip;
private final int mSource;
private final int mFlags;
@Nullable private final Uri mLinkUri;
@Nullable private final String mExtra;
private ContentInfoArgumentMatcher(@NonNull ClipData clip, int source, int flags,
@Nullable Uri linkUri, @Nullable String extra) {
mClip = clip;
mSource = source;
mFlags = flags;
mLinkUri = linkUri;
mExtra = extra;
}
@Override
public boolean matches(ContentInfo actual) {
ClipData.Item expectedItem = mClip.getItemAt(0);
ClipData.Item actualItem = actual.getClip().getItemAt(0);
return Objects.equals(expectedItem.getText(), actualItem.getText())
&& Objects.equals(expectedItem.getUri(), actualItem.getUri())
&& mSource == actual.getSource()
&& mFlags == actual.getFlags()
&& Objects.equals(mLinkUri, actual.getLinkUri())
&& extrasMatch(actual.getExtras());
}
private boolean extrasMatch(Bundle actualExtras) {
if (mExtra == null) {
return actualExtras == null;
}
String actualExtraValue = actualExtras.getString(EXTRA_KEY);
return Objects.equals(mExtra, actualExtraValue);
}
}
}