Loading core/java/android/webkit/AccessibilityInjector.java +115 −48 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.webkit; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.provider.Settings; import android.speech.tts.TextToSpeech; Loading Loading @@ -159,7 +160,7 @@ class AccessibilityInjector { * <p> * This should only be called before a page loads. */ private void addAccessibilityApisIfNecessary() { public void addAccessibilityApisIfNecessary() { if (!isAccessibilityEnabled() || !isJavaScriptEnabled()) { return; } Loading Loading @@ -333,8 +334,9 @@ class AccessibilityInjector { */ public void onPageStarted(String url) { mAccessibilityScriptInjected = false; if (DEBUG) if (DEBUG) { Log.w(TAG, "[" + mWebView.hashCode() + "] Started loading new page"); } addAccessibilityApisIfNecessary(); } Loading @@ -348,32 +350,59 @@ class AccessibilityInjector { */ public void onPageFinished(String url) { if (!isAccessibilityEnabled()) { mAccessibilityScriptInjected = false; toggleFallbackAccessibilityInjector(false); return; } if (!shouldInjectJavaScript(url)) { mAccessibilityScriptInjected = false; toggleFallbackAccessibilityInjector(true); if (DEBUG) Log.d(TAG, "[" + mWebView.hashCode() + "] Using fallback accessibility support"); return; if (shouldInjectJavaScript(url)) { // If we're supposed to use the JS screen reader, request a // callback to confirm that CallbackHandler is working. if (DEBUG) { Log.d(TAG, "[" + mWebView.hashCode() + "] Request callback "); } mCallback.requestCallback(mWebView, mInjectScriptRunnable); } } /** * Runnable used to inject the JavaScript-based screen reader if the * {@link CallbackHandler} API was successfully exposed to JavaScript. */ private Runnable mInjectScriptRunnable = new Runnable() { @Override public void run() { if (DEBUG) { Log.d(TAG, "[" + mWebView.hashCode() + "] Received callback"); } injectJavaScript(); } }; /** * Called by {@link #mInjectScriptRunnable} to inject the JavaScript-based * screen reader after confirming that the {@link CallbackHandler} API is * functional. */ private void injectJavaScript() { toggleFallbackAccessibilityInjector(false); if (!mAccessibilityScriptInjected) { mAccessibilityScriptInjected = true; final String injectionUrl = getScreenReaderInjectionUrl(); mWebView.loadUrl(injectionUrl); if (DEBUG) if (DEBUG) { Log.d(TAG, "[" + mWebView.hashCode() + "] Loading screen reader into WebView"); } } else { if (DEBUG) if (DEBUG) { Log.w(TAG, "[" + mWebView.hashCode() + "] Attempted to inject screen reader twice"); } } } /** * Adjusts the accessibility injection state to reflect changes in the Loading Loading @@ -447,12 +476,10 @@ class AccessibilityInjector { * been done. */ private void addTtsApis() { if (mTextToSpeech != null) { return; } if (DEBUG) Log.d(TAG, "[" + mWebView.hashCode() + "] Adding TTS APIs into WebView"); if (mTextToSpeech == null) { mTextToSpeech = new TextToSpeechWrapper(mContext); } mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE); } Loading @@ -461,34 +488,29 @@ class AccessibilityInjector { * already been done. */ private void removeTtsApis() { if (mTextToSpeech == null) { return; } if (DEBUG) Log.d(TAG, "[" + mWebView.hashCode() + "] Removing TTS APIs from WebView"); mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE); if (mTextToSpeech != null) { mTextToSpeech.stop(); mTextToSpeech.shutdown(); mTextToSpeech = null; } private void addCallbackApis() { if (mCallback != null) { return; mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE); } private void addCallbackApis() { if (mCallback == null) { mCallback = new CallbackHandler(ALIAS_TRAVERSAL_JS_INTERFACE); } mWebView.addJavascriptInterface(mCallback, ALIAS_TRAVERSAL_JS_INTERFACE); } private void removeCallbackApis() { if (mCallback == null) { return; if (mCallback != null) { mCallback = null; } mWebView.removeJavascriptInterface(ALIAS_TRAVERSAL_JS_INTERFACE); mCallback = null; } /** Loading Loading @@ -638,9 +660,10 @@ class AccessibilityInjector { private volatile boolean mShutdown; public TextToSpeechWrapper(Context context) { if (DEBUG) if (DEBUG) { Log.d(WRAP_TAG, "[" + hashCode() + "] Initializing text-to-speech on thread " + Thread.currentThread().getId() + "..."); } final String pkgName = context.getPackageName(); Loading Loading @@ -672,13 +695,15 @@ class AccessibilityInjector { public int speak(String text, int queueMode, HashMap<String, String> params) { synchronized (mTextToSpeech) { if (!mReady) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to speak before TTS init"); } return TextToSpeech.ERROR; } else { if (DEBUG) if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Speak called from JS binder"); } } return mTextToSpeech.speak(text, queueMode, params); } Loading @@ -689,13 +714,15 @@ class AccessibilityInjector { public int stop() { synchronized (mTextToSpeech) { if (!mReady) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to stop before initialize"); } return TextToSpeech.ERROR; } else { if (DEBUG) if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Stop called from JS binder"); } } return mTextToSpeech.stop(); } Loading @@ -705,13 +732,15 @@ class AccessibilityInjector { protected void shutdown() { synchronized (mTextToSpeech) { if (!mReady) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Called shutdown before initialize"); } } else { if (DEBUG) if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Shutting down text-to-speech from " + "thread " + Thread.currentThread().getId() + "..."); } } mShutdown = true; mReady = false; mTextToSpeech.shutdown(); Loading @@ -723,14 +752,16 @@ class AccessibilityInjector { public void onInit(int status) { synchronized (mTextToSpeech) { if (!mShutdown && (status == TextToSpeech.SUCCESS)) { if (DEBUG) if (DEBUG) { Log.d(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Initialized successfully"); } mReady = true; } else { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Failed to initialize"); } mReady = false; } } Loading @@ -745,10 +776,11 @@ class AccessibilityInjector { @Override public void onError(String utteranceId) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Failed to speak utterance"); } } @Override public void onDone(String utteranceId) { Loading @@ -770,12 +802,16 @@ class AccessibilityInjector { private final AtomicInteger mResultIdCounter = new AtomicInteger(); private final Object mResultLock = new Object(); private final String mInterfaceName; private final Handler mMainHandler; private Runnable mCallbackRunnable; private boolean mResult = false; private int mResultId = -1; private CallbackHandler(String interfaceName) { mInterfaceName = interfaceName; mMainHandler = new Handler(); } /** Loading Loading @@ -826,25 +862,29 @@ class AccessibilityInjector { private boolean waitForResultTimedLocked(int resultId) { final long startTimeMillis = SystemClock.uptimeMillis(); if (DEBUG) if (DEBUG) { Log.d(TAG, "Waiting for CVOX result with ID " + resultId + "..."); } while (true) { // Fail if we received a callback from the future. if (mResultId > resultId) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Aborted CVOX result"); } return false; } final long elapsedTimeMillis = (SystemClock.uptimeMillis() - startTimeMillis); // Succeed if we received the callback we were expecting. if (DEBUG) if (DEBUG) { Log.w(TAG, "Check " + mResultId + " versus expected " + resultId); } if (mResultId == resultId) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Received CVOX result after " + elapsedTimeMillis + " ms"); } return true; } Loading @@ -852,21 +892,24 @@ class AccessibilityInjector { // Fail if we've already exceeded the timeout. if (waitTimeMillis <= 0) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Timed out while waiting for CVOX result"); } return false; } try { if (DEBUG) if (DEBUG) { Log.w(TAG, "Start waiting..."); } mResultLock.wait(waitTimeMillis); } catch (InterruptedException ie) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Interrupted while waiting for CVOX result"); } } } } /** * Callback exposed to JavaScript. Handles returning the result of a Loading @@ -878,8 +921,9 @@ class AccessibilityInjector { @JavascriptInterface @SuppressWarnings("unused") public void onResult(String id, String result) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Saw CVOX result of '" + result + "' for ID " + id); } final int resultId; try { Loading @@ -893,11 +937,34 @@ class AccessibilityInjector { mResult = Boolean.parseBoolean(result); mResultId = resultId; } else { if (DEBUG) if (DEBUG) { Log.w(TAG, "Result with ID " + resultId + " was stale vesus " + mResultId); } } mResultLock.notifyAll(); } } /** * Requests a callback to ensure that the JavaScript interface for this * object has been added successfully. * * @param webView The web view to request a callback from. * @param callbackRunnable Runnable to execute if a callback is received. */ public void requestCallback(WebView webView, Runnable callbackRunnable) { mCallbackRunnable = callbackRunnable; webView.loadUrl("javascript:(function() { " + mInterfaceName + ".callback(); })();"); } @JavascriptInterface @SuppressWarnings("unused") public void callback() { if (mCallbackRunnable != null) { mMainHandler.post(mCallbackRunnable); mCallbackRunnable = null; } } } } core/java/android/webkit/WebViewClassic.java +3 −0 Original line number Diff line number Diff line Loading @@ -2500,6 +2500,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // Remove all pending messages because we are restoring previous // state. mWebViewCore.removeMessages(); if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().addAccessibilityApisIfNecessary(); } // Send a restore state message. mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index); } Loading Loading
core/java/android/webkit/AccessibilityInjector.java +115 −48 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.webkit; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.provider.Settings; import android.speech.tts.TextToSpeech; Loading Loading @@ -159,7 +160,7 @@ class AccessibilityInjector { * <p> * This should only be called before a page loads. */ private void addAccessibilityApisIfNecessary() { public void addAccessibilityApisIfNecessary() { if (!isAccessibilityEnabled() || !isJavaScriptEnabled()) { return; } Loading Loading @@ -333,8 +334,9 @@ class AccessibilityInjector { */ public void onPageStarted(String url) { mAccessibilityScriptInjected = false; if (DEBUG) if (DEBUG) { Log.w(TAG, "[" + mWebView.hashCode() + "] Started loading new page"); } addAccessibilityApisIfNecessary(); } Loading @@ -348,32 +350,59 @@ class AccessibilityInjector { */ public void onPageFinished(String url) { if (!isAccessibilityEnabled()) { mAccessibilityScriptInjected = false; toggleFallbackAccessibilityInjector(false); return; } if (!shouldInjectJavaScript(url)) { mAccessibilityScriptInjected = false; toggleFallbackAccessibilityInjector(true); if (DEBUG) Log.d(TAG, "[" + mWebView.hashCode() + "] Using fallback accessibility support"); return; if (shouldInjectJavaScript(url)) { // If we're supposed to use the JS screen reader, request a // callback to confirm that CallbackHandler is working. if (DEBUG) { Log.d(TAG, "[" + mWebView.hashCode() + "] Request callback "); } mCallback.requestCallback(mWebView, mInjectScriptRunnable); } } /** * Runnable used to inject the JavaScript-based screen reader if the * {@link CallbackHandler} API was successfully exposed to JavaScript. */ private Runnable mInjectScriptRunnable = new Runnable() { @Override public void run() { if (DEBUG) { Log.d(TAG, "[" + mWebView.hashCode() + "] Received callback"); } injectJavaScript(); } }; /** * Called by {@link #mInjectScriptRunnable} to inject the JavaScript-based * screen reader after confirming that the {@link CallbackHandler} API is * functional. */ private void injectJavaScript() { toggleFallbackAccessibilityInjector(false); if (!mAccessibilityScriptInjected) { mAccessibilityScriptInjected = true; final String injectionUrl = getScreenReaderInjectionUrl(); mWebView.loadUrl(injectionUrl); if (DEBUG) if (DEBUG) { Log.d(TAG, "[" + mWebView.hashCode() + "] Loading screen reader into WebView"); } } else { if (DEBUG) if (DEBUG) { Log.w(TAG, "[" + mWebView.hashCode() + "] Attempted to inject screen reader twice"); } } } /** * Adjusts the accessibility injection state to reflect changes in the Loading Loading @@ -447,12 +476,10 @@ class AccessibilityInjector { * been done. */ private void addTtsApis() { if (mTextToSpeech != null) { return; } if (DEBUG) Log.d(TAG, "[" + mWebView.hashCode() + "] Adding TTS APIs into WebView"); if (mTextToSpeech == null) { mTextToSpeech = new TextToSpeechWrapper(mContext); } mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE); } Loading @@ -461,34 +488,29 @@ class AccessibilityInjector { * already been done. */ private void removeTtsApis() { if (mTextToSpeech == null) { return; } if (DEBUG) Log.d(TAG, "[" + mWebView.hashCode() + "] Removing TTS APIs from WebView"); mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE); if (mTextToSpeech != null) { mTextToSpeech.stop(); mTextToSpeech.shutdown(); mTextToSpeech = null; } private void addCallbackApis() { if (mCallback != null) { return; mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE); } private void addCallbackApis() { if (mCallback == null) { mCallback = new CallbackHandler(ALIAS_TRAVERSAL_JS_INTERFACE); } mWebView.addJavascriptInterface(mCallback, ALIAS_TRAVERSAL_JS_INTERFACE); } private void removeCallbackApis() { if (mCallback == null) { return; if (mCallback != null) { mCallback = null; } mWebView.removeJavascriptInterface(ALIAS_TRAVERSAL_JS_INTERFACE); mCallback = null; } /** Loading Loading @@ -638,9 +660,10 @@ class AccessibilityInjector { private volatile boolean mShutdown; public TextToSpeechWrapper(Context context) { if (DEBUG) if (DEBUG) { Log.d(WRAP_TAG, "[" + hashCode() + "] Initializing text-to-speech on thread " + Thread.currentThread().getId() + "..."); } final String pkgName = context.getPackageName(); Loading Loading @@ -672,13 +695,15 @@ class AccessibilityInjector { public int speak(String text, int queueMode, HashMap<String, String> params) { synchronized (mTextToSpeech) { if (!mReady) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to speak before TTS init"); } return TextToSpeech.ERROR; } else { if (DEBUG) if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Speak called from JS binder"); } } return mTextToSpeech.speak(text, queueMode, params); } Loading @@ -689,13 +714,15 @@ class AccessibilityInjector { public int stop() { synchronized (mTextToSpeech) { if (!mReady) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to stop before initialize"); } return TextToSpeech.ERROR; } else { if (DEBUG) if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Stop called from JS binder"); } } return mTextToSpeech.stop(); } Loading @@ -705,13 +732,15 @@ class AccessibilityInjector { protected void shutdown() { synchronized (mTextToSpeech) { if (!mReady) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Called shutdown before initialize"); } } else { if (DEBUG) if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Shutting down text-to-speech from " + "thread " + Thread.currentThread().getId() + "..."); } } mShutdown = true; mReady = false; mTextToSpeech.shutdown(); Loading @@ -723,14 +752,16 @@ class AccessibilityInjector { public void onInit(int status) { synchronized (mTextToSpeech) { if (!mShutdown && (status == TextToSpeech.SUCCESS)) { if (DEBUG) if (DEBUG) { Log.d(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Initialized successfully"); } mReady = true; } else { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Failed to initialize"); } mReady = false; } } Loading @@ -745,10 +776,11 @@ class AccessibilityInjector { @Override public void onError(String utteranceId) { if (DEBUG) if (DEBUG) { Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Failed to speak utterance"); } } @Override public void onDone(String utteranceId) { Loading @@ -770,12 +802,16 @@ class AccessibilityInjector { private final AtomicInteger mResultIdCounter = new AtomicInteger(); private final Object mResultLock = new Object(); private final String mInterfaceName; private final Handler mMainHandler; private Runnable mCallbackRunnable; private boolean mResult = false; private int mResultId = -1; private CallbackHandler(String interfaceName) { mInterfaceName = interfaceName; mMainHandler = new Handler(); } /** Loading Loading @@ -826,25 +862,29 @@ class AccessibilityInjector { private boolean waitForResultTimedLocked(int resultId) { final long startTimeMillis = SystemClock.uptimeMillis(); if (DEBUG) if (DEBUG) { Log.d(TAG, "Waiting for CVOX result with ID " + resultId + "..."); } while (true) { // Fail if we received a callback from the future. if (mResultId > resultId) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Aborted CVOX result"); } return false; } final long elapsedTimeMillis = (SystemClock.uptimeMillis() - startTimeMillis); // Succeed if we received the callback we were expecting. if (DEBUG) if (DEBUG) { Log.w(TAG, "Check " + mResultId + " versus expected " + resultId); } if (mResultId == resultId) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Received CVOX result after " + elapsedTimeMillis + " ms"); } return true; } Loading @@ -852,21 +892,24 @@ class AccessibilityInjector { // Fail if we've already exceeded the timeout. if (waitTimeMillis <= 0) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Timed out while waiting for CVOX result"); } return false; } try { if (DEBUG) if (DEBUG) { Log.w(TAG, "Start waiting..."); } mResultLock.wait(waitTimeMillis); } catch (InterruptedException ie) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Interrupted while waiting for CVOX result"); } } } } /** * Callback exposed to JavaScript. Handles returning the result of a Loading @@ -878,8 +921,9 @@ class AccessibilityInjector { @JavascriptInterface @SuppressWarnings("unused") public void onResult(String id, String result) { if (DEBUG) if (DEBUG) { Log.w(TAG, "Saw CVOX result of '" + result + "' for ID " + id); } final int resultId; try { Loading @@ -893,11 +937,34 @@ class AccessibilityInjector { mResult = Boolean.parseBoolean(result); mResultId = resultId; } else { if (DEBUG) if (DEBUG) { Log.w(TAG, "Result with ID " + resultId + " was stale vesus " + mResultId); } } mResultLock.notifyAll(); } } /** * Requests a callback to ensure that the JavaScript interface for this * object has been added successfully. * * @param webView The web view to request a callback from. * @param callbackRunnable Runnable to execute if a callback is received. */ public void requestCallback(WebView webView, Runnable callbackRunnable) { mCallbackRunnable = callbackRunnable; webView.loadUrl("javascript:(function() { " + mInterfaceName + ".callback(); })();"); } @JavascriptInterface @SuppressWarnings("unused") public void callback() { if (mCallbackRunnable != null) { mMainHandler.post(mCallbackRunnable); mCallbackRunnable = null; } } } }
core/java/android/webkit/WebViewClassic.java +3 −0 Original line number Diff line number Diff line Loading @@ -2500,6 +2500,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // Remove all pending messages because we are restoring previous // state. mWebViewCore.removeMessages(); if (isAccessibilityInjectionEnabled()) { getAccessibilityInjector().addAccessibilityApisIfNecessary(); } // Send a restore state message. mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index); } Loading