com.android.talkback.controller.FullScreenReadControllerApp.java Source code

Java tutorial

Introduction

Here is the source code for com.android.talkback.controller.FullScreenReadControllerApp.java

Source

/*
 * Copyright (C) 2012 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.controller;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.PowerManager;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;

import com.android.talkback.CursorGranularity;
import com.android.talkback.InputModeManager;
import com.android.talkback.R;
import com.android.talkback.SpeechController;
import com.google.android.marvin.talkback.TalkBackService;

import com.android.talkback.eventprocessor.EventState;
import com.android.utils.AccessibilityEventListener;
import com.android.utils.AccessibilityEventUtils;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.LogUtils;
import com.android.utils.WebInterfaceUtils;
import com.android.utils.compat.accessibilityservice.AccessibilityServiceCompatUtils;
import com.android.utils.traversal.OrderedTraversalStrategy;
import com.android.utils.traversal.TraversalStrategy;

public class FullScreenReadControllerApp implements FullScreenReadController, AccessibilityEventListener {
    /** Tag used for log output and wake lock */
    private static final String TAG = "FullScreenReadController";

    /** The possible states of the controller. */
    private static final int STATE_STOPPED = 0;
    private static final int STATE_READING_FROM_BEGINNING = 1;
    private static final int STATE_READING_FROM_NEXT = 2;
    private static final int STATE_ENTERING_LEGACY_WEB_CONTENT = 3;

    /** Event types that should interrupt continuous reading, if active. */
    private static final int MASK_EVENT_TYPES_INTERRUPT_CONTINUOUS = AccessibilityEvent.TYPE_VIEW_CLICKED
            | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED | AccessibilityEvent.TYPE_VIEW_SELECTED
            | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED | AccessibilityEventCompat.TYPE_ANNOUNCEMENT
            | AccessibilityEventCompat.TYPE_GESTURE_DETECTION_START
            | AccessibilityEventCompat.TYPE_TOUCH_EXPLORATION_GESTURE_START
            | AccessibilityEventCompat.TYPE_TOUCH_INTERACTION_START | AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER
            | AccessibilityEventCompat.TYPE_VIEW_TEXT_SELECTION_CHANGED;

    /**
     * The current state of the controller. Should only be updated through
     * {@link FullScreenReadControllerApp#setReadingState(int)}
     */
    private int mCurrentState = STATE_STOPPED;

    /** The parent service */
    private final TalkBackService mService;

    /** Controller for linearly navigating the view hierarchy tree */
    private CursorController mCursorController;

    /** Feedback controller for audio feedback */
    private final FeedbackController mFeedbackController;

    /** Wake lock for keeping the device unlocked while reading */
    private PowerManager.WakeLock mWakeLock;

    @SuppressWarnings("deprecation")
    public FullScreenReadControllerApp(FeedbackController feedbackController, CursorController cursorController,
            TalkBackService service) {
        if (cursorController == null)
            throw new IllegalStateException();
        if (feedbackController == null)
            throw new IllegalStateException();

        mCursorController = cursorController;
        mFeedbackController = feedbackController;
        mService = service;
        mWakeLock = ((PowerManager) service.getSystemService(Context.POWER_SERVICE))
                .newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG);
    }

    /**
     * Releases all resources held by this controller and save any persistent
     * preferences.
     */
    public void shutdown() {
        interrupt();
    }

    /**
     * Starts linearly reading from the node with accessibility focus.
     */
    public void startReadingFromNextNode() {
        if (isActive()) {
            return;
        }

        final AccessibilityNodeInfoCompat currentNode = mCursorController.getCursor();
        if (currentNode == null) {
            return;
        }

        setReadingState(STATE_READING_FROM_NEXT);

        mCursorController.setGranularity(CursorGranularity.DEFAULT, false /* fromUser */);

        if (!mWakeLock.isHeld()) {
            mWakeLock.acquire();
        }

        // Avoid reading the elements in web content twice by calling directly
        // into ChromeVox rather than advancing CursorController first.
        if (WebInterfaceUtils.hasLegacyWebContent(currentNode)) {
            moveIntoWebContent();
        } else {
            moveForward();
        }

        currentNode.recycle();
    }

    /**
     * Starts linearly reading from the top of the view hierarchy.
     */
    public void startReadingFromBeginning() {
        AccessibilityNodeInfoCompat rootNode = null;
        AccessibilityNodeInfoCompat currentNode = null;

        if (isActive()) {
            return;
        }

        try {
            rootNode = AccessibilityServiceCompatUtils.getRootInActiveWindow(mService);
            if (rootNode == null) {
                return;
            }

            TraversalStrategy traversal = new OrderedTraversalStrategy(rootNode);
            try {
                currentNode = AccessibilityNodeInfoUtils.searchFocus(traversal, rootNode,
                        TraversalStrategy.SEARCH_FOCUS_FORWARD, AccessibilityNodeInfoUtils.FILTER_SHOULD_FOCUS);
            } finally {
                traversal.recycle();
            }

            if (currentNode == null) {
                return;
            }

            setReadingState(STATE_READING_FROM_BEGINNING);

            mCursorController.setGranularity(CursorGranularity.DEFAULT, false /* fromUser */);

            if (!mWakeLock.isHeld()) {
                mWakeLock.acquire();
            }

            // This is potentially a refocus, so we should set the refocus flag just in case.
            EventState.getInstance().addEvent(EventState.EVENT_NODE_REFOCUSED);
            mCursorController.clearCursor();
            mCursorController.setCursor(currentNode); // Will automatically move forward.

            if (WebInterfaceUtils.hasLegacyWebContent(currentNode)) {
                moveIntoWebContent();
            }
        } finally {
            AccessibilityNodeInfoUtils.recycleNodes(rootNode, currentNode);
        }
    }

    /**
     * Stops speech output and view traversal at the current position.
     */
    public void interrupt() {
        setReadingState(STATE_STOPPED);

        if (mWakeLock.isHeld()) {
            mWakeLock.release();
        }
    }

    private void moveForward() {
        if (!mCursorController.next(false /* shouldWrap */, false /* shouldScroll */,
                false /*useInputFocusAsPivotIfEmpty*/, InputModeManager.INPUT_MODE_UNKNOWN)) {
            mFeedbackController.playAuditory(R.raw.complete, 1.3f, 1);
            interrupt();
        }

        if (currentNodeHasWebContent()) {
            moveIntoWebContent();
        }
    }

    private void moveIntoWebContent() {
        final AccessibilityNodeInfoCompat webNode = mCursorController.getCursor();
        if (webNode == null) {
            // Reset state.
            interrupt();
            return;
        }

        if (mCurrentState == STATE_READING_FROM_BEGINNING) {
            // Reset ChromeVox's active indicator to the start to the page.
            WebInterfaceUtils.performNavigationAtGranularityAction(webNode, WebInterfaceUtils.DIRECTION_BACKWARD,
                    AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE);
        }

        WebInterfaceUtils.performNavigationAtGranularityAction(webNode, WebInterfaceUtils.DIRECTION_FORWARD,
                AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE);

        setReadingState(STATE_ENTERING_LEGACY_WEB_CONTENT);

        webNode.recycle();
    }

    private void setReadingState(int newState) {
        LogUtils.log(TAG, Log.VERBOSE, "Continuous reading switching to mode: %s", newState);

        mCurrentState = newState;

        TalkBackService service = TalkBackService.getInstance();
        if (service != null) {
            service.getSpeechController().setShouldInjectAutoReadingCallbacks(isActive(), mNodeSpokenRunnable);
        }
    }

    public boolean isReadingLegacyWebContent() {
        return mCurrentState == STATE_ENTERING_LEGACY_WEB_CONTENT;
    }

    /**
     * Returns whether full-screen reading is currently active. Equivalent to
     * calling {@code mCurrentState != STATE_STOPPED}.
     *
     * @return Whether full-screen reading is currently active.
     */
    public boolean isActive() {
        return mCurrentState != STATE_STOPPED;
    }

    private boolean currentNodeHasWebContent() {
        final AccessibilityNodeInfoCompat currentNode = mCursorController.getCursor();
        if (currentNode == null) {
            return false;
        }

        final boolean isWebContent = WebInterfaceUtils.hasLegacyWebContent(currentNode);
        currentNode.recycle();
        return isWebContent;
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (!isActive()) {
            return;
        }

        // Only interrupt full screen reading on events that can't be generated
        // by automated cursor movement or from delayed user interaction.
        if (AccessibilityEventUtils.eventMatchesAnyType(event, MASK_EVENT_TYPES_INTERRUPT_CONTINUOUS)) {
            interrupt();
        }
    }

    /** Runnable executed when a node has finished being spoken */
    private final SpeechController.UtteranceCompleteRunnable mNodeSpokenRunnable = new SpeechController.UtteranceCompleteRunnable() {
        @Override
        public void run(int status) {
            if (isActive() && !isReadingLegacyWebContent() && status != SpeechController.STATUS_INTERRUPTED) {
                moveForward();
            }
        }
    };
}