TextView: Open context menu on right click.

- Construct context menu in #onCreateContextMenu.
- Ignore non-primary buttons for selection.
- Don't open context menu on long click.

Bug: 19544351
Bug: 19397276
Bug: 24872170

Change-Id: I4690e1485682be4f9b504b8fb6af936b979f13b8
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 2d1f855..15cea77 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -72,6 +72,7 @@
 import android.util.SparseArray;
 import android.view.ActionMode;
 import android.view.ActionMode.Callback;
+import android.view.ContextMenu;
 import android.view.DisplayListCanvas;
 import android.view.DragEvent;
 import android.view.Gravity;
@@ -135,13 +136,16 @@
     // Tag used when the Editor maintains its own separate UndoManager.
     private static final String UNDO_OWNER_TAG = "Editor";
 
-    // Ordering constants used to place the Action Mode items in their menu.
-    private static final int MENU_ITEM_ORDER_CUT = 1;
-    private static final int MENU_ITEM_ORDER_COPY = 2;
-    private static final int MENU_ITEM_ORDER_PASTE = 3;
-    private static final int MENU_ITEM_ORDER_SHARE = 4;
-    private static final int MENU_ITEM_ORDER_SELECT_ALL = 5;
-    private static final int MENU_ITEM_ORDER_REPLACE = 6;
+    // Ordering constants used to place the Action Mode or context menu items in their menu.
+    private static final int MENU_ITEM_ORDER_UNDO = 1;
+    private static final int MENU_ITEM_ORDER_REDO = 2;
+    private static final int MENU_ITEM_ORDER_CUT = 3;
+    private static final int MENU_ITEM_ORDER_COPY = 4;
+    private static final int MENU_ITEM_ORDER_PASTE = 5;
+    private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 6;
+    private static final int MENU_ITEM_ORDER_SHARE = 7;
+    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_PROCESS_TEXT_INTENT_ACTIONS_START = 10;
 
     // Each Editor manages its own undo stack.
@@ -184,6 +188,7 @@
 
     boolean mDiscardNextActionUp;
     boolean mIgnoreActionUpEvent;
+    private boolean mIgnoreNextMouseActionUpOrDown;
 
     long mShowCursor;
     Blink mBlink;
@@ -209,6 +214,8 @@
     boolean mPreserveDetachedSelection;
     boolean mTemporaryDetach;
 
+    boolean mIsBeingLongClicked;
+
     SuggestionsPopupWindow mSuggestionsPopupWindow;
     SuggestionRangeSpan mSuggestionRangeSpan;
     Runnable mShowSuggestionRunnable;
@@ -224,6 +231,7 @@
     private PositionListener mPositionListener;
 
     float mLastDownPositionX, mLastDownPositionY;
+    private float mContextMenuAnchorX, mContextMenuAnchorY;
     Callback mCustomSelectionActionModeCallback;
     Callback mCustomInsertionActionModeCallback;
 
@@ -239,6 +247,9 @@
     // Only for mouse input.
     private static final int TAP_STATE_TRIPLE_CLICK = 3;
 
+    // The button state as of the last time #onTouchEvent is called.
+    private int mLastButtonState;
+
     private Runnable mInsertionActionModeRunnable;
 
     // The span controller helps monitoring the changes to which the Editor needs to react:
@@ -1314,7 +1325,33 @@
         }
     }
 
+    private boolean shouldFilterOutTouchEvent(MotionEvent event) {
+        if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+            return false;
+        }
+        final boolean primaryButtonStateChanged =
+                ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
+        final int action = event.getActionMasked();
+        if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
+                && !primaryButtonStateChanged) {
+            return true;
+        }
+        if (action == MotionEvent.ACTION_MOVE
+                && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
+            return true;
+        }
+        return false;
+    }
+
     void onTouchEvent(MotionEvent event) {
+        final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
+        mLastButtonState = event.getButtonState();
+        if (filterOutEvent) {
+            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                mDiscardNextActionUp = true;
+            }
+            return;
+        }
         updateTapState(event);
         updateFloatingToolbarVisibility(event);
 
@@ -2318,6 +2355,84 @@
         text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
     }
 
