com.android.switchaccess.treebuilding.TreeBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.android.switchaccess.treebuilding.TreeBuilder.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.treebuilding;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.TextUtils;

import com.android.switchaccess.AccessibilityNodeActionNode;
import com.android.switchaccess.ContextMenuItem;
import com.android.switchaccess.OptionScanNode;
import com.android.switchaccess.SwitchAccessNodeCompat;
import com.android.switchaccess.SwitchAccessPreferenceActivity;
import com.android.switchaccess.SwitchAccessWindowInfo;
import com.android.talkback.R;
import com.android.utils.SharedPreferencesUtils;
import com.android.utils.traversal.OrderedTraversalController;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * Base class for tree building. Includes some common utility methods.
 */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class TreeBuilder implements SharedPreferences.OnSharedPreferenceChangeListener {
    /* TODO Support all actions, perhaps conditioned on user preferences */
    protected static final Set<Integer> FRAMEWORK_ACTIONS = new HashSet<>(Arrays.asList(
            AccessibilityNodeInfoCompat.ACTION_CLICK, AccessibilityNodeInfoCompat.ACTION_DISMISS,
            AccessibilityNodeInfoCompat.ACTION_LONG_CLICK, AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD,
            AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD,
            AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
            AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY));
    protected static final int[] MOVEMENT_GRANULARITIES_ONE_LINE = {
            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER,
            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD };
    protected static final int[] MOVEMENT_GRANULARITIES_MULTILINE = {
            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER,
            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE,
            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE,
            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PARAGRAPH,
            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD };
    protected static final int SYSTEM_ACTION_MAX = 0x01FFFFFF;

    protected final Context mContext;

    /* Specifies whether to automatically click on clickable views */
    private boolean mAutoSelect;

    public TreeBuilder(Context context) {
        mContext = context;
        SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(mContext);
        updatePrefs(prefs);
        prefs.registerOnSharedPreferenceChangeListener(this);
    }

    /**
     * Clean up when this object is no longer needed
     */
    public void shutdown() {
        SharedPreferences prefs = SharedPreferencesUtils.getSharedPreferences(mContext);
        prefs.unregisterOnSharedPreferenceChangeListener(this);
    }

    /**
     * Add a view hierarchy to the top of a tree
     * @param node The root of the view hierarchy to be added to the tree
     * @param treeToBuildOn The tree that should be traversed if the user doesn't choose anything
     *                      from the view hierarchy
     * @return An updated tree that includes {@code treeToBuildOn}
     */
    abstract public OptionScanNode addViewHierarchyToTree(SwitchAccessNodeCompat node,
            OptionScanNode treeToBuildOn);

    /**
     * Add view hierarchies from several windows to the top of a tree
     * @param windowList The windows whose hierarchies should be added to the tree
     * @param treeToBuildOn The tree that should be traversed if the user doesn't choose anything
     *                      from the view hierarchy
     * @return An updated tree that includes {@code treeToBuildOn}
     */
    abstract public OptionScanNode addWindowListToTree(List<SwitchAccessWindowInfo> windowList,
            OptionScanNode treeToBuildOn);

    /**
     * Build a context menu out of a list of items
     * @param actionList The items that should be in the context menu
     *
     * @return A context menu tree with the specified actions
     */
    abstract public OptionScanNode buildContextMenu(List<? extends ContextMenuItem> actionList);

    @Override
    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
        updatePrefs(prefs);
    }

    private void updatePrefs(SharedPreferences prefs) {
        mAutoSelect = SwitchAccessPreferenceActivity.isGlobalMenuAutoselectOn(mContext);
    }

    protected static boolean isActionSupported(AccessibilityNodeInfoCompat.AccessibilityActionCompat action) {
        /* White-listed framework actions */
        if (action.getId() <= SYSTEM_ACTION_MAX) {
            return FRAMEWORK_ACTIONS.contains(action.getId());
        }
        /* Support custom actions with proper labels */
        return !TextUtils.isEmpty(action.getLabel());
    }

    /**
     * Obtain a list of nodes in the order TalkBack would traverse them
     *
     * @param root The root of the tree to traverse
     * @return The nodes in {@code root}'s subtree (including root) in the order TalkBack would
     * traverse them.
     */
    protected static LinkedList<SwitchAccessNodeCompat> getNodesInTalkBackOrder(SwitchAccessNodeCompat root) {
        LinkedList<SwitchAccessNodeCompat> outList = new LinkedList<>();
        OrderedTraversalController traversalController = new OrderedTraversalController();
        traversalController.initOrder(root, true);
        AccessibilityNodeInfoCompat node = traversalController.findFirst();
        while (node != null) {
            outList.add(new SwitchAccessNodeCompat(node.getInfo(), root.getWindowsAbove()));
            node = traversalController.findNext(node);
        }
        traversalController.recycle();
        return outList;
    }

    /**
     * Get the actions associated with the given compat node.
     *
     * @param compat The node whose actions should be obtained.
     * @return A list of {@code ContextMenuItems}, representing all the actions associated with the
     * specified node. They may be organized into sub-menus if the actions require arguments.
     * If no actions are associated with the node, an empty list is returned.
     */
    protected List<AccessibilityNodeActionNode> getCompatActionNodes(SwitchAccessNodeCompat compat) {
        if (!compat.isVisibleToUser() || compat.getHasSameBoundsAsAncestor()) {
            return new ArrayList<>(0);
        }
        List<AccessibilityNodeActionNode> actionNodes = getCompatActionNodesInternal(compat);
        if (!actionNodes.isEmpty()) {
            List<SwitchAccessNodeCompat> descendantsWithSameBounds = compat.getDescendantsWithDuplicateBounds();
            List<AccessibilityNodeActionNode> actionsFromDescendants = new ArrayList<>();

            int duplicateBoundsDisambiguationNumber = 2;
            for (int i = 0; i < descendantsWithSameBounds.size(); i++) {
                SwitchAccessNodeCompat descendantWithSameBounds = descendantsWithSameBounds.get(i);
                List<AccessibilityNodeActionNode> descendantActions = getCompatActionNodesInternal(
                        descendantWithSameBounds);
                // Append the child actions to the parent's list
                if (!descendantActions.isEmpty()) {
                    for (int j = 0; j < descendantActions.size(); j++) {
                        descendantActions.get(j)
                                .setNumberToAppendToDuplicateAction(duplicateBoundsDisambiguationNumber);
                    }
                    actionsFromDescendants.addAll(descendantActions);
                    duplicateBoundsDisambiguationNumber++;
                }
                descendantWithSameBounds.recycle();
            }

            if (!actionsFromDescendants.isEmpty()) {
                // Add a disambiguation number to this node's actions
                for (int i = 0; i < actionNodes.size(); i++) {
                    actionNodes.get(i).setNumberToAppendToDuplicateAction(1);
                }
                actionNodes.addAll(actionsFromDescendants);
            }
        }
        return actionNodes;
    }

    private List<AccessibilityNodeActionNode> getCompatActionNodesInternal(SwitchAccessNodeCompat compat) {
        List<AccessibilityNodeActionNode> actionNodes = new ArrayList<>();
        List<AccessibilityNodeInfoCompat.AccessibilityActionCompat> actions = compat.getActionList();
        for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action : actions) {
            if (mAutoSelect && (action.getId() == AccessibilityNodeInfoCompat.ACTION_CLICK)) {
                actionNodes.clear();
                actionNodes.add(new AccessibilityNodeActionNode(compat, action));
                return actionNodes;
            }
            if (isActionSupported(action)) {
                if ((action.getId() == AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY) || (action
                        .getId() == AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY)) {
                    /*
                     * These actions can populate a long context menu, and all Views with
                     * content descriptions support them. We therefore try to filter out what
                     * we should surface to provide the user with exactly the set of actions that
                     * are relevant to the view.
                     */
                    boolean canMoveInDirection = !TextUtils.isEmpty(compat.getText());
                    if (canMoveInDirection && (compat.getTextSelectionStart() == compat.getTextSelectionEnd())) {
                        // Nothing is selected
                        int cursorPosition = compat.getTextSelectionStart();
                        boolean forward = (action
                                .getId() == AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
                        canMoveInDirection &= !(forward && cursorPosition == compat.getText().length());
                        canMoveInDirection &= !(!forward && cursorPosition == 0);
                        canMoveInDirection &= cursorPosition >= 0;
                    }
                    if (compat.isEditable() && canMoveInDirection) {
                        int movementGranularities = compat.getMovementGranularities();
                        int[] supportedGranularities = (compat.isMultiLine() ? MOVEMENT_GRANULARITIES_MULTILINE
                                : MOVEMENT_GRANULARITIES_ONE_LINE);
                        for (int granularity : supportedGranularities) {
                            if ((movementGranularities & granularity) != 0) {
                                Bundle args = new Bundle();
                                args.putInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
                                        granularity);
                                AccessibilityNodeActionNode node = new AccessibilityNodeActionNode(compat, action,
                                        args);
                                actionNodes.add(node);
                            }
                        }
                    }
                } else {
                    actionNodes.add(new AccessibilityNodeActionNode(compat, action));
                }
            }
        }
        return actionNodes;
    }
}