Initialize the focus properly

1. When an element in the scrollable container is scrolled off the
screen, restore the focus by focusing on the scrollable container.
If the element is removed, do nothing.
2. Lower the focus level of the scrollable container. When restoring
the focus, don't focus on the scrollable container unless there is
no view to take focus.

Fixes: 167300379
Fixes: 171526996

Test: atest CarUILibUnitTests
Test: manual test Media and Dialer

Change-Id: I601eb8a5cb1e0f34e3f48eb1474ab5e9ffd50043
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 8ae6de7..6c9630b 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
@@ -32,6 +32,7 @@
 
 import com.android.car.ui.recyclerview.TestContentLimitingAdapter;
 import com.android.car.ui.test.R;
+import com.android.car.ui.utils.CarUiUtils;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -40,6 +41,8 @@
 /** Unit test for {@link FocusParkingView}. */
 public class FocusParkingViewTest {
 
+    private static final int NUM_ITEMS = 40;
+
     @Rule
     public ActivityTestRule<FocusParkingViewTestActivity> mActivityRule =
             new ActivityTestRule<>(FocusParkingViewTestActivity.class);
@@ -62,7 +65,8 @@
 
         mList.post(() -> {
             mList.setLayoutManager(new LinearLayoutManager(mActivity));
-            mList.setAdapter(new TestContentLimitingAdapter(/* numItems= */ 2));
+            mList.setAdapter(new TestContentLimitingAdapter(NUM_ITEMS));
+            CarUiUtils.setRotaryScrollEnabled(mList, /* isVertical= */ true);
         });
     }
 
@@ -153,6 +157,32 @@
                         parent.removeView(firstItem);
                         assertThat(mFocusedByDefault.isFocused()).isTrue();
                     }
+                })
+        );
+    }
+
+    @Test
+    public void testRestoreFocusInRoot_recyclerViewItemScrolledOffScreen() {
+        mList.post(() -> mList.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        View firstItem = mList.getLayoutManager().findViewByPosition(0);
+                        firstItem.requestFocus();
+                        assertThat(firstItem.isFocused()).isTrue();
+
+                        mList.scrollToPosition(NUM_ITEMS - 1);
+                        mList.getViewTreeObserver().addOnGlobalLayoutListener(
+                                new ViewTreeObserver.OnGlobalLayoutListener() {
+                                    @Override
+                                    public void onGlobalLayout() {
+                                        mList.getViewTreeObserver()
+                                                .removeOnGlobalLayoutListener(this);
+                                        assertThat(mList.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 06ff9fd..3cc9b7e 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
@@ -25,13 +25,13 @@
 import static com.android.car.ui.utils.ViewUtils.IMPLICIT_DEFAULT_FOCUS;
 import static com.android.car.ui.utils.ViewUtils.NO_FOCUS;
 import static com.android.car.ui.utils.ViewUtils.REGULAR_FOCUS;
+import static com.android.car.ui.utils.ViewUtils.SCROLLABLE_CONTAINER_FOCUS;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.view.View;
 import android.view.ViewTreeObserver;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.test.rule.ActivityTestRule;
@@ -83,7 +83,11 @@
         mList5 = mActivity.findViewById(R.id.list5);
         mRoot = mFocusArea1.getRootView();
 
-        mRoot.post(() -> setUpRecyclerView(mList5));
+        mRoot.post(() -> {
+            mList5.setLayoutManager(new LinearLayoutManager(mActivity));
+            mList5.setAdapter(new TestContentLimitingAdapter(/* numItems= */ 2));
+            CarUiUtils.setRotaryScrollEnabled(mList5, /* isVertical= */ true);
+        });
     }
 
     @Test
@@ -113,7 +117,6 @@
                     @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);
@@ -289,9 +292,7 @@
 
     @Test
     public void testRequestFocus_nullView() {
-        mRoot.post(() -> {
-            assertRequestFocus(null, false);
-        });
+        mRoot.post(() -> assertRequestFocus(null, false));
     }
 
     @Test
@@ -379,7 +380,6 @@
                     @Override
                     public void onGlobalLayout() {
                         mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                        CarUiUtils.setRotaryScrollEnabled(mList5, /* isVertical= */ true);
                         assertRequestFocus(mList5, false);
                     }
                 }));
@@ -425,7 +425,7 @@
     @Test
     public void testAdjustFocus_differentFocusLevels() {
         mRoot.post(() -> {
-            assertThat(ViewUtils.adjustFocus(mFocusArea2, NO_FOCUS)).isTrue();
+            assertThat(ViewUtils.adjustFocus(mFocusArea2, SCROLLABLE_CONTAINER_FOCUS)).isTrue();
             assertThat(ViewUtils.adjustFocus(mFocusArea2, REGULAR_FOCUS)).isFalse();
 
             assertThat(ViewUtils.adjustFocus(mFocusArea5, REGULAR_FOCUS)).isTrue();
@@ -436,6 +436,13 @@
 
             assertThat(ViewUtils.adjustFocus(mFocusArea3, DEFAULT_FOCUS)).isTrue();
             assertThat(ViewUtils.adjustFocus(mFocusArea3, FOCUSED_BY_DEFAULT)).isFalse();
+
+            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+            firstItem.setFocusable(false);
+            View secondItem = mList5.getLayoutManager().findViewByPosition(1);
+            secondItem.setFocusable(false);
+            assertThat(ViewUtils.adjustFocus(mFocusArea5, NO_FOCUS)).isTrue();
+            assertThat(ViewUtils.adjustFocus(mFocusArea5, SCROLLABLE_CONTAINER_FOCUS)).isFalse();
         });
     }
 
@@ -447,6 +454,8 @@
             mFocusArea2.setVisibility(INVISIBLE);
             assertThat(ViewUtils.getFocusLevel(mView2)).isEqualTo(NO_FOCUS);
 
+            assertThat(ViewUtils.getFocusLevel(mList5)).isEqualTo(SCROLLABLE_CONTAINER_FOCUS);
+
             assertThat(ViewUtils.getFocusLevel(mView4)).isEqualTo(REGULAR_FOCUS);
 
             mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
@@ -473,9 +482,4 @@
             assertThat(view.isFocused()).isEqualTo(focused);
         }
     }
