Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7c51284d authored by Svetoslav's avatar Svetoslav
Browse files

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
parent 8c47e856
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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
+57 −24
Original line number Diff line number Diff line
@@ -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,
@@ -7094,18 +7105,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        return getContentDescription();
    }
    /**
     * Gets whether accessibility selection can be extended.
     *
     * @return If selection is extensible.
     *
     * @hide
     */
    public boolean isAccessibilitySelectionExtendable() {
        return false;
    }
    /**
     * @hide
     */
    public int getAccessibilityCursorPosition() {
    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,
+98 −6
Original line number Diff line number Diff line
@@ -131,16 +131,22 @@ public class AccessibilityNodeInfo implements Parcelable {
     * 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 @@ public class AccessibilityNodeInfo implements Parcelable {
     * 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()
     *
@@ -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.
     * <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>
@@ -226,6 +273,9 @@ public class AccessibilityNodeInfo implements Parcelable {
     * <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";
@@ -237,10 +287,52 @@ public class AccessibilityNodeInfo implements Parcelable {
     * <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";

    /**
     * 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.
     */
+104 −11
Original line number Diff line number Diff line
@@ -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 selectionStart = getSelectionStart();
            if (selectionStart >= 0) {
                return selectionStart;
            }
        }
        return ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
    }

    /**
     * @hide
     */
    public boolean isAccessibilitySelectionExtendable() {
        return true;
    }

    /**
     * @hide
     */
    @Override
    public int getAccessibilitySelectionEnd() {
        if (TextUtils.isEmpty(getContentDescription())) {
            final int selectionEnd = getSelectionEnd();
            if (selectionEnd >= 0) {
                return selectionEnd;
            }
        }
        return super.getAccessibilityCursorPosition();
        return ACCESSIBILITY_CURSOR_POSITION_UNDEFINED;
    }

    /**
     * @hide
     */
    @Override
    public void setAccessibilityCursorPosition(int index) {
        if (getAccessibilityCursorPosition() == index) {
    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);
        }
    }

+5 −1
Original line number Diff line number Diff line
@@ -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