Skip to content

Commit 992ceef

Browse files
committed
Fix accessibility API injection.
Also adds a heartbeat check so that if something does go wrong while adding the JavaScript APIs, we don't attempt to use the JavaScript-based screen reader. Bug: 7450237 Change-Id: Ifbce77cf93115f658386c520b8226941607b2afe
1 parent 1f177cb commit 992ceef

File tree

2 files changed

+118
-48
lines changed

2 files changed

+118
-48
lines changed

core/java/android/webkit/AccessibilityInjector.java

Lines changed: 115 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import android.content.Context;
2020
import android.os.Bundle;
21+
import android.os.Handler;
2122
import android.os.SystemClock;
2223
import android.provider.Settings;
2324
import 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
}

core/java/android/webkit/WebViewClassic.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,6 +2500,9 @@ public WebBackForwardList restoreState(Bundle inState) {
25002500
// Remove all pending messages because we are restoring previous
25012501
// state.
25022502
mWebViewCore.removeMessages();
2503+
if (isAccessibilityInjectionEnabled()) {
2504+
getAccessibilityInjector().addAccessibilityApisIfNecessary();
2505+
}
25032506
// Send a restore state message.
25042507
mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index);
25052508
}

0 commit comments

Comments
 (0)