blob: e1dc0013bf4501b9d54d351812f93981b536d0d8 [file] [log] [blame]
/*
* 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;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.List;
import java.util.function.Predicate;
/**
* Utility methods for traversing {@link AccessibilityNodeInfo} trees.
*/
class TreeTraverser {
@NonNull
private NodeCopier mNodeCopier = new NodeCopier();
/**
* Iterates starting at {@code node} and then progressing through its ancestors, looking for a
* node that satisfies {@code targetPredicate}. Returns the first such node (or a copy if it's
* the {@code node} itself), or null if none. The caller is responsible for recycling the
* result.
*/
@Nullable
AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node,
@NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
return findNodeOrAncestor(node, /* stopPredicate= */ null, targetPredicate);
}
/**
* Iterates starting at {@code node} and then progressing through its ancestors, looking for a
* node that satisfies {@code targetPredicate}. Returns the first such node (or a copy if it's
* the {@code node} itself), or null if no such nodes are encountered before reaching the root
* or a node which satisfies {@code stopPredicate}. The caller is responsible for recycling the
* result.
*/
@VisibleForTesting
@Nullable
AccessibilityNodeInfo findNodeOrAncestor(@NonNull AccessibilityNodeInfo node,
@Nullable Predicate<AccessibilityNodeInfo> stopPredicate,
@NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
AccessibilityNodeInfo currentNode = copyNode(node);
while (currentNode != null
&& (stopPredicate == null || !stopPredicate.test(currentNode))) {
if (targetPredicate.test(currentNode)) {
return currentNode;
}
AccessibilityNodeInfo parentNode = currentNode.getParent();
currentNode.recycle();
currentNode = parentNode;
}
Utils.recycleNode(currentNode);
return null;
}
/**
* Searches {@code node} and its descendants in depth-first order and returns the first node
* satisfying {@code targetPredicate}, if any, or returns null if not found. The caller is
* responsible for recycling the result.
*/
@Nullable
AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node,
@NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
return depthFirstSearch(node, /* skipPredicate= */ null, targetPredicate);
}
/**
* Searches {@code node} and its descendants in depth-first order, skipping any node that
* satisfies {@code skipPredicate} as well as its descendants, and returns the first node
* satisfying {@code targetPredicate}, if any, or returns null if not found. If
* {@code skipPredicate} is null, no nodes are skipped. The caller is responsible for recycling
* the result.
*/
@Nullable
@VisibleForTesting
AccessibilityNodeInfo depthFirstSearch(@NonNull AccessibilityNodeInfo node,
@Nullable Predicate<AccessibilityNodeInfo> skipPredicate,
@NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
if (skipPredicate != null && skipPredicate.test(node)) {
return null;
}
if (targetPredicate.test(node)) {
return copyNode(node);
}
for (int i = 0; i < node.getChildCount(); i++) {
AccessibilityNodeInfo child = node.getChild(i);
if (child == null) {
continue;
}
AccessibilityNodeInfo result = depthFirstSearch(child, skipPredicate, targetPredicate);
child.recycle();
if (result != null) {
return result;
}
}
return null;
}
/**
* Searches {@code node} and its descendants in reverse depth-first order, and returns the first
* node satisfying {@code targetPredicate}, if any, or returns null if not found. The caller is
* responsible for recycling the result.
*/
@VisibleForTesting
@Nullable
AccessibilityNodeInfo reverseDepthFirstSearch(@NonNull AccessibilityNodeInfo node,
@NonNull Predicate<AccessibilityNodeInfo> targetPredicate) {
for (int i = node.getChildCount() - 1; i >= 0; i--) {
AccessibilityNodeInfo child = node.getChild(i);
if (child == null) {
continue;
}
AccessibilityNodeInfo result =
reverseDepthFirstSearch(child, targetPredicate);
child.recycle();
if (result != null) {
return result;
}
}
if (targetPredicate.test(node)) {
return copyNode(node);
}
return null;
}
/**
* Iterates through {@code node} and its descendants in depth-first order, adding nodes which
* satisfy {@code selectPredicate} to {@code selectedNodes}. Descendants of these nodes aren't
* checked. The caller is responsible for recycling the added nodes.
*/
@VisibleForTesting
void depthFirstSelect(@NonNull AccessibilityNodeInfo node,
@NonNull Predicate<AccessibilityNodeInfo> selectPredicate,
@NonNull List<AccessibilityNodeInfo> selectedNodes) {
if (selectPredicate.test(node)) {
selectedNodes.add(copyNode(node));
return;
}
for (int i = 0; i < node.getChildCount(); i++) {
AccessibilityNodeInfo child = node.getChild(i);
if (child == null) {
continue;
}
depthFirstSelect(child, selectPredicate, selectedNodes);
child.recycle();
}
}
/** Sets a node copier for testing. */
@VisibleForTesting
void setNodeCopier(@NonNull NodeCopier nodeCopier) {
mNodeCopier = nodeCopier;
}
private AccessibilityNodeInfo copyNode(@Nullable AccessibilityNodeInfo node) {
return mNodeCopier.copy(node);
}
}