Introduce rotary container

A rotary container contains focusable elements. When initializing
focus, the first element in the rotary container is prioritized
to take focus. When searching for nudge target, the  bounds of
the rotary container is the minimum bounds containing its descendants.

Fixes: 171737334
Test: atest CarUILibUnitTests
Change-Id: I8db82b85f351f24facbd0ef08626397fb9ca4dd3
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
index dce68c5..8ae6de7 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusParkingViewTest.java
@@ -151,7 +151,7 @@
 
                         ViewGroup parent = (ViewGroup) firstItem.getParent();
                         parent.removeView(firstItem);
-                        assertThat(mList.isFocused()).isTrue();
+                        assertThat(mFocusedByDefault.isFocused()).isTrue();
                     }
                 }));
     }
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
index ea7c855..06ff9fd 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
@@ -113,6 +113,7 @@
                     @Override
                     public void onGlobalLayout() {
                         mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        CarUiUtils.setRotaryScrollEnabled(mList5, /* isVertical= */ true);
                         View firstItem = mList5.getLayoutManager().findViewByPosition(0);
                         assertThat(ViewUtils.getAncestorScrollableContainer(firstItem))
                                 .isEqualTo(mList5);
@@ -360,12 +361,25 @@
     }
 
     @Test
+    public void testRequestFocus_rotaryContainer() {
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        assertRequestFocus(mList5, false);
+                    }
+                }));
+    }
+
+    @Test
     public void testRequestFocus_scrollableContainer() {
         mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
                 new ViewTreeObserver.OnGlobalLayoutListener() {
                     @Override
                     public void onGlobalLayout() {
                         mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        CarUiUtils.setRotaryScrollEnabled(mList5, /* isVertical= */ true);
                         assertRequestFocus(mList5, false);
                     }
                 }));
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
index d58cba2..02d7326 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_parking_view_test_activity.xml
@@ -41,7 +41,5 @@
     <com.android.car.ui.recyclerview.CarUiRecyclerView
         android:id="@+id/list"
         android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:contentDescription="com.android.car.ui.utils.VERTICALLY_SCROLLABLE"/>
-    <!-- TODO(b/171339427) Use app:rotaryScrollEnabled instead of contentDescription. -->
+        android:layout_height="wrap_content"/>
 </LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
index f5b9028..558dd9f 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
@@ -91,8 +91,6 @@
         <com.android.car.ui.recyclerview.CarUiRecyclerView
             android:id="@+id/list5"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="com.android.car.ui.utils.VERTICALLY_SCROLLABLE"/>
-        <!-- TODO(b/171339427) Use app:rotaryScrollEnabled instead of contentDescription. -->
+            android:layout_height="wrap_content"/>
     </com.android.car.ui.FocusArea>
 </LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index b755213..5fd6ed4 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -16,6 +16,7 @@
 package com.android.car.ui.recyclerview;
 
 import static com.android.car.ui.utils.CarUiUtils.findViewByRefId;
+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;
 
@@ -318,6 +319,11 @@
         // Disable the default focus highlight. No highlight should appear when this view is
         // focused.
         setDefaultFocusHighlightEnabled(false);
+
+        // This view is a rotary container if it's not a scrollable container.
+        if (!rotaryScrollEnabled) {
+            super.setContentDescription(ROTARY_CONTAINER);
+        }
     }
 
     @Override
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
index 1f1de0a..aaaae6c 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/RotaryConstants.java
@@ -23,15 +23,34 @@
  */
 public final class RotaryConstants {
     /**
-     * Content description indicating that the rotary controller should scroll this view
-     * horizontally.
+     * Content description indicating that the view is a rotary container.
+     * <p>
+     * A rotary container contains focusable elements. When initializing focus, the first element
+     * in the rotary container is prioritized to take focus. When searching for nudge target, the
+     * bounds of the rotary container is the minimum bounds containing its descendants.
+     * <p>
+     * A rotary container shouldn't be focusable unless it's a scrollable container. Though it
+     * can't be focused, it can be scrolled as a side-effect of moving the focus within it.
+     */
+    public static final String ROTARY_CONTAINER =
+            "com.android.car.ui.utils.ROTARY_CONTAINER";
+
+    /**
+     * Content description indicating that the view is a scrollable container and can be scrolled
+     * horizontally by the rotary controller.
+     * <p>
+     * A scrollable container is a focusable rotary container. When it's focused, it can be scrolled
+     * when the rotary controller rotates. A scrollable container is often used to show long text.
      */
     public static final String ROTARY_HORIZONTALLY_SCROLLABLE =
             "com.android.car.ui.utils.HORIZONTALLY_SCROLLABLE";
 
     /**
-     * Content description indicating that the rotary controller should scroll this view
-     * vertically.
+     * Content description indicating that the view is a scrollable container and can be scrolled
+     * vertically by the rotary controller.
+     * <p>
+     * A scrollable container is a focusable rotary container. When it's focused, it can be scrolled
+     * when the rotary controller rotates. A scrollable container is often used to show long text.
      */
     public static final String ROTARY_VERTICALLY_SCROLLABLE =
             "com.android.car.ui.utils.VERTICALLY_SCROLLABLE";
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
index 853649b..f1ffad6 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
@@ -18,6 +18,7 @@
 
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
 
+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;
 
@@ -202,24 +203,31 @@
 
     /**
      * Returns whether the {@code view} is an implicit default focus view, i.e., the first focusable
-     * item in a scrollable container.
+     * item in a rotary container.
      */
     @VisibleForTesting
     static boolean isImplicitDefaultFocusView(@NonNull View view) {
-        ViewGroup scrollableContainer = null;
+        ViewGroup rotaryContainer = null;
         ViewParent parent = view.getParent();
         while (parent != null && parent instanceof ViewGroup) {
             ViewGroup viewGroup = (ViewGroup) parent;
-            if (isScrollableContainer(viewGroup)) {
-                scrollableContainer = viewGroup;
+            if (isRotaryContainer(viewGroup)) {
+                rotaryContainer = viewGroup;
                 break;
             }
             parent = parent.getParent();
         }
-        if (scrollableContainer == null) {
+        if (rotaryContainer == null) {
             return false;
         }
-        return findFirstFocusableDescendant(scrollableContainer) == view;
+        return findFirstFocusableDescendant(rotaryContainer) == view;
+    }
+
+    private static boolean isRotaryContainer(@NonNull View view) {
+        CharSequence contentDescription = view.getContentDescription();
+        return TextUtils.equals(contentDescription, ROTARY_CONTAINER)
+                || TextUtils.equals(contentDescription, ROTARY_VERTICALLY_SCROLLABLE)
+                || TextUtils.equals(contentDescription, ROTARY_HORIZONTALLY_SCROLLABLE);
     }
 
     private static boolean isScrollableContainer(@NonNull View view) {
@@ -317,16 +325,16 @@
 
     /**
      * Searches the {@code view} and its descendants in depth first order, and returns the first
-     * implicit default focus view, i.e., the first focusable item in the first scrollable
-     * container. Returns null if not found.
+     * implicit default focus view, i.e., the first focusable item in the first rotary container.
+     * Returns null if not found.
      */
     @VisibleForTesting
     @Nullable
     static View findImplicitDefaultFocusView(@NonNull View view) {
-        View scrollableContainer = findScrollableContainer(view);
-        return scrollableContainer == null
+        View rotaryContainer = findRotaryContainer(view);
+        return rotaryContainer == null
                 ? null
-                : findFirstFocusableDescendant(scrollableContainer);
+                : findFirstFocusableDescendant(rotaryContainer);
     }
 
     /**
@@ -343,12 +351,12 @@
 
     /**
      * Searches the {@code view} and its descendants in depth first order, and returns the first
-     * scrollable container shown on the screen. Returns null if not found.
+     * rotary container shown on the screen. Returns null if not found.
      */
     @Nullable
-    private static View findScrollableContainer(@NonNull View view) {
+    private static View findRotaryContainer(@NonNull View view) {
         return depthFirstSearch(view,
-                /* targetPredicate= */ v -> isScrollableContainer(v),
+                /* targetPredicate= */ v -> isRotaryContainer(v),
                 /* skipPredicate= */ v -> !v.isShown());
     }