Merge "Skip empty nodes when finding next focusable" into rvc-qpr-dev am: 720cf3c088

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Car/RotaryController/+/12551163

Change-Id: I5ffd7576651828dde63e87e65276f1f08b7dc319
diff --git a/src/com/android/car/rotary/Navigator.java b/src/com/android/car/rotary/Navigator.java
index 3219ddc..4c6b4e1 100644
--- a/src/com/android/car/rotary/Navigator.java
+++ b/src/com/android/car/rotary/Navigator.java
@@ -676,10 +676,11 @@
     }
 
     /**
-     * Returns the previous node before {@code referenceNode} in Tab order or the next node after
-     * {@code referenceNode} in Tab order, depending on {@code direction}. The search is limited to
-     * descendants of {@code containerNode}. Returns null if there are no focusable descendants in
-     * the given direction. The caller is responsible for recycling the result.
+     * Returns the previous node  before {@code referenceNode} in Tab order that can take focus or
+     * the next node after {@code referenceNode} in Tab order that can take focus, depending on
+     * {@code direction}. The search is limited to descendants of {@code containerNode}. Returns
+     * null if there are no descendants that can take focus in the given direction. The caller is
+     * responsible for recycling the result.
      *
      * @param containerNode the node with descendants
      * @param referenceNode a descendant of {@code containerNode} to start from
@@ -687,24 +688,30 @@
      * @return the node before or after {@code referenceNode} or null if none
      */
     @Nullable
-    static AccessibilityNodeInfo findFocusableDescendantInDirection(
+    AccessibilityNodeInfo findFocusableDescendantInDirection(
             @NonNull AccessibilityNodeInfo containerNode,
             @NonNull AccessibilityNodeInfo referenceNode,
             int direction) {
-        AccessibilityNodeInfo targetNode = referenceNode.focusSearch(direction);
-        if (targetNode == null
-                || targetNode.equals(containerNode)
-                || !Utils.canTakeFocus(targetNode)
-                || !Utils.isDescendant(containerNode, targetNode)) {
-            Utils.recycleNode(targetNode);
-            return null;
-        }
-        if (targetNode.equals(referenceNode)) {
-            L.w((direction == View.FOCUS_FORWARD ? "Next" : "Previous")
-                    + " node is the same node: " + referenceNode);
-            Utils.recycleNode(targetNode);
-            return null;
-        }
+        AccessibilityNodeInfo targetNode = copyNode(referenceNode);
+        do {
+            AccessibilityNodeInfo nextTargetNode = targetNode.focusSearch(direction);
+            if (nextTargetNode == null
+                    || nextTargetNode.equals(containerNode)
+                    || !Utils.isDescendant(containerNode, nextTargetNode)) {
+                Utils.recycleNode(nextTargetNode);
+                Utils.recycleNode(targetNode);
+                return null;
+            }
+            if (nextTargetNode.equals(referenceNode) || nextTargetNode.equals(targetNode)) {
+                L.w((direction == View.FOCUS_FORWARD ? "Next" : "Previous")
+                        + " node is the same node: " + referenceNode);
+                Utils.recycleNode(nextTargetNode);
+                Utils.recycleNode(targetNode);
+                return null;
+            }
+            targetNode.recycle();
+            targetNode = nextTargetNode;
+        } while (!Utils.canTakeFocus(targetNode));
         return targetNode;
     }
 
diff --git a/src/com/android/car/rotary/RotaryService.java b/src/com/android/car/rotary/RotaryService.java
index 89cb097..9fd5d14 100644
--- a/src/com/android/car/rotary/RotaryService.java
+++ b/src/com/android/car/rotary/RotaryService.java
@@ -907,7 +907,7 @@
                 if (mFocusedNode.equals(sourceNode)) {
                     break;
                 }
-                AccessibilityNodeInfo target = Navigator.findFocusableDescendantInDirection(
+                AccessibilityNodeInfo target = mNavigator.findFocusableDescendantInDirection(
                         sourceNode, mFocusedNode,
                         mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS
                                 ? View.FOCUS_BACKWARD
diff --git a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
index 4d5d76b..75bc6c3 100644
--- a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
+++ b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
@@ -2047,6 +2047,50 @@
     }
 
     /**
+     * Tests {@link Navigator#findNextFocusableDescendant} in the following node tree:
+     * <pre>
+     *                     root
+     *                      |
+     *                      |
+     *                  container
+     *               /    /   \   \
+     *            /      /     \      \
+     *     button1  button2  button3  button4
+     * </pre>
+     * where {@code button3} and {@code button4} have empty bounds.
+     */
+    @Test
+    public void testFindNextFocusableDescendantWithEmptyBounds() {
+        AccessibilityNodeInfo root = mNodeBuilder.build();
+        AccessibilityNodeInfo container = mNodeBuilder.setParent(root).build();
+        AccessibilityNodeInfo button1 = mNodeBuilder.setParent(container).build();
+        AccessibilityNodeInfo button2 = mNodeBuilder.setParent(container).build();
+        AccessibilityNodeInfo button3 = mNodeBuilder.setParent(container)
+                .setBoundsInScreen(new Rect(5, 10, 5, 10)).build();
+        AccessibilityNodeInfo button4 = mNodeBuilder.setParent(container)
+                .setBoundsInScreen(new Rect(20, 40, 20, 40)).build();
+
+        int direction = View.FOCUS_FORWARD;
+        when(button1.focusSearch(direction)).thenReturn(button2);
+        when(button2.focusSearch(direction)).thenReturn(button3);
+        when(button3.focusSearch(direction)).thenReturn(button4);
+        when(button4.focusSearch(direction)).thenReturn(button1);
+
+        AccessibilityNodeInfo target = mNavigator.findFocusableDescendantInDirection(container,
+                button1, View.FOCUS_FORWARD);
+        assertThat(target).isSameAs(button2);
+        target = mNavigator.findFocusableDescendantInDirection(container, button2,
+                View.FOCUS_FORWARD);
+        assertThat(target).isSameAs(button1);
+        target = mNavigator.findFocusableDescendantInDirection(container, button3,
+                View.FOCUS_FORWARD);
+        assertThat(target).isSameAs(button1);
+        target = mNavigator.findFocusableDescendantInDirection(container, button4,
+                View.FOCUS_FORWARD);
+        assertThat(target).isSameAs(button1);
+    }
+
+    /**
      * Tests {@link Navigator#findFirstFocusableDescendant} in the following node tree:
      * <pre>
      *                     root