/* * Copyright (C) 2015 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.utils.traversal; import android.annotation.TargetApi; import android.os.Build; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.util.Log; import android.view.accessibility.AccessibilityNodeInfo; import com.android.utils.LogUtils; import com.android.utils.WebInterfaceUtils; import java.util.LinkedHashMap; import java.util.Map; public class OrderedTraversalController { private WorkingTree mTree; private Map<AccessibilityNodeInfoCompat, WorkingTree> mNodeTreeMap; private Map<AccessibilityNodeInfoCompat, Boolean> mSpeakNodesCache; public OrderedTraversalController() { mNodeTreeMap = new LinkedHashMap<>(); } public void setSpeakNodesCache(Map<AccessibilityNodeInfoCompat, Boolean> speakNodeCache) { mSpeakNodesCache = speakNodeCache; } /** * before start next traversal node search the controller must be initialized. * The initialisation step includes traversal through all accessibility nodes hierarchy * to collect information about traversal order of separate subtrees and moving subtries that * has custom befor/after traverse view order * * @param compatRoot - accessibility node that serves as root node for tree hierarchy * the controller works with * @param includeChildrenOfNodesWithWebActions whether to calculator order for nodes that * support web actions. Although TalkBack uses the naviagation order specified by the * nodes, Switch Access needs to know about all nodes at the time the tree is being * created. */ public void initOrder(AccessibilityNodeInfoCompat compatRoot, boolean includeChildrenOfNodesWithWebActions) { if (compatRoot == null) { return; } NodeCachedBoundsCalculator boundsCalculator = new NodeCachedBoundsCalculator(); boundsCalculator.setSpeakNodesCache(mSpeakNodesCache); mTree = createWorkingTree(AccessibilityNodeInfoCompat.obtain(compatRoot), null, boundsCalculator, includeChildrenOfNodesWithWebActions); reorderTree(); } /** * Creates tree that reproduces AccessibilityNodeInfoCompat tree hierarchy * @param rootNode root node that is starting point for tree reproduction * @param parent parent WorkingTree node for subtree that would be returned in this method * @param includeChildrenOfNodesWithWebActions whether to calculator order for nodes that * support web actions. Although TalkBack uses the naviagation order specified by the * nodes, Switch Access needs to know about all nodes at the time the tree is being * created. * @return subtree that reproduces accessibility node hierarchy */ private WorkingTree createWorkingTree(AccessibilityNodeInfoCompat rootNode, WorkingTree parent, NodeCachedBoundsCalculator boundsCalculator, boolean includeChildrenOfNodesWithWebActions) { if (mNodeTreeMap.containsKey(rootNode)) { LogUtils.log(OrderedTraversalController.class, Log.WARN, "creating node tree with looped nodes - break the loop edge"); return null; } WorkingTree tree = new WorkingTree(rootNode, parent); mNodeTreeMap.put(rootNode, tree); // When we reach a node that supports web navigation, we traverse using the web navigation // actions, so we should not try to determine the ordering of its descendants. if (!includeChildrenOfNodesWithWebActions && WebInterfaceUtils.supportsWebActions(rootNode)) { return tree; } ReorderedChildrenIterator iterator = ReorderedChildrenIterator.createAscendingIterator(rootNode, boundsCalculator); while (iterator != null && iterator.hasNext()) { AccessibilityNodeInfoCompat child = iterator.next(); WorkingTree childSubTree = createWorkingTree(child, tree, boundsCalculator, includeChildrenOfNodesWithWebActions); if (childSubTree != null) { tree.addChild(childSubTree); } } if (iterator != null) { iterator.recycle(); } return tree; } /** * reorder previously created tree according to after/before view traversal order on * separate nodes */ private void reorderTree() { for(WorkingTree subtree : mNodeTreeMap.values()) { AccessibilityNodeInfoCompat node = subtree.getNode(); AccessibilityNodeInfoCompat beforeNode = getTraversalBefore(node); if (beforeNode != null) { WorkingTree targetTree = mNodeTreeMap.get(beforeNode); moveNodeBefore(subtree, targetTree); } else { AccessibilityNodeInfoCompat afterNode = getTraversalAfter(node); if(afterNode != null) { WorkingTree targetTree = mNodeTreeMap.get(afterNode); moveNodeAfter(subtree, targetTree); } } } } private AccessibilityNodeInfoCompat getTraversalBefore(AccessibilityNodeInfoCompat node) { //TODO remove the check after AccessibilityNodeInfoCompat.getTraversalBefore() would be committed if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { return null; } AccessibilityNodeInfo info = ((AccessibilityNodeInfo)node.getInfo()).getTraversalBefore(); if (info == null) { return null; } return new AccessibilityNodeInfoCompat(info); } private AccessibilityNodeInfoCompat getTraversalAfter(AccessibilityNodeInfoCompat node) { //TODO remove the check after AccessibilityNodeInfoCompat.getTraversalAfter() would be committed if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { return null; } AccessibilityNodeInfo info = ((AccessibilityNodeInfo)node.getInfo()).getTraversalAfter(); if (info == null) { return null; } return new AccessibilityNodeInfoCompat(info); } private void moveNodeBefore(WorkingTree movingTree, WorkingTree targetTree) { if (movingTree == null || targetTree == null) { return; } //noinspection StatementWithEmptyBody if(movingTree.hasNoChild(targetTree)) { moveNodeBeforeNonChild(movingTree, targetTree); } else { // no operation if move child before parent } } private void moveNodeBeforeNonChild(WorkingTree movingTree, WorkingTree targetTree) { WorkingTree movingTreeRoot = getParentsThatAreMovedBeforeOrSameNode(movingTree); detachSubtreeFromItsParent(movingTreeRoot); //swap target node with moving node on targets node parent children list WorkingTree parent = targetTree.getParent(); if (parent != null) { parent.swapChild(targetTree, movingTreeRoot); } movingTreeRoot.setParent(parent); //add target node as last child of moving node movingTree.addChild(targetTree); targetTree.setParent(movingTree); } /** * This method is called before moving subtree. It checks if parent of that node was moved * on its place because it has before property to that node. In that case parent node should * be moved with movingTree node. * @return top node that should be moved with movingTree node. */ private WorkingTree getParentsThatAreMovedBeforeOrSameNode(WorkingTree movingTree) { WorkingTree parent = movingTree.getParent(); if (parent == null) { return movingTree; } AccessibilityNodeInfoCompat parentNode = parent.getNode(); AccessibilityNodeInfoCompat parentNodeBefore = getTraversalBefore(parentNode); if (parentNodeBefore == null) { return movingTree; } if (parentNodeBefore.equals(movingTree.getNode())) { return getParentsThatAreMovedBeforeOrSameNode(parent); } return movingTree; } private void detachSubtreeFromItsParent(WorkingTree subtree) { WorkingTree movingTreeParent = subtree.getParent(); if (movingTreeParent != null) { movingTreeParent.removeChild(subtree); } subtree.setParent(null); } private void moveNodeAfter(WorkingTree movingTree, WorkingTree targetTree) { if (movingTree == null || targetTree == null) { return; } //noinspection StatementWithEmptyBody if(movingTree.hasNoChild(targetTree)) { moveNodeAfterNonChild(movingTree, targetTree); } else { // no operation if move parent after child } } private void moveNodeAfterNonChild(WorkingTree movingTree, WorkingTree targetTree) { movingTree = getParentsThatAreMovedBeforeOrSameNode(movingTree); detachSubtreeFromItsParent(movingTree); targetTree.addChild(movingTree); movingTree.setParent(targetTree); } public AccessibilityNodeInfoCompat findNext(AccessibilityNodeInfoCompat node) { WorkingTree tree = mNodeTreeMap.get(node); if (tree == null) { LogUtils.log(Log.WARN, "findNext(), can't find WorkingTree for AccessibilityNodeInfo"); return null; } WorkingTree nextTree = tree.getNext(); if (nextTree != null) { return AccessibilityNodeInfoCompat.obtain(nextTree.getNode()); } return null; } public AccessibilityNodeInfoCompat findPrevious(AccessibilityNodeInfoCompat node) { WorkingTree tree = mNodeTreeMap.get(node); if (tree == null) { LogUtils.log(Log.WARN, "findPrevious(), can't find WorkingTree for AccessibilityNodeInfo"); return null; } WorkingTree prevTree = tree.getPrevious(); if (prevTree != null) { return AccessibilityNodeInfoCompat.obtain(prevTree.getNode()); } return null; } /** * Searches first node to be focused */ public AccessibilityNodeInfoCompat findFirst() { if (mTree == null) { return null; } return AccessibilityNodeInfoCompat.obtain(mTree.getRoot().getNode()); } public AccessibilityNodeInfoCompat findFirst(AccessibilityNodeInfoCompat rootNode) { if (rootNode == null) { return null; } WorkingTree tree = mNodeTreeMap.get(rootNode); if (tree == null) { return null; } return AccessibilityNodeInfoCompat.obtain(tree.getNode()); } /** * Searches last node to be focused */ public AccessibilityNodeInfoCompat findLast() { if (mTree == null) { return null; } return AccessibilityNodeInfoCompat.obtain(mTree.getRoot().getLastNode().getNode()); } public AccessibilityNodeInfoCompat findLast(AccessibilityNodeInfoCompat rootNode) { if (rootNode == null) { return null; } WorkingTree tree = mNodeTreeMap.get(rootNode); if (tree == null) { return null; } return AccessibilityNodeInfoCompat.obtain(tree.getLastNode().getNode()); } /** * when controller finishes its search it should be recycled */ public void recycle() { for (AccessibilityNodeInfoCompat subtree : mNodeTreeMap.keySet()) { subtree.recycle(); } mNodeTreeMap.clear(); } }