/* * Copyright (C) 2012 Google Inc. * * 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.utils; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import com.android.utils.traversal.ReorderedChildrenIterator; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * A class that simplifies traversal of node trees. * * This class keeps track of an {@link AccessibilityNodeInfoCompat} * object and can traverse to other nodes in the tree, or be reset to * other nodes. The node can be owned, in which case it will be * recycled when traversed away from or when a new node is assigned * to an object of this class. * * Any node can be assigned to objects of this class, including nodes that * are not visible to the user. The traversal methods, however, will only * traverse to visible nodes. * * @see AccessibilityNodeInfoUtils#isVisible(AccessibilityNodeInfoCompat) */ public class AccessibilityNodeInfoRef { private AccessibilityNodeInfoCompat mNode; private boolean mOwned; /** * Returns the current node. */ public AccessibilityNodeInfoCompat get() { return mNode; } /** * Clears this object, recycling the underlying node if owned. * This object should not be used after this method is called. */ // TODO: Add a pool if proven necessary. public void recycle() { clear(); } /** * Clears this object, recycling the underlying node if owned. */ public void clear() { reset((AccessibilityNodeInfoCompat) null); } /** * Resets this object to contain a new node, taking ownership of the * new node. */ public void reset(AccessibilityNodeInfoCompat newNode) { if (mNode != newNode && mNode != null && mOwned) { mNode.recycle(); } mNode = newNode; mOwned = true; } /** * Resets this object with the node held by {@code newNode}. * if {@code newNode} was owning the node, ownership is * transfered to this object. */ public void reset(AccessibilityNodeInfoRef newNode) { reset(newNode.get()); mOwned = newNode.mOwned; newNode.mOwned = false; } /** * Creates a new instance of this class containing a new copy of * {@code node}. */ public static AccessibilityNodeInfoRef obtain( AccessibilityNodeInfoCompat node) { return new AccessibilityNodeInfoRef( AccessibilityNodeInfoCompat.obtain(node), true); } /** * Creates a new instance of this class without assuming ownership of * {@code node}. */ public static AccessibilityNodeInfoRef unOwned(AccessibilityNodeInfoCompat node) { return node != null ? new AccessibilityNodeInfoRef(node, false) : null; } /** * Creates a new instance of this class taking ownership of {@code node}. */ private static AccessibilityNodeInfoRef owned( AccessibilityNodeInfoCompat node) { return node != null ? new AccessibilityNodeInfoRef(node, true) : null; } /** * Creates an {@link AccessibilityNodeInfoRef} with a refreshed copy * of {@code node}, taking ownership of the copy. * If {@code node} is {@code null}, {@code null} is returned. */ public static AccessibilityNodeInfoRef refreshed( AccessibilityNodeInfoCompat node) { return owned(AccessibilityNodeInfoUtils.refreshNode(node)); } /** * Makes sure that this object owns its own copy of the node * it holds by creating a new copy of the node if not already * owned or doing nothing otherwise. */ public AccessibilityNodeInfoRef makeOwned() { if (mNode != null && !mOwned) { reset(AccessibilityNodeInfoCompat.obtain(mNode)); } return this; } public AccessibilityNodeInfoRef() { } public static boolean isNull( AccessibilityNodeInfoRef ref) { return ref == null || ref.get() == null; } private AccessibilityNodeInfoRef(AccessibilityNodeInfoCompat node, boolean owned) { mNode = node; mOwned = owned; } /** * Releases the ownership of the underlying node if it was owned, * returning the underlying node. This is typically chained with * {@link #makeOwned} to have a copy that can be put in another * container or {@link AccessibilityNodeInfoRef}. * After this call, this object still refers to the underlying node * so that any of the traversal methods can be used afterwards. */ public AccessibilityNodeInfoCompat release() { mOwned = false; return mNode; } /** * Traverses to the last child of this node, returning {@code true} * on success. */ boolean lastChild() { if (mNode == null || mNode.getChildCount() < 1) { return false; } ReorderedChildrenIterator iterator = ReorderedChildrenIterator.createDescendingIterator(mNode); try { while (iterator.hasNext()) { AccessibilityNodeInfoCompat newNode = iterator.next(); if (newNode == null) { return false; } if (AccessibilityNodeInfoUtils.isVisible(newNode)) { reset(newNode); return true; } newNode.recycle(); } } finally { iterator.recycle(); } return false; } /** * Traverses to the previous sibling of this node within its parent, * returning {@code true} on success. */ boolean previousSibling() { if (mNode == null) { return false; } AccessibilityNodeInfoCompat parent = mNode.getParent(); if (parent == null) { return false; } ReorderedChildrenIterator iterator = ReorderedChildrenIterator.createDescendingIterator(parent); try { if (!moveIteratorAfterNode(iterator, mNode)) { return false; } while (iterator.hasNext()) { AccessibilityNodeInfoCompat newNode = iterator.next(); if (newNode == null) { return false; } if (AccessibilityNodeInfoUtils.isVisible(newNode)) { reset(newNode); return true; } newNode.recycle(); } } finally { iterator.recycle(); parent.recycle(); } return false; } /** * Traverses to the first child of this node if any, returning * {@code true} on success. */ boolean firstChild() { if (mNode == null) { return false; } ReorderedChildrenIterator iterator = ReorderedChildrenIterator.createAscendingIterator(mNode); try { while (iterator.hasNext()) { AccessibilityNodeInfoCompat newNode = iterator.next(); if (newNode == null) { return false; } if (AccessibilityNodeInfoUtils.isVisible(newNode)) { reset(newNode); return true; } newNode.recycle(); } } finally { iterator.recycle(); } return false; } /** * Traverses to the next sibling of this node within its parent, returning * {@code true} on success. */ boolean nextSibling() { if (mNode == null) { return false; } AccessibilityNodeInfoCompat parent = mNode.getParent(); if (parent == null) { return false; } ReorderedChildrenIterator iterator = ReorderedChildrenIterator.createAscendingIterator(parent); try { if (!moveIteratorAfterNode(iterator, mNode)) { return false; } while (iterator.hasNext()) { AccessibilityNodeInfoCompat newNode = iterator.next(); if (newNode == null) { return false; } if (AccessibilityNodeInfoUtils.isVisible(newNode)) { reset(newNode); return true; } newNode.recycle(); } } finally { iterator.recycle(); parent.recycle(); } return false; } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean moveIteratorAfterNode(Iterator<AccessibilityNodeInfoCompat> iterator, AccessibilityNodeInfoCompat node) { if (node == null) { return false; } while (iterator.hasNext()) { AccessibilityNodeInfoCompat nextNode = iterator.next(); try { if (node.equals(nextNode)) { return true; } } finally { AccessibilityNodeInfoUtils.recycleNodes(nextNode); } } return false; } /** * Traverses to the parent of this node, returning {@code true} on * success. On failure, returns {@code false} and does not move. */ boolean parent() { if (mNode == null) { return false; } Set<AccessibilityNodeInfoCompat> visitedNodes = new HashSet<>(); visitedNodes.add(AccessibilityNodeInfoCompat.obtain(mNode)); AccessibilityNodeInfoCompat parentNode = mNode.getParent(); try { while (parentNode != null) { if (visitedNodes.contains(parentNode)) { parentNode.recycle(); return false; } if (AccessibilityNodeInfoUtils.isVisible(parentNode)) { reset(parentNode); return true; } visitedNodes.add(parentNode); parentNode = parentNode.getParent(); } } finally { AccessibilityNodeInfoUtils.recycleNodes(visitedNodes); } return false; } /** * Traverses to the next node in depth-first order, returning {@code true} * on success. */ public boolean nextInOrder() { if (mNode == null) { return false; } if (firstChild()) { return true; } if (nextSibling()) { return true; } AccessibilityNodeInfoRef tmp = unOwned(mNode); while (tmp.parent()) { if (tmp.nextSibling()) { reset(tmp); return true; } } tmp.clear(); return false; } /** * Traverses to the previous node in depth-first order, returning * {@code true} on success. */ public boolean previousInOrder() { if (mNode == null) { return false; } if (previousSibling()) { lastDescendant(); return true; } return parent(); } /** * Traverses to the last descendant of this node, returning {@code true} on * success. */ public boolean lastDescendant() { if (!lastChild()) { return false; } Set<AccessibilityNodeInfoCompat> visitedNodes = new HashSet<>(); try { while (lastChild()) { if (visitedNodes.contains(mNode)) { return false; } visitedNodes.add(AccessibilityNodeInfoCompat.obtain(mNode)); } } finally { AccessibilityNodeInfoUtils.recycleNodes(visitedNodes); } return true; } }