-
-    private void setUpRecyclerView(@NonNull CarUiRecyclerView list) {
-        list.setLayoutManager(new LinearLayoutManager(mActivity));
-        list.setAdapter(new TestContentLimitingAdapter(/* numItems= */ 2));
-    }
 }
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..72c2366 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
@@ -84,10 +84,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="vertical">
-        <View
-            android:layout_width="100dp"
-            android:layout_height="100dp"
-            android:focusable="true"/>
         <com.android.car.ui.recyclerview.CarUiRecyclerView
             android:id="@+id/list5"
             android:layout_width="wrap_content"
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
index 481e522..085ae23 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
@@ -137,6 +137,11 @@
             // (such as focus, clear focus) on the nodes in the window. So FocusParkingView has to
             // grab the focus proactively.
             super.requestFocus(FOCUS_DOWN, null);
+
+            // OnGlobalFocusChangeListener won't be triggered when the window lost focus, so reset
+            // the focused view here.
+            mFocusedView = null;
+            mScrollableContainer = null;
         } else if (isFocused()) {
             // When FocusParkingView is focused and the window just gets focused, transfer the view
             // focus to a non-FocusParkingView in the window.
@@ -185,9 +190,9 @@
     }
 
     private boolean restoreFocusInRoot() {
-        // The focused view was in a scrollable container and it was removed, e.g., it was scrolled
-        // off the screen. Let's focus on the scrollable container so that the rotary controller
-        // can scroll it.
+        // The focused view was in a scrollable container and the Framework unfocused it because it
+        // was scrolled off the screen. In this case focus on the scrollable container so that the
+        // rotary controller can scroll the scrollable container.
         if (maybeFocusOnScrollableContainer()) {
             return true;
         }
@@ -196,9 +201,14 @@
     }
 
     private boolean maybeFocusOnScrollableContainer() {
+        // If the focused view was in a scrollable container and it was scrolled off the screen,
+        // focus on the scrollable container. When a view is scrolled off the screen, it is no
+        // longer attached to window and its parent is not null. When a view is removed, its parent
+        // is null. There is no need to focus on the scrollable container when its focused element
+        // is removed.
         if (mFocusedView != null && !mFocusedView.isAttachedToWindow()
-                && mScrollableContainer != null && mScrollableContainer.isAttachedToWindow()
-                && mScrollableContainer.isShown()) {
+                && mFocusedView.getParent() != null && mScrollableContainer != null
+                && mScrollableContainer.isAttachedToWindow() && mScrollableContainer.isShown()) {
             RecyclerView recyclerView = mScrollableContainer instanceof RecyclerView
                     ? (RecyclerView) mScrollableContainer
                     : null;
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 f1ffad6..fdc787b 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
@@ -51,27 +51,33 @@
      */
     public static final int NO_FOCUS = 1;
 
-    /** A regular view is focused. */
-    public static final int REGULAR_FOCUS = 2;
+    /** A scrollable container is focused. */
+    public static final int SCROLLABLE_CONTAINER_FOCUS = 2;
+
+    /**
+     * A regular view is focused. A regular View is a View that is neither a FocusParkingView nor a
+     * scrollable container.
+     */
+    public static final int REGULAR_FOCUS = 3;
 
     /**
      * An implicit default focus view (i.e., the first focusable item in a scrollable container) is
      * focused.
      */
-    public static final int IMPLICIT_DEFAULT_FOCUS = 3;
+    public static final int IMPLICIT_DEFAULT_FOCUS = 4;
 
     /** The {@code app:defaultFocus} view is focused. */
-    public static final int DEFAULT_FOCUS = 4;
+    public static final int DEFAULT_FOCUS = 5;
 
     /** The {@code android:focusedByDefault} view is focused. */
-    public static final int FOCUSED_BY_DEFAULT = 5;
+    public static final int FOCUSED_BY_DEFAULT = 6;
 
     /**
      * Focus level of a view. When adjusting the focus, the view with the highest focus level will
      * be focused.
      */
-    @IntDef(flag = true, value = {NO_FOCUS, REGULAR_FOCUS, IMPLICIT_DEFAULT_FOCUS,
-            DEFAULT_FOCUS, FOCUSED_BY_DEFAULT})
+    @IntDef(flag = true, value = {NO_FOCUS, SCROLLABLE_CONTAINER_FOCUS, REGULAR_FOCUS,
+            IMPLICIT_DEFAULT_FOCUS, DEFAULT_FOCUS, FOCUSED_BY_DEFAULT})
     @Retention(RetentionPolicy.SOURCE)
     public @interface FocusLevel {
     }
@@ -171,8 +177,11 @@
         if (currentLevel < IMPLICIT_DEFAULT_FOCUS && focusOnImplicitDefaultFocusView(root)) {
             return true;
         }
-        if (currentLevel < REGULAR_FOCUS) {
-            return focusOnFirstFocus(root);
+        if (currentLevel < REGULAR_FOCUS && focusOnFirstRegularView(root)) {
+            return true;
+        }
+        if (currentLevel < SCROLLABLE_CONTAINER_FOCUS) {
+            return focusOnScrollableContainer(root);
         }
         return false;
     }
@@ -192,6 +201,9 @@
         if (isImplicitDefaultFocusView(view)) {
             return IMPLICIT_DEFAULT_FOCUS;
         }
+        if (isScrollableContainer(view)) {
+            return SCROLLABLE_CONTAINER_FOCUS;
+        }
         return REGULAR_FOCUS;
     }
 
@@ -270,20 +282,35 @@
     }
 
     /**
-     * Tries to focus on the first focusable view in the view tree in depth first order. If failed,
-     * keeps trying other views in depth first order until succeeded.
+     * Tries to focus on the first focusable view in the view tree in depth first order, excluding
+     * the FocusParkingView and scrollable containers. If focusing on the first such view fails,
+     * keeps trying other views in depth first order until succeeds or there are no more such views.
      *
      * @param root the root of the view tree
      * @return whether succeeded
      */
-    private static boolean focusOnFirstFocus(@NonNull View root) {
+    private static boolean focusOnFirstRegularView(@NonNull View root) {
         View focusedView = ViewUtils.depthFirstSearch(root,
-                /* targetPredicate= */ v -> canTakeFocus(v) && requestFocus(v),
+                /* targetPredicate= */
+                v -> !isScrollableContainer(v) && canTakeFocus(v) && requestFocus(v),
                 /* skipPredicate= */ v -> !v.isShown());
         return focusedView != null;
     }
 
     /**
+     * Focuses on the first scrollable container in the view tree, if any.
+     *
+     * @param root the root of the view tree
+     * @return whether succeeded
+     */
+    private static boolean focusOnScrollableContainer(@NonNull View root) {
+        View focusedView = ViewUtils.depthFirstSearch(root,
+                /* targetPredicate= */ v -> isScrollableContainer(v) && canTakeFocus(v),
+                /* skipPredicate= */ v -> !v.isShown());
+        return requestFocus(focusedView);
+    }
+
+    /**
      * Searches the {@code root}'s descendants in depth first order, and returns the first
      * {@code app:defaultFocus} view that can take focus. Returns null if not found.
      */