Update the focus logic of FocusArea and FocusParkingView

When the framework wants to restore the focus, focus on the default
view defined by us. As a result, RotaryService doesn't need to adjust
the focus any more.

Fixes: 169252130
Fixes: 168240318
Fixes: 164218771
Fixes: 159929766

Test: atest CarUILibUnitTests, and manual test
Change-Id: I5d546654437b5e7874b6478026ec92e8b9e492c5
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
index 81df306..2482a5b 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
@@ -23,6 +23,7 @@
         <activity android:name="com.android.car.ui.FocusAreaTestActivity" />
         <activity android:name="com.android.car.ui.FocusParkingViewTestActivity" />
         <activity android:name="com.android.car.ui.preference.PreferenceTestActivity" />
+        <activity android:name="com.android.car.ui.utils.ViewUtilsTestActivity" />
         <activity
             android:name="com.android.car.ui.toolbar.ToolbarTestActivity"
             android:theme="@style/Theme.CarUi.WithToolbar"/>
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
index 9c1ce88..51f166c 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
@@ -111,8 +111,6 @@
 
     @Test
     public void testFocusOnDefaultFocus() throws Exception {
-        assertThat(mDefaultFocus.isFocused()).isFalse();
-
         Bundle bundle = new Bundle();
         CountDownLatch latch = new CountDownLatch(1);
         mFocusArea.post(() -> {
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 887ac8e..9f24d6a 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
@@ -46,7 +46,7 @@
     }
 
     @Test
-    public void testFocusParkingViewCanTakeFocus() throws Exception {
+    public void testFocusParkingViewCannotTakeFocus() throws Exception {
         FocusParkingView focusParkingView = mActivity.findViewById(R.id.focus_parking);
 
         CountDownLatch latch = new CountDownLatch(1);
@@ -58,7 +58,7 @@
         });
         latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
 
-        assertThat(focusParkingView.isFocused()).isTrue();
+        assertThat(focusParkingView.isFocused()).isFalse();
     }
     @Test
     public void testFocusParkingViewFocusedWhenWindowLostFocus() throws Exception {
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
new file mode 100644
index 0000000..76797e3
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.utils;
+
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.ui.FocusArea;
+import com.android.car.ui.FocusParkingView;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.test.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Unit tests for {@link ViewUtils}. */
+public class ViewUtilsTest {
+
+    @Rule
+    public ActivityTestRule<ViewUtilsTestActivity> mActivityRule =
+            new ActivityTestRule<>(ViewUtilsTestActivity.class);
+
+    private ViewUtilsTestActivity mActivity;
+    private FocusArea mFocusArea1;
+    private FocusArea mFocusArea2;
+    private FocusArea mFocusArea3;
+    private FocusArea mFocusArea4;
+    private FocusArea mFocusArea5;
+    private FocusParkingView mFpv;
+    private View mView2;
+    private View mFocusedByDefault3;
+    private View mView4;
+    private View mDefaultFocus4;
+    private CarUiRecyclerView mList5;
+    private View mRoot;
+
+    @Before
+    public void setUp() {
+        mActivity = mActivityRule.getActivity();
+        mFocusArea1 = mActivity.findViewById(R.id.focus_area1);
+        mFocusArea2 = mActivity.findViewById(R.id.focus_area2);
+        mFocusArea3 = mActivity.findViewById(R.id.focus_area3);
+        mFocusArea4 = mActivity.findViewById(R.id.focus_area4);
+        mFocusArea5 = mActivity.findViewById(R.id.focus_area5);
+        mFpv = mActivity.findViewById(R.id.fpv);
+        mView2 = mActivity.findViewById(R.id.view2);
+        mFocusedByDefault3 = mActivity.findViewById(R.id.focused_by_default3);
+        mView4 = mActivity.findViewById(R.id.view4);
+        mDefaultFocus4 = mActivity.findViewById(R.id.default_focus4);
+        mList5 = mActivity.findViewById(R.id.list5);
+        mRoot = mFocusArea1.getRootView();
+
+        mRoot.post(() -> {
+            setUpRecyclerView(mList5);
+        });
+    }
+
+    @Test
+    public void testRootVisible() {
+        mRoot.post(() -> assertThat(mRoot.getVisibility()).isEqualTo(VISIBLE));
+    }
+
+    @Test
+    public void testFindFocusedByDefaultView() {
+        mRoot.post(() -> {
+            View focusedByDefault = ViewUtils.findFocusedByDefaultView(mRoot);
+            assertThat(focusedByDefault).isEqualTo(mFocusedByDefault3);
+        });
+    }
+
+    @Test
+    public void testFindFocusedByDefaultView_skipNotFocusable() {
+        mRoot.post(() -> {
+            mFocusedByDefault3.setFocusable(false);
+            View focusedByDefault = ViewUtils.findFocusedByDefaultView(mRoot);
+            assertThat(focusedByDefault).isNull();
+        });
+    }
+
+    @Test
+    public void testFindFocusedByDefaultView_skipInvisibleView() {
+        mRoot.post(() -> {
+            mFocusArea3.setVisibility(INVISIBLE);
+            assertThat(mFocusArea3.getVisibility()).isEqualTo(INVISIBLE);
+            View focusedByDefault = ViewUtils.findFocusedByDefaultView(mRoot);
+            assertThat(focusedByDefault).isNull();
+        });
+    }
+
+    @Test
+    public void testFindFocusedByDefaultView_skipInvisibleAncestor() {
+        mRoot.post(() -> {
+            mRoot.setVisibility(INVISIBLE);
+            View focusedByDefault = ViewUtils.findFocusedByDefaultView(mFocusArea3);
+            assertThat(focusedByDefault).isNull();
+        });
+    }
+
+    @Test
+    public void testFindImplicitDefaultFocusView_inRoot() {
+        mRoot.post(() -> {
+            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+            View implicitDefaultFocus = ViewUtils.findImplicitDefaultFocusView(mRoot);
+            assertThat(implicitDefaultFocus).isEqualTo(firstItem);
+        });
+    }
+
+    @Test
+    public void testFindImplicitDefaultFocusView_inFocusArea() {
+        mRoot.post(() -> {
+            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+            View implicitDefaultFocus = ViewUtils.findImplicitDefaultFocusView(mFocusArea5);
+            assertThat(implicitDefaultFocus).isEqualTo(firstItem);
+        });
+    }
+
+    @Test
+    public void testFindImplicitDefaultFocusView_skipInvisibleAncestor() {
+        mRoot.post(() -> {
+            mRoot.setVisibility(INVISIBLE);
+            View implicitDefaultFocus = ViewUtils.findImplicitDefaultFocusView(mFocusArea5);
+            assertThat(implicitDefaultFocus).isNull();
+        });
+    }
+
+    @Test
+    public void testFindFirstFocusableDescendant() {
+        mRoot.post(() -> {
+            mFocusArea2.setFocusable(true);
+            View firstFocusable = ViewUtils.findFirstFocusableDescendant(mRoot);
+            assertThat(firstFocusable).isEqualTo(mFocusArea2);
+        });
+    }
+
+    @Test
+    public void testFindFirstFocusableDescendant_skipItself() {
+        mRoot.post(() -> {
+            mFocusArea2.setFocusable(true);
+            View firstFocusable = ViewUtils.findFirstFocusableDescendant(mFocusArea2);
+            assertThat(firstFocusable).isEqualTo(mView2);
+        });
+    }
+
+    @Test
+    public void testFindFirstFocusableDescendant_skipInvisibleAndGoneView() {
+        mRoot.post(() -> {
+            mFocusArea2.setVisibility(INVISIBLE);
+            mFocusArea3.setVisibility(GONE);
+            View firstFocusable = ViewUtils.findFirstFocusableDescendant(mRoot);
+            assertThat(firstFocusable).isEqualTo(mView4);
+        });
+    }
+
+    @Test
+    public void testFindFirstFocusableDescendant_skipInvisibleAncestor() {
+        mRoot.post(() -> {
+            mRoot.setVisibility(INVISIBLE);
+            View firstFocusable = ViewUtils.findFirstFocusableDescendant(mFocusArea2);
+            assertThat(firstFocusable).isNull();
+        });
+    }
+
+    @Test
+    public void testIsImplicitDefaultFocusView_firstItem() {
+        mRoot.post(() -> {
+            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+            assertThat(ViewUtils.isImplicitDefaultFocusView(firstItem)).isTrue();
+        });
+    }
+
+    @Test
+    public void testIsImplicitDefaultFocusView_secondItem() {
+        mRoot.post(() -> {
+            View secondItem = mList5.getLayoutManager().findViewByPosition(1);
+            assertThat(ViewUtils.isImplicitDefaultFocusView(secondItem)).isFalse();
+        });
+    }
+
+    @Test
+    public void testIsImplicitDefaultFocusView_normalView() {
+        mRoot.post(() -> {
+            assertThat(ViewUtils.isImplicitDefaultFocusView(mView2)).isFalse();
+        });
+    }
+
+    @Test
+    public void testIsImplicitDefaultFocusView_skipInvisibleAncestor() {
+        mRoot.post(() -> {
+            mFocusArea5.setVisibility(INVISIBLE);
+            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+            assertThat(ViewUtils.isImplicitDefaultFocusView(firstItem)).isFalse();
+        });
+    }
+
+    @Test
+    public void testRequestFocus() {
+        mRoot.post(() -> {
+            assertRequestFocus(mView2, true);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_nullView() {
+        mRoot.post(() -> {
+            assertRequestFocus(null, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_alreadyFocused() {
+        mRoot.post(() -> {
+            assertRequestFocus(mView2, true);
+            // mView2 is already focused before requesting focus.
+            assertRequestFocus(mView2, true);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_notFocusable() {
+        mRoot.post(() -> {
+            mView2.setFocusable(false);
+            assertRequestFocus(mView2, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_disabled() {
+        mRoot.post(() -> {
+            mView2.setEnabled(false);
+            assertRequestFocus(mView2, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_notVisible() {
+        mRoot.post(() -> {
+            mView2.setVisibility(View.INVISIBLE);
+            assertRequestFocus(mView2, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_skipInvisibleAncestor() {
+        mRoot.post(() -> {
+            mFocusArea2.setVisibility(View.INVISIBLE);
+            assertRequestFocus(mView2, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_zeroWidth() {
+        mRoot.post(() -> {
+            mView2.setRight(mView2.getLeft());
+            assertThat(mView2.getWidth()).isEqualTo(0);
+            assertRequestFocus(mView2, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_detachedFromWindow() {
+        mRoot.post(() -> {
+            mFocusArea2.removeView(mView2);
+            assertRequestFocus(mView2, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_FocusParkingView() {
+        mRoot.post(() -> {
+            assertRequestFocus(mView2, true);
+            assertRequestFocus(mFpv, false);
+        });
+    }
+
+    @Test
+    public void testRequestFocus_scrollableContainer() {
+        mRoot.post(() -> {
+            assertRequestFocus(mList5, false);
+        });
+    }
+
+    @Test
+    public void testAdjustFocus_inRoot() {
+        mRoot.post(() -> {
+            assertRequestFocus(mView2, true);
+            ViewUtils.adjustFocus(mRoot, null);
+            assertThat(mFocusedByDefault3.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testAdjustFocus_inFocusAreaWithDefaultFocus() {
+        mRoot.post(() -> {
+            assertRequestFocus(mView2, true);
+            ViewUtils.adjustFocus(mFocusArea3, null);
+            assertThat(mFocusedByDefault3.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testAdjustFocus_inFocusAreaWithoutDefaultFocus() {
+        mRoot.post(() -> {
+            assertRequestFocus(mView4, true);
+            ViewUtils.adjustFocus(mFocusArea2, null);
+            assertThat(mView2.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testAdjustFocus_inFocusAreaWithoutFocusableDescendant() {
+        mRoot.post(() -> {
+            assertRequestFocus(mView2, true);
+            boolean success = ViewUtils.adjustFocus(mFocusArea1, null);
+            assertThat(mFocusArea1.hasFocus()).isFalse();
+            assertThat(success).isFalse();
+        });
+    }
+
+    private static void assertRequestFocus(@Nullable View view, boolean focused) {
+        boolean result = ViewUtils.requestFocus(view);
+        assertThat(result).isEqualTo(focused);
+        if (view != null) {
+            assertThat(view.isFocused()).isEqualTo(focused);
+        }
+    }
+
+    private void setUpRecyclerView(@NonNull CarUiRecyclerView list) {
+        list.setLayoutManager(new LinearLayoutManager(mActivity));
+        list.setAdapter(new TestAdapter());
+    }
+
+    private static class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
+
+        private static final List<String> DATA = Arrays.asList("first", "second");
+
+        @NonNull
+        @Override
+        public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(parent.getContext())
+                    .inflate(R.layout.test_list_item, parent, false);
+            return new TestViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
+            holder.bind(DATA.get(position));
+        }
+
+        @Override
+        public int getItemCount() {
+            return DATA.size();
+        }
+    }
+
+    private static class TestViewHolder extends RecyclerView.ViewHolder {
+        private final TextView mTextView;
+
+        TestViewHolder(View view) {
+            super(view);
+            mTextView = view.findViewById(R.id.text);
+        }
+
+        void bind(String text) {
+            mTextView.setText(text);
+        }
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTestActivity.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTestActivity.java
new file mode 100644
index 0000000..87434d4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTestActivity.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.utils;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.car.ui.test.R;
+
+/** An activity used for testing {@link ViewUtils}. */
+public class ViewUtilsTestActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.view_utils_test_activity);
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml
index e789145..1fab70e 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/test_list_item.xml
@@ -18,7 +18,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:orientation="horizontal">
+    android:orientation="horizontal"
+    android:focusable="true">
 
     <TextView
         android:layout_width="wrap_content"
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
new file mode 100644
index 0000000..558dd9f
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/view_utils_test_activity.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <com.android.car.ui.FocusParkingView
+        android:id="@+id/fpv"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+    <com.android.car.ui.FocusArea
+        android:id="@+id/focus_area1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <View
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+    </com.android.car.ui.FocusArea>
+    <com.android.car.ui.FocusArea
+        android:id="@+id/focus_area2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <View
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+        <View
+            android:id="@+id/view2"
+            android:layout_width="100dp"
+            android:layout_height="100dp"
+            android:focusable="true"/>
+    </com.android.car.ui.FocusArea>
+    <com.android.car.ui.FocusArea
+        android:id="@+id/focus_area3"
+        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"/>
+        <View
+            android:id="@+id/focused_by_default3"
+            android:layout_width="100dp"
+            android:layout_height="100dp"
+            android:focusable="true"
+            android:focusedByDefault="true"/>
+    </com.android.car.ui.FocusArea>
+    <com.android.car.ui.FocusArea
+        android:id="@+id/focus_area4"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:defaultFocus="@+id/default_focus4">
+        <View
+            android:id="@+id/view4"
+            android:layout_width="100dp"
+            android:layout_height="100dp"
+            android:focusable="true"/>
+        <View
+            android:id="@+id/default_focus4"
+            android:layout_width="100dp"
+            android:layout_height="100dp"
+            android:focusable="true"/>
+    </com.android.car.ui.FocusArea>
+    <com.android.car.ui.FocusArea
+        android:id="@+id/focus_area5"
+        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"
+            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/FocusArea.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
index b71b3ab..4f0fa94 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
@@ -25,11 +25,14 @@
 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.NUDGE_DIRECTION;
+import static com.android.car.ui.utils.ViewUtils.NO_FOCUS;
+import static com.android.car.ui.utils.ViewUtils.REGULAR_FOCUS;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.SystemClock;
@@ -38,7 +41,6 @@
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewParent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.LinearLayout;
 
@@ -289,7 +291,7 @@
             mPreviousFocusArea = null;
             return;
         }
-        mPreviousFocusArea = getAncestorFocusArea(oldFocus);
+        mPreviousFocusArea = ViewUtils.getAncestorFocusArea(oldFocus);
         if (mPreviousFocusArea == null) {
             Log.w(TAG, "No parent FocusArea for " + oldFocus);
         }
@@ -306,7 +308,7 @@
         if (!hasFocus || oldFocus == null) {
             return;
         }
-        FocusArea oldFocusArea = getAncestorFocusArea(oldFocus);
+        FocusArea oldFocusArea = ViewUtils.getAncestorFocusArea(oldFocus);
         if (oldFocusArea != this) {
             return;
         }
@@ -457,6 +459,28 @@
     }
 
     @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        // To ensure the focus is initialized properly in rotary mode when there is a window focus
+        // change, this FocusArea will grab the focus from the currently focused view if one of this
+        // FocusArea's descendants is a better focus candidate than the currently focused view.
+        if (hasWindowFocus && !isInTouchMode()) {
+            maybeAdjustFocus();
+        }
+        super.onWindowFocusChanged(hasWindowFocus);
+    }
+
+    /**
+     * Focuses on another view in this FocusArea if the view is a better focus candidate than the
+     * currently focused view.
+     */
+    private boolean maybeAdjustFocus() {
+        View root = getRootView();
+        View focus = root.findFocus();
+        return ViewUtils.adjustFocus(root, focus);
+    }
+
+
+    @Override
     public boolean performAccessibilityAction(int action, Bundle arguments) {
         switch (action) {
             case ACTION_FOCUS:
@@ -481,12 +505,6 @@
     }
 
     private boolean focusOnDescendant() {
-        if (focusOnFocusedByDefaultView()) {
-            return true;
-        }
-        if (focusOnPrimaryFocusView()) {
-            return true;
-        }
         if (mDefaultFocusOverridesHistory) {
             // Check mDefaultFocus before last focused view.
             if (focusDefaultFocusView() || focusOnLastFocusedView()) {
@@ -501,28 +519,26 @@
         return focusOnFirstFocusableView();
     }
 
-    private boolean focusOnFocusedByDefaultView() {
-        View focusedByDefaultView = ViewUtils.findFocusedByDefaultView(this);
-        return requestFocus(focusedByDefaultView);
-    }
-
-    private boolean focusOnPrimaryFocusView() {
-        View primaryFocus = ViewUtils.findPrimaryFocusView(this);
-        return requestFocus(primaryFocus);
-    }
-
     private boolean focusDefaultFocusView() {
-        return requestFocus(mDefaultFocusView);
+        return ViewUtils.adjustFocus(this, /* currentLevel= */ REGULAR_FOCUS);
+    }
+
+    /**
+     * Gets the {@code app:defaultFocus} view.
+     *
+     * @hidden
+     */
+    public View getDefaultFocusView() {
+        return mDefaultFocusView;
     }
 
     private boolean focusOnLastFocusedView() {
         View lastFocusedView = mRotaryCache.getFocusedView(SystemClock.uptimeMillis());
-        return requestFocus(lastFocusedView);
+        return ViewUtils.requestFocus(lastFocusedView);
     }
 
     private boolean focusOnFirstFocusableView() {
-        View firstFocusableView = ViewUtils.findFocusableDescendant(this);
-        return requestFocus(firstFocusableView);
+        return ViewUtils.adjustFocus(this, /* currentLevel= */ NO_FOCUS);
     }
 
     private boolean nudgeToShortcutView(Bundle arguments) {
@@ -541,7 +557,7 @@
             // nudge to another FocusArea.
             return false;
         }
-        return requestFocus(mNudgeShortcutView);
+        return ViewUtils.requestFocus(mNudgeShortcutView);
     }
 
     private boolean nudgeToAnotherFocusArea(Bundle arguments) {
@@ -597,17 +613,6 @@
                 + "FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, or FOCUS_RIGHT.");
     }
 
-    private static FocusArea getAncestorFocusArea(@NonNull View view) {
-        ViewParent parent = view.getParent();
-        while (parent != null) {
-            if (parent instanceof FocusArea) {
-                return (FocusArea) parent;
-            }
-            parent = parent.getParent();
-        }
-        return null;
-    }
-
     @Nullable
     private FocusArea getSpecifiedFocusArea(int direction) {
         maybeInitializeSpecifiedFocusAreas();
@@ -661,6 +666,16 @@
         bundle.putInt(FOCUS_AREA_BOTTOM_BOUND_OFFSET, mBottomOffset);
     }
 
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+        return maybeAdjustFocus();
+    }
+
+    @Override
+    public boolean restoreDefaultFocus() {
+        return maybeAdjustFocus();
+    }
+
     private void maybeInitializeSpecifiedFocusAreas() {
         if (mSpecifiedNudgeFocusAreaMap != null) {
             return;
@@ -673,15 +688,6 @@
         }
     }
 
-    private boolean requestFocus(@Nullable View view) {
-        if (view == null || !view.isAttachedToWindow()) {
-            return false;
-        }
-        // Exit touch mode and focus the view. The view may not be focusable in touch mode, so we
-        // need to exit touch mode before focusing it.
-        return view.performAccessibilityAction(ACTION_FOCUS, null);
-    }
-
     /** Sets the padding (in pixels) of the FocusArea highlight. */
     public void setHighlightPadding(int left, int top, int right, int bottom) {
         if (mPaddingLeft == left && mPaddingTop == top && mPaddingRight == right
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 8a42cea..c0893bb 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
@@ -21,10 +21,11 @@
 import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.Nullable;
@@ -33,10 +34,11 @@
 
 /**
  * A transparent {@link View} that can take focus. It's used by {@link
- * com.android.car.rotary.RotaryService} to support rotary controller navigation. Each {@link
- * android.view.Window} should have one FocusParkingView as the first focusable view in the view
- * tree, and outside of all {@link FocusArea}s. If multiple FocusParkingView are added in the
- * window, only the first one will be focusable.
+ * com.android.car.rotary.RotaryService} to support rotary controller navigation. It's also used to
+ * initialize the focus when in rotary mode.
+ * <p>
+ * To support the rotary controller, each {@link android.view.Window} must have a FocusParkingView
+ * as the first focusable view in the view tree, and outside of all {@link FocusArea}s.
  * <p>
  * Android doesn't clear focus automatically when focus is set in another window. If we try to clear
  * focus in the previous window, Android will re-focus a view in that window, resulting in two
@@ -45,19 +47,33 @@
  * matter whether it's focused or not. It can take focus so that RotaryService can "park" the focus
  * on it to remove the focus highlight.
  * <p>
- * If the focused view is scrolled off the screen, Android will refocus the first focusable view in
- * the window. The FocusParkingView should be the first view so that it gets focus. The
- * RotaryService detects this and moves focus to the scrolling container.
- * <p>
  * If there is only one focus area in the current window, rotating the controller within the focus
  * area will cause RotaryService to move the focus around from the view on the right to the view on
  * the left or vice versa. Adding this view to each window can fix this issue. When RotaryService
  * finds out the focus target is a FocusParkingView, it will know a wrap-around is going to happen.
  * Then it will avoid the wrap-around by not moving focus.
+ * <p>
+ * To ensure the focus is initialized properly when there is a window change, the FocusParkingView
+ * will not get focused when the framework wants to focus on it. Instead, it will try to find a
+ * better focus target in the window and focus on the target. That said, the FocusParkingView can
+ * still be focused in order to clear focus highlight in the window, such as when RotaryService
+ * performs {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_FOCUS} on the
+ * FocusParkingView, or the window has lost focus.
  */
 public class FocusParkingView extends View {
     private static final String TAG = "FocusParkingView";
 
+    /**
+     * The focused view in the window containing this FocusParkingView. It's null if no view is
+     * focused, or the focused view is a FocusParkingView.
+     */
+    @Nullable
+    private View mFocusedView;
+
+    /** The scrollable container that contains the {@link #mFocusedView}, if any. */
+    @Nullable
+    ViewGroup mScrollableContainer;
+
     public FocusParkingView(Context context) {
         super(context);
         init();
@@ -94,6 +110,12 @@
 
         // Prevent Android from drawing the default focus highlight for this view when it's focused.
         setDefaultFocusHighlightEnabled(false);
+
+        // Keep track of the focused view so that we can recover focus when it's removed.
+        getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> {
+            mFocusedView = newFocus instanceof FocusParkingView ? null : newFocus;
+            mScrollableContainer = ViewUtils.getAncestorScrollableContainer(mFocusedView);
+        });
     }
 
     @Override
@@ -107,12 +129,16 @@
     @Override
     public void onWindowFocusChanged(boolean hasWindowFocus) {
         if (!hasWindowFocus) {
-            // We need to clear the focus (by parking the focus on the FocusParkingView) once the
-            // current window goes to background. This can't be done by RotaryService because
-            // RotaryService sees the window as removed, thus can't perform any action (such as
-            // focus, clear focus) on the nodes in the window. So FocusParkingView has to grab the
-            // focus proactively.
-            requestFocus();
+            // We need to clear the focus highlight(by parking the focus on the FocusParkingView)
+            // once the current window goes to background. This can't be done by RotaryService
+            // because RotaryService sees the window as removed, thus can't perform any action
+            // (such as focus, clear focus) on the nodes in the window. So FocusParkingView has to
+            // grab the focus proactively.
+            super.requestFocus(FOCUS_DOWN, 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.
+            restoreFocusInRoot();
         }
         super.onWindowFocusChanged(hasWindowFocus);
     }
@@ -126,21 +152,7 @@
     public boolean performAccessibilityAction(int action, Bundle arguments) {
         switch (action) {
             case ACTION_RESTORE_DEFAULT_FOCUS:
-                View root = getRootView();
-
-                // If there is a view focused by default and it can take focus, move focus to it.
-                View defaultFocus = ViewUtils.findFocusedByDefaultView(root);
-                if (defaultFocus != null) {
-                    return defaultFocus.requestFocus();
-                }
-
-                // If there is a primary focus view, move focus to it.
-                View primaryFocus = ViewUtils.findPrimaryFocusView(root);
-                if (primaryFocus != null) {
-                    return primaryFocus.requestFocus();
-                }
-
-                return false;
+                return restoreFocusInRoot();
             case ACTION_HIDE_IME:
                 InputMethodManager inputMethodManager =
                         getContext().getSystemService(InputMethodManager.class);
@@ -149,33 +161,37 @@
             case ACTION_FOCUS:
                 // Don't leave this to View to handle as it will exit touch mode.
                 if (!hasFocus()) {
-                    return requestFocus();
+                    return super.requestFocus(FOCUS_DOWN, null);
                 }
-                break;
+                return false;
         }
         return super.performAccessibilityAction(action, arguments);
     }
 
     @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
+    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+        // Find a better target to focus instead of focusing this FocusParkingView when the
+        // framework wants to focus it.
+        return restoreFocusInRoot();
+    }
 
-        // If there is a FocusParkingView already, make the one after in the view tree
-        // non-focusable.
-        boolean []isBefore = new boolean[1];
-        View anotherFpv = ViewUtils.depthFirstSearch(getRootView(), v -> {
-            if (this == v) {
-                isBefore[0] = true;
-            }
-            return v != this && v instanceof FocusParkingView && v.isFocusable();
-        });
-        if (anotherFpv != null) {
-            Log.w(TAG, "There should be only one FocusParkingView in the window");
-            if (isBefore[0]) {
-                anotherFpv.setFocusable(false);
-            } else {
-                setFocusable(false);
-            }
+    @Override
+    public boolean restoreDefaultFocus() {
+        // Find a better target to focus instead of focusing this FocusParkingView when the
+        // framework wants to focus it.
+        return restoreFocusInRoot();
+    }
+
+    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.
+        if (mFocusedView != null && !mFocusedView.isAttachedToWindow()
+                && mScrollableContainer != null && mScrollableContainer.isAttachedToWindow()
+                && mScrollableContainer.isShown() && mScrollableContainer.requestFocus()) {
+            return true;
         }
+        // Otherwise find the best target view to focus.
+        return ViewUtils.adjustFocus(getRootView(), /* currentFocus= */ null);
     }
 }
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 99154df..1f1de0a 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
@@ -70,7 +70,7 @@
     /** Action performed on a FocusArea to move focus to another FocusArea. */
     public static final int ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA = 0x02000000;
 
-    /** Action performed on a FocusParkingView to restore the default focus. */
+    /** Action performed on a FocusParkingView to restore the focus in the window. */
     public static final int ACTION_RESTORE_DEFAULT_FOCUS = 0x04000000;
 
     /** Action performed on a FocusParkingView to hide the IME. */
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 6dd993b..7c95577 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
@@ -16,7 +16,7 @@
 
 package com.android.car.ui.utils;
 
-import static android.view.View.VISIBLE;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
 
 import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
 import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
@@ -24,9 +24,18 @@
 import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewParent;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.ui.FocusArea;
+import com.android.car.ui.FocusParkingView;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Utility class used by {@link com.android.car.ui.FocusArea} and {@link
@@ -36,50 +45,310 @@
  */
 public final class ViewUtils {
 
-    /** This is a utility class */
+    /**
+     * No view is focused, the focused view is not shown, or the focused view is a FocusParkingView.
+     */
+    public static final int NO_FOCUS = 1;
+
+    /** A regular view is focused. */
+    public static final int REGULAR_FOCUS = 2;
+
+    /**
+     * 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;
+
+    /** The {@code app:defaultFocus} view is focused. */
+    public static final int DEFAULT_FOCUS = 4;
+
+    /** The {@code android:focusedByDefault} view is focused. */
+    public static final int FOCUSED_BY_DEFAULT = 5;
+
+    /**
+     * 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})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FocusLevel {
+    }
+
+    /** This is a utility class. */
     private ViewUtils() {
     }
 
     /**
-     * Searches the {@code view} and its descendants in depth first order, and returns the first
-     * view that is focused by default, can take focus, but has no invisible ancestors. Returns null
-     * if not found.
+     * This is a functional interface and can therefore be used as the assignment target for a
+     * lambda expression or method reference.
+     *
+     * @param <T> the type of the input to the predicate
+     */
+    private interface Predicate<T> {
+        /** Evaluates this predicate on the given argument. */
+        boolean test(@NonNull T t);
+    }
+
+    /** Gets the ancestor FocusArea of the {@code view}, if any. Returns null if not found. */
+    @Nullable
+    public static FocusArea getAncestorFocusArea(@NonNull View view) {
+        ViewParent parent = view.getParent();
+        while (parent != null) {
+            if (parent instanceof FocusArea) {
+                return (FocusArea) parent;
+            }
+            parent = parent.getParent();
+        }
+        return null;
+    }
+
+    /**
+     * Gets the ancestor scrollable container of the {@code view}, if any. Returns null if not
+     * found.
      */
     @Nullable
-    public static View findFocusedByDefaultView(@NonNull View view) {
-        return depthFirstSearch(view,
-                /* targetPredicate= */ v -> v.isFocusedByDefault() && canTakeFocus(v),
-                /* skipPredicate= */ v -> v.getVisibility() != VISIBLE);
+    public static ViewGroup getAncestorScrollableContainer(@Nullable View view) {
+        if (view == null) {
+            return null;
+        }
+        ViewParent parent = view.getParent();
+        // A scrollable container can't contain a FocusArea, so let's return earlier if we found
+        // a FocusArea.
+        while (parent != null && parent instanceof ViewGroup && !(parent instanceof FocusArea)) {
+            ViewGroup viewGroup = (ViewGroup) parent;
+            if (isScrollableContainer(viewGroup)) {
+                return viewGroup;
+            }
+            parent = parent.getParent();
+        }
+        return null;
+    }
+
+    /**
+     * Focuses on the {@code view} if it can be focused.
+     *
+     * @return whether it was successfully focused or already focused
+     */
+    public static boolean requestFocus(@Nullable View view) {
+        if (view == null || !canTakeFocus(view)) {
+            return false;
+        }
+        if (view.isFocused()) {
+            return true;
+        }
+        // Exit touch mode and focus the view. The view may not be focusable in touch mode, so we
+        // need to exit touch mode before focusing it.
+        return view.performAccessibilityAction(ACTION_FOCUS, /* arguments= */ null);
+    }
+
+    /**
+     * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel}. If
+     * the view's FocusLevel is higher than the {@code currentFocus}'s FocusLevel, focuses on the
+     * view.
+     *
+     * @return whether the view is focused
+     */
+    public static boolean adjustFocus(@NonNull View root, @Nullable View currentFocus) {
+        @FocusLevel int level = getFocusLevel(currentFocus);
+        return adjustFocus(root, level);
+    }
+
+    /**
+     * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel}. If
+     * the view's FocusLevel is higher than {@code currentLevel}, focuses on the view.
+     *
+     * @return whether the view is focused
+     */
+    public static boolean adjustFocus(@NonNull View root, @FocusLevel int currentLevel) {
+        if (currentLevel < FOCUSED_BY_DEFAULT && focusOnFocusedByDefaultView(root)) {
+            return true;
+        }
+        if (currentLevel < DEFAULT_FOCUS && focusOnDefaultFocusView(root)) {
+            return true;
+        }
+        if (currentLevel < IMPLICIT_DEFAULT_FOCUS && focusOnImplicitDefaultFocusView(root)) {
+            return true;
+        }
+        if (currentLevel < REGULAR_FOCUS) {
+            return focusOnFirstFocus(root);
+        }
+        return false;
+    }
+
+    @FocusLevel
+    private static int getFocusLevel(@Nullable View view) {
+        if (view == null || view instanceof FocusParkingView || !view.isShown()) {
+            return NO_FOCUS;
+        }
+        if (view.isFocusedByDefault()) {
+            return FOCUSED_BY_DEFAULT;
+        }
+        if (isDefaultFocus(view)) {
+            return DEFAULT_FOCUS;
+        }
+        if (isImplicitDefaultFocusView(view)) {
+            return IMPLICIT_DEFAULT_FOCUS;
+        }
+        return REGULAR_FOCUS;
+    }
+
+    /** Returns whether the {@code view} is a {@code app:defaultFocus} view. */
+    private static boolean isDefaultFocus(@NonNull View view) {
+        FocusArea parent = getAncestorFocusArea(view);
+        return parent != null && view == parent.getDefaultFocusView();
+    }
+
+    /**
+     * Returns whether the {@code view} is an implicit default focus view, i.e., the first focusable
+     * item in a scrollable container.
+     */
+    @VisibleForTesting
+    static boolean isImplicitDefaultFocusView(@NonNull View view) {
+        ViewGroup scrollableContainer = null;
+        ViewParent parent = view.getParent();
+        while (parent != null && parent instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) parent;
+            if (isScrollableContainer(viewGroup)) {
+                scrollableContainer = viewGroup;
+                break;
+            }
+            parent = parent.getParent();
+        }
+        if (scrollableContainer == null) {
+            return false;
+        }
+        return findFirstFocusableDescendant(scrollableContainer) == view;
+    }
+
+    private static boolean isScrollableContainer(@NonNull View view) {
+        CharSequence contentDescription = view.getContentDescription();
+        return TextUtils.equals(contentDescription, ROTARY_VERTICALLY_SCROLLABLE)
+                || TextUtils.equals(contentDescription, ROTARY_HORIZONTALLY_SCROLLABLE);
+    }
+
+    /**
+     * Focuses on the first {@code app:defaultFocus} view in the view tree, if any.
+     *
+     * @param root the root of the view tree
+     * @return whether succeeded
+     */
+    private static boolean focusOnDefaultFocusView(@NonNull View root) {
+        View defaultFocus = findDefaultFocusView(root);
+        return requestFocus(defaultFocus);
+    }
+
+    /**
+     * Focuses on the first {@code android:focusedByDefault} view in the view tree, if any.
+     *
+     * @param root the root of the view tree
+     * @return whether succeeded
+     */
+    private static boolean focusOnFocusedByDefaultView(@NonNull View root) {
+        View focusedByDefault = findFocusedByDefaultView(root);
+        return requestFocus(focusedByDefault);
+    }
+
+    /**
+     * Focuses on the first implicit default focus view in the view tree, if any.
+     *
+     * @param root the root of the view tree
+     * @return whether succeeded
+     */
+    private static boolean focusOnImplicitDefaultFocusView(@NonNull View root) {
+        View implicitDefaultFocus = findImplicitDefaultFocusView(root);
+        return requestFocus(implicitDefaultFocus);
+    }
+
+    /**
+     * 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.
+     *
+     * @param root the root of the view tree
+     * @return whether succeeded
+     */
+    private static boolean focusOnFirstFocus(@NonNull View root) {
+        View focusedView = ViewUtils.depthFirstSearch(root,
+                /* targetPredicate= */ v -> canTakeFocus(v) && requestFocus(v),
+                /* skipPredicate= */ v -> !v.isShown());
+        return focusedView != null;
+    }
+
+    /**
+     * 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.
+     */
+    @Nullable
+    private static View findDefaultFocusView(@NonNull View view) {
+        if (!view.isShown()) {
+            return null;
+        }
+        if (view instanceof FocusArea) {
+            FocusArea focusArea = (FocusArea) view;
+            View defaultFocus = focusArea.getDefaultFocusView();
+            if (defaultFocus != null && canTakeFocus(defaultFocus)) {
+                return defaultFocus;
+            }
+        } else if (view instanceof ViewGroup) {
+            ViewGroup parent = (ViewGroup) view;
+            for (int i = 0; i < parent.getChildCount(); i++) {
+                View child = parent.getChildAt(i);
+                View defaultFocus = findDefaultFocusView(child);
+                if (defaultFocus != null) {
+                    return defaultFocus;
+                }
+            }
+        }
+        return null;
     }
 
     /**
      * Searches the {@code view} and its descendants in depth first order, and returns the first
-     * primary focus view, i.e., the first focusable item in a scrollable container. Returns null
-     * if not found.
+     * {@code android:focusedByDefault} view that can take focus. Returns null if not found.
      */
-    public static View findPrimaryFocusView(@NonNull View view) {
+    @VisibleForTesting
+    @Nullable
+    static View findFocusedByDefaultView(@NonNull View view) {
+        return depthFirstSearch(view,
+                /* targetPredicate= */ v -> v.isFocusedByDefault() && canTakeFocus(v),
+                /* skipPredicate= */ v -> !v.isShown());
+    }
+
+    /**
+     * 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.
+     */
+    @VisibleForTesting
+    @Nullable
+    static View findImplicitDefaultFocusView(@NonNull View view) {
         View scrollableContainer = findScrollableContainer(view);
-        return scrollableContainer == null ? null : findFocusableDescendant(scrollableContainer);
+        return scrollableContainer == null
+                ? null
+                : findFirstFocusableDescendant(scrollableContainer);
     }
 
     /**
      * Searches the {@code view}'s descendants in depth first order, and returns the first view
-     * that can take focus but has no invisible ancestors, or null if not found.
+     * that can take focus, or null if not found.
      */
+    @VisibleForTesting
     @Nullable
-    public static View findFocusableDescendant(@NonNull View view) {
+    static View findFirstFocusableDescendant(@NonNull View view) {
         return depthFirstSearch(view,
                 /* targetPredicate= */ v -> v != view && canTakeFocus(v),
-                /* skipPredicate= */ v -> v.getVisibility() != VISIBLE);
+                /* skipPredicate= */ v -> !v.isShown());
     }
 
     /**
      * Searches the {@code view} and its descendants in depth first order, and returns the first
-     * view that meets the given condition. Returns null if not found.
+     * scrollable container shown on the screen. Returns null if not found.
      */
     @Nullable
-    public static View depthFirstSearch(@NonNull View view, @NonNull Predicate<View> predicate) {
-        return depthFirstSearch(view, predicate, /* skipPredicate= */ null);
+    private static View findScrollableContainer(@NonNull View view) {
+        return depthFirstSearch(view,
+                /* targetPredicate= */ v -> isScrollableContainer(v),
+                /* skipPredicate= */ v -> !v.isShown());
     }
 
     /**
@@ -90,7 +359,7 @@
     @Nullable
     private static View depthFirstSearch(@NonNull View view,
             @NonNull Predicate<View> targetPredicate,
-            @NonNull Predicate<View> skipPredicate) {
+            @Nullable Predicate<View> skipPredicate) {
         if (skipPredicate != null && skipPredicate.test(view)) {
             return null;
         }
@@ -110,35 +379,13 @@
         return null;
     }
 
-    /**
-     * This is a functional interface and can therefore be used as the assignment target for a
-     * lambda expression or method reference.
-     *
-     * @param <T> the type of the input to the predicate
-     */
-    public interface Predicate<T> {
-        /** Evaluates this predicate on the given argument. */
-        boolean test(@NonNull T t);
-    }
-
-    /**
-     * Searches the {@code view} and its descendants in depth first order, and returns the first
-     * scrollable container that has no invisible ancestors. Returns null if not found.
-     */
-    @Nullable
-    private static View findScrollableContainer(@NonNull View view) {
-        return depthFirstSearch(view,
-                /* targetPredicate= */ v -> {
-                    CharSequence contentDescription = v.getContentDescription();
-                    return TextUtils.equals(contentDescription, ROTARY_VERTICALLY_SCROLLABLE)
-                            || TextUtils.equals(contentDescription, ROTARY_HORIZONTALLY_SCROLLABLE);
-                },
-                /* skipPredicate= */ v -> v.getVisibility() != VISIBLE);
-    }
-
     /** Returns whether {@code view} can be focused. */
     private static boolean canTakeFocus(@NonNull View view) {
-        return view.isFocusable() && view.isEnabled() && view.getVisibility() == VISIBLE
-                && view.getWidth() > 0 && view.getHeight() > 0;
+        return view.isFocusable() && view.isEnabled() && view.isShown()
+                && view.getWidth() > 0 && view.getHeight() > 0 && view.isAttachedToWindow()
+                && !(view instanceof FocusParkingView)
+                // If it's a scrollable container, it can be focused only when it has no focusable
+                // descendants. We focus on it so that the rotary controller can scroll it.
+                && (!isScrollableContainer(view) || findFirstFocusableDescendant(view) == null);
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
index e823bdf..ab238cc 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
@@ -147,9 +147,30 @@
 
     <!-- Attributes for FocusArea. -->
     <declare-styleable name="FocusArea">
-        <!-- The ID of a focusable descendant view which should be focused when the user nudges to
-             this FocusArea, if there was no view focused in the FocusArea or
-             car_ui_focus_area_default_focus_overrides_history is true. -->
+        <!-- The ID of the default focus view. The view will be prioritized when searching for a
+             focus target.
+             (1) When the user nudges the rotary controller, it will search for a target FocusArea,
+                 then search for a target view within the target FocusArea, and focus on the target
+                 view. The target view is chosen in the following order:
+                   1. the "android:focusedByDefault" view, if any
+                   2. the "app:defaultFocus" view, if any
+                   3. the first focusable item in a scrollable container, if any
+                   4. previously focused view, if any and the cache is not stale
+                   5. the first focusable view, if any
+                 Note that 4 will be prioritized over 1&2&3 when
+                 car_ui_focus_area_default_focus_overrides_history is false.
+             (2) When it needs to initialize the focus (such as when a window is opened), it will
+                 search for a view in the window and focus on it. The view is chosen in the
+                 following order:
+                   1. the first "android:focusedByDefault" view, if any
+                   2. the first "app:defaultFocus" view, if any
+                   3. the first focusable item in a scrollable container, if any
+                   4. the first focusable view that is not a FocusParkingView, if any
+             If there is only one FocusArea that needs to set default focus, you can use either
+             "app:defaultFocus" or "android:focusedByDefault". If there are more than one, you
+             should use "android:focusedByDefault" in the primary FocusArea, and use
+             "app:defaultFocus" in other FocusAreas. -->
+
         <attr name="defaultFocus" format="reference"/>
 
         <!-- The paddings of FocusArea highlight. It does't impact the paddings on its child views,