/*
* 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.eventprocessor;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Message;
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.talkback.FeedbackItem;
import com.android.talkback.R;
import com.android.talkback.SpeechController;
import com.android.talkback.speechrules.NodeSpeechRuleProcessor;
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. When a node is accessibility-focused and hints are enabled,
* the hint will be queued after a short delay.
*/
public class ProcessorAccessibilityHints implements AccessibilityEventListener {
private final SharedPreferences mPrefs;
private final Context mContext;
private final SpeechController mSpeechController;
private final NodeSpeechRuleProcessor mRuleProcessor;
private final A11yHintHandler mHandler;
public ProcessorAccessibilityHints(Context context, SpeechController speechController) {
if (speechController == null) throw new IllegalStateException();
mPrefs = SharedPreferencesUtils.getSharedPreferences(context);
mContext = context;
mSpeechController = speechController;
mRuleProcessor = NodeSpeechRuleProcessor.getInstance();
mHandler = new A11yHintHandler(this);
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (!areHintsEnabled()) {
return;
}
// Clear hints that were generated before a click or in an old window configuration.
final int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED ||
eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) {
cancelA11yHint();
return;
}
if (eventType == AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
EventState eventState = EventState.getInstance();
if (eventState.checkAndClearRecentEvent(
EventState.EVENT_SKIP_HINT_AFTER_GRANULARITY_MOVE)) {
return;
}
if (eventState.checkAndClearRecentEvent(
EventState.EVENT_SKIP_HINT_AFTER_CURSOR_CONTROL)) {
return;
}
AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
AccessibilityNodeInfoCompat source = record.getSource();
if (source != null) {
postA11yHintRunnable(source);
// DO NOT RECYCLE. postA11yHintRunnable will save the node.
}
}
}
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 source The source node.
*/
private String getHintFromNode(AccessibilityNodeInfoCompat source) {
if (source == null) {
return null;
}
final CharSequence text = mRuleProcessor.getHintForNode(source);
if (TextUtils.isEmpty(text)) {
return null;
}
return text.toString();
}
private void speakHint(String text) {
// Use QUEUE mode so that we don't interrupt more important messages.
mSpeechController.speak(
text, SpeechController.QUEUE_MODE_QUEUE, FeedbackItem.FLAG_NO_HISTORY, null);
}
/**
* The source node whose hint will be read by the utterance complete action.
*/
private AccessibilityNodeInfoCompat mPendingHintSource;
/**
* Starts the hint timeout. Call this for every event that triggers a hint.
*/
private void postA11yHintRunnable(AccessibilityNodeInfoCompat node) {
cancelA11yHint();
if (mPendingHintSource != null) {
mPendingHintSource.recycle();
}
mPendingHintSource = node;
// 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();
if (mPendingHintSource != null) {
mPendingHintSource.recycle();
}
mPendingHintSource = 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 (mPendingHintSource == null) {
return;
}
mHandler.startA11yHintTimeout(mPendingHintSource);
}
};
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: {
AccessibilityNodeInfoCompat source = (AccessibilityNodeInfoCompat) msg.obj;
AccessibilityNodeInfoCompat refreshed =
AccessibilityNodeInfoUtils.refreshNode(source);
if (refreshed != null) {
if (refreshed.isAccessibilityFocused()) {
String hint = parent.getHintFromNode(source);
parent.speakHint(hint);
LogUtils.log(this, Log.VERBOSE, "Speaking hint for node: %s",
refreshed);
} else {
LogUtils.log(this, Log.VERBOSE, "Skipping hint for node: %s",
refreshed);
}
refreshed.recycle();
}
source.recycle();
break;
}
}
}
public void startA11yHintTimeout(AccessibilityNodeInfoCompat source) {
if (source != null) {
final Message msg = obtainMessage(LONG_HOVER_TIMEOUT,
AccessibilityNodeInfoCompat.obtain(source));
sendMessageDelayed(msg, DELAY_LONG_HOVER_TIMEOUT);
LogUtils.log(this, Log.VERBOSE, "Queuing hint for node: %s", source);
}
}
public void cancelA11yHintTimeout() {
removeMessages(LONG_HOVER_TIMEOUT);
}
}
}