Support a non Chassis FocusParkingView in RotaryService

This is so that 3P apps Signin and Settings pages will be rotary
compliant before using Chassis.

Bug: 171925388
Test: manual (use fake FocusParkingView in KitchenSink)
Test: atest CarRotaryControllerRoboTests
Change-Id: Id0e1a0478c23a3a9dc5a81b1b7c29b6d94ca7c3b
diff --git a/src/com/android/car/rotary/RotaryService.java b/src/com/android/car/rotary/RotaryService.java
index deee6f8..2fe740c 100644
--- a/src/com/android/car/rotary/RotaryService.java
+++ b/src/com/android/car/rotary/RotaryService.java
@@ -951,8 +951,29 @@
             L.e("No FocusParkingView in the window containing " + node);
             return false;
         }
-        boolean result = fpv.performAction(ACTION_RESTORE_DEFAULT_FOCUS);
+
+        if (Utils.isCarUiFocusParkingView(fpv)) {
+            boolean result = fpv.performAction(ACTION_RESTORE_DEFAULT_FOCUS);
+            fpv.recycle();
+            return result;
+        }
+
+        AccessibilityWindowInfo w = fpv.getWindow();
         fpv.recycle();
+        if (w == null) {
+            L.e("No window found for the generic FocusParkingView");
+            return false;
+        }
+        AccessibilityNodeInfo root = w.getRoot();
+        w.recycle();
+        AccessibilityNodeInfo firstFocusable = mNavigator.findFirstFocusableDescendant(root);
+        root.recycle();
+        if (firstFocusable == null) {
+            L.e("No focusable element in the window containing the generic FocusParkingView");
+            return false;
+        }
+        boolean result = firstFocusable.performAction(ACTION_FOCUS);
+        firstFocusable.recycle();
         return result;
     }
 
diff --git a/src/com/android/car/rotary/Utils.java b/src/com/android/car/rotary/Utils.java
index 39e3308..9289ba1 100644
--- a/src/com/android/car/rotary/Utils.java
+++ b/src/com/android/car/rotary/Utils.java
@@ -16,11 +16,11 @@
 
 package com.android.car.rotary;
 
-import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
 import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
 import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
 
@@ -55,6 +55,10 @@
     static final String FOCUS_AREA_CLASS_NAME = FocusArea.class.getName();
     @VisibleForTesting
     static final String FOCUS_PARKING_VIEW_CLASS_NAME = FocusParkingView.class.getName();
+    @VisibleForTesting
+    static final String GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME =
+            "com.android.car.rotary.FocusParkingView";
+
     private static final String WEB_VIEW_CLASS_NAME = WebView.class.getName();
 
     private Utils() {
@@ -177,12 +181,28 @@
         return false;
     }
 
-    /** Returns whether the given {@code node} represents a {@link FocusParkingView}. */
+    /**
+     * Returns whether the given {@code node} represents a car ui lib {@link FocusParkingView} or a
+     * generic FocusParkingView.
+     */
     static boolean isFocusParkingView(@NonNull AccessibilityNodeInfo node) {
+        return isCarUiFocusParkingView(node) || isGenericFocusParkingView(node);
+    }
+    /** Returns whether the given {@code node} represents a car ui lib {@link FocusParkingView}. */
+    static boolean isCarUiFocusParkingView(@NonNull AccessibilityNodeInfo node) {
         CharSequence className = node.getClassName();
         return className != null && FOCUS_PARKING_VIEW_CLASS_NAME.contentEquals(className);
     }
 
