/* * 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.switchaccess; import android.annotation.TargetApi; import android.graphics.Rect; import android.os.Build; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * This class works around shortcomings of AccessibilityNodeInfo/Compat. One major issue is that * the visibility of Views that are covered by other Views or Windows is not handled completely * by the framework, but other issues may crop up over time. * * In order to support performing actions on the UI, we need to have access to the real Info. This * class can thus either wrap or extend AccessibilityNodeInfo or Compat. Because most of the * methods in Compat work fine, a wrapper will include huge amounts of boilerplate, so this is * an extension of the Compat class (Info is final). * * The biggest issue with this class is that it can't override the static {@code obtain} methods * in compat. That means that it is not compatible with utils methods built for Compat classes. * Arguably it thus shouldn't extend Compat, but the boilerplate savings seems worth dealing with. * We may eventually drop the extending and completely hide the Compat implementation if such * obtaining becomes an issue. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class SwitchAccessNodeCompat extends AccessibilityNodeInfoCompat { private final List<AccessibilityWindowInfo> mWindowsAbove; private boolean mVisibilityCalculated = false; private Rect mVisibleBoundsInScreen; private Boolean mBoundsDuplicateAncestor; /** * Find the largest sub-rectangle that doesn't intersect a specified one. * * @param rectToModify The rect that may be modified to avoid intersections * @param otherRect The rect that should be avoided */ private static void adjustRectToAvoidIntersection(Rect rectToModify, Rect otherRect) { /* * Some rectangles are flipped around (left > right). Make sure we have two Rects free of * such pathologies. */ rectToModify.sort(); otherRect.sort(); /* * Intersect rectToModify with four rects that represent cuts of the entire space along * lines defined by the otherRect's edges */ Rect[] cuts = { new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, otherRect.left, Integer.MAX_VALUE), new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, otherRect.top), new Rect(otherRect.right, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE), new Rect(Integer.MIN_VALUE, otherRect.bottom, Integer.MAX_VALUE, Integer.MAX_VALUE) }; int maxIntersectingRectArea = 0; int indexOfLargestIntersection = -1; for (int i = 0; i < cuts.length; i++) { if (cuts[i].intersect(rectToModify)) { /* Reassign this cut to its intersection with rectToModify */ int visibleRectArea = cuts[i].width() * cuts[i].height(); if (visibleRectArea > maxIntersectingRectArea) { maxIntersectingRectArea = visibleRectArea; indexOfLargestIntersection = i; } } } if (maxIntersectingRectArea <= 0) { // The rectToModify isn't within any of our cuts, so it's entirely occuled by otherRect. rectToModify.setEmpty(); return; } rectToModify.set(cuts[indexOfLargestIntersection]); } /** * @param info The info to wrap */ public SwitchAccessNodeCompat(Object info) { this(info, null); } /** * @param info The info to wrap * @param windowsAbove The windows sitting on top of the current one. This * list is used to compute visibility. */ public SwitchAccessNodeCompat(Object info, List<AccessibilityWindowInfo> windowsAbove) { super(info); if (info == null) { throw new NullPointerException(); } if (windowsAbove == null) { mWindowsAbove = Collections.emptyList(); } else { mWindowsAbove = new ArrayList<>(windowsAbove); } } @Override public SwitchAccessNodeCompat getParent() { AccessibilityNodeInfo info = (AccessibilityNodeInfo) getInfo(); AccessibilityNodeInfo parent = info.getParent(); return (parent == null) ? null : new SwitchAccessNodeCompat(parent, this.mWindowsAbove); } @Override public SwitchAccessNodeCompat getChild(int index) { AccessibilityNodeInfo info = (AccessibilityNodeInfo) getInfo(); AccessibilityNodeInfo child = info.getChild(index); return (child == null) ? null : new SwitchAccessNodeCompat(child, this.mWindowsAbove); } /** * @return An immutable copy of the current window list */ public List<AccessibilityWindowInfo> getWindowsAbove() { return Collections.unmodifiableList(mWindowsAbove); } /** * Get the largest rectangle in the bounds of the View that is not covered by another window. * * @param visibleBoundsInScreen The rect to return the visible bounds in */ public void getVisibleBoundsInScreen(Rect visibleBoundsInScreen) { updateVisibility(); visibleBoundsInScreen.set(mVisibleBoundsInScreen); } /** * Check if this node has been found to have bounds matching an ancestor, which means it gets * special treatment during traversal. * * @return {@code true} if this node was found to have the same bounds as an ancestor. */ public boolean getHasSameBoundsAsAncestor() { // Only need to check parent if (mBoundsDuplicateAncestor == null) { SwitchAccessNodeCompat parent = getParent(); if (parent == null) { mBoundsDuplicateAncestor = false; } else { Rect parentBounds = new Rect(); Rect myBounds = new Rect(); parent.getBoundsInScreen(parentBounds); getBoundsInScreen(myBounds); mBoundsDuplicateAncestor = myBounds.equals(parentBounds); parent.recycle(); } } return mBoundsDuplicateAncestor; } /** * Get a child with duplicate bounds in the screen, if one exists. * * @return A child with duplicate bounds or {@code null} if none exists. */ public List<SwitchAccessNodeCompat> getDescendantsWithDuplicateBounds() { Rect myBounds = new Rect(); getBoundsInScreen(myBounds); List<SwitchAccessNodeCompat> descendantsWithDuplicateBounds = new ArrayList<>(); addDescendantsWithBoundsToList(descendantsWithDuplicateBounds, myBounds); return descendantsWithDuplicateBounds; } private void addDescendantsWithBoundsToList( List<SwitchAccessNodeCompat> listOfNodes, Rect bounds) { Rect childBounds = new Rect(); for (int i = 0; i < getChildCount(); i++) { SwitchAccessNodeCompat child = getChild(i); if (child == null) { continue; } child.getBoundsInScreen(childBounds); if (bounds.equals(childBounds) && !listOfNodes.contains(child)) { child.mBoundsDuplicateAncestor = true; listOfNodes.add(child); child.addDescendantsWithBoundsToList(listOfNodes, bounds); } else { // Children can't be bigger than parents, so once the bounds are different they // must be smaller, and further descendants won't duplicate the bounds child.recycle(); } } } /** * Obtain a new copy of this object. The resulting node must be recycled for efficient use * of underlying resources. * * @return A new copy of the node */ public SwitchAccessNodeCompat obtainCopy() { SwitchAccessNodeCompat obtainedInstance = new SwitchAccessNodeCompat(AccessibilityNodeInfo .obtain((AccessibilityNodeInfo) getInfo()), mWindowsAbove); /* Preserve lazily-initialized value if we have it */ if (mVisibilityCalculated) { obtainedInstance.mVisibilityCalculated = true; obtainedInstance.mVisibleBoundsInScreen = new Rect(mVisibleBoundsInScreen); } obtainedInstance.mBoundsDuplicateAncestor = mBoundsDuplicateAncestor; return obtainedInstance; } private void updateVisibility() { if (!mVisibilityCalculated) { mVisibleBoundsInScreen = new Rect(); getBoundsInScreen(mVisibleBoundsInScreen); /* Deal with visibility implications for windows above */ Rect windowBoundsInScreen = new Rect(); for (int i = 0; i < mWindowsAbove.size(); ++i) { mWindowsAbove.get(i).getBoundsInScreen(windowBoundsInScreen); mVisibleBoundsInScreen.sort(); windowBoundsInScreen.sort(); if (Rect.intersects(mVisibleBoundsInScreen, windowBoundsInScreen)) { adjustRectToAvoidIntersection(mVisibleBoundsInScreen, windowBoundsInScreen); } } mVisibilityCalculated = true; } } }