Merge "Fix WebView accessibility scripts." into jb-mr1-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
02f0daa580
@ -21,6 +21,10 @@ import android.os.Bundle;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.speech.tts.TextToSpeech;
|
import android.speech.tts.TextToSpeech;
|
||||||
|
import android.speech.tts.TextToSpeech.Engine;
|
||||||
|
import android.speech.tts.TextToSpeech.OnInitListener;
|
||||||
|
import android.speech.tts.UtteranceProgressListener;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.accessibility.AccessibilityManager;
|
import android.view.accessibility.AccessibilityManager;
|
||||||
@ -44,6 +48,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
* APIs.
|
* APIs.
|
||||||
*/
|
*/
|
||||||
class AccessibilityInjector {
|
class AccessibilityInjector {
|
||||||
|
private static final String TAG = AccessibilityInjector.class.getSimpleName();
|
||||||
|
|
||||||
|
private static boolean DEBUG = false;
|
||||||
|
|
||||||
// The WebViewClassic this injector is responsible for managing.
|
// The WebViewClassic this injector is responsible for managing.
|
||||||
private final WebViewClassic mWebViewClassic;
|
private final WebViewClassic mWebViewClassic;
|
||||||
|
|
||||||
@ -90,6 +98,10 @@ class AccessibilityInjector {
|
|||||||
private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE =
|
private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE =
|
||||||
"cvox.AndroidVox.performAction('%1s')";
|
"cvox.AndroidVox.performAction('%1s')";
|
||||||
|
|
||||||
|
// JS code used to shut down an active AndroidVox instance.
|
||||||
|
private static final String TOGGLE_CVOX_TEMPLATE =
|
||||||
|
"javascript:(function() { cvox.ChromeVox.host.activateOrDeactivateChromeVox(%b); })();";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of the AccessibilityInjector based on
|
* Creates an instance of the AccessibilityInjector based on
|
||||||
* {@code webViewClassic}.
|
* {@code webViewClassic}.
|
||||||
@ -117,6 +129,7 @@ class AccessibilityInjector {
|
|||||||
|
|
||||||
addTtsApis();
|
addTtsApis();
|
||||||
addCallbackApis();
|
addCallbackApis();
|
||||||
|
toggleAndroidVox(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,10 +139,20 @@ class AccessibilityInjector {
|
|||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public void removeAccessibilityApisIfNecessary() {
|
public void removeAccessibilityApisIfNecessary() {
|
||||||
|
toggleAndroidVox(false);
|
||||||
removeTtsApis();
|
removeTtsApis();
|
||||||
removeCallbackApis();
|
removeCallbackApis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void toggleAndroidVox(boolean state) {
|
||||||
|
if (!mAccessibilityScriptInjected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String code = String.format(TOGGLE_CVOX_TEMPLATE, state);
|
||||||
|
mWebView.loadUrl(code);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes an {@link AccessibilityNodeInfo} with the actions and
|
* Initializes an {@link AccessibilityNodeInfo} with the actions and
|
||||||
* movement granularity levels supported by this
|
* movement granularity levels supported by this
|
||||||
@ -196,7 +219,7 @@ class AccessibilityInjector {
|
|||||||
if (mAccessibilityScriptInjected) {
|
if (mAccessibilityScriptInjected) {
|
||||||
return sendActionToAndroidVox(action, arguments);
|
return sendActionToAndroidVox(action, arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mAccessibilityInjectorFallback != null) {
|
if (mAccessibilityInjectorFallback != null) {
|
||||||
return mAccessibilityInjectorFallback.performAccessibilityAction(action, arguments);
|
return mAccessibilityInjectorFallback.performAccessibilityAction(action, arguments);
|
||||||
}
|
}
|
||||||
@ -262,6 +285,9 @@ class AccessibilityInjector {
|
|||||||
*/
|
*/
|
||||||
public void onPageStarted(String url) {
|
public void onPageStarted(String url) {
|
||||||
mAccessibilityScriptInjected = false;
|
mAccessibilityScriptInjected = false;
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(TAG, "[" + mWebView.hashCode() + "] Started loading new page");
|
||||||
|
addAccessibilityApisIfNecessary();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -282,15 +308,23 @@ class AccessibilityInjector {
|
|||||||
if (!shouldInjectJavaScript(url)) {
|
if (!shouldInjectJavaScript(url)) {
|
||||||
mAccessibilityScriptInjected = false;
|
mAccessibilityScriptInjected = false;
|
||||||
toggleFallbackAccessibilityInjector(true);
|
toggleFallbackAccessibilityInjector(true);
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "[" + mWebView.hashCode() + "] Using fallback accessibility support");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFallbackAccessibilityInjector(false);
|
toggleFallbackAccessibilityInjector(false);
|
||||||
|
|
||||||
final String injectionUrl = getScreenReaderInjectionUrl();
|
if (!mAccessibilityScriptInjected) {
|
||||||
mWebView.loadUrl(injectionUrl);
|
mAccessibilityScriptInjected = true;
|
||||||
|
final String injectionUrl = getScreenReaderInjectionUrl();
|
||||||
mAccessibilityScriptInjected = true;
|
mWebView.loadUrl(injectionUrl);
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "[" + mWebView.hashCode() + "] Loading screen reader into WebView");
|
||||||
|
} else {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(TAG, "[" + mWebView.hashCode() + "] Attempted to inject screen reader twice");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -368,6 +402,8 @@ class AccessibilityInjector {
|
|||||||
if (mTextToSpeech != null) {
|
if (mTextToSpeech != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "[" + mWebView.hashCode() + "] Adding TTS APIs into WebView");
|
||||||
mTextToSpeech = new TextToSpeechWrapper(mContext);
|
mTextToSpeech = new TextToSpeechWrapper(mContext);
|
||||||
mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE);
|
mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE);
|
||||||
}
|
}
|
||||||
@ -381,6 +417,8 @@ class AccessibilityInjector {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "[" + mWebView.hashCode() + "] Removing TTS APIs from WebView");
|
||||||
mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE);
|
mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE);
|
||||||
mTextToSpeech.stop();
|
mTextToSpeech.stop();
|
||||||
mTextToSpeech.shutdown();
|
mTextToSpeech.shutdown();
|
||||||
@ -527,35 +565,141 @@ class AccessibilityInjector {
|
|||||||
* Used to protect the TextToSpeech class, only exposing the methods we want to expose.
|
* Used to protect the TextToSpeech class, only exposing the methods we want to expose.
|
||||||
*/
|
*/
|
||||||
private static class TextToSpeechWrapper {
|
private static class TextToSpeechWrapper {
|
||||||
private TextToSpeech mTextToSpeech;
|
private static final String WRAP_TAG = TextToSpeechWrapper.class.getSimpleName();
|
||||||
|
|
||||||
|
private final HashMap<String, String> mTtsParams;
|
||||||
|
private final TextToSpeech mTextToSpeech;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this wrapper is ready to speak. If this is {@code true} then
|
||||||
|
* {@link #mShutdown} is guaranteed to be {@code false}.
|
||||||
|
*/
|
||||||
|
private volatile boolean mReady;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this wrapper was shut down. If this is {@code true} then
|
||||||
|
* {@link #mReady} is guaranteed to be {@code false}.
|
||||||
|
*/
|
||||||
|
private volatile boolean mShutdown;
|
||||||
|
|
||||||
public TextToSpeechWrapper(Context context) {
|
public TextToSpeechWrapper(Context context) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(WRAP_TAG, "[" + hashCode() + "] Initializing text-to-speech on thread "
|
||||||
|
+ Thread.currentThread().getId() + "...");
|
||||||
|
|
||||||
final String pkgName = context.getPackageName();
|
final String pkgName = context.getPackageName();
|
||||||
mTextToSpeech = new TextToSpeech(context, null, null, pkgName + ".**webview**", true);
|
|
||||||
|
mReady = false;
|
||||||
|
mShutdown = false;
|
||||||
|
|
||||||
|
mTtsParams = new HashMap<String, String>();
|
||||||
|
mTtsParams.put(Engine.KEY_PARAM_UTTERANCE_ID, WRAP_TAG);
|
||||||
|
|
||||||
|
mTextToSpeech = new TextToSpeech(
|
||||||
|
context, mInitListener, null, pkgName + ".**webview**", true);
|
||||||
|
mTextToSpeech.setOnUtteranceProgressListener(mErrorListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public boolean isSpeaking() {
|
public boolean isSpeaking() {
|
||||||
return mTextToSpeech.isSpeaking();
|
synchronized (mTextToSpeech) {
|
||||||
|
if (!mReady) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mTextToSpeech.isSpeaking();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public int speak(String text, int queueMode, HashMap<String, String> params) {
|
public int speak(String text, int queueMode, HashMap<String, String> params) {
|
||||||
return mTextToSpeech.speak(text, queueMode, params);
|
synchronized (mTextToSpeech) {
|
||||||
|
if (!mReady) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to speak before TTS init");
|
||||||
|
return TextToSpeech.ERROR;
|
||||||
|
} else {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.i(WRAP_TAG, "[" + hashCode() + "] Speak called from JS binder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mTextToSpeech.speak(text, queueMode, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public int stop() {
|
public int stop() {
|
||||||
return mTextToSpeech.stop();
|
synchronized (mTextToSpeech) {
|
||||||
|
if (!mReady) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to stop before initialize");
|
||||||
|
return TextToSpeech.ERROR;
|
||||||
|
} else {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.i(WRAP_TAG, "[" + hashCode() + "] Stop called from JS binder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mTextToSpeech.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
protected void shutdown() {
|
protected void shutdown() {
|
||||||
mTextToSpeech.shutdown();
|
synchronized (mTextToSpeech) {
|
||||||
|
if (!mReady) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(WRAP_TAG, "[" + hashCode() + "] Called shutdown before initialize");
|
||||||
|
} else {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.i(WRAP_TAG, "[" + hashCode() + "] Shutting down text-to-speech from "
|
||||||
|
+ "thread " + Thread.currentThread().getId() + "...");
|
||||||
|
}
|
||||||
|
mShutdown = true;
|
||||||
|
mReady = false;
|
||||||
|
mTextToSpeech.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final OnInitListener mInitListener = new OnInitListener() {
|
||||||
|
@Override
|
||||||
|
public void onInit(int status) {
|
||||||
|
synchronized (mTextToSpeech) {
|
||||||
|
if (!mShutdown && (status == TextToSpeech.SUCCESS)) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
|
||||||
|
+ "] Initialized successfully");
|
||||||
|
mReady = true;
|
||||||
|
} else {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
|
||||||
|
+ "] Failed to initialize");
|
||||||
|
mReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final UtteranceProgressListener mErrorListener = new UtteranceProgressListener() {
|
||||||
|
@Override
|
||||||
|
public void onStart(String utteranceId) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String utteranceId) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
|
||||||
|
+ "] Failed to speak utterance");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDone(String utteranceId) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -625,6 +769,8 @@ class AccessibilityInjector {
|
|||||||
* @return Whether the result was received.
|
* @return Whether the result was received.
|
||||||
*/
|
*/
|
||||||
private boolean waitForResultTimedLocked(int resultId) {
|
private boolean waitForResultTimedLocked(int resultId) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.d(TAG, "Waiting for CVOX result...");
|
||||||
long waitTimeMillis = RESULT_TIMEOUT;
|
long waitTimeMillis = RESULT_TIMEOUT;
|
||||||
final long startTimeMillis = SystemClock.uptimeMillis();
|
final long startTimeMillis = SystemClock.uptimeMillis();
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -642,6 +788,8 @@ class AccessibilityInjector {
|
|||||||
}
|
}
|
||||||
mResultLock.wait(waitTimeMillis);
|
mResultLock.wait(waitTimeMillis);
|
||||||
} catch (InterruptedException ie) {
|
} catch (InterruptedException ie) {
|
||||||
|
if (DEBUG)
|
||||||
|
Log.w(TAG, "Timed out while waiting for CVOX result");
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user