1818
1919import android .content .Context ;
2020import android .os .Bundle ;
21+ import android .os .Handler ;
2122import android .os .SystemClock ;
2223import android .provider .Settings ;
2324import android .speech .tts .TextToSpeech ;
@@ -159,7 +160,7 @@ public void toggleAccessibilityFeedback(boolean enabled) {
159160 * <p>
160161 * This should only be called before a page loads.
161162 */
162- private void addAccessibilityApisIfNecessary () {
163+ public void addAccessibilityApisIfNecessary () {
163164 if (!isAccessibilityEnabled () || !isJavaScriptEnabled ()) {
164165 return ;
165166 }
@@ -333,8 +334,9 @@ public void handleSelectionChangedIfNecessary(String selectionString) {
333334 */
334335 public void onPageStarted (String url ) {
335336 mAccessibilityScriptInjected = false ;
336- if (DEBUG )
337+ if (DEBUG ) {
337338 Log .w (TAG , "[" + mWebView .hashCode () + "] Started loading new page" );
339+ }
338340 addAccessibilityApisIfNecessary ();
339341 }
340342
@@ -348,30 +350,57 @@ public void onPageStarted(String url) {
348350 */
349351 public void onPageFinished (String url ) {
350352 if (!isAccessibilityEnabled ()) {
351- mAccessibilityScriptInjected = false ;
352353 toggleFallbackAccessibilityInjector (false );
353354 return ;
354355 }
355356
356- if (!shouldInjectJavaScript (url )) {
357- mAccessibilityScriptInjected = false ;
358- toggleFallbackAccessibilityInjector (true );
359- if (DEBUG )
360- Log .d (TAG , "[" + mWebView .hashCode () + "] Using fallback accessibility support" );
361- return ;
357+ toggleFallbackAccessibilityInjector (true );
358+
359+ if (shouldInjectJavaScript (url )) {
360+ // If we're supposed to use the JS screen reader, request a
361+ // callback to confirm that CallbackHandler is working.
362+ if (DEBUG ) {
363+ Log .d (TAG , "[" + mWebView .hashCode () + "] Request callback " );
364+ }
365+
366+ mCallback .requestCallback (mWebView , mInjectScriptRunnable );
367+ }
368+ }
369+
370+ /**
371+ * Runnable used to inject the JavaScript-based screen reader if the
372+ * {@link CallbackHandler} API was successfully exposed to JavaScript.
373+ */
374+ private Runnable mInjectScriptRunnable = new Runnable () {
375+ @ Override
376+ public void run () {
377+ if (DEBUG ) {
378+ Log .d (TAG , "[" + mWebView .hashCode () + "] Received callback" );
379+ }
380+
381+ injectJavaScript ();
362382 }
383+ };
363384
385+ /**
386+ * Called by {@link #mInjectScriptRunnable} to inject the JavaScript-based
387+ * screen reader after confirming that the {@link CallbackHandler} API is
388+ * functional.
389+ */
390+ private void injectJavaScript () {
364391 toggleFallbackAccessibilityInjector (false );
365392
366393 if (!mAccessibilityScriptInjected ) {
367394 mAccessibilityScriptInjected = true ;
368395 final String injectionUrl = getScreenReaderInjectionUrl ();
369396 mWebView .loadUrl (injectionUrl );
370- if (DEBUG )
397+ if (DEBUG ) {
371398 Log .d (TAG , "[" + mWebView .hashCode () + "] Loading screen reader into WebView" );
399+ }
372400 } else {
373- if (DEBUG )
401+ if (DEBUG ) {
374402 Log .w (TAG , "[" + mWebView .hashCode () + "] Attempted to inject screen reader twice" );
403+ }
375404 }
376405 }
377406
@@ -447,12 +476,10 @@ private boolean isScriptInjectionEnabled() {
447476 * been done.
448477 */
449478 private void addTtsApis () {
450- if (mTextToSpeech ! = null ) {
451- return ;
479+ if (mTextToSpeech = = null ) {
480+ mTextToSpeech = new TextToSpeechWrapper ( mContext ) ;
452481 }
453- if (DEBUG )
454- Log .d (TAG , "[" + mWebView .hashCode () + "] Adding TTS APIs into WebView" );
455- mTextToSpeech = new TextToSpeechWrapper (mContext );
482+
456483 mWebView .addJavascriptInterface (mTextToSpeech , ALIAS_TTS_JS_INTERFACE );
457484 }
458485
@@ -461,34 +488,29 @@ private void addTtsApis() {
461488 * already been done.
462489 */
463490 private void removeTtsApis () {
464- if (mTextToSpeech == null ) {
465- return ;
491+ if (mTextToSpeech != null ) {
492+ mTextToSpeech .stop ();
493+ mTextToSpeech .shutdown ();
494+ mTextToSpeech = null ;
466495 }
467496
468- if (DEBUG )
469- Log .d (TAG , "[" + mWebView .hashCode () + "] Removing TTS APIs from WebView" );
470497 mWebView .removeJavascriptInterface (ALIAS_TTS_JS_INTERFACE );
471- mTextToSpeech .stop ();
472- mTextToSpeech .shutdown ();
473- mTextToSpeech = null ;
474498 }
475499
476500 private void addCallbackApis () {
477- if (mCallback ! = null ) {
478- return ;
501+ if (mCallback = = null ) {
502+ mCallback = new CallbackHandler ( ALIAS_TRAVERSAL_JS_INTERFACE ) ;
479503 }
480504
481- mCallback = new CallbackHandler (ALIAS_TRAVERSAL_JS_INTERFACE );
482505 mWebView .addJavascriptInterface (mCallback , ALIAS_TRAVERSAL_JS_INTERFACE );
483506 }
484507
485508 private void removeCallbackApis () {
486- if (mCallback = = null ) {
487- return ;
509+ if (mCallback ! = null ) {
510+ mCallback = null ;
488511 }
489512
490513 mWebView .removeJavascriptInterface (ALIAS_TRAVERSAL_JS_INTERFACE );
491- mCallback = null ;
492514 }
493515
494516 /**
@@ -638,9 +660,10 @@ private static class TextToSpeechWrapper {
638660 private volatile boolean mShutdown ;
639661
640662 public TextToSpeechWrapper (Context context ) {
641- if (DEBUG )
663+ if (DEBUG ) {
642664 Log .d (WRAP_TAG , "[" + hashCode () + "] Initializing text-to-speech on thread "
643665 + Thread .currentThread ().getId () + "..." );
666+ }
644667
645668 final String pkgName = context .getPackageName ();
646669
@@ -672,12 +695,14 @@ public boolean isSpeaking() {
672695 public int speak (String text , int queueMode , HashMap <String , String > params ) {
673696 synchronized (mTextToSpeech ) {
674697 if (!mReady ) {
675- if (DEBUG )
698+ if (DEBUG ) {
676699 Log .w (WRAP_TAG , "[" + hashCode () + "] Attempted to speak before TTS init" );
700+ }
677701 return TextToSpeech .ERROR ;
678702 } else {
679- if (DEBUG )
703+ if (DEBUG ) {
680704 Log .i (WRAP_TAG , "[" + hashCode () + "] Speak called from JS binder" );
705+ }
681706 }
682707
683708 return mTextToSpeech .speak (text , queueMode , params );
@@ -689,12 +714,14 @@ public int speak(String text, int queueMode, HashMap<String, String> params) {
689714 public int stop () {
690715 synchronized (mTextToSpeech ) {
691716 if (!mReady ) {
692- if (DEBUG )
717+ if (DEBUG ) {
693718 Log .w (WRAP_TAG , "[" + hashCode () + "] Attempted to stop before initialize" );
719+ }
694720 return TextToSpeech .ERROR ;
695721 } else {
696- if (DEBUG )
722+ if (DEBUG ) {
697723 Log .i (WRAP_TAG , "[" + hashCode () + "] Stop called from JS binder" );
724+ }
698725 }
699726
700727 return mTextToSpeech .stop ();
@@ -705,12 +732,14 @@ public int stop() {
705732 protected void shutdown () {
706733 synchronized (mTextToSpeech ) {
707734 if (!mReady ) {
708- if (DEBUG )
735+ if (DEBUG ) {
709736 Log .w (WRAP_TAG , "[" + hashCode () + "] Called shutdown before initialize" );
737+ }
710738 } else {
711- if (DEBUG )
739+ if (DEBUG ) {
712740 Log .i (WRAP_TAG , "[" + hashCode () + "] Shutting down text-to-speech from "
713741 + "thread " + Thread .currentThread ().getId () + "..." );
742+ }
714743 }
715744 mShutdown = true ;
716745 mReady = false ;
@@ -723,14 +752,16 @@ protected void shutdown() {
723752 public void onInit (int status ) {
724753 synchronized (mTextToSpeech ) {
725754 if (!mShutdown && (status == TextToSpeech .SUCCESS )) {
726- if (DEBUG )
755+ if (DEBUG ) {
727756 Log .d (WRAP_TAG , "[" + TextToSpeechWrapper .this .hashCode ()
728757 + "] Initialized successfully" );
758+ }
729759 mReady = true ;
730760 } else {
731- if (DEBUG )
761+ if (DEBUG ) {
732762 Log .w (WRAP_TAG , "[" + TextToSpeechWrapper .this .hashCode ()
733763 + "] Failed to initialize" );
764+ }
734765 mReady = false ;
735766 }
736767 }
@@ -745,9 +776,10 @@ public void onStart(String utteranceId) {
745776
746777 @ Override
747778 public void onError (String utteranceId ) {
748- if (DEBUG )
779+ if (DEBUG ) {
749780 Log .w (WRAP_TAG , "[" + TextToSpeechWrapper .this .hashCode ()
750781 + "] Failed to speak utterance" );
782+ }
751783 }
752784
753785 @ Override
@@ -770,12 +802,16 @@ private static class CallbackHandler {
770802 private final AtomicInteger mResultIdCounter = new AtomicInteger ();
771803 private final Object mResultLock = new Object ();
772804 private final String mInterfaceName ;
805+ private final Handler mMainHandler ;
806+
807+ private Runnable mCallbackRunnable ;
773808
774809 private boolean mResult = false ;
775810 private int mResultId = -1 ;
776811
777812 private CallbackHandler (String interfaceName ) {
778813 mInterfaceName = interfaceName ;
814+ mMainHandler = new Handler ();
779815 }
780816
781817 /**
@@ -826,44 +862,51 @@ private void clearResultLocked() {
826862 private boolean waitForResultTimedLocked (int resultId ) {
827863 final long startTimeMillis = SystemClock .uptimeMillis ();
828864
829- if (DEBUG )
865+ if (DEBUG ) {
830866 Log .d (TAG , "Waiting for CVOX result with ID " + resultId + "..." );
867+ }
831868
832869 while (true ) {
833870 // Fail if we received a callback from the future.
834871 if (mResultId > resultId ) {
835- if (DEBUG )
872+ if (DEBUG ) {
836873 Log .w (TAG , "Aborted CVOX result" );
874+ }
837875 return false ;
838876 }
839877
840878 final long elapsedTimeMillis = (SystemClock .uptimeMillis () - startTimeMillis );
841879
842880 // Succeed if we received the callback we were expecting.
843- if (DEBUG )
881+ if (DEBUG ) {
844882 Log .w (TAG , "Check " + mResultId + " versus expected " + resultId );
883+ }
845884 if (mResultId == resultId ) {
846- if (DEBUG )
885+ if (DEBUG ) {
847886 Log .w (TAG , "Received CVOX result after " + elapsedTimeMillis + " ms" );
887+ }
848888 return true ;
849889 }
850890
851891 final long waitTimeMillis = (RESULT_TIMEOUT - elapsedTimeMillis );
852892
853893 // Fail if we've already exceeded the timeout.
854894 if (waitTimeMillis <= 0 ) {
855- if (DEBUG )
895+ if (DEBUG ) {
856896 Log .w (TAG , "Timed out while waiting for CVOX result" );
897+ }
857898 return false ;
858899 }
859900
860901 try {
861- if (DEBUG )
902+ if (DEBUG ) {
862903 Log .w (TAG , "Start waiting..." );
904+ }
863905 mResultLock .wait (waitTimeMillis );
864906 } catch (InterruptedException ie ) {
865- if (DEBUG )
907+ if (DEBUG ) {
866908 Log .w (TAG , "Interrupted while waiting for CVOX result" );
909+ }
867910 }
868911 }
869912 }
@@ -878,8 +921,9 @@ private boolean waitForResultTimedLocked(int resultId) {
878921 @ JavascriptInterface
879922 @ SuppressWarnings ("unused" )
880923 public void onResult (String id , String result ) {
881- if (DEBUG )
924+ if (DEBUG ) {
882925 Log .w (TAG , "Saw CVOX result of '" + result + "' for ID " + id );
926+ }
883927 final int resultId ;
884928
885929 try {
@@ -893,11 +937,34 @@ public void onResult(String id, String result) {
893937 mResult = Boolean .parseBoolean (result );
894938 mResultId = resultId ;
895939 } else {
896- if (DEBUG )
940+ if (DEBUG ) {
897941 Log .w (TAG , "Result with ID " + resultId + " was stale vesus " + mResultId );
942+ }
898943 }
899944 mResultLock .notifyAll ();
900945 }
901946 }
947+
948+ /**
949+ * Requests a callback to ensure that the JavaScript interface for this
950+ * object has been added successfully.
951+ *
952+ * @param webView The web view to request a callback from.
953+ * @param callbackRunnable Runnable to execute if a callback is received.
954+ */
955+ public void requestCallback (WebView webView , Runnable callbackRunnable ) {
956+ mCallbackRunnable = callbackRunnable ;
957+
958+ webView .loadUrl ("javascript:(function() { " + mInterfaceName + ".callback(); })();" );
959+ }
960+
961+ @ JavascriptInterface
962+ @ SuppressWarnings ("unused" )
963+ public void callback () {
964+ if (mCallbackRunnable != null ) {
965+ mMainHandler .post (mCallbackRunnable );
966+ mCallbackRunnable = null ;
967+ }
968+ }
902969 }
903970}
0 commit comments