From 7c51284d8019ed04ab296be84839d8a90ac042fa Mon Sep 17 00:00:00 2001
From: Svetoslav
Date: Wed, 30 Jan 2013 23:02:08 -0800
Subject: [PATCH] Add accessibility actions for text editing.
Currently text editing is pretty hard (certain operations even
impossible) for a blind person. To address the issue this change
adds APIs that enable an accessibility service to perform basic
text editing operations such as copy, paste, cut, set selection,
extend selection while moving at a given granularity.
The new APIs enable an accessibility service to expose a gesture
driven efficient text editing facility.
bug:8098384
Change-Id: I82b200138a3fdf4c0c316b774fc08a096ced29d0
---
api/current.txt | 7 +
core/java/android/view/View.java | 81 ++++++++----
.../accessibility/AccessibilityNodeInfo.java | 104 ++++++++++++++-
core/java/android/widget/TextView.java | 121 ++++++++++++++++--
.../AccessibilityManagerService.java | 6 +-
5 files changed, 274 insertions(+), 45 deletions(-)
diff --git a/api/current.txt b/api/current.txt
index fab07f6985e39..235c1b736eac7 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26345,21 +26345,28 @@ package android.view.accessibility {
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 b9babdcc2e039..11c80c26ebf19 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -1562,9 +1562,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
int mAccessibilityViewId = NO_ID;
- /**
- * @hide
- */
private int mAccessibilityCursorPosition = ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
/**
@@ -2516,8 +2513,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
/**
* 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
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 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
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 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
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 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
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 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * 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 6d0a2375ece6c..7a3d7c3c14c7b 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -131,16 +131,22 @@ public class AccessibilityNodeInfo implements Parcelable {
* at a given movement granularity. For example, move to the next character,
* word, etc.
*
- * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}
- * Example:
+ * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
+ * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}
+ * Example: Move to the previous character and do not extend selection.
*
* 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);
*
*
*
+ * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ *
* @see #setMovementGranularities(int)
* @see #getMovementGranularities()
*
@@ -157,17 +163,23 @@ public class AccessibilityNodeInfo implements Parcelable {
* at a given movement granularity. For example, move to the next character,
* word, etc.
*
- * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}
- * Example:
+ * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<,
+ * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}
+ * Example: Move to the next character and do not extend selection.
*
* 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);
*
*
*
+ * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT
+ * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN
+ *
* @see #setMovementGranularities(int)
* @see #getMovementGranularities()
*
@@ -219,6 +231,41 @@ public class AccessibilityNodeInfo implements Parcelable {
*/
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.
+ *
+ * Arguments: {@link #ACTION_ARGUMENT_SELECTION_START_INT},
+ * {@link #ACTION_ARGUMENT_SELECTION_END_INT}
+ * Example:
+ *
+ * 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);
+ *
+ *
+ *
+ * @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.
*
@@ -226,9 +273,12 @@ public class AccessibilityNodeInfo implements Parcelable {
* Actions: {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY},
* {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY}
*
+ *
+ * @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 @@ public class AccessibilityNodeInfo implements Parcelable {
* Actions: {@link #ACTION_NEXT_HTML_ELEMENT},
* {@link #ACTION_PREVIOUS_HTML_ELEMENT}
*
+ *
+ * @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.
+ *
+ * Type: boolean
+ * Actions: {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY},
+ * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY}
+ *
+ *
+ * @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.
+ *
+ * Type: int
+ * Actions: {@link #ACTION_SET_SELECTION}
+ *
+ *
+ * @see #ACTION_SET_SELECTION
+ */
+ public static final String ACTION_ARGUMENT_SELECTION_START_INT =
+ "ACTION_ARGUMENT_SELECTION_START_INT";
+
+ /**
+ * Argument for specifying the selection end.
+ *
+ * Type: int
+ * Actions: {@link #ACTION_SET_SELECTION}
+ *
+ *
+ * @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 f8db622ab5c10..2f02780270f33 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -7985,6 +7985,80 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
| 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* @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 b7c3450bcafd1..ded42ee916b23 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2260,7 +2260,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub {
| 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