Skip to content

Commit df31369

Browse files
committed
Fix WebView accessibility scripts.
Ensures A11y APIs are added to WebView before page loads. Prevents calls to TTS before init and after shutdown. Ensures calls to TTS are thread- safe. Activates and deactivates feedback from AndroidVox when view is attached and detached. Adds a flag for logging debug output, as well as a lot of debug output. Bug: 7326781 Change-Id: I5d4ab7f9fb1669f98ef05ae207e897565d3086c9
1 parent 2514456 commit df31369

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)