Add API for nested pre-processing of a11y events; fix ResolverDrawerLayout

Add API for handling nested pre-processing of accessibility events
similar to nested pre-scroll or pre-fling. This allows custom views to
delegate a nested scroll to a parent via the accessibility system.

Use this functionality to allow opening the ResolverDrawerLayout via
accessibility commands.

Bug 18827274

Change-Id: Icd5a502605b78a861bb03e7b11923841a72eb9ab
diff --git a/api/current.txt b/api/current.txt
index 24501f3..96c50a2 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -33332,6 +33332,7 @@
     method public boolean dispatchKeyShortcutEvent(android.view.KeyEvent);
     method public boolean dispatchNestedFling(float, float, boolean);
     method public boolean dispatchNestedPreFling(float, float);
+    method public boolean dispatchNestedPrePerformAccessibilityAction(int, android.os.Bundle);
     method public boolean dispatchNestedPreScroll(int, int, int[], int[]);
     method public boolean dispatchNestedScroll(int, int, int, int, int[]);
     method public boolean dispatchPopulateAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
@@ -34144,6 +34145,7 @@
     method protected abstract void onLayout(boolean, int, int, int, int);
     method public boolean onNestedFling(android.view.View, float, float, boolean);
     method public boolean onNestedPreFling(android.view.View, float, float);
+    method public boolean onNestedPrePerformAccessibilityAction(android.view.View, int, android.os.Bundle);
     method public void onNestedPreScroll(android.view.View, int, int, int[]);
     method public void onNestedScroll(android.view.View, int, int, int, int);
     method public void onNestedScrollAccepted(android.view.View, android.view.View, int);
@@ -34292,6 +34294,7 @@
     method public abstract void notifySubtreeAccessibilityStateChanged(android.view.View, android.view.View, int);
     method public abstract boolean onNestedFling(android.view.View, float, float, boolean);
     method public abstract boolean onNestedPreFling(android.view.View, float, float);
