com.android.switchaccess.SwitchAccessNodeCompat.java Source code

Java tutorial

Introduction

Here is the source code for com.android.switchaccess.SwitchAccessNodeCompat.java

Source

/*
 * 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;
        }
    }
}