Loading api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -60576,6 +60576,7 @@ package android.widget { field public static final int SOURCE_DRAG_AND_DROP = 2; // 0x2 field public static final int SOURCE_INPUT_METHOD = 1; // 0x1 field public static final int SOURCE_MENU = 0; // 0x0 field public static final int SOURCE_PROCESS_TEXT = 4; // 0x4 } public class ScrollView extends android.widget.FrameLayout { core/java/android/widget/RichContentReceiver.java +7 −1 Original line number Diff line number Diff line Loading @@ -75,7 +75,7 @@ public interface RichContentReceiver<T extends View> { * @hide */ @IntDef(prefix = {"SOURCE_"}, value = {SOURCE_MENU, SOURCE_INPUT_METHOD, SOURCE_DRAG_AND_DROP, SOURCE_AUTOFILL}) SOURCE_AUTOFILL, SOURCE_PROCESS_TEXT}) @Retention(RetentionPolicy.SOURCE) @interface Source {} Loading Loading @@ -104,6 +104,12 @@ public interface RichContentReceiver<T extends View> { */ int SOURCE_AUTOFILL = 3; /** * Specifies that the operation was triggered by a result from a * {@link android.content.Intent#ACTION_PROCESS_TEXT PROCESS_TEXT} action in the selection menu. */ int SOURCE_PROCESS_TEXT = 4; /** * Flags to configure the insertion behavior. * Loading core/java/android/widget/TextView.java +3 −5 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_C import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX; import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; import static android.widget.RichContentReceiver.SOURCE_PROCESS_TEXT; import android.R; import android.annotation.CallSuper; Loading Loading @@ -2127,7 +2128,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT); if (result != null) { if (isTextEditable()) { replaceSelectionWithText(result); ClipData clip = ClipData.newPlainText("", result); mRichContentReceiver.onReceive(this, clip, SOURCE_PROCESS_TEXT, 0); if (mEditor != null) { mEditor.refreshTextActionMode(); } Loading Loading @@ -12834,10 +12836,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return length > 0; } void replaceSelectionWithText(CharSequence text) { ((Editable) mText).replace(getSelectionStart(), getSelectionEnd(), text); } private void paste(boolean withFormatting) { ClipboardManager clipboard = getClipboardManagerForUser(); ClipData clip = clipboard.getPrimaryClip(); Loading core/java/android/widget/TextViewRichContentReceiver.java +16 −11 Original line number Diff line number Diff line Loading @@ -49,7 +49,7 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> @Override public boolean onReceive(@NonNull TextView textView, @NonNull ClipData clip, @Source int source, int flags) { @Source int source, @Flags int flags) { if (source == SOURCE_AUTOFILL) { return onReceiveForAutofill(textView, clip, flags); } Loading Loading @@ -78,12 +78,7 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> } if (itemText != null) { if (!didFirst) { final int selStart = Selection.getSelectionStart(editable); final int selEnd = Selection.getSelectionEnd(editable); final int start = Math.max(0, Math.min(selStart, selEnd)); final int end = Math.max(0, Math.max(selStart, selEnd)); Selection.setSelection(editable, end); editable.replace(start, end, itemText); replaceSelection(editable, itemText); didFirst = true; } else { editable.insert(Selection.getSelectionEnd(editable), "\n"); Loading @@ -94,8 +89,18 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> return didFirst; } private static void replaceSelection(@NonNull Editable editable, @NonNull CharSequence replacement) { final int selStart = Selection.getSelectionStart(editable); final int selEnd = Selection.getSelectionEnd(editable); final int start = Math.max(0, Math.min(selStart, selEnd)); final int end = Math.max(0, Math.max(selStart, selEnd)); Selection.setSelection(editable, end); editable.replace(start, end, replacement); } private static boolean onReceiveForAutofill(@NonNull TextView textView, @NonNull ClipData clip, int flags) { @Flags int flags) { final CharSequence text = coerceToText(clip, textView.getContext(), flags); if (text.length() == 0) { return false; Loading @@ -109,16 +114,16 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> } private static boolean onReceiveForDragAndDrop(@NonNull TextView textView, @NonNull ClipData clip, int flags) { @NonNull ClipData clip, @Flags int flags) { final CharSequence text = coerceToText(clip, textView.getContext(), flags); if (text.length() == 0) { return false; } textView.replaceSelectionWithText(text); replaceSelection((Editable) textView.getText(), text); return true; } private static CharSequence coerceToText(ClipData clip, Context context, int flags) { private static CharSequence coerceToText(ClipData clip, Context context, @Flags int flags) { SpannableStringBuilder ssb = new SpannableStringBuilder(); for (int i = 0; i < clip.getItemCount(); i++) { CharSequence itemText; Loading core/tests/coretests/src/android/widget/TextViewProcessTextTest.java 0 → 100644 +229 −0 Original line number Diff line number Diff line /* * 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; import static android.widget.RichContentReceiver.SOURCE_PROCESS_TEXT; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import android.app.Activity; import android.app.Instrumentation; import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; import android.text.Selection; import android.text.Spannable; import android.widget.TextView.BufferType; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; 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.Set; /** * Tests for {@link Intent#ACTION_PROCESS_TEXT} functionality in {@link TextView}. */ @RunWith(AndroidJUnit4.class) @MediumTest public class TextViewProcessTextTest { @Rule public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>( TextViewActivity.class); private Instrumentation mInstrumentation; private Activity mActivity; private TextView mTextView; @Before public void before() { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mActivity = mActivityRule.getActivity(); } @Test public void testProcessTextActivityResultNonEditable() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new TextView(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection((Spannable) mTextView.getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); // We need to run this in the UI thread, as it will create a Toast. mActivityRule.runOnUiThread(() -> { triggerOnActivityResult(Activity.RESULT_OK, "Text is replaced."); }); mInstrumentation.waitForIdleSync(); // This is a TextView, which can't be modified. Hence no change should have been made. assertEquals(originalText, mTextView.getText().toString()); verifyZeroInteractions(mockReceiverWrapper.mMock); } @Test public void testProcessTextActivityResultEditable_defaultRichContentReceiver() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); CharSequence newText = "Text is replaced."; triggerOnActivityResult(Activity.RESULT_OK, newText); assertEquals(newText, mTextView.getText().toString()); } @Test public void testProcessTextActivityResultEditable_customRichContentReceiver() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); CharSequence newText = "Text is replaced."; triggerOnActivityResult(Activity.RESULT_OK, newText); ClipData expectedClip = ClipData.newPlainText("", newText); verify(mockReceiverWrapper.mMock, times(1)).onReceive( eq(mTextView), clipEq(expectedClip), eq(SOURCE_PROCESS_TEXT), eq(0)); verifyNoMoreInteractions(mockReceiverWrapper.mMock); } @Test public void testProcessTextActivityResultCancel() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); CharSequence newText = "Text is replaced."; triggerOnActivityResult(Activity.RESULT_CANCELED, newText); assertEquals(originalText, mTextView.getText().toString()); verifyZeroInteractions(mockReceiverWrapper.mMock); } @Test public void testProcessTextActivityNoData() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, null); assertEquals(originalText, mTextView.getText().toString()); verifyZeroInteractions(mockReceiverWrapper.mMock); } private void triggerOnActivityResult(int resultCode, CharSequence replacementText) { Intent data = new Intent(); data.putExtra(Intent.EXTRA_PROCESS_TEXT, replacementText); mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, resultCode, data); } // This wrapper is used so that we only mock and verify the public callback methods. In addition // to the public methods, the RichContentReceiver interface has some hidden default methods; // we don't want to mock or assert calls to these helper functions (they are an implementation // detail). private static class MockReceiverWrapper implements RichContentReceiver<TextView> { private final RichContentReceiver<TextView> mMock; @SuppressWarnings("unchecked") MockReceiverWrapper() { this.mMock = Mockito.mock(RichContentReceiver.class); } public RichContentReceiver<TextView> getMock() { return mMock; } @Override public boolean onReceive(TextView view, ClipData clip, @Source int source, @Flags int flags) { return mMock.onReceive(view, clip, source, flags); } @Override public Set<String> getSupportedMimeTypes() { return mMock.getSupportedMimeTypes(); } } private static ClipData clipEq(ClipData expected) { return argThat(new ClipDataArgumentMatcher(expected)); } private static class ClipDataArgumentMatcher implements ArgumentMatcher<ClipData> { private final ClipData mExpected; private ClipDataArgumentMatcher(ClipData expected) { this.mExpected = expected; } @Override public boolean matches(ClipData actual) { ClipDescription actualDesc = actual.getDescription(); ClipDescription expectedDesc = mExpected.getDescription(); return expectedDesc.getLabel().equals(actualDesc.getLabel()) && actualDesc.getMimeTypeCount() == 1 && expectedDesc.getMimeType(0).equals(actualDesc.getMimeType(0)) && actual.getItemCount() == 1 && mExpected.getItemAt(0).getText().equals(actual.getItemAt(0).getText()); } } } Loading
api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -60576,6 +60576,7 @@ package android.widget { field public static final int SOURCE_DRAG_AND_DROP = 2; // 0x2 field public static final int SOURCE_INPUT_METHOD = 1; // 0x1 field public static final int SOURCE_MENU = 0; // 0x0 field public static final int SOURCE_PROCESS_TEXT = 4; // 0x4 } public class ScrollView extends android.widget.FrameLayout {
core/java/android/widget/RichContentReceiver.java +7 −1 Original line number Diff line number Diff line Loading @@ -75,7 +75,7 @@ public interface RichContentReceiver<T extends View> { * @hide */ @IntDef(prefix = {"SOURCE_"}, value = {SOURCE_MENU, SOURCE_INPUT_METHOD, SOURCE_DRAG_AND_DROP, SOURCE_AUTOFILL}) SOURCE_AUTOFILL, SOURCE_PROCESS_TEXT}) @Retention(RetentionPolicy.SOURCE) @interface Source {} Loading Loading @@ -104,6 +104,12 @@ public interface RichContentReceiver<T extends View> { */ int SOURCE_AUTOFILL = 3; /** * Specifies that the operation was triggered by a result from a * {@link android.content.Intent#ACTION_PROCESS_TEXT PROCESS_TEXT} action in the selection menu. */ int SOURCE_PROCESS_TEXT = 4; /** * Flags to configure the insertion behavior. * Loading
core/java/android/widget/TextView.java +3 −5 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_C import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX; import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; import static android.widget.RichContentReceiver.SOURCE_PROCESS_TEXT; import android.R; import android.annotation.CallSuper; Loading Loading @@ -2127,7 +2128,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT); if (result != null) { if (isTextEditable()) { replaceSelectionWithText(result); ClipData clip = ClipData.newPlainText("", result); mRichContentReceiver.onReceive(this, clip, SOURCE_PROCESS_TEXT, 0); if (mEditor != null) { mEditor.refreshTextActionMode(); } Loading Loading @@ -12834,10 +12836,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return length > 0; } void replaceSelectionWithText(CharSequence text) { ((Editable) mText).replace(getSelectionStart(), getSelectionEnd(), text); } private void paste(boolean withFormatting) { ClipboardManager clipboard = getClipboardManagerForUser(); ClipData clip = clipboard.getPrimaryClip(); Loading
core/java/android/widget/TextViewRichContentReceiver.java +16 −11 Original line number Diff line number Diff line Loading @@ -49,7 +49,7 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> @Override public boolean onReceive(@NonNull TextView textView, @NonNull ClipData clip, @Source int source, int flags) { @Source int source, @Flags int flags) { if (source == SOURCE_AUTOFILL) { return onReceiveForAutofill(textView, clip, flags); } Loading Loading @@ -78,12 +78,7 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> } if (itemText != null) { if (!didFirst) { final int selStart = Selection.getSelectionStart(editable); final int selEnd = Selection.getSelectionEnd(editable); final int start = Math.max(0, Math.min(selStart, selEnd)); final int end = Math.max(0, Math.max(selStart, selEnd)); Selection.setSelection(editable, end); editable.replace(start, end, itemText); replaceSelection(editable, itemText); didFirst = true; } else { editable.insert(Selection.getSelectionEnd(editable), "\n"); Loading @@ -94,8 +89,18 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> return didFirst; } private static void replaceSelection(@NonNull Editable editable, @NonNull CharSequence replacement) { final int selStart = Selection.getSelectionStart(editable); final int selEnd = Selection.getSelectionEnd(editable); final int start = Math.max(0, Math.min(selStart, selEnd)); final int end = Math.max(0, Math.max(selStart, selEnd)); Selection.setSelection(editable, end); editable.replace(start, end, replacement); } private static boolean onReceiveForAutofill(@NonNull TextView textView, @NonNull ClipData clip, int flags) { @Flags int flags) { final CharSequence text = coerceToText(clip, textView.getContext(), flags); if (text.length() == 0) { return false; Loading @@ -109,16 +114,16 @@ final class TextViewRichContentReceiver implements RichContentReceiver<TextView> } private static boolean onReceiveForDragAndDrop(@NonNull TextView textView, @NonNull ClipData clip, int flags) { @NonNull ClipData clip, @Flags int flags) { final CharSequence text = coerceToText(clip, textView.getContext(), flags); if (text.length() == 0) { return false; } textView.replaceSelectionWithText(text); replaceSelection((Editable) textView.getText(), text); return true; } private static CharSequence coerceToText(ClipData clip, Context context, int flags) { private static CharSequence coerceToText(ClipData clip, Context context, @Flags int flags) { SpannableStringBuilder ssb = new SpannableStringBuilder(); for (int i = 0; i < clip.getItemCount(); i++) { CharSequence itemText; Loading
core/tests/coretests/src/android/widget/TextViewProcessTextTest.java 0 → 100644 +229 −0 Original line number Diff line number Diff line /* * 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; import static android.widget.RichContentReceiver.SOURCE_PROCESS_TEXT; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import android.app.Activity; import android.app.Instrumentation; import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; import android.text.Selection; import android.text.Spannable; import android.widget.TextView.BufferType; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; 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.Set; /** * Tests for {@link Intent#ACTION_PROCESS_TEXT} functionality in {@link TextView}. */ @RunWith(AndroidJUnit4.class) @MediumTest public class TextViewProcessTextTest { @Rule public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>( TextViewActivity.class); private Instrumentation mInstrumentation; private Activity mActivity; private TextView mTextView; @Before public void before() { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mActivity = mActivityRule.getActivity(); } @Test public void testProcessTextActivityResultNonEditable() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new TextView(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection((Spannable) mTextView.getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); // We need to run this in the UI thread, as it will create a Toast. mActivityRule.runOnUiThread(() -> { triggerOnActivityResult(Activity.RESULT_OK, "Text is replaced."); }); mInstrumentation.waitForIdleSync(); // This is a TextView, which can't be modified. Hence no change should have been made. assertEquals(originalText, mTextView.getText().toString()); verifyZeroInteractions(mockReceiverWrapper.mMock); } @Test public void testProcessTextActivityResultEditable_defaultRichContentReceiver() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); CharSequence newText = "Text is replaced."; triggerOnActivityResult(Activity.RESULT_OK, newText); assertEquals(newText, mTextView.getText().toString()); } @Test public void testProcessTextActivityResultEditable_customRichContentReceiver() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); CharSequence newText = "Text is replaced."; triggerOnActivityResult(Activity.RESULT_OK, newText); ClipData expectedClip = ClipData.newPlainText("", newText); verify(mockReceiverWrapper.mMock, times(1)).onReceive( eq(mTextView), clipEq(expectedClip), eq(SOURCE_PROCESS_TEXT), eq(0)); verifyNoMoreInteractions(mockReceiverWrapper.mMock); } @Test public void testProcessTextActivityResultCancel() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); CharSequence newText = "Text is replaced."; triggerOnActivityResult(Activity.RESULT_CANCELED, newText); assertEquals(originalText, mTextView.getText().toString()); verifyZeroInteractions(mockReceiverWrapper.mMock); } @Test public void testProcessTextActivityNoData() throws Throwable { mActivityRule.runOnUiThread(() -> mTextView = new EditText(mActivity)); mInstrumentation.waitForIdleSync(); CharSequence originalText = "This is some text."; mTextView.setText(originalText, BufferType.SPANNABLE); assertEquals(originalText, mTextView.getText().toString()); mTextView.setTextIsSelectable(true); Selection.setSelection(((EditText) mTextView).getText(), 0, mTextView.getText().length()); MockReceiverWrapper mockReceiverWrapper = new MockReceiverWrapper(); mTextView.setRichContentReceiver(mockReceiverWrapper); mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, Activity.RESULT_OK, null); assertEquals(originalText, mTextView.getText().toString()); verifyZeroInteractions(mockReceiverWrapper.mMock); } private void triggerOnActivityResult(int resultCode, CharSequence replacementText) { Intent data = new Intent(); data.putExtra(Intent.EXTRA_PROCESS_TEXT, replacementText); mTextView.onActivityResult(TextView.PROCESS_TEXT_REQUEST_CODE, resultCode, data); } // This wrapper is used so that we only mock and verify the public callback methods. In addition // to the public methods, the RichContentReceiver interface has some hidden default methods; // we don't want to mock or assert calls to these helper functions (they are an implementation // detail). private static class MockReceiverWrapper implements RichContentReceiver<TextView> { private final RichContentReceiver<TextView> mMock; @SuppressWarnings("unchecked") MockReceiverWrapper() { this.mMock = Mockito.mock(RichContentReceiver.class); } public RichContentReceiver<TextView> getMock() { return mMock; } @Override public boolean onReceive(TextView view, ClipData clip, @Source int source, @Flags int flags) { return mMock.onReceive(view, clip, source, flags); } @Override public Set<String> getSupportedMimeTypes() { return mMock.getSupportedMimeTypes(); } } private static ClipData clipEq(ClipData expected) { return argThat(new ClipDataArgumentMatcher(expected)); } private static class ClipDataArgumentMatcher implements ArgumentMatcher<ClipData> { private final ClipData mExpected; private ClipDataArgumentMatcher(ClipData expected) { this.mExpected = expected; } @Override public boolean matches(ClipData actual) { ClipDescription actualDesc = actual.getDescription(); ClipDescription expectedDesc = mExpected.getDescription(); return expectedDesc.getLabel().equals(actualDesc.getLabel()) && actualDesc.getMimeTypeCount() == 1 && expectedDesc.getMimeType(0).equals(actualDesc.getMimeType(0)) && actual.getItemCount() == 1 && mExpected.getItemAt(0).getText().equals(actual.getItemAt(0).getText()); } } }