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

Commit b4b98e34 authored by Ben Murdoch's avatar Ben Murdoch
Browse files

Hide TextView context menu items when unavailable

When a TextView context menu item is not available, don't add
it to the menu (instead of adding it and disabling it).

Bug: 345709107
Flag: com.android.text.flags.context_menu_hide_unavailable_items
Test: atest TextViewContextMenuTest

Change-Id: I80093d3af09fde46b2035b4d8fa74e34d0f31d65
parent 049214f0
Loading
Loading
Loading
Loading
+130 −56
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
import static android.widget.TextView.ACCESSIBILITY_ACTION_SMART_START_ID;

import static com.android.graphics.hwui.flags.Flags.highContrastTextSmallTextRect;
import static com.android.text.flags.Flags.contextMenuHideUnavailableItems;

import android.R;
import android.animation.ValueAnimator;
@@ -3250,6 +3251,78 @@ public class Editor {
        final int menuItemOrderShare = 9;
        final int menuItemOrderAutofill = 10;

        if (contextMenuHideUnavailableItems()) {
            if (mTextView.canUndo()) {
                menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_UNDO, menuItemOrderUndo,
                                com.android.internal.R.string.undo)
                        .setAlphabeticShortcut('z')
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
                        .setIcon(a.getDrawable(0));
            }

            if (mTextView.canRedo()) {
                menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_REDO, menuItemOrderRedo,
                                com.android.internal.R.string.redo)
                        .setAlphabeticShortcut('z', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
                        .setIcon(a.getDrawable(1));
            }

            if (mTextView.canCut()) {
                menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, menuItemOrderCut,
                                com.android.internal.R.string.cut)
                        .setAlphabeticShortcut('x')
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
                        .setIcon(a.getDrawable(2));
            }

            if (mTextView.canCopy()) {
                menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, menuItemOrderCopy,
                                com.android.internal.R.string.copy)
                        .setAlphabeticShortcut('c')
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
                        .setIcon(a.getDrawable(3));
            }

            if (mTextView.canPaste()) {
                menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, menuItemOrderPaste,
                                com.android.internal.R.string.paste)
                        .setAlphabeticShortcut('v')
                        .setIcon(a.getDrawable(4))
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
            }

            if (mTextView.canPasteAsPlainText()) {
                menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE_AS_PLAIN_TEXT,
                                menuItemOrderPasteAsPlainText,
                                com.android.internal.R.string.paste_as_plain_text)
                        .setAlphabeticShortcut('v', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
                        .setIcon(a.getDrawable(4))
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
            }

            if (mTextView.canSelectAllText()) {
                menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL,
                                menuItemOrderSelectAll, com.android.internal.R.string.selectAll)
                        .setAlphabeticShortcut('a')
                        .setIcon(a.getDrawable(5))
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
            }

            if (mTextView.canShare()) {
                menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, menuItemOrderShare,
                                com.android.internal.R.string.share)
                        .setIcon(a.getDrawable(6))
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
            }

            final String selected = mTextView.getSelectedText();
            if (mTextView.canRequestAutofill() && (selected == null || selected.isEmpty())) {
                menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, menuItemOrderAutofill,
                                android.R.string.autofill)
                        .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
            }
        } else {
            menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_UNDO, menuItemOrderUndo,
                            com.android.internal.R.string.undo)
                    .setAlphabeticShortcut('z')
@@ -3306,6 +3379,7 @@ public class Editor {
                    .setEnabled(mTextView.canRequestAutofill()
                            && (selected == null || selected.isEmpty()))
                    .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
        }
        a.recycle();
    }

