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 import java.util.function.Predicate; 27 28 /** 29 * Utility methods for traversing {@link AccessibilityNodeInfo} trees. 30 */ 31 class TreeTraverser { 32 33 @NonNull 34 private NodeCopier mNodeCopier = new NodeCopier(); 35 36 /** 37 * Iterates starting at {@code node} and then progressing through its ancestors, looking for a 38 * node that satisfies {@code targetPredicate}. Returns the first such node (or a copy if it's 39 * the {@code node} itself), or null if none. The caller is responsible for recycling the 40 * result. 41 */ 42 @Nullable findNodeOrAncestor(@onNull AccessibilityNodeInfo node, @NonNull Predicate<AccessibilityNodeInfo> targetPredicate)43 AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node, 44 @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) { 45 return findNodeOrAncestor(node, /* stopPredicate= */ null, targetPredicate); 46 } 47 48 /** 49 * Iterates starting at {@code node} and then progressing through its ancestors, looking for a 50 * node that satisfies {@code targetPredicate}. Returns the first such node (or a copy if it's 51 * the {@code node} itself), or null if no such nodes are encountered before reaching the root 52 * or a node which satisfies {@code stopPredicate}. The caller is responsible for recycling the 53 * result. 54 */ 55 @VisibleForTesting 56 @Nullable findNodeOrAncestor(@onNull AccessibilityNodeInfo node, @Nullable Predicate<AccessibilityNodeInfo> stopPredicate, @NonNull Predicate<AccessibilityNodeInfo> targetPredicate)57 AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node, 58 @Nullable Predicate<AccessibilityNodeInfo> stopPredicate, 59 @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) { 60 AccessibilityNodeInfo currentNode = copyNode(node); 61 while (currentNode != null 62 && (stopPredicate == null || !stopPredicate.test(currentNode))) { 63 if (targetPredicate.test(currentNode)) { 64 return currentNode; 65 } 66 AccessibilityNodeInfo parentNode = currentNode.getParent(); 67 currentNode.recycle(); 68 currentNode = parentNode; 69 } 70 Utils.recycleNode(currentNode); 71 return null; 72 } 73 74 /** 75 * Searches {@code node} and its descendants in depth-first order and returns the first node 76 * satisfying {@code targetPredicate}, if any, or returns null if not found. The caller is 77 * responsible for recycling the result. 78 */ 79 @Nullable depthFirstSearch(@onNull AccessibilityNodeInfo node, @NonNull Predicate<AccessibilityNodeInfo> targetPredicate)80 AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node, 81 @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) { 82 return depthFirstSearch(node, /* skipPredicate= */ null, targetPredicate); 83 } 84 85 /** 86 * Searches {@code node} and its descendants in depth-first order, skipping any node that 87 * satisfies {@code skipPredicate} as well as its descendants, and returns the first node 88 * satisfying {@code targetPredicate}, if any, or returns null if not found. If 89 * {@code skipPredicate} is null, no nodes are skipped. The caller is responsible for recycling 90 * the result. 91 */ 92 @Nullable 93 @VisibleForTesting depthFirstSearch(@onNull AccessibilityNodeInfo node, @Nullable Predicate<AccessibilityNodeInfo> skipPredicate, @NonNull Predicate<AccessibilityNodeInfo> targetPredicate)94 AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node, 95 @Nullable Predicate<AccessibilityNodeInfo> skipPredicate, 96 @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) { 97 if (skipPredicate != null && skipPredicate.test(node)) { 98 return null; 99 } 100 if (targetPredicate.test(node)) { 101 return copyNode(node); 102 } 103 for (int i = 0; i < node.getChildCount(); i++) { 104 AccessibilityNodeInfo child = node.getChild(i); 105 if (child == null) { 106 continue; 107 } 108 AccessibilityNodeInfo result = depthFirstSearch(child, skipPredicate, targetPredicate); 109 child.recycle(); 110 if (result != null) { 111 return result; 112 } 113 } 114 return null; 115 } 116 117 /** 118 * Searches {@code node} and its descendants in reverse depth-first order, and returns the first 119 * node satisfying {@code targetPredicate}, if any, or returns null if not found. The caller is 120 * responsible for recycling the result. 121 */ 122 @VisibleForTesting 123 @Nullable reverseDepthFirstSearch(@onNull AccessibilityNodeInfo node, @NonNull Predicate<AccessibilityNodeInfo> targetPredicate)124 AccessibilityNodeInfo reverseDepthFirstSearch(@NonNull AccessibilityNodeInfo node, 125 @NonNull Predicate<AccessibilityNodeInfo> targetPredicate) { 126 for (int i = node.getChildCount() - 1; i >= 0; i--) { 127 AccessibilityNodeInfo child = node.getChild(i); 128 if (child == null) { 129 continue; 130 } 131 AccessibilityNodeInfo result = 132 reverseDepthFirstSearch(child, targetPredicate); 133 child.recycle(); 134 if (result != null) { 135 return result; 136 } 137 } 138 if (targetPredicate.test(node)) { 139 return copyNode(node); 140 } 141 return null; 142 } 143 144 /** 145 * Iterates through {@code node} and its descendants in depth-first order, adding nodes which 146 * satisfy {@code selectPredicate} to {@code selectedNodes}. Descendants of these nodes aren't 147 * checked. The caller is responsible for recycling the added nodes. 148 */ 149 @VisibleForTesting depthFirstSelect(@onNull AccessibilityNodeInfo node, @NonNull Predicate<AccessibilityNodeInfo> selectPredicate, @NonNull List<AccessibilityNodeInfo> selectedNodes)150 void depthFirstSelect(@NonNull AccessibilityNodeInfo node, 151 @NonNull Predicate<AccessibilityNodeInfo> selectPredicate, 152 @NonNull List<AccessibilityNodeInfo> selectedNodes) { 153 if (selectPredicate.test(node)) { 154 selectedNodes.add(copyNode(node)); 155 return; 156 } 157 for (int i = 0; i < node.getChildCount(); i++) { 158 AccessibilityNodeInfo child = node.getChild(i); 159 if (child == null) { 160 continue; 161 } 162 depthFirstSelect(child, selectPredicate, selectedNodes); 163 child.recycle(); 164 } 165 } 166 167 /** Sets a node copier for testing. */ 168 @VisibleForTesting setNodeCopier(@onNull NodeCopier nodeCopier)169 void setNodeCopier(@NonNull NodeCopier nodeCopier) { 170 mNodeCopier = nodeCopier; 171 } 172 copyNode(@ullable AccessibilityNodeInfo node)173 private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) { 174 return mNodeCopier.copy(node); 175 } 176 } 177