1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.rotary;
18 
19 import android.view.accessibility.AccessibilityNodeInfo;
20 
21 import androidx.annotation.NonNull;
22 import androidx.annotation.Nullable;
23 import androidx.annotation.VisibleForTesting;
24 
25 import java.util.List;
26 
27 /**
28  * Utility methods for traversing {@link AccessibilityNodeInfo} trees.
29  */
30 class TreeTraverser {
31 
32     @NonNull
33     private NodeCopier mNodeCopier = new NodeCopier();
34 
35     /**
36      * Iterates starting at {@code node} and then progressing through its ancestors, looking for a
37      * node that satisfies {@code targetPredicate}. Returns the first such node (or a copy if it's
38      * the {@code node} itself), or null if none. The caller is responsible for recycling the
39      * result.
40      */
41     @Nullable
findNodeOrAncestor(@onNull AccessibilityNodeInfo node, @NonNull NodePredicate targetPredicate)42     AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node,
43             @NonNull NodePredicate targetPredicate) {
44         return findNodeOrAncestor(node, /* stopPredicate= */ null, targetPredicate);
45     }
46 
47     /**
48      * Iterates starting at {@code node} and then progressing through its ancestors, looking for a
49      * node that satisfies {@code targetPredicate}. Returns the first such node (or a copy if it's
50      * the {@code node} itself), or null if no such nodes are encountered before reaching the root
51      * or a node which satisfies {@code stopPredicate}. The caller is responsible for recycling the
52      * result.
53      */
54     @VisibleForTesting
55     @Nullable
findNodeOrAncestor(@onNull AccessibilityNodeInfo node, @Nullable NodePredicate stopPredicate, @NonNull NodePredicate targetPredicate)56     AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node,
57             @Nullable NodePredicate stopPredicate, @NonNull NodePredicate targetPredicate) {
58         AccessibilityNodeInfo currentNode = copyNode(node);
59         while (currentNode != null
60                 && (stopPredicate == null || !stopPredicate.isTarget(currentNode))) {
61             if (targetPredicate.isTarget(currentNode)) {
62                 return currentNode;
63             }
64             AccessibilityNodeInfo parentNode = currentNode.getParent();
65             currentNode.recycle();
66             currentNode = parentNode;
67         }
68         Utils.recycleNode(currentNode);
69         return null;
70     }
71 
72     /**
73      * Searches {@code node} and its descendants in depth-first order and returns the first node
74      * satisfying {@code targetPredicate}, if any, or returns null if not found. The caller is
75      * responsible for recycling the result.
76      */
77     @Nullable
depthFirstSearch(@onNull AccessibilityNodeInfo node, @NonNull NodePredicate targetPredicate)78     AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node,
79             @NonNull NodePredicate targetPredicate) {
80         return depthFirstSearch(node, /* skipPredicate= */ null, targetPredicate);
81     }
82 
83     /**
84      * Searches {@code node} and its descendants in depth-first order, skipping any node that
85      * satisfies {@code skipPredicate} as well as its descendants, and returns the first node
86      * satisfying {@code targetPredicate}, if any, or returns null if not found. If
87      * {@code skipPredicate} is null, no nodes are skipped. The caller is responsible for recycling
88      * the result.
89      */
90     @Nullable
91     @VisibleForTesting
depthFirstSearch(@onNull AccessibilityNodeInfo node, @Nullable NodePredicate skipPredicate, @NonNull NodePredicate targetPredicate)92     AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node,
93             @Nullable NodePredicate skipPredicate, @NonNull NodePredicate targetPredicate) {
94         if (skipPredicate != null && skipPredicate.isTarget(node)) {
95             return null;
96         }
97         if (targetPredicate.isTarget(node)) {
98             return copyNode(node);
99         }
100         for (int i = 0; i < node.getChildCount(); i++) {
101             AccessibilityNodeInfo child = node.getChild(i);
102             if (child == null) {
103                 continue;
104             }
105             AccessibilityNodeInfo result = depthFirstSearch(child, skipPredicate, targetPredicate);
106             child.recycle();
107             if (result != null) {
108                 return result;
109             }
110         }
111         return null;
112     }
113 
114     /**
115      * Searches {@code node} and its descendants in reverse depth-first order, and returns the first
116      * node satisfying {@code targetPredicate}, if any, or returns null if not found. The caller is
117      * responsible for recycling the result.
118      */
119     @VisibleForTesting
120     @Nullable
reverseDepthFirstSearch(@onNull AccessibilityNodeInfo node, @NonNull NodePredicate targetPredicate)121     AccessibilityNodeInfo reverseDepthFirstSearch(@NonNull AccessibilityNodeInfo node,
122             @NonNull NodePredicate targetPredicate) {
123         for (int i = node.getChildCount() - 1; i >= 0; i--) {
124             AccessibilityNodeInfo child = node.getChild(i);
125             if (child == null) {
126                 continue;
127             }
128             AccessibilityNodeInfo result =
129                     reverseDepthFirstSearch(child, targetPredicate);
130             child.recycle();
131             if (result != null) {
132                 return result;
133             }
134         }
135         if (targetPredicate.isTarget(node)) {
136             return copyNode(node);
137         }
138         return null;
139     }
140 
141     /**
142      * Iterates through {@code node} and its descendants in depth-first order, adding nodes which
143      * satisfy {@code selectPredicate} to {@code selectedNodes}. Descendants of these nodes aren't
144      * checked. The caller is responsible for recycling the added nodes.
145      */
146     @VisibleForTesting
depthFirstSelect(@onNull AccessibilityNodeInfo node, @NonNull NodePredicate selectPredicate, @NonNull List<AccessibilityNodeInfo> selectedNodes)147     void depthFirstSelect(@NonNull AccessibilityNodeInfo node,
148             @NonNull NodePredicate selectPredicate,
149             @NonNull List<AccessibilityNodeInfo> selectedNodes) {
150         if (selectPredicate.isTarget(node)) {
151             selectedNodes.add(copyNode(node));
152             return;
153         }
154         for (int i = 0; i < node.getChildCount(); i++) {
155             AccessibilityNodeInfo child = node.getChild(i);
156             if (child == null) {
157                 continue;
158             }
159             depthFirstSelect(child, selectPredicate, selectedNodes);
160             child.recycle();
161         }
162     }
163 
164     /** A function that takes an {@link AccessibilityNodeInfo} and returns a {@code boolean}. */
165     @VisibleForTesting
166     interface NodePredicate {
isTarget(@onNull AccessibilityNodeInfo node)167         boolean isTarget(@NonNull AccessibilityNodeInfo node);
168     }
169 
170     /** Sets a node copier for testing. */
171     @VisibleForTesting
setNodeCopier(@onNull NodeCopier nodeCopier)172     void setNodeCopier(@NonNull NodeCopier nodeCopier) {
173         mNodeCopier = nodeCopier;
174     }
175 
copyNode(@ullable AccessibilityNodeInfo node)176     private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
177         return mNodeCopier.copy(node);
178     }
179 }
180