Merge "Improving accessibility focus traversal." into jb-dev
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index d42757d..e1f1db2 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -330,7 +330,7 @@
                                 if (provider != null) {
                                     List<AccessibilityNodeInfo> infosFromProvider =
                                         provider.findAccessibilityNodeInfosByText(text,
-                                                virtualDescendantId);
+                                                AccessibilityNodeInfo.UNDEFINED);
                                     if (infosFromProvider != null) {
                                         infos.addAll(infosFromProvider);
                                     }
diff --git a/core/java/android/view/FocusFinder.java b/core/java/android/view/FocusFinder.java
index 6bf1888..9063cea 100644
--- a/core/java/android/view/FocusFinder.java
+++ b/core/java/android/view/FocusFinder.java
@@ -276,7 +276,10 @@
                 return focusables.get(position + 1);
             }
         }
-        return focusables.get(0);
+        if (!focusables.isEmpty()) {
+            return focusables.get(0);
+        }
+        return null;
     }
 
     private static View getBackwardFocusable(ViewGroup root, View focused,
@@ -293,7 +296,10 @@
                 return focusables.get(position - 1);
             }
         }
-        return focusables.get(count - 1);
+        if (!focusables.isEmpty()) {
+            return focusables.get(count - 1);
+        }
+        return null;
     }
 
     /**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 2f54615..14523d3 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -6027,8 +6027,7 @@
             return;
         }
         if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) {
-            if (AccessibilityManager.getInstance(mContext).isEnabled()
-                    && includeForAccessibility()) {
+            if (canTakeAccessibilityFocusFromHover()) {
                 views.add(this);
                 return;
             }
@@ -6181,57 +6180,28 @@
         }
     }
 
-    /**
-     * Find the best view to take accessibility focus from a hover.
-     * This function finds the deepest actionable view and if that
-     * fails ask the parent to take accessibility focus from hover.
-     *
-     * @param x The X hovered location in this view coorditantes.
-     * @param y The Y hovered location in this view coorditantes.
-     * @return Whether the request was handled.
-     *
-     * @hide
-     */
-    public boolean requestAccessibilityFocusFromHover(float x, float y) {
-        if (onRequestAccessibilityFocusFromHover(x, y)) {
-            return true;
-        }
-        ViewParent parent = mParent;
-        if (parent instanceof View) {
-            View parentView = (View) parent;
-
-            float[] position = mAttachInfo.mTmpTransformLocation;
-            position[0] = x;
-            position[1] = y;
-
-            // Compensate for the transformation of the current matrix.
-            if (!hasIdentityMatrix()) {
-                getMatrix().mapPoints(position);
+    private void requestAccessibilityFocusFromHover() {
+        if (includeForAccessibility() && isActionableForAccessibility()) {
+            requestAccessibilityFocus();
+        } else {
+            if (mParent != null) {
+                View nextFocus = mParent.findViewToTakeAccessibilityFocusFromHover(this, this);
+                if (nextFocus != null) {
+                    nextFocus.requestAccessibilityFocus();
+                }
             }
-
-            // Compensate for the parent scroll and the offset
-            // of this view stop from the parent top.
-            position[0] += mLeft - parentView.mScrollX;
-            position[1] += mTop - parentView.mScrollY;
-
-            return parentView.requestAccessibilityFocusFromHover(position[0], position[1]);
         }
-        return false;
     }
 
     /**
-     * Requests to give this View focus from hover.
-     *
-     * @param x The X hovered location in this view coorditantes.
-     * @param y The Y hovered location in this view coorditantes.
-     * @return Whether the request was handled.
-     *
      * @hide
      */
-    public boolean onRequestAccessibilityFocusFromHover(float x, float y) {
-        if (includeForAccessibility()
-                && (isActionableForAccessibility() || hasListenersForAccessibility())) {
-            return requestAccessibilityFocus();
+    public boolean canTakeAccessibilityFocusFromHover() {
+        if (includeForAccessibility() && isActionableForAccessibility()) {
+            return true;
+        }
+        if (mParent != null) {
+            return (mParent.findViewToTakeAccessibilityFocusFromHover(this, this) == this);
         }
         return false;
     }
@@ -6493,14 +6463,15 @@
      * important for accessibility are regarded.
      *
      * @return Whether to regard the view for accessibility.
+     *
+     * @hide
      */
-    boolean includeForAccessibility() {
+    public boolean includeForAccessibility() {
         if (mAttachInfo != null) {
             if (!mAttachInfo.mIncludeNotImportantViews) {
                 return isImportantForAccessibility();
-            } else {
-                return true;
             }
+            return true;
         }
         return false;
     }
@@ -6511,8 +6482,10 @@
      * accessiiblity.
      *
      * @return True if the view is actionable for accessibility.
+     *
+     * @hide
      */
-    private boolean isActionableForAccessibility() {
+    public boolean isActionableForAccessibility() {
         return (isClickable() || isLongClickable() || isFocusable());
     }
 
@@ -7688,7 +7661,7 @@
                     && pointInView(event.getX(), event.getY())) {
                 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
                 mSendingHoverAccessibilityEvents = true;
-                requestAccessibilityFocusFromHover((int) event.getX(), (int) event.getY());
+                requestAccessibilityFocusFromHover();
             }
         } else {
             if (action == MotionEvent.ACTION_HOVER_EXIT
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index f55b7ac..b95ca5e 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -628,7 +628,11 @@
      *        FOCUS_RIGHT, or 0 for not applicable.
      */
     public View focusSearch(View focused, int direction) {
-        if (isRootNamespace()) {
+        // If we are moving accessibility focus we want to consider all
+        // views no matter if they are on the screen. It is responsibility
+        // of the accessibility service to check whether the result is in
+        // the screen.
+        if (isRootNamespace() && (direction & FOCUS_ACCESSIBILITY) == 0) {
             // root namespace means we should consider ourselves the top of the
             // tree for focus searching; otherwise we could be focus searching
             // into other tabs.  see LocalActivityManager and TabHost for more info
@@ -857,20 +861,13 @@
      * {@inheritDoc}
      */
     @Override
-    public void addFocusables(ArrayList<View> views, int direction) {
-        addFocusables(views, direction, FOCUSABLES_TOUCH_MODE);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
     public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
         final int focusableCount = views.size();
 
         final int descendantFocusability = getDescendantFocusability();
 
-        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
+        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS
+                || (focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) {
             final int count = mChildrenCount;
             final View[] children = mChildren;
 
@@ -886,10 +883,11 @@
         // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
         // to avoid the focus search finding layouts when a more precise search
         // among the focusable children would be more interesting.
-        if (
-            descendantFocusability != FOCUS_AFTER_DESCENDANTS ||
+        if (descendantFocusability != FOCUS_AFTER_DESCENDANTS
                 // No focusable descendants
-                (focusableCount == views.size())) {
+                || (focusableCount == views.size())
+                // We are collecting accessibility focusables.
+                || (focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY) {
             super.addFocusables(views, direction, focusableMode);
         }
     }
@@ -1659,6 +1657,20 @@
     }
 
     /**
+     * @hide
+     */
+    @Override
+    public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant) {
+        if (includeForAccessibility() && isActionableForAccessibility()) {
+            return this;
+        }
+        if (mParent != null) {
+            return mParent.findViewToTakeAccessibilityFocusFromHover(this, descendant);
+        }
+        return null;
+    }
+
+    /**
      * Implement this method to intercept hover events before they are handled
      * by child views.
      * <p>
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index ddff91d..d93b996 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -295,4 +295,16 @@
      * @hide
      */
     public void childAccessibilityStateChanged(View child);
+
+    /**
+     * A descendant requests this view to find a candidate to take accessibility
+     * focus from hover.
+     *
+     * @param child The child making the call.
+     * @param descendant The descendant that made the initial request.
+     * @return A view to take accessibility focus.
+     *
+     * @hide
+     */
+    public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant);
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 90e6034..f86e036 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2325,6 +2325,14 @@
         return true;
     }
 
+    @Override
+    public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant) {
+        if (descendant.includeForAccessibility()) {
+            return descendant;
+        }
+        return null;
+    }
+
     /**
      * We want to draw a highlight around the current accessibility focused.
      * Since adding a style for all possible view is not a viable option we
@@ -2520,6 +2528,20 @@
         return handled;
     }
 
+    /**
+     * @hide
+     */
+    public View getAccessibilityFocusedHost() {
+        return mAccessibilityFocusedHost;
+    }
+
+    /**
+     * @hide
+     */
+    public AccessibilityNodeInfo getAccessibilityFocusedVirtualView() {
+        return mAccessibilityFocusedVirtualView;
+    }
+
     void setAccessibilityFocusedHost(View host) {
         if (mAccessibilityFocusedHost != null && mAccessibilityFocusedVirtualView == null) {
             mAccessibilityFocusedHost.clearAccessibilityFocusNoCallbacks();
@@ -2672,7 +2694,7 @@
     /**
      * Return true if child is an ancestor of parent, (or equal to the parent).
      */
-    static boolean isViewDescendantOf(View child, View parent) {
+    public static boolean isViewDescendantOf(View child, View parent) {
         if (child == parent) {
             return true;
         }
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 3b4ec7d..ab9d370 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -42,6 +42,7 @@
 import android.util.StateSet;
 import android.view.ActionMode;
 import android.view.ContextMenu.ContextMenuInfo;
+import android.view.FocusFinder;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
 import android.view.InputDevice;
@@ -56,6 +57,7 @@
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 import android.view.ViewParent;
+import android.view.ViewRootImpl;
 import android.view.ViewTreeObserver;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -1327,6 +1329,119 @@
     }
 
     @Override
+    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+        if ((focusableMode & FOCUSABLES_ACCESSIBILITY) == FOCUSABLES_ACCESSIBILITY
+                && (direction == ACCESSIBILITY_FOCUS_FORWARD
+                        || direction == ACCESSIBILITY_FOCUS_BACKWARD)) {
+            if (canTakeAccessibilityFocusFromHover()) {
+                views.add(this);
+            }
+        } else {
+            super.addFocusables(views, direction, focusableMode);
+        }
+    }
+
+    @Override
+    public View focusSearch(int direction) {
+        return focusSearch(null, direction);
+    }
+
+    @Override
+    public View focusSearch(View focused, int direction) {
+        switch (direction) {
+            case ACCESSIBILITY_FOCUS_FORWARD: {
+                ViewRootImpl viewRootImpl = getViewRootImpl();
+                if (viewRootImpl == null) {
+                    break;
+                }
+                View currentFocus = viewRootImpl.getAccessibilityFocusedHost();
+                if (currentFocus == null) {
+                    break;
+                }
+                // If we have the focus try giving it to the first child.
+                if (currentFocus == this) {
+                    final int firstVisiblePosition = getFirstVisiblePosition();
+                    if (firstVisiblePosition >= 0) {
+                        return getChildAt(0);
+                    }
+                    return null;
+                }
+                // Find the item that has accessibility focus.
+                final int currentPosition = getPositionForView(currentFocus);
+                if (currentPosition < 0 || currentPosition >= getCount()) {
+                    break;
+                }
+                // Try to advance focus in the current item.
+                View currentItem = getChildAt(currentPosition - getFirstVisiblePosition());
+                if (currentItem instanceof ViewGroup) {
+                    ViewGroup currentItemGroup = (ViewGroup) currentItem;
+                    View nextFocus = FocusFinder.getInstance().findNextFocus(currentItemGroup,
+                                currentFocus, direction);
+                    if (nextFocus != null && nextFocus != currentItemGroup
+                            && nextFocus != currentFocus) {
+                        return nextFocus;
+                    }
+                }
+                // Try to move focus to the next item.
+                final int nextPosition = currentPosition - getFirstVisiblePosition() + 1;
+                if (nextPosition < getChildCount()) {
+                    return getChildAt(nextPosition);
+                }
+            } break;
+            case ACCESSIBILITY_FOCUS_BACKWARD: {
+                ViewRootImpl viewRootImpl = getViewRootImpl();
+                if (viewRootImpl == null) {
+                    break;
+                }
+                View currentFocus = viewRootImpl.getAccessibilityFocusedHost();
+                if (currentFocus == null) {
+                    break;
+                }
+                // If we have the focus do a generic search.
+                if (currentFocus == this) {
+                    return super.focusSearch(this, direction);
+                }
+                // Find the item that has accessibility focus.
+                final int currentPosition = getPositionForView(currentFocus);
+                if (currentPosition < 0 || currentPosition >= getCount()) {
+                    break;
+                }
+                // Try to advance focus in the current item.
+                View currentItem = getChildAt(currentPosition - getFirstVisiblePosition());
+                if (currentItem instanceof ViewGroup) {
+                    ViewGroup currentItemGroup = (ViewGroup) currentItem;
+                    View nextFocus = FocusFinder.getInstance().findNextFocus(currentItemGroup,
+                                currentFocus, direction);
+                    if (nextFocus != null && nextFocus != currentItemGroup
+                            && nextFocus != currentFocus) {
+                        return nextFocus;
+                    }
+                }
+                // Try to move focus to the previous item.
+                final int nextPosition = currentPosition - getFirstVisiblePosition() - 1;
+                if (nextPosition >= 0) {
+                    return getChildAt(nextPosition);
+                } else {
+                    return this;
+                }
+            }
+        }
+        return super.focusSearch(focused, direction);
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public View findViewToTakeAccessibilityFocusFromHover(View child, View descendant) {
+        final int position = getPositionForView(child);
+        if (position != INVALID_POSITION) {
+            return getChildAt(position - mFirstPosition);
+        }
+        return super.findViewToTakeAccessibilityFocusFromHover(child, descendant);
+    }
+
+    @Override
     public void sendAccessibilityEvent(int eventType) {
         // Since this class calls onScrollChanged even if the mFirstPosition and the
         // child count have not changed we will avoid sending duplicate accessibility
diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java
index abfc577..502de31 100644
--- a/core/java/android/widget/AdapterView.java
+++ b/core/java/android/widget/AdapterView.java
@@ -24,7 +24,6 @@
 import android.util.SparseArray;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
-import android.view.MotionEvent;
 import android.view.SoundEffectConstants;
 import android.view.View;
 import android.view.ViewDebug;
@@ -32,6 +31,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
 
 /**
  * An AdapterView is a view whose children are determined by an {@link Adapter}.
@@ -957,24 +957,6 @@
         event.setItemCount(getCount());
     }
 
-    /**
-     * @hide
-     */
-    @Override
-    public boolean onRequestAccessibilityFocusFromHover(float x, float y) {
-        // We prefer to five focus to the child instead of this view.
-        // Usually the children are not actionable for accessibility,
-        // and they will not take accessibility focus, so we give it.
-        final int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            if (isTransformedTouchPointInView(x, y, child, null)) {
-                return child.requestAccessibilityFocus();
-            }
-        }
-        return super.onRequestAccessibilityFocusFromHover(x, y);
-    }
-
     private boolean isScrollableForAccessibility() {
         T adapter = getAdapter();
         if (adapter != null) {