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;
+ }
}