+    void setContextMenuAnchor(float x, float y) {
+        mContextMenuAnchorX = x;
+        mContextMenuAnchorY = y;
+    }
+
+    void onCreateContextMenu(ContextMenu menu) {
+        if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
+                || Float.isNaN(mContextMenuAnchorY)) {
+            return;
+        }
+        final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
+        if (offset == -1) {
+            return;
+        }
+        final boolean isOnSelection = mTextView.hasSelection()
+                && offset >= mTextView.getSelectionStart() && offset <= mTextView.getSelectionEnd();
+        if (!isOnSelection) {
+            // Right clicked position is not on the selection. Remove the selection and move the
+            // cursor to the right clicked position.
+            stopTextActionMode();
+            Selection.setSelection((Spannable) mTextView.getText(), offset);
+        }
+
+        // TODO: Add suggestions in the context menu.
+
+        menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
+                com.android.internal.R.string.undo)
+                .setAlphabeticShortcut('z')
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canUndo());
+        menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
+                com.android.internal.R.string.redo)
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canRedo());
+
+        menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
+                com.android.internal.R.string.cut)
+                .setAlphabeticShortcut('x')
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canCut());
+        menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
+                com.android.internal.R.string.copy)
+                .setAlphabeticShortcut('c')
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canCopy());
+        menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
+                com.android.internal.R.string.paste)
+                .setAlphabeticShortcut('v')
+                .setEnabled(mTextView.canPaste())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+        menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+                com.android.internal.R.string.paste_as_plain_text)
+                .setEnabled(mTextView.canPaste())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+        menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
+                com.android.internal.R.string.share)
+                .setEnabled(mTextView.canShare())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+        menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
+                com.android.internal.R.string.selectAll)
+                .setAlphabeticShortcut('a')
+                .setEnabled(mTextView.canSelectAllText())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+
+        mPreserveDetachedSelection = true;
+    }
+
+    private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
+            new MenuItem.OnMenuItemClickListener() {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
+                return true;
+            }
+            return mTextView.onTextContextMenuItem(item.getItemId());
+        }
+    };
+
     /**
      * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
      * pop-up should be displayed.
@@ -2710,6 +2825,9 @@
         }
 
         public void hide() {
+            if (!isShowing()) {
+                return;
+            }
             mPopupWindow.dismiss();
             getPositionListener().removeSubscriber(this);
         }
@@ -2759,8 +2877,10 @@
 
             @Override
             public void dismiss() {
+                if (!isShowing()) {
+                    return;
+                }
                 super.dismiss();
-
                 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
 
                 // Safe cast since show() checks that mTextView.getText() is an Editable
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index ae360fc..89888f0 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -111,6 +111,7 @@
 import android.view.AccessibilityIterators.TextSegmentIterator;
 import android.view.ActionMode;
 import android.view.Choreographer;
+import android.view.ContextMenu;
 import android.view.DragEvent;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
@@ -8497,6 +8498,29 @@
         return super.onGenericMotionEvent(event);
     }
 
+    @Override
+    protected void onCreateContextMenu(ContextMenu menu) {
+        if (mEditor != null) {
+            mEditor.onCreateContextMenu(menu);
+        }
+    }
+
+    @Override
+    public boolean showContextMenu() {
+        if (mEditor != null) {
+            mEditor.setContextMenuAnchor(Float.NaN, Float.NaN);
+        }
+        return super.showContextMenu();
+    }
+
+    @Override
+    public boolean showContextMenu(float x, float y) {
+        if (mEditor != null) {
+            mEditor.setContextMenuAnchor(x, y);
+        }
+        return super.showContextMenu(x, y);
+    }
+
     /**
      * @return True iff this TextView contains a text that can be edited, or if this is
      * a selectable TextView.
@@ -9398,12 +9422,17 @@
     public boolean performLongClick() {
         boolean handled = false;
 
+        if (mEditor != null) {
+            mEditor.mIsBeingLongClicked = true;
+        }
+
         if (super.performLongClick()) {
             handled = true;
         }
 
         if (mEditor != null) {
             handled |= mEditor.performLongClick(handled);
+            mEditor.mIsBeingLongClicked = false;
         }
 
         if (handled) {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 4d967ff..f726860 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2453,6 +2453,9 @@
     <!-- Item on EditText context menu. This action is used to paste from the clipboard into the eidt field -->
     <string name="paste">Paste</string>
 
+    <!-- Item on EditText context menu. This action is used to paste from the clipboard into the eidt field without formatting -->
+    <string name="paste_as_plain_text">Paste as plain text</string>
+
     <!-- Item on EditText context menu. This action is used to replace the current word by other suggested words, suggested by the IME or the spell checker -->
     <string name="replace">Replace\u2026</string>
 
@@ -2465,6 +2468,12 @@
     <!-- Item on EditText context menu. Added only when the context menu is not empty, it enable selection context mode. [CHAR LIMIT=20] -->
     <string name="selectTextMode">Select text</string>
 
+    <!-- Item on EditText context menu. This action is used to undo a text edit operation. -->
+    <string name="undo">Undo</string>
+
+    <!-- Item on EditText context menu. This action is used to redo a text edit operation. -->
+    <string name="redo">Redo</string>
+
     <!-- Text selection contextual mode title, displayed in the CAB. [CHAR LIMIT=20] -->
     <string name="textSelectionCABTitle">Text selection</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 517bb75..640f90b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -456,7 +456,10 @@
   <java-symbol type="string" name="notification_title" />
   <java-symbol type="string" name="permission_request_notification_with_subtitle" />
   <java-symbol type="string" name="prepend_shortcut_label" />
+  <java-symbol type="string" name="paste_as_plain_text" />
   <java-symbol type="string" name="replace" />
+  <java-symbol type="string" name="undo" />
+  <java-symbol type="string" name="redo" />
   <java-symbol type="string" name="textSelectionCABTitle" />
   <java-symbol type="string" name="BaMmi" />
   <java-symbol type="string" name="CLIRDefaultOffNextCallOff" />