Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ea6cb121 authored by Abodunrinwa Toki's avatar Abodunrinwa Toki
Browse files

Add "Paste as plain text" in TextView's toolbar.

Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest
Bug: 36179795
Change-Id: Iee0502678adcfb9de58c107b9247a528718b2c40
parent 90074d1e
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -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;
+16 −7
Original line number Diff line number Diff line
@@ -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
+21 −0
Original line number Diff line number Diff line
@@ -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;
+58 −1
Original line number Diff line number Diff line
@@ -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 ActivityInstrumentationTestCase2<TextV
                    }

                    @Override
                    public void onDestroyActionMode(ActionMode actionMode) {}
                    public void onDestroyActionMode(ActionMode actionMode) {
                    }
                }));
        final String text = "droid@android.com";

@@ -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
    }
}
+27 −15
Original line number Diff line number Diff line
@@ -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,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();
@@ -116,7 +116,6 @@ public final class TextViewAssertions {
            } 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}.
     */