Skip to content

Commit 02f0daa

Browse files
sganovAndroid (Google) Code Review
authored andcommitted
Merge "Fix WebView accessibility scripts." into jb-mr1-dev
2 parents 1460c8c + df31369 commit 02f0daa

File tree

1 file changed

+159
-11
lines changed

1 file changed

+159
-11
lines changed

core/java/android/webkit/AccessibilityInjector.java

Lines changed: 159 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
import android.os.SystemClock;
2222
import android.provider.Settings;
2323
import android.speech.tts.TextToSpeech;
24+
import android.speech.tts.TextToSpeech.Engine;
25+
import android.speech.tts.TextToSpeech.OnInitListener;
26+
import android.speech.tts.UtteranceProgressListener;
27+
import android.util.Log;
2428
import android.view.KeyEvent;
2529
import android.view.View;
2630
import android.view.accessibility.AccessibilityManager;
@@ -44,6 +48,10 @@
4448
* APIs.
4549
*/
4650
class AccessibilityInjector {
51+
private static final String TAG = AccessibilityInjector.class.getSimpleName();
52+
53+
private static boolean DEBUG = false;
54+
4755
// The WebViewClassic this injector is responsible for managing.
4856
private final WebViewClassic mWebViewClassic;
4957

@@ -90,6 +98,10 @@ class AccessibilityInjector {
9098
private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE =
9199
"cvox.AndroidVox.performAction('%1s')";
92100

101+
// JS code used to shut down an active AndroidVox instance.
102+
private static final String TOGGLE_CVOX_TEMPLATE =
103+
"javascript:(function() { cvox.ChromeVox.host.activateOrDeactivateChromeVox(%b); })();";
104+
93105
/**
94106
* Creates an instance of the AccessibilityInjector based on
95107
* {@code webViewClassic}.
@@ -117,6 +129,7 @@ public void addAccessibilityApisIfNecessary() {
117129

118130
addTtsApis();
119131
addCallbackApis();
132+
toggleAndroidVox(true);
120133
}
121134

122135
/**
@@ -126,10 +139,20 @@ public void addAccessibilityApisIfNecessary() {
126139
* </p>
127140
*/
128141
public void removeAccessibilityApisIfNecessary() {
142+
toggleAndroidVox(false);
129143
removeTtsApis();
130144
removeCallbackApis();
131145
}
132146

147+
private void toggleAndroidVox(boolean state) {
148+
if (!mAccessibilityScriptInjected) {
149+
return;
150+
}
151+
152+
final String code = String.format(TOGGLE_CVOX_TEMPLATE, state);
153+
mWebView.loadUrl(code);
154+
}
155+
133156
/**
134157
* Initializes an {@link AccessibilityNodeInfo} with the actions and
135158
* movement granularity levels supported by this
@@ -196,7 +219,7 @@ public boolean performAccessibilityAction(int action, Bundle arguments) {
196219
if (mAccessibilityScriptInjected) {
197220
return sendActionToAndroidVox(action, arguments);
198221
}
199-
222+
200223
if (mAccessibilityInjectorFallback != null) {
201224
return mAccessibilityInjectorFallback.performAccessibilityAction(action, arguments);
202225
}
@@ -262,6 +285,9 @@ public void handleSelectionChangedIfNecessary(String selectionString) {
262285
*/
263286
public void onPageStarted(String url) {
264287
mAccessibilityScriptInjected = false;
288+
if (DEBUG)
289+
Log.w(TAG, "[" + mWebView.hashCode() + "] Started loading new page");
290+
addAccessibilityApisIfNecessary();
265291
}
266292

267293
/**
@@ -282,15 +308,23 @@ public void onPageFinished(String url) {
282308
if (!shouldInjectJavaScript(url)) {
283309
mAccessibilityScriptInjected = false;
284310
toggleFallbackAccessibilityInjector(true);
311+
if (DEBUG)
312+
Log.d(TAG, "[" + mWebView.hashCode() + "] Using fallback accessibility support");
285313
return;
286314
}
287315

288316
toggleFallbackAccessibilityInjector(false);
289317

290-
final String injectionUrl = getScreenReaderInjectionUrl();
291-
mWebView.loadUrl(injectionUrl);
292-
293-
mAccessibilityScriptInjected = true;
318+
if (!mAccessibilityScriptInjected) {
319+
mAccessibilityScriptInjected = true;
320+
final String injectionUrl = getScreenReaderInjectionUrl();
321+
mWebView.loadUrl(injectionUrl);
322+
if (DEBUG)
323+
Log.d(TAG, "[" + mWebView.hashCode() + "] Loading screen reader into WebView");
324+
} else {
325+
if (DEBUG)
326+
Log.w(TAG, "[" + mWebView.hashCode() + "] Attempted to inject screen reader twice");
327+
}
294328
}
295329

296330
/**
@@ -368,6 +402,8 @@ private void addTtsApis() {
368402
if (mTextToSpeech != null) {
369403
return;
370404
}
405+
if (DEBUG)
406+
Log.d(TAG, "[" + mWebView.hashCode() + "] Adding TTS APIs into WebView");
371407
mTextToSpeech = new TextToSpeechWrapper(mContext);
372408
mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE);
373409
}
@@ -381,6 +417,8 @@ private void removeTtsApis() {
381417
return;
382418
}
383419

420+
if (DEBUG)
421+
Log.d(TAG, "[" + mWebView.hashCode() + "] Removing TTS APIs from WebView");
384422
mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE);
385423
mTextToSpeech.stop();
386424
mTextToSpeech.shutdown();
@@ -527,35 +565,141 @@ private boolean sendActionToAndroidVox(int action, Bundle arguments) {
527565
* Used to protect the TextToSpeech class, only exposing the methods we want to expose.
528566
*/
529567
private static class TextToSpeechWrapper {
530-
private TextToSpeech mTextToSpeech;
568+
private static final String WRAP_TAG = TextToSpeechWrapper.class.getSimpleName();
569+
570+
private final HashMap<String, String> mTtsParams;
571+
private final TextToSpeech mTextToSpeech;
572+
573+
/**
574+
* Whether this wrapper is ready to speak. If this is {@code true} then
575+
* {@link #mShutdown} is guaranteed to be {@code false}.
576+
*/
577+
private volatile boolean mReady;
578+
579+
/**
580+
* Whether this wrapper was shut down. If this is {@code true} then
581+
* {@link #mReady} is guaranteed to be {@code false}.
582+
*/
583+
private volatile boolean mShutdown;
531584

532585
public TextToSpeechWrapper(Context context) {
586+
if (DEBUG)
587+
Log.d(WRAP_TAG, "[" + hashCode() + "] Initializing text-to-speech on thread "
588+
+ Thread.currentThread().getId() + "...");
589+
533590
final String pkgName = context.getPackageName();
534-
mTextToSpeech = new TextToSpeech(context, null, null, pkgName + ".**webview**", true);
591+
592+
mReady = false;
593+
mShutdown = false;
594+
595+
mTtsParams = new HashMap<String, String>();
596+
mTtsParams.put(Engine.KEY_PARAM_UTTERANCE_ID, WRAP_TAG);
597+
598+
mTextToSpeech = new TextToSpeech(
599+
context, mInitListener, null, pkgName + ".**webview**", true);
600+
mTextToSpeech.setOnUtteranceProgressListener(mErrorListener);
535601
}
536602

537603
@JavascriptInterface
538604
@SuppressWarnings("unused")
539605
public boolean isSpeaking() {
540-
return mTextToSpeech.isSpeaking();
606+
synchronized (mTextToSpeech) {
607+
if (!mReady) {
608+
return false;
609+
}
610+
611+
return mTextToSpeech.isSpeaking();
612+
}
541613
}
542614

543615
@JavascriptInterface
544616
@SuppressWarnings("unused")
545617
public int speak(String text, int queueMode, HashMap<String, String> params) {
546-
return mTextToSpeech.speak(text, queueMode, params);
618+
synchronized (mTextToSpeech) {
619+
if (!mReady) {
620+
if (DEBUG)
621+
Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to speak before TTS init");
622+
return TextToSpeech.ERROR;
623+
} else {
624+
if (DEBUG)
625+
Log.i(WRAP_TAG, "[" + hashCode() + "] Speak called from JS binder");
626+
}
627+
628+
return mTextToSpeech.speak(text, queueMode, params);
629+
}
547630
}
548631

549632
@JavascriptInterface
550633
@SuppressWarnings("unused")
551634
public int stop() {
552-
return mTextToSpeech.stop();
635+
synchronized (mTextToSpeech) {
636+
if (!mReady) {
637+
if (DEBUG)
638+
Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to stop before initialize");
639+
return TextToSpeech.ERROR;
640+
} else {
641+
if (DEBUG)
642+
Log.i(WRAP_TAG, "[" + hashCode() + "] Stop called from JS binder");
643+
}
644+
645+
return mTextToSpeech.stop();
646+
}
553647
}
554648

555649
@SuppressWarnings("unused")
556650
protected void shutdown() {
557-
mTextToSpeech.shutdown();
651+
synchronized (mTextToSpeech) {
652+
if (!mReady) {
653+
if (DEBUG)
654+
Log.w(WRAP_TAG, "[" + hashCode() + "] Called shutdown before initialize");
655+
} else {
656+
if (DEBUG)
657+
Log.i(WRAP_TAG, "[" + hashCode() + "] Shutting down text-to-speech from "
658+
+ "thread " + Thread.currentThread().getId() + "...");
659+
}
660+
mShutdown = true;
661+
mReady = false;
662+
mTextToSpeech.shutdown();
663+
}
558664
}
665+
666+
private final OnInitListener mInitListener = new OnInitListener() {
667+
@Override
668+
public void onInit(int status) {
669+
synchronized (mTextToSpeech) {
670+
if (!mShutdown && (status == TextToSpeech.SUCCESS)) {
671+
if (DEBUG)
672+
Log.d(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
673+
+ "] Initialized successfully");
674+
mReady = true;
675+
} else {
676+
if (DEBUG)
677+
Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
678+
+ "] Failed to initialize");
679+
mReady = false;
680+
}
681+
}
682+
}
683+
};
684+
685+
private final UtteranceProgressListener mErrorListener = new UtteranceProgressListener() {
686+
@Override
687+
public void onStart(String utteranceId) {
688+
// Do nothing.
689+
}
690+
691+
@Override
692+
public void onError(String utteranceId) {
693+
if (DEBUG)
694+
Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
695+
+ "] Failed to speak utterance");
696+
}
697+
698+
@Override
699+
public void onDone(String utteranceId) {
700+
// Do nothing.
701+
}
702+
};
559703
}
560704

561705
/**
@@ -625,6 +769,8 @@ private void clearResultLocked() {
625769
* @return Whether the result was received.
626770
*/
627771
private boolean waitForResultTimedLocked(int resultId) {
772+
if (DEBUG)
773+
Log.d(TAG, "Waiting for CVOX result...");
628774
long waitTimeMillis = RESULT_TIMEOUT;
629775
final long startTimeMillis = SystemClock.uptimeMillis();
630776
while (true) {
@@ -642,6 +788,8 @@ private boolean waitForResultTimedLocked(int resultId) {
642788
}
643789
mResultLock.wait(waitTimeMillis);
644790
} catch (InterruptedException ie) {
791+
if (DEBUG)
792+
Log.w(TAG, "Timed out while waiting for CVOX result");
645793
/* ignore */
646794
}
647795
}

0 commit comments

Comments
 (0)