Merge "Add accessibility actions for text editing."
diff --git a/api/current.txt b/api/current.txt
index 8df9c24..ab91974 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26352,21 +26352,28 @@
     method public void setVisibleToUser(boolean);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int ACTION_ACCESSIBILITY_FOCUS = 64; // 0x40
+    field public static final java.lang.String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN = "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN";
     field public static final java.lang.String ACTION_ARGUMENT_HTML_ELEMENT_STRING = "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
     field public static final java.lang.String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT = "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT";
+    field public static final java.lang.String ACTION_ARGUMENT_SELECTION_END_INT = "ACTION_ARGUMENT_SELECTION_END_INT";
+    field public static final java.lang.String ACTION_ARGUMENT_SELECTION_START_INT = "ACTION_ARGUMENT_SELECTION_START_INT";
     field public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 128; // 0x80
     field public static final int ACTION_CLEAR_FOCUS = 2; // 0x2
     field public static final int ACTION_CLEAR_SELECTION = 8; // 0x8
     field public static final int ACTION_CLICK = 16; // 0x10
+    field public static final int ACTION_COPY = 16384; // 0x4000
+    field public static final int ACTION_CUT = 65536; // 0x10000
     field public static final int ACTION_FOCUS = 1; // 0x1
     field public static final int ACTION_LONG_CLICK = 32; // 0x20
     field public static final int ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 256; // 0x100
     field public static final int ACTION_NEXT_HTML_ELEMENT = 1024; // 0x400
+    field public static final int ACTION_PASTE = 32768; // 0x8000
     field public static final int ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 512; // 0x200
     field public static final int ACTION_PREVIOUS_HTML_ELEMENT = 2048; // 0x800
     field public static final int ACTION_SCROLL_BACKWARD = 8192; // 0x2000
     field public static final int ACTION_SCROLL_FORWARD = 4096; // 0x1000
     field public static final int ACTION_SELECT = 4; // 0x4
+    field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
     field public static final android.os.Parcelable.Creator CREATOR;
     field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
     field public static final int FOCUS_INPUT = 1; // 0x1
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index b9babdc..11c80c26 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -1562,9 +1562,6 @@
      */
     int mAccessibilityViewId = NO_ID;
 
-    /**
-     * @hide
-     */
     private int mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
 
     /**
@@ -2516,8 +2513,10 @@
 
     /**
      * The undefined cursor position.
+     *
+     * @hide
      */
