diff options
| -rw-r--r-- | core/java/android/widget/Editor.java | 186 | ||||
| -rw-r--r-- | core/java/android/widget/TextView.java | 32 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/widget/TextViewContextMenuTest.java | 156 |
3 files changed, 310 insertions, 64 deletions
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 03a26722da8f..0acc6bde5bfd 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -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,62 +3251,135 @@ public class Editor { final int menuItemOrderShare = 9; final int menuItemOrderAutofill = 10; - 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)) - .setEnabled(mTextView.canUndo()); - 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)) - .setEnabled(mTextView.canRedo()); - - menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, menuItemOrderCut, - com.android.internal.R.string.cut) - .setAlphabeticShortcut('x') - .setOnMenuItemClickListener(mOnContextMenuItemClickListener) - .setIcon(a.getDrawable(2)) - .setEnabled(mTextView.canCut()); - menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, menuItemOrderCopy, - com.android.internal.R.string.copy) - .setAlphabeticShortcut('c') - .setOnMenuItemClickListener(mOnContextMenuItemClickListener) - .setIcon(a.getDrawable(3)) - .setEnabled(mTextView.canCopy()); - menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, menuItemOrderPaste, - com.android.internal.R.string.paste) - .setAlphabeticShortcut('v') - .setEnabled(mTextView.canPaste()) - .setIcon(a.getDrawable(4)) - .setOnMenuItemClickListener(mOnContextMenuItemClickListener); - 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) - .setEnabled(mTextView.canPasteAsPlainText()) - .setIcon(a.getDrawable(4)) - .setOnMenuItemClickListener(mOnContextMenuItemClickListener); - menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL, - menuItemOrderSelectAll, com.android.internal.R.string.selectAll) - .setAlphabeticShortcut('a') - .setEnabled(mTextView.canSelectAllText()) - .setIcon(a.getDrawable(5)) - .setOnMenuItemClickListener(mOnContextMenuItemClickListener); - - menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, menuItemOrderShare, - com.android.internal.R.string.share) - .setEnabled(mTextView.canShare()) - .setIcon(a.getDrawable(6)) - .setOnMenuItemClickListener(mOnContextMenuItemClickListener); - final String selected = mTextView.getSelectedText(); - menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, menuItemOrderAutofill, - android.R.string.autofill) - .setEnabled(mTextView.canRequestAutofill() - && (selected == null || selected.isEmpty())) - .setOnMenuItemClickListener(mOnContextMenuItemClickListener); + 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') + .setOnMenuItemClickListener(mOnContextMenuItemClickListener) + .setIcon(a.getDrawable(0)) + .setEnabled(mTextView.canUndo()); + 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)) + .setEnabled(mTextView.canRedo()); + + menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, menuItemOrderCut, + com.android.internal.R.string.cut) + .setAlphabeticShortcut('x') + .setOnMenuItemClickListener(mOnContextMenuItemClickListener) + .setIcon(a.getDrawable(2)) + .setEnabled(mTextView.canCut()); + menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, menuItemOrderCopy, + com.android.internal.R.string.copy) + .setAlphabeticShortcut('c') + .setOnMenuItemClickListener(mOnContextMenuItemClickListener) + .setIcon(a.getDrawable(3)) + .setEnabled(mTextView.canCopy()); + menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, menuItemOrderPaste, + com.android.internal.R.string.paste) + .setAlphabeticShortcut('v') + .setEnabled(mTextView.canPaste()) + .setIcon(a.getDrawable(4)) + .setOnMenuItemClickListener(mOnContextMenuItemClickListener); + 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) + .setEnabled(mTextView.canPasteAsPlainText()) + .setIcon(a.getDrawable(4)) + .setOnMenuItemClickListener(mOnContextMenuItemClickListener); + menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL, + menuItemOrderSelectAll, com.android.internal.R.string.selectAll) + .setAlphabeticShortcut('a') + .setEnabled(mTextView.canSelectAllText()) + .setIcon(a.getDrawable(5)) + .setOnMenuItemClickListener(mOnContextMenuItemClickListener); + + menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, menuItemOrderShare, + com.android.internal.R.string.share) + .setEnabled(mTextView.canShare()) + .setIcon(a.getDrawable(6)) + .setOnMenuItemClickListener(mOnContextMenuItemClickListener); + final String selected = mTextView.getSelectedText(); + menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, menuItemOrderAutofill, + android.R.string.autofill) + .setEnabled(mTextView.canRequestAutofill() + && (selected == null || selected.isEmpty())) + .setOnMenuItemClickListener(mOnContextMenuItemClickListener); + } a.recycle(); } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 1ea20fa85bd4..a346a679ea00 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -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()); } diff --git a/core/tests/coretests/src/android/widget/TextViewContextMenuTest.java b/core/tests/coretests/src/android/widget/TextViewContextMenuTest.java index bcf1053e8ddd..3e76977c99fa 100644 --- a/core/tests/coretests/src/android/widget/TextViewContextMenuTest.java +++ b/core/tests/coretests/src/android/widget/TextViewContextMenuTest.java @@ -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()); + } } |