Add unit tests for FocusArea and FocusParkingView

And fix flaky tests related to RecyclerView.

Bug: 170344916
Bug: 170423337
Bug: 170419833

Test: atest CarUILibUnitTests
Change-Id: Ibec1c98adae0ad2dfa1d6b723971aa18b14c7e4f
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 5f57aba..14d23b3 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
@@ -16,20 +16,30 @@
 
 package com.android.car.ui;
 
+import static android.view.View.FOCUS_DOWN;
+import static android.view.View.FOCUS_LEFT;
+import static android.view.View.FOCUS_RIGHT;
+import static android.view.View.FOCUS_UP;
 import static android.view.View.LAYOUT_DIRECTION_LTR;
 import static android.view.View.LAYOUT_DIRECTION_RTL;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
 
+import static com.android.car.ui.RotaryCache.CACHE_TYPE_DISABLED;
+import static com.android.car.ui.RotaryCache.CACHE_TYPE_NEVER_EXPIRE;
+import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_SHORTCUT;
+import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_BOTTOM_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_LEFT_BOUND_OFFSET;
 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.google.common.truth.Truth.assertThat;
 
 import android.os.Bundle;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
 
 import androidx.annotation.NonNull;
 import androidx.test.rule.ActivityTestRule;
@@ -52,80 +62,359 @@
             new ActivityTestRule<>(FocusAreaTestActivity.class);
 
     private FocusAreaTestActivity mActivity;
-    private TestFocusArea mFocusArea;
+    private TestFocusArea mFocusArea1;
     private TestFocusArea mFocusArea2;
-    private View mChild;
-    private View mDefaultFocus;
-    private View mNonChild;
-    private View mChild1;
-    private View mChild2;
+    private TestFocusArea mFocusArea3;
+    private TestFocusArea mFocusArea4;
+    private FocusParkingView mFpv;
+    private View mView1;
+    private Button mButton1;
+    private View mView2;
+    private View mDefaultFocus2;
+    private View mView3;
+    private View mNudgeShortcut3;
+    private View mView4;
 
     @Before
     public void setUp() {
         mActivity = mActivityRule.getActivity();
-        mFocusArea = mActivity.findViewById(R.id.focus_area);
-        mFocusArea.enableForegroundHighlight();
+        mFocusArea1 = mActivity.findViewById(R.id.focus_area1);
         mFocusArea2 = mActivity.findViewById(R.id.focus_area2);
-        mChild = mActivity.findViewById(R.id.child);
-        mDefaultFocus = mActivity.findViewById(R.id.default_focus);
-        mNonChild = mActivity.findViewById(R.id.non_child);
-        mChild1 = mActivity.findViewById(R.id.child1);
-        mChild2 = mActivity.findViewById(R.id.child2);
+        mFocusArea3 = mActivity.findViewById(R.id.focus_area3);
+        mFocusArea4 = mActivity.findViewById(R.id.focus_area4);
+        mFpv = mActivity.findViewById(R.id.fpv);
+        mView1 = mActivity.findViewById(R.id.view1);
+        mButton1 = mActivity.findViewById(R.id.button1);
+        mView2 = mActivity.findViewById(R.id.view2);
+        mDefaultFocus2 = mActivity.findViewById(R.id.default_focus2);
+        mView3 = mActivity.findViewById(R.id.view3);
+        mNudgeShortcut3 = mActivity.findViewById(R.id.nudge_shortcut3);
+        mView4 = mActivity.findViewById(R.id.view4);
     }
 
     @Test
-    public void testLoseFocus() throws Exception {
-        mChild.post(() -> {
-            mChild.requestFocus();
-        });
-        mFocusArea.setOnDrawCalled(false);
-        mFocusArea.setDrawCalled(false);
-
-        // FocusArea lost focus.
+    public void testDrawMethodsCalled() throws Exception {
         CountDownLatch latch = new CountDownLatch(1);
-        mNonChild.post(() -> {
-            mNonChild.requestFocus();
-            mNonChild.post(() -> {
-                latch.countDown();
-            });
+        mView1.post(() -> {
+            mView1.requestFocus();
+            mFocusArea1.enableForegroundHighlight();
+            mFocusArea2.enableForegroundHighlight();
+            mFocusArea1.setOnDrawCalled(false);
+            mFocusArea1.setDrawCalled(false);
+            mFocusArea2.setOnDrawCalled(false);
+            mFocusArea2.setDrawCalled(false);
+
+            mView2.requestFocus();
+            mView2.post(() -> latch.countDown());
         });
-        assertDrawMethodsCalled(latch);
+
+        // The methods should be called when a FocusArea gains or loses focus.
+        assertDrawMethodsCalled(mFocusArea1, latch);
+        assertDrawMethodsCalled(mFocusArea2, latch);
     }
 
     @Test
-    public void testGetFocus() throws Exception {
-        mNonChild.post(() -> {
-            mNonChild.requestFocus();
-        });
-        mFocusArea.setOnDrawCalled(false);
-        mFocusArea.setDrawCalled(false);
+    public void testPerformAccessibilityAction_actionNudgeShortcut() {
+        mFocusArea1.post(() -> {
+            // Nudge to the nudgeShortcut view.
+            mView3.requestFocus();
+            assertThat(mView3.isFocused()).isTrue();
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_RIGHT);
+            mFocusArea3.performAccessibilityAction(ACTION_NUDGE_SHORTCUT, arguments);
+            assertThat(mNudgeShortcut3.isFocused()).isTrue();
 
-        // FocusArea got focus.
-        CountDownLatch latch = new CountDownLatch(1);
-        mChild.post(() -> {
-            mChild.requestFocus();
-            mChild.post(() -> {
-                latch.countDown();
-            });
+            // nudgeShortcutDirection doesn't match. The focus should stay the same.
+            mView3.requestFocus();
+            assertThat(mView3.isFocused()).isTrue();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+            mFocusArea3.performAccessibilityAction(ACTION_NUDGE_SHORTCUT, arguments);
+            assertThat(mView3.isFocused()).isTrue();
+
+            // No nudgeShortcut view in the current FocusArea. The focus should stay the same.
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_RIGHT);
+            mFocusArea1.performAccessibilityAction(ACTION_NUDGE_SHORTCUT, arguments);
+            assertThat(mView1.isFocused()).isTrue();
         });
-        assertDrawMethodsCalled(latch);
+    }
+
+
+    @Test
+    public void testPerformAccessibilityAction_actionFocus() {
+        mFocusArea1.post(() -> {
+            mFocusArea1.performAccessibilityAction(ACTION_FOCUS, null);
+            assertThat(mView1.isFocused()).isTrue();
+
+            // It should focus on the default or the first view in the FocusArea.
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+        });
     }
 
     @Test
-    public void testFocusOnDefaultFocus() throws Exception {
-        Bundle bundle = new Bundle();
-        CountDownLatch latch = new CountDownLatch(1);
-        mFocusArea.post(() -> {
-            mFocusArea.performAccessibilityAction(ACTION_FOCUS, bundle);
-            latch.countDown();
+    public void testPerformAccessibilityAction_actionFocus_enabledFocusCache() {
+        mFocusArea1.post(() -> {
+            RotaryCache cache =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea1.setRotaryCache(cache);
+
+            mButton1.requestFocus();
+            assertThat(mButton1.isFocused()).isTrue();
+            mView2.requestFocus();
+            assertThat(mView2.isFocused()).isTrue();
+
+            // With cache, it should focus on the lastly focused view in the FocusArea.
+            mFocusArea1.performAccessibilityAction(ACTION_FOCUS, null);
+            assertThat(mButton1.isFocused()).isTrue();
         });
-        latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
-        assertThat(mDefaultFocus.isFocused()).isTrue();
+    }
+
+    @Test
+    public void testPerformAccessibilityAction_actionFocus_disabledFocusCache() {
+        mFocusArea1.post(() -> {
+            RotaryCache cache = new RotaryCache(CACHE_TYPE_DISABLED, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea1.setRotaryCache(cache);
+
+            mButton1.requestFocus();
+            assertThat(mButton1.isFocused()).isTrue();
+            mView2.requestFocus();
+            assertThat(mView2.isFocused()).isTrue();
+
+            // Without cache, it should focus on the default or the first view in the FocusArea.
+            mFocusArea1.performAccessibilityAction(ACTION_FOCUS, null);
+            assertThat(mView1.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testPerformAccessibilityAction_actionFocus_lastFocusedViewRemoved() {
+        mFocusArea1.post(() -> {
+            // Focus on mDefaultFocus2 in mFocusArea2, then mView1 in mFocusArea21.
+            mDefaultFocus2.requestFocus();
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            // Remove mDefaultFocus2, then Perform ACTION_FOCUS on mFocusArea2.
+            mFocusArea2.removeView(mDefaultFocus2);
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+
+            // mView2 in mFocusArea2 should get focused.
+            assertThat(mView2.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_enabledCache() {
+        mFocusArea1.post(() -> {
+            RotaryCache cache1 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea1.setRotaryCache(cache1);
+            RotaryCache cache2 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea2.setRotaryCache(cache2);
+
+            // Focus on the second view in mFocusArea1, then nudge to mFocusArea2.
+            mButton1.requestFocus();
+            assertThat(mButton1.isFocused()).isTrue();
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+            // Nudge back. It should focus on the cached view (mButton1) in the cached
+            // FocusArea (mFocusArea1).
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mButton1.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_mixedCache() {
+        mFocusArea1.post(() -> {
+            // Disabled FocusCache but enabled FocusAreaCache.
+            RotaryCache cache1 =
+                    new RotaryCache(CACHE_TYPE_DISABLED, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea1.setRotaryCache(cache1);
+            RotaryCache cache2 =
+                    new RotaryCache(CACHE_TYPE_DISABLED, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea2.setRotaryCache(cache2);
+
+            // Focus on the second view in mFocusArea1, then nudge to mFocusArea2.
+            mButton1.requestFocus();
+            assertThat(mButton1.isFocused()).isTrue();
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+            // Nudge back. Since FocusCache is disabled, it should focus on the default or the first
+            // view (mView1) in the cached FocusArea (mFocusArea1).
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mView1.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_mixedCache2() {
+        mFocusArea1.post(() -> {
+            // Enabled FocusCache but disabled FocusAreaCache.
+            RotaryCache cache1 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_DISABLED, 0);
+            mFocusArea1.setRotaryCache(cache1);
+            RotaryCache cache2 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_DISABLED, 0);
+            mFocusArea2.setRotaryCache(cache2);
+
+            // Focus on the second view in mFocusArea1, then nudge to mFocusArea2.
+            mButton1.requestFocus();
+            assertThat(mButton1.isFocused()).isTrue();
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+            // Nudge back. Since FocusAreaCache is disabled, nudge should fail and the focus should
+            // stay the same.
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testPerformAccessibilityAction_actionNudgeToAnotherFocusArea_specifiedTarget() {
+        mFocusArea1.post(() -> {
+            // Nudge to specified FocusArea.
+            mView4.requestFocus();
+            assertThat(mView4.isFocused()).isTrue();
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_LEFT);
+            mFocusArea4.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+            // Direction doesn't match specified FocusArea. The focus should stay the same.
+            mView4.requestFocus();
+            assertThat(mView4.isFocused()).isTrue();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea4.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mView4.isFocused()).isTrue();
+
+            // The FocusArea doesn't specify a target FocusArea. The focus should stay the same.
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_LEFT);
+            mFocusArea1.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mView1.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testDefaultFocusOverridesHistory_override() {
+        mFocusArea1.post(() -> {
+            RotaryCache cache =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea2.setRotaryCache(cache);
+            mFocusArea2.setDefaultFocusOverridesHistory(true);
+
+            mView2.requestFocus();
+            assertThat(mView2.isFocused()).isTrue();
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            // The focused view should be the default focus view rather than the cached view.
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testDefaultFocusOverridesHistory_notOverride() {
+        mFocusArea1.post(() -> {
+            RotaryCache cache =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea2.setRotaryCache(cache);
+            mFocusArea2.setDefaultFocusOverridesHistory(false);
+
+            mView2.requestFocus();
+            assertThat(mView2.isFocused()).isTrue();
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            // The focused view should be the cached view rather than the default focus view.
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+            assertThat(mView2.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testClearFocusAreaHistoryWhenRotating_clear() {
+        mFocusArea1.post(() -> {
+            RotaryCache cache1 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea1.setRotaryCache(cache1);
+            mFocusArea1.setClearFocusAreaHistoryWhenRotating(true);
+            RotaryCache cache2 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea2.setRotaryCache(cache2);
+            mFocusArea2.setClearFocusAreaHistoryWhenRotating(true);
+
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            // Nudging down from mFocusArea1 to mFocusArea2.
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+            // Rotate.
+            mView2.requestFocus();
+            assertThat(mView2.isFocused()).isTrue();
+            // Since nudge history is cleared, nudging up should fail and the focus should stay
+            // the same.
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mView2.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testClearFocusAreaHistoryWhenRotating_notClear() {
+        mFocusArea1.post(() -> {
+            RotaryCache cache1 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea1.setRotaryCache(cache1);
+            mFocusArea1.setClearFocusAreaHistoryWhenRotating(false);
+            RotaryCache cache2 =
+                    new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
+            mFocusArea2.setRotaryCache(cache2);
+            mFocusArea2.setClearFocusAreaHistoryWhenRotating(false);
+
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            // Nudging down from mFocusArea1 to mFocusArea2.
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+            // Rotate.
+            mView2.requestFocus();
+            assertThat(mView2.isFocused()).isTrue();
+            // Nudging up should move focus to mFocusArea1 according to nudge history.
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mView1.isFocused()).isTrue();
+        });
     }
 
     @Test
     public void testBoundsOffset() {
-        assertThat(mFocusArea.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_LTR);
+        assertThat(mFocusArea1.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_LTR);
 
         // FocusArea's bounds offset specified in layout file:
         // 10dp(start), 20dp(end), 30dp(top), 40dp(bottom).
@@ -133,36 +422,33 @@
         int right = dp2Px(20);
         int top = dp2Px(30);
         int bottom = dp2Px(40);
-        AccessibilityNodeInfo node = mFocusArea.createAccessibilityNodeInfo();
+        AccessibilityNodeInfo node = mFocusArea1.createAccessibilityNodeInfo();
         assertBoundsOffset(node, left, top, right, bottom);
         node.recycle();
     }
 
     @Test
-    public void testBoundsOffsetWithRtl() throws Exception {
-        CountDownLatch latch = new CountDownLatch(1);
-        mFocusArea.post(() -> {
-            mFocusArea.setLayoutDirection(LAYOUT_DIRECTION_RTL);
-            latch.countDown();
-        });
-        latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
-        assertThat(mFocusArea.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_RTL);
+    public void testBoundsOffsetWithRtl() {
+        mFocusArea1.post(() -> {
+            mFocusArea1.setLayoutDirection(LAYOUT_DIRECTION_RTL);
+            assertThat(mFocusArea1.getLayoutDirection()).isEqualTo(LAYOUT_DIRECTION_RTL);
 
-        // FocusArea highlight padding specified in layout file:
-        // 10dp(start), 20dp(end), 30dp(top), 40dp(bottom).
-        int left = dp2Px(20);
-        int right = dp2Px(10);
-        int top = dp2Px(30);
-        int bottom = dp2Px(40);
-        AccessibilityNodeInfo node = mFocusArea.createAccessibilityNodeInfo();
-        assertBoundsOffset(node, left, top, right, bottom);
-        node.recycle();
+            // FocusArea highlight padding specified in layout file:
+            // 10dp(start), 20dp(end), 30dp(top), 40dp(bottom).
+            int left = dp2Px(20);
+            int right = dp2Px(10);
+            int top = dp2Px(30);
+            int bottom = dp2Px(40);
+            AccessibilityNodeInfo node = mFocusArea1.createAccessibilityNodeInfo();
+            assertBoundsOffset(node, left, top, right, bottom);
+            node.recycle();
+        });
     }
 
     @Test
     public void testSetBoundsOffset() {
-        mFocusArea.setBoundsOffset(50, 60, 70, 80);
-        AccessibilityNodeInfo node = mFocusArea.createAccessibilityNodeInfo();
+        mFocusArea1.setBoundsOffset(50, 60, 70, 80);
+        AccessibilityNodeInfo node = mFocusArea1.createAccessibilityNodeInfo();
         assertBoundsOffset(node, 50, 60, 70, 80);
         node.recycle();
     }
@@ -181,20 +467,37 @@
     }
 
     @Test
-    public void testLastFocusedViewRemoved() {
-        mChild1.post(() -> {
-            // Focus on mChild1 in mFocusArea2, then mChild in mFocusArea .
-            mChild1.requestFocus();
-            assertThat(mChild1.isFocused()).isTrue();
-            mChild.requestFocus();
-            assertThat(mChild.isFocused()).isTrue();
+    public void testBug170423337() {
+        mFocusArea1.post(() -> {
+            // Focus on app bar (assume mFocusArea1 is app bar).
+            mView1.requestFocus();
 
-            // Remove mChild1 in mFocusArea2, then Perform ACTION_FOCUS on mFocusArea2.
-            mFocusArea2.removeView(mChild1);
-            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, null);
+            // Nudge down to browse list (assume mFocusArea2 is browse list).
+            Bundle arguments = new Bundle();
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_DOWN);
+            mFocusArea2.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
 
-            // mChild2 in mFocusArea2 should get focused.
-            assertThat(mChild2.isFocused()).isTrue();
+            // Nudge down to playback control bar (assume mFocusArea3 is playback control bar).
+            mFocusArea3.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mView3.isFocused()).isTrue();
+
+            // Nudge down to navigation bar (navigation bar is in system window without FocusAreas).
+            mFpv.performAccessibilityAction(ACTION_FOCUS, null);
+
+            // Nudge up to playback control bar.
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea3.performAccessibilityAction(ACTION_FOCUS, arguments);
+            assertThat(mView3.isFocused()).isTrue();
+
+            // Nudge up to browse list.
+            arguments.putInt(NUDGE_DIRECTION, FOCUS_UP);
+            mFocusArea3.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mDefaultFocus2.isFocused()).isTrue();
+
+            // Nudge up, and it should focus on app bar.
+            mFocusArea2.performAccessibilityAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments);
+            assertThat(mView1.isFocused()).isTrue();
         });
     }
 
@@ -212,9 +515,10 @@
         return (int) (dp * mActivity.getResources().getDisplayMetrics().density + 0.5f);
     }
 
-    private void assertDrawMethodsCalled(CountDownLatch latch) throws Exception {
+    private void assertDrawMethodsCalled(@NonNull TestFocusArea focusArea, CountDownLatch latch)
+            throws Exception {
         latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
-        assertThat(mFocusArea.onDrawCalled()).isTrue();
-        assertThat(mFocusArea.drawCalled()).isTrue();
+        assertThat(focusArea.onDrawCalled()).isTrue();
+        assertThat(focusArea.drawCalled()).isTrue();
     }
 }
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 0183e5f..dce68c5 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
@@ -24,6 +24,7 @@
 
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
 
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -45,6 +46,7 @@
 
     private FocusParkingViewTestActivity mActivity;
     private FocusParkingView mFpv;
+    private ViewGroup mParent1;
     private View mView1;
     private View mFocusedByDefault;
     private RecyclerView mList;
@@ -53,6 +55,7 @@
     public void setUp() {
         mActivity = mActivityRule.getActivity();
         mFpv = mActivity.findViewById(R.id.fpv);
+        mParent1 = mActivity.findViewById(R.id.parent1);
         mView1 = mActivity.findViewById(R.id.view1);
         mFocusedByDefault = mActivity.findViewById(R.id.focused_by_default);
         mList = mActivity.findViewById(R.id.list);
@@ -137,14 +140,64 @@
 
     @Test
     public void testRestoreFocusInRoot_recyclerViewItemRemoved() {
-        mFpv.post(() -> {
-            View firstItem = mList.getLayoutManager().findViewByPosition(0);
-            firstItem.requestFocus();
-            assertThat(firstItem.isFocused()).isTrue();
+        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();
 
-            ViewGroup parent = (ViewGroup) firstItem.getParent();
-            parent.removeView(firstItem);
-            assertThat(mList.isFocused()).isTrue();
+                        ViewGroup parent = (ViewGroup) firstItem.getParent();
+                        parent.removeView(firstItem);
+                        assertThat(mList.isFocused()).isTrue();
+                    }
+                }));
+    }
+
+    @Test
+    public void testRestoreFocusInRoot_focusedViewRemoved() {
+        mFpv.post(() -> {
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            ViewGroup parent = (ViewGroup) mView1.getParent();
+            parent.removeView(mView1);
+            assertThat(mFocusedByDefault.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testRestoreFocusInRoot_focusedViewDisabled() {
+        mFpv.post(() -> {
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            mView1.setEnabled(false);
+            assertThat(mFocusedByDefault.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testRestoreFocusInRoot_focusedViewBecomesInvisible() {
+        mFpv.post(() -> {
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            mView1.setVisibility(View.INVISIBLE);
+            assertThat(mFocusedByDefault.isFocused()).isTrue();
+        });
+    }
+
+    @Test
+    public void testRestoreFocusInRoot_focusedViewParentBecomesInvisible() {
+        mFpv.post(() -> {
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            mParent1.setVisibility(View.INVISIBLE);
+            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 44e3e43..ea7c855 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
@@ -29,6 +29,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.view.View;
+import android.view.ViewTreeObserver;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -82,9 +83,7 @@
         mList5 = mActivity.findViewById(R.id.list5);
         mRoot = mFocusArea1.getRootView();
 
-        mRoot.post(() -> {
-            setUpRecyclerView(mList5);
-        });
+        mRoot.post(() -> setUpRecyclerView(mList5));
     }
 
     @Test
@@ -109,10 +108,16 @@
 
     @Test
     public void testGetAncestorScrollableContainer() {
-        mRoot.post(() -> {
-            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
-            assertThat(ViewUtils.getAncestorScrollableContainer(firstItem)).isEqualTo(mList5);
-        });
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+                        assertThat(ViewUtils.getAncestorScrollableContainer(firstItem))
+                                .isEqualTo(mList5);
+                    }
+                }));
     }
 
     @Test
@@ -158,20 +163,31 @@
 
     @Test
     public void testFindImplicitDefaultFocusView_inRoot() {
-        mRoot.post(() -> {
-            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
-            View implicitDefaultFocus = ViewUtils.findImplicitDefaultFocusView(mRoot);
-            assertThat(implicitDefaultFocus).isEqualTo(firstItem);
-        });
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        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);
-        });
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+                        View implicitDefaultFocus =
+                                ViewUtils.findImplicitDefaultFocusView(mFocusArea5);
+                        assertThat(implicitDefaultFocus).isEqualTo(firstItem);
+                    }
+                }));
     }
 
     @Test
@@ -222,41 +238,52 @@
 
     @Test
     public void testIsImplicitDefaultFocusView_firstItem() {
-        mRoot.post(() -> {
-            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
-            assertThat(ViewUtils.isImplicitDefaultFocusView(firstItem)).isTrue();
-        });
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        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();
-        });
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        View secondItem = mList5.getLayoutManager().findViewByPosition(1);
+                        assertThat(ViewUtils.isImplicitDefaultFocusView(secondItem)).isFalse();
+                    }
+                }));
     }
 
     @Test
     public void testIsImplicitDefaultFocusView_normalView() {
-        mRoot.post(() -> {
-            assertThat(ViewUtils.isImplicitDefaultFocusView(mView2)).isFalse();
-        });
+        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();
-        });
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        mFocusArea5.setVisibility(INVISIBLE);
+                        View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+                        assertThat(ViewUtils.isImplicitDefaultFocusView(firstItem)).isFalse();
+                    }
+                }));
     }
 
     @Test
     public void testRequestFocus() {
-        mRoot.post(() -> {
-            assertRequestFocus(mView2, true);
-        });
+        mRoot.post(() -> assertRequestFocus(mView2, true));
     }
 
     @Test
@@ -334,9 +361,14 @@
 
     @Test
     public void testRequestFocus_scrollableContainer() {
-        mRoot.post(() -> {
-            assertRequestFocus(mList5, false);
-        });
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        assertRequestFocus(mList5, false);
+                    }
+                }));
     }
 
     @Test
@@ -403,8 +435,16 @@
 
             assertThat(ViewUtils.getFocusLevel(mView4)).isEqualTo(REGULAR_FOCUS);
 
-            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
-            assertThat(ViewUtils.getFocusLevel(firstItem)).isEqualTo(IMPLICIT_DEFAULT_FOCUS);
+            mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                    new ViewTreeObserver.OnGlobalLayoutListener() {
+                        @Override
+                        public void onGlobalLayout() {
+                            mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                            View firstItem = mList5.getLayoutManager().findViewByPosition(0);
+                            assertThat(ViewUtils.getFocusLevel(firstItem))
+                                    .isEqualTo(IMPLICIT_DEFAULT_FOCUS);
+                        }
+                    }));
 
             assertThat(ViewUtils.getFocusLevel(mDefaultFocus4)).isEqualTo(DEFAULT_FOCUS);
 
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
index 524de8d..da1255d 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
@@ -18,52 +18,79 @@
     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">
-    <View
-        android:focusable="true"
-        android:layout_width="100dp"
-        android:layout_height="100dp"/>
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <com.android.car.ui.FocusParkingView
+        android:id="@+id/fpv"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
     <com.android.car.ui.TestFocusArea
-        android:id="@+id/focus_area"
+        android:id="@+id/focus_area1"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        app:defaultFocus="@+id/default_focus"
         app:startBoundOffset="10dp"
         app:endBoundOffset="20dp"
         app:topBoundOffset="30dp"
         app:bottomBoundOffset="40dp">
         <View
-            android:id="@+id/child"
+            android:id="@+id/view1"
             android:focusable="true"
             android:layout_width="100dp"
             android:layout_height="100dp"/>
-        <View
-            android:id="@+id/default_focus"
-            android:focusable="true"
+        <Button
+            android:id="@+id/button1"
             android:layout_width="100dp"
             android:layout_height="100dp"/>
     </com.android.car.ui.TestFocusArea>
-    <View
-        android:id="@+id/non_child"
-        android:focusable="true"
-        android:layout_width="100dp"
-        android:layout_height="100dp"/>
     <com.android.car.ui.TestFocusArea
         android:id="@+id/focus_area2"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        app:defaultFocus="@+id/default_focus2"
         app:highlightPaddingHorizontal="10dp"
         app:highlightPaddingVertical="20dp"
         app:highlightPaddingStart="30dp"
         app:highlightPaddingTop="40dp"
         app:startBoundOffset="50dp">
         <View
-            android:id="@+id/child1"
+            android:id="@+id/view2"
             android:focusable="true"
             android:layout_width="100dp"
             android:layout_height="100dp"/>
         <View
-            android:id="@+id/child2"
+            android:id="@+id/default_focus2"
+            android:focusable="true"
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+    </com.android.car.ui.TestFocusArea>
+    <com.android.car.ui.TestFocusArea
+        android:id="@+id/focus_area3"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:nudgeShortcut="@+id/nudge_shortcut3"
+        app:nudgeShortcutDirection="right">
+        <View
+            android:id="@+id/view3"
+            android:focusable="true"
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+        <View
+            android:focusable="true"
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+        <View
+            android:id="@+id/nudge_shortcut3"
+            android:focusable="true"
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+    </com.android.car.ui.TestFocusArea>
+    <com.android.car.ui.TestFocusArea
+        android:id="@+id/focus_area4"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:nudgeLeft="@+id/focus_area2">
+        <View
+            android:id="@+id/view4"
             android:focusable="true"
             android:layout_width="100dp"
             android:layout_height="100dp"/>
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..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
@@ -22,11 +22,16 @@
         android:id="@+id/fpv"
         android:layout_width="10dp"
         android:layout_height="10dp"/>
-    <View
-        android:id="@+id/view1"
-        android:layout_width="100dp"
-        android:layout_height="40dp"
-        android:focusable="true"/>
+    <LinearLayout
+        android:id="@+id/parent1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <View
+            android:id="@+id/view1"
+            android:layout_width="100dp"
+            android:layout_height="40dp"
+            android:focusable="true"/>
+    </LinearLayout>
     <View
         android:id="@+id/focused_by_default"
         android:layout_width="100dp"
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 4f0fa94..457d8cc 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
@@ -713,4 +713,19 @@
     void enableForegroundHighlight() {
         mEnableForegroundHighlight = true;
     }
+
+    @VisibleForTesting
+    void setDefaultFocusOverridesHistory(boolean override) {
+        mDefaultFocusOverridesHistory = override;
+    }
+
+    @VisibleForTesting
+    void setRotaryCache(@NonNull RotaryCache rotaryCache) {
+        mRotaryCache = rotaryCache;
+    }
+
+    @VisibleForTesting
+    void setClearFocusAreaHistoryWhenRotating(boolean clear) {
+        mClearFocusAreaHistoryWhenRotating = clear;
+    }
 }