-    private static final int ACCESSIBILITY_CURSOR_POSITION_UNDEFINED = -1;
+    public static final int ACCESSIBILITY_CURSOR_POSITION_UNDEFINED = -1;
 
     /**
      * Indicates that the screen has changed state and is now off.
@@ -7009,21 +7008,25 @@
                 if (arguments != null) {
                     final int granularity = arguments.getInt(
                             AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
-                    return nextAtGranularity(granularity);
+                    final boolean extendSelection = arguments.getBoolean(
+                            AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
+                    return nextAtGranularity(granularity, extendSelection);
                 }
             } break;
             case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
                 if (arguments != null) {
                     final int granularity = arguments.getInt(
                             AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
-                    return previousAtGranularity(granularity);
+                    final boolean extendSelection = arguments.getBoolean(
+                            AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
+                    return previousAtGranularity(granularity, extendSelection);
                 }
             } break;
         }
         return false;
     }
 
-    private boolean nextAtGranularity(int granularity) {
+    private boolean nextAtGranularity(int granularity, boolean extendSelection) {
         CharSequence text = getIterableTextForAccessibility();
         if (text == null || text.length() == 0) {
             return false;
@@ -7032,21 +7035,32 @@
         if (iterator == null) {
             return false;
         }
-        final int current = getAccessibilityCursorPosition();
+        int current = getAccessibilitySelectionEnd();
+        if (current == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) {
+            current = 0;
+        }
         final int[] range = iterator.following(current);
         if (range == null) {
             return false;
         }
         final int start = range[0];
         final int end = range[1];
-        setAccessibilityCursorPosition(end);
+        if (extendSelection && isAccessibilitySelectionExtendable()) {
+            int selectionStart = getAccessibilitySelectionStart();
+            if (selectionStart == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) {
+                selectionStart = start;
+            }
+            setAccessibilitySelection(selectionStart, end);
+        } else {
+            setAccessibilitySelection(end, end);
+        }
         sendViewTextTraversedAtGranularityEvent(
                 AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
                 granularity, start, end);
         return true;
     }
 
-    private boolean previousAtGranularity(int granularity) {
+    private boolean previousAtGranularity(int granularity, boolean extendSelection) {
         CharSequence text = getIterableTextForAccessibility();
         if (text == null || text.length() == 0) {
             return false;
@@ -7055,15 +7069,9 @@
         if (iterator == null) {
             return false;
         }
-        int current = getAccessibilityCursorPosition();
+        int current = getAccessibilitySelectionStart();
         if (current == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) {
             current = text.length();
-            setAccessibilityCursorPosition(current);
-        } else if (granularity == AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER) {
-            // When traversing by character we always put the cursor after the character
-            // to ease edit and have to compensate before asking the for previous segment.
-            current--;
-            setAccessibilityCursorPosition(current);
         }
         final int[] range = iterator.preceding(current);
         if (range == null) {
@@ -7071,11 +7079,14 @@
         }
         final int start = range[0];
         final int end = range[1];
-        // Always put the cursor after the character to ease edit.
-        if (granularity == AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER) {
-            setAccessibilityCursorPosition(end);
+        if (extendSelection && isAccessibilitySelectionExtendable()) {
+            int selectionEnd = getAccessibilitySelectionEnd();
+            if (selectionEnd == ACCESSIBILITY_CURSOR_POSITION_UNDEFINED) {
+                selectionEnd = end;
+            }
+            setAccessibilitySelection(start, selectionEnd);
         } else {
-            setAccessibilityCursorPosition(start);
+            setAccessibilitySelection(start, start);
         }
         sendViewTextTraversedAtGranularityEvent(
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
@@ -7095,17 +7106,39 @@
     }
 
     /**
+     * Gets whether accessibility selection can be extended.
+     *
+     * @return If selection is extensible.
+     *
      * @hide
      */
-    public int getAccessibilityCursorPosition() {
+    public boolean isAccessibilitySelectionExtendable() {
+        return false;
+    }
+
+    /**
+     * @hide
+     */
+    public int getAccessibilitySelectionStart() {
         return mAccessibilityCursorPosition;
     }
 
     /**
      * @hide
      */
-    public void setAccessibilityCursorPosition(int position) {
-        mAccessibilityCursorPosition = position;
+    public int getAccessibilitySelectionEnd() {
+        return getAccessibilitySelectionStart();
+    }
+
+    /**
+     * @hide
+     */
+    public void setAccessibilitySelection(int start, int end) {
+        if (start >= 0 && start == end && end <= getIterableTextForAccessibility().length()) {
+            mAccessibilityCursorPosition = start;
+        } else {
+            mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
+        }
     }
 
     private void sendViewTextTraversedAtGranularityEvent(int action, int granularity,
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 6d0a237..7a3d7c3 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -131,16 +131,22 @@
      * at a given movement granularity. For example, move to the next character,
      * word, etc.
      * <p>
-     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<br>
-     * <strong>Example:</strong>
+     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
+     * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
+     * <strong>Example:</strong> Move to the previous character and do not extend selection.
      * <code><pre><p>
      *   Bundle arguments = new Bundle();
      *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
      *           AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
+     *   arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
+     *           false);
      *   info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, arguments);
      * </code></pre></p>
      * </p>
      *
+     * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+     * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+     *
      * @see #setMovementGranularities(int)
      * @see #getMovementGranularities()
      *
@@ -157,17 +163,23 @@
      * at a given movement granularity. For example, move to the next character,
      * word, etc.
      * <p>
-     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<br>
-     * <strong>Example:</strong>
+     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
+     * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br>
+     * <strong>Example:</strong> Move to the next character and do not extend selection.
      * <code><pre><p>
      *   Bundle arguments = new Bundle();
      *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
      *           AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
+     *   arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN,
+     *           false);
      *   info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
      *           arguments);
      * </code></pre></p>
      * </p>
      *