+24 −8
Original line number Diff line number Diff line
@@ -15552,15 +15552,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        }
    }
    boolean canUndo() {
    /** @hide */
    @VisibleForTesting
    public boolean canUndo() {
        return mEditor != null && mEditor.canUndo();
    }
    boolean canRedo() {
    /** @hide */
    @VisibleForTesting
    public boolean canRedo() {
        return mEditor != null && mEditor.canRedo();
    }
    boolean canCut() {
    /** @hide */
    @VisibleForTesting
    public boolean canCut() {
        if (hasPasswordTransformationMethod()) {
            return false;
        }
@@ -15573,7 +15579,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        return false;
    }
    boolean canCopy() {
    /** @hide */
    @VisibleForTesting
    public boolean canCopy() {
        if (hasPasswordTransformationMethod()) {
            return false;
        }
@@ -15594,7 +15602,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                && isSuggestionsEnabled() && mEditor.shouldOfferToShowSuggestions();
    }
    boolean canShare() {
    /** @hide */
    @VisibleForTesting
    public boolean canShare() {
        if (!getContext().canStartActivityForResult() || !isDeviceProvisioned()
                || !getContext().getResources().getBoolean(
                com.android.internal.R.bool.config_textShareSupported)) {
@@ -15613,8 +15623,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        return mDeviceProvisionedState == DEVICE_PROVISIONED_YES;
    }
    /** @hide */
    @VisibleForTesting
    @UnsupportedAppUsage
    boolean canPaste() {
    public boolean canPaste() {
        return (mText instanceof Editable
                && mEditor != null && mEditor.mKeyListener != null
                && getSelectionStart() >= 0
@@ -15622,7 +15634,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                && getClipboardManagerForUser().hasPrimaryClip());
    }
    boolean canPasteAsPlainText() {
    /** @hide */
    @VisibleForTesting
    public boolean canPasteAsPlainText() {
        if (!canPaste()) {
            return false;
        }
@@ -15644,7 +15658,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        return canShare();
    }
    boolean canSelectAllText() {
    /** @hide */
    @VisibleForTesting
    public boolean canSelectAllText() {
        return canSelectText() && !hasPasswordTransformationMethod()
                && !(getSelectionStart() == 0 && getSelectionEnd() == mText.length());
    }
+156 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -40,6 +41,9 @@ import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.Icon;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.textclassifier.TextClassification;
@@ -47,9 +51,12 @@ import android.view.textclassifier.TextClassification;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.verification.VerificationMode;

/**
 * TextViewTest tests {@link TextView}.
@@ -86,6 +93,10 @@ public class TextViewContextMenuTest {

    private SelectionActionModeHelper mMockHelper;

    @ClassRule public static final SetFlagsRule.ClassRule SET_FLAGS_CLASS_RULE =
            new SetFlagsRule.ClassRule();
    @Rule public final SetFlagsRule mSetFlagsRule = SET_FLAGS_CLASS_RULE.createSetFlagsRule();

    @Before
    public void setUp() {
        mMockHelper = mock(SelectionActionModeHelper.class);
@@ -234,6 +245,7 @@ public class TextViewContextMenuTest {
    }

    @Test
    @DisableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testAutofillMenuItemEnabledWhenNoTextSelected() {
        ContextMenu menu = mock(ContextMenu.class);
        MenuItem mockMenuItem = newMockMenuItem();
@@ -254,6 +266,7 @@ public class TextViewContextMenuTest {
    }

    @Test
    @DisableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testAutofillMenuItemNotEnabledWhenTextSelected() {
        ContextMenu menu = mock(ContextMenu.class);
        MenuItem mockMenuItem = newMockMenuItem();
@@ -271,4 +284,147 @@ public class TextViewContextMenuTest {
        verify(menu).add(anyInt(), eq(TextView.ID_AUTOFILL), anyInt(), anyInt());
        verify(mockAutofillMenuItem).setEnabled(false);
    }

    private interface EditTextSetup {
        void run(EditText et);
    }

    private void verifyMenuItemNotAdded(EditTextSetup setup, int id, VerificationMode times) {
        ContextMenu menu = mock(ContextMenu.class);
        MenuItem mockMenuItem = newMockMenuItem();
        when(menu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mockMenuItem);
        EditText et = spy(new EditText(getInstrumentation().getContext()));
        setup.run(et);
        Editor editor = new Editor(et);
        editor.setTextContextMenuItems(menu);
        verify(menu, times).add(anyInt(), eq(id), anyInt(), anyInt());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuUndoNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canUndo(),
                TextView.ID_UNDO, never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuUndoAddedWhenAvailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canUndo(), TextView.ID_UNDO,
                times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuRedoNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canRedo(), TextView.ID_REDO,
                never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuRedoAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canRedo(), TextView.ID_REDO,
                times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuCutNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canCut(), TextView.ID_CUT,
                never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuCutAddedWhenAvailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canCut(), TextView.ID_CUT,
                times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuCopyNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canCopy(), TextView.ID_COPY,
                never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuCopyAddedWhenAvailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canCopy(), TextView.ID_COPY,
                times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuPasteNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canPaste(), TextView.ID_PASTE,
                never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuPasteAddedWhenAvailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canPaste(), TextView.ID_PASTE,
                times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuPasteAsPlaintextNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canPasteAsPlainText(),
                        TextView.ID_PASTE_AS_PLAIN_TEXT, never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuPasteAsPlaintextAddedWhenAvailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canPasteAsPlainText(),
                        TextView.ID_PASTE_AS_PLAIN_TEXT, times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuSelectAllNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canSelectAllText(),
                        TextView.ID_SELECT_ALL, never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuSelectAllAddedWhenAvailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canSelectAllText(),
                        TextView.ID_SELECT_ALL, times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuShareNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canShare(), TextView.ID_SHARE,
                never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuShareAddedWhenAvailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(true).when(spy).canShare(), TextView.ID_SHARE,
                times(1));
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuAutofillNotAddedWhenUnavailable() {
        verifyMenuItemNotAdded((spy) -> doReturn(false).when(spy).canRequestAutofill(),
                TextView.ID_AUTOFILL, never());
    }

    @Test
    @EnableFlags(com.android.text.flags.Flags.FLAG_CONTEXT_MENU_HIDE_UNAVAILABLE_ITEMS)
    public void testContextMenuAutofillNotAddedWhenUnavailableBecauseTextSelected() {
        verifyMenuItemNotAdded((spy) -> {
            doReturn(true).when(spy).canRequestAutofill();
            doReturn("test").when(spy).getSelectedText();
        }, TextView.ID_AUTOFILL, never());
    }
}