2121import android .os .SystemClock ;
2222import android .provider .Settings ;
2323import 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 ;
2428import android .view .KeyEvent ;
2529import android .view .View ;
2630import android .view .accessibility .AccessibilityManager ;
4448 * APIs.
4549 */
4650class 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