From ea6cb1215e97232bf96da02e35b6b8e938572eaa Mon Sep 17 00:00:00 2001 From: Abodunrinwa Toki Date: Fri, 28 Apr 2017 22:14:13 +0100 Subject: [PATCH] Add "Paste as plain text" in TextView's toolbar. Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest Bug: 36179795 Change-Id: Iee0502678adcfb9de58c107b9247a528718b2c40 --- core/java/android/text/TextUtils.java | 18 ++++++ core/java/android/widget/Editor.java | 23 +++++--- core/java/android/widget/TextView.java | 21 +++++++ .../android/widget/TextViewActivityTest.java | 59 ++++++++++++++++++- .../widget/espresso/TextViewAssertions.java | 42 ++++++++----- 5 files changed, 140 insertions(+), 23 deletions(-) diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index ee2b38e4f390a..081deebaed774 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -40,6 +40,7 @@ import android.text.style.ForegroundColorSpan; 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.TtsSpan; 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 @@ public class TextUtils { 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 481c160369b30..18554be5c91cc 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -154,10 +154,10 @@ public class Editor { 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 @@ public class Editor { .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 @@ public class Editor { mode.setSubtitle(null); mode.setTitleOptionalHint(true); populateMenuWithItems(menu); - updateAssistMenuItem(menu); Callback customCallback = getCustomCallback(); if (customCallback != null) { @@ -3843,8 +3842,18 @@ public class Editor { .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 629216e32fa95..242dcf535d327 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -35,6 +35,7 @@ import android.annotation.XmlRes; 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .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 2203b6abb8fd0..ebab129a70e62 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.longPressAndDragOnText; 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 android.support.test.espresso.matcher.ViewMatchers.withText; 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 android.view.KeyEvent; import com.android.frameworks.coretests.R; +import junit.framework.AssertionFailedError; + /** * Tests the TextView widget from an Activity */ @@ -708,7 +718,8 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2styledtext"); + 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 6e44cd83b0488..2532731511677 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.Rect; 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 @@ public final class TextViewAssertions { * @param index A matcher representing the expected index. */ public static ViewAssertion hasInsertionPointerAtIndex(final Matcher 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"); } }; } @@ -136,6 +135,19 @@ public final class TextViewAssertions { return new CursorPositionAssertion(CursorPositionAssertion.RIGHT); } + /** + * 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}. */