+    /**
+     * Returns whether the given {@code node} represents a generic FocusParkingView (primarily used
+     * as a fallback for potential apps that are not using Chassis).
+     */
+    static boolean isGenericFocusParkingView(@NonNull AccessibilityNodeInfo node) {
+        CharSequence className = node.getClassName();
+        return className != null && GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME.contentEquals(className);
+    }
+
     /** Returns whether the given {@code node} represents a {@link FocusArea}. */
     static boolean isFocusArea(@NonNull AccessibilityNodeInfo node) {
         CharSequence className = node.getClassName();
diff --git a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
index 73a84ba..278c9f0 100644
--- a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
+++ b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
@@ -137,6 +137,38 @@
     /**
      * Tests {@link Navigator#findRotateTarget} in the following node tree:
      * <pre>
+     *                     root
+     *                    /    \
+     *                   /      \
+     *           focusArea  genericFocusParkingView
+     *            /    \
+     *           /      \
+     *       button1  button2
+     * </pre>
+     */
+    @Test
+    public void testFindRotateTargetNoWrapAroundWithGenericFpv() {
+        AccessibilityNodeInfo root = mNodeBuilder.build();
+        AccessibilityNodeInfo focusArea = mNodeBuilder.setParent(root).setFocusArea().build();
+        AccessibilityNodeInfo button1 = mNodeBuilder.setParent(focusArea).build();
+        AccessibilityNodeInfo button2 = mNodeBuilder.setParent(focusArea).build();
+
+        AccessibilityNodeInfo focusParkingView = mNodeBuilder.setParent(
+                root).setGenericFpv().build();
+
+        int direction = View.FOCUS_FORWARD;
+        when(button1.focusSearch(direction)).thenReturn(button2);
+        when(button2.focusSearch(direction)).thenReturn(focusParkingView);
+        when(focusParkingView.focusSearch(direction)).thenReturn(button1);
+
+        // Rotate at the end of focus area, no wrap-around should happen.
+        FindRotateTargetResult target = mNavigator.findRotateTarget(button2, direction, 1);
+        assertThat(target).isNull();
+    }
+
+    /**
+     * Tests {@link Navigator#findRotateTarget} in the following node tree:
+     * <pre>
      *                          root
      *                         /  |  \
      *                       /    |    \
@@ -168,6 +200,34 @@
      *                         /  |  \
      *                       /    |    \
      *                     /      |      \
+     *              button1   button2  genericFocusParkingView
+     * </pre>
+     */
+    @Test
+    public void testFindRotateTargetNoWrapAround2WithGenericFpv() {
+        AccessibilityNodeInfo root = mNodeBuilder.build();
+        AccessibilityNodeInfo button1 = mNodeBuilder.setParent(root).build();
+        AccessibilityNodeInfo button2 = mNodeBuilder.setParent(root).build();
+        AccessibilityNodeInfo focusParkingView = mNodeBuilder.setParent(
+                root).setGenericFpv().build();
+
+        int direction = View.FOCUS_FORWARD;
+        when(button1.focusSearch(direction)).thenReturn(button2);
+        when(button2.focusSearch(direction)).thenReturn(focusParkingView);
+        when(focusParkingView.focusSearch(direction)).thenReturn(button1);
+
+        // Rotate at the end of focus area, no wrap-around should happen.
+        FindRotateTargetResult target = mNavigator.findRotateTarget(button2, direction, 1);
+        assertThat(target).isNull();
+    }
+
+    /**
+     * Tests {@link Navigator#findRotateTarget} in the following node tree:
+     * <pre>
+     *                          root
+     *                         /  |  \
+     *                       /    |    \
+     *                     /      |      \
      *               button1   invisible   button2
      * </pre>
      */
diff --git a/tests/robotests/src/com/android/car/rotary/NodeBuilder.java b/tests/robotests/src/com/android/car/rotary/NodeBuilder.java
index 7a54118..09b26b2 100644
--- a/tests/robotests/src/com/android/car/rotary/NodeBuilder.java
+++ b/tests/robotests/src/com/android/car/rotary/NodeBuilder.java
@@ -19,6 +19,7 @@
 
 import static com.android.car.rotary.Utils.FOCUS_AREA_CLASS_NAME;
 import static com.android.car.rotary.Utils.FOCUS_PARKING_VIEW_CLASS_NAME;
+import static com.android.car.rotary.Utils.GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
@@ -244,6 +245,10 @@
         return setClassName(FOCUS_PARKING_VIEW_CLASS_NAME);
     }
 
+    NodeBuilder setGenericFpv() {
+        return setClassName(GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME);
+    }
+
     NodeBuilder setScrollableContainer() {
         return setContentDescription(ROTARY_VERTICALLY_SCROLLABLE);
     }
diff --git a/tests/robotests/src/com/android/car/rotary/NodeBuilderTest.java b/tests/robotests/src/com/android/car/rotary/NodeBuilderTest.java
index af7da8e..ed8dfad 100644
--- a/tests/robotests/src/com/android/car/rotary/NodeBuilderTest.java
+++ b/tests/robotests/src/com/android/car/rotary/NodeBuilderTest.java
@@ -19,6 +19,7 @@
 
 import static com.android.car.rotary.Utils.FOCUS_AREA_CLASS_NAME;
 import static com.android.car.rotary.Utils.FOCUS_PARKING_VIEW_CLASS_NAME;
+import static com.android.car.rotary.Utils.GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
@@ -186,6 +187,12 @@
     }
 
     @Test
+    public void testSetGenericFpv() {
+        AccessibilityNodeInfo node = mNodeBuilder.setGenericFpv().build();
+        assertThat(node.getClassName().toString()).isEqualTo(GENERIC_FOCUS_PARKING_VIEW_CLASS_NAME);
+    }
+
+    @Test
     public void testSetScrollableContainer() {
         AccessibilityNodeInfo node = mNodeBuilder.setScrollableContainer().build();
         assertThat(node.getContentDescription().toString()).isEqualTo(ROTARY_VERTICALLY_SCROLLABLE);