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