Adding explicit text traversal granularities and actions for web navigation.

1. The granularities for traversing the text content of an accessibility
   node info are now predefined constants and custom ones will not be
   supported. This is the simplest solution - we can always add namespaced
   user defined ones (unlikely).

2. Added actions for traversing web content. These actions can be used by
   an accessibility service to transparently drive the JavaScript based
   screen reader that is used for handling web content.

3. Added a new accessibility event type for traversing the content of a
   view. This event is needed to announce to the user what is the next
   element, i.e. the one next to the cursor, after the view's text was
   traversed.

bug:5932640
bug:6389591

Change-Id: I144647da55bc4005c64f89865ef333af8359e145
diff --git a/api/current.txt b/api/current.txt
index 981d465..9d24d91 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -24927,6 +24927,7 @@
     method public static java.lang.String eventTypeToString(int);
     method public long getEventTime();
     method public int getEventType();
+    method public int getGranularity();
     method public java.lang.CharSequence getPackageName();
     method public android.view.accessibility.AccessibilityRecord getRecord(int);
     method public int getRecordCount();
@@ -24936,6 +24937,7 @@
     method public static android.view.accessibility.AccessibilityEvent obtain();
     method public void setEventTime(long);
     method public void setEventType(int);
+    method public void setGranularity(int);
     method public void setPackageName(java.lang.CharSequence);
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator CREATOR;
@@ -24957,6 +24959,7 @@
     field public static final int TYPE_VIEW_SELECTED = 4; // 0x4
     field public static final int TYPE_VIEW_TEXT_CHANGED = 16; // 0x10
     field public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 8192; // 0x2000
+    field public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_GRANULARITY = 131072; // 0x20000
     field public static final int TYPE_WINDOW_CONTENT_CHANGED = 2048; // 0x800
     field public static final int TYPE_WINDOW_STATE_CHANGED = 32; // 0x20
   }
@@ -24997,7 +25000,7 @@
     method public int getChildCount();
     method public java.lang.CharSequence getClassName();
     method public java.lang.CharSequence getContentDescription();
-    method public java.lang.CharSequence[] getGranularities();
+    method public int getGranularities();
     method public java.lang.CharSequence getPackageName();
     method public android.view.accessibility.AccessibilityNodeInfo getParent();
     method public java.lang.CharSequence getText();
@@ -25031,7 +25034,7 @@
     method public void setEnabled(boolean);
     method public void setFocusable(boolean);
     method public void setFocused(boolean);
-    method public void setGranularities(java.lang.CharSequence[]);
+    method public void setGranularities(int);
     method public void setLongClickable(boolean);
     method public void setPackageName(java.lang.CharSequence);
     method public void setParent(android.view.View);
@@ -25044,7 +25047,8 @@
     method public void setText(java.lang.CharSequence);
     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_GRANULARITY = "ACTION_ARGUMENT_GRANULARITY";
+    field public static final java.lang.String ACTION_ARGUMENT_GRANULARITY_INT = "ACTION_ARGUMENT_GRANULARITY_INT";
+    field public static final java.lang.String ACTION_ARGUMENT_HTML_ELEMENT_STRING = "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
     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
@@ -25052,11 +25056,18 @@
     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_GRANULARITY = 256; // 0x100
+    field public static final int ACTION_NEXT_HTML_ELEMENT = 1024; // 0x400
     field public static final int ACTION_PREVIOUS_AT_GRANULARITY = 512; // 0x200
+    field public static final int ACTION_PREVIOUS_HTML_ELEMENT = 2048; // 0x800
     field public static final int ACTION_SELECT = 4; // 0x4
     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