+     * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+     * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+     *
      * @see #setMovementGranularities(int)
      * @see #getMovementGranularities()
      *
@@ -220,15 +232,53 @@
     public static final int ACTION_SCROLL_BACKWARD = 0x00002000;
 
     /**
+     * Action to copy the current selection to the clipboard.
+     */
+    public static final int ACTION_COPY = 0x00004000;
+
+    /**
+     * Action to paste the current clipboard content.
+     */
+    public static final int ACTION_PASTE = 0x00008000;
+
+    /**
+     * Action to cut the current selection and place it to the clipboard.
+     */
+    public static final int ACTION_CUT = 0x00010000;
+
+    /**
+     * Action to set the selection. Performing this action with no arguments
+     * clears the selection.
+     * <p>
+     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_SELECTION_START_INT},
+     * {@link #ACTION_ARGUMENT_SELECTION_END_INT}<br>
+     * <strong>Example:</strong>
+     * <code><pre><p>
+     *   Bundle arguments = new Bundle();
+     *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1);
+     *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2);
+     *   info.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments);
+     * </code></pre></p>
+     * </p>
+     *
+     * @see #ACTION_ARGUMENT_SELECTION_START_INT
+     * @see #ACTION_ARGUMENT_SELECTION_END_INT
+     */
+    public static final int ACTION_SET_SELECTION = 0x00020000;
+
+    /**
      * Argument for which movement granularity to be used when traversing the node text.
      * <p>
      * <strong>Type:</strong> int<br>
      * <strong>Actions:</strong> {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY},
      * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY}
      * </p>
+     *
+     * @see #ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+     * @see #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
      */
     public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT =
-        "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT";
+            "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT";
 
     /**
      * Argument for which HTML element to get moving to the next/previous HTML element.
@@ -237,9 +287,51 @@
      * <strong>Actions:</strong> {@link #ACTION_NEXT_HTML_ELEMENT},
      *         {@link #ACTION_PREVIOUS_HTML_ELEMENT}
      * </p>
+     *
+     * @see #ACTION_NEXT_HTML_ELEMENT
+     * @see #ACTION_PREVIOUS_HTML_ELEMENT
      */
     public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING =
