Loading core/java/android/text/TextUtils.java +18 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading core/java/android/widget/Editor.java +16 −7 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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) Loading Loading @@ -3775,7 +3775,6 @@ public class Editor { mode.setSubtitle(null); mode.setTitleOptionalHint(true); populateMenuWithItems(menu); updateAssistMenuItem(menu); Callback customCallback = getCustomCallback(); if (customCallback != null) { Loading Loading @@ -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 Loading core/java/android/widget/TextView.java +21 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading core/tests/coretests/src/android/widget/TextViewActivityTest.java +58 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 */ Loading Loading @@ -708,7 +718,8 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV } @Override public void onDestroyActionMode(ActionMode actionMode) {} public void onDestroyActionMode(ActionMode actionMode) { } })); final String text = "droid@android.com"; Loading @@ -717,4 +728,50 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV 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 } } core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java +27 −15 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -100,9 +102,7 @@ public final class TextViewAssertions { * @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) { return (view, exception) -> { if (view instanceof TextView) { TextView textView = (TextView) view; int selectionStart = textView.getSelectionStart(); Loading @@ -116,7 +116,6 @@ public final class TextViewAssertions { } else { throw new AssertionFailedError("TextView not found"); } } }; } Loading @@ -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}. */ Loading Loading
core/java/android/text/TextUtils.java +18 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading
core/java/android/widget/Editor.java +16 −7 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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) Loading Loading @@ -3775,7 +3775,6 @@ public class Editor { mode.setSubtitle(null); mode.setTitleOptionalHint(true); populateMenuWithItems(menu); updateAssistMenuItem(menu); Callback customCallback = getCustomCallback(); if (customCallback != null) { Loading Loading @@ -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 Loading
core/java/android/widget/TextView.java +21 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading
core/tests/coretests/src/android/widget/TextViewActivityTest.java +58 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 */ Loading Loading @@ -708,7 +718,8 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV } @Override public void onDestroyActionMode(ActionMode actionMode) {} public void onDestroyActionMode(ActionMode actionMode) { } })); final String text = "droid@android.com"; Loading @@ -717,4 +728,50 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV 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 } }
core/tests/coretests/src/android/widget/espresso/TextViewAssertions.java +27 −15 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -100,9 +102,7 @@ public final class TextViewAssertions { * @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) { return (view, exception) -> { if (view instanceof TextView) { TextView textView = (TextView) view; int selectionStart = textView.getSelectionStart(); Loading @@ -116,7 +116,6 @@ public final class TextViewAssertions { } else { throw new AssertionFailedError("TextView not found"); } } }; } Loading @@ -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}. */ Loading