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