Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df313699 authored by alanv's avatar alanv
Browse files

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
parent 25144566
Loading
Loading
Loading
Loading
+159 −11
Original line number Diff line number Diff line
@@ -21,6 +21,10 @@ import android.os.Bundle;
import android.os.SystemClock;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.Engine;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
@@ -44,6 +48,10 @@ import java.util.concurrent.atomic.AtomicInteger;
 * APIs.
 */
class AccessibilityInjector {
    private static final String TAG = AccessibilityInjector.class.getSimpleName();

    private static boolean DEBUG = false;

    // The WebViewClassic this injector is responsible for managing.
    private final WebViewClassic mWebViewClassic;

@@ -90,6 +98,10 @@ class AccessibilityInjector {
    private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE =
            "cvox.AndroidVox.performAction('%1s')";

    // JS code used to shut down an active AndroidVox instance.
    private static final String TOGGLE_CVOX_TEMPLATE =
            "javascript:(function() { cvox.ChromeVox.host.activateOrDeactivateChromeVox(%b); })();";

    /**
     * Creates an instance of the AccessibilityInjector based on
     * {@code webViewClassic}.
@@ -117,6 +129,7 @@ class AccessibilityInjector {

        addTtsApis();
        addCallbackApis();
        toggleAndroidVox(true);
    }

    /**
@@ -126,10 +139,20 @@ class AccessibilityInjector {
     * </p>
     */
    public void removeAccessibilityApisIfNecessary() {
        toggleAndroidVox(false);
        removeTtsApis();
        removeCallbackApis();
    }

    private void toggleAndroidVox(boolean state) {
        if (!mAccessibilityScriptInjected) {
            return;
        }

        final String code = String.format(TOGGLE_CVOX_TEMPLATE, state);
        mWebView.loadUrl(code);
    }

    /**
     * Initializes an {@link AccessibilityNodeInfo} with the actions and
     * movement granularity levels supported by this
@@ -262,6 +285,9 @@ class AccessibilityInjector {
     */
    public void onPageStarted(String url) {
        mAccessibilityScriptInjected = false;
        if (DEBUG)
            Log.w(TAG, "[" + mWebView.hashCode() + "] Started loading new page");
        addAccessibilityApisIfNecessary();
    }

    /**
@@ -282,15 +308,23 @@ class AccessibilityInjector {
        if (!shouldInjectJavaScript(url)) {
            mAccessibilityScriptInjected = false;
            toggleFallbackAccessibilityInjector(true);
            if (DEBUG)
                Log.d(TAG, "[" + mWebView.hashCode() + "] Using fallback accessibility support");
            return;
        }

        toggleFallbackAccessibilityInjector(false);

        if (!mAccessibilityScriptInjected) {
            mAccessibilityScriptInjected = true;
            final String injectionUrl = getScreenReaderInjectionUrl();
            mWebView.loadUrl(injectionUrl);

        mAccessibilityScriptInjected = true;
            if (DEBUG)
                Log.d(TAG, "[" + mWebView.hashCode() + "] Loading screen reader into WebView");
        } else {
            if (DEBUG)
                Log.w(TAG, "[" + mWebView.hashCode() + "] Attempted to inject screen reader twice");
        }
    }

    /**
@@ -368,6 +402,8 @@ class AccessibilityInjector {
        if (mTextToSpeech != null) {
            return;
        }
        if (DEBUG)
            Log.d(TAG, "[" + mWebView.hashCode() + "] Adding TTS APIs into WebView");
        mTextToSpeech = new TextToSpeechWrapper(mContext);
        mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE);
    }
@@ -381,6 +417,8 @@ class AccessibilityInjector {
            return;
        }

        if (DEBUG)
            Log.d(TAG, "[" + mWebView.hashCode() + "] Removing TTS APIs from WebView");
        mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE);
        mTextToSpeech.stop();
        mTextToSpeech.shutdown();
@@ -527,37 +565,143 @@ class AccessibilityInjector {
     * Used to protect the TextToSpeech class, only exposing the methods we want to expose.
     */
    private static class TextToSpeechWrapper {
        private TextToSpeech mTextToSpeech;
        private static final String WRAP_TAG = TextToSpeechWrapper.class.getSimpleName();

        private final HashMap<String, String> mTtsParams;
        private final TextToSpeech mTextToSpeech;

        /**
         * Whether this wrapper is ready to speak. If this is {@code true} then
         * {@link #mShutdown} is guaranteed to be {@code false}.
         */
        private volatile boolean mReady;

        /**
         * Whether this wrapper was shut down. If this is {@code true} then
         * {@link #mReady} is guaranteed to be {@code false}.
         */
        private volatile boolean mShutdown;

        public TextToSpeechWrapper(Context context) {
            if (DEBUG)
                Log.d(WRAP_TAG, "[" + hashCode() + "] Initializing text-to-speech on thread "
                        + Thread.currentThread().getId() + "...");

            final String pkgName = context.getPackageName();
            mTextToSpeech = new TextToSpeech(context, null, null, pkgName + ".**webview**", true);

            mReady = false;
            mShutdown = false;

            mTtsParams = new HashMap<String, String>();
            mTtsParams.put(Engine.KEY_PARAM_UTTERANCE_ID, WRAP_TAG);

            mTextToSpeech = new TextToSpeech(
                    context, mInitListener, null, pkgName + ".**webview**", true);
            mTextToSpeech.setOnUtteranceProgressListener(mErrorListener);
        }

        @JavascriptInterface
        @SuppressWarnings("unused")
        public boolean isSpeaking() {
            synchronized (mTextToSpeech) {
                if (!mReady) {
                    return false;
                }

                return mTextToSpeech.isSpeaking();
            }
        }

        @JavascriptInterface
        @SuppressWarnings("unused")
        public int speak(String text, int queueMode, HashMap<String, String> params) {
            synchronized (mTextToSpeech) {
                if (!mReady) {
                    if (DEBUG)
                        Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to speak before TTS init");
                    return TextToSpeech.ERROR;
                } else {
                    if (DEBUG)
                        Log.i(WRAP_TAG, "[" + hashCode() + "] Speak called from JS binder");
                }

                return mTextToSpeech.speak(text, queueMode, params);
            }
        }

        @JavascriptInterface
        @SuppressWarnings("unused")
        public int stop() {
            synchronized (mTextToSpeech) {
                if (!mReady) {
                    if (DEBUG)
                        Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to stop before initialize");
                    return TextToSpeech.ERROR;
                } else {
                    if (DEBUG)
                        Log.i(WRAP_TAG, "[" + hashCode() + "] Stop called from JS binder");
                }

                return mTextToSpeech.stop();
            }
        }

        @SuppressWarnings("unused")
        protected void shutdown() {
            synchronized (mTextToSpeech) {
                if (!mReady) {
                    if (DEBUG)
                        Log.w(WRAP_TAG, "[" + hashCode() + "] Called shutdown before initialize");
                } else {
                    if (DEBUG)
                        Log.i(WRAP_TAG, "[" + hashCode() + "] Shutting down text-to-speech from "
                                + "thread " + Thread.currentThread().getId() + "...");
                }
                mShutdown = true;
                mReady = false;
                mTextToSpeech.shutdown();
            }
        }

        private final OnInitListener mInitListener = new OnInitListener() {
            @Override
            public void onInit(int status) {
                synchronized (mTextToSpeech) {
                    if (!mShutdown && (status == TextToSpeech.SUCCESS)) {
                        if (DEBUG)
                            Log.d(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
                                    + "] Initialized successfully");
                        mReady = true;
                    } else {
                        if (DEBUG)
                            Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
                                    + "] Failed to initialize");
                        mReady = false;
                    }
                }
            }
        };

        private final UtteranceProgressListener mErrorListener = new UtteranceProgressListener() {
            @Override
            public void onStart(String utteranceId) {
                // Do nothing.
            }

            @Override
            public void onError(String utteranceId) {
                if (DEBUG)
                    Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode()
                            + "] Failed to speak utterance");
            }

            @Override
            public void onDone(String utteranceId) {
                // Do nothing.
            }
        };
    }

    /**
     * Exposes result interface to JavaScript.
     */
@@ -625,6 +769,8 @@ class AccessibilityInjector {
         * @return Whether the result was received.
         */
        private boolean waitForResultTimedLocked(int resultId) {
            if (DEBUG)
                Log.d(TAG, "Waiting for CVOX result...");
            long waitTimeMillis = RESULT_TIMEOUT;
            final long startTimeMillis = SystemClock.uptimeMillis();
            while (true) {
@@ -642,6 +788,8 @@ class AccessibilityInjector {
                    }
                    mResultLock.wait(waitTimeMillis);
                } catch (InterruptedException ie) {
                    if (DEBUG)
                        Log.w(TAG, "Timed out while waiting for CVOX result");
                    /* ignore */
                }
            }