com.android.talkback.speechrules.NodeSpeechRuleProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.android.talkback.speechrules.NodeSpeechRuleProcessor.java

Source

/*
 * Copyright (C) 2011 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.talkback.speechrules;

import android.annotation.TargetApi;
import android.os.Build;
import android.text.Spannable;
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.talkback.R;

import android.content.Context;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.LogUtils;
import com.android.utils.Role;
import com.android.utils.StringBuilderUtils;
import com.android.utils.traversal.ReorderedChildrenIterator;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;

/**
 * Rule-based processor for {@link AccessibilityNodeInfoCompat}s.
 */
public class NodeSpeechRuleProcessor {

    private static final LinkedList<NodeSpeechRule> mRules = new LinkedList<>();
    private static final RuleSwitch mRuleSwitch = new RuleSwitch();

    private static NodeSpeechRuleProcessor sInstance;

    static {
        // Rules are matched in the order they are added, so make sure to place
        // general rules after specific ones (e.g. Button after RadioButton).
        mRules.add(new RuleSimpleHintTemplate(Role.ROLE_DROP_DOWN_LIST, R.string.template_hint_spinner,
                R.string.template_hint_spinner_keyboard));
        mRules.add(mRuleSwitch);
        mRules.add(new RuleNonTextViews()); // ImageViews and ImageButtons
        mRules.add(new RuleEditText());
        mRules.add(new RuleSeekBar());
        mRules.add(new RulePager());
        mRules.add(new RulePagerPage());
        mRules.add(new RuleContainer());

        mRules.add(new RuleViewGroup());

        // Always add the default rule last.
        mRules.add(new RuleDefault());
    }

    // TODO: remove this
    public static void initialize(Context context) {
        sInstance = new NodeSpeechRuleProcessor(context);
    }

    // TODO: remove this
    public static NodeSpeechRuleProcessor getInstance() {
        if (sInstance == null) {
            throw new RuntimeException("NodeSpeechRuleProcessor not initialized");
        }

        return sInstance;
    }

    /** The parent context. */
    private final Context mContext;

    private NodeSpeechRuleProcessor(Context context) {
        mContext = context;
    }

    /**
     * Returns the best description for the subtree rooted at
     * {@code announcedNode}.
     *
     * @param announcedNode The root node of the subtree to describe.
     * @param event The source event, may be {@code null} when called with
     *            non-source nodes.
     * @param source The event's source node.
     * @return The best description for a node.
     */
    public CharSequence getDescriptionForTree(AccessibilityNodeInfoCompat announcedNode, AccessibilityEvent event,
            AccessibilityNodeInfoCompat source) {
        if (announcedNode == null) {
            return null;
        }

        final SpannableStringBuilder builder = new SpannableStringBuilder();

        Set<AccessibilityNodeInfoCompat> visitedNodes = new HashSet<>();
        appendDescriptionForTree(announcedNode, builder, event, source, visitedNodes);
        AccessibilityNodeInfoUtils.recycleNodes(visitedNodes);
        formatTextWithLabel(announcedNode, builder);
        appendRootMetadataToBuilder(announcedNode, builder);

        return builder;
    }

    /**
     * Returns hint text for a node.
     *
     * @param node The node to provide hint text for.
     * @return The node's hint text.
     */
    public CharSequence getHintForNode(AccessibilityNodeInfoCompat node) {
        // Disabled items don't have any hint text.
        if (!node.isEnabled()) {
            return null;
        }

        for (NodeSpeechRule rule : mRules) {
            if ((rule instanceof NodeHintRule) && rule.accept(node, null)) {
                LogUtils.log(this, Log.VERBOSE, "Processing node hint using %s", rule);
                return ((NodeHintRule) rule).getHintText(mContext, node);
            }
        }

        return null;
    }

    private void appendDescriptionForTree(AccessibilityNodeInfoCompat announcedNode, SpannableStringBuilder builder,
            AccessibilityEvent event, AccessibilityNodeInfoCompat source,
            Set<AccessibilityNodeInfoCompat> visitedNodes) {
        if (announcedNode == null) {
            return;
        }

        AccessibilityNodeInfoCompat visitedNode = AccessibilityNodeInfoCompat.obtain(announcedNode);
        if (!visitedNodes.add(visitedNode)) {
            visitedNode.recycle();
            return;
        }

        final AccessibilityEvent nodeEvent = (announcedNode.equals(source)) ? event : null;
        final CharSequence nodeDesc = getDescriptionForNode(announcedNode, nodeEvent);
        final boolean blockChildDescription = hasOverridingContentDescription(announcedNode);

        SpannableStringBuilder childStringBuilder = new SpannableStringBuilder();

        if (!blockChildDescription) {
            // Recursively append descriptions for visible and non-focusable child nodes.
            ReorderedChildrenIterator iterator = ReorderedChildrenIterator.createAscendingIterator(announcedNode);
            while (iterator.hasNext()) {
                AccessibilityNodeInfoCompat child = iterator.next();
                if (AccessibilityNodeInfoUtils.isVisible(child)
                        && !AccessibilityNodeInfoUtils.isAccessibilityFocusable(child)) {
                    appendDescriptionForTree(child, childStringBuilder, event, source, visitedNodes);
                }
            }

            iterator.recycle();
        }

        // If any one of the following is satisfied:
        // 1. The root node has a description.
        // 2. The root has no override content description and the children have some description.
        // Then we should append the status information for this node.
        // This is used to avoid displaying checked/expanded status alone without node description.
        //
        if (!TextUtils.isEmpty(nodeDesc) || !TextUtils.isEmpty(childStringBuilder)) {
            appendExpandedOrCollapsedStatus(announcedNode, event, builder);
            appendCheckedStatus(announcedNode, event, builder);
        }

        StringBuilderUtils.appendWithSeparator(builder, nodeDesc);
        StringBuilderUtils.appendWithSeparator(builder, childStringBuilder);
    }

