Refactor RotaryService and Navigator

1. Update handleViewFocusedEvent(). The focus will be adjusted properly
by the framework due to a recent change, so RotaryService doesn't need
to adjust the focus any more. As a result, PendingFocusedNodes class is
not needed any more.
2. Remove RotaryCache since the history is kept in FocusArea class due
to a recent change.
3. Due to a recent change, the nudge target finding is mostly done on
the app side. So update the logic in RotaryService accordingly.
4. Remove failing roboletric tests. Since roboletric tests are being
deprecated, don't bother updating them to make them pass. Equivalent
instrumented tests will be added later.
5. Replace NodePredicate with Predicate in Java library.

Bug: 168844996
Bug: 170344916
Test: atest CarRotaryControllerRoboTests, and manual test
Change-Id: Ib3199df71da625f5dd907f23bc6b8625a121ca36
Merged-In: Ib3199df71da625f5dd907f23bc6b8625a121ca36
diff --git a/res/values/integers.xml b/res/values/integers.xml
index 22e323f..15d24ec 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -25,49 +25,9 @@
     rotate event is smaller than this value, we'll treat the rotation event as 2 rotations. -->
     <integer name="rotation_acceleration_2x_ms">40</integer>
 
-    <!-- Values for FocusHistoryCache, which saves last focused node by FocusArea. -->
-    <!-- Type of FocusHistoryCache. The values are defined in RotaryCache. 1 means the cache
-    is disabled, 2 means entries in the cache will expire after a period of time, and 3 means
-    elements in the cache will never expire as long as RotaryService is alive. -->
-    <integer name="focus_history_cache_type">2</integer>
-    <!-- How many milliseconds before an entry in FocusHistoryCache expires. Must be positive value
-    when focus_history_cache_type is 2. -->
-    <integer name="focus_history_expiration_time_ms">10000</integer>
-    <!-- Size of FocusHistoryCache. -->
-    <integer name="focus_history_cache_size">10</integer>
-
-    <!-- Values for FocusAreaHistoryCache, which saves target FocusArea by source FocusArea and
-    direction. -->
-    <!-- Type of FocusAreaHistoryCache. The values are defined in RotaryCache. 1 means the
-    cache is disabled, 2 means entries in the cache will expire after a period of time, and 3 means
-    elements in the cache will never expire as long as RotaryService is alive. -->
-    <integer name="focus_area_history_cache_type">2</integer>
-    <!-- How many milliseconds before an entry in FocusAreaHistoryCache expires. Must be positive
-    value when focus_history_cache_type is 2. -->
-    <integer name="focus_area_history_expiration_time_ms">10000</integer>
-    <!-- Size of FocusAreaHistoryCache. -->
-    <integer name="focus_area_history_cache_size">5</integer>
-
-    <!-- Values for FocusWindowCache, which saves the last focused node for each window. -->
-    <!-- Type of FocusWindowCache. The values are defined in RotaryCache. 1 means the
-    cache is disabled, 2 means entries in the cache will expire after a period of time, and 3 means
-    elements in the cache will never expire as long as RotaryService is alive. -->
-    <integer name="focus_window_cache_type">2</integer>
-    <!-- How many milliseconds before an entry in FocusWindowCache expires. Must be positive
-    value when focus_window_cache_type is 2. -->
-    <integer name="focus_window_expiration_time_ms">600000</integer>
-    <!-- Size of FocusWindowCache. -->
-    <integer name="focus_window_cache_size">5</integer>
-
     <!-- How many milliseconds to ignore TYPE_VIEW_CLICKED events after performing ACTION_CLICK or
     injecting KEYCODE_DPAD_CENTER. -->
     <integer name="ignore_view_clicked_ms">200</integer>
     <!-- How many milliseconds to wait for TYPE_VIEW_SCROLLED events after scrolling. -->
     <integer name="after_scroll_timeout_ms">200</integer>
-    <!-- How many milliseconds to wait for TYPE_VIEW_FOCUSED events after performing ACTION_FOCUS.
-    -->
-    <integer name="after_focus_timeout_ms">200</integer>
-    <!-- How many milliseconds to ignore TYPE_VIEW_FOCUSED events of FocusParkingView after
-    a WINDOWS_CHANGE_ADDED event. -->
-    <integer name="ignore_fpv_focused_ms">200</integer>
 </resources>
diff --git a/src/com/android/car/rotary/Navigator.java b/src/com/android/car/rotary/Navigator.java
index f418907..f6a7e08 100644
--- a/src/com/android/car/rotary/Navigator.java
+++ b/src/com/android/car/rotary/Navigator.java
@@ -19,7 +19,6 @@
 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD;
 
 import android.graphics.Rect;
-import android.os.SystemClock;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityWindowInfo;
@@ -34,6 +33,7 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Predicate;
 
 /**
  * A helper class used for finding the next focusable node when the rotary controller is rotated or
@@ -47,191 +47,31 @@
     @NonNull
     private final TreeTraverser mTreeTraverser = new TreeTraverser();
 
-    private final RotaryCache mRotaryCache;
-
     private final int mHunLeft;
     private final int mHunRight;
 
     @View.FocusRealDirection
     private int mHunNudgeDirection;
 
-    Navigator(@RotaryCache.CacheType int focusHistoryCacheType,
-            int focusHistoryCacheSize,
-            int focusHistoryExpirationTimeMs,
-            @RotaryCache.CacheType int focusAreaHistoryCacheType,
-            int focusAreaHistoryCacheSize,
-            int focusAreaHistoryExpirationTimeMs,
-            @RotaryCache.CacheType int focusWindowCacheType,
-            int focusWindowCacheSize,
-            int focusWindowExpirationTimeMs,
-            int hunLeft,
-            int hunRight,
-            boolean showHunOnBottom) {
-        mRotaryCache = new RotaryCache(focusHistoryCacheType,
-                focusHistoryCacheSize,
-                focusHistoryExpirationTimeMs,
-                focusAreaHistoryCacheType,
-                focusAreaHistoryCacheSize,
-                focusAreaHistoryExpirationTimeMs,
-                focusWindowCacheType,
-                focusWindowCacheSize,
-                focusWindowExpirationTimeMs);
+    Navigator(int hunLeft, int hunRight, boolean showHunOnBottom) {
         mHunLeft = hunLeft;
         mHunRight = hunRight;
         mHunNudgeDirection = showHunOnBottom ? View.FOCUS_DOWN : View.FOCUS_UP;
     }
 
-    /** Clears focus area history cache. */
-    void clearFocusAreaHistory() {
-        mRotaryCache.clearFocusAreaHistory();
-    }
-
-    /** Caches the focused node by focus area and by window. */
-    void saveFocusedNode(@NonNull AccessibilityNodeInfo focusedNode) {
-        long elapsedRealtime = SystemClock.elapsedRealtime();
-        AccessibilityNodeInfo focusArea = getAncestorFocusArea(focusedNode);
-        mRotaryCache.saveFocusedNode(focusArea, focusedNode, elapsedRealtime);
-        mRotaryCache.saveWindowFocus(focusedNode, elapsedRealtime);
-        Utils.recycleNode(focusArea);
-    }
-
     /**
-     * Returns the most recently focused valid node in window {@code windowId}, or {@code null} if
-     * there are no valid nodes saved by {@link #saveFocusedNode}. The caller is responsible for
-     * recycling the result.
-     */
-    AccessibilityNodeInfo getMostRecentFocus(int windowId) {
-        return mRotaryCache.getMostRecentFocus(windowId, SystemClock.elapsedRealtime());
-    }
-
-    /**
-     * Returns the target focusable for a nudge. Convenience method for when the {@code editNode} is
-     * null.
-     *
-     * @see #findNudgeTarget(List, AccessibilityNodeInfo, int, AccessibilityNodeInfo)
-     */
-    @Nullable
-    AccessibilityNodeInfo findNudgeTarget(@NonNull List<AccessibilityWindowInfo> windows,
-            @NonNull AccessibilityNodeInfo sourceNode, int direction) {
-        return findNudgeTarget(windows, sourceNode, direction, null);
-    }
-
-    /**
-     * Returns the target focusable for a nudge:
-     * <ol>
-     *     <li>If the HUN is present and the nudge is towards it, a focusable in the HUN is
-     *         returned. See {@link #findHunNudgeTarget} for details.
-     *     <li>If the nudge is leaving the IME, return focus to the view that was left focused when
-     *         the IME appeared.
-     *     <li>Otherwise, a target focus area is chosen, either from the focus area history or by
-     *         choosing the best candidate. See {@link #findNudgeTargetFocusArea} for details.
-     *     <li>Finally a focusable view within the chosen focus area is chosen, either from the
-     *         focus history or by choosing the best candidate.
-     * </ol>
-     * Saves nudge history except when nudging out of the IME. The caller is responsible for
-     * recycling the result.
+     * Returns a target node representing a HUN FocusArea for a nudge in {@code direction}. The
+     * caller is responsible for recycling the result.
      *
      * @param windows    a list of windows to search from
      * @param sourceNode the current focus
      * @param direction  nudge direction, must be {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
      *                   {@link View#FOCUS_LEFT}, or {@link View#FOCUS_RIGHT}
-     * @param editNode   node currently being edited by the IME, if any
-     * @return a view that can take focus (visible, focusable and enabled) within another {@link
-     *         FocusArea}, which is in the given {@code direction} from the current {@link
-     *         FocusArea}, or null if not found
+     * @return a node representing a HUN FocusArea, or null if the HUN isn't present, the nudge
+     *         isn't in the direction of the HUN, or the HUN contains no views that can take focus
      */
     @Nullable
-    AccessibilityNodeInfo findNudgeTarget(@NonNull List<AccessibilityWindowInfo> windows,
-            @NonNull AccessibilityNodeInfo sourceNode, int direction,
-            @Nullable AccessibilityNodeInfo editNode) {
-        // If the user is trying to nudge to the HUN, search for a focus area in the HUN window.
-        AccessibilityNodeInfo hunNudgeTarget = findHunNudgeTarget(windows, sourceNode, direction);
-        if (hunNudgeTarget != null) {
-            return hunNudgeTarget;
-        }
-
-        long elapsedRealtime = SystemClock.elapsedRealtime();
-        AccessibilityNodeInfo currentFocusArea = getAncestorFocusArea(sourceNode);
-        AccessibilityNodeInfo targetFocusArea =
-                findNudgeTargetFocusArea(windows, sourceNode, currentFocusArea, direction);
-        if (targetFocusArea == null) {
-            Utils.recycleNode(currentFocusArea);
-            return null;
-        }
-
-        // If the user is nudging out of an IME, return to the field they were editing, if any.
-        // Don't save nudge history in this case.
-        AccessibilityWindowInfo sourceWindow =
-                Utils.findWindowWithId(windows, sourceNode.getWindowId());
-        AccessibilityWindowInfo targetWindow =
-                Utils.findWindowWithId(windows, targetFocusArea.getWindowId());
-        if (sourceWindow != null && targetWindow != null
-                && sourceWindow.getType() == AccessibilityWindowInfo.TYPE_INPUT_METHOD
-                && targetWindow.getType() != AccessibilityWindowInfo.TYPE_INPUT_METHOD) {
-            if (editNode != null && editNode.getWindowId() == targetWindow.getId()) {
-                Utils.recycleNode(currentFocusArea);
-                Utils.recycleNode(targetFocusArea);
-                return copyNode(editNode);
-            }
-        }
-
-        // Return the recently focused node within the target focus area, if any.
-        AccessibilityNodeInfo cachedFocusedNode =
-                mRotaryCache.getFocusedNode(targetFocusArea, elapsedRealtime);
-        if (cachedFocusedNode != null) {
-            // Save nudge history.
-            mRotaryCache.saveTargetFocusArea(
-                    currentFocusArea, targetFocusArea, direction, elapsedRealtime);
-
-            Utils.recycleNode(currentFocusArea);
-            Utils.recycleNode(targetFocusArea);
-            return cachedFocusedNode;
-        }
-
-        // Make a list of candidate nodes in the target FocusArea.
-        List<AccessibilityNodeInfo> candidateNodes = new ArrayList<>();
-        addFocusDescendants(targetFocusArea, candidateNodes);
-
-        // Choose the best candidate as the target node.
-        AccessibilityNodeInfo bestCandidate =
-                chooseBestNudgeCandidate(sourceNode, candidateNodes, direction);
-
-        // Save nudge history if we're going to move focus.
-        if (bestCandidate != null) {
-            mRotaryCache.saveTargetFocusArea(
-                    currentFocusArea, targetFocusArea, direction, elapsedRealtime);
-        }
-
-        Utils.recycleNode(currentFocusArea);
-        Utils.recycleNodes(candidateNodes);
-        Utils.recycleNode(targetFocusArea);
-        return bestCandidate;
-    }
-
-    /**
-     * Returns the target focusable for a nudge to the HUN if the HUN is present and the nudge is
-     * in the right direction. The target focusable is chosen as follows:
-     * <ol>
-     *     <li>The best candidate focus area is chosen. If there aren't any valid candidates, the
-     *         first (only) focus area in the HUN is used. This happens when nudging from a view
-     *         obscured by the HUN.
-     *     <li>The focus history is checked. If one of the focusable views in the chosen focus area
-     *         is in the cache, it's returned.
-     *     <li>Finally the best candidate focusable view in the chosen focus area is selected.
-     *         Again, if there aren't any candidates, the first focusable view is chosen.
-     * </ol>
-     * The caller is responsible for recycling the result.
-     *
-     * @param windows    a list of windows to search from
-     * @param sourceNode the current focus
-     * @param direction  nudge direction, must be {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
-     *                   {@link View#FOCUS_LEFT}, or {@link View#FOCUS_RIGHT}
-     * @return a view that can take focus (visible, focusable and enabled) within the HUN, or null
-     *         if the HUN isn't present, the nudge isn't in the direction of the HUN, or the HUN
-     *         contains no views that can take focus
-     */
-    @Nullable
-    AccessibilityNodeInfo findHunNudgeTarget(@NonNull List<AccessibilityWindowInfo> windows,
+    AccessibilityNodeInfo findHunFocusArea(@NonNull List<AccessibilityWindowInfo> windows,
             @NonNull AccessibilityNodeInfo sourceNode, int direction) {
         if (direction != mHunNudgeDirection) {
             return null;
@@ -260,38 +100,7 @@
             targetFocusArea = copyNode(hunFocusAreas.get(0));
         }
         Utils.recycleNodes(hunFocusAreas);
-        if (targetFocusArea == null) {
-            return null;
-        }
-
-        // Save nudge history.
-        AccessibilityNodeInfo currentFocusArea = getAncestorFocusArea(sourceNode);
-        long elapsedRealtime = SystemClock.elapsedRealtime();
-        mRotaryCache.saveTargetFocusArea(
-                currentFocusArea, targetFocusArea, direction, elapsedRealtime);
-
-        // Check the cache to see if a node was focused in the HUN.
-        AccessibilityNodeInfo cachedFocusedNode =
-                mRotaryCache.getFocusedNode(targetFocusArea, elapsedRealtime);
-        if (cachedFocusedNode != null) {
-            Utils.recycleNode(targetFocusArea);
-            Utils.recycleNode(currentFocusArea);
-            return cachedFocusedNode;
-        }
-
-        // Choose the best candidate target node. The HUN may overlap the source node, in which
-        // case the geometric search will fail. The fallback is to use the first focusable node.
-        List<AccessibilityNodeInfo> candidateNodes = new ArrayList<>();
-        addFocusDescendants(targetFocusArea, candidateNodes);
-        AccessibilityNodeInfo bestCandidate =
-                chooseBestNudgeCandidate(sourceNode, candidateNodes, direction);
-        if (bestCandidate == null && !candidateNodes.isEmpty()) {
-            bestCandidate = copyNode(candidateNodes.get(0));
-        }
-        Utils.recycleNodes(candidateNodes);
-        Utils.recycleNode(targetFocusArea);
-        Utils.recycleNode(currentFocusArea);
-        return bestCandidate;
+        return targetFocusArea;
     }
 
     /**
@@ -402,141 +211,56 @@
         return target == null ? null : new FindRotateTargetResult(target, advancedCount);
     }
 
-    /**
-     * Searches the {@code rootNode} and its descendants in depth-first order, and returns the first
-     * focus descendant (a node inside a focus area that can take focus) if any, or returns null if
-     * not found. The caller is responsible for recycling the result.
-     */
-    AccessibilityNodeInfo findFirstFocusDescendant(@NonNull AccessibilityNodeInfo rootNode) {
-        // First try finding the first focus area and searching forward from the focus area. This
-        // is a quick way to find the first node but it doesn't always work.
-        AccessibilityNodeInfo focusDescendant = findFirstFocus(rootNode);
-        if (focusDescendant != null) {
-            return focusDescendant;
-        }
-
-        // Fall back to tree traversal.
-        L.w("Falling back to tree traversal");
-        focusDescendant = findDepthFirstFocus(rootNode);
-        if (focusDescendant == null) {
-            L.w("No node can take focus in the current window");
-        }
-        return focusDescendant;
-    }
-
     /** Sets a mock Utils instance for testing. */
     @VisibleForTesting
     void setNodeCopier(@NonNull NodeCopier nodeCopier) {
         mNodeCopier = nodeCopier;
         mTreeTraverser.setNodeCopier(nodeCopier);
-        mRotaryCache.setNodeCopier(nodeCopier);
     }
 
     /**
-     * Searches all the nodes in the {@code window}, and returns the node representing a {@link
+     * Searches the window containing {@code node}, and returns the node representing a {@link
      * FocusParkingView}, if any, or returns null if not found. The caller is responsible for
      * recycling the result.
      */
-    AccessibilityNodeInfo findFocusParkingView(@NonNull AccessibilityWindowInfo window) {
-        AccessibilityNodeInfo root = window.getRoot();
-        if (root == null) {
-            L.e("No root node in " + window);
+    @Nullable
+    AccessibilityNodeInfo findFocusParkingView(@NonNull AccessibilityNodeInfo node) {
+        AccessibilityWindowInfo window = node.getWindow();
+        if (window == null) {
+            L.w("Failed to get window for node " + node);
             return null;
         }
-        AccessibilityNodeInfo focusParkingView = findFocusParkingView(root);
-        root.recycle();
-        return focusParkingView;
-    }
-
-    /**
-     * Searches the {@code node} and its descendants in depth-first order, and returns the node
-     * representing a {@link FocusParkingView}, if any, or returns null if not found. The caller is
-     * responsible for recycling the result.
-     */
-    private AccessibilityNodeInfo findFocusParkingView(@NonNull AccessibilityNodeInfo node) {
-        return mTreeTraverser.depthFirstSearch(node, /* skipPredicate= */ Utils::isFocusArea,
+        AccessibilityNodeInfo root = window.getRoot();
+        window.recycle();
+        if (root == null) {
+            L.e("No root node that contains " + node);
+            return null;
+        }
+        AccessibilityNodeInfo fpv = mTreeTraverser.depthFirstSearch(
+                root,
+                /* skipPredicate= */ Utils::isFocusArea,
                 /* targetPredicate= */ Utils::isFocusParkingView);
+        root.recycle();
+        return fpv;
     }
 
     /**
-     * Searches the {@code rootNode} and its descendants in depth-first order for the first focus
-     * area, and returns the first node that can take focus in tab order from the focus area.
-     * The return value could be a node inside or outside the first focus area, or null if not
-     * found. The caller is responsible for recycling result.
-     */
-    private AccessibilityNodeInfo findFirstFocus(@NonNull AccessibilityNodeInfo rootNode) {
-        AccessibilityNodeInfo focusArea = findFirstFocusArea(rootNode);
-        if (focusArea == null) {
-            L.w("No FocusArea in the tree");
-            // rootNode is an implicit focus area if no explicit FocusAreas are specified.
-            focusArea = copyNode(rootNode);
-        }
-
-        AccessibilityNodeInfo targetNode = focusArea.focusSearch(View.FOCUS_FORWARD);
-        AccessibilityNodeInfo firstTarget = copyNode(targetNode);
-        // focusSearch() searches in the active window, which has at least one FocusParkingView. We
-        // need to skip it.
-        while (targetNode != null && Utils.isFocusParkingView(targetNode)) {
-            L.d("Found FocusParkingView, continue focusSearch() ...");
-            AccessibilityNodeInfo nextTargetNode = targetNode.focusSearch(View.FOCUS_FORWARD);
-            targetNode.recycle();
-            targetNode = nextTargetNode;
-
-            // If we found the same FocusParkingView again, it means all the focusable views in
-            // current window are FocusParkingViews, so we should just return null.
-            if (firstTarget.equals(targetNode)) {
-                L.w("Stop focusSearch() because there is no view to take focus except "
-                        + "FocusParkingViews");
-                Utils.recycleNode(targetNode);
-                targetNode = null;
-                break;
-            }
-        }
-        Utils.recycleNode(firstTarget);
-        focusArea.recycle();
-        return targetNode;
-    }
-
-    /**
-     * Searches the given {@code node} and its descendants in depth-first order, and returns the
-     * first {@link FocusArea}, or returns null if not found. The caller is responsible for
-     * recycling the result.
-     */
-    private AccessibilityNodeInfo findFirstFocusArea(@NonNull AccessibilityNodeInfo node) {
-        return mTreeTraverser.depthFirstSearch(node, Utils::isFocusArea);
-    }
-
-    /**
-     * Searches the given {@code node} and its descendants in depth-first order, and returns the
-     * first node that can take focus, or returns null if not found. The caller is responsible for
-     * recycling result.
-     */
-    private AccessibilityNodeInfo findDepthFirstFocus(@NonNull AccessibilityNodeInfo node) {
-        return mTreeTraverser.depthFirstSearch(node, Utils::canTakeFocus);
-    }
-
-    /**
-     * Returns the target focus area for a nudge in the given {@code direction} from the current
-     * focus, or null if not found. Checks the cache first. If nothing is found in the cache,
-     * returns the best nudge target from among all the candidate focus areas. The caller is
-     * responsible for updating the cache and recycling the result.
+     * Returns the best target focus area for a nudge in the given {@code direction}. The caller is
+     * responsible for recycling the result.
+     *
+     * @param windows          a list of windows to search from
+     * @param sourceNode       the current focus
+     * @param currentFocusArea the current focus area
+     * @param direction        nudge direction, must be {@link View#FOCUS_UP}, {@link
+     *                         View#FOCUS_DOWN}, {@link View#FOCUS_LEFT}, or {@link
+     *                         View#FOCUS_RIGHT}
      */
     AccessibilityNodeInfo findNudgeTargetFocusArea(
             @NonNull List<AccessibilityWindowInfo> windows,
-            @NonNull AccessibilityNodeInfo focusedNode,
+            @NonNull AccessibilityNodeInfo sourceNode,
             @NonNull AccessibilityNodeInfo currentFocusArea,
             int direction) {
-        long elapsedRealtime = SystemClock.elapsedRealtime();
-        // If there is a target focus area in the cache, returns it.
-        AccessibilityNodeInfo cachedTargetFocusArea =
-                mRotaryCache.getTargetFocusArea(currentFocusArea, direction, elapsedRealtime);
-        if (cachedTargetFocusArea != null && Utils.canHaveFocus(cachedTargetFocusArea)) {
-            return cachedTargetFocusArea;
-        }
-        Utils.recycleNode(cachedTargetFocusArea);
-
-        // No target focus area in the cache; we need to search the node tree to find it.
-        AccessibilityWindowInfo currentWindow = focusedNode.getWindow();
+        AccessibilityWindowInfo currentWindow = sourceNode.getWindow();
         if (currentWindow == null) {
             L.e("Currently focused window is null");
             return null;
@@ -569,7 +293,7 @@
 
         // Choose the best candidate as our target focus area.
         AccessibilityNodeInfo targetFocusArea =
-                chooseBestNudgeCandidate(focusedNode, candidateFocusAreas, direction);
+                chooseBestNudgeCandidate(sourceNode, candidateFocusAreas, direction);
         Utils.recycleNodes(candidateFocusAreas);
         return targetFocusArea;
     }
@@ -655,7 +379,7 @@
      * HUN appears at the top or bottom of the screen and on the height of the notification being
      * displayed so they aren't used.
      */
-    boolean isHunWindow(@NonNull AccessibilityWindowInfo window) {
+    private boolean isHunWindow(@NonNull AccessibilityWindowInfo window) {
         if (window.getType() != AccessibilityWindowInfo.TYPE_SYSTEM) {
             return false;
         }
@@ -751,29 +475,6 @@
     }
 
     /**
-     * Adds the given {@code node} and all its focus descendants (nodes that can take focus) to the
-     * given list. The caller is responsible for recycling added nodes.
-     */
-    private void addFocusDescendants(@NonNull AccessibilityNodeInfo node,
-            @NonNull List<AccessibilityNodeInfo> results) {
-        // Include off-screen nodes within a WebView.
-        if (Utils.isWebView(node)) {
-            mTreeTraverser.depthFirstSelect(node, Utils::canPerformFocus, results);
-            return;
-        }
-
-        if (Utils.canTakeFocus(node)) {
-            results.add(node);
-        }
-        for (int i = 0; i < node.getChildCount(); i++) {
-            AccessibilityNodeInfo child = node.getChild(i);
-            if (child != null) {
-                addFocusDescendants(child, results);
-            }
-        }
-    }
-
-    /**
      * Returns a copy of the best candidate from among the given {@code candidates} for a nudge
      * from {@code sourceNode} in the given {@code direction}. Returns null if none of the {@code
      * candidates} are in the given {@code direction}. The caller is responsible for recycling the
@@ -873,7 +574,7 @@
      */
     @NonNull
     AccessibilityNodeInfo getAncestorFocusArea(@NonNull AccessibilityNodeInfo node) {
-        NodePredicate isFocusAreaOrRoot = candidateNode -> {
+        Predicate<AccessibilityNodeInfo> isFocusAreaOrRoot = candidateNode -> {
             if (Utils.isFocusArea(candidateNode)) {
                 // The candidateNode is a focus area.
                 return true;
diff --git a/src/com/android/car/rotary/NodePredicate.java b/src/com/android/car/rotary/NodePredicate.java
deleted file mode 100644
index 1ef8c49..0000000
--- a/src/com/android/car/rotary/NodePredicate.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.rotary;
-
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import androidx.annotation.NonNull;
-
-/** A function that takes an {@link AccessibilityNodeInfo} and returns a {@code boolean}. */
-interface NodePredicate {
-    boolean isTarget(@NonNull AccessibilityNodeInfo node);
-}
diff --git a/src/com/android/car/rotary/PendingFocusedNodes.java b/src/com/android/car/rotary/PendingFocusedNodes.java
deleted file mode 100644
index 5325678..0000000
--- a/src/com/android/car/rotary/PendingFocusedNodes.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.rotary;
-
-import android.os.SystemClock;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * Cache of nodes that have performed {@link AccessibilityNodeInfo#ACTION_FOCUS} but haven't
- * reported a {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} event yet.
- */
-class PendingFocusedNodes {
-    /**
-     * The map to store the nodes.
-     * <p>
-     * The key of an entry is the node. The value of an entry is its expire time, which is the time
-     * when it's added (in {@link SystemClock#uptimeMillis}) plus {@link #mTimeoutMs}.
-     * <p>
-     * An entry is added when the node just performed the focus action successfully, and it's
-     * removed when we receive the focus event for the node.
-     */
-    private final Map<AccessibilityNodeInfo, Long> mMap = new HashMap();
-
-    /** How many milliseconds will an entry expire after it's put in the cache. */
-    private long mTimeoutMs;
-
-    @NonNull
-    private NodeCopier mNodeCopier = new NodeCopier();
-
-    PendingFocusedNodes(long timeoutMs) {
-        mTimeoutMs = timeoutMs;
-    }
-
-    /** Returns whether the {@code node} is in the cache. */
-    boolean contains(@NonNull AccessibilityNodeInfo node) {
-        refresh();
-        return mMap.containsKey(node);
-    }
-
-    /** Puts a copy of the {@code node} in the cache. */
-    void put(@NonNull AccessibilityNodeInfo node) {
-        mMap.put(copyNode(node), getUptimeMs() + mTimeoutMs);
-    }
-
-    /** Returns whether the cache is empty. */
-    boolean isEmpty() {
-        refresh();
-        return mMap.isEmpty();
-    }
-
-    /**
-     * Searches for a node in the cache satisfying the given condition. If succeeded, removes the
-     * first found node and returns true. Otherwise returns false.
-     */
-    boolean removeFirstIf(@NonNull NodePredicate targetPredicate) {
-        for (Map.Entry<AccessibilityNodeInfo, Long> entry : mMap.entrySet()) {
-            AccessibilityNodeInfo node = entry.getKey();
-            if (targetPredicate.isTarget(node)) {
-                L.d("A node in mPendingFocusedNodes was removed");
-                mMap.remove(node);
-                node.recycle();
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Removes nodes saved in the cache if they're not in the view tree any more, or they're
-     * expired.
-     */
-    @VisibleForTesting
-    void refresh() {
-        long uptimeMs = getUptimeMs();
-        Iterator<Map.Entry<AccessibilityNodeInfo, Long>> iterator = mMap.entrySet().iterator();
-        while (iterator.hasNext()) {
-            Map.Entry<AccessibilityNodeInfo, Long> entry = iterator.next();
-            AccessibilityNodeInfo node = entry.getKey();
-            if (!node.refresh()) {
-                L.d("Pending focused node is not in view tree: " + node);
-                iterator.remove();
-                node.recycle();
-                continue;
-            }
-            long expireTimeMs = entry.getValue();
-            if (uptimeMs > expireTimeMs) {
-                L.d("Pending focused node is expired: " + node);
-                iterator.remove();
-                node.recycle();
-            }
-        }
-    }
-
-    @VisibleForTesting
-    long getUptimeMs() {
-        return SystemClock.uptimeMillis();
-    }
-
-    /** Sets a mock Utils instance for testing. */
-    @VisibleForTesting
-    void setNodeCopier(@NonNull NodeCopier nodeCopier) {
-        mNodeCopier = nodeCopier;
-    }
-
-    /** Returns the size of the cache. */
-    @VisibleForTesting
-    int size() {
-        return mMap.size();
-    }
-
-    private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
-        return mNodeCopier.copy(node);
-    }
-}
diff --git a/src/com/android/car/rotary/RotaryCache.java b/src/com/android/car/rotary/RotaryCache.java
deleted file mode 100644
index 6bb9ac6..0000000
--- a/src/com/android/car/rotary/RotaryCache.java
+++ /dev/null
@@ -1,463 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.rotary;
-
-import android.os.SystemClock;
-import android.util.LruCache;
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * Cache of rotation and nudge history of rotary controller. With this cache, the users can reverse
- * course and go back where they were if they accidentally nudge too far.
- */
-class RotaryCache {
-
-    /** The cache is disabled. */
-    @VisibleForTesting
-    static final int CACHE_TYPE_DISABLED = 1;
-    /** Entries in the cache will expire after a period of time. */
-    @VisibleForTesting
-    static final int CACHE_TYPE_EXPIRED_AFTER_SOME_TIME = 2;
-    /** Entries in the cache will never expire as long as RotaryService is alive. */
-    @VisibleForTesting
-    static final int CACHE_TYPE_NEVER_EXPIRE = 3;
-
-    @IntDef(flag = true, value = {
-            CACHE_TYPE_DISABLED, CACHE_TYPE_EXPIRED_AFTER_SOME_TIME, CACHE_TYPE_NEVER_EXPIRE})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface CacheType {
-    }
-
-    @NonNull
-    private NodeCopier mNodeCopier = new NodeCopier();
-
-    /** Cache of last focused node by focus area. */
-    @NonNull
-    private final FocusHistoryCache mFocusHistoryCache;
-
-    /** Cache of target focus area by source focus area and direction (up, down, left or right). */
-    @NonNull
-    private final FocusAreaHistoryCache mFocusAreaHistoryCache;
-
-    /**
-     * Cache of recently focused nodes in recently focused windows. Used to recover when the
-     * focused window closes.
-     */
-    @NonNull
-    private final FocusWindowCache mFocusWindowCache;
-
-    /** A record of when a node was focused. */
-    private static class FocusHistory {
-
-        /**
-         * A node representing a focusable {@link View} or a {@link com.android.car.ui.FocusArea}.
-         */
-        @NonNull
-        final AccessibilityNodeInfo node;
-
-        /** The {@link SystemClock#uptimeMillis} when this history was recorded. */
-        final long timestamp;
-
-        FocusHistory(@NonNull AccessibilityNodeInfo node, long timestamp) {
-            this.node = node;
-            this.timestamp = timestamp;
-        }
-    }
-
-    /**
-     * A combination of a source focus area and a direction (up, down, left or right). Used as a key
-     * in {@link #mFocusAreaHistoryCache}.
-     */
-    private static class FocusAreaHistory {
-
-        @NonNull
-        final AccessibilityNodeInfo sourceFocusArea;
-        final int direction;
-
-        FocusAreaHistory(@NonNull AccessibilityNodeInfo sourceFocusArea, int direction) {
-            this.sourceFocusArea = sourceFocusArea;
-            this.direction = direction;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (o == null || getClass() != o.getClass()) {
-                return false;
-            }
-            FocusAreaHistory that = (FocusAreaHistory) o;
-            return direction == that.direction
-                    && Objects.equals(sourceFocusArea, that.sourceFocusArea);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(sourceFocusArea, direction);
-        }
-    }
-
-    /** An entry in {@link #mFocusWindowCache}. */
-    private static class FocusWindowHistory {
-        /** A node in a window representing a focused {@link View}. */
-        @NonNull
-        final AccessibilityNodeInfo mNode;
-
-        /** The {@link SystemClock#uptimeMillis} when this history was recorded. */
-        final long mTimestamp;
-
-        FocusWindowHistory(@NonNull AccessibilityNodeInfo node, long timestamp) {
-            this.mNode = node;
-            this.mTimestamp = timestamp;
-        }
-
-        void recycle() {
-            this.mNode.recycle();
-        }
-    }
-
-    /** A cache of the last focused node by focus area. */
-    private class FocusHistoryCache extends LruCache<AccessibilityNodeInfo, FocusHistory> {
-
-        /** Type of the cache. */
-        private final @CacheType int mCacheType;
-
-        /** How many milliseconds before an entry in the cache expires. */
-        private final int mExpirationTimeMs;
-
-        FocusHistoryCache(@CacheType int cacheType, int size, int expirationTimeMs) {
-            super(size);
-            mCacheType = cacheType;
-            mExpirationTimeMs = expirationTimeMs;
-            if (mCacheType == CACHE_TYPE_EXPIRED_AFTER_SOME_TIME && mExpirationTimeMs <= 0) {
-                throw new IllegalArgumentException(
-                        "Expiration time must be positive if CacheType is "
-                                + "CACHE_TYPE_EXPIRED_AFTER_SOME_TIME");
-            }
-        }
-
-        boolean enabled() {
-            return mCacheType != CACHE_TYPE_DISABLED;
-        }
-
-        boolean isValidFocusHistory(@Nullable FocusHistory focusHistory, long elapsedRealtime) {
-            if (focusHistory == null || focusHistory.node == null) {
-                return false;
-            }
-            switch (mCacheType) {
-                case CACHE_TYPE_NEVER_EXPIRE:
-                    return true;
-                case CACHE_TYPE_EXPIRED_AFTER_SOME_TIME:
-                    return elapsedRealtime - focusHistory.timestamp < mExpirationTimeMs;
-                default:
-                    return false;
-            }
-        }
-
-        @Override
-        protected void entryRemoved(boolean evicted, AccessibilityNodeInfo key,
-                FocusHistory oldValue, FocusHistory newValue) {
-            Utils.recycleNode(key);
-            Utils.recycleNode(oldValue.node);
-        }
-    }
-
-    /**
-     * A cache of the target focus area to nudge to, by source focus area and direction (up, down,
-     * left or right).
-     */
-    private class FocusAreaHistoryCache extends LruCache<FocusAreaHistory, FocusHistory> {
-
-        /** Type of the cache. */
-        private final @CacheType int mCacheType;
-
-        /** How many milliseconds before an entry in the cache expires. */
-        private final int mExpirationTimeMs;
-
-        FocusAreaHistoryCache(@CacheType int cacheType, int size, int expirationTimeMs) {
-            super(size);
-            mCacheType = cacheType;
-            mExpirationTimeMs = expirationTimeMs;
-            if (mCacheType == CACHE_TYPE_EXPIRED_AFTER_SOME_TIME && mExpirationTimeMs <= 0) {
-                throw new IllegalArgumentException(
-                        "Expiration time must be positive if CacheType is "
-                                + "CACHE_TYPE_EXPIRED_AFTER_SOME_TIME");
-            }
-        }
-
-        boolean enabled() {
-            return mCacheType != CACHE_TYPE_DISABLED;
-        }
-
-        boolean isValidFocusHistory(@Nullable FocusHistory focusHistory, long elapsedRealtime) {
-            if (focusHistory == null || focusHistory.node == null) {
-                return false;
-            }
-            switch (mCacheType) {
-                case CACHE_TYPE_NEVER_EXPIRE:
-                    return true;
-                case CACHE_TYPE_EXPIRED_AFTER_SOME_TIME:
-                    return elapsedRealtime - focusHistory.timestamp < mExpirationTimeMs;
-                default:
-                    return false;
-            }
-        }
-
-        @Override
-        protected void entryRemoved(boolean evicted, FocusAreaHistory key, FocusHistory oldValue,
-                FocusHistory newValue) {
-            Utils.recycleNode(key.sourceFocusArea);
-            Utils.recycleNode(oldValue.node);
-        }
-    }
-
-    /**
-     * A cache of recently focused nodes in recently focused windows. Used to recover when the
-     * focused window closes.
-     */
-    private class FocusWindowCache extends LruCache<Integer, FocusWindowHistory> {
-        @CacheType
-        final int mCacheType;
-        final int mExpirationTimeMs;
-
-        FocusWindowCache(@CacheType int cacheType, int size, int expirationTimeMs) {
-            super(size);
-            mCacheType = cacheType;
-            mExpirationTimeMs = expirationTimeMs;
-            if (cacheType == CACHE_TYPE_EXPIRED_AFTER_SOME_TIME && expirationTimeMs <= 0) {
-                throw new IllegalArgumentException(
-                        "Expiration time must be positive if CacheType is "
-                                + "CACHE_TYPE_EXPIRED_AFTER_SOME_TIME");
-            }
-        }
-
-        /**
-         * Returns whether an entry in this cache is valid. To be valid:
-         * <ul>
-         *     <li>the cached node must still be in the view tree
-         *     <li>the cached node must still be able to take focus
-         *     <li>the cache entry must not have expired
-         * </ul>
-         */
-        boolean isValidEntry(@NonNull FocusWindowHistory focusWindowHistory, long elapsedRealtime) {
-            if (!focusWindowHistory.mNode.refresh()
-                    || !Utils.canTakeFocus(focusWindowHistory.mNode)) {
-                return false;
-            }
-
-            switch (mCacheType) {
-                case CACHE_TYPE_NEVER_EXPIRE:
-                    return true;
-                case CACHE_TYPE_EXPIRED_AFTER_SOME_TIME:
-                    return elapsedRealtime - focusWindowHistory.mTimestamp < mExpirationTimeMs;
-                default:
-                    return false;
-            }
-        }
-
-        /**
-         * Stores the given (window ID, node) pair, overwriting the existing pair with the given
-         * window ID, if any.
-         */
-        void put(int windowId, @NonNull AccessibilityNodeInfo node, long elapsedRealtime) {
-            if (mCacheType == CACHE_TYPE_DISABLED) {
-                return;
-            }
-            put(windowId, new FocusWindowHistory(copyNode(node), elapsedRealtime));
-        }
-
-        /**
-         * Returns the most recently focused valid node in window {@code windowId}, or {@code null}
-         * if there are no valid nodes in the cache. The caller is responsible for recycling the
-         * result.
-         */
-        @Nullable
-        AccessibilityNodeInfo getMostRecentValidNode(int windowId, long elapsedRealtime) {
-            FocusWindowHistory focusWindowHistory = get(windowId);
-            if (focusWindowHistory != null && isValidEntry(focusWindowHistory, elapsedRealtime)) {
-                return copyNode(focusWindowHistory.mNode);
-            }
-            return null;
-        }
-
-        @Override
-        protected void entryRemoved(boolean evicted, Integer windowId, FocusWindowHistory oldValue,
-                FocusWindowHistory newValue) {
-            oldValue.recycle();
-        }
-    }
-
-    RotaryCache(@CacheType int focusHistoryCacheType,
-            int focusHistoryCacheSize,
-            int focusHistoryExpirationTimeMs,
-            @CacheType int focusAreaHistoryCacheType,
-            int focusAreaHistoryCacheSize,
-            int focusAreaHistoryExpirationTimeMs,
-            @CacheType int focusWindowCacheType,
-            int focusWindowCacheSize,
-            int focusWindowExpirationTimeMs) {
-        mFocusHistoryCache = new FocusHistoryCache(
-                focusHistoryCacheType, focusHistoryCacheSize, focusHistoryExpirationTimeMs);
-        mFocusAreaHistoryCache = new FocusAreaHistoryCache(focusAreaHistoryCacheType,
-                focusAreaHistoryCacheSize, focusAreaHistoryExpirationTimeMs);
-        mFocusWindowCache = new FocusWindowCache(focusWindowCacheType, focusWindowCacheSize,
-                focusWindowExpirationTimeMs);
-    }
-
-    /**
-     * Searches the cache to find the last focused node in the given {@code focusArea}. Returns the
-     * node, or null if there is nothing in the cache, the cache is stale, the view represented
-     * by the node is no longer in the view tree, or the node's state has changed so that it can't
-     * take focus any more. The caller is responsible for recycling the result.
-     */
-    AccessibilityNodeInfo getFocusedNode(@NonNull AccessibilityNodeInfo focusArea,
-            long elapsedRealtime) {
-        if (mFocusHistoryCache.enabled()) {
-            FocusHistory focusHistory = mFocusHistoryCache.get(focusArea);
-            if (mFocusHistoryCache.isValidFocusHistory(focusHistory, elapsedRealtime)) {
-                AccessibilityNodeInfo node = copyNode(focusHistory.node);
-                // Refresh the node in case the view represented by the node is no longer in the
-                // view tree, or the node's state (e.g., isFocused()) has changed.
-                AccessibilityNodeInfo refreshedNode = Utils.refreshNode(node);
-
-                // If the node's state has changed so that it can't take focus any more, return
-                // null.
-                if (refreshedNode != null && !Utils.canTakeFocus(refreshedNode)) {
-                    Utils.recycleNode(refreshedNode);
-                    refreshedNode = null;
-                }
-                return refreshedNode;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Caches the last focused node by focus area. A copy of {@code focusArea} and {@code
-     * focusedNode} will be saved in the cache.
-     */
-    void saveFocusedNode(@NonNull AccessibilityNodeInfo focusArea,
-            @NonNull AccessibilityNodeInfo focusedNode, long elapsedRealtime) {
-        if (mFocusHistoryCache.enabled()) {
-            mFocusHistoryCache.put(
-                    copyNode(focusArea), new FocusHistory(copyNode(focusedNode), elapsedRealtime));
-        }
-    }
-
-    /**
-     * Searches the cache to find the target focus area for a nudge in a given {@code direction}
-     * from a given focus area. Returns the focus area, or null if there is nothing in the cache,
-     * the cache is stale, or the view represented by the node is no longer in the view tree.
-     * The caller is responsible for recycling the result.
-     */
-    AccessibilityNodeInfo getTargetFocusArea(@NonNull AccessibilityNodeInfo sourceFocusArea,
-            int direction, long elapsedRealtime) {
-        if (mFocusAreaHistoryCache.enabled()) {
-            FocusHistory focusHistory =
-                    mFocusAreaHistoryCache.get(new FocusAreaHistory(sourceFocusArea, direction));
-            if (mFocusAreaHistoryCache.isValidFocusHistory(focusHistory, elapsedRealtime)) {
-                AccessibilityNodeInfo focusArea = copyNode(focusHistory.node);
-                // Refresh the node in case the view represented by the node is no longer in the
-                // view tree.
-                return Utils.refreshNode(focusArea);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Caches the focus area nudge history. A copy of {@code sourceFocusArea} and {@code
-     * targetFocusArea} will be saved in the cache.
-     */
-    void saveTargetFocusArea(@NonNull AccessibilityNodeInfo sourceFocusArea,
-            @NonNull AccessibilityNodeInfo targetFocusArea, int direction, long elapsedRealtime) {
-        if (mFocusAreaHistoryCache.enabled()) {
-            int oppositeDirection = getOppositeDirection(direction);
-            mFocusAreaHistoryCache
-                    .put(new FocusAreaHistory(copyNode(targetFocusArea), oppositeDirection),
-                            new FocusHistory(copyNode(sourceFocusArea), elapsedRealtime));
-        }
-    }
-
-    /** Clears the focus area nudge history cache. */
-    void clearFocusAreaHistory() {
-        if (mFocusAreaHistoryCache.enabled()) {
-            mFocusAreaHistoryCache.evictAll();
-        }
-    }
-
-    @VisibleForTesting
-    boolean isFocusAreaHistoryCacheEmpty() {
-        return mFocusAreaHistoryCache.size() == 0;
-    }
-
-    /** Saves the most recently focused node within a window. */
-    void saveWindowFocus(@NonNull AccessibilityNodeInfo focusedNode, long elapsedRealtime) {
-        mFocusWindowCache.put(focusedNode.getWindowId(), focusedNode, elapsedRealtime);
-    }
-
-    /**
-     * Returns the most recently focused valid node in window {@code windowId}, or {@code null} if
-     * there are no valid nodes saved by {@link #saveWindowFocus}. The caller is responsible for
-     * recycling the result.
-     */
-    @Nullable
-    AccessibilityNodeInfo getMostRecentFocus(int windowId, long elapsedRealtime) {
-        return mFocusWindowCache.getMostRecentValidNode(windowId, elapsedRealtime);
-    }
-
-    /** Returns the direction opposite the given {@code direction} */
-    @VisibleForTesting
-    static int getOppositeDirection(int direction) {
-        switch (direction) {
-            case View.FOCUS_LEFT:
-                return View.FOCUS_RIGHT;
-            case View.FOCUS_RIGHT:
-                return View.FOCUS_LEFT;
-            case View.FOCUS_UP:
-                return View.FOCUS_DOWN;
-            case View.FOCUS_DOWN:
-                return View.FOCUS_UP;
-        }
-        throw new IllegalArgumentException("direction must be "
-                + "FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, or FOCUS_RIGHT.");
-    }
-
-    /** Sets a mock {@link NodeCopier} instance for testing. */
-    @VisibleForTesting
-    void setNodeCopier(@NonNull NodeCopier nodeCopier) {
-        mNodeCopier = nodeCopier;
-    }
-
-    private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
-        return mNodeCopier.copy(node);
-    }
-}
diff --git a/src/com/android/car/rotary/RotaryService.java b/src/com/android/car/rotary/RotaryService.java
index be15728..79e03cf 100644
--- a/src/com/android/car/rotary/RotaryService.java
+++ b/src/com/android/car/rotary/RotaryService.java
@@ -41,7 +41,6 @@
 import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD;
 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_APPLICATION;
 import static android.view.accessibility.AccessibilityWindowInfo.TYPE_INPUT_METHOD;
-import static android.view.accessibility.AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
 
 import static com.android.car.ui.utils.RotaryConstants.ACTION_HIDE_IME;
 import static com.android.car.ui.utils.RotaryConstants.ACTION_NUDGE_SHORTCUT;
@@ -157,9 +156,6 @@
      */
     private int mRotationAcceleration2xMs;
 
-    /** Whether to clear focus area history when the user rotates the controller. */
-    private boolean mClearFocusAreaHistoryWhenRotating;
-
     /**
      * The currently focused node, if any. It's null if no nodes are focused or a {@link
      * com.android.car.ui.FocusParkingView} is focused.
@@ -180,23 +176,6 @@
     private AccessibilityNodeInfo mFocusArea = null;
 
     /**
-     * The previously focused node, if any. It's null if no nodes were focused or a {@link
-     * com.android.car.ui.FocusParkingView} was focused.
-     */
-    private AccessibilityNodeInfo mPreviousFocusedNode = null;
-
-    /**
-     * The currently focused {@link com.android.car.ui.FocusParkingView} that was focused by us to
-     * clear the focus, if any.
-     */
-    private AccessibilityNodeInfo mFocusParkingView = null;
-
-    /**
-     * The current scrollable container, if any. Either {@link #mFocusedNode} or an ancestor of it.
-     */
-    private AccessibilityNodeInfo mScrollableContainer = null;
-
-    /**
      * The last clicked node by touching the screen, if any were clicked since we last navigated.
      */
     private AccessibilityNodeInfo mLastTouchedNode = null;
@@ -221,19 +200,6 @@
      */
     private long mLastViewClickedTime;
 
-    /**
-     * How many milliseconds to ignore {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} events of
-     * {@link com.android.car.ui.FocusParkingView} after a {@link
-     * AccessibilityEvent#WINDOWS_CHANGE_ADDED} event.
-     */
-    private long mIgnoreFpvFocusedMs;
-
-    /**
-     * The time of the last {@link AccessibilityEvent#WINDOWS_CHANGE_ADDED} event in {@link
-     * SystemClock#uptimeMillis}.
-     */
-    private long mLastWindowAddedTime;
-
     /** Component name of rotary IME. Empty if none. */
     @Nullable private String mRotaryInputMethod;
 
@@ -349,8 +315,6 @@
 
     private final WindowCache mWindowCache = new WindowCache();
 
-    private PendingFocusedNodes mPendingFocusedNodes;
-
     @Override
     public void onCreate() {
         super.onCreate();
@@ -358,30 +322,6 @@
         mRotationAcceleration3xMs = res.getInteger(R.integer.rotation_acceleration_3x_ms);
         mRotationAcceleration2xMs = res.getInteger(R.integer.rotation_acceleration_2x_ms);
 
-        mClearFocusAreaHistoryWhenRotating =
-                res.getBoolean(R.bool.clear_focus_area_history_when_rotating);
-
-        @RotaryCache.CacheType int focusHistoryCacheType =
-                res.getInteger(R.integer.focus_history_cache_type);
-        int focusHistoryCacheSize =
-                res.getInteger(R.integer.focus_history_cache_size);
-        int focusHistoryExpirationTimeMs =
-                res.getInteger(R.integer.focus_history_expiration_time_ms);
-
-        @RotaryCache.CacheType int focusAreaHistoryCacheType =
-                res.getInteger(R.integer.focus_area_history_cache_type);
-        int focusAreaHistoryCacheSize =
-                res.getInteger(R.integer.focus_area_history_cache_size);
-        int focusAreaHistoryExpirationTimeMs =
-                res.getInteger(R.integer.focus_area_history_expiration_time_ms);
-
-        @RotaryCache.CacheType int focusWindowCacheType =
-                res.getInteger(R.integer.focus_window_cache_type);
-        int focusWindowCacheSize =
-                res.getInteger(R.integer.focus_window_cache_size);
-        int focusWindowExpirationTimeMs =
-                res.getInteger(R.integer.focus_window_expiration_time_ms);
-
         int hunMarginHorizontal =
                 res.getDimensionPixelSize(R.dimen.notification_headsup_card_margin_horizontal);
         int hunLeft = hunMarginHorizontal;
@@ -392,21 +332,8 @@
 
         mIgnoreViewClickedMs = res.getInteger(R.integer.ignore_view_clicked_ms);
         mAfterScrollTimeoutMs = res.getInteger(R.integer.after_scroll_timeout_ms);
-        mIgnoreFpvFocusedMs = res.getInteger(R.integer.ignore_fpv_focused_ms);
 
-        mNavigator = new Navigator(
-                focusHistoryCacheType,
-                focusHistoryCacheSize,
-                focusHistoryExpirationTimeMs,
-                focusAreaHistoryCacheType,
-                focusAreaHistoryCacheSize,
-                focusAreaHistoryExpirationTimeMs,
-                focusWindowCacheType,
-                focusWindowCacheSize,
-                focusWindowExpirationTimeMs,
-                hunLeft,
-                hunRight,
-                showHunOnBottom);
+        mNavigator = new Navigator(hunLeft, hunRight, showHunOnBottom);
 
         mPrefs = createDeviceProtectedStorageContext().getSharedPreferences(SHARED_PREFS,
                 Context.MODE_PRIVATE);
@@ -424,9 +351,6 @@
         if (isValidIme(rotaryInputMethod)) {
             mRotaryInputMethod = rotaryInputMethod;
         }
-
-        long afterFocusTimeoutMs = res.getInteger(R.integer.after_focus_timeout_ms);
-        mPendingFocusedNodes = new PendingFocusedNodes(afterFocusTimeoutMs);
     }
 
     /**
@@ -527,7 +451,7 @@
                 break;
             }
             case TYPE_VIEW_SCROLLED: {
-                handleViewScrolledEvent(event, source);
+                handleViewScrolledEvent(source);
                 break;
             }
             case TYPE_WINDOW_STATE_CHANGED: {
@@ -775,102 +699,24 @@
             @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
-        // a view was focused by us (we performed ACTION_FOCUS) or by Android. Based on that we'll
-        // accept the focus and update mFocusedNode, or move focus to another view.
+        // focused in touch mode.
         if (!mInRotaryMode) {
             return;
         }
-
-        // No need to handle TYPE_VIEW_FOCUSED event if sourceNode is null.
         if (sourceNode == null) {
+            L.w("Null source node in " + event);
             return;
         }
-
-        // The focused node could be a FocusParkingView, or a non-FocusParkingView.
-        // It could be focused by us, or by Android automatically. There are 5 cases:
-
-        // Case 1: the focused node is a FocusParkingView and it was focused by us to clear the
-        // focus in another window. In this case we should do nothing but reset mFocusParkingView.
-        if (sourceNode.equals(mFocusParkingView)) {
-            L.d("A FocusParkingView was focused because we cleared the focus in another window");
-            Utils.recycleNode(mFocusParkingView);
-            mFocusParkingView = null;
+        // If it's not a FocusParkingView, update mFocusedNode.
+        if (!Utils.isFocusParkingView(sourceNode)) {
+            setFocusedNode(sourceNode);
             return;
         }
-
-        // Case 2: the focused node is a FocusParkingView and it was focused by Android when
-        // scrolling pushed the focused view out of the viewport. When this happens, focus the
-        // scrollable container.
-        boolean isFpv = Utils.isFocusParkingView(sourceNode);
-        if (isFpv && mFocusedNode != null && mScrollableContainer != null
-                && SystemClock.uptimeMillis() < mAfterScrollActionUntil) {
-            mScrollableContainer = Utils.refreshNode(mScrollableContainer);
-            if (mScrollableContainer != null) {
-                L.d("Moving focus from FocusParkingView to scrollable container");
-                performFocusAction(mScrollableContainer);
-            } else {
-                L.d("mScrollableContainer is not in the view tree");
-            }
-            return;
+        // If it's a FocusParkingView, only update mFocusedNode when it's in the same window
+        // with mFocusedNode.
+        if (mFocusedNode != null && sourceNode.getWindowId() == mFocusedNode.getWindowId()) {
+            setFocusedNode(null);
         }
-
-        // Case 3: we have performed ACTION_FOCUS on non-FocusParkingViews and we are waiting for
-        // them to be focused. In this case we should ignore any other node focused events since
-        // the focus will be moved to a node we're waiting for soon.
-        if (!mPendingFocusedNodes.isEmpty()) {
-            L.d("Waiting for mPendingFocusedNodes to get focused");
-            // If a node we're waiting for, or one of its descendants (i.e., the node is a
-            // FocusArea) finally got focused, remove it from mPendingFocusedNodes.
-            boolean match = mPendingFocusedNodes.removeFirstIf(
-                    node -> sourceNode.equals(node) || Utils.isDescendant(node, sourceNode));
-            if (match && !sourceNode.equals(mFocusedNode)) {
-                setFocusedNode(sourceNode);
-            }
-            return;
-        }
-
-        // The node was focused by Android automatically. For example:
-        // 1. When the previously focused view is removed by the app, Android will focus on
-        //    the first focusable view in the window, which is a FocusParkingView (because we
-        //    require that a FocusParkingView must be placed as the first focusable view in a
-        //    window).
-        // 2. When a dialog window is opened via the rotary controller, Android will focus on the
-        //    previously focused view in the dialog window, if any, or the first focusable view in
-        //    the dialog window if there are no previously focused views. In the former case the
-        //    focused view is a non-FocusParkingView (such as the "Close" button), in the later case
-        //    the focused view is a FocusParkingView.
-
-        // Case 4: Android focused on a FocusParkingView.
-        if (isFpv) {
-            L.d("Android focused a FocusParkingView automatically " + sourceNode);
-            if (mFocusedNode != null) {
-                // If mFocusedNode is in the same window with the FocusParkingView, we only set
-                // mFocusedNode to null. Otherwise we need to call setFocusedNode(null) to clear the
-                // focus in the previous window as well.
-                if (mFocusedNode.getWindowId() == sourceNode.getWindowId()) {
-                    Utils.recycleNode(mFocusedNode);
-                    mFocusedNode = null;
-                } else {
-                    setFocusedNode(null);
-                }
-            }
-
-            // Android focused on the  FocusParkingView because a window was just added. In this
-            // case we should move focus to a node nearby.
-            if (event.getEventTime() < mLastWindowAddedTime + mIgnoreFpvFocusedMs) {
-                focusNodeNearby(sourceNode);
-            }
-            return;
-        }
-
-        // Case 5: Android focused a non-FocusParkingView. We should update mFocusedNode.
-        L.d("Android focused a non-FocusParkingView automatically " + sourceNode);
-        if (sourceNode.equals(mFocusedNode)) {
-            L.d("mFocusedNode was set before receiving focused event");
-            return;
-        }
-        setFocusedNode(sourceNode);
     }
 
     /** Handles {@link AccessibilityEvent#TYPE_VIEW_CLICKED} event. */
@@ -914,8 +760,7 @@
     }
 
     /** Handles {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event. */
-    private void handleViewScrolledEvent(@NonNull AccessibilityEvent event,
-            @Nullable AccessibilityNodeInfo sourceNode) {
+    private void handleViewScrolledEvent(@Nullable AccessibilityNodeInfo sourceNode) {
         if (mAfterScrollAction == AfterScrollAction.NONE
                 || SystemClock.uptimeMillis() >= mAfterScrollActionUntil) {
             return;
@@ -941,7 +786,7 @@
                         + (mAfterScrollAction == AfterScrollAction.FOCUS_PREVIOUS
                             ? "previous" : "next")
                         + " after scroll");
-                if (performFocusAction(target)) {
+                if (target.performAction(ACTION_FOCUS)) {
                     mAfterScrollAction = AfterScrollAction.NONE;
                 }
                 Utils.recycleNode(target);
@@ -959,7 +804,7 @@
                 L.d("Focusing "
                         + (mAfterScrollAction == AfterScrollAction.FOCUS_FIRST ? "first" : "last")
                         + " after scroll");
-                if (performFocusAction(target)) {
+                if (target.performAction(ACTION_FOCUS)) {
                     mAfterScrollAction = AfterScrollAction.NONE;
                 }
                 Utils.recycleNode(target);
@@ -1002,13 +847,9 @@
         }
 
         // Restore focus to the last focused node in the last focused window.
-        Integer lastWindowId = mWindowCache.getMostRecentWindowId();
-        if (lastWindowId == null) {
-            return;
-        }
-        AccessibilityNodeInfo recentFocus = mNavigator.getMostRecentFocus(lastWindowId);
+        AccessibilityNodeInfo recentFocus = mWindowCache.getMostRecentFocusedNode();
         if (recentFocus != null) {
-            performFocusAction(recentFocus);
+            recentFocus.performAction(ACTION_FOCUS);
             recentFocus.recycle();
         }
     }
@@ -1018,7 +859,6 @@
      * added. Moves focus to the IME window when it appears.
      */
     private void handleWindowAddedEvent(@NonNull AccessibilityEvent event) {
-        mLastWindowAddedTime = event.getEventTime();
         // Save the window type by window ID.
         int windowId = event.getWindowId();
         List<AccessibilityWindowInfo> windows = getWindows();
@@ -1027,7 +867,7 @@
             Utils.recycleWindows(windows);
             return;
         }
-        mWindowCache.put(windowId, window.getType());
+        mWindowCache.saveWindowType(windowId, window.getType());
 
         // Nothing more to do if we're in touch mode.
         if (!mInRotaryMode) {
@@ -1058,19 +898,33 @@
         }
         Utils.recycleWindows(windows);
 
-        // TODO: Use app:defaultFocus
-        AccessibilityNodeInfo nodeToFocus = mNavigator.findFirstFocusDescendant(root);
-        root.recycle();
-        if (nodeToFocus != null) {
-            L.d("Move focus to IME");
-            // If the focused node is editable, save it so that we can return to it when the user
-            // nudges out of the IME.
-            if (mFocusedNode != null && mFocusedNode.isEditable()) {
-                setEditNode(mFocusedNode);
-            }
-            performFocusAction(nodeToFocus);
-            nodeToFocus.recycle();
+        // If the focused node is editable, save it so that we can return to it when the user
+        // nudges out of the IME.
+        if (mFocusedNode != null && mFocusedNode.isEditable()) {
+            setEditNode(mFocusedNode);
         }
+
+        boolean success = restoreDefaultFocus(root);
+        if (!success) {
+            L.d("Failed to restore default focus in " + root);
+        }
+        root.recycle();
+    }
+
+    private boolean restoreDefaultFocus(@NonNull AccessibilityNodeInfo node) {
+        AccessibilityNodeInfo fpv = mNavigator.findFocusParkingView(node);
+
+        // Refresh the node to ensure the focused state is up to date. The node came directly from
+        // the node tree but it could have been cached by the accessibility framework.
+        fpv = Utils.refreshNode(fpv);
+
+        if (fpv == null) {
+            L.e("No FocusParkingView in the window containing " + node);
+            return false;
+        }
+        boolean result = fpv.performAction(ACTION_RESTORE_DEFAULT_FOCUS);
+        fpv.recycle();
+        return result;
     }
 
     private static int getKeyCode(KeyEvent event) {
@@ -1184,12 +1038,16 @@
 
     private boolean nudgeTo(@NonNull List<AccessibilityWindowInfo> windows, int direction) {
         // If the HUN is in the nudge direction, nudge to it.
-        AccessibilityNodeInfo hunNudgeTarget =
-                mNavigator.findHunNudgeTarget(windows, mFocusedNode, direction);
-        if (hunNudgeTarget != null) {
-            performFocusAction(hunNudgeTarget);
-            hunNudgeTarget.recycle();
-            return true;
+        AccessibilityNodeInfo hunFocusArea =
+                mNavigator.findHunFocusArea(windows, mFocusedNode, direction);
+        boolean success = false;
+        if (hunFocusArea != null) {
+            success = hunFocusArea.performAction(ACTION_FOCUS);
+            L.d(success ? "Nudge to HUN" : " Failed to nudge to HUN " + hunFocusArea);
+            hunFocusArea.recycle();
+            if (success) {
+                return true;
+            }
         }
 
         // Try to move the focus to the shortcut node.
@@ -1206,13 +1064,11 @@
 
         // No shortcut node, so move the focus in the given direction.
         // First, try to perform ACTION_NUDGE on mFocusArea to nudge to another FocusArea.
-        if (mFocusArea != null) {
-            arguments.clear();
-            arguments.putInt(NUDGE_DIRECTION, direction);
-            if (mFocusArea.performAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments)) {
-                L.d("Nudge to user specified FocusArea");
-                return true;
-            }
+        arguments.clear();
+        arguments.putInt(NUDGE_DIRECTION, direction);
+        if (mFocusArea.performAction(ACTION_NUDGE_TO_ANOTHER_FOCUS_AREA, arguments)) {
+            L.d("Nudge to user specified FocusArea");
+            return true;
         }
 
         // No specified FocusArea or cached FocusArea in the direction, so mFocusArea doesn't know
@@ -1226,16 +1082,20 @@
         if (Utils.isFocusArea(targetFocusArea)) {
             arguments.clear();
             arguments.putInt(NUDGE_DIRECTION, direction);
-            if (targetFocusArea.performAction(ACTION_FOCUS, arguments)) {
-                L.d("Nudge to the nearest FocusArea");
-                return true;
-            }
-            return false;
+            success = targetFocusArea.performAction(ACTION_FOCUS, arguments);
+            L.d("Nudging to the nearest FocusArea "
+                    + (success ? "succeeded" : "failed: " + targetFocusArea));
+            targetFocusArea.recycle();
+            return success;
         }
 
         // targetFocusArea is an implicit FocusArea (i.e., the root node of a window without any
-        // FocusAreas), so focus on the first focusable node in it.
-        return focusFirstFocusDescendant(targetFocusArea);
+        // FocusAreas), so restore the focus in it.
+        success = restoreDefaultFocus(targetFocusArea);
+        L.d("Nudging to the nearest implicit focus area "
+                + (success ? "succeeded" : "failed: " + targetFocusArea));
+        targetFocusArea.recycle();
+        return success;
     }
 
     private void handleRotaryEvent(RotaryEvent rotaryEvent) {
@@ -1250,28 +1110,12 @@
     }
 
     private void handleRotateEvent(boolean clockwise, int count, long eventTime) {
-        // Clear focus area history if configured to do so, but not when rotating in the HUN. The
-        // HUN overlaps the application window so it's common for focus areas to overlap, causing
-        // geometric searches to fail. History is essential here.
-        if (mClearFocusAreaHistoryWhenRotating && !isFocusInHunWindow()) {
-            mNavigator.clearFocusAreaHistory();
-        }
         if (initFocus()) {
             return;
         }
 
         int rotationCount = getRotateAcceleration(count, eventTime);
 
-        // If a scrollable container is focused, no focusable descendants are visible, so scroll the
-        // container.
-        AccessibilityNodeInfo.AccessibilityAction scrollAction =
-                clockwise ? ACTION_SCROLL_FORWARD : ACTION_SCROLL_BACKWARD;
-        if (mFocusedNode != null && Utils.isScrollableContainer(mFocusedNode)
-                && mFocusedNode.getActionList().contains(scrollAction)) {
-            injectScrollEvent(mFocusedNode, clockwise, rotationCount);
-            return;
-        }
-
         // If the focused node is in direct manipulation mode, manipulate it directly.
         if (mInDirectManipulationMode) {
             if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
@@ -1297,7 +1141,7 @@
         Navigator.FindRotateTargetResult result =
                 mNavigator.findRotateTarget(mFocusedNode, direction, rotationCount);
         if (result != null) {
-            if (performFocusAction(result.node)) {
+            if (result.node.performAction(ACTION_FOCUS)) {
                 remainingRotationCount -= result.advancedCount;
             }
             Utils.recycleNode(result.node);
@@ -1313,9 +1157,13 @@
         // is only supported in the application window because injected events always go to the
         // application window. We don't bother checking whether the scrollable container can
         // currently scroll because there's nothing else to do if it can't.
-        if (remainingRotationCount > 0 && isInApplicationWindow(mFocusedNode)
-                && mScrollableContainer != null) {
-            injectScrollEvent(mScrollableContainer, clockwise, remainingRotationCount);
+        if (remainingRotationCount > 0 && isInApplicationWindow(mFocusedNode)) {
+            AccessibilityNodeInfo scrollableContainer =
+                    mNavigator.findScrollableContainer(mFocusedNode);
+            if (scrollableContainer != null) {
+                injectScrollEvent(scrollableContainer, clockwise, remainingRotationCount);
+                scrollableContainer.recycle();;
+            }
         }
     }
 
@@ -1390,21 +1238,6 @@
         return result;
     }
 
-    /** Returns whether {@link #mFocusedNode} is in the HUN window. */
-    private boolean isFocusInHunWindow() {
-        if (mFocusedNode == null) {
-            return false;
-        }
-        AccessibilityWindowInfo window = mFocusedNode.getWindow();
-        if (window == null) {
-            L.w("Failed to get window of " + mFocusedNode);
-            return false;
-        }
-        boolean result = mNavigator.isHunWindow(window);
-        Utils.recycleWindow(window);
-        return result;
-    }
-
     private void updateDirectManipulationMode(@NonNull AccessibilityEvent event, boolean enable) {
         if (!mInRotaryMode || !DirectManipulationHelper.isDirectManipulation(event)) {
             return;
@@ -1535,8 +1368,6 @@
         mFocusedNode = Utils.refreshNode(mFocusedNode);
         mEditNode = Utils.refreshNode(mEditNode);
         mLastTouchedNode = Utils.refreshNode(mLastTouchedNode);
-        mScrollableContainer = Utils.refreshNode(mScrollableContainer);
-        mPreviousFocusedNode = Utils.refreshNode(mPreviousFocusedNode);
         mFocusArea = Utils.refreshNode(mFocusArea);
         mIgnoreViewClickedNode = Utils.refreshNode(mIgnoreViewClickedNode);
     }
@@ -1546,13 +1377,10 @@
      * following:<ol>
      *     <li>If {@link #mFocusedNode} isn't null and represents a view that still exists, does
      *         nothing. The event isn't consumed in this case. This is the normal case.
-     *     <li>If {@link #mScrollableContainer} isn't null and represents a view that still exists,
-     *         focuses it. The event isn't consumed in this case. This can happen when the user
-     *         rotates quickly as they scroll into a section without any focusable views.
      *     <li>If {@link #mLastTouchedNode} isn't null and represents a view that still exists,
      *         focuses it. The event is consumed in this case. This happens when the user switches
      *         from touch to rotary.
-     *     <li>Otherwise focuses a node nearby and consumes the event.
+     *     <li>Otherwise focuses the best target in the node tree and consumes the event.
      * </ol>
      *
      * @return whether the event was consumed by this method. When {@code false},
@@ -1577,91 +1405,18 @@
             // focused any more. In this case we should set mFocusedNode to null.
             setFocusedNode(null);
         }
-        if (mScrollableContainer != null) {
-            if (performFocusAction(mScrollableContainer)) {
-                return false;
-            }
-        }
-        if (mLastTouchedNode != null) {
-            if (focusLastTouchedNode()) {
-                return true;
-            }
+        if (mLastTouchedNode != null && focusLastTouchedNode()) {
+            return true;
         }
         AccessibilityNodeInfo root = getRootInActiveWindow();
         if (root != null) {
-            focusNodeNearby(root);
+            restoreDefaultFocus(root);
             Utils.recycleNode(root);
         }
         return true;
     }
 
     /**
-     * This method moves focus to a node nearby in the current active window, which is chosen in the
-     * following order:
-     * <ol>
-     *   <li> the recent focus saved in the cache, if any
-     *   <li> the previously focused node ({@link #mPreviousFocusedNode}), if any
-     *   <li> the default focus (app:defaultFocus) in the FocusArea that contains {@link
-     *        #mFocusedNode}, if any
-     *   <li> the first focusable view in the FocusArea that contains {@link #mFocusedNode}, if any,
-     *        excluding any FocusParkingViews
-     *   <li> the most recent focus in the window, if any, excluding any FocusParkingViews
-     *   <li> the default focus in the window, if any, excluding any FocusParkingViews
-     *   <li> the first focusable view in the window, if any, excluding any FocusParkingViews
-     * </ol>
-     */
-    private void focusNodeNearby(@NonNull AccessibilityNodeInfo node) {
-        int windowId = node.getWindowId();
-        if (windowId == UNDEFINED_WINDOW_ID) {
-            L.e("No windowId for node: " + node);
-            return;
-        }
-
-        AccessibilityNodeInfo recentFocus = mNavigator.getMostRecentFocus(windowId);
-        if (recentFocus != null && performFocusAction(recentFocus)) {
-            L.d("Move focus to the last focused node in the window");
-            recentFocus.recycle();
-            return;
-        }
-        Utils.recycleNode(recentFocus);
-
-        mPreviousFocusedNode = Utils.refreshNode(mPreviousFocusedNode);
-        if (mPreviousFocusedNode != null && mPreviousFocusedNode.getWindowId() == windowId) {
-            boolean success = performFocusAction(mPreviousFocusedNode);
-            if (success) {
-                L.d("Move focus to the previously focused node");
-                return;
-            }
-        }
-
-        mFocusArea = Utils.refreshNode(mFocusArea);
-        if (mFocusArea != null && mFocusArea.getWindowId() == windowId) {
-            boolean success = mFocusArea.performAction(ACTION_FOCUS);
-            if (success) {
-                L.d("Move focus to a view within the current FocusArea");
-                return;
-            }
-        }
-
-        AccessibilityNodeInfo fpv = findFocusParkingView(node);
-        if (fpv != null && fpv.performAction(ACTION_RESTORE_DEFAULT_FOCUS)) {
-            L.d("Move focus to the default focus in the window");
-            fpv.recycle();
-            return;
-        }
-        Utils.recycleNode(fpv);
-
-        L.d("Try to focus on the first focusable view in the window");
-        AccessibilityNodeInfo rootNode = getRootInActiveWindow();
-        if (rootNode == null) {
-            L.e("rootNode of active window is null");
-            return;
-        }
-        focusFirstFocusDescendant(rootNode);
-        rootNode.recycle();
-    }
-
-    /**
      * Clears the current rotary focus if {@code targetFocus} is null, or in a different window
      * unless focus is moving from an editable field to the IME.
      * <p>
@@ -1707,31 +1462,25 @@
             L.e("Don't call clearFocusInCurrentWindow() when mFocusedNode is null");
             return false;
         }
-        AccessibilityNodeInfo focusParkingView = findFocusParkingView(mFocusedNode);
+        AccessibilityNodeInfo fpv = mNavigator.findFocusParkingView(mFocusedNode);
 
         // Refresh the node to ensure the focused state is up to date. The node came directly from
         // the node tree but it could have been cached by the accessibility framework.
-        focusParkingView = Utils.refreshNode(focusParkingView);
+        fpv = Utils.refreshNode(fpv);
 
-        if (focusParkingView == null) {
+        if (fpv == null) {
+            L.e("No FocusParkingView in the window that contains " + mFocusedNode);
             return false;
         }
-        if (focusParkingView.isFocused()) {
-            L.d("FocusParkingView is already focused " + focusParkingView);
+        if (fpv.isFocused()) {
+            L.d("FocusParkingView is already focused " + fpv);
             return true;
         }
-        boolean result = focusParkingView.performAction(ACTION_FOCUS);
-        if (result) {
-            if (mFocusParkingView != null) {
-                L.e("mFocusParkingView should be null but is " + mFocusParkingView);
-                Utils.recycleNode(mFocusParkingView);
-            }
-            mFocusParkingView = copyNode(focusParkingView);
-            L.d("Performed focus on FocusParkingView: " + focusParkingView);
-        } else {
-            L.w("Failed to perform ACTION_FOCUS on FocusParkingView: " + focusParkingView);
+        boolean result = fpv.performAction(ACTION_FOCUS);
+        if (!result) {
+            L.w("Failed to perform ACTION_FOCUS on " + fpv);
         }
-        focusParkingView.recycle();
+        fpv.recycle();
         return result;
     }
 
@@ -1744,7 +1493,7 @@
     private boolean focusLastTouchedNode() {
         boolean lastTouchedNodeFocused = false;
         if (mLastTouchedNode != null) {
-            lastTouchedNodeFocused = performFocusAction(mLastTouchedNode);
+            lastTouchedNodeFocused = mLastTouchedNode.performAction(ACTION_FOCUS);
             if (mLastTouchedNode != null) {
                 setLastTouchedNode(null);
             }
@@ -1753,21 +1502,6 @@
     }
 
     /**
-     * Focuses the first focus descendant of the given {@code node}, if any. Returns whether the
-     * the node is focused.
-     */
-    private boolean focusFirstFocusDescendant(@NonNull AccessibilityNodeInfo node) {
-        AccessibilityNodeInfo targetNode = mNavigator.findFirstFocusDescendant(node);
-        if (targetNode == null) {
-            L.w("Failed to find the first focus descendant");
-            return false;
-        }
-        boolean success = performFocusAction(targetNode);
-        targetNode.recycle();
-        return success;
-    }
-
-    /**
      * Sets {@link #mFocusedNode} to a copy of the given node, and clears {@link #mLastTouchedNode}.
      */
     private void setFocusedNode(@Nullable AccessibilityNodeInfo focusedNode) {
@@ -1796,11 +1530,6 @@
         // Close the IME when navigating from an editable view to a non-editable view.
         maybeCloseIme(focusedNode);
 
-        Utils.recycleNode(mPreviousFocusedNode);
-        mFocusedNode = Utils.refreshNode(mFocusedNode);
-        mPreviousFocusedNode = copyNode(mFocusedNode);
-        L.d("mPreviousFocusedNode set to: " + mPreviousFocusedNode);
-
         Utils.recycleNode(mFocusedNode);
         mFocusedNode = copyNode(focusedNode);
         L.d("mFocusedNode set to: " + mFocusedNode);
@@ -1808,19 +1537,8 @@
         Utils.recycleNode(mFocusArea);
         mFocusArea = mFocusedNode == null ? null : mNavigator.getAncestorFocusArea(mFocusedNode);
 
-        // Set mScrollableContainer to the scrollable container which contains mFocusedNode, if any.
-        // Skip if mFocusedNode is a FocusParkingView. The FocusParkingView is focused when the
-        // focus view is scrolled off the screen. We'll focus the scrollable container when we
-        // receive the TYPE_VIEW_FOCUSED event in this case.
-        if (mFocusedNode == null) {
-            setScrollableContainer(null);
-        } else if (!Utils.isFocusParkingView(mFocusedNode)) {
-            setScrollableContainer(mNavigator.findScrollableContainer(mFocusedNode));
-        }
-
-        // Cache the focused node by focus area.
         if (mFocusedNode != null) {
-            mNavigator.saveFocusedNode(mFocusedNode);
+            mWindowCache.saveFocusedNode(mFocusedNode.getWindowId(), mFocusedNode);
         }
     }
 
@@ -1854,43 +1572,14 @@
 
         // To close the IME, we'll ask the FocusParkingView in the previous window to perform
         // ACTION_HIDE_IME.
-        AccessibilityNodeInfo focusParkingView = findFocusParkingView(mFocusedNode);
-        if (focusParkingView == null) {
+        AccessibilityNodeInfo fpv = mNavigator.findFocusParkingView(mFocusedNode);
+        if (fpv == null) {
             return;
         }
-        if (!focusParkingView.performAction(ACTION_HIDE_IME)) {
+        if (!fpv.performAction(ACTION_HIDE_IME)) {
             L.w("Failed to close IME");
         }
-        focusParkingView.recycle();
-    }
-
-    /**
-     * Returns the FocusParkingView in the same window with the given {@code node}, or null if not
-     * found.
-     */
-    private AccessibilityNodeInfo findFocusParkingView(@NonNull AccessibilityNodeInfo node) {
-        AccessibilityWindowInfo window = node.getWindow();
-        if (window == null) {
-            L.w("Failed to get window of node: " + node);
-            return null;
-        }
-        AccessibilityNodeInfo fpv = mNavigator.findFocusParkingView(window);
-        if (fpv == null) {
-            L.e("No FocusParkingView in " + window);
-        }
-        window.recycle();
-        return fpv;
-    }
-
-    private void setScrollableContainer(@Nullable AccessibilityNodeInfo scrollableContainer) {
-        if ((mScrollableContainer == null && scrollableContainer == null)
-                || (mScrollableContainer != null
-                        && mScrollableContainer.equals(scrollableContainer))) {
-            return;
-        }
-
-        Utils.recycleNode(mScrollableContainer);
-        mScrollableContainer = copyNode(scrollableContainer);
+        fpv.recycle();
     }
 
     /**
@@ -1973,108 +1662,6 @@
     }
 
     /**
-     * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on a copy of the given {@code
-     * targetNode}.
-     *
-     * @param targetNode the node to perform action on
-     *
-     * @return true if {@code targetNode} was focused already or became focused after performing
-     *         {@link AccessibilityNodeInfo#ACTION_FOCUS}
-     */
-    private boolean performFocusAction(@NonNull AccessibilityNodeInfo targetNode) {
-        return performFocusAction(targetNode, /* arguments= */ null);
-    }
-
-    /**
-     * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on a copy of the given {@code
-     * targetNode}.
-     *
-     * @param targetNode the node to perform action on
-     * @param arguments optional bundle with additional arguments
-     *
-     * @return true if {@code targetNode} was focused already or became focused after performing
-     *         {@link AccessibilityNodeInfo#ACTION_FOCUS}
-     */
-    private boolean performFocusAction(
-            @NonNull AccessibilityNodeInfo targetNode, @Nullable Bundle arguments) {
-        // If performFocusActionInternal is called on a reference to a saved node, for example
-        // mFocusedNode, mFocusedNode might get recycled. If we use mFocusedNode later, it might
-        // cause a crash. So let's pass a copy here.
-        AccessibilityNodeInfo copyNode = copyNode(targetNode);
-        boolean success = performFocusActionInternal(copyNode, arguments);
-        copyNode.recycle();
-        return success;
-    }
-
-    /**
-     * Performs {@link AccessibilityNodeInfo#ACTION_FOCUS} on the given {@code targetNode}.
-     * <p>
-     * Note: Only {@link #performFocusAction(AccessibilityNodeInfo, Bundle)} can call this method.
-     */
-    private boolean performFocusActionInternal(
-            @NonNull AccessibilityNodeInfo targetNode, @Nullable Bundle arguments) {
-        if (targetNode.equals(mFocusedNode)) {
-            L.d("No need to focus on targetNode because it's already focused: " + targetNode);
-            return true;
-        }
-        boolean isInWebView = mNavigator.isInWebView(targetNode);
-        if (targetNode.isFocused() && !isInWebView) {
-            // This happens when:
-            // 1. A window has no FocusParkingView, thus leaving the window won't clear the view
-            //    focus in it. When going back to the window, we may find that the targetNode is
-            //    already focused. This is not allowed because we require a window must have a
-            //    FocusParkingView.
-            // 2. A window has a FocusParkingView as the first focusable view, and has a view with
-            //    android:focusedByDefault="true". When the currently focused view is removed,
-            //    Android will focus on the first focusable view (i.e., the FocusParkingView), which
-            //    fires a TYPE_VIEW_FOCUSED event. Because there is a focusedByDefault view, Android
-            //    then moves focus to it, which fires another TYPE_VIEW_FOCUSED event. When
-            //    receiving the first event (in onAutoFocusFocusParkingView()), we will find a view
-            //    nearby and try to focus it. The view we found might be the focusedByDefault view,
-            //    which was already focused by Android. This is fine, and we just need to update
-            //    mFocusedNode.
-            // If the target node is in a WebView, it may not actually be focused. In this case, we
-            // go ahead and perform ACTION_FOCUS to focus it.
-            L.w("The focus on targetNode might not be cleared: " + targetNode);
-            setFocusedNode(targetNode);
-            return true;
-        }
-        if (mPendingFocusedNodes.contains(targetNode)) {
-            L.w("Don't focus on targetNode because we just tried to focus on it and are still "
-                    + "waiting for the focus event: " + targetNode);
-            return false;
-        }
-        if (!Utils.isFocusArea(targetNode) && Utils.hasFocus(targetNode) && !isInWebView) {
-            // One of targetNode's descendants is already focused, so we can't perform ACTION_FOCUS
-            // on targetNode directly unless it's a FocusArea. The workaround is to clear the focus
-            // first (by focusing on the FocusParkingView), then focus on targetNode. The
-            // prohibition on focusing a node that has focus doesn't apply in WebViews.
-            L.d("One of targetNode's descendants is already focused: " + targetNode);
-            if (!clearFocusInCurrentWindow()) {
-                return false;
-            }
-        }
-
-        // Now we can perform ACTION_FOCUS on targetNode since it doesn't have focus, its
-        // descendant's focus has been cleared, or it's a FocusArea.
-        boolean result = targetNode.performAction(ACTION_FOCUS, arguments);
-        if (!result) {
-            L.w("Failed to perform ACTION_FOCUS on node " + targetNode);
-            return false;
-        }
-        L.d("Performed focus on node " + targetNode);
-
-        // Update the focused node and pending focused nodes.
-        // 1. If targetNode doesn't represent a FocusArea, targetNode is the focused node.
-        // 2. If targetNode represents a FocusArea, targetNode won't get focused. Instead, one of
-        //    its descendants will get focused and report a TYPE_VIEW_FOCUSED event. We'll update
-        //    the focused node properly when handling the event.
-        setFocusedNode(targetNode);
-        mPendingFocusedNodes.put(targetNode);
-        return true;
-    }
-
-    /**
      * Returns the number of "ticks" to rotate for a single rotate event with the given detent
      * {@code count} at the given time. Uses and updates {@link #mLastRotateEventTime}. The result
      * will be one, two, or three times the given detent {@code count} depending on the interval
diff --git a/src/com/android/car/rotary/TreeTraverser.java b/src/com/android/car/rotary/TreeTraverser.java
index 1cccbb0..e1dc001 100644
--- a/src/com/android/car/rotary/TreeTraverser.java
+++ b/src/com/android/car/rotary/TreeTraverser.java
@@ -23,6 +23,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import java.util.List;
+import java.util.function.Predicate;
 
 /**
  * Utility methods for traversing {@link AccessibilityNodeInfo} trees.
@@ -40,7 +41,7 @@
      */
     @Nullable
     AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node,
-            @NonNull NodePredicate targetPredicate) {
+            @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
         return findNodeOrAncestor(node, /* stopPredicate= */ null, targetPredicate);
     }
 
@@ -54,11 +55,12 @@
     @VisibleForTesting
     @Nullable
     AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node,
-            @Nullable NodePredicate stopPredicate, @NonNull NodePredicate targetPredicate) {
+            @Nullable Predicate<AccessibilityNodeInfo> stopPredicate,
+            @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
         AccessibilityNodeInfo currentNode = copyNode(node);
         while (currentNode != null
-                && (stopPredicate == null || !stopPredicate.isTarget(currentNode))) {
-            if (targetPredicate.isTarget(currentNode)) {
+                && (stopPredicate == null || !stopPredicate.test(currentNode))) {
+            if (targetPredicate.test(currentNode)) {
                 return currentNode;
             }
             AccessibilityNodeInfo parentNode = currentNode.getParent();
@@ -76,7 +78,7 @@
      */
     @Nullable
     AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node,
-            @NonNull NodePredicate targetPredicate) {
+            @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
         return depthFirstSearch(node, /* skipPredicate= */ null, targetPredicate);
     }
 
@@ -90,11 +92,12 @@
     @Nullable
     @VisibleForTesting
     AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node,
-            @Nullable NodePredicate skipPredicate, @NonNull NodePredicate targetPredicate) {
-        if (skipPredicate != null && skipPredicate.isTarget(node)) {
+            @Nullable Predicate<AccessibilityNodeInfo> skipPredicate,
+            @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
+        if (skipPredicate != null && skipPredicate.test(node)) {
             return null;
         }
-        if (targetPredicate.isTarget(node)) {
+        if (targetPredicate.test(node)) {
             return copyNode(node);
         }
         for (int i = 0; i < node.getChildCount(); i++) {
@@ -119,7 +122,7 @@
     @VisibleForTesting
     @Nullable
     AccessibilityNodeInfo reverseDepthFirstSearch(@NonNull AccessibilityNodeInfo node,
-            @NonNull NodePredicate targetPredicate) {
+            @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
         for (int i = node.getChildCount() - 1; i >= 0; i--) {
             AccessibilityNodeInfo child = node.getChild(i);
             if (child == null) {
@@ -132,7 +135,7 @@
                 return result;
             }
         }
-        if (targetPredicate.isTarget(node)) {
+        if (targetPredicate.test(node)) {
             return copyNode(node);
         }
         return null;
@@ -145,9 +148,9 @@
      */
     @VisibleForTesting
     void depthFirstSelect(@NonNull AccessibilityNodeInfo node,
-            @NonNull NodePredicate selectPredicate,
+            @NonNull Predicate<AccessibilityNodeInfo> selectPredicate,
             @NonNull List<AccessibilityNodeInfo> selectedNodes) {
-        if (selectPredicate.isTarget(node)) {
+        if (selectPredicate.test(node)) {
             selectedNodes.add(copyNode(node));
             return;
         }
diff --git a/src/com/android/car/rotary/WindowCache.java b/src/com/android/car/rotary/WindowCache.java
index 0406a96..9e21e6b 100644
--- a/src/com/android/car/rotary/WindowCache.java
+++ b/src/com/android/car/rotary/WindowCache.java
@@ -15,26 +15,52 @@
  */
 package com.android.car.rotary;
 
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Stack;
 
-/** Cache of window IDs and types. */
+/** Cache of window type and most recently focused node for each window ID. */
 class WindowCache {
     /** Window IDs. */
     private final Stack<Integer> mWindowIds;
     /** Window types keyed by window IDs. */
     private final Map<Integer, Integer> mWindowTypes;
+    /** Most recent focused nodes keyed by window IDs. */
+    private final Map<Integer, AccessibilityNodeInfo> mFocusedNodes;
+
+    @NonNull
+    private NodeCopier mNodeCopier = new NodeCopier();
 
     WindowCache() {
         mWindowIds = new Stack<>();
         mWindowTypes = new HashMap<>();
+        mFocusedNodes = new HashMap<>();
     }
 
-    /** Puts an entry, removing the existing entry, if any. */
-    void put(int windowId, int windowType) {
+    /**
+     * Saves the focused node of the given window, removing the existing entry, if any. This method
+     * should be called when the focused node changed.
+     */
+    void saveFocusedNode(int windowId, @NonNull AccessibilityNodeInfo focusedNode) {
+        if (mFocusedNodes.containsKey(windowId)) {
+            // Call remove(Integer) to remove.
+            AccessibilityNodeInfo oldNode = mFocusedNodes.remove(windowId);
+            oldNode.recycle();
+        }
+        mFocusedNodes.put(windowId, copyNode(focusedNode));
+    }
+
+    /**
+     * Saves the type of the given window, removing the existing entry, if any. This method should
+     * be called when a window was just added.
+     */
+    void saveWindowType(int windowId, int windowType) {
         Integer id = windowId;
         if (mWindowIds.contains(id)) {
             // Call remove(Integer) to remove.
@@ -45,13 +71,17 @@
         mWindowTypes.put(windowId, windowType);
     }
 
-    /** Removes an entry if it exists. */
+    /**
+     * Removes an entry if it exists. This method should be called when a window was just removed.
+     */
     void remove(int windowId) {
         Integer id = windowId;
         if (mWindowIds.contains(id)) {
             // Call remove(Integer) to remove.
             mWindowIds.remove(id);
             mWindowTypes.remove(id);
+            AccessibilityNodeInfo node = mFocusedNodes.remove(id);
+            Utils.recycleNode(node);
         }
     }
 
@@ -61,12 +91,29 @@
         return mWindowTypes.get(windowId);
     }
 
-    /** Gets the most recently added window ID, or null if none. */
+    /**
+     * Returns a copy of the most recently focused node in the most recently added window, or null
+     * if none.
+     */
     @Nullable
-    Integer getMostRecentWindowId() {
+    AccessibilityNodeInfo getMostRecentFocusedNode() {
         if (mWindowIds.isEmpty()) {
             return null;
         }
-        return mWindowIds.peek();
+        Integer recentWindowId = mWindowIds.peek();
+        if (recentWindowId == null) {
+            return null;
+        }
+        return copyNode(mFocusedNodes.get(recentWindowId));
+    }
+
+    /** Sets a mock NodeCopier instance for testing. */
+    @VisibleForTesting
+    void setNodeCopier(@NonNull NodeCopier nodeCopier) {
+        mNodeCopier = nodeCopier;
+    }
+
+    private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
+        return mNodeCopier.copy(node);
     }
 }
diff --git a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
index 75bc6c3..73a84ba 100644
--- a/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
+++ b/tests/robotests/src/com/android/car/rotary/NavigatorTest.java
@@ -36,7 +36,6 @@
 import org.robolectric.RobolectricTestRunner;
 
 import java.util.ArrayList;
-import java.util.List;
 
 @RunWith(RobolectricTestRunner.class)
 public class NavigatorTest {
@@ -49,18 +48,7 @@
     public void setUp() {
         mHunWindowBounds = new Rect(50, 10, 950, 200);
         mNodeBuilder = new NodeBuilder(new ArrayList<>());
-        mNavigator = new Navigator(
-                /* focusHistoryCacheType= */ RotaryCache.CACHE_TYPE_NEVER_EXPIRE,
-                /* focusHistoryCacheSize= */ 10,
-                /* focusHistoryExpirationTimeMs= */ 0,
-                /* focusAreaHistoryCacheType= */ RotaryCache.CACHE_TYPE_NEVER_EXPIRE,
-                /* focusAreaHistoryCacheSize= */ 5,
-                /* focusAreaHistoryExpirationTimeMs= */ 0,
-                /* focusWindowCacheType= */ RotaryCache.CACHE_TYPE_NEVER_EXPIRE,
-                /* focusWindowCacheSize= */ 5,
-                /* focusWindowExpirationTimeMs= */ 0,
-                mHunWindowBounds.left,
-                mHunWindowBounds.right,
+        mNavigator = new Navigator(mHunWindowBounds.left, mHunWindowBounds.right,
                 /* showHunOnBottom= */ false);
         mNavigator.setNodeCopier(MockNodeCopierProvider.get());
     }
@@ -500,1430 +488,6 @@
     }
 
     /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    ****************leftWindow**************    **************rightWindow****************
-     *    *                                      *    *                                       *
-     *    *  ========topLeft focus area========  *    *  ========topRight focus area========  *
-     *    *  =                                =  *    *  =                                 =  *
-     *    *  =  .............  .............  =  *    *  =  .............                  =  *
-     *    *  =  .           .  .           .  =  *    *  =  .           .                  =  *
-     *    *  =  . topLeft1  .  .  topLeft2 .  =  *    *  =  . topRight1 .                  =  *
-     *    *  =  .           .  .           .  =  *    *  =  .           .                  =  *
-     *    *  =  .............  .............  =  *    *  =  .............                  =  *
-     *    *  =                                =  *    *  =                                 =  *
-     *    *  ==================================  *    *  ===================================  *
-     *    *                                      *    *                                       *
-     *    *  =======middleLeft focus area======  *    *                                       *
-     *    *  =                                =  *    *                                       *
-     *    *  =  .............  .............  =  *    *                                       *
-     *    *  =  .           .  .           .  =  *    *                                       *
-     *    *  =  .middleLeft1.  .middleLeft2.  =  *    *                                       *
-     *    *  =  . disabled  .  . disabled  .  =  *    *                                       *
-     *    *  =  .............  .............  =  *    *                                       *
-     *    *  =                                =  *    *                                       *
-     *    *  ==================================  *    *                                       *
-     *    *                                      *    *                                       *
-     *    *  =======bottomLeft focus area======  *    *                                       *
-     *    *  =                                =  *    *                                       *
-     *    *  =  .............  .............  =  *    *                                       *
-     *    *  =  .           .  .           .  =  *    *                                       *
-     *    *  =  .bottomLeft1.  .bottomLeft2.  =  *    *                                       *
-     *    *  =  .           .  .           .  =  *    *                                       *
-     *    *  =  .............  .............  =  *    *                                       *
-     *    *  =                                =  *    *                                       *
-     *    *  ==================================  *    *                                       *
-     *    *                                      *    *                                       *
-     *    ****************************************    *****************************************
-     * </pre>
-     */
-    @Test
-    public void testFindNudgeTarget() {
-        // There are 2 windows. This is the left window.
-        Rect leftWindowBounds = new Rect(0, 0, 400, 1200);
-        AccessibilityWindowInfo leftWindow = new WindowBuilder()
-                .setBoundsInScreen(leftWindowBounds)
-                .build();
-        // We must specify window and boundsInScreen for each node when finding nudge target.
-        AccessibilityNodeInfo leftRoot = mNodeBuilder
-                .setWindow(leftWindow)
-                .setBoundsInScreen(leftWindowBounds)
-                .build();
-        setRootNodeForWindow(leftRoot, leftWindow);
-
-        // Left window has 3 vertically aligned focus areas.
-        AccessibilityNodeInfo topLeft = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 400, 400))
-                .build();
-        AccessibilityNodeInfo middleLeft = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 400, 400, 800))
-                .build();
-        AccessibilityNodeInfo bottomLeft = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 800, 400, 1200))
-                .build();
-
-        // Each focus area but middleLeft has 2 horizontally aligned views that can take focus.
-        AccessibilityNodeInfo topLeft1 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(topLeft)
-                .setBoundsInScreen(new Rect(0, 0, 200, 400))
-                .build();
-        AccessibilityNodeInfo topLeft2 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(topLeft)
-                .setBoundsInScreen(new Rect(200, 0, 400, 400))
-                .build();
-        AccessibilityNodeInfo bottomLeft1 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(bottomLeft)
-                .setBoundsInScreen(new Rect(0, 800, 200, 1200))
-                .build();
-        AccessibilityNodeInfo bottomLeft2 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(bottomLeft)
-                .setBoundsInScreen(new Rect(200, 800, 400, 1200))
-                .build();
-
-        // middleLeft focus area has 2 disabled views, so that it will be skipped when nudging.
-        AccessibilityNodeInfo middleLeft1 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(bottomLeft)
-                .setEnabled(false)
-                .setBoundsInScreen(new Rect(0, 400, 200, 800))
-                .build();
-        AccessibilityNodeInfo middleLeft2 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(bottomLeft)
-                .setEnabled(false)
-                .setBoundsInScreen(new Rect(200, 400, 400, 800))
-                .build();
-
-        // This is the right window.
-        Rect rightWindowBounds = new Rect(400, 0, 800, 1200);
-        AccessibilityWindowInfo rightWindow = new WindowBuilder()
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        AccessibilityNodeInfo rightRoot = mNodeBuilder
-                .setWindow(rightWindow)
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        setRootNodeForWindow(rightRoot, rightWindow);
-
-        // Right window has 1 focus area.
-        AccessibilityNodeInfo topRight = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .build();
-
-        // The focus area has 1 view that can take focus.
-        AccessibilityNodeInfo topRight1 = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(topRight)
-                .setBoundsInScreen(new Rect(400, 0, 600, 400))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(leftWindow);
-        windows.add(rightWindow);
-
-        // Nudge within the same window.
-        AccessibilityNodeInfo target =
-                mNavigator.findNudgeTarget(windows, topLeft1, View.FOCUS_DOWN);
-        assertThat(target).isSameAs(bottomLeft1);
-
-        // Reach to the boundary.
-        target = mNavigator.findNudgeTarget(windows, topLeft1, View.FOCUS_UP);
-        assertThat(target).isNull();
-
-        // Nudge to a different window.
-        target = mNavigator.findNudgeTarget(windows, topRight1, View.FOCUS_LEFT);
-        assertThat(target).isSameAs(topLeft2);
-
-        // When nudging back, the focus should return to the previously focused node within the
-        // previous focus area, rather than the geometrically close node or focus area.
-
-        // Firstly, we need to save the focused node.
-        mNavigator.saveFocusedNode(bottomLeft1);
-        // Then nudge to right.
-        target = mNavigator.findNudgeTarget(windows, bottomLeft1, View.FOCUS_RIGHT);
-        assertThat(target).isSameAs(topRight1);
-        // Then nudge back.
-        target = mNavigator.findNudgeTarget(windows, topRight1, View.FOCUS_LEFT);
-        assertThat(target).isSameAs(bottomLeft1);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    ****************leftWindow**************    **************rightWindow***************
-     *    *                                      *    *                                      *
-     *    *  ===left focus area===   parking1    *    *   parking2   ===right focus area===  *
-     *    *  =                   =               *    *              =                    =  *
-     *    *  =  .............    =               *    *              =  .............     =  *
-     *    *  =  .           .    =               *    *              =  .           .     =  *
-     *    *  =  .   left    .    =               *    *              =  .   right   .     =  *
-     *    *  =  .           .    =               *    *              =  .           .     =  *
-     *    *  =  .............    =               *    *              =  .............     =  *
-     *    *  =                   =               *    *              =                    =  *
-     *    *  =====================               *    *              ======================  *
-     *    *                                      *    *                                      *
-     *    ****************************************    *****************************************
-     * </pre>
-     */
-    @Test
-    public void testFindNudgeTargetWithFocusParkingView() {
-        // 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 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setBoundsInScreen(leftWindowBounds)
-                .build();
-        setRootNodeForWindow(leftRoot, leftWindow);
-
-        // Left focus area and its view inside.
-        AccessibilityNodeInfo leftFocusArea = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 300, 400))
-                .build();
-        AccessibilityNodeInfo left = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftFocusArea)
-                .setBoundsInScreen(new Rect(0, 0, 300, 400))
-                .build();
-
-        // Left focus parking view.
-        AccessibilityNodeInfo parking1 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftFocusArea)
-                .setFpv()
-                .setBoundsInScreen(new Rect(350, 0, 351, 1))
-                .build();
-
-        // Right window.
-        Rect rightWindowBounds = new Rect(400, 0, 800, 400);
-        AccessibilityWindowInfo rightWindow = new WindowBuilder()
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        AccessibilityNodeInfo rightRoot = mNodeBuilder
-                .setWindow(rightWindow)
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        setRootNodeForWindow(rightRoot, rightWindow);
-
-        // Right focus area and its view inside.
-        AccessibilityNodeInfo rightFocusArea = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(500, 0, 800, 400))
-                .build();
-        AccessibilityNodeInfo right = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightFocusArea)
-                .setBoundsInScreen(new Rect(500, 0, 800, 400))
-                .build();
-
-        // Right focus parking view.
-        AccessibilityNodeInfo parking2 = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightFocusArea)
-                .setFpv()
-                .setBoundsInScreen(new Rect(450, 0, 451, 1))
-                .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(right);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    ****************mainWindow**************
-     *    *                                      *
-     *    *    ==============top=============    *
-     *    *    =                            =    *
-     *    *    =  ...........  ...........  =    *
-     *    *    =  .         .  .         .  =    *
-     *    *    =  . topLeft .  . topRight.  =    *
-     *    *    =  .         .  .         .  =    *
-     *    *    =  ...........  ...........  =    *
-     *    *    =                            =    *
-     *    *    ==============================    *
-     *    *                                      *
-     *    *    ============bottom============    *
-     *    *    =                            =    *
-     *    *    =  ...........  ...........  =    *
-     *    *    =  .         .  .         .  =    *
-     *    *    =  . bottom  .  . bottom  .  =    *
-     *    *    =  .  Left   .  .  Right  .  =    *
-     *    *    =  ...........  ...........  =    *
-     *    *    =                            =    *
-     *    *    ==============================    *
-     *    *                                      *
-     *    ****************************************
-     * </pre>
-     * with the HUN overlapping the top of the main window:
-     * <pre>
-     *       *************hunWindow************
-     *       * ..............  .............. *
-     *       * .  hunLeft   .  .  hunRight  . *
-     *       * ..............  .............. *
-     *       **********************************
-     * </pre>
-     */
-    @Test
-    public void testFindHunNudgeTarget() {
-        // There are two windows. This is the HUN window.
-        AccessibilityWindowInfo hunWindow = new WindowBuilder()
-                .setBoundsInScreen(mHunWindowBounds)
-                .setType(AccessibilityWindowInfo.TYPE_SYSTEM)
-                .build();
-        // We must specify window and boundsInScreen for each node when finding nudge target.
-        AccessibilityNodeInfo hunRoot = mNodeBuilder
-                .setWindow(hunWindow)
-                .setBoundsInScreen(mHunWindowBounds)
-                .setFocusable(false)
-                .build();
-        setRootNodeForWindow(hunRoot, hunWindow);
-
-        // HUN window has two views that can take focus (directly in the root).
-        AccessibilityNodeInfo hunLeft = mNodeBuilder
-                .setWindow(hunWindow)
-                .setParent(hunRoot)
-                .setBoundsInScreen(new Rect(mHunWindowBounds.left, mHunWindowBounds.top,
-                        mHunWindowBounds.centerX(), mHunWindowBounds.bottom))
-                .build();
-        AccessibilityNodeInfo hunRight = mNodeBuilder
-                .setWindow(hunWindow)
-                .setParent(hunRoot)
-                .setBoundsInScreen(new Rect(mHunWindowBounds.centerX(), mHunWindowBounds.top,
-                        mHunWindowBounds.right, mHunWindowBounds.bottom))
-                .build();
-
-        // This is the main window.
-        Rect mainWindowBounds = new Rect(0, 0, 1000, 1000);
-        AccessibilityWindowInfo mainWindow = new WindowBuilder()
-                .setBoundsInScreen(mainWindowBounds)
-                .build();
-        AccessibilityNodeInfo mainRoot = mNodeBuilder
-                .setWindow(mainWindow)
-                .setBoundsInScreen(mainWindowBounds)
-                .setFocusable(false)
-                .build();
-        setRootNodeForWindow(mainRoot, mainWindow);
-
-        // Main window has two focus areas.
-        AccessibilityNodeInfo topFocusArea = mNodeBuilder
-                .setWindow(mainWindow)
-                .setParent(mainRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 1000, 500))
-                .build();
-        AccessibilityNodeInfo bottomFocusArea = mNodeBuilder
-                .setWindow(mainWindow)
-                .setParent(mainRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 500, 1000, 1000))
-                .build();
-
-        // The top focus area has two views that can take focus.
-        AccessibilityNodeInfo topLeft = mNodeBuilder
-                .setWindow(mainWindow)
-                .setParent(topFocusArea)
-                .setBoundsInScreen(new Rect(0, 0, 500, 500))
-                .build();
-        AccessibilityNodeInfo topRight = mNodeBuilder
-                .setWindow(mainWindow)
-                .setParent(topFocusArea)
-                .setBoundsInScreen(new Rect(500, 0, 1000, 500))
-                .build();
-
-        // The bottom focus area has two views that can take focus.
-        AccessibilityNodeInfo bottomLeft = mNodeBuilder
-                .setWindow(mainWindow)
-                .setParent(bottomFocusArea)
-                .setBoundsInScreen(new Rect(0, 500, 500, 1000))
-                .build();
-        AccessibilityNodeInfo bottomRight = mNodeBuilder
-                .setWindow(mainWindow)
-                .setParent(bottomFocusArea)
-                .setBoundsInScreen(new Rect(500, 500, 1000, 1000))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(hunWindow);
-        windows.add(mainWindow);
-
-        // Nudging up from the top left or right view should go to the HUN's left button. The
-        // source and target overlap so geometric targeting fails. We should fall back to using the
-        // first focusable view in the HUN.
-        AccessibilityNodeInfo target = mNavigator.findNudgeTarget(windows, topLeft, View.FOCUS_UP);
-        assertThat(target).isSameAs(hunLeft);
-        target = mNavigator.findNudgeTarget(windows, topRight, View.FOCUS_UP);
-        assertThat(target).isSameAs(hunLeft);
-
-        // Nudging up from the bottom left or right view should go to the corresponding button in
-        // the HUN, skipping over the top focus area. Geometric targeting should work.
-        target = mNavigator.findNudgeTarget(windows, bottomLeft, View.FOCUS_UP);
-        assertThat(target).isSameAs(hunLeft);
-        target = mNavigator.findNudgeTarget(windows, bottomRight, View.FOCUS_UP);
-        assertThat(target).isSameAs(hunRight);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     * In the same window
-     *
-     *            ======focus area 1===========
-     *            =                  *view1*  =
-     *            =============================
-     *
-     *            ========focus area 2=========
-     *            = *view2*                   =
-     *            =============================
-     *
-     *    =====source focus area=====
-     *    = *    source view      * =
-     *    ===========================
-     * </pre>
-     */
-    @Test
-    public void testNudgeToFocusAreaWithNoCandidates() {
-        Rect windowBounds = new Rect(0, 0, 600, 500);
-        AccessibilityWindowInfo window = new WindowBuilder()
-                .setBoundsInScreen(windowBounds)
-                .build();
-        AccessibilityNodeInfo root = mNodeBuilder
-                .setWindow(window)
-                .setBoundsInScreen(windowBounds)
-                .build();
-        setRootNodeForWindow(root, window);
-
-        // Currently focused view.
-        AccessibilityNodeInfo sourceFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 400, 400, 500))
-                .build();
-        AccessibilityNodeInfo sourceView = mNodeBuilder
-                .setWindow(window)
-                .setParent(sourceFocusArea)
-                .setBoundsInScreen(new Rect(0, 400, 400, 500))
-                .build();
-
-        // focusArea1 is a better candidate than focusArea2 for a nudge to right, but its descendant
-        // view is not a candidate.
-        AccessibilityNodeInfo focusArea1 = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(200, 0, 600, 100))
-                .build();
-        AccessibilityNodeInfo view1 = mNodeBuilder
-                .setWindow(window)
-                .setParent(focusArea1)
-                .setBoundsInScreen(new Rect(599, 0, 600, 100))
-                .build();
-
-        // focusArea2 is a worse candidate than focusArea1 for a nudge to right, but its descendant
-        // view is a candidate.
-        AccessibilityNodeInfo focusArea2 = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(200, 200, 600, 300))
-                .build();
-        AccessibilityNodeInfo view2 = mNodeBuilder
-                .setWindow(window)
-                .setParent(focusArea2)
-                .setBoundsInScreen(new Rect(200, 200, 201, 300))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(window);
-
-        // Nudge from sourceView to right, and it should go to view1.
-        AccessibilityNodeInfo target
-                = mNavigator.findNudgeTarget(windows, sourceView, View.FOCUS_RIGHT);
-        assertThat(target).isSameAs(view1);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     * In the same window
-     *
-     *          =======app bar focus area=========
-     *          =             *tab*              =
-     *          ==================================
-     *          =====browse list focus area ======
-     *          =                                =
-     *          =                                =
-     *          =         *list item 1*          =
-     *          =                                =
-     *          =                                =
-     *          =  ===control bar focus area===  =
-     *          =  =   *control bar button*   =  =
-     *          =  ============================  =
-     *          =                                =
-     *          =        *list item 2*           =
-     *          ==================================
-     * </pre>
-     */
-    @Test
-    public void testNudgeToOverlappedFocusArea() {
-        Rect windowBounds = new Rect(0, 0, 100, 100);
-        AccessibilityWindowInfo window = new WindowBuilder()
-                .setBoundsInScreen(windowBounds)
-                .build();
-        AccessibilityNodeInfo root = mNodeBuilder
-                .setWindow(window)
-                .setBoundsInScreen(windowBounds)
-                .build();
-        setRootNodeForWindow(root, window);
-
-        AccessibilityNodeInfo appBarFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 100, 10))
-                .build();
-        AccessibilityNodeInfo tab = mNodeBuilder
-                .setWindow(window)
-                .setParent(appBarFocusArea)
-                .setBoundsInScreen(new Rect(0, 0, 100, 10))
-                .build();
-
-        AccessibilityNodeInfo browseListFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 10, 100, 100))
-                .build();
-        AccessibilityNodeInfo listItem1 = mNodeBuilder
-                .setWindow(window)
-                .setParent(browseListFocusArea)
-                .setBoundsInScreen(new Rect(0, 40, 100, 50))
-                .build();
-        AccessibilityNodeInfo listItem2 = mNodeBuilder
-                .setWindow(window)
-                .setParent(browseListFocusArea)
-                .setBoundsInScreen(new Rect(0, 90, 100, 100))
-                .build();
-
-        AccessibilityNodeInfo ControlBarFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 80, 100, 90))
-                .build();
-        AccessibilityNodeInfo controlButton = mNodeBuilder
-                .setWindow(window)
-                .setParent(ControlBarFocusArea)
-                .setBoundsInScreen(new Rect(0, 80, 100, 90))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(window);
-
-        // Nudge up from controlButton, it should go to listItem1.
-        AccessibilityNodeInfo target
-                = mNavigator.findNudgeTarget(windows, controlButton, View.FOCUS_UP);
-        assertThat(target).isSameAs(listItem1);
-
-        // Nudge up from listItem1, it should go to tab.
-        target = mNavigator.findNudgeTarget(windows, listItem1, View.FOCUS_UP);
-        assertThat(target).isSameAs(tab);
-
-        // Disable cache.
-        mNavigator = new Navigator(
-                /* focusHistoryCacheType= */ RotaryCache.CACHE_TYPE_DISABLED,
-                /* focusHistoryCacheSize= */ 10,
-                /* focusHistoryExpirationTimeMs= */ 0,
-                /* focusAreaHistoryCacheType= */ RotaryCache.CACHE_TYPE_DISABLED,
-                /* focusAreaHistoryCacheSize= */ 5,
-                /* focusAreaHistoryExpirationTimeMs= */ 0,
-                /* focusWindowCacheType= */ RotaryCache.CACHE_TYPE_DISABLED,
-                /* focusWindowCacheSize= */ 5,
-                /* focusWindowExpirationTimeMs= */ 0,
-                mHunWindowBounds.left,
-                mHunWindowBounds.right,
-                /* showHunOnBottom= */ false);
-        mNavigator.setNodeCopier(MockNodeCopierProvider.get());
-
-        // Nudge down from listItem1, it should go to controlButton.
-        target = mNavigator.findNudgeTarget(windows, listItem1, View.FOCUS_DOWN);
-        assertThat(target).isSameAs(controlButton);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     * In the same window
-     *
-     *    =====source focus area=====
-     *    = *    source view      * =
-     *    ===========================
-     *
-     *    ======target focus area====
-     *    =   ---non-focusable----  =
-     *    =   -                  -  =
-     *    =   -  *target view*   -  =
-     *    =   -                  -  =
-     *    =   ---view container---  =
-     *    ===========================
-     * </pre>
-     */
-    @Test
-    public void testNudgeToFocusAreaWithIndirectChild() {
-        Rect windowBounds = new Rect(0, 0, 100, 200);
-        AccessibilityWindowInfo window = new WindowBuilder()
-                .setBoundsInScreen(windowBounds)
-                .build();
-        AccessibilityNodeInfo root = mNodeBuilder
-                .setWindow(window)
-                .setBoundsInScreen(windowBounds)
-                .build();
-        setRootNodeForWindow(root, window);
-
-        // Currently focused view.
-        AccessibilityNodeInfo sourceFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 100, 100))
-                .build();
-        AccessibilityNodeInfo sourceView = mNodeBuilder
-                .setWindow(window)
-                .setParent(sourceFocusArea)
-                .setBoundsInScreen(new Rect(0, 0, 100, 100))
-                .build();
-
-        // Target view.
-        AccessibilityNodeInfo targetFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 100, 100, 200))
-                .build();
-        // viewContainer is non-focusable.
-        AccessibilityNodeInfo viewContainer = mNodeBuilder
-                .setWindow(window)
-                .setParent(targetFocusArea)
-                .setFocusable(false)
-                .setBoundsInScreen(new Rect(0, 100, 100, 200))
-                .build();
-        AccessibilityNodeInfo targetView = mNodeBuilder
-                .setWindow(window)
-                .setParent(viewContainer)
-                .setBoundsInScreen(new Rect(0, 100, 100, 200))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(window);
-
-        // Nudge down from sourceView, and it should go to targetView.
-        AccessibilityNodeInfo target
-                = mNavigator.findNudgeTarget(windows, sourceView, View.FOCUS_DOWN);
-        assertThat(target).isSameAs(targetView);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     * In the same window
-     *
-     *    =====source focus area=====
-     *    = *    source view      * =
-     *    ===========================
-     *
-     *    ======target focus area====
-     *    =   -----focusable------  =
-     *    =   -                  -  =
-     *    =   -  *target view*   -  =
-     *    =   -                  -  =
-     *    =   ---view container---  =
-     *    ===========================
-     * </pre>
-     */
-    @Test
-    public void testNudgeToFocusAreaWithNestedFocusableChild() {
-        Rect windowBounds = new Rect(0, 0, 100, 200);
-        AccessibilityWindowInfo window = new WindowBuilder()
-                .setBoundsInScreen(windowBounds)
-                .build();
-        AccessibilityNodeInfo root = mNodeBuilder
-                .setWindow(window)
-                .setBoundsInScreen(windowBounds)
-                .build();
-        setRootNodeForWindow(root, window);
-
-        // Currently focused view.
-        AccessibilityNodeInfo sourceFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 100, 100))
-                .build();
-        AccessibilityNodeInfo sourceView = mNodeBuilder
-                .setWindow(window)
-                .setParent(sourceFocusArea)
-                .setBoundsInScreen(new Rect(0, 0, 100, 100))
-                .build();
-
-        // Target view.
-        AccessibilityNodeInfo targetFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 100, 100, 200))
-                .build();
-        // viewContainer is focusable.
-        AccessibilityNodeInfo viewContainer = mNodeBuilder
-                .setWindow(window)
-                .setParent(targetFocusArea)
-                .setBoundsInScreen(new Rect(0, 100, 100, 200))
-                .build();
-        AccessibilityNodeInfo targetView = mNodeBuilder
-                .setWindow(window)
-                .setParent(viewContainer)
-                .setBoundsInScreen(new Rect(0, 100, 100, 200))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(window);
-
-        // Nudge down from sourceView, and it should go to viewContainer.
-        AccessibilityNodeInfo target
-                = mNavigator.findNudgeTarget(windows, sourceView, View.FOCUS_DOWN);
-        assertThat(target).isSameAs(viewContainer);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    ********* app window **********
-     *    *                             *
-     *    *  === target focus area ===  *
-     *    *  =                       =  *
-     *    *  =       [view1]         =  *
-     *    *  =                       =  *
-     *    *  =    [target view]      =  *
-     *    *  =                       =  *
-     *    *  =       [view3]         =  *
-     *    *  =                       =  *
-     *    *  =========================  *
-     *    *                             *
-     *    *******************************
-     *
-     *    ********* IME window **********
-     *    *                             *
-     *    *  === source focus area ===  *
-     *    *  =                       =  *
-     *    *  =    [source view]      =  *
-     *    *  =                       =  *
-     *    *  =========================  *
-     *    *                             *
-     *    *******************************
-     * </pre>
-     */
-    @Test
-    public void testNudgeOutOfIme() {
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-
-        int appWindowId = 0x42;
-        Rect appWindowBounds = new Rect(0, 0, 400, 300);
-        AccessibilityWindowInfo appWindow = new WindowBuilder()
-                .setId(appWindowId)
-                .setBoundsInScreen(appWindowBounds)
-                .setType(AccessibilityWindowInfo.TYPE_APPLICATION)
-                .build();
-        windows.add(appWindow);
-        AccessibilityNodeInfo appRoot = mNodeBuilder
-                .setWindow(appWindow)
-                .setWindowId(appWindowId)
-                .setBoundsInScreen(appWindowBounds)
-                .build();
-        setRootNodeForWindow(appRoot, appWindow);
-        AccessibilityNodeInfo targetFocusArea = mNodeBuilder
-                .setWindow(appWindow)
-                .setWindowId(appWindowId)
-                .setParent(appRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 400, 300))
-                .build();
-        AccessibilityNodeInfo view1 = mNodeBuilder
-                .setWindow(appWindow)
-                .setWindowId(appWindowId)
-                .setParent(targetFocusArea)
-                .setBoundsInScreen(new Rect(0, 0, 400, 100))
-                .build();
-        AccessibilityNodeInfo targetView = mNodeBuilder
-                .setWindow(appWindow)
-                .setWindowId(appWindowId)
-                .setParent(targetFocusArea)
-                .setBoundsInScreen(new Rect(0, 100, 400, 200))
-                .build();
-        AccessibilityNodeInfo view3 = mNodeBuilder
-                .setWindow(appWindow)
-                .setWindowId(appWindowId)
-                .setParent(targetFocusArea)
-                .setBoundsInScreen(new Rect(0, 200, 400, 300))
-                .build();
-
-        int imeWindowId = 0x39;
-        Rect imeWindowBounds = new Rect(0, 300, 400, 400);
-        AccessibilityWindowInfo imeWindow = new WindowBuilder()
-                .setId(imeWindowId)
-                .setBoundsInScreen(imeWindowBounds)
-                .setType(AccessibilityWindowInfo.TYPE_INPUT_METHOD)
-                .build();
-        windows.add(imeWindow);
-        AccessibilityNodeInfo imeRoot = mNodeBuilder
-                .setWindow(imeWindow)
-                .setWindowId(imeWindowId)
-                .setBoundsInScreen(imeWindowBounds)
-                .build();
-        setRootNodeForWindow(imeRoot, imeWindow);
-        AccessibilityNodeInfo sourceFocusArea = mNodeBuilder
-                .setWindow(imeWindow)
-                .setWindowId(imeWindowId)
-                .setParent(imeRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 300, 400, 400))
-                .build();
-        AccessibilityNodeInfo sourceView = mNodeBuilder
-                .setWindow(imeWindow)
-                .setWindowId(imeWindowId)
-                .setParent(sourceFocusArea)
-                .setBoundsInScreen(new Rect(0, 300, 400, 400))
-                .build();
-
-        // Nudge up from sourceView with the targetView already focused, and it should go to
-        // targetView. This is what happens when the user nudges up from the IME to go back to the
-        // EditText they were editing.
-        AccessibilityNodeInfo target
-                = mNavigator.findNudgeTarget(windows, sourceView, View.FOCUS_UP, targetView);
-        assertThat(target).isSameAs(targetView);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    **********leftWindow*********    ****************rightWindow*****************
-     *    *                           *    *                                          *
-     *    *  ===left focus area===    *    *    ==========right focus area========    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =                   =    *    *    =  .........scrollable.........  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =      left         =    *    *    =  .      non-focusable       .  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =                   =    *    *    =  .......recyclerView.........  =    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =====================    *    *    ==================================    *
-     *    *                           *    *                                          *
-     *    *****************************    ********************************************
-     * </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 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setBoundsInScreen(leftWindowBounds)
-                .build();
-        setRootNodeForWindow(leftRoot, leftWindow);
-
-        // Left focus area and its view inside.
-        AccessibilityNodeInfo leftFocusArea = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 400, 400))
-                .build();
-        AccessibilityNodeInfo left = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftFocusArea)
-                .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 = mNodeBuilder
-                .setWindow(rightWindow)
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        setRootNodeForWindow(rightRoot, rightWindow);
-
-        // Right focus area and its view inside.
-        AccessibilityNodeInfo rightFocusArea = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .build();
-        AccessibilityNodeInfo recyclerView = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightFocusArea)
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .setScrollableContainer()
-                .setScrollable(true)
-                .build();
-        AccessibilityNodeInfo nonFocusable = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(recyclerView)
-                .setFocusable(false)
-                .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(recyclerView);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    **********leftWindow*********    ****************rightWindow*****************
-     *    *                           *    *                                          *
-     *    *  ===left focus area===    *    *    ==========right focus area========    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =                   =    *    *    =  .........scrollable.........  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =      left         =    *    *    =  .    non-focusable         .  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =                   =    *    *    =  .  focusable(off screen)   .  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =                   =    *    *    =  .......recyclerView.........  =    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =====================    *    *    ==================================    *
-     *    *                           *    *                                          *
-     *    *****************************    ********************************************
-     * </pre>
-     */
-    @Test
-    public void testFindNudgeTargetReturnScrollableContainer2() {
-        // 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 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setBoundsInScreen(leftWindowBounds)
-                .build();
-        setRootNodeForWindow(leftRoot, leftWindow);
-
-        // Left focus area and its view inside.
-        AccessibilityNodeInfo leftFocusArea = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 400, 400))
-                .build();
-        AccessibilityNodeInfo left = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftFocusArea)
-                .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 = mNodeBuilder
-                .setWindow(rightWindow)
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        setRootNodeForWindow(rightRoot, rightWindow);
-
-        // Right focus area and its view inside.
-        AccessibilityNodeInfo rightFocusArea = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .build();
-        AccessibilityNodeInfo recyclerView = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightFocusArea)
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .setScrollableContainer()
-                .setScrollable(true)
-                .build();
-        AccessibilityNodeInfo nonFocusable = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(recyclerView)
-                .setFocusable(false)
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .build();
-        AccessibilityNodeInfo focusable = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(recyclerView)
-                .setBoundsInScreen(new Rect(0, 0, 0, 0))
-                .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(recyclerView);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    **********leftWindow*********    ****************rightWindow*****************
-     *    *                           *    *                                          *
-     *    *  ===left focus area===    *    *    ==========right focus area========    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =                   =    *    *    =  .......non-scrollable.......  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =      left         =    *    *    =  .    non-focusable         .  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =                   =    *    *    =  .......recyclerView.........  =    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =====================    *    *    ==================================    *
-     *    *                           *    *                                          *
-     *    *****************************    ********************************************
-     * </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 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setBoundsInScreen(leftWindowBounds)
-                .build();
-        setRootNodeForWindow(leftRoot, leftWindow);
-
-        // Left focus area and its view inside.
-        AccessibilityNodeInfo leftFocusArea = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 400, 400))
-                .build();
-        AccessibilityNodeInfo left = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftFocusArea)
-                .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 = mNodeBuilder
-                .setWindow(rightWindow)
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        setRootNodeForWindow(rightRoot, rightWindow);
-
-        // Right focus area and its view inside.
-        AccessibilityNodeInfo rightFocusArea = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .build();
-        AccessibilityNodeInfo recyclerView = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightFocusArea)
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .setScrollableContainer()
-                .build();
-        AccessibilityNodeInfo nonFocusable = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(recyclerView)
-                .setFocusable(false)
-                .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(null);
-    }
-
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     *    **********leftWindow*********    ****************rightWindow*****************
-     *    *                           *    *                                          *
-     *    *  ===left focus area===    *    *    ==========right focus area========    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =                   =    *    *    =  .........scrollable.........  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =      left         =    *    *    =  .       focusable          .  =    *
-     *    *  =                   =    *    *    =  .                          .  =    *
-     *    *  =                   =    *    *    =  .......recyclerView.........  =    *
-     *    *  =                   =    *    *    =                                =    *
-     *    *  =====================    *    *    ==================================    *
-     *    *                           *    *                                          *
-     *    *****************************    ********************************************
-     * </pre>
-     */
-    @Test
-    public void testFindNudgeTargetSkipScrollableContainer2() {
-        // 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 = mNodeBuilder
-                .setWindow(leftWindow)
-                .setBoundsInScreen(leftWindowBounds)
-                .build();
-        setRootNodeForWindow(leftRoot, leftWindow);
-
-        // Left focus area and its view inside.
-        AccessibilityNodeInfo leftFocusArea = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 400, 400))
-                .build();
-        AccessibilityNodeInfo left = mNodeBuilder
-                .setWindow(leftWindow)
-                .setParent(leftFocusArea)
-                .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 = mNodeBuilder
-                .setWindow(rightWindow)
-                .setBoundsInScreen(rightWindowBounds)
-                .build();
-        setRootNodeForWindow(rightRoot, rightWindow);
-
-        // Right focus area and its view inside.
-        AccessibilityNodeInfo rightFocusArea = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightRoot)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .build();
-        AccessibilityNodeInfo recyclerView = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(rightFocusArea)
-                .setBoundsInScreen(new Rect(400, 0, 800, 400))
-                .setScrollableContainer()
-                .setScrollable(true)
-                .build();
-        AccessibilityNodeInfo focusable = mNodeBuilder
-                .setWindow(rightWindow)
-                .setParent(recyclerView)
-                .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#findNudgeTarget} in the following layout:
-     * <pre>
-     * In the same window
-     *
-     *          =====contact list focus area======        ========app bar focus area========
-     *          = ********contact list********** =  <-->  =            *tab*               =
-     *          = *                            * =        ==================================
-     *          = *                            * =
-     *          = *                            * =
-     *          = *   non-focusable item1      * =
-     *          = *                            * =
-     *          = *   non-focusable item2      * =
-     *          = *                            * =
-     *          = ********recyclerView********** =
-     *          ==================================
-     *
-     *          ========nav bar focus area========
-     *          =            *button*            =
-     *          ==================================
-     * </pre>
-     * Where app bar focus area overlaps with contact list focus area.
-     */
-    @Test
-    public void testFindNudgeTargetReturnContactList() {
-        Rect windowBounds = new Rect(0, 0, 100, 100);
-        AccessibilityWindowInfo window = new WindowBuilder()
-                .setBoundsInScreen(windowBounds)
-                .build();
-        AccessibilityNodeInfo root = mNodeBuilder
-                .setWindow(window)
-                .setBoundsInScreen(windowBounds)
-                .build();
-        setRootNodeForWindow(root, window);
-
-        AccessibilityNodeInfo contactListFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 100, 80))
-                .build();
-        AccessibilityNodeInfo contactList = mNodeBuilder
-                .setWindow(window)
-                .setParent(contactListFocusArea)
-                .setBoundsInScreen(new Rect(0, 0, 100, 80))
-                .setScrollableContainer()
-                .setScrollable(true)
-                .build();
-
-        AccessibilityNodeInfo item1 = mNodeBuilder
-                .setWindow(window)
-                .setParent(contactList)
-                .setFocusable(false)
-                .setBoundsInScreen(new Rect(0, 40, 100, 50))
-                .build();
-        AccessibilityNodeInfo item2 = mNodeBuilder
-                .setWindow(window)
-                .setParent(contactList)
-                .setFocusable(false)
-                .setBoundsInScreen(new Rect(0, 50, 100, 60))
-                .build();
-
-        AccessibilityNodeInfo appBarFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 0, 100, 20))
-                .build();
-        AccessibilityNodeInfo tab = mNodeBuilder
-                .setWindow(window)
-                .setParent(appBarFocusArea)
-                .setBoundsInScreen(new Rect(40, 0, 50, 20))
-                .build();
-
-        AccessibilityNodeInfo navBarFocusArea = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 80, 100, 100))
-                .build();
-        AccessibilityNodeInfo button = mNodeBuilder
-                .setWindow(window)
-                .setParent(navBarFocusArea)
-                .setBoundsInScreen(new Rect(40, 80, 50, 100))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(window);
-
-        // Nudge down from tab, it should go to contact list.
-        AccessibilityNodeInfo target
-                = mNavigator.findNudgeTarget(windows, tab, View.FOCUS_DOWN);
-        assertThat(target).isSameAs(contactList);
-    }
-
-    /**
-     * Tests {@link Navigator#findNudgeTarget} in the following layout:
-     * <pre>
-     * In the same window
-     *
-     *          ==========focus area 1============
-     *          =  .........top offset.........  =
-     *          =  .        *view1*           .  =
-     *          =  .......bottom offset........  =
-     *          =                                =
-     *          =  ========focus area 2========  =
-     *          =  =         *view2*          =  =
-     *          =  ============================  =
-     *          =                                =
-     *          =                                =
-     *          =                                =
-     *          =                                =
-     *          ==================================
-     *
-     *          ===========focus area 3===========
-     *          =            *view3*             =
-     *          ==================================
-     * </pre>
-     */
-    @Test
-    public void testFindNudgeTargetWithFocusAreaBoundsOffset() {
-        Rect windowBounds = new Rect(0, 0, 100, 100);
-        AccessibilityWindowInfo window = new WindowBuilder()
-                .setBoundsInScreen(windowBounds)
-                .build();
-        AccessibilityNodeInfo root = mNodeBuilder
-                .setWindow(window)
-                .setBoundsInScreen(windowBounds)
-                .build();
-        setRootNodeForWindow(root, window);
-
-        AccessibilityNodeInfo focusArea1 = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setFocusAreaBoundsOffset(0, 0, 0, 70)
-                .setBoundsInScreen(new Rect(0, 0, 100, 80))
-                .build();
-        AccessibilityNodeInfo view1 = mNodeBuilder
-                .setWindow(window)
-                .setParent(focusArea1)
-                .setBoundsInScreen(new Rect(0, 0, 100, 10))
-                .build();
-
-        AccessibilityNodeInfo focusArea2 = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 10, 100, 20))
-                .build();
-        AccessibilityNodeInfo view2 = mNodeBuilder
-                .setWindow(window)
-                .setParent(focusArea2)
-                .setBoundsInScreen(new Rect(0, 10, 100, 20))
-                .build();
-
-        AccessibilityNodeInfo focusArea3 = mNodeBuilder
-                .setWindow(window)
-                .setParent(root)
-                .setFocusArea()
-                .setBoundsInScreen(new Rect(0, 90, 100, 100))
-                .build();
-        AccessibilityNodeInfo view3 = mNodeBuilder
-                .setWindow(window)
-                .setParent(focusArea3)
-                .setBoundsInScreen(new Rect(0, 90, 100, 100))
-                .build();
-
-        List<AccessibilityWindowInfo> windows = new ArrayList<>();
-        windows.add(window);
-
-        // Nudge up from view3, it should go to view2.
-        AccessibilityNodeInfo target
-                = mNavigator.findNudgeTarget(windows, view3, View.FOCUS_UP);
-        assertThat(target).isSameAs(view2);
-    }
-
-    /**
-     * Tests {@link Navigator#findFirstFocusDescendant} in the following node tree:
-     * <pre>
-     *                   root
-     *                  /    \
-     *                /       \
-     *          focusArea1  focusArea2
-     *           /   \          /   \
-     *         /      \        /     \
-     *     button1 button2 button3 button4
-     * </pre>
-     */
-    @Test
-    public void testFindFirstFocusDescendant() {
-        AccessibilityNodeInfo root = mNodeBuilder.setFocusable(false).build();
-        AccessibilityNodeInfo focusArea1 = mNodeBuilder.setParent(root).setFocusArea().build();
-        AccessibilityNodeInfo focusArea2 = mNodeBuilder.setParent(root).setFocusArea().build();
-
-        AccessibilityNodeInfo button1 = mNodeBuilder.setParent(focusArea1).build();
-        AccessibilityNodeInfo button2 = mNodeBuilder.setParent(focusArea1).build();
-        AccessibilityNodeInfo button3 = mNodeBuilder.setParent(focusArea2).build();
-        AccessibilityNodeInfo button4 = mNodeBuilder.setParent(focusArea2).build();
-
-        int direction = View.FOCUS_FORWARD;
-
-        // Search forward from the focus area.
-        when(focusArea1.focusSearch(direction)).thenReturn(button2);
-        AccessibilityNodeInfo target = mNavigator.findFirstFocusDescendant(root);
-        assertThat(target).isSameAs(button2);
-
-        // Fall back to tree traversal.
-        when(focusArea1.focusSearch(direction)).thenReturn(null);
-        target = mNavigator.findFirstFocusDescendant(root);
-        assertThat(target).isSameAs(button1);
-    }
-
-    /**
-     * Tests {@link Navigator#findFirstFocusDescendant} in the following node tree:
-     * <pre>
-     *                     root
-     *                    /    \
-     *                   /      \
-     *      focusParkingView   focusArea
-     *                           /    \
-     *                          /      \
-     *                      button1   button2
-     * </pre>
-     */
-    @Test
-    public void testFindFirstFocusDescendantWithFocusParkingView() {
-        AccessibilityNodeInfo root = mNodeBuilder.setFocusable(false).build();
-        AccessibilityNodeInfo focusParkingView = mNodeBuilder.setParent(root).setFpv().build();
-        AccessibilityNodeInfo focusArea = mNodeBuilder.setParent(root).setFocusArea().build();
-
-        AccessibilityNodeInfo button1 = mNodeBuilder.setParent(focusArea).build();
-        AccessibilityNodeInfo button2 = mNodeBuilder.setParent(focusArea).build();
-
-        int direction = View.FOCUS_FORWARD;
-
-        // Search forward from the focus area.
-        when(focusArea.focusSearch(direction)).thenReturn(button2);
-        AccessibilityNodeInfo target = mNavigator.findFirstFocusDescendant(root);
-        assertThat(target).isSameAs(button2);
-
-        // Fall back to tree traversal.
-        when(focusArea.focusSearch(direction)).thenReturn(null);
-        target = mNavigator.findFirstFocusDescendant(root);
-        assertThat(target).isSameAs(button1);
-    }
-
-    /**
      * Tests {@link Navigator#findScrollableContainer} in the following node tree:
      * <pre>
      *                root
diff --git a/tests/robotests/src/com/android/car/rotary/PendingFocusedNodesTest.java b/tests/robotests/src/com/android/car/rotary/PendingFocusedNodesTest.java
deleted file mode 100644
index e31ef38..0000000
--- a/tests/robotests/src/com/android/car/rotary/PendingFocusedNodesTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.rotary;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Spy;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.ArrayList;
-
-@RunWith(RobolectricTestRunner.class)
-public class PendingFocusedNodesTest {
-    private static final long TIMEOUT_MS = 200;
-    private static final long UPTIME_MS = 0;
-
-    @Spy
-    private PendingFocusedNodes mPendingFocusedNodes;
-
-    private NodeBuilder mNodeBuilder;
-
-    private AccessibilityNodeInfo mNode1;
-    private AccessibilityNodeInfo mNode2;
-
-    @Before
-    public void setUp() {
-        mPendingFocusedNodes = spy(new PendingFocusedNodes(TIMEOUT_MS));
-        doReturn(UPTIME_MS).when(mPendingFocusedNodes).getUptimeMs();
-        mPendingFocusedNodes.setNodeCopier(MockNodeCopierProvider.get());
-
-        mNodeBuilder = new NodeBuilder(new ArrayList<>());
-
-        mNode1 = mNodeBuilder.build();
-        mNode2 = mNodeBuilder.build();
-    }
-
-    @Test
-    public void testContains() {
-        assertThat(mPendingFocusedNodes.contains(mNode1)).isFalse();
-
-        mPendingFocusedNodes.put(mNode1);
-        assertThat(mPendingFocusedNodes.contains(mNode1)).isTrue();
-        assertThat(mPendingFocusedNodes.contains(mNode2)).isFalse();
-    }
-
-    @Test
-    public void testIsEmpty() {
-        assertThat(mPendingFocusedNodes.isEmpty()).isTrue();
-
-        mPendingFocusedNodes.put(mNode1);
-        assertThat(mPendingFocusedNodes.isEmpty()).isFalse();
-    }
-
-    @Test
-    public void testRefreshDoesNotRemoveNode() {
-        mPendingFocusedNodes.put(mNode1);
-        mPendingFocusedNodes.refresh();
-        assertThat(mPendingFocusedNodes.contains(mNode1)).isTrue();
-    }
-
-    @Test
-    public void testRefreshRemovesExpiredNode() {
-        mPendingFocusedNodes.put(mNode1);
-        when(mPendingFocusedNodes.getUptimeMs()).thenReturn(UPTIME_MS + TIMEOUT_MS + 1);
-        mPendingFocusedNodes.refresh();
-        assertThat(mPendingFocusedNodes.isEmpty()).isTrue();
-    }
-
-    @Test
-    public void testRefreshRemovesNodeNotInViewTree() {
-        AccessibilityNodeInfo node = mNodeBuilder.setInViewTree(false).build();
-        mPendingFocusedNodes.put(node);
-        mPendingFocusedNodes.refresh();
-        assertThat(mPendingFocusedNodes.isEmpty()).isTrue();
-    }
-
-    @Test
-    public void testRemoveIf() {
-        mPendingFocusedNodes.put(mNode1);
-        AccessibilityNodeInfo disabled1 = mNodeBuilder.setEnabled(false).build();
-        AccessibilityNodeInfo disabled2 = mNodeBuilder.setEnabled(false).build();
-        mPendingFocusedNodes.put(disabled1);
-        mPendingFocusedNodes.put(disabled2);
-
-        boolean removed = mPendingFocusedNodes.removeFirstIf(node -> !node.isEnabled());
-        assertThat(removed).isTrue();
-        assertThat(mPendingFocusedNodes.size()).isEqualTo(2);
-        assertThat(mPendingFocusedNodes.contains(mNode1)).isTrue();
-
-        removed = mPendingFocusedNodes.removeFirstIf(node -> !node.isEnabled());
-        assertThat(removed).isTrue();
-        assertThat(mPendingFocusedNodes.size()).isEqualTo(1);
-        assertThat(mPendingFocusedNodes.contains(disabled1)).isFalse();
-        assertThat(mPendingFocusedNodes.contains(disabled2)).isFalse();
-        assertThat(mPendingFocusedNodes.contains(mNode1)).isTrue();
-
-        removed = mPendingFocusedNodes.removeFirstIf(node -> !node.isFocusable());
-        assertThat(removed).isFalse();
-        assertThat(mPendingFocusedNodes.contains(mNode1)).isTrue();
-    }
-}
diff --git a/tests/robotests/src/com/android/car/rotary/RotaryCacheTest.java b/tests/robotests/src/com/android/car/rotary/RotaryCacheTest.java
deleted file mode 100644
index 901fea2..0000000
--- a/tests/robotests/src/com/android/car/rotary/RotaryCacheTest.java
+++ /dev/null
@@ -1,371 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.rotary;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-import java.util.ArrayList;
-
-@RunWith(RobolectricTestRunner.class)
-public class RotaryCacheTest {
-    private static final int FOCUS_CACHE_SIZE = 10;
-    private static final int FOCUS_AREA_CACHE_SIZE = 5;
-    private static final int FOCUS_WINDOW_CACHE_SIZE = 5;
-    private static final int CACHE_TIME_OUT_MS = 10000;
-    private static final int ACTIVE_WINDOW_ID = 42;
-
-    private RotaryCache mRotaryCache;
-
-    private NodeBuilder mNodeBuilder;
-
-    private AccessibilityNodeInfo mFocusArea;
-    private AccessibilityNodeInfo mTargetFocusArea;
-    private AccessibilityNodeInfo mFocusedNode;
-
-    private long mValidTime;
-    private long mExpiredTime;
-
-    @Before
-    public void setUp() {
-        mRotaryCache = new RotaryCache(
-                /* focusHistoryCacheType= */ RotaryCache.CACHE_TYPE_EXPIRED_AFTER_SOME_TIME,
-                /* focusHistoryCacheSize= */ FOCUS_CACHE_SIZE,
-                /* focusHistoryExpirationTimeMs= */ CACHE_TIME_OUT_MS,
-                /* focusAreaHistoryCacheType= */ RotaryCache.CACHE_TYPE_EXPIRED_AFTER_SOME_TIME,
-                /* focusAreaHistoryCacheSize= */ FOCUS_AREA_CACHE_SIZE,
-                /* focusAreaHistoryExpirationTimeMs= */ CACHE_TIME_OUT_MS,
-                /* focusWindowCacheType= */ RotaryCache.CACHE_TYPE_EXPIRED_AFTER_SOME_TIME,
-                /* focusWindowCacheSize= */ FOCUS_WINDOW_CACHE_SIZE,
-                /* focusWindowExpirationTimeMs= */ CACHE_TIME_OUT_MS);
-
-        mRotaryCache.setNodeCopier(MockNodeCopierProvider.get());
-
-        mNodeBuilder = new NodeBuilder(new ArrayList<>());
-
-        mFocusArea = createNode();
-        mTargetFocusArea = createNode();
-        mFocusedNode = createNode(ACTIVE_WINDOW_ID);
-
-        mValidTime = CACHE_TIME_OUT_MS - 1;
-        mExpiredTime = CACHE_TIME_OUT_MS + 1;
-    }
-
-    @Test
-    public void testClearFocusAreaHistoryCache() {
-        // Save a focus area.
-        mRotaryCache.saveTargetFocusArea(mFocusArea, mTargetFocusArea, View.FOCUS_UP, 0);
-        assertThat(mRotaryCache.isFocusAreaHistoryCacheEmpty()).isFalse();
-
-        mRotaryCache.clearFocusAreaHistory();
-        assertThat(mRotaryCache.isFocusAreaHistoryCacheEmpty()).isTrue();
-    }
-
-    @Test
-    public void testGetFocusedNodeInTheCache() {
-        // Save a focused node.
-        mRotaryCache.saveFocusedNode(mFocusArea, mFocusedNode, 0);
-
-        // In the cache.
-        AccessibilityNodeInfo node = mRotaryCache.getFocusedNode(mFocusArea, mValidTime);
-        assertThat(node).isEqualTo(mFocusedNode);
-    }
-
-    @Test
-    public void testGetFocusedNodeNotInTheCache() {
-        // Not in the cache.
-        AccessibilityNodeInfo node = mRotaryCache.getFocusedNode(mTargetFocusArea, mValidTime);
-        assertThat(node).isNull();
-    }
-
-    @Test
-    public void testGetFocusedNodeExpiredCache() {
-        // Save a focused node.
-        mRotaryCache.saveFocusedNode(mFocusArea, mFocusedNode, 0);
-
-        // Expired cache.
-        AccessibilityNodeInfo node = mRotaryCache.getFocusedNode(mFocusArea, mExpiredTime);
-        assertThat(node).isNull();
-    }
-
-    @Test
-    public void testGetFocusedNodeNotInViewTree() {
-        // Save a node that is no longer in the view tree.
-        AccessibilityNodeInfo node =
-                mNodeBuilder.setInViewTree(false).setFocusable(true).build();
-        mRotaryCache.saveFocusedNode(mFocusArea, node, 0);
-
-        AccessibilityNodeInfo result = mRotaryCache.getFocusedNode(mFocusArea, mValidTime);
-        assertThat(result).isNull();
-    }
-
-    @Test
-    public void testGetFocusedNodeCannotTakeFocus() {
-        // Save a node that is still in the view tree but can't take focus.
-        AccessibilityNodeInfo node =
-                mNodeBuilder.setInViewTree(true).setFocusable(false).build();
-        mRotaryCache.saveFocusedNode(mFocusArea, node, 0);
-
-        AccessibilityNodeInfo result = mRotaryCache.getFocusedNode(mFocusArea, mValidTime);
-        assertThat(result).isNull();
-    }
-
-    /** Saves so many nodes that the cache overflows and the previously saved node is kicked off. */
-    @Test
-    public void testFocusCacheOverflow() {
-        // Save a focused node (mFocusedNode).
-        mRotaryCache.saveFocusedNode(mFocusArea, mFocusedNode, 0);
-
-        // Save FOCUS_CACHE_SIZE nodes to make the cache overflow.
-        for (int i = 0; i < FOCUS_CACHE_SIZE; i++) {
-            saveFocusHistory();
-        }
-
-        // mFocusedNode should have been kicked off.
-        AccessibilityNodeInfo savedNode = mRotaryCache.getFocusedNode(mFocusArea, mValidTime);
-        assertThat(savedNode).isNull();
-    }
-
-    @Test
-    public void testFocusCacheNotOverflow() {
-        // Save a focused node (mFocusedNode).
-        mRotaryCache.saveFocusedNode(mFocusArea, mFocusedNode, 0);
-
-        // Save (FOCUS_CACHE_SIZE - 1) nodes so that the cache is just full.
-        for (int i = 0; i < FOCUS_CACHE_SIZE - 1; i++) {
-            saveFocusHistory();
-        }
-
-        // mFocusedNode should still be in the cache.
-        AccessibilityNodeInfo savedNode = mRotaryCache.getFocusedNode(mFocusArea, mValidTime);
-        assertThat(savedNode).isEqualTo(mFocusedNode);
-    }
-
-    @Test
-    public void testGetTargetFocusAreaInTheCache() {
-        int direction = View.FOCUS_LEFT;
-        int oppositeDirection = RotaryCache.getOppositeDirection(direction);
-
-        // Save a focus area.
-        mRotaryCache.saveTargetFocusArea(mFocusArea, mTargetFocusArea, direction, 0);
-
-        // In the cache.
-        AccessibilityNodeInfo node =
-                mRotaryCache.getTargetFocusArea(mTargetFocusArea, oppositeDirection, mValidTime);
-        assertThat(node).isEqualTo(mFocusArea);
-    }
-
-    @Test
-    public void testGetTargetFocusAreaNotInTheCache() {
-        int direction = View.FOCUS_LEFT;
-
-        // Save a focus area.
-        mRotaryCache.saveTargetFocusArea(mFocusArea, mTargetFocusArea, direction, 0);
-
-        // Not in the cache because the direction doesn't match.
-        AccessibilityNodeInfo node = mRotaryCache.getTargetFocusArea(mTargetFocusArea, direction,
-                mValidTime);
-        assertThat(node).isNull();
-    }
-
-    @Test
-    public void testGetTargetFocusAreaNotInViewTree() {
-        int direction = View.FOCUS_LEFT;
-        int oppositeDirection = RotaryCache.getOppositeDirection(direction);
-
-        // Save a focus area that is no longer in the view tree.
-        AccessibilityNodeInfo focusArea = mNodeBuilder.setInViewTree(false).build();
-        mRotaryCache.saveTargetFocusArea(focusArea, mTargetFocusArea, direction, 0);
-
-        AccessibilityNodeInfo result =
-                mRotaryCache.getTargetFocusArea(mTargetFocusArea, oppositeDirection, mValidTime);
-        assertThat(result).isNull();
-    }
-
-    @Test
-    public void testGetTargetFocusAreaExpiredCache() {
-        int direction = View.FOCUS_LEFT;
-        int oppositeDirection = RotaryCache.getOppositeDirection(direction);
-
-        // Save a focus area.
-        mRotaryCache.saveTargetFocusArea(mFocusArea, mTargetFocusArea, direction, 0);
-
-        // Expired cache.
-        AccessibilityNodeInfo node = mRotaryCache.getTargetFocusArea(mTargetFocusArea,
-                oppositeDirection, mExpiredTime);
-        assertThat(node).isNull();
-    }
-
-    /**
-     * Saves so many focus areas that the cache overflows and the previously saved focus area is
-     * kicked off.
-     */
-    @Test
-    public void testFocusAreaCacheOverflow() {
-        int direction = View.FOCUS_RIGHT;
-        int oppositeDirection = RotaryCache.getOppositeDirection(direction);
-
-        // Save a focus area.
-        mRotaryCache.saveTargetFocusArea(mFocusArea, mTargetFocusArea, direction, 0);
-
-        // Save FOCUS_AREA_CACHE_SIZE focus areas to make the cache overflow.
-        for (int i = 0; i < FOCUS_AREA_CACHE_SIZE; i++) {
-            saveFocusAreaHistory();
-        }
-
-        // Previously saved focus area should have been kicked off.
-        AccessibilityNodeInfo savedFocusArea =
-                mRotaryCache.getTargetFocusArea(mTargetFocusArea, oppositeDirection, mValidTime);
-        assertThat(savedFocusArea).isNull();
-    }
-
-    @Test
-    public void testFocusAreaCacheNotOverflow() {
-        int direction = View.FOCUS_RIGHT;
-        int oppositeDirection = RotaryCache.getOppositeDirection(direction);
-
-        // Save a focus area.
-        mRotaryCache.saveTargetFocusArea(mFocusArea, mTargetFocusArea, direction, 0);
-
-        // Save (FOCUS_AREA_CACHE_SIZE - 1) focus areas so that the cache is just full.
-        for (int i = 0; i < FOCUS_AREA_CACHE_SIZE - 1; i++) {
-            saveFocusAreaHistory();
-        }
-
-        // Previously saved focus area should still be in the cache.
-        AccessibilityNodeInfo savedFocusArea =
-                mRotaryCache.getTargetFocusArea(mTargetFocusArea, oppositeDirection, mValidTime);
-        assertThat(savedFocusArea).isEqualTo(mFocusArea);
-    }
-
-    @Test
-    public void testGetWindowFocusInTheCache() {
-        // Save a window focus.
-        mRotaryCache.saveWindowFocus(mFocusedNode, 0);
-
-        // In the cache.
-        AccessibilityNodeInfo node = mRotaryCache.getMostRecentFocus(ACTIVE_WINDOW_ID, mValidTime);
-        assertThat(node).isEqualTo(mFocusedNode);
-    }
-
-    @Test
-    public void testGetWindowFocusNotInTheCache() {
-        // Not in the cache.
-        AccessibilityNodeInfo node = mRotaryCache.getMostRecentFocus(ACTIVE_WINDOW_ID, mValidTime);
-        assertThat(node).isNull();
-    }
-
-    @Test
-    public void testGetWindowFocusExpiredCache() {
-        // Save a window focus.
-        mRotaryCache.saveWindowFocus(mFocusedNode, 0);
-
-        // Expired cache.
-        AccessibilityNodeInfo node =
-                mRotaryCache.getMostRecentFocus(ACTIVE_WINDOW_ID, mExpiredTime);
-        assertThat(node).isNull();
-    }
-
-    @Test
-    public void testGetWindowFocusNotInViewTree() {
-        // Save a window focus that is no longer in the view tree.
-        AccessibilityNodeInfo node = mNodeBuilder
-                .setInViewTree(false)
-                .setFocusable(true)
-                .setWindowId(ACTIVE_WINDOW_ID)
-                .build();
-        mRotaryCache.saveWindowFocus(node, 0);
-
-        AccessibilityNodeInfo result =
-                mRotaryCache.getMostRecentFocus(ACTIVE_WINDOW_ID, mValidTime);
-        assertThat(result).isNull();
-    }
-
-    @Test
-    public void testGetWindowFocusCannotTakeFocus() {
-        // Save a window focus that is still in the view tree but can't take focus.
-        AccessibilityNodeInfo node = mNodeBuilder
-                .setInViewTree(true)
-                .setFocusable(false)
-                .setWindowId(ACTIVE_WINDOW_ID)
-                .build();
-        mRotaryCache.saveWindowFocus(node, 0);
-
-        AccessibilityNodeInfo result =
-                mRotaryCache.getMostRecentFocus(ACTIVE_WINDOW_ID, mValidTime);
-        assertThat(result).isNull();
-    }
-
-    @Test
-    public void testGetWindowFocusWithWrongId() {
-        // Save a window focus.
-        mRotaryCache.saveWindowFocus(mFocusedNode, 0);
-
-        // In the cache.
-        AccessibilityNodeInfo node =
-                mRotaryCache.getMostRecentFocus(ACTIVE_WINDOW_ID + 1, mValidTime);
-        assertThat(node).isNull();
-    }
-
-    @Test
-    public void testGetWindowFocusInMultipleWindows() {
-        // Save two window focuses in one window and then two in another.
-        AccessibilityNodeInfo node1InWindow1 = createNode(1);
-        AccessibilityNodeInfo node2InWindow1 = createNode(1);
-        AccessibilityNodeInfo node1InWindow2 = createNode(2);
-        AccessibilityNodeInfo node2InWindow2 = createNode(2);
-        mRotaryCache.saveWindowFocus(node1InWindow1, 0);
-        mRotaryCache.saveWindowFocus(node2InWindow1, 0);
-        mRotaryCache.saveWindowFocus(node1InWindow2, 0);
-        mRotaryCache.saveWindowFocus(node2InWindow2, 0);
-
-        // The most recent node should be the second node in the second window.
-        AccessibilityNodeInfo node = mRotaryCache.getMostRecentFocus(2, mValidTime);
-        assertThat(node).isEqualTo(node2InWindow2);
-    }
-
-    /** Creates a node. */
-    private AccessibilityNodeInfo createNode() {
-        return mNodeBuilder.build();
-    }
-
-    /** Creates a node with given {@code windowId}. */
-    private AccessibilityNodeInfo createNode(int windowId) {
-        return mNodeBuilder.setWindowId(windowId).build();
-    }
-
-    /** Creates a FocusHistory and saves it in the cache. */
-    private void saveFocusHistory() {
-        AccessibilityNodeInfo focusArea = createNode();
-        AccessibilityNodeInfo node = createNode();
-        mRotaryCache.saveFocusedNode(focusArea, node, 0);
-    }
-
-    /** Creates a FocusAreaHistory and saves it in the cache. */
-    private void saveFocusAreaHistory() {
-        AccessibilityNodeInfo focusArea = createNode();
-        AccessibilityNodeInfo targetFocusArea = createNode();
-        int direction = View.FOCUS_UP; // Any valid direction (up, down, left, or right) is fine.
-        mRotaryCache.saveTargetFocusArea(focusArea, targetFocusArea, direction, 0);
-    }
-}
diff --git a/tests/robotests/src/com/android/car/rotary/WindowCacheTest.java b/tests/robotests/src/com/android/car/rotary/WindowCacheTest.java
index 755ec0a..d33e7ba 100644
--- a/tests/robotests/src/com/android/car/rotary/WindowCacheTest.java
+++ b/tests/robotests/src/com/android/car/rotary/WindowCacheTest.java
@@ -37,8 +37,8 @@
     @Before
     public void setUp() {
         mWindowCache = new WindowCache();
-        mWindowCache.put(WINDOW_ID_1, TYPE_APPLICATION);
-        mWindowCache.put(WINDOW_ID_2, TYPE_SYSTEM);
+        mWindowCache.saveWindowType(WINDOW_ID_1, TYPE_APPLICATION);
+        mWindowCache.saveWindowType(WINDOW_ID_2, TYPE_SYSTEM);
     }
 
     @Test
@@ -53,18 +53,4 @@
         type = mWindowCache.getWindowType(WINDOW_ID_3);
         assertThat(type).isNull();
     }
-
-    @Test
-    public void testGetMostRecentWindowId() {
-        Integer id = mWindowCache.getMostRecentWindowId();
-        assertThat(id).isEqualTo(WINDOW_ID_2);
-
-        mWindowCache.remove(id);
-        id = mWindowCache.getMostRecentWindowId();
-        assertThat(id).isEqualTo(WINDOW_ID_1);
-
-        mWindowCache.remove(id);
-        id = mWindowCache.getMostRecentWindowId();
-        assertThat(id).isNull();
-    }
 }