-        "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
+            "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
+
+    /**
+     * Argument for whether when moving at granularity to extend the selection
+     * or to move it otherwise.
+     * <p>
+     * <strong>Type:</strong> boolean<br>
+     * <strong>Actions:</strong> {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY},
+     * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY}
+     * </p>
+     *
+     * @see #ACTION_NEXT_AT_MOVEMENT_GRANULARITY
+     * @see #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY
+     */
+    public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN =
+            "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN";
+
+    /**
+     * Argument for specifying the selection start.
+     * <p>
+     * <strong>Type:</strong> int<br>
+     * <strong>Actions:</strong> {@link #ACTION_SET_SELECTION}
+     * </p>
+     *
+     * @see #ACTION_SET_SELECTION
+     */
+    public static final String ACTION_ARGUMENT_SELECTION_START_INT =
+            "ACTION_ARGUMENT_SELECTION_START_INT";
+
+    /**
+     * Argument for specifying the selection end.
+     * <p>
+     * <strong>Type:</strong> int<br>
+     * <strong>Actions:</strong> {@link #ACTION_SET_SELECTION}
+     * </p>
+     *
+     * @see #ACTION_SET_SELECTION
+     */
+    public static final String ACTION_ARGUMENT_SELECTION_END_INT =
+            "ACTION_ARGUMENT_SELECTION_END_INT";
 
     /**
      * The input focus.
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index f8db622..2f02780 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -7985,6 +7985,80 @@
                     | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
                     | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE);
         }
+        if (isFocused()) {
+            if (canSelectText()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
+            }
+            if (canCopy()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_COPY);
+            }
+            if (canPaste()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_PASTE);
+            }
+            if (canCut()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_CUT);
+            }
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_COPY: {
+                if (isFocused() && canCopy()) {
+                    if (onTextContextMenuItem(ID_COPY)) {
+                        notifyAccessibilityStateChanged();
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_PASTE: {
+                if (isFocused() && canPaste()) {
+                    if (onTextContextMenuItem(ID_PASTE)) {
+                        notifyAccessibilityStateChanged();
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_CUT: {
+                if (isFocused() && canCut()) {
+                    if (onTextContextMenuItem(ID_CUT)) {
+                        notifyAccessibilityStateChanged();
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
+                if (isFocused() && canSelectText()) {
+                    final int start = (arguments != null) ? arguments.getInt(
+                            AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) : -1;
+                    final int end = (arguments != null) ? arguments.getInt(
+                            AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) : -1;
+                    CharSequence text = getIterableTextForAccessibility();
+                    if (text == null) {
+                        return false;
+                    }
+                    // No arguments clears the selection.
+                    if (start == end && end == -1) {
+                        Selection.removeSelection((Spannable) text);
+                        notifyAccessibilityStateChanged();
+                        return true;
+                    }
+                    if (start >= 0 && start <= end && end <= text.length()) {
+                        Selection.setSelection((Spannable) text, start, end);
+                        // Make sure selection mode is engaged.
+                        if (mEditor != null) {
+                            mEditor.startSelectionActionMode();
+                        }
+                        notifyAccessibilityStateChanged();
+                        return true;
+                    }
+                }
+            } return false;
+            default: {
+                return super.performAccessibilityAction(action, arguments);
+            }
+        }
     }
 
     @Override
@@ -8554,32 +8628,51 @@
      * @hide
      */
     @Override
-    public int getAccessibilityCursorPosition() {
+    public int getAccessibilitySelectionStart() {
         if (TextUtils.isEmpty(getContentDescription())) {
-            final int selectionEnd = getSelectionEnd();
-            if (selectionEnd >= 0) {
-                return selectionEnd;
+            final int selectionStart = getSelectionStart();
+            if (selectionStart >= 0) {
+                return selectionStart;
             }
         }
-        return super.getAccessibilityCursorPosition();
+        return ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isAccessibilitySelectionExtendable() {
+        return true;
     }
 
     /**
      * @hide
      */
     @Override
-    public void setAccessibilityCursorPosition(int index) {
-        if (getAccessibilityCursorPosition() == index) {
+    public int getAccessibilitySelectionEnd() {
+        if (TextUtils.isEmpty(getContentDescription())) {
+            final int selectionEnd = getSelectionEnd();
+            if (selectionEnd >= 0) {
+                return selectionEnd;
+            }
+        }
+        return ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void setAccessibilitySelection(int start, int end) {
+        if (getAccessibilitySelectionStart() == start
+                && getAccessibilitySelectionEnd() == end) {
             return;
         }
-        if (TextUtils.isEmpty(getContentDescription())) {
-            if (index >= 0 && index <= mText.length()) {
-                Selection.setSelection((Spannable) mText, index);
-            } else {
-                Selection.removeSelection((Spannable) mText);
-            }
+        CharSequence text = getIterableTextForAccessibility();
+        if (start >= 0 && start <= end && end <= text.length()) {
+            Selection.setSelection((Spannable) text, start, end);
         } else {
-            super.setAccessibilityCursorPosition(index);
+            Selection.removeSelection((Spannable) text);
         }
     }
 
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
index 93187c10..f4592f7 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2225,7 +2225,11 @@
             | AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT
             | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT
             | AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
-            | AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
+            | AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
+            | AccessibilityNodeInfo.ACTION_COPY
+            | AccessibilityNodeInfo.ACTION_PASTE
+            | AccessibilityNodeInfo.ACTION_CUT
+            | AccessibilityNodeInfo.ACTION_SET_SELECTION;
 
         private static final int RETRIEVAL_ALLOWING_EVENT_TYPES =
             AccessibilityEvent.TYPE_VIEW_CLICKED