Add "Paste as plain text" in TextView's toolbar.
Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest
Bug: 36179795
Change-Id: Iee0502678adcfb9de58c107b9247a528718b2c40
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index ee2b38e..081deeb 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -40,6 +40,7 @@
import android.text.style.LeadingMarginSpan;
import android.text.style.LocaleSpan;
import android.text.style.MetricAffectingSpan;
+import android.text.style.ParagraphStyle;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.ReplacementSpan;
@@ -56,6 +57,7 @@
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
+import android.text.style.UpdateAppearance;
import android.util.Log;
import android.util.Printer;
import android.view.View;
@@ -1903,6 +1905,22 @@
return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count);
}
+ /**
+ * Returns whether or not the specified spanned text has a style span.
+ * @hide
+ */
+ public static boolean hasStyleSpan(@NonNull Spanned spanned) {
+ Preconditions.checkArgument(spanned != null);
+ final Class<?>[] styleClasses = {
+ CharacterStyle.class, ParagraphStyle.class, UpdateAppearance.class};
+ for (Class<?> clazz : styleClasses) {
+ if (spanned.nextSpanTransition(-1, spanned.length(), clazz) < spanned.length()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private static Object sLock = new Object();
private static char[] sTemp = null;
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 481c160..18554be 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -154,10 +154,10 @@
private static final int MENU_ITEM_ORDER_COPY = 5;
private static final int MENU_ITEM_ORDER_PASTE = 6;
private static final int MENU_ITEM_ORDER_SHARE = 7;
- private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 8;
- private static final int MENU_ITEM_ORDER_SELECT_ALL = 9;
- private static final int MENU_ITEM_ORDER_REPLACE = 10;
- private static final int MENU_ITEM_ORDER_AUTOFILL = 11;
+ private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
+ private static final int MENU_ITEM_ORDER_REPLACE = 9;
+ private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
+ private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
// Each Editor manages its own undo stack.
@@ -2634,9 +2634,9 @@
.setAlphabeticShortcut('v')
.setEnabled(mTextView.canPaste())
.setOnMenuItemClickListener(mOnContextMenuItemClickListener);
- menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+ menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
com.android.internal.R.string.paste_as_plain_text)
- .setEnabled(mTextView.canPaste())
+ .setEnabled(mTextView.canPasteAsPlainText())
.setOnMenuItemClickListener(mOnContextMenuItemClickListener);
menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
com.android.internal.R.string.share)
@@ -3775,7 +3775,6 @@
mode.setSubtitle(null);
mode.setTitleOptionalHint(true);
populateMenuWithItems(menu);
- updateAssistMenuItem(menu);
Callback customCallback = getCustomCallback();
if (customCallback != null) {
@@ -3843,8 +3842,18 @@
.setShowAsAction(mode);
}
+ if (mTextView.canPasteAsPlainText()) {
+ menu.add(
+ Menu.NONE,
+ TextView.ID_PASTE_AS_PLAIN_TEXT,
+ MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+ com.android.internal.R.string.paste_as_plain_text)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+
updateSelectAllItem(menu);
updateReplaceItem(menu);
+ updateAssistMenuItem(menu);
}
@Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 629216e..242dcf5 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -35,6 +35,7 @@
import android.app.Activity;
import android.app.assist.AssistStructure;
import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
@@ -11042,6 +11043,26 @@
.hasPrimaryClip());
}
+ boolean canPasteAsPlainText() {
+ if (!canPaste()) {
+ return false;
+ }
+
+ final ClipData clipData =
+ ((ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE))
+ .getPrimaryClip();
+ final ClipDescription description = clipData.getDescription();
+ final boolean isPlainType = description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
+ final CharSequence text = clipData.getItemAt(0).getText();
+ if (isPlainType && (text instanceof Spanned)) {
+ Spanned spanned = (Spanned) text;
+ if (TextUtils.hasStyleSpan(spanned)) {
+ return true;
+ }
+ }
+ return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
+ }
+
boolean canProcessText() {
if (getId() == View.NO_ID) {
return false;
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index 2203b6a..ebab129 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -28,6 +28,7 @@
import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex;
import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
import static android.widget.espresso.TextViewAssertions.hasSelection;
+import static android.widget.espresso.TextViewAssertions.doesNotHaveStyledText;
import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarItemIndex;
import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed;
import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsNotDisplayed;
@@ -47,9 +48,16 @@
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.is;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.text.TextUtils;
+import android.text.Spanned;
+import android.support.test.espresso.NoMatchingViewException;
+import android.support.test.espresso.ViewAssertion;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.View;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.widget.espresso.CustomViewActions.RelativeCoordinatesProvider;
@@ -64,6 +72,8 @@
import com.android.frameworks.coretests.R;
+import junit.framework.AssertionFailedError;
+
/**
* Tests the TextView widget from an Activity
*/
@@ -708,7 +718,8 @@
}
@Override
- public void onDestroyActionMode(ActionMode actionMode) {}
+ public void onDestroyActionMode(ActionMode actionMode) {
+ }
}));
final String text = "droid@android.com";
@@ -717,4 +728,50 @@
sleepForFloatingToolbarPopup();
assertFloatingToolbarItemIndex(android.R.id.textAssist, 0);
}
+
+ public void testPastePlainText_menuAction() throws Exception {
+ initializeClipboardWithText(TextStyle.STYLED);
+
+ onView(withId(R.id.textview)).perform(replaceText(""));
+ onView(withId(R.id.textview)).perform(longClick());
+ sleepForFloatingToolbarPopup();
+ clickFloatingToolbarItem(
+ getActivity().getString(com.android.internal.R.string.paste_as_plain_text));
+ getInstrumentation().waitForIdleSync();
+
+ onView(withId(R.id.textview)).check(matches(withText("styledtext")));
+ onView(withId(R.id.textview)).check(doesNotHaveStyledText());
+ }
+
+ public void testPastePlainText_noMenuItemForPlainText() {
+ initializeClipboardWithText(TextStyle.PLAIN);
+
+ onView(withId(R.id.textview)).perform(replaceText(""));
+ onView(withId(R.id.textview)).perform(longClick());
+ sleepForFloatingToolbarPopup();
+
+ assertFloatingToolbarDoesNotContainItem(
+ getActivity().getString(com.android.internal.R.string.paste_as_plain_text));
+ }
+
+ private void initializeClipboardWithText(TextStyle textStyle) {
+ final ClipData clip;
+ switch (textStyle) {
+ case STYLED:
+ clip = ClipData.newHtmlText("html", "styledtext", "<b>styledtext</b>");
+ break;
+ case PLAIN:
+ clip = ClipData.newPlainText("plain", "plaintext");
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid text style");
+ }
+ getActivity().getWindow().getDecorView().post(() ->
+ getActivity().getSystemService(ClipboardManager.class).setPrimaryClip( clip));
+ getInstrumentation().waitForIdleSync();
+ }
+
+ private enum TextStyle {
+ PLAIN, STYLED
+ }
}
diff --git a/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java b/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java
index 6e44cd8..2532731 100644
--- a/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java
+++ b/core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java
@@ -26,6 +26,8 @@
import android.graphics.drawable.Drawable;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewAssertion;
+import android.text.Spanned;
+import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
@@ -100,22 +102,19 @@
* @param index A matcher representing the expected index.
*/
public static ViewAssertion hasInsertionPointerAtIndex(final Matcher<Integer> index) {
- return new ViewAssertion() {
- @Override
- public void check(View view, NoMatchingViewException exception) {
- if (view instanceof TextView) {
- TextView textView = (TextView) view;
- int selectionStart = textView.getSelectionStart();
- int selectionEnd = textView.getSelectionEnd();
- try {
- assertThat(selectionStart, index);
- assertThat(selectionEnd, index);
- } catch (IndexOutOfBoundsException e) {
- throw new AssertionFailedError(e.getMessage());
- }
- } else {
- throw new AssertionFailedError("TextView not found");
+ return (view, exception) -> {
+ if (view instanceof TextView) {
+ TextView textView = (TextView) view;
+ int selectionStart = textView.getSelectionStart();
+ int selectionEnd = textView.getSelectionEnd();
+ try {
+ assertThat(selectionStart, index);
+ assertThat(selectionEnd, index);
+ } catch (IndexOutOfBoundsException e) {
+ throw new AssertionFailedError(e.getMessage());
}
+ } else {
+ throw new AssertionFailedError("TextView not found");
}
};
}
@@ -137,6 +136,19 @@
}
/**
+ * Returns a {@link ViewAssertion} that asserts that the TextView does not contain styled text.
+ */
+ public static ViewAssertion doesNotHaveStyledText() {
+ return (view, exception) -> {
+ final CharSequence text = ((TextView) view).getText();
+ if (text instanceof Spanned && !TextUtils.hasStyleSpan((Spanned) text)) {
+ return;
+ }
+ throw new AssertionFailedError("TextView has styled text");
+ };
+ }
+
+ /**
* A {@link ViewAssertion} to check the selected text in a {@link TextView}.
*/
private static final class TextSelectionAssertion implements ViewAssertion {