Don't move focus to RecyclerView
To scroll a RecyclerView, we can focus on it directly, but we
shouldn't move focus to it via rotating or nudging the rotary
controller if it has a descendant to take focus.
Fixes: 160924453
Test: manual test and atest CarRotaryControllerRoboTests
Change-Id: Id52d6c4276447e3203a3ec19f7865550032b4b6d
diff --git a/src/com/android/car/rotary/Navigator.java b/src/com/android/car/rotary/Navigator.java
index a2410f1..1cc951f 100644
--- a/src/com/android/car/rotary/Navigator.java
+++ b/src/com/android/car/rotary/Navigator.java
@@ -298,13 +298,9 @@
* Returns the target focusable for a rotate. The caller is responsible for recycling the node
* in the result.
*
- * <p>If {@code skipNode} isn't null, this node will be skipped. This is used when the focus is
- * inside a scrollable container to avoid moving the focus to the scrollable container itself.
- *
* <p>Limits navigation to focusable views within a scrollable container's viewport, if any.
*
* @param sourceNode the current focus
- * @param skipNode a node to skip - optional
* @param direction rotate direction, must be {@link View#FOCUS_FORWARD} or {@link
* View#FOCUS_BACKWARD}
* @param rotationCount the number of "ticks" to rotate. Only count nodes that can take focus
@@ -318,17 +314,13 @@
* given {@code direction}, {@code null} is returned.
*/
@Nullable
- FindRotateTargetResult findRotateTarget(@NonNull AccessibilityNodeInfo sourceNode,
- @Nullable AccessibilityNodeInfo skipNode, int direction, int rotationCount) {
+ FindRotateTargetResult findRotateTarget(
+ @NonNull AccessibilityNodeInfo sourceNode, int direction, int rotationCount) {
int advancedCount = 0;
AccessibilityNodeInfo currentFocusArea = getAncestorFocusArea(sourceNode);
AccessibilityNodeInfo targetNode = copyNode(sourceNode);
for (int i = 0; i < rotationCount; i++) {
AccessibilityNodeInfo nextTargetNode = targetNode.focusSearch(direction);
- if (skipNode != null && skipNode.equals(nextTargetNode)) {
- Utils.recycleNode(nextTargetNode);
- nextTargetNode = skipNode.focusSearch(direction);
- }
AccessibilityNodeInfo targetFocusArea =
nextTargetNode == null ? null : getAncestorFocusArea(nextTargetNode);
@@ -337,6 +329,16 @@
// focus area in the window, including when the root node is treated as a focus area.
if (nextTargetNode != null && currentFocusArea.equals(targetFocusArea)
&& !Utils.isFocusParkingView(nextTargetNode)) {
+ // If nextTargetNode is a scrollable container with descendants that can take focus,
+ // skip it, and search for the next target.
+ if (Utils.isScrollableContainer(nextTargetNode)
+ && Utils.descendantCanTakeFocus(nextTargetNode)) {
+ Utils.recycleNode(targetNode);
+ Utils.recycleNode(targetFocusArea);
+ targetNode = nextTargetNode;
+ --i;
+ continue;
+ }
// If we're navigating through a scrolling view that can scroll in the specified
// direction and the next view is off-screen, don't advance to it. (We'll scroll
// instead.)
diff --git a/src/com/android/car/rotary/RotaryService.java b/src/com/android/car/rotary/RotaryService.java
index 8820e5b..8a737dd 100644
--- a/src/com/android/car/rotary/RotaryService.java
+++ b/src/com/android/car/rotary/RotaryService.java
@@ -460,13 +460,20 @@
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
+ L.v("onAccessibilityEvent: " + event);
+ AccessibilityNodeInfo source = event.getSource();
+ if (source != null) {
+ L.v("event source: " + source);
+ }
+ L.v("event window ID: " + Integer.toHexString(event.getWindowId()));
+
switch (event.getEventType()) {
case TYPE_VIEW_FOCUSED: {
- handleViewFocusedEvent(event);
+ handleViewFocusedEvent(event, source);
break;
}
case TYPE_VIEW_CLICKED: {
- handleViewClickedEvent(event);
+ handleViewClickedEvent(event, source);
break;
}
case TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
@@ -478,7 +485,7 @@
break;
}
case TYPE_VIEW_SCROLLED: {
- handleViewScrolledEvent(event);
+ handleViewScrolledEvent(event, source);
break;
}
case TYPE_WINDOW_STATE_CHANGED: {
@@ -498,6 +505,7 @@
default:
// Do nothing.
}
+ Utils.recycleNode(source);
}
/**
@@ -702,7 +710,8 @@
}
/** Handles {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} event. */
- private void handleViewFocusedEvent(@NonNull AccessibilityEvent event) {
+ private void handleViewFocusedEvent(@NonNull AccessibilityEvent event,
+ @Nullable AccessibilityNodeInfo sourceNode) {
// A view was focused. We ignore focus changes in touch mode. We don't use
// TYPE_VIEW_FOCUSED to keep mLastTouchedNode up to date because most views can't be
// focused in touch mode. In rotary mode, we use TYPE_VIEW_FOCUSED events to detect whether
@@ -711,7 +720,6 @@
if (!mInRotaryMode) {
return;
}
- AccessibilityNodeInfo sourceNode = event.getSource();
// No need to handle TYPE_VIEW_FOCUSED event if sourceNode is null.
if (sourceNode == null) {
@@ -727,7 +735,6 @@
L.d("A FocusParkingView was focused because we cleared the focus in another window");
Utils.recycleNode(mFocusParkingView);
mFocusParkingView = null;
- sourceNode.recycle();
return;
}
@@ -744,7 +751,6 @@
} else {
L.d("mScrollableContainer is not in the view tree");
}
- sourceNode.recycle();
return;
}
@@ -760,7 +766,6 @@
if (match && !sourceNode.equals(mFocusedNode)) {
setFocusedNode(sourceNode);
}
- sourceNode.recycle();
return;
}
@@ -780,18 +785,17 @@
if (isFpv) {
L.d("Move focus to a nearby view because Android focused a FocusParkingView");
onFocusParkingViewFocusedAutomatically(sourceNode);
- Utils.recycleNode(sourceNode);
return;
}
// Case 5: Android focused a non-FocusParkingView. We should update mFocusedNode.
L.d("Android focused a non-FocusParkingView automatically " + sourceNode);
onNodeFocusedAutomatically(sourceNode);
- Utils.recycleNode(sourceNode);
}
/** Handles {@link AccessibilityEvent#TYPE_VIEW_CLICKED} event. */
- private void handleViewClickedEvent(@NonNull AccessibilityEvent event) {
+ private void handleViewClickedEvent(@NonNull AccessibilityEvent event,
+ @Nullable AccessibilityNodeInfo sourceNode) {
// A view was clicked. If we triggered the click via performAction(ACTION_CLICK) or
// by injecting KEYCODE_DPAD_CENTER, we ignore it. Otherwise, we assume the user
// touched the screen. In this case, we update mLastTouchedNode, and clear the focus
@@ -805,12 +809,10 @@
// Note: there is no way to tell whether the window is removed in click event
// because window remove event (TYPE_WINDOWS_CHANGED with type
// WINDOWS_CHANGE_REMOVED) comes AFTER click event.
- AccessibilityNodeInfo sourceNode = event.getSource();
if (mIgnoreViewClickedNode != null
&& event.getEventTime() < mIgnoreViewClickedUntil
&& ((sourceNode == null) || mIgnoreViewClickedNode.equals(sourceNode))) {
setIgnoreViewClickedNode(null);
- Utils.recycleNode(sourceNode);
return;
}
@@ -829,18 +831,16 @@
if (!sourceNode.equals(mLastTouchedNode) && Utils.canTakeFocus(sourceNode)) {
setLastTouchedNode(sourceNode);
}
- sourceNode.recycle();
}
/** Handles {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. */
- private void handleViewScrolledEvent(@NonNull AccessibilityEvent event) {
+ private void handleViewScrolledEvent(@NonNull AccessibilityEvent event,
+ @Nullable AccessibilityNodeInfo sourceNode) {
if (mAfterScrollAction == AfterScrollAction.NONE
|| SystemClock.uptimeMillis() >= mAfterScrollActionUntil) {
return;
}
- AccessibilityNodeInfo sourceNode = event.getSource();
if (sourceNode == null || !Utils.isScrollableContainer(sourceNode)) {
- Utils.recycleNode(sourceNode);
return;
}
switch (mAfterScrollAction) {
@@ -888,7 +888,6 @@
throw new IllegalStateException(
"Unknown after scroll action: " + mAfterScrollAction);
}
- Utils.recycleNode(sourceNode);
}
/**
@@ -1144,13 +1143,11 @@
return;
}
- // If the focused node is not in direct manipulation mode, move the focus. Skip over
- // mScrollableContainer; we don't want to navigate from a focusable descendant to the
- // scrollable container except as a side-effect of scrolling.
+ // If the focused node is not in direct manipulation mode, move the focus.
int remainingRotationCount = rotationCount;
int direction = clockwise ? View.FOCUS_FORWARD : View.FOCUS_BACKWARD;
- Navigator.FindRotateTargetResult result = mNavigator.findRotateTarget(mFocusedNode,
- /* skipNode= */ mScrollableContainer, direction, rotationCount);
+ Navigator.FindRotateTargetResult result =
+ mNavigator.findRotateTarget(mFocusedNode, direction, rotationCount);
if (result != null) {
if (performFocusAction(result.node)) {
remainingRotationCount -= result.advancedCount;
@@ -1256,7 +1253,7 @@
return result;
}
- private void updateDirectManipulationMode(AccessibilityEvent event, boolean enable) {
+ private void updateDirectManipulationMode(@NonNull AccessibilityEvent event, boolean enable) {
if (!mInRotaryMode || !DirectManipulationHelper.isDirectManipulation(event)) {
return;
}
diff --git a/src/com/android/car/rotary/Utils.java b/src/com/android/car/rotary/Utils.java
index 9d4d5fd..683bcec 100644
--- a/src/com/android/car/rotary/Utils.java
+++ b/src/com/android/car/rotary/Utils.java
@@ -82,17 +82,34 @@
return null;
}
- /** Returns whether the given {@code node} can be focused by a rotary controller. */
+ /**
+ * Returns whether the given {@code node} can be focused by rotating or nudging the rotary
+ * controller.
+ * <ul>
+ * <li>To be reachable via the rotary controller, a node must be able to perform {@link
+ * AccessibilityNodeInfo#ACTION_FOCUS}, which requires the node to be visible to the
+ * user, focusable, and enabled.
+ * <li>In addition, though a {@link FocusParkingView} can perform {@link
+ * AccessibilityNodeInfo#ACTION_FOCUS}, it can't be reached directly via the rotary
+ * controller.
+ * <li>If a node is a focusable container, it can be reached via the rotary controller only
+ * when it has no descendants to take focus.
+ * </ul>
+ *
+ */
static boolean canTakeFocus(@NonNull AccessibilityNodeInfo node) {
return node.isVisibleToUser() && node.isFocusable() && node.isEnabled()
- && !isFocusParkingView(node);
+ && !isFocusParkingView(node)
+ && (!isScrollableContainer(node) || !descendantCanTakeFocus(node));
}
/** Returns whether the given {@code node} or its descendants can take focus. */
static boolean canHaveFocus(@NonNull AccessibilityNodeInfo node) {
- if (canTakeFocus(node)) {
- return true;
- }
+ return canTakeFocus(node) || descendantCanTakeFocus(node);
+ }
+
+ /** Returns whether the given {@code node}'s descendants can take focus. */
+ static boolean descendantCanTakeFocus(@NonNull AccessibilityNodeInfo node) {
for (int i = 0; i < node.getChildCount(); i++) {
AccessibilityNodeInfo childNode = node.getChild(i);
if (childNode != null) {
diff --git a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
index fcef14c..96058b8 100644
--- a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
+++ b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
@@ -15,6 +15,8 @@
*/
package com.android.car.rotary;
+import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.AdditionalAnswers.returnsFirstArg;
@@ -32,7 +34,6 @@
import com.android.car.rotary.Navigator.FindRotateTargetResult;
import com.android.car.ui.FocusArea;
import com.android.car.ui.FocusParkingView;
-import com.android.car.ui.utils.RotaryConstants;
import org.junit.Before;
import org.junit.Test;
@@ -136,24 +137,19 @@
when(button3.focusSearch(direction)).thenReturn(null);
// Rotate once, the focus should move from button1 to button2.
- FindRotateTargetResult target = mNavigator.findRotateTarget(button1, null, direction, 1);
+ FindRotateTargetResult target = mNavigator.findRotateTarget(button1, direction, 1);
assertThat(target.node).isSameAs(button2);
assertThat(target.advancedCount).isEqualTo(1);
// Rotate twice, the focus should move from button1 to button3.
- target = mNavigator.findRotateTarget(button1, null, direction, 2);
+ target = mNavigator.findRotateTarget(button1, direction, 2);
assertThat(target.node).isSameAs(button3);
assertThat(target.advancedCount).isEqualTo(2);
// Rotate 3 times and exceed the boundary, the focus should stay at the boundary.
- target = mNavigator.findRotateTarget(button1, null, direction, 3);
+ target = mNavigator.findRotateTarget(button1, direction, 3);
assertThat(target.node).isSameAs(button3);
assertThat(target.advancedCount).isEqualTo(2);
-
- // Rotate once, skipping button2; the focus should move to button3.
- target = mNavigator.findRotateTarget(button1, button2, direction, 1);
- assertThat(target.node).isSameAs(button3);
- assertThat(target.advancedCount).isEqualTo(1);
}
/**
@@ -206,7 +202,7 @@
when(focusParkingView.focusSearch(direction)).thenReturn(button1);
// Rotate at the end of focus area, no wrap-around should happen.
- FindRotateTargetResult target = mNavigator.findRotateTarget(button2, null, direction, 1);
+ FindRotateTargetResult target = mNavigator.findRotateTarget(button2, direction, 1);
assertThat(target).isNull();
}
@@ -252,11 +248,100 @@
when(focusParkingView.focusSearch(direction)).thenReturn(button1);
// Rotate at the end of focus area, no wrap-around should happen.
- FindRotateTargetResult target = mNavigator.findRotateTarget(button2, null, direction, 1);
+ FindRotateTargetResult target = mNavigator.findRotateTarget(button2, direction, 1);
assertThat(target).isNull();
}
/**
+ * Tests {@link Navigator#findRotateTarget} in the following node tree:
+ * <pre>
+ * root
+ * / | \
+ * / | \
+ * / | \
+ * button1 scrollable button2
+ * container
+ * |
+ * non-focusable
+ * </pre>
+ */
+ @Test
+ public void testFindRotateTargetReturnScrollableContainer() {
+ AccessibilityNodeInfo root = new NodeBuilder().setNodeList(mNodeList).build();
+ AccessibilityNodeInfo button1 = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(root)
+ .build();
+ AccessibilityNodeInfo button2 = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(root)
+ .build();
+ AccessibilityNodeInfo scrollableContainer = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(root)
+ .setContentDescription(ROTARY_VERTICALLY_SCROLLABLE)
+ .build();
+ AccessibilityNodeInfo nonFocusable = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(scrollableContainer)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .build();
+
+ int direction = View.FOCUS_FORWARD;
+ when(button1.focusSearch(direction)).thenReturn(scrollableContainer);
+ when(scrollableContainer.focusSearch(direction)).thenReturn(button2);
+
+ FindRotateTargetResult target = mNavigator.findRotateTarget(button1, direction, 1);
+ assertThat(target.node).isSameAs(scrollableContainer);
+ }
+
+ /**
+ * Tests {@link Navigator#findRotateTarget} in the following node tree:
+ * <pre>
+ * root
+ * / | \
+ * / | \
+ * / | \
+ * button1 scrollable button2
+ * container
+ * |
+ * focusable
+ * </pre>
+ */
+ @Test
+ public void testFindRotateTargetSkipScrollableContainer() {
+ AccessibilityNodeInfo root = new NodeBuilder().setNodeList(mNodeList).build();
+ AccessibilityNodeInfo button1 = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(root)
+ .build();
+ AccessibilityNodeInfo button2 = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(root)
+ .build();
+ AccessibilityNodeInfo scrollableContainer = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(root)
+ .setContentDescription(ROTARY_VERTICALLY_SCROLLABLE)
+ .build();
+ AccessibilityNodeInfo focusable = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setParent(scrollableContainer)
+ .setFocusable(true)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .build();
+
+ int direction = View.FOCUS_FORWARD;
+ when(button1.focusSearch(direction)).thenReturn(scrollableContainer);
+ when(scrollableContainer.focusSearch(direction)).thenReturn(button2);
+
+ FindRotateTargetResult target = mNavigator.findRotateTarget(button1, direction, 1);
+ assertThat(target.node).isSameAs(button2);
+ }
+
+ /**
* Tests {@link Navigator#findRotateTarget} in the following layout:
* <pre>
* ============ focus area ============
@@ -294,7 +379,7 @@
.setNodeList(mNodeList)
.setParent(focusArea)
.setFocusable(true)
- .setContentDescription(RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE)
+ .setContentDescription(ROTARY_VERTICALLY_SCROLLABLE)
.setActionList(new ArrayList<>(Collections.singletonList(
AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD)))
.setBoundsInScreen(new Rect(0, 0, 100, 100))
@@ -322,18 +407,18 @@
when(button3.focusSearch(direction)).thenReturn(null);
// Rotate once, the focus should move from button1 to button2.
- FindRotateTargetResult target = mNavigator.findRotateTarget(button1, null, direction, 1);
+ FindRotateTargetResult target = mNavigator.findRotateTarget(button1, direction, 1);
assertThat(target.node).isSameAs(button2);
assertThat(target.advancedCount).isEqualTo(1);
// Rotate twice, the focus should move from button1 to button2 since button3 is out of
// bounds.
- target = mNavigator.findRotateTarget(button1, null, direction, 2);
+ target = mNavigator.findRotateTarget(button1, direction, 2);
assertThat(target.node).isSameAs(button2);
assertThat(target.advancedCount).isEqualTo(1);
// Rotate three times should do the same.
- target = mNavigator.findRotateTarget(button1, null, direction, 3);
+ target = mNavigator.findRotateTarget(button1, direction, 3);
assertThat(target.node).isSameAs(button2);
assertThat(target.advancedCount).isEqualTo(1);
}
@@ -1201,6 +1286,204 @@
}
/**
+ * Tests {@link Navigator#findNudgeTarget} in the following layout:
+ * <pre>
+ * **********leftWindow********* ****************rightWindow*****************
+ * * * * *
+ * * ===left focus area=== * * ==========right focus area======== *
+ * * = = * * = = *
+ * * = = * * = ....scrollableContainer..... = *
+ * * = = * * = . . = *
+ * * = left = * * = . non-focusable . = *
+ * * = = * * = . . = *
+ * * = = * * = ............................ = *
+ * * = = * * = = *
+ * * ===================== * * ================================== *
+ * * * * *
+ * ***************************** ********************************************
+ * </pre>
+ */
+ @Test
+ public void testFindNudgeTargetReturnScrollableContainer() {
+ // There are 2 windows. This is the left window.
+ Rect leftWindowBounds = new Rect(0, 0, 400, 400);
+ AccessibilityWindowInfo leftWindow = new WindowBuilder()
+ .setBoundsInScreen(leftWindowBounds)
+ .build();
+ AccessibilityNodeInfo leftRoot = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(leftWindow)
+ .setBoundsInScreen(leftWindowBounds)
+ .build();
+ setRootNodeForWindow(leftRoot, leftWindow);
+
+ // Left focus area and its view inside.
+ AccessibilityNodeInfo leftFocusArea = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(leftWindow)
+ .setParent(leftRoot)
+ .setClassName(FOCUS_AREA_CLASS_NAME)
+ .setBoundsInScreen(new Rect(0, 0, 400, 400))
+ .build();
+ AccessibilityNodeInfo left = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(leftWindow)
+ .setParent(leftFocusArea)
+ .setFocusable(true)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .setBoundsInScreen(new Rect(0, 0, 400, 400))
+ .build();
+
+ // Right window.
+ Rect rightWindowBounds = new Rect(400, 0, 800, 400);
+ AccessibilityWindowInfo rightWindow = new WindowBuilder()
+ .setBoundsInScreen(rightWindowBounds)
+ .build();
+ AccessibilityNodeInfo rightRoot = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setBoundsInScreen(rightWindowBounds)
+ .build();
+ setRootNodeForWindow(rightRoot, rightWindow);
+
+ // Right focus area and its view inside.
+ AccessibilityNodeInfo rightFocusArea = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setParent(rightRoot)
+ .setClassName(FOCUS_AREA_CLASS_NAME)
+ .setBoundsInScreen(new Rect(400, 0, 800, 400))
+ .build();
+ AccessibilityNodeInfo scrollableContainer = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setParent(rightFocusArea)
+ .setFocusable(true)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .setBoundsInScreen(new Rect(400, 0, 800, 400))
+ .setContentDescription(ROTARY_VERTICALLY_SCROLLABLE)
+ .build();
+ AccessibilityNodeInfo nonFocusable = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setParent(scrollableContainer)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .setBoundsInScreen(new Rect(400, 0, 800, 400))
+ .build();
+
+ List<AccessibilityWindowInfo> windows = new ArrayList<>();
+ windows.add(leftWindow);
+ windows.add(rightWindow);
+
+ // Nudge from left window to right window.
+ AccessibilityNodeInfo target = mNavigator.findNudgeTarget(windows, left, View.FOCUS_RIGHT);
+ assertThat(target).isSameAs(scrollableContainer);
+ }
+
+
+ /**
+ * Tests {@link Navigator#findNudgeTarget} in the following layout:
+ * <pre>
+ * **********leftWindow********* ****************rightWindow*****************
+ * * * * *
+ * * ===left focus area=== * * ==========right focus area======== *
+ * * = = * * = = *
+ * * = = * * = ....scrollableContainer..... = *
+ * * = = * * = . . = *
+ * * = left = * * = . focusable . = *
+ * * = = * * = . . = *
+ * * = = * * = ............................ = *
+ * * = = * * = = *
+ * * ===================== * * ================================== *
+ * * * * *
+ * ***************************** ********************************************
+ * </pre>
+ */
+ @Test
+ public void testFindNudgeTargetSkipScrollableContainer() {
+ // There are 2 windows. This is the left window.
+ Rect leftWindowBounds = new Rect(0, 0, 400, 400);
+ AccessibilityWindowInfo leftWindow = new WindowBuilder()
+ .setBoundsInScreen(leftWindowBounds)
+ .build();
+ AccessibilityNodeInfo leftRoot = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(leftWindow)
+ .setBoundsInScreen(leftWindowBounds)
+ .build();
+ setRootNodeForWindow(leftRoot, leftWindow);
+
+ // Left focus area and its view inside.
+ AccessibilityNodeInfo leftFocusArea = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(leftWindow)
+ .setParent(leftRoot)
+ .setClassName(FOCUS_AREA_CLASS_NAME)
+ .setBoundsInScreen(new Rect(0, 0, 400, 400))
+ .build();
+ AccessibilityNodeInfo left = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(leftWindow)
+ .setParent(leftFocusArea)
+ .setFocusable(true)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .setBoundsInScreen(new Rect(0, 0, 400, 400))
+ .build();
+
+ // Right window.
+ Rect rightWindowBounds = new Rect(400, 0, 800, 400);
+ AccessibilityWindowInfo rightWindow = new WindowBuilder()
+ .setBoundsInScreen(rightWindowBounds)
+ .build();
+ AccessibilityNodeInfo rightRoot = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setBoundsInScreen(rightWindowBounds)
+ .build();
+ setRootNodeForWindow(rightRoot, rightWindow);
+
+ // Right focus area and its view inside.
+ AccessibilityNodeInfo rightFocusArea = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setParent(rightRoot)
+ .setClassName(FOCUS_AREA_CLASS_NAME)
+ .setBoundsInScreen(new Rect(400, 0, 800, 400))
+ .build();
+ AccessibilityNodeInfo scrollableContainer = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setParent(rightFocusArea)
+ .setFocusable(true)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .setBoundsInScreen(new Rect(400, 0, 800, 400))
+ .setContentDescription(ROTARY_VERTICALLY_SCROLLABLE)
+ .build();
+ AccessibilityNodeInfo focusable = new NodeBuilder()
+ .setNodeList(mNodeList)
+ .setWindow(rightWindow)
+ .setParent(scrollableContainer)
+ .setFocusable(true)
+ .setVisibleToUser(true)
+ .setEnabled(true)
+ .setBoundsInScreen(new Rect(400, 0, 800, 400))
+ .build();
+
+ List<AccessibilityWindowInfo> windows = new ArrayList<>();
+ windows.add(leftWindow);
+ windows.add(rightWindow);
+
+ // Nudge from left window to right window.
+ AccessibilityNodeInfo target = mNavigator.findNudgeTarget(windows, left, View.FOCUS_RIGHT);
+ assertThat(target).isSameAs(focusable);
+ }
+
+ /**
* Tests {@link Navigator#findFirstFocusDescendant} in the following node tree:
* <pre>
* root
@@ -1357,7 +1640,7 @@
.setNodeList(mNodeList)
.setParent(focusArea)
.setFocusable(true)
- .setContentDescription(RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE)
+ .setContentDescription(ROTARY_VERTICALLY_SCROLLABLE)
.build();
AccessibilityNodeInfo container = new NodeBuilder()
.setNodeList(mNodeList)