/* * 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.graphics.Rect; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.util.Log; import com.android.utils.AccessibilityNodeInfoUtils; import com.android.utils.LogUtils; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Calculates the utility bounds of the node. If node is not supposed to get accessibility focus * the utility bounds is calculated on the base of minimum rect that contains all accessibility * focusable nodes inside node hierarchy rooted by this node. * WARNING: Calculator does not obtain or recycle nodes for performance reason. * If any of the nodes that was calculated is recycled the calculator could get wrong results */ public class NodeCachedBoundsCalculator { private static final Rect EMPTY_RECT = new Rect(); private Map<AccessibilityNodeInfoCompat, Rect> mBoundsMap = new HashMap<>(); private Map<AccessibilityNodeInfoCompat, Boolean> mSpeakNodesCache; private Set<AccessibilityNodeInfoCompat> mCalculatingNodes = new HashSet<>(); private Rect mTempRect = new Rect(); public void setSpeakNodesCache(Map<AccessibilityNodeInfoCompat, Boolean> speakNodeCache) { mSpeakNodesCache = speakNodeCache; } public Rect getBounds(AccessibilityNodeInfoCompat node) { Rect bounds = getBoundsInternal(node); if (bounds == EMPTY_RECT) { return null; } return bounds; } private Rect getBoundsInternal(AccessibilityNodeInfoCompat node) { if (node == null) { return EMPTY_RECT; } if (mCalculatingNodes.contains(node)) { LogUtils.log(Log.WARN, "node tree loop detected while calculating node bounds"); return EMPTY_RECT; } Rect bounds = mBoundsMap.get(node); if (bounds == null) { mCalculatingNodes.add(node); bounds = fetchBound(node); mBoundsMap.put(node, bounds); mCalculatingNodes.remove(node); } return bounds; } private Rect fetchBound(AccessibilityNodeInfoCompat node) { if (node == null || !AccessibilityNodeInfoUtils.isVisible(node)) { return EMPTY_RECT; } if (AccessibilityNodeInfoUtils.shouldFocusNode(node, mSpeakNodesCache)) { Rect bounds = new Rect(); node.getBoundsInScreen(bounds); return bounds; } int childCount = node.getChildCount(); int minTop = Integer.MAX_VALUE; int minLeft = Integer.MAX_VALUE; int maxBottom = Integer.MIN_VALUE; int maxRight = Integer.MIN_VALUE; AccessibilityNodeInfoCompat child = null; boolean hasChildBounds = false; for (int i = 0; i < childCount; i++) { try { child = node.getChild(i); Rect bounds = getBoundsInternal(child); if (bounds != EMPTY_RECT) { hasChildBounds = true; if (bounds.top < minTop) { minTop = bounds.top; } if (bounds.left < minLeft) { minLeft = bounds.left; } if (bounds.right > maxRight) { maxRight = bounds.right; } if (bounds.bottom > maxBottom) { maxBottom = bounds.bottom; } } } finally { AccessibilityNodeInfoUtils.recycleNodes(child); } } Rect bounds = new Rect(); node.getBoundsInScreen(bounds); if (hasChildBounds) { bounds.top = Math.max(minTop, bounds.top); bounds.left = Math.max(minLeft, bounds.left); bounds.right = Math.min(maxRight, bounds.right); bounds.bottom = Math.min(maxBottom, bounds.bottom); } return bounds; } /** * If node is not supposed to be accessibility focused by TalkBack * NodeBoundsCalculator calculates useful bounds of focusable children. * The method checks if the node uses its children useful bounds or uses its own bounds */ public boolean usesChildrenBounds(AccessibilityNodeInfoCompat node) { if (node == null) { return false; } Rect bounds = getBounds(node); if (bounds == null) { return false; } node.getBoundsInScreen(mTempRect); return !mTempRect.equals(bounds); } }