Make rotary scrolling opt-in for RecyclerViews

Rotary scrolling isn't needed for most CarUiRecyclerViews because all
the items are focusable. It causes problems when there's a gap between
adjacent rows, as is the case in the launcher. This CL makes rotary
scrolling opt-in rather than opt-out for CarUiRecyclerViews. We can
enable rotary scrolling where we need it, either declaratively or
programmatically via a new method in CarUiUtils.

Test: try scrolling in Launcher and Settings
Test: try scrolling in scroll tab of RotaryPlayground
Test: set contentDescription programmatically to enable rotary scrolling
Test: make RotaryPlayground enable rotary scrolling programmatically
Bug: 160922045
Change-Id: Ia6ed5ddafedd9c628cb9d5430fd92b1388215ea8
Merged-In: Ia6ed5ddafedd9c628cb9d5430fd92b1388215ea8
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 0ad8cd7..9e55e36 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
@@ -36,5 +36,7 @@
     <com.android.car.ui.recyclerview.CarUiRecyclerView
         android:id="@+id/list"
         android:layout_width="wrap_content"
-        android:layout_height="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. -->
 </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 558dd9f..f5b9028 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,6 +91,8 @@
         <com.android.car.ui.recyclerview.CarUiRecyclerView
             android:id="@+id/list5"
             android:layout_width="wrap_content"
-            android:layout_height="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. -->
     </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 00e06b9..b755213 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
@@ -187,7 +187,7 @@
     }
 
     private void init(Context context, AttributeSet attrs, int defStyleAttr) {
-        initRotaryScroll(context, attrs, defStyleAttr);
+        initRotaryScroll();
         setClipToPadding(false);
         TypedArray a = context.obtainStyledAttributes(
                 attrs,
@@ -283,28 +283,19 @@
     }
 
     /**
-     * If this view's content description isn't set to opt out of scrolling via the rotary
-     * controller, initialize it accordingly.
+     * If this view's content description is set to opt into scrolling via the rotary controller,
+     * initialize it accordingly.
      */
-    private void initRotaryScroll(Context context, AttributeSet attrs, int defStyleAttr) {
+    private void initRotaryScroll() {
         CharSequence contentDescription = getContentDescription();
-        if (contentDescription == null) {
-            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
-                    defStyleAttr, /* defStyleRes= */ 0);
-            int orientation = a.getInt(R.styleable.RecyclerView_android_orientation,
-                    LinearLayout.VERTICAL);
-            setContentDescription(
-                    orientation == LinearLayout.HORIZONTAL
-                            ? ROTARY_HORIZONTALLY_SCROLLABLE
-                            : ROTARY_VERTICALLY_SCROLLABLE);
-        } else if (!ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription)
-                && !ROTARY_VERTICALLY_SCROLLABLE.contentEquals(contentDescription)) {
-            return;
-        }
+        boolean rotaryScrollEnabled = contentDescription != null
+                && (ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription)
+                || ROTARY_VERTICALLY_SCROLLABLE.contentEquals(contentDescription));
 
-        // Convert SOURCE_ROTARY_ENCODER scroll events into SOURCE_MOUSE scroll events that
-        // RecyclerView knows how to handle.
-        setOnGenericMotionListener((v, event) -> {
+        // If rotary scrolling is enabled, set a generic motion event listener to convert
+        // SOURCE_ROTARY_ENCODER scroll events into SOURCE_MOUSE scroll events that RecyclerView
+        // knows how to handle.
+        setOnGenericMotionListener(rotaryScrollEnabled ? (v, event) -> {
             if (event.getAction() == MotionEvent.ACTION_SCROLL) {
                 if (event.getSource() == InputDevice.SOURCE_ROTARY_ENCODER) {
                     MotionEvent mouseEvent = MotionEvent.obtain(event);
@@ -314,11 +305,11 @@
                 }
             }
             return false;
-        });
+        } : null);
 
-        // Mark this view as focusable. This view will be focused when no focusable elements are
-        // visible.
-        setFocusable(true);
+        // If rotary scrolling is enabled, mark this view as focusable. This view will be focused
+        // when no focusable elements are visible.
+        setFocusable(rotaryScrollEnabled);
 
         // Focus this view before descendants so that the RotaryService can focus this view when it
         // wants to.
@@ -534,6 +525,12 @@
         removeItemDecoration(mDividerItemDecorationGrid);
     }
 
+    @Override
+    public void setContentDescription(CharSequence contentDescription) {
+        super.setContentDescription(contentDescription);
+        initRotaryScroll();
+    }
+
     private static RuntimeException andLog(String msg, Throwable t) {
         Log.e(TAG, msg, t);
         throw new RuntimeException(msg, t);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
index b27a81a..ce4d8a5 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
@@ -15,6 +15,9 @@
  */
 package com.android.car.ui.utils;
 
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
+
 import android.app.Activity;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -153,6 +156,19 @@
     }
 
     /**
+     * Enables rotary scrolling for {@code view}, either vertically (if {@code isVertical} is true)
+     * or horizontally (if {@code isVertical} is false). With rotary scrolling enabled, rotating the
+     * rotary controller will scroll rather than moving the focus when moving the focus would cause
+     * a lot of scrolling. Rotary scrolling should be enabled for scrolling views which contain
+     * content which the user may want to see but can't interact with, either alone or along with
+     * interactive (focusable) content.
+     */
+    public static void setRotaryScrollEnabled(@NonNull View view, boolean isVertical) {
+        view.setContentDescription(
+                isVertical ? ROTARY_VERTICALLY_SCROLLABLE : ROTARY_HORIZONTALLY_SCROLLABLE);
+    }
+
+    /**
      * It behaves similarly to {@link View#findViewById(int)}, except that on Q and below,
      * it will first resolve the id to whatever it references.
      *