Update ExploreByTouchHelper to calculate bounds on screen properly

Previously, the bounds in parent were assumed to be relative to the host view.
Now it properly handles calculation of the bounds of nested views.

Bug: 33196811
Test: Try the ExploreByTouchHelper sample in Support4Demos
Test: Run the ExploreByTouchHelperTest

Change-Id: Icf007e33f519281f4a78b4f5b46017b6a121e17f
diff --git a/compat/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java b/compat/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
index 0f0df96..0b00094 100644
--- a/compat/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/compat/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -16,10 +16,13 @@
 
 package android.support.v4.view.accessibility;
 
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
 import android.support.v4.accessibilityservice.AccessibilityServiceInfoCompat;
 import android.support.v4.view.ViewCompat;
 import android.text.InputType;
@@ -2365,6 +2368,14 @@
 
     private final Object mInfo;
 
+    /**
+     *  android.support.v4.widget.ExploreByTouchHelper.HOST_ID = -1;
+     *
+     *  @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public int mParentVirtualDescendantId = -1;
+
     // Actions introduced in IceCreamSandwich
 
     /**
@@ -3171,6 +3182,7 @@
      * @param virtualDescendantId The id of the virtual descendant.
      */
     public void setParent(View root, int virtualDescendantId) {
+        mParentVirtualDescendantId = virtualDescendantId;
         IMPL.setParent(mInfo, root, virtualDescendantId);
     }
 
diff --git a/core-ui/java/android/support/v4/widget/ExploreByTouchHelper.java b/core-ui/java/android/support/v4/widget/ExploreByTouchHelper.java
index 1b21bc2..526575c 100644
--- a/core-ui/java/android/support/v4/widget/ExploreByTouchHelper.java
+++ b/core-ui/java/android/support/v4/widget/ExploreByTouchHelper.java
@@ -847,21 +847,45 @@
         }
         node.setFocused(isFocused);
 
-        // Set the visibility based on the parent bound.
-        if (intersectVisibleToUser(mTempParentRect)) {
-            node.setVisibleToUser(true);
-            node.setBoundsInParent(mTempParentRect);
-        }
+        mHost.getLocationOnScreen(mTempGlobalRect);
 
         // If not explicitly specified, calculate screen-relative bounds and
         // offset for scroll position based on bounds in parent.
         node.getBoundsInScreen(mTempScreenRect);
         if (mTempScreenRect.equals(INVALID_PARENT_BOUNDS)) {
-            mHost.getLocationOnScreen(mTempGlobalRect);
             node.getBoundsInParent(mTempScreenRect);
+
+            // If there is a parent node, adjust bounds based on the parent node.
+            if (node.mParentVirtualDescendantId != HOST_ID) {
+                AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
+                // Walk up the node tree to adjust the screen rect.
+                for (int virtualDescendantId = node.mParentVirtualDescendantId;
+                        virtualDescendantId != HOST_ID;
+                        virtualDescendantId = parentNode.mParentVirtualDescendantId) {
+                    // Reset the values in the parent node we'll be using.
+                    parentNode.setParent(mHost, HOST_ID);
+                    parentNode.setBoundsInParent(INVALID_PARENT_BOUNDS);
+                    // Adjust the bounds for the parent node.
+                    onPopulateNodeForVirtualView(virtualDescendantId, parentNode);
+                    parentNode.getBoundsInParent(mTempParentRect);
+                    mTempScreenRect.offset(mTempParentRect.left, mTempParentRect.top);
+                }
+                parentNode.recycle();
+            }
+            // Adjust the rect for the host view's location.
             mTempScreenRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
                     mTempGlobalRect[1] - mHost.getScrollY());
+        }
+
+        if (mHost.getLocalVisibleRect(mTempVisibleRect)) {
+            mTempVisibleRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
+                    mTempGlobalRect[1] - mHost.getScrollY());
+            mTempScreenRect.intersect(mTempVisibleRect);
             node.setBoundsInScreen(mTempScreenRect);
+
+            if (isVisibleToUser(mTempScreenRect)) {
+                node.setVisibleToUser(true);
+            }
         }
 
         return node;
@@ -903,7 +927,7 @@
      * @param localRect a rectangle in local (parent) coordinates
      * @return whether the specified {@link Rect} is visible on the screen
      */
-    private boolean intersectVisibleToUser(Rect localRect) {
+    private boolean isVisibleToUser(Rect localRect) {
         // Missing or empty bounds mean this view is not visible.
         if ((localRect == null) || localRect.isEmpty()) {
             return false;
@@ -925,17 +949,7 @@
         }
 
         // A null parent implies the view is not visible.
-        if (viewParent == null) {
-            return false;
-        }
-
-        // If no portion of the parent is visible, this view is not visible.
-        if (!mHost.getLocalVisibleRect(mTempVisibleRect)) {
-            return false;
-        }
-
-        // Check if the view intersects the visible portion of the parent.
-        return localRect.intersect(mTempVisibleRect);
+        return viewParent != null;
     }
 
     /**
diff --git a/core-ui/tests/java/android/support/v4/widget/ExploreByTouchHelperTest.java b/core-ui/tests/java/android/support/v4/widget/ExploreByTouchHelperTest.java
index b875c37..15260c2 100644
--- a/core-ui/tests/java/android/support/v4/widget/ExploreByTouchHelperTest.java
+++ b/core-ui/tests/java/android/support/v4/widget/ExploreByTouchHelperTest.java
@@ -85,8 +85,7 @@
                 helper.getAccessibilityNodeProvider(mHost).createAccessibilityNodeInfo(1);
         assertNotNull(scrolledNode);
 
-        mHost.getLocalVisibleRect(hostBounds);
-        hostBounds.intersect(nodeBoundsInParent);
+        // Bounds in parent should not be affected by visibility.
         final Rect scrolledNodeBoundsInParent = new Rect();
         scrolledNode.getBoundsInParent(scrolledNodeBoundsInParent);
         assertEquals("Wrong bounds in parent after scrolling",
diff --git a/samples/Support4Demos/res/values/strings.xml b/samples/Support4Demos/res/values/strings.xml
index 2d4f0db..24d88d6 100644
--- a/samples/Support4Demos/res/values/strings.xml
+++ b/samples/Support4Demos/res/values/strings.xml
@@ -215,6 +215,7 @@
     <string name="sample_item_a">Sample item A</string>
     <string name="sample_item_b">Sample item B</string>
     <string name="sample_item_c">Sample item C</string>
+    <string name="sample_item_d">Sample item D</string>
 
     <!-- ContentLoadingProgressBar -->
     <string name="content_loading_progress_bar">Widget/Content Loading Progress Bar</string>
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/widget/ExploreByTouchHelperActivity.java b/samples/Support4Demos/src/com/example/android/supportv4/widget/ExploreByTouchHelperActivity.java
index 3f7675c..10db8f3 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/widget/ExploreByTouchHelperActivity.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/widget/ExploreByTouchHelperActivity.java
@@ -86,6 +86,13 @@
         CustomView.CustomItem itemC =
                 customView.addItem(getString(R.string.sample_item_c), 0, 0.75f, 1, 1);
         customView.setParentItem(itemC, itemB);
+
+        // Add an item at the left quarter of Item C.
+        CustomView.CustomItem itemD =
+                customView.addItem(getString(R.string.sample_item_d), 0, 0f, 0.25f, 1);
+        customView.setParentItem(itemD, itemC);
+
+        customView.layoutItems();
     }
 
     /**
@@ -169,12 +176,28 @@
         public void setParentItem(CustomItem item, CustomItem parent) {
             item.mParent = parent;
             parent.mChildren.add(item.mId);
-            RectF bounds = item.mBounds;
-            item.mBounds = new RectF(parent.mBounds.left + bounds.left * parent.mBounds.width(),
-                    parent.mBounds.top + bounds.top * parent.mBounds.height(),
-                    parent.mBounds.left + bounds.right * parent.mBounds.width(),
-                    parent.mBounds.top + bounds.bottom * parent.mBounds.height());
+        }
 
+        /**
+         * Walk the view hierarchy of each item and calculate mBoundsInRoot.
+         */
+        public void layoutItems() {
+            for (CustomItem item : mItems) {
+                layoutItem(item);
+            }
+        }
+
+        void layoutItem(CustomItem item) {
+            item.mBoundsInRoot = new RectF(item.mBounds);
+            CustomItem parent = item.mParent;
+            while (parent != null) {
+                RectF bounds = item.mBoundsInRoot;
+                item.mBoundsInRoot.set(parent.mBounds.left + bounds.left * parent.mBounds.width(),
+                        parent.mBounds.top + bounds.top * parent.mBounds.height(),
+                        parent.mBounds.left + bounds.right * parent.mBounds.width(),
+                        parent.mBounds.top + bounds.bottom * parent.mBounds.height());
+                parent = parent.mParent;
+            }
         }
 
         @Override
@@ -193,7 +216,7 @@
                     paint.setColor(item.mChecked ? Color.MAGENTA : Color.GREEN);
                 }
                 paint.setStyle(Style.FILL);
-                scaleRectF(item.mBounds, bounds, width, height);
+                scaleRectF(item.mBoundsInRoot, bounds, width, height);
                 canvas.drawRect(bounds, paint);
                 paint.setColor(Color.WHITE);
                 paint.setTextAlign(Align.CENTER);
@@ -232,7 +255,7 @@
             // Search in reverse order, so that topmost items are selected first.
             for (int i = n - 1; i >= 0; i--) {
                 final CustomItem item = mItems.get(i);
-                if (item.mBounds.contains(scaledX, scaledY)) {
+                if (item.mBoundsInRoot.contains(scaledX, scaledY)) {
                     return i;
                 }
             }
@@ -321,8 +344,12 @@
                 // hit detection performed in getVirtualViewAt() and
                 // onTouchEvent().
                 final Rect bounds = mTempRect;
-                final int height = getHeight();
-                final int width = getWidth();
+                int height = getHeight();
+                int width = getWidth();
+                if (item.mParent != null) {
+                    width = (int) (width * item.mParent.mBoundsInRoot.width());
+                    height = (int) (height * item.mParent.mBoundsInRoot.height());
+                }
                 scaleRectF(item.mBounds, bounds, width, height);
                 node.setBoundsInParent(bounds);
 
@@ -365,6 +392,7 @@
             private List<Integer> mChildren = new ArrayList<>();
             private String mDescription;
             private RectF mBounds;
+            private RectF mBoundsInRoot;
             private boolean mChecked;
         }
     }