+    field public static final int GRANULARITY_CHARACTER = 1; // 0x1
+    field public static final int GRANULARITY_LINE = 4; // 0x4
+    field public static final int GRANULARITY_PAGE = 16; // 0x10
+    field public static final int GRANULARITY_PARAGRAPH = 8; // 0x8
+    field public static final int GRANULARITY_WORD = 2; // 0x2
   }
 
   public abstract class AccessibilityNodeProvider {
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index 6cb1578..6d1166e 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -226,6 +226,23 @@
  *   <li>{@link #getContentDescription()} - The content description of the source.</li>
  * </ul>
  * </p>
+ * <b>View text traversed at granularity</b> - represents the event of traversing the
+ * text of a view at a given granularity. For example, moving to the next word.</br>
+ * <em>Type:</em> {@link #TYPE_VIEW_TEXT_TRAVERSED_AT_GRANULARITY} </br>
+ * <em>Properties:</em></br>
+ * <ul>
+ *   <li>{@link #getEventType()} - The type of the event.</li>
+ *   <li>{@link #getSource()} - The source info (for registered clients).</li>
+ *   <li>{@link #getClassName()} - The class name of the source.</li>
+ *   <li>{@link #getPackageName()} - The package name of the source.</li>
+ *   <li>{@link #getEventTime()}  - The event time.</li>
+ *   <li>{@link #getText()} - The text of the current text at the granularity.</li>
+ *   <li>{@link #isPassword()} - Whether the source is password.</li>
+ *   <li>{@link #isEnabled()} - Whether the source is enabled.</li>
+ *   <li>{@link #getContentDescription()} - The content description of the source.</li>
+ *   <li>{@link #getGranularity()} - Sets the granularity at which a view's text was traversed.</li>
+ * </ul>
+ * </p>
  * <p>
  * <b>View scrolled</b> - represents the event of scrolling a view. If
  * the source is a descendant of {@link android.widget.AdapterView} the
@@ -580,6 +597,11 @@
     public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000;
 
     /**
+     * Represents the event of traversing the text of a view at a given granularity.
+     */
+    public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_GRANULARITY = 0x00020000;
+
+    /**
      * Mask for {@link AccessibilityEvent} all types.
      *
      * @see #TYPE_VIEW_CLICKED
@@ -597,6 +619,7 @@
      * @see #TYPE_VIEW_SCROLLED
      * @see #TYPE_VIEW_TEXT_SELECTION_CHANGED
      * @see #TYPE_ANNOUNCEMENT
+     * @see #TYPE_VIEW_TEXT_TRAVERSED_AT_GRANULARITY
      */
     public static final int TYPES_ALL_MASK = 0xFFFFFFFF;
 
@@ -610,6 +633,7 @@
     private int mEventType;
     private CharSequence mPackageName;
     private long mEventTime;
+    int mGranularity;
 
     private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>();
 
@@ -627,6 +651,7 @@
     void init(AccessibilityEvent event) {
         super.init(event);
         mEventType = event.mEventType;
+        mGranularity = event.mGranularity;
         mEventTime = event.mEventTime;
         mPackageName = event.mPackageName;
     }
@@ -744,6 +769,27 @@
     }
 
     /**
+     * Sets the text granularity that was traversed.
+     *
+     * @param granularity The granularity.
+     *
+     * @throws IllegalStateException If called from an AccessibilityService.
+     */
+    public void setGranularity(int granularity) {
+        enforceNotSealed();
+        mGranularity = granularity;
+    }
+
+    /**
+     * Gets the text granularity that was traversed.
+     *
+     * @return The granularity.
+     */
+    public int getGranularity() {
+        return mGranularity;
+    }
+
+    /**
      * Returns a cached instance if such is available or a new one is
      * instantiated with its type property set.
      *
@@ -831,6 +877,7 @@
     protected void clear() {
         super.clear();
         mEventType = 0;
+        mGranularity = 0;
         mPackageName = null;
         mEventTime = 0;
         while (!mRecords.isEmpty()) {
@@ -847,6 +894,7 @@
     public void initFromParcel(Parcel parcel) {
         mSealed = (parcel.readInt() == 1);
         mEventType = parcel.readInt();
+        mGranularity = parcel.readInt();
         mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
         mEventTime = parcel.readLong();
         mConnectionId = parcel.readInt();
@@ -897,6 +945,7 @@
     public void writeToParcel(Parcel parcel, int flags) {
         parcel.writeInt(isSealed() ? 1 : 0);
         parcel.writeInt(mEventType);
+        parcel.writeInt(mGranularity);
         TextUtils.writeToParcel(mPackageName, parcel, 0);
         parcel.writeLong(mEventTime);
         parcel.writeInt(mConnectionId);
@@ -953,6 +1002,7 @@
         builder.append("EventType: ").append(eventTypeToString(mEventType));
         builder.append("; EventTime: ").append(mEventTime);
         builder.append("; PackageName: ").append(mPackageName);
+        builder.append("; Granularity: ").append(mGranularity);
         builder.append(super.toString());
         if (DEBUG) {
             builder.append("\n");
@@ -1033,6 +1083,8 @@
                 return "TYPE_VIEW_ACCESSIBILITY_FOCUSED";
             case TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
                 return "TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED";
+            case TYPE_VIEW_TEXT_TRAVERSED_AT_GRANULARITY:
+                return "TYPE_CURRENT_AT_GRANULARITY_CHANGED";
             default:
                 return null;
         }
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 3cb3b54..aec18fe 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -102,7 +102,7 @@
     public static final int ACTION_CLEAR_SELECTION = 0x00000008;
 
     /**
-     * Action that clicks on the node info.
+     * Action that long clicks on the node info.
      */
     public static final int ACTION_CLICK = 0x00000010;
 
@@ -122,78 +122,105 @@
     public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080;
 
     /**
-     * Action that requests from the node to go to the next entity in its content
-     * at a given granularity. For example, move to the next word, link, etc.
+     * Action that requests to go to the next entity in this node's text
+     * at a given granularity. For example, move to the next character, word, etc.
      * <p>
-     * <strong>Arguments:</strong>
-     * <ul>
-     * <li>
-     * {@link #ACTION_ARGUMENT_GRANULARITY}
-     * </li>
-     * <li>
-     * </p>
-     * <p>
+     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_GRANULARITY_INT}<br>
      * <strong>Example:</strong>
      * <code><pre><p>
-     *   // Assume the first granularity was presented to the user and she is
-     *   // making an explicit action to traverse the node at that granularity.
-     *   CharSequence granularity = info.getGranularity(0);
      *   Bundle arguments = new Bundle();
-     *   arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_GRANULARITY, granularity);
+     *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_GRANULARITY_INT,
+     *           AccessibilityNodeInfo.GRANULARITY_CHARACTER);
      *   info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_GRANULARITY, arguments);
      * </code></pre></p>
-     * </li>
-     * </ul>
      * </p>
-     * @see #setGranularities(CharSequence[])
+     *
+     * @see #setGranularities(int)
      * @see #getGranularities()
+     *
+     * @see #GRANULARITY_CHARACTER
+     * @see #GRANULARITY_WORD
+     * @see #GRANULARITY_LINE
+     * @see #GRANULARITY_PARAGRAPH
+     * @see #GRANULARITY_PAGE
      */
     public static final int ACTION_NEXT_AT_GRANULARITY = 0x00000100;
 
     /**
-     * Action that requests from the node to go to the previous entity in its content
-     * at a given granularity. For example, move to the next word, link, etc.
+     * Action that requests to go to the previous entity in this node's text
+     * at a given granularity. For example, move to the next character, word, etc.
      * <p>
-     * <strong>Arguments:</strong>
-     * <ul>
-     * <li>
-     *  {@link #ACTION_ARGUMENT_GRANULARITY}
-     * </li>
-     * <li>
-     * </p>
-     * <p>
+     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_GRANULARITY_INT}<br>
      * <strong>Example:</strong>
      * <code><pre><p>
-     *   // Assume the first granularity was presented to the user and she is
-     *   // making an explicit action to traverse the node at that granularity.
-     *   CharSequence granularity = info.getGranularity(0);
      *   Bundle arguments = new Bundle();
-     *   arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_GRANULARITY, granularity);
-     *   info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_GRANULARITY, arguments);
+     *   arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_GRANULARITY_INT,
+     *           AccessibilityNodeInfo.GRANULARITY_CHARACTER);
+     *   info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_GRANULARITY, arguments);
      * </code></pre></p>
-     * </li>
-     * </ul>
      * </p>
-     * @see #setGranularities(CharSequence[])
+     *
+     * @see #setGranularities(int)
      * @see #getGranularities()
+     *
+     * @see #GRANULARITY_CHARACTER
+     * @see #GRANULARITY_WORD
+     * @see #GRANULARITY_LINE
+     * @see #GRANULARITY_PARAGRAPH
+     * @see #GRANULARITY_PAGE
      */
     public static final int ACTION_PREVIOUS_AT_GRANULARITY = 0x00000200;
 
     /**
-     * Argument for which content granularity to be used when traversing the node content.
+     * Action to move to the next HTML element of a given type. For example, move
+     * to the BUTTON, INPUT, TABLE, etc.
      * <p>
-     * <strong>Actions:</strong>
-     * <ul>
-     * <li>
-     * {@link #ACTION_PREVIOUS_AT_GRANULARITY}
-     * </li>
-     * <li>
-     * {@link #ACTION_PREVIOUS_AT_GRANULARITY}
-     * </li>
-     * </ul>
+     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br>
+     * <strong>Example:</strong>
+     * <code><pre><p>
+     *   Bundle arguments = new Bundle();
+     *   arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON");
+     *   info.performAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, arguments);
+     * </code></pre></p>
      * </p>
      */
-    public static final String ACTION_ARGUMENT_GRANULARITY = "ACTION_ARGUMENT_GRANULARITY";
+    public static final int ACTION_NEXT_HTML_ELEMENT = 0x00000400;
+
+    /**
+     * Action to move to the previous HTML element of a given type. For example, move
+     * to the BUTTON, INPUT, TABLE, etc.
+     * <p>
+     * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br>
+     * <strong>Example:</strong>
+     * <code><pre><p>
+     *   Bundle arguments = new Bundle();
+     *   arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON");
+     *   info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, arguments);
+     * </code></pre></p>
+     * </p>
+     */
+    public static final int ACTION_PREVIOUS_HTML_ELEMENT = 0x00000800;
+
+    /**
+     * Argument for which text granularity to be used when traversing the node text.
+     * <p>
+     * <strong>Type:</strong> int<br>
+     * <strong>Actions:</strong> {@link #ACTION_NEXT_AT_GRANULARITY},
+     * {@link #ACTION_PREVIOUS_AT_GRANULARITY}
+     * </p>
+     */
+    public static final String ACTION_ARGUMENT_GRANULARITY_INT = "ACTION_ARGUMENT_GRANULARITY_INT";
+
+    /**
+     * Argument for which HTML element to get moving to the next/previous HTML element.
+     * <p>
+     * <strong>Type:</strong> String<br>
+     * <strong>Actions:</strong> {@link #ACTION_NEXT_HTML_ELEMENT},
+     *         {@link #ACTION_PREVIOUS_HTML_ELEMENT}
+     * </p>
+     */
+    public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING =
+        "ACTION_ARGUMENT_HTML_ELEMENT_STRING";
 
     /**
      * The input focus.
@@ -205,6 +232,33 @@
      */
     public static final int FOCUS_ACCESSIBILITY = 2;
 
+    // Granularities
+
+    /**
+     * Granularity bit for traversing the text of a node by character.
+     */
+    public static final int GRANULARITY_CHARACTER = 0x00000001;
+
+    /**
+     * Granularity bit for traversing the text of a node by word.
+     */
+    public static final int GRANULARITY_WORD = 0x00000002;
+
+    /**
+     * Granularity bit for traversing the text of a node by line.
+     */
+    public static final int GRANULARITY_LINE = 0x00000004;
+
+    /**
+     * Granularity bit for traversing the text of a node by paragraph.
+     */
+    public static final int GRANULARITY_PARAGRAPH = 0x00000008;
+
+    /**
+     * Granularity bit for traversing the text of a node by page.
+     */
+    public static final int GRANULARITY_PAGE = 0x00000010;
+
     // Boolean attributes.
 
     private static final int PROPERTY_CHECKABLE = 0x00000001;
@@ -308,7 +362,7 @@
     private final SparseLongArray mChildNodeIds = new SparseLongArray();
     private int mActions;
 
-    private CharSequence[] mGranularities;
+    private int mGranularities;
 
     private int mConnectionId = UNDEFINED;
 
@@ -532,28 +586,28 @@
     }
 
     /**
-     * Sets the granularities for traversing the content of this node.
+     * Sets the text granularities for traversing the text of this node.
      * <p>
      *   <strong>Note:</strong> Cannot be called from an
      *   {@link android.accessibilityservice.AccessibilityService}.
      *   This class is made immutable before being delivered to an AccessibilityService.
      * </p>
      *
-     * @param granularities The granularity names.
+     * @param granularities The bit mask with granularities.
      *
      * @throws IllegalStateException If called from an AccessibilityService.
      */
-    public void setGranularities(CharSequence[] granularities) {
+    public void setGranularities(int granularities) {
         enforceNotSealed();
         mGranularities = granularities;
     }
 
     /**
-     * Gets the granularities for traversing the content of this node.
+     * Gets the granularities for traversing the text of this node.
      *
-     * @return The count.
+     * @return The bit mask with granularities.
      */
-    public CharSequence[] getGranularities() {
+    public int getGranularities() {
         return mGranularities;
     }
 
@@ -1339,8 +1393,6 @@
         parcel.writeLong(mParentNodeId);
         parcel.writeInt(mConnectionId);
 
-        parcel.writeCharSequenceArray(mGranularities);
-
         SparseLongArray childIds = mChildNodeIds;
         final int childIdsSize = childIds.size();
         parcel.writeInt(childIdsSize);
@@ -1360,6 +1412,8 @@
 
         parcel.writeInt(mActions);
 
+        parcel.writeInt(mGranularities);
+
         parcel.writeInt(mBooleanProperties);
 
         parcel.writeCharSequence(mPackageName);
@@ -1392,7 +1446,7 @@
         mContentDescription = other.mContentDescription;
         mActions= other.mActions;
         mBooleanProperties = other.mBooleanProperties;
-        mGranularities = (other.mGranularities) != null ? other.mGranularities.clone() : null;
+        mGranularities = other.mGranularities;
         final int otherChildIdCount = other.mChildNodeIds.size();
         for (int i = 0; i < otherChildIdCount; i++) {
             mChildNodeIds.put(i, other.mChildNodeIds.valueAt(i));    
@@ -1411,8 +1465,6 @@
         mParentNodeId = parcel.readLong();
         mConnectionId = parcel.readInt();
 
-        mGranularities = parcel.readCharSequenceArray();
-
         SparseLongArray childIds = mChildNodeIds;
         final int childrenSize = parcel.readInt();
         for (int i = 0; i < childrenSize; i++) {
@@ -1432,6 +1484,8 @@
 
         mActions = parcel.readInt();
 
+        mGranularities = parcel.readInt();
+
         mBooleanProperties = parcel.readInt();
 
         mPackageName = parcel.readCharSequence();
@@ -1449,7 +1503,7 @@
         mParentNodeId = ROOT_NODE_ID;
         mWindowId = UNDEFINED;
         mConnectionId = UNDEFINED;
-        mGranularities = null;
+        mGranularities = 0;
         mChildNodeIds.clear();
         mBoundsInParent.set(0, 0, 0, 0);
         mBoundsInScreen.set(0, 0, 0, 0);
@@ -1482,6 +1536,29 @@
         }
     }
 
+    /**
+     * Gets the human readable granularity symbolic name.
+     *
+     * @param granularity The action.
+     * @return The symbolic name.
+     */
+    private static String getGranularitySymbolicName(int granularity) {
+        switch (granularity) {
+            case GRANULARITY_CHARACTER:
+                return "GRANULARITY_CHARACTER";
+            case GRANULARITY_WORD:
+                return "GRANULARITY_WORD";
+            case GRANULARITY_LINE:
+                return "GRANULARITY_LINE";
+            case GRANULARITY_PARAGRAPH:
+                return "GRANULARITY_PARAGRAPH";
+            case GRANULARITY_PAGE:
+                return "GRANULARITY_PAGE";
+            default:
+                throw new IllegalArgumentException("Unknown granularity: " + granularity);
+        }
+    }
+
     private boolean canPerformRequestOverConnection(long accessibilityNodeId) {
         return (mWindowId != UNDEFINED
                 && getAccessibilityViewId(accessibilityNodeId) != UNDEFINED
@@ -1529,11 +1606,13 @@
             builder.append("; virtualDescendantId: " + getVirtualDescendantId(mSourceNodeId));
             builder.append("; mParentNodeId: " + mParentNodeId);
 
-            CharSequence[] granularities = mGranularities;
+            int granularities = mGranularities;
             builder.append("; granularities: [");
-            for (int i = 0, count = granularities.length; i < count; i++) {
-                builder.append(granularities[i]);
-                if (i < count - 1) {
+            while (granularities != 0) {
+                final int granularity = 1 << Integer.numberOfTrailingZeros(granularities);
+                granularities &= ~granularity;
+                builder.append(getGranularitySymbolicName(granularity));
+                if (granularities != 0) {
                     builder.append(", ");
                 }
             }
@@ -1570,7 +1649,6 @@
         builder.append("; scrollable: " + isScrollable());
 
         builder.append("; [");
-
         for (int actionBits = mActions; actionBits != 0;) {
             final int action = 1 << Integer.numberOfTrailingZeros(actionBits);
             actionBits &= ~action;
@@ -1579,7 +1657,6 @@
                 builder.append(", ");
             }
         }
-
         builder.append("]");
 
         return builder.toString();
diff --git a/services/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
index b9404f3..01ddf1f 100644
--- a/services/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1554,7 +1554,7 @@
             IAccessibilityInteractionConnection connection = null;
             synchronized (mLock) {
                 final boolean permissionGranted = mSecurityPolicy.canPerformActionLocked(this,
-                        resolvedWindowId, action);
+                        resolvedWindowId, action, arguments);
                 if (!permissionGranted) {
                     return false;
                 } else {
@@ -1702,7 +1702,16 @@
             | AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS
             | AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS
             | AccessibilityNodeInfo.ACTION_NEXT_AT_GRANULARITY
-            | AccessibilityNodeInfo.ACTION_PREVIOUS_AT_GRANULARITY;
+            | AccessibilityNodeInfo.ACTION_PREVIOUS_AT_GRANULARITY
+            | AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT
+            | AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT;
+
+        private static final int VALID_GRANULARITIES =
+            AccessibilityNodeInfo.GRANULARITY_CHARACTER
+            | AccessibilityNodeInfo.GRANULARITY_WORD
+            | AccessibilityNodeInfo.GRANULARITY_LINE
+            | AccessibilityNodeInfo.GRANULARITY_PARAGRAPH
+            | AccessibilityNodeInfo.GRANULARITY_PAGE;
 
         private static final int RETRIEVAL_ALLOWING_EVENT_TYPES =
             AccessibilityEvent.TYPE_VIEW_CLICKED
@@ -1752,10 +1761,12 @@
             return canRetrieveWindowContent(service) && isRetrievalAllowingWindow(windowId);
         }
 
-        public boolean canPerformActionLocked(Service service, int windowId, int action) {
+        public boolean canPerformActionLocked(Service service, int windowId, int action,
+                Bundle arguments) {
             return canRetrieveWindowContent(service)
                 && isRetrievalAllowingWindow(windowId)
-                && isActionPermitted(action);
+                && isActionPermitted(action)
+                && isActionArgumentsValid(action, arguments);
         }
 
         public boolean canRetrieveWindowContent(Service service) {
@@ -1779,6 +1790,29 @@
              return (VALID_ACTIONS & action) != 0;
         }
 
+        private boolean isActionArgumentsValid(int action, Bundle arguments) {
+            switch (action) {
+                case AccessibilityNodeInfo.ACTION_NEXT_AT_GRANULARITY:
+                case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_GRANULARITY: {
+                    if (arguments.size() == 1) {
+                        final int granularity = arguments.getInt(
+                                AccessibilityNodeInfo.ACTION_ARGUMENT_GRANULARITY_INT);
+                        return (granularity & VALID_GRANULARITIES) != 0
+                                && Integer.bitCount(granularity) == 1;
+                    }
+                } break;
+                case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
+                case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: {
+                    if (arguments.size() == 1) {
+                        String element = arguments.getString(
+                                AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
+                        return !TextUtils.isEmpty(element);
+                    }
+                } break;
+            }
+            return false;
+        }
+
         private void enforceCallingPermission(String permission, String function) {
             if (OWN_PROCESS_ID == Binder.getCallingPid()) {
                 return;