com.android.screenspeak.eventprocessor.ProcessorAccessibilityHints.java Source code

Java tutorial

Introduction

Here is the source code for com.android.screenspeak.eventprocessor.ProcessorAccessibilityHints.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.screenspeak.eventprocessor;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import com.android.screenspeak.FeedbackItem;
import com.android.screenspeak.R;
import com.android.screenspeak.SpeechController;
import com.android.screenspeak.controller.CursorControllerApp;
import com.android.screenspeak.speechrules.NodeSpeechRuleProcessor;
import com.android.screenspeak.tutorial.AccessibilityTutorialActivity;
import com.android.screenspeak.controller.GestureActionMonitor;
import com.android.utils.AccessibilityEventListener;
import com.android.utils.AccessibilityNodeInfoUtils;
import com.android.utils.LogUtils;
import com.android.utils.SharedPreferencesUtils;
import com.android.utils.WeakReferenceHandler;

/**
 * Manages accessibility hints. If a HOVER_ENTER event passes through this
 * processor or a control receives focus via linear exploration, the relevant
 * hint will be spoken unless an event is received before the timeout completes.
 */
public class ProcessorAccessibilityHints
        implements AccessibilityEventListener, GestureActionMonitor.GestureActionListener {
    private final GestureActionMonitor mGestureActionMonitor = new GestureActionMonitor();

    private final SharedPreferences mPrefs;
    private final Context mContext;
    private final SpeechController mSpeechController;
    private final CursorControllerApp mCursorController;
    private final NodeSpeechRuleProcessor mRuleProcessor;
    private final A11yHintHandler mHandler;

    private AccessibilityNodeInfoCompat mWaitingForExit;
    private boolean mIsTouchExploring;

    public ProcessorAccessibilityHints(Context context, SpeechController speechController,
            CursorControllerApp cursorController) {
        if (speechController == null)
            throw new IllegalStateException();
        mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
        mContext = context;
        mSpeechController = speechController;
        mCursorController = cursorController;
        mRuleProcessor = NodeSpeechRuleProcessor.getInstance();
        mHandler = new A11yHintHandler(this);

        mGestureActionMonitor.setListener(this);
        LocalBroadcastManager.getInstance(mContext).registerReceiver(mGestureActionMonitor,
                GestureActionMonitor.FILTER);
    }

    @Override
    public void onGestureAction(String action) {
        cancelA11yHint();

        if ((action.equals(mContext.getString(R.string.shortcut_value_next))
                || action.equals(mContext.getString(R.string.shortcut_value_previous))) && areHintsEnabled()) {
            final AccessibilityNodeInfoCompat source = mCursorController.getCursor();
            if (source == null) {
                return;
            }

            if (mCursorController.isLinearNavigationLocked(source)) {
                return;
            }

            final CharSequence text = mRuleProcessor.getHintForNode(source);
            if (!TextUtils.isEmpty(text)) {
                postA11yHintRunnable(text.toString());
            }
            source.recycle();
        }
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        final int eventType = event.getEventType();

        if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
                || eventType == AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START) {
            mIsTouchExploring = true;
        }

        if (eventType == AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END
                || eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {
            mIsTouchExploring = false;
            cacheEnteredNode(null);
            cancelA11yHint();
            return;
        }

        if (!mIsTouchExploring || ((eventType != AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
                && (eventType != AccessibilityEvent.TYPE_VIEW_HOVER_EXIT))) {
            return;
        }

        final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
        final AccessibilityNodeInfoCompat source = record.getSource();

        if (source == null) {
            return;
        }

        if (eventType == AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
            cacheEnteredNode(source);
            String hint = getHintFromEvent(event);
            if (hint != null) {
                postA11yHintRunnable(hint);
            }
        } else if (eventType == AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT) {
            if (source.equals(mWaitingForExit)) {
                cancelA11yHint();
            }
        }

        source.recycle();
    }

    private boolean areHintsEnabled() {
        return SharedPreferencesUtils.getBooleanPref(mPrefs, mContext.getResources(), R.string.pref_a11y_hints_key,
                R.bool.pref_a11y_hints_default);
    }

    /**
     * Given an {@link AccessibilityEvent}, obtains the long hover utterance.
     *
     * @param event The source event.
     */
    private String getHintFromEvent(AccessibilityEvent event) {
        final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
        AccessibilityNodeInfoCompat source = record.getSource();

        if (source == null) {
            return null;
        }

        // If this was a HOVER_ENTER event, we need to compute the focused node.
        // TODO: We're already doing this in the touch exploration formatter --
        // maybe this belongs there instead?
        if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER) {
            source = AccessibilityNodeInfoUtils.findFocusFromHover(source);
            if (source == null) {
                return null;
            }
        }

        final CharSequence text = mRuleProcessor.getHintForNode(source);
        source.recycle();

        if (TextUtils.isEmpty(text)) {
            return null;
        }

        return text.toString();
    }

    private void speakHint(String text) {
        // Never speak hint text if the tutorial is active
        if (AccessibilityTutorialActivity.isTutorialActive()) {
            LogUtils.log(this, Log.VERBOSE, "Dropping hint speech because tutorial is active.");
            return;
        }

        // Use QUEUE mode so that we don't interrupt more important messages.
        mSpeechController.speak(text, SpeechController.QUEUE_MODE_QUEUE, FeedbackItem.FLAG_NO_HISTORY, null);
    }

    private void cacheEnteredNode(AccessibilityNodeInfoCompat node) {
        if (mWaitingForExit != null) {
            mWaitingForExit.recycle();
            mWaitingForExit = null;
        }

        if (node != null) {
            mWaitingForExit = AccessibilityNodeInfoCompat.obtain(node);
        }
    }

    /**
     * The hint that will be read by the utterance complete action.
     */
    private String mPendingHint;

    /**
     * Starts the hint timeout. Call this for every event that triggers a hint.
     */
    private void postA11yHintRunnable(String hint) {
        cancelA11yHint();

        mPendingHint = hint;

        // The timeout starts after the current text is spoken.
        mSpeechController.addUtteranceCompleteAction(mSpeechController.peekNextUtteranceId(), mA11yHintRunnable);
    }

    /**
     * Removes the hint timeout and completion action. Call this for every event.
     */
    private void cancelA11yHint() {
        mHandler.cancelA11yHintTimeout();

        mPendingHint = null;
    }

    /**
     * Posts a delayed hint action.
     */
    private final SpeechController.UtteranceCompleteRunnable mA11yHintRunnable = new SpeechController.UtteranceCompleteRunnable() {
        @Override
        public void run(int status) {
            // The utterance must have been spoken successfully.
            if (status != SpeechController.STATUS_SPOKEN) {
                return;
            }

            if (mPendingHint == null) {
                return;
            }

            mHandler.startA11yHintTimeout(mPendingHint);
        }
    };

    private static class A11yHintHandler extends WeakReferenceHandler<ProcessorAccessibilityHints> {
        /**
         * Message identifier for a verbose (long-hover) notification.
         */
        private static final int LONG_HOVER_TIMEOUT = 1;

        /**
         * Timeout before reading a verbose (long-hover) notification.
         */
        private static final long DELAY_LONG_HOVER_TIMEOUT = 1000;

        public A11yHintHandler(ProcessorAccessibilityHints parent) {
            super(parent);
        }

        @Override
        public void handleMessage(Message msg, ProcessorAccessibilityHints parent) {
            switch (msg.what) {
            case LONG_HOVER_TIMEOUT: {
                final String hint = (String) msg.obj;
                parent.speakHint(hint);
                break;
            }
            }
        }

        public void startA11yHintTimeout(String hint) {
            final Message msg = obtainMessage(LONG_HOVER_TIMEOUT, hint);

            sendMessageDelayed(msg, DELAY_LONG_HOVER_TIMEOUT);
        }

        public void cancelA11yHintTimeout() {
            removeMessages(LONG_HOVER_TIMEOUT);
        }
    }
}