summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/java/android/webkit/AccessibilityInjector.java170
1 files changed, 159 insertions, 11 deletions
diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java
index a51a8f6..d6576e1 100644
--- a/core/java/android/webkit/AccessibilityInjector.java
+++ b/core/java/android/webkit/AccessibilityInjector.java
@@ -21,6 +21,10 @@ import android.os.Bundle;
import android.os.SystemClock;
import android.provider.Settings;
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.View;
import android.view.accessibility.AccessibilityManager;
@@ -44,6 +48,10 @@ import java.util.concurrent.atomic.AtomicInteger;
* APIs.
*/
class AccessibilityInjector {
+ private static final String TAG = AccessibilityInjector.class.getSimpleName();
+
+ private static boolean DEBUG = false;
+
// The WebViewClassic this injector is responsible for managing.
private final WebViewClassic mWebViewClassic;
@@ -90,6 +98,10 @@ class AccessibilityInjector {
private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE =
"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
* {@code webViewClassic}.
@@ -117,6 +129,7 @@ class AccessibilityInjector {
addTtsApis();
addCallbackApis();
+ toggleAndroidVox(true);
}
/**
@@ -126,10 +139,20 @@ class AccessibilityInjector {
* </p>
*/
public void removeAccessibilityApisIfNecessary() {
+ toggleAndroidVox(false);
removeTtsApis();
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
* movement granularity levels supported by this
@@ -196,7 +219,7 @@ class AccessibilityInjector {
if (mAccessibilityScriptInjected) {
return sendActionToAndroidVox(action, arguments);
}
-
+
if (mAccessibilityInjectorFallback != null) {
return mAccessibilityInjectorFallback.performAccessibilityAction(action, arguments);
}
@@ -262,6 +285,9 @@ class AccessibilityInjector {
*/
public void onPageStarted(String url) {
mAccessibilityScriptInjected = false;
+ if (DEBUG)
+ Log.w(TAG, "[" + mWebView.hashCode() + "] Started loading new page");
+ addAccessibilityApisIfNecessary();
}
/**
@@ -282,15 +308,23 @@ class AccessibilityInjector {
if (!shouldInjectJavaScript(url)) {
mAccessibilityScriptInjected = false;
toggleFallbackAccessibilityInjector(true);
+ if (DEBUG)
+ Log.d(TAG, "[" + mWebView.hashCode() + "] Using fallback accessibility support");
return;
}
toggleFallbackAccessibilityInjector(false);
- final String injectionUrl = getScreenReaderInjectionUrl();
- mWebView.loadUrl(injectionUrl);
-
- mAccessibilityScriptInjected = true;
+ if (!mAccessibilityScriptInjected) {
+ mAccessibilityScriptInjected = true;
+ final String injectionUrl = getScreenReaderInjectionUrl();
+ 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) {
return;
}
+ if (DEBUG)
+ Log.d(TAG, "[" + mWebView.hashCode() + "] Adding TTS APIs into WebView");
mTextToSpeech = new TextToSpeechWrapper(mContext);
mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE);
}
@@ -381,6 +417,8 @@ class AccessibilityInjector {
return;
}
+ if (DEBUG)
+ Log.d(TAG, "[" + mWebView.hashCode() + "] Removing TTS APIs from WebView");
mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE);
mTextToSpeech.stop();
mTextToSpeech.shutdown();
@@ -527,35 +565,141 @@ class AccessibilityInjector {
* Used to protect the TextToSpeech class, only exposing the methods we want to expose.
*/
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) {
+ if (DEBUG)
+ Log.d(WRAP_TAG, "[" + hashCode() + "] Initializing text-to-speech on thread "
+ + Thread.currentThread().getId() + "...");
+
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
@SuppressWarnings("unused")
public boolean isSpeaking() {
- return mTextToSpeech.isSpeaking();
+ synchronized (mTextToSpeech) {
+ if (!mReady) {
+ return false;
+ }
+
+ return mTextToSpeech.isSpeaking();
+ }
}
@JavascriptInterface
@SuppressWarnings("unused")
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
@SuppressWarnings("unused")
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")
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.
*/
private boolean waitForResultTimedLocked(int resultId) {
+ if (DEBUG)
+ Log.d(TAG, "Waiting for CVOX result...");
long waitTimeMillis = RESULT_TIMEOUT;
final long startTimeMillis = SystemClock.uptimeMillis();
while (true) {
@@ -642,6 +788,8 @@ class AccessibilityInjector {
}
mResultLock.wait(waitTimeMillis);
} catch (InterruptedException ie) {
+ if (DEBUG)
+ Log.w(TAG, "Timed out while waiting for CVOX result");
/* ignore */
}
}