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