Merge "Introduce rotary container" into rvc-qpr-dev
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 fa2e65c..57e908f 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;
 
@@ -330,6 +331,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());
     }