+    method public abstract boolean onNestedPrePerformAccessibilityAction(android.view.View, int, android.os.Bundle);
     method public abstract void onNestedPreScroll(android.view.View, int, int, int[]);
     method public abstract void onNestedScroll(android.view.View, int, int, int, int);
     method public abstract void onNestedScrollAccepted(android.view.View, android.view.View, int);
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 23792be..6a36c26 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -8159,6 +8159,34 @@
     }
 
     /**
+     * Report an accessibility action to this view's parents for delegated processing.
+     *
+     * <p>Implementations of {@link #performAccessibilityAction(int, Bundle)} may internally
+     * call this method to delegate an accessibility action to a supporting parent. If the parent
+     * returns true from its
+     * {@link ViewParent#onNestedPrePerformAccessibilityAction(View, int, android.os.Bundle)}
+     * method this method will return true to signify that the action was consumed.</p>
+     *
+     * <p>This method is useful for implementing nested scrolling child views. If
+     * {@link #isNestedScrollingEnabled()} returns true and the action is a scrolling action
+     * a custom view implementation may invoke this method to allow a parent to consume the
+     * scroll first. If this method returns true the custom view should skip its own scrolling
+     * behavior.</p>
+     *
+     * @param action Accessibility action to delegate
+     * @param arguments Optional action arguments
+     * @return true if the action was consumed by a parent
+     */
+    public boolean dispatchNestedPrePerformAccessibilityAction(int action, Bundle arguments) {
+        for (ViewParent p = getParent(); p != null; p = p.getParent()) {
+            if (p.onNestedPrePerformAccessibilityAction(this, action, arguments)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Performs the specified accessibility action on the view. For
      * possible accessibility actions look at {@link AccessibilityNodeInfo}.
      * <p>
@@ -8168,6 +8196,11 @@
      * is responsible for handling this call.
      * </p>
      *
+     * <p>The default implementation will delegate
+     * {@link AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD} and
+     * {@link AccessibilityNodeInfo#ACTION_SCROLL_FORWARD} to nested scrolling parents if
+     * {@link #isNestedScrollingEnabled() nested scrolling is enabled} on this view.</p>
+     *
      * @param action The action to perform.
      * @param arguments Optional action arguments.
      * @return Whether the action was performed.
@@ -8188,6 +8221,14 @@
     * @hide Until we've refactored all accessibility delegation methods.
     */
     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (isNestedScrollingEnabled()
+                && (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
+                || action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
+            if (dispatchNestedPrePerformAccessibilityAction(action, arguments)) {
+                return true;
+            }
+        }
+
         switch (action) {
             case AccessibilityNodeInfo.ACTION_CLICK: {
                 if (isClickable()) {
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 22c5185..6678ff2 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -32,6 +32,7 @@
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Parcelable;
 import android.os.SystemClock;
 import android.util.AttributeSet;
@@ -2923,6 +2924,22 @@
 
     /**
      * {@inheritDoc}
+     *
+     * <p>Subclasses should always call <code>super.onNestedPrePerformAccessibilityAction</code></p>
+     *
+     * @param target The target view dispatching this action
+     * @param action Action being performed; see
+     *               {@link android.view.accessibility.AccessibilityNodeInfo}
+     * @param args Optional action arguments
+     * @return false by default. Subclasses should return true if they handle the event.
+     */
+    @Override
+    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
      */
     @Override
     void dispatchDetachedFromWindow() {
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index 87a37f4..035871d 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.view.accessibility.AccessibilityEvent;
 
 /**
@@ -551,4 +552,23 @@
      * @return true if this parent consumed the fling ahead of the target view
      */
     public boolean onNestedPreFling(View target, float velocityX, float velocityY);
+
+    /**
+     * React to an accessibility action delegated by a target descendant view before the target
+     * processes it.
+     *
+     * <p>This method may be called by a target descendant view if the target wishes to give
+     * a view in its parent chain a chance to react to the event before normal processing occurs.
+     * Most commonly this will be a scroll event such as
+     * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_SCROLL_FORWARD}.
+     * A ViewParent that supports acting as a nested scrolling parent should override this
+     * method and act accordingly to implement scrolling via accesibility systems.</p>
+     *
+     * @param target The target view dispatching this action
+     * @param action Action being performed; see
+     *               {@link android.view.accessibility.AccessibilityNodeInfo}
+     * @param arguments Optional action arguments
+     * @return true if the action was consumed by this ViewParent
+     */
+    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle arguments);
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 34c27d7..25dd9c7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -6413,6 +6413,11 @@
         return false;
     }
 
+    @Override
+    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+        return false;
+    }
+
     void changeCanvasOpacity(boolean opaque) {
         Log.d(TAG, "changeCanvasOpacity: opaque=" + opaque);
         if (mAttachInfo.mHardwareRenderer != null) {
diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
index ed7af2f..4e48454 100644
--- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java
+++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.AttributeSet;
@@ -31,6 +32,8 @@
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.animation.AnimationUtils;
 import android.widget.AbsListView;
 import android.widget.OverScroller;
@@ -367,8 +370,14 @@
                     child.offsetTopAndBottom((int) dy);
                 }
             }
+            final boolean isCollapsedOld = mCollapseOffset != 0;
             mCollapseOffset = newPos;
             mTopOffset += dy;
+            final boolean isCollapsedNew = newPos != 0;
+            if (isCollapsedOld != isCollapsedNew) {
+                notifyViewAccessibilityStateChangedIfNeeded(
+                        AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+            }
             postInvalidateOnAnimation();
             return dy;
         }
@@ -571,6 +580,50 @@
     }
 
     @Override
+    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+        if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
+            return true;
+        }
+
+        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
+            smoothScrollTo(0, 0);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        event.setClassName(ResolverDrawerLayout.class.getName());
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setClassName(ResolverDrawerLayout.class.getName());
+        if (isEnabled()) {
+            if (mCollapseOffset != 0) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+                info.setScrollable(true);
+            }
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (super.performAccessibilityAction(action, arguments)) {
+            return true;
+        }
+
+        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
+            smoothScrollTo(0, 0);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
         int widthSize = sourceWidth;
@@ -615,7 +668,13 @@
         mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
 
         if (isLaidOut()) {
+            final boolean isCollapsedOld = mCollapseOffset != 0;
             mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
+            final boolean isCollapsedNew = mCollapseOffset != 0;
+            if (isCollapsedOld != isCollapsedNew) {
+                notifyViewAccessibilityStateChangedIfNeeded(
+                        AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+            }
         } else {
             // Start out collapsed at first unless we restored state for otherwise
             mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;