    /**
     * Determines whether the node has a contentDescription that should cause its subtree's
     * description to be ignored.
     * The traditional behavior in TalkBack has been to return {@code true} for any node with a
     * non-empty contentDescription. In this function, we thus whitelist certain roles where it
     * doesn't make sense for the contentDescription to override the entire subtree.
     */
    private static boolean hasOverridingContentDescription(AccessibilityNodeInfoCompat node) {
        switch (Role.getRole(node)) {
        case Role.ROLE_PAGER:
        case Role.ROLE_GRID:
        case Role.ROLE_LIST:
            return false;
        default:
            return node != null && !TextUtils.isEmpty(node.getContentDescription());
        }
    }

    /**
     * Processes the specified node using a series of speech rules.
     *
     * @param node The node to process.
     * @param event The source event, may be {@code null} when called with
     *            non-source nodes.
     * @return A string representing the given node, or {@code null} if the node
     *         could not be processed.
     */
    public CharSequence getDescriptionForNode(AccessibilityNodeInfoCompat node, AccessibilityEvent event) {
        for (NodeSpeechRule rule : mRules) {
            if (rule.accept(node, event)) {
                LogUtils.log(this, Log.VERBOSE, "Processing node using %s", rule);
                return rule.format(mContext, node, event);
            }
        }

        return null;
    }

    /**
     * If the supplied node has a label, replaces the builder text with a
     * version formatted with the label.
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    private void formatTextWithLabel(AccessibilityNodeInfoCompat node, SpannableStringBuilder builder) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)
            return;

        // TODO: add getLabeledBy to support lib
        AccessibilityNodeInfo info = (AccessibilityNodeInfo) node.getInfo();
        if (info == null)
            return;
        AccessibilityNodeInfo labeledBy = info.getLabeledBy();
        if (labeledBy == null)
            return;
        AccessibilityNodeInfoCompat labelNode = new AccessibilityNodeInfoCompat(labeledBy);

        final SpannableStringBuilder labelDescription = new SpannableStringBuilder();
        Set<AccessibilityNodeInfoCompat> visitedNodes = new HashSet<>();
        appendDescriptionForTree(labelNode, labelDescription, null, null, visitedNodes);
        AccessibilityNodeInfoUtils.recycleNodes(visitedNodes);
        if (TextUtils.isEmpty(labelDescription)) {
            return;
        }

        final String labeled = mContext.getString(R.string.template_labeled_item, builder, labelDescription);
        Spannable spannableLabeledText = StringBuilderUtils.createSpannableFromTextWithTemplate(labeled, builder);

        // Replace the text of the builder.
        builder.clear();
        builder.append(spannableLabeledText);
    }

    /**
     * Appends meta-data about node's disabled state (if actionable).
     * <p>
     * This should only be applied to the root node of a tree.
     */
    private void appendRootMetadataToBuilder(AccessibilityNodeInfoCompat node,
            SpannableStringBuilder descriptionBuilder) {
        // Append state for actionable but disabled nodes.
        if (AccessibilityNodeInfoUtils.isActionableForAccessibility(node) && !node.isEnabled()) {
            StringBuilderUtils.appendWithSeparator(descriptionBuilder, mContext.getString(R.string.value_disabled));
        }

        // Append the control's selected state.
        if (node.isSelected()) {
            StringBuilderUtils.appendWithSeparator(descriptionBuilder, mContext.getString(R.string.value_selected));
        }
    }

    /**
     * Appends meta-data about the node's checked (if checkable) states.
     * <p>
     * This should be applied to all nodes in a tree, including the root.
     */
    private void appendCheckedStatus(AccessibilityNodeInfoCompat node, AccessibilityEvent event,
            SpannableStringBuilder descriptionBuilder) {
        // Append the control's checked state, if applicable. Ignore nodes that
        // were accepted by the switch rule, since they already include the
        // checkable state.
        if (node.isCheckable() && !mRuleSwitch.accept(node, event)) {
            CharSequence checkedString = mContext
                    .getString(node.isChecked() ? R.string.value_checked : R.string.value_not_checked);
            StringBuilderUtils.appendWithSeparator(descriptionBuilder, checkedString);
        }
    }

    /**
     * Appends meta-data about the node's expandable/collapsible states.
     * <p>
     * This should be applied to all nodes in a tree, including the root.
     */
    private void appendExpandedOrCollapsedStatus(AccessibilityNodeInfoCompat node, AccessibilityEvent event,
            SpannableStringBuilder descriptionBuilder) {
        // Append the control's expandable/collapsible state, if applicable.
        if (AccessibilityNodeInfoUtils.isExpandable(node)) {
            CharSequence collapsedString = mContext.getString(R.string.value_collapsed);
            StringBuilderUtils.appendWithSeparator(descriptionBuilder, collapsedString);
        }
        if (AccessibilityNodeInfoUtils.isCollapsible(node)) {
            CharSequence expandedString = mContext.getString(R.string.value_expanded);
            StringBuilderUtils.appendWithSeparator(descriptionBuilder, expandedString);
        }
    }
}