com.googlecode.eyesfree.brailleback.DefaultNavigationMode.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.eyesfree.brailleback.DefaultNavigationMode.java

Source

/*
 * Copyright (C) 2012 Google Inc.
 *
 * 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.googlecode.eyesfree.brailleback;

import com.googlecode.eyesfree.braille.display.BrailleInputEvent;
import com.googlecode.eyesfree.brailleback.rule.BrailleRule;
import com.googlecode.eyesfree.brailleback.rule.BrailleRuleRepository;
import com.googlecode.eyesfree.brailleback.utils.AccessibilityEventUtils;
import com.googlecode.eyesfree.compat.accessibilityservice.AccessibilityServiceCompatUtils;
import com.googlecode.eyesfree.utils.AccessibilityNodeInfoRef;
import com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils;
import com.googlecode.eyesfree.utils.LogUtils;
import com.googlecode.eyesfree.utils.WebInterfaceUtils;

import android.accessibilityservice.AccessibilityService;
import android.os.Bundle;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.EditText;

/**
 * Navigation mode that is based on traversing the node tree using
 * accessibility focus.
 */
// TODO: Consolidate parts of this class with similar code in TalkBack.
class DefaultNavigationMode implements NavigationMode {
    private static final int DIRECTION_BACKWARD = WebInterfaceUtils.DIRECTION_BACKWARD;
    private static final int DIRECTION_FORWARD = WebInterfaceUtils.DIRECTION_FORWARD;

    private static final String HTML_ELEMENT_MOVE_BY_SECTION = WebInterfaceUtils.HTML_ELEMENT_MOVE_BY_SECTION;
    private static final String HTML_ELEMENT_MOVE_BY_CONTROL = WebInterfaceUtils.HTML_ELEMENT_MOVE_BY_CONTROL;
    private static final String HTML_ELEMENT_MOVE_BY_LIST = WebInterfaceUtils.HTML_ELEMENT_MOVE_BY_LIST;

    private static final int ACTION_TOGGLE_INCREMENTAL_SEARCH = WebInterfaceUtils.ACTION_TOGGLE_INCREMENTAL_SEARCH;

    // Actions in this range are reserved for Braille use.
    // TODO: use event arguments instead of this hack.
    private static final int ACTION_BRAILLE_CLICK_MAX = -275000000;
    private static final int ACTION_BRAILLE_CLICK_MIN = -275999999;

    private final DisplayManager mDisplayManager;
    private final AccessibilityService mAccessibilityService;
    private final SelfBrailleManager mSelfBrailleManager;
    private final NodeBrailler mNodeBrailler;
    private final FeedbackManager mFeedbackManager;
    private final FocusFinder mFocusFinder;
    private final BrailleRuleRepository mRuleRepository;
    private final Bundle mArgumentBundle = new Bundle();

    private boolean mActive = false;
    private AccessibilityNodeInfoRef mLastFocusedNode = new AccessibilityNodeInfoRef();

    public DefaultNavigationMode(DisplayManager displayManager, AccessibilityService accessibilityService,
            FeedbackManager feedbackManager, SelfBrailleManager selfBrailleManager, NodeBrailler nodeBrailler,
            BrailleRuleRepository ruleRepository) {
        mDisplayManager = displayManager;
        mAccessibilityService = accessibilityService;
        mSelfBrailleManager = selfBrailleManager;
        mRuleRepository = ruleRepository;
        mNodeBrailler = nodeBrailler;
        mFeedbackManager = feedbackManager;
        mFocusFinder = new FocusFinder(mAccessibilityService);
    }

    @Override
    public boolean onPanLeftOverflow(DisplayManager.Content content) {
        return mFeedbackManager.emitOnFailure(onPanLeftOverflowInternal(content),
                FeedbackManager.TYPE_NAVIGATE_OUT_OF_BOUNDS);
    }

    private boolean onPanLeftOverflowInternal(DisplayManager.Content content) {
        AccessibilityNodeInfoCompat currentNode = null;
        AccessibilityNodeInfoCompat firstNode = null;
        try {
            // If the currently focused node is a web view, we attempt
            // to delegate navigation to the web view first.
            currentNode = getFocusedNode(true);
            if (currentNode != null && WebInterfaceUtils.hasWebContent(currentNode)
                    && WebInterfaceUtils.performNavigationAtGranularityAction(currentNode, DIRECTION_BACKWARD,
                            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE)) {
                return true;
            }

            // Check if we need to scroll.
            if (autoScrollItem(currentNode, AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)) {
                return true;
            }

            firstNode = AccessibilityNodeInfoUtils.refreshNode(content.getFirstNode());
            // If the content doesn't have a first node, fall back on the
            // currently focused node.
            if (firstNode == null || !firstNode.isVisibleToUser()) {
                AccessibilityNodeInfoUtils.recycleNodes(firstNode);
                firstNode = currentNode;
                currentNode = null;
            }
            if (firstNode == null) {
                return false;
            }
            AccessibilityNodeInfoCompat target = mFocusFinder.linear(firstNode, FocusFinder.SEARCH_BACKWARD);
            return moveFocus(firstNode, DIRECTION_BACKWARD);
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(currentNode, firstNode);
        }
    }

    @Override
    public boolean onPanRightOverflow(DisplayManager.Content content) {
        return mFeedbackManager.emitOnFailure(onPanRightOverflowInternal(content),
                FeedbackManager.TYPE_NAVIGATE_OUT_OF_BOUNDS);
    }

    private boolean onPanRightOverflowInternal(DisplayManager.Content content) {
        AccessibilityNodeInfoCompat currentNode = getFocusedNode(true);
        try {
            if (currentNode != null && WebInterfaceUtils.hasWebContent(currentNode)
                    && WebInterfaceUtils.performNavigationAtGranularityAction(currentNode, DIRECTION_FORWARD,
                            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE)) {
                return true;
            }

            // Check if we need to scroll.
            if (autoScrollItem(currentNode, AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
                return true;
            }
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(currentNode);
        }
        AccessibilityNodeInfoRef target = findNodeForPanRight(content);
        if (AccessibilityNodeInfoRef.isNull(target)) {
            return false;
        }
        try {
            return target.get().performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
        } finally {
            target.recycle();
        }
    }

    /**
     * Find the first node to focus when panning right, taking into account
     * that some nodes have their children included on the braille display,
     * while some (such as containers) don't.
     */
    private AccessibilityNodeInfoRef findNodeForPanRight(DisplayManager.Content content) {
        AccessibilityNodeInfoRef current = AccessibilityNodeInfoRef.refreshed(content.getFirstNode());
        if (current == null) {
            current = new AccessibilityNodeInfoRef();
        }
        // Simplify the algorithm by finding the node to stop our traversal
        // at, which is null if the display is at the end of the tree
        // (in pre-order traversal order).
        AccessibilityNodeInfoRef sentinel = AccessibilityNodeInfoRef.refreshed(content.getLastNode());
        try {
            if (AccessibilityNodeInfoRef.isNull(current) || !current.get().isVisibleToUser()
                    || AccessibilityNodeInfoRef.isNull(sentinel)) {
                current.reset(linearSearch(FocusFinder.SEARCH_FORWARD));
                return current;
            }
            nextUpwardsOrClear(sentinel);
            while (!AccessibilityNodeInfoRef.isNull(current) && !current.get().equals(sentinel.get())) {
                BrailleRule rule = mRuleRepository.find(current.get());
                // If children of this node are not included on the display,
                // look for focusable descendants.
                if (!rule.includeChildren(current.get(), mAccessibilityService)) {
                    AccessibilityNodeInfoCompat focusableDescendant = FocusFinder
                            .findFirstFocusableDescendant(current.get(), mAccessibilityService);
                    if (focusableDescendant != null) {
                        current.reset(focusableDescendant);
                        return current;
                    }
                    nextUpwardsOrClear(current);
                } else {
                    if (!current.nextInOrder()) {
                        current.clear();
                    }
                }
            }
            if (AccessibilityNodeInfoRef.isNull(current)) {
                if (!AccessibilityNodeInfoRef.isNull(sentinel)) {
                    // We missed the sentinel, which would happen if there is
                    // some node tree change under our feet.  Fall back on a
                    // normal linear forward search so the user doesn't get
                    // stuck.
                    current.reset(linearSearch(FocusFinder.SEARCH_FORWARD));
                } else {
                    // Reached the end of the tree.
                }
            } else {
                // We reached the sentinel, now focus that node or one that we
                // find by linear forward search.
                if (!AccessibilityNodeInfoUtils.shouldFocusNode(mAccessibilityService, current.get())) {
                    current.reset(mFocusFinder.linear(current.get(), FocusFinder.SEARCH_FORWARD));
                }
            }
            return current;
        } finally {
            if (sentinel != null) {
                sentinel.recycle();
            }
        }
    }

    /**
     * Moves {@code node} to the next sibling, or, if that doesn't succeed,
     * to the next sibling of the parent, recursively, until a next
     * node is found or the root is reached.  In the latter case, {@code node}
     * is cleared.
     */
    private void nextUpwardsOrClear(AccessibilityNodeInfoRef node) {
        if (node.nextSibling()) {
            return;
        }
        while (node.parent()) {
            if (node.nextSibling()) {
                return;
            }
        }
        node.clear();
    }

    private boolean linePrevious(DisplayManager.Content content) {
        return mFeedbackManager.emitOnFailure(linePreviousInternal(content),
                FeedbackManager.TYPE_NAVIGATE_OUT_OF_BOUNDS);
    }

    /**
     * Moves accessibility focus to the first focusable node of the previous
     * 'line'.
     */
    private boolean linePreviousInternal(DisplayManager.Content content) {
        AccessibilityNodeInfoRef left = new AccessibilityNodeInfoRef();
        AccessibilityNodeInfoRef right = new AccessibilityNodeInfoRef();
        AccessibilityNodeInfoCompat target = null;
        AccessibilityNodeInfoCompat currentNode = getFocusedNode(true);
        try {
            if (currentNode != null && WebInterfaceUtils.hasWebContent(currentNode)
                    && WebInterfaceUtils.performNavigationAtGranularityAction(currentNode, DIRECTION_BACKWARD,
                            AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE)) {
                return true;
            }

            // Check if we need to scroll.
            if (autoScrollItem(currentNode, AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)) {
                return true;
            }

            AccessibilityNodeInfoRef firstNode = AccessibilityNodeInfoRef.unOwned(content.getFirstNode());
            // If the content doesn't have a first node, fall back on the
            // currently focused node.
            if (AccessibilityNodeInfoRef.isNull(firstNode)) {
                firstNode = AccessibilityNodeInfoRef.owned(currentNode);
                currentNode = null;
            }
            if (AccessibilityNodeInfoRef.isNull(firstNode)) {
                return false;
            }
            // Move backwards one step from the first node that is currently
            // displayed.
            target = mFocusFinder.linear(firstNode.get(), FocusFinder.SEARCH_BACKWARD);
            firstNode.recycle();
            if (target == null) {
                return false;
            }
            // Find what would be covered by the display if target
            // would have accessibility focus.
            mNodeBrailler.findDisplayExtentFromNode(target, left, right);
            // Find the first focusable nodes moving forward from left,
            // i fleft is not focusable itself.
            if (!AccessibilityNodeInfoUtils.shouldFocusNode(mAccessibilityService, left.get())) {
                left.reset(mFocusFinder.linear(left.get(), FocusFinder.SEARCH_FORWARD));
            }
            // If we didn't find a focusable node at the beginning of the
            // line we are trying to move to, just move to target as
            // a fallback.
            if (AccessibilityNodeInfoRef.isNull(left)) {
                left.reset(target);
                target = null;
            }
            return left.get().performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(target, currentNode);
            left.recycle();
            right.recycle();
        }
    }

    private boolean lineNext(DisplayManager.Content content) {
        return onPanRightOverflow(content);
    }

    private boolean itemPrevious() {
        return mFeedbackManager.emitOnFailure(navigateItem(DIRECTION_BACKWARD),
                FeedbackManager.TYPE_NAVIGATE_OUT_OF_BOUNDS);
    }

    private boolean navigateItem(int direction) {
        AccessibilityNodeInfoCompat currentNode = getFocusedNode(true);
        try {
            if (currentNode != null && WebInterfaceUtils.hasWebContent(currentNode)
                    && WebInterfaceUtils.performNavigationByDOMObject(currentNode, direction)) {
                return true;
            }
            // Check if we need to scroll.
            int scrollDirection = (direction == DIRECTION_FORWARD) ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
                    : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
            if (autoScrollItem(currentNode, scrollDirection)) {
                return true;
            }
            return moveFocus(currentNode, direction);
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(currentNode);
        }
    }

    private boolean itemNext() {
        return mFeedbackManager.emitOnFailure(navigateItem(DIRECTION_FORWARD),
                FeedbackManager.TYPE_NAVIGATE_OUT_OF_BOUNDS);
    }

    private boolean navigateHTMLElementAction(int direction, String element) {
        AccessibilityNodeInfoCompat currentNode = getFocusedNode(true);
        try {
            if (currentNode != null && WebInterfaceUtils.hasWebContent(currentNode)
                    && WebInterfaceUtils.performNavigationToHtmlElementAction(currentNode, direction, element)) {
                return true;
            }
            return false;
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(currentNode);
        }
    }

    private boolean handleIncrementalSearchAction() {
        AccessibilityNodeInfoCompat currentNode = getFocusedNode(true);
        try {
            if (currentNode != null && WebInterfaceUtils.hasWebContent(currentNode)
                    && mFeedbackManager.emitOnFailure(
                            WebInterfaceUtils.performSpecialAction(currentNode, ACTION_TOGGLE_INCREMENTAL_SEARCH),
                            FeedbackManager.TYPE_COMMAND_FAILED)) {
                return true;
            }
            return false;
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(currentNode);
        }
    }

    @Override
    public boolean onMappedInputEvent(BrailleInputEvent event, DisplayManager.Content content) {
        switch (event.getCommand()) {
        case BrailleInputEvent.CMD_NAV_ITEM_PREVIOUS:
            return itemPrevious();
        case BrailleInputEvent.CMD_NAV_ITEM_NEXT:
            return itemNext();
        case BrailleInputEvent.CMD_NAV_LINE_PREVIOUS:
            return linePrevious(content);
        case BrailleInputEvent.CMD_NAV_LINE_NEXT:
            return lineNext(content);
        case BrailleInputEvent.CMD_ACTIVATE_CURRENT:
            // Activate the current node, but don't fall back on the
            // root if focus is cleared.
            return mFeedbackManager.emitOnFailure(activateNode(getFocusedNode(false)),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_LONG_PRESS_CURRENT:
            // Long click the current node, but don't fall back on the
            // root if focus is cleared.
            return mFeedbackManager.emitOnFailure(longClickNode(getFocusedNode(false)),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_ROUTE: {
            AccessibilityNodeInfoCompat node = DisplaySpans.getAccessibilityNodeFromPosition(event.getArgument(),
                    content.getText());
            return mFeedbackManager.emitOnFailure(activateNode(node, event.getArgument()),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        }
        case BrailleInputEvent.CMD_LONG_PRESS_ROUTE: {
            AccessibilityNodeInfoCompat node = DisplaySpans.getAccessibilityNodeFromPosition(event.getArgument(),
                    content.getText());
            return mFeedbackManager.emitOnFailure(longClickNode(node), FeedbackManager.TYPE_COMMAND_FAILED);
        }
        case BrailleInputEvent.CMD_SCROLL_FORWARD:
            return mFeedbackManager.emitOnFailure(attemptScrollAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_SCROLL_BACKWARD:
            return mFeedbackManager.emitOnFailure(attemptScrollAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_NAV_TOP:
            return mFeedbackManager.emitOnFailure(attemptNavigateTop(), FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_NAV_BOTTOM:
            return mFeedbackManager.emitOnFailure(attemptNavigateBottom(), FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_SECTION_NEXT:
            return mFeedbackManager.emitOnFailure(
                    navigateHTMLElementAction(DIRECTION_FORWARD, HTML_ELEMENT_MOVE_BY_SECTION),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_SECTION_PREVIOUS:
            return mFeedbackManager.emitOnFailure(
                    navigateHTMLElementAction(DIRECTION_BACKWARD, HTML_ELEMENT_MOVE_BY_SECTION),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_CONTROL_NEXT:
            return mFeedbackManager.emitOnFailure(
                    navigateHTMLElementAction(DIRECTION_FORWARD, HTML_ELEMENT_MOVE_BY_CONTROL),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_CONTROL_PREVIOUS:
            return mFeedbackManager.emitOnFailure(
                    navigateHTMLElementAction(DIRECTION_BACKWARD, HTML_ELEMENT_MOVE_BY_CONTROL),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_LIST_NEXT:
            return mFeedbackManager.emitOnFailure(
                    navigateHTMLElementAction(DIRECTION_FORWARD, HTML_ELEMENT_MOVE_BY_LIST),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_LIST_PREVIOUS:
            return mFeedbackManager.emitOnFailure(
                    navigateHTMLElementAction(DIRECTION_BACKWARD, HTML_ELEMENT_MOVE_BY_LIST),
                    FeedbackManager.TYPE_COMMAND_FAILED);
        case BrailleInputEvent.CMD_TOGGLE_INCREMENTAL_SEARCH:
            return handleIncrementalSearchAction();
        }
        return false;
    }

    @Override
    public void onActivate() {
        mActive = true;
        mLastFocusedNode.clear();
        // Braille the focused node, or if that fails, braille
        // the first focusable node.
        if (!brailleFocusedNode()) {
            brailleFirstFocusableNode();
        }
    }

    @Override
    public void onDeactivate() {
        mActive = false;
    }

    @Override
    public void onObserveAccessibilityEvent(AccessibilityEvent event) {
        // Nothing to do.
    }

    @Override
    public boolean onAccessibilityEvent(AccessibilityEvent event) {
        switch (event.getEventType()) {
        case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
            brailleNodeFromEvent(event);
            break;
        case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
            brailleFocusedNode();
            break;
        case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
            if (!brailleFocusedNode()) {
                // Since focus is typically not set in a newly opened
                // window, so braille the window as-if the first focusable
                // node had focus.  We don't update the focus because that
                // will make other services (e.g. talkback) reflect this
                // change, which is not desired.
                brailleFirstFocusableNode();
            }
            break;
        }
        return true;
    }

    @Override
    public void onInvalidateAccessibilityNode(AccessibilityNodeInfoCompat node) {
        brailleFocusedNode();
    }

    private boolean moveFocus(AccessibilityNodeInfoCompat from, int direction) {
        int searchDirection = (direction == DIRECTION_BACKWARD) ? FocusFinder.SEARCH_BACKWARD
                : FocusFinder.SEARCH_FORWARD;
        AccessibilityNodeInfoCompat next = null;
        next = mFocusFinder.linear(from, searchDirection);
        try {
            if (next != null) {
                return next.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
            }
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(next);
        }
        return false;
    }

    private AccessibilityNodeInfoCompat getFocusedNode(boolean fallbackOnRoot) {
        return FocusFinder.getFocusedNode(mAccessibilityService, fallbackOnRoot);
    }

    private boolean activateNode(AccessibilityNodeInfoCompat node) {
        return activateNode(node, -1);
    }

    private boolean activateNode(AccessibilityNodeInfoCompat node, int position) {
        if (node == null) {
            return false;
        }
        AccessibilityNodeInfoRef current = AccessibilityNodeInfoRef.unOwned(node);
        try {
            do {
                LogUtils.log(this, Log.VERBOSE, "Considering to click: %s", current.get().getInfo());
                int supportedActions = current.get().getActions();
                int action = 0;
                // For edit texts, the click action doesn't currently focus
                // the view, so we special case it here.
                // TODO: Revise when that changes.
                if (AccessibilityNodeInfoUtils.nodeMatchesClassByType(mAccessibilityService, current.get(),
                        EditText.class)) {
                    if ((supportedActions & AccessibilityNodeInfo.ACTION_FOCUS) != 0) {
                        action = AccessibilityNodeInfo.ACTION_FOCUS;
                    } else {
                        // Put accessibility focus on the field.  If it is
                        // already focused and the IME is selected, that will
                        // activate the editing.
                        action = AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
                    }
                } else if (position >= 0 && isSelfBrailled(current.get())
                        && (supportedActions & AccessibilityNodeInfo.ACTION_CLICK) != 0) {
                    // Generate a fake "action". For instance, a click at
                    // position 33 maps to -275000033.
                    // TODO: Remove this hack when a better way to pass this
                    // information exists.
                    int fakeAction = ACTION_BRAILLE_CLICK_MAX - position;
                    if (fakeAction < ACTION_BRAILLE_CLICK_MIN) {
                        LogUtils.log(this, Log.WARN, "underflow activating node %s at position %d", current.get(),
                                position);
                        fakeAction = ACTION_BRAILLE_CLICK_MIN;
                    } else if (fakeAction > ACTION_BRAILLE_CLICK_MAX) {
                        LogUtils.log(this, Log.WARN, "overflow activating node %s at position %d", current.get(),
                                position);
                        fakeAction = ACTION_BRAILLE_CLICK_MAX;
                    }
                    if (WebInterfaceUtils.performSpecialAction(current.get(), fakeAction)) {
                        return true;
                    }
                } else if ((supportedActions & AccessibilityNodeInfo.ACTION_CLICK) != 0) {
                    action = AccessibilityNodeInfo.ACTION_CLICK;
                }
                if (action != 0 && current.get().performAction(action)) {
                    return true;
                } else {
                    LogUtils.log(this, Log.VERBOSE, "Action %d failed", action);
                }
            } while (current.parent());
        } finally {
            current.recycle();
        }
        LogUtils.log(this, Log.VERBOSE, "Click action failed");
        return false;
    }

    private boolean longClickNode(AccessibilityNodeInfoCompat node) {
        if (node == null) {
            return false;
        }
        AccessibilityNodeInfoRef current = AccessibilityNodeInfoRef.unOwned(node);
        try {
            do {
                LogUtils.log(this, Log.VERBOSE, "Considering to long click: %s", current.get().getInfo());
                if (current.get().performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)) {
                    return true;
                }
            } while (current.parent());
        } finally {
            current.recycle();
        }
        LogUtils.log(this, Log.VERBOSE, "Long click action failed");
        return false;
    }

    /**
     * Formats some braille content from an {@link AccessibilityEvent}.
     *
     * @param event The event from which to format an utterance.
     * @return The formatted utterance.
     */
    private DisplayManager.Content formatEventToBraille(AccessibilityEvent event) {
        AccessibilityNodeInfoCompat eventNode = getNodeFromEvent(event);
        if (eventNode != null) {
            DisplayManager.Content ret = mNodeBrailler.brailleNode(eventNode);
            ret.setPanStrategy(DisplayManager.Content.PAN_CURSOR);
            mLastFocusedNode.reset(eventNode);
            return ret;
        }

        // Fall back on putting the event text on the display.
        // TODO: This can interfere with what's on the display and should be
        // done in a more disciplined manner.
        LogUtils.log(this, Log.VERBOSE, "No node on event, falling back on event text");
        mLastFocusedNode.clear();
        return new DisplayManager.Content(AccessibilityEventUtils.getEventText(event));
    }

    private void brailleNodeFromEvent(AccessibilityEvent event) {
        mDisplayManager.setContent(formatEventToBraille(event));
    }

    private boolean brailleFocusedNode() {
        AccessibilityNodeInfoCompat focused = getFocusedNode(false);
        if (focused != null) {
            DisplayManager.Content content = mNodeBrailler.brailleNode(focused);
            if (focused.equals(mLastFocusedNode.get())
                    && (content.getPanStrategy() == DisplayManager.Content.PAN_RESET)) {
                content.setPanStrategy(DisplayManager.Content.PAN_KEEP);
            }
            mDisplayManager.setContent(content);
            mLastFocusedNode.reset(focused);
            return true;
        }
        return false;
    }

    private void brailleFirstFocusableNode() {
        AccessibilityNodeInfo unwrappedRoot = mAccessibilityService.getRootInActiveWindow();
        if (unwrappedRoot != null) {
            AccessibilityNodeInfoCompat root = new AccessibilityNodeInfoCompat(unwrappedRoot);
            AccessibilityNodeInfoCompat toBraille = null;
            if (AccessibilityNodeInfoUtils.shouldFocusNode(mAccessibilityService, root)) {
                toBraille = root;
                root = null;
            } else {
                toBraille = mFocusFinder.linear(root, FocusFinder.SEARCH_FORWARD);
                if (toBraille == null) {
                    // Fall back on root as a last resort.
                    toBraille = root;
                    root = null;
                }
            }
            DisplayManager.Content content = mNodeBrailler.brailleNode(toBraille);
            if (AccessibilityNodeInfoRef.isNull(mLastFocusedNode)
                    && (content.getPanStrategy() == DisplayManager.Content.PAN_RESET)) {
                content.setPanStrategy(DisplayManager.Content.PAN_KEEP);
            }
            mLastFocusedNode.clear();
            mDisplayManager.setContent(content);
            AccessibilityNodeInfoUtils.recycleNodes(root, toBraille);
        }
    }

    private AccessibilityNodeInfoCompat getNodeFromEvent(AccessibilityEvent event) {
        AccessibilityNodeInfo node = event.getSource();
        if (node != null) {
            return new AccessibilityNodeInfoCompat(node);
        } else {
            return null;
        }
    }

    private AccessibilityNodeInfoCompat linearSearch(int direction) {
        AccessibilityNodeInfoCompat source = getFocusedNode(true);
        AccessibilityNodeInfoCompat result = mFocusFinder.linear(source, direction);
        AccessibilityNodeInfoUtils.recycleNodes(source);
        return result;
    }

    /**
     * Attempts to scroll using the specified action.
     */
    private boolean attemptScrollAction(int action) {
        AccessibilityNodeInfoCompat focusedNode = null;
        AccessibilityNodeInfoCompat scrollableNode = null;

        try {
            focusedNode = getFocusedNode(false);
            if (focusedNode == null) {
                return false;
            }

            scrollableNode = AccessibilityNodeInfoUtils.getSelfOrMatchingAncestor(mAccessibilityService,
                    focusedNode, AccessibilityNodeInfoUtils.FILTER_SCROLLABLE);
            if (scrollableNode == null) {
                return false;
            }

            return scrollableNode.performAction(action);
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(focusedNode, scrollableNode);
        }
    }

    private boolean isSelfBrailled(AccessibilityNodeInfoCompat node) {
        return mSelfBrailleManager.hasContentForNode(node);
    }

    /**
     * Attempts to navigate to the top-most focusable node in the tree.
     */
    private boolean attemptNavigateTop() {
        AccessibilityNodeInfoCompat root = AccessibilityServiceCompatUtils
                .getRootInActiveWindow(mAccessibilityService);
        AccessibilityNodeInfoCompat toFocus = null;
        if (AccessibilityNodeInfoUtils.shouldFocusNode(mAccessibilityService, root)) {
            toFocus = root;
            root = null;
        } else {
            toFocus = mFocusFinder.linear(root, FocusFinder.SEARCH_FORWARD);
            if (toFocus == null) {
                // Fall back on root as a last resort.
                toFocus = root;
                root = null;
            }
        }

        try {
            if (toFocus.isAccessibilityFocused()) {
                brailleFocusedNode();
                return true;
            }

            return toFocus.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(root, toFocus);
        }
    }

    /**
     * Attempts to navigate to the bottom-most focusable node in the tree.
     */
    private boolean attemptNavigateBottom() {
        AccessibilityNodeInfoCompat root = AccessibilityServiceCompatUtils
                .getRootInActiveWindow(mAccessibilityService);
        AccessibilityNodeInfoCompat toFocus = FocusFinder.findLastFocusableDescendant(root, mAccessibilityService);

        try {
            if (toFocus == null) {
                if (AccessibilityNodeInfoUtils.shouldFocusNode(mAccessibilityService, root)) {
                    root = null;
                    toFocus = root;
                } else {
                    return false;
                }
            }

            if (toFocus.isAccessibilityFocused()) {
                brailleFocusedNode();
                return true;
            }

            return toFocus.performAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(root, toFocus);
        }
    }

    /**
     * Checks if the specified node is suitable for autoscrolling and will
     * try to scroll it if it is.
     * Note we might have an issue here when more than one node is on the line
     * or not the whole list item is visible.
     */
    private boolean autoScrollItem(AccessibilityNodeInfoCompat node, int scrollDirection) {
        int edgeDirection = (scrollDirection == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) ? 1 : -1;
        return AccessibilityNodeInfoUtils.isAutoScrollEdgeListItem(mAccessibilityService, node, edgeDirection)
                && attemptScrollAction(scrollDirection);
    }
}