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

Commit 585f13f8 authored by Svetoslav Ganov's avatar Svetoslav Ganov
Browse files

Accessibility support for WebViews

Change-Id: Ibb139192bae4d60fd53a7872b19c06312bb41558
parent 569a57dc
Loading
Loading
Loading
Loading
+59 −1
Original line number Diff line number Diff line
@@ -34,7 +34,10 @@ import android.content.res.Resources;
import android.database.Cursor;
import android.database.SQLException;
import android.net.Uri;
import android.os.*;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.AndroidException;
import android.util.Config;
@@ -2526,6 +2529,60 @@ public final class Settings {
        public static final String ENABLED_ACCESSIBILITY_SERVICES =
            "enabled_accessibility_services";

        /**
         * If injection of accessibility enhancing JavaScript scripts
         * is enabled.
         * <p>
         *   Note: Accessibility injecting scripts are served by the
         *   Google infrastructure and enable users with disabilities to
         *   efficiantly navigate in and explore web content.
         * </p>
         * <p>
         *   This property represents a boolean value.
         * </p>
         * @hide
         */
        public static final String ACCESSIBILITY_SCRIPT_INJECTION =
            "accessibility_script_injection";

        /**
         * Key bindings for navigation in built-in accessibility support for web content.
         * <p>
         *   Note: These key bindings are for the built-in accessibility navigation for
         *   web content which is used as a fall back solution if JavaScript in a WebView
         *   is not enabled or the user has not opted-in script injection from Google.
         * </p>
         * <p>
         *   The bindings are separated by semi-colon. A binding is a mapping from
         *   a key to a sequence of actions (for more details look at
         *   android.webkit.AccessibilityInjector). A key is represented as the hexademical
         *   string representation of an integer obtained from a meta state (optional) shifted
         *   sixteen times left and bitwise ored with a key code. An action is represented
         *   as a hexademical string representation of an integer where the first two digits
         *   are navigation action index, the second, the third, and the fourth digit pairs
         *   represent the action arguments. The separate actions in a binding are colon
         *   separated. The key and the action sequence it maps to are separated by equals.
         * </p>
         * <p>
         *   For example, the binding below maps the DPAD right button to traverse the
         *   current navigation axis once without firing an accessibility event and to
         *   perform the same traversal again but to fire an event:
         *   <code>
         *     0x16=0x01000100:0x01000101;
         *   </code>
         * </p>
         * <p>
         *   The goal of this binding is to enable dynamic rebinding of keys to
         *   navigation actions for web content without requiring a framework change.
         * </p>
         * <p>
         *   This property represents a string value.
         * </p>
         * @hide
         */
        public static final String ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS =
            "accessibility_web_content_key_bindings";

        /**
         * Setting to always use the default text-to-speech settings regardless
         * of the application settings.
@@ -3497,6 +3554,7 @@ public final class Settings {
            PARENTAL_CONTROL_REDIRECT_URL,
            USB_MASS_STORAGE_ENABLED,
            ACCESSIBILITY_ENABLED,
            ACCESSIBILITY_SCRIPT_INJECTION,
            BACKUP_AUTO_RESTORE,
            ENABLED_ACCESSIBILITY_SERVICES,
            TTS_USE_DEFAULTS,
+414 −29
Original line number Diff line number Diff line
@@ -16,27 +16,95 @@

package android.webkit;

import android.provider.Settings;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.util.Log;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.webkit.WebViewCore.EventHub;

import java.util.ArrayList;
import java.util.Stack;

/**
 * This class injects accessibility into WebViews with disabled JavaScript or
 * WebViews with enabled JavaScript but for which we have no accessibility
 * script to inject.
 * </p>
 * Note: To avoid changes in the framework upon changing the available
 *       navigation axis, or reordering the navigation axis, or changing
 *       the key bindings, or defining sequence of actions to be bound to
 *       a given key this class is navigation axis agnostic. It is only
 *       aware of one navigation axis which is in fact the default behavior
 *       of webViews while using the DPAD/TrackBall.
 * </p>
 * In general a key binding is a mapping from meta state + key code to
 * a sequence of actions. For more detail how to specify key bindings refer to
 * {@link android.provider.Settings.Secure#ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS}.
 * </p>
 * The possible actions are invocations to
 * {@link #setCurrentAxis(int, boolean, String)}, or
 * {@link #traverseCurrentAxis(int, boolean, String)}
 * {@link #traverseGivenAxis(int, int, boolean, String)}
 * {@link #prefromAxisTransition(int, int, boolean, String)}
 * referred via the values of:
 * {@link #ACTION_SET_CURRENT_AXIS},
 * {@link #ACTION_TRAVERSE_CURRENT_AXIS},
 * {@link #ACTION_TRAVERSE_GIVEN_AXIS},
 * {@link #ACTION_PERFORM_AXIS_TRANSITION},
 * respectively.
 * The arguments for the action invocation are specified as offset
 * hexademical pairs. Note the last argument of the invocation
 * should NOT be specified in the binding as it is provided by
 * this class. For details about the key binding implementation
 * refer to {@link AccessibilityWebContentKeyBinding}.
 */
class AccessibilityInjector {
    private static final String LOG_TAG = "AccessibilityInjector";

    private static final boolean DEBUG = true;

    private static final int ACTION_SET_CURRENT_AXIS = 0;
    private static final int ACTION_TRAVERSE_CURRENT_AXIS = 1;
    private static final int ACTION_TRAVERSE_GIVEN_AXIS = 2;
    private static final int ACTION_PERFORM_AXIS_TRANSITION = 3;

    // the default WebView behavior abstracted as a navigation axis
    private static final int NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR = 7;

    // these are the same for all instances so make them process wide
    private static SparseArray<AccessibilityWebContentKeyBinding> sBindings =
        new SparseArray<AccessibilityWebContentKeyBinding>();

    // Handle to the WebView this injector is associated with.
    // handle to the WebView this injector is associated with.
    private final WebView mWebView;

    // events scheduled for sending as soon as we receive the selected text
    private final Stack<AccessibilityEvent> mScheduledEventStack = new Stack<AccessibilityEvent>();

    // the current traversal axis
    private int mCurrentAxis = 2; // sentence

    // we need to consume the up if we have handled the last down
    private boolean mLastDownEventHandled;

    // getting two empty selection strings in a row we let the WebView handle the event
    private boolean mIsLastSelectionStringNull;

    // keep track of last direction
    private int mLastDirection;

    /**
     * Creates a new injector associated with a given VwebView.
     * Creates a new injector associated with a given {@link WebView}.
     *
     * @param webView The associated WebView.
     */
    public AccessibilityInjector(WebView webView) {
        mWebView = webView;
        ensureWebContentKeyBindings();
    }

    /**
@@ -45,55 +113,372 @@ class AccessibilityInjector {
     * @return True if the event was processed.
     */
    public boolean onKeyEvent(KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_UP) {
            return mLastDownEventHandled;
        }

        // as a proof of concept let us do the simplest example
        mLastDownEventHandled = false;

        if (event.getAction() != KeyEvent.ACTION_UP) {
        int key = event.getMetaState() << AccessibilityWebContentKeyBinding.OFFSET_META_STATE |
            event.getKeyCode() << AccessibilityWebContentKeyBinding.OFFSET_KEY_CODE;

        AccessibilityWebContentKeyBinding binding = sBindings.get(key);
        if (binding == null) {
            return false;
        }

        int keyCode = event.getKeyCode();

        switch (keyCode) {
            case KeyEvent.KEYCODE_N:
                modifySelection("extend", "forward", "sentence");
        for (int i = 0, count = binding.getActionCount(); i < count; i++) {
            int actionCode = binding.getActionCode(i);
            String contentDescription = Integer.toHexString(binding.getAction(i));
            switch (actionCode) {
                case ACTION_SET_CURRENT_AXIS:
                    int axis = binding.getFirstArgument(i);
                    boolean sendEvent = (binding.getSecondArgument(i) == 1);
                    setCurrentAxis(axis, sendEvent, contentDescription);
                    mLastDownEventHandled = true;
                    break;
            case KeyEvent.KEYCODE_P:
                modifySelection("extend", "backward", "sentence");
                case ACTION_TRAVERSE_CURRENT_AXIS:
                    int direction = binding.getFirstArgument(i);
                    // on second null selection string in same direction => WebView handle the event
                    if (direction == mLastDirection && mIsLastSelectionStringNull) {
                        mLastDirection = direction;
                        mIsLastSelectionStringNull = false;
                        return false;
                    }
                    mLastDirection = direction;
                    sendEvent = (binding.getSecondArgument(i) == 1);
                    mLastDownEventHandled = traverseCurrentAxis(direction, sendEvent,
                            contentDescription);
                    break;
                case ACTION_TRAVERSE_GIVEN_AXIS:
                    direction = binding.getFirstArgument(i);
                    // on second null selection string in same direction => WebView handle the event
                    if (direction == mLastDirection && mIsLastSelectionStringNull) {
                        mLastDirection = direction;
                        mIsLastSelectionStringNull = false;
                        return false;
                    }
                    mLastDirection = direction;
                    axis =  binding.getSecondArgument(i);
                    sendEvent = (binding.getThirdArgument(i) == 1);
                    traverseGivenAxis(direction, axis, sendEvent, contentDescription);
                    mLastDownEventHandled = true;
                    break;
                case ACTION_PERFORM_AXIS_TRANSITION:
                    int fromAxis = binding.getFirstArgument(i);
                    int toAxis = binding.getSecondArgument(i);
                    sendEvent = (binding.getThirdArgument(i) == 1);
                    prefromAxisTransition(fromAxis, toAxis, sendEvent, contentDescription);
                    mLastDownEventHandled = true;
                    break;
                default:
                    Log.w(LOG_TAG, "Unknown action code: " + actionCode);
            }
        }

        return mLastDownEventHandled;
    }

    /**
     * Set the current navigation axis which will be used while
     * calling {@link #traverseCurrentAxis(int, boolean, String)}.
     *
     * @param axis The axis to set.
     * @param sendEvent Whether to send an accessibility event to
     *        announce the change.
     */
    private void setCurrentAxis(int axis, boolean sendEvent, String contentDescription) {
        mCurrentAxis = axis;
        if (sendEvent) {
            AccessibilityEvent event = getPartialyPopulatedAccessibilityEvent();
            event.getText().add(String.valueOf(axis));
            event.setContentDescription(contentDescription);
            sendAccessibilityEvent(event);
        }
    }

    /**
     * Performs conditional transition one axis to another.
     *
     * @param fromAxis The axis which must be the current for the transition to occur.
     * @param toAxis The axis to which to transition.
     * @param sendEvent Flag if to send an event to announce successful transition.
     * @param contentDescription A description of the performed action.
     */
    private void prefromAxisTransition(int fromAxis, int toAxis, boolean sendEvent,
            String contentDescription) {
        if (mCurrentAxis == fromAxis) {
            setCurrentAxis(toAxis, sendEvent, contentDescription);
        }
    }

    /**
     * Traverse the document along the current navigation axis.
     *
     * @param direction The direction of traversal.
     * @param sendEvent Whether to send an accessibility event to
     *        announce the change.
     * @param contentDescription A description of the performed action.
     * @see #setCurrentAxis(int, boolean, String)
     */
    private boolean traverseCurrentAxis(int direction, boolean sendEvent,
            String contentDescription) {
        return traverseGivenAxis(direction, mCurrentAxis, sendEvent, contentDescription);
    }

    /**
     * Traverse the document along the given navigation axis.
     *
     * @param direction The direction of traversal.
     * @param axis The axis along which to traverse.
     * @param sendEvent Whether to send an accessibility event to
     *        announce the change.
     * @param contentDescription A description of the performed action.
     */
    private boolean traverseGivenAxis(int direction, int axis, boolean sendEvent,
            String contentDescription) {
        // if the axis is the default let WebView handle the event
        if (axis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) {
            return false;
        }
        WebViewCore webViewCore = mWebView.getWebViewCore();
        if (webViewCore != null) {
            AccessibilityEvent event = null;
            if (sendEvent) {
                event = getPartialyPopulatedAccessibilityEvent();
                // the text will be set upon receiving the selection string
                event.setContentDescription(contentDescription);
            }
            mScheduledEventStack.push(event);
            webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis);
        }
        return true;
    }

    /**
     * Called when the <code>selectionString</code> has changed.
     */
    public void onSelectionStringChange(String selectionString) {
        // put the selection string in an AccessibilityEvent and send it
        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
        mIsLastSelectionStringNull = (selectionString == null);
        AccessibilityEvent event = mScheduledEventStack.pop();
        if (event != null) {
            event.getText().add(selectionString);
        mWebView.sendAccessibilityEventUnchecked(event);
            sendAccessibilityEvent(event);
        }
    }

    /**
     * Modifies the current selection.
     * Sends an {@link AccessibilityEvent}.
     *
     * @param alter Specifies how to alter the selection.
     * @param direction The direction in which to alter the selection.
     * @param granularity The granularity of the selection modification.
     * @param event The event to send.
     */
    private void modifySelection(String alter, String direction, String granularity) {
        WebViewCore webViewCore = mWebView.getWebViewCore();
    private void sendAccessibilityEvent(AccessibilityEvent event) {
        if (DEBUG) {
            Log.d(LOG_TAG, "Dispatching: " + event);
        }
        AccessibilityManager.getInstance(mWebView.getContext()).sendAccessibilityEvent(event);
    }

    /**
     * @return An accessibility event whose members are populated except its
     *         text and content description.
     */
    private AccessibilityEvent getPartialyPopulatedAccessibilityEvent() {
        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SELECTED);
        event.setClassName(mWebView.getClass().getName());
        event.setPackageName(mWebView.getContext().getPackageName());
        event.setEnabled(mWebView.isEnabled());
        return event;
    }

        if (webViewCore == null) {
    /**
     * Ensures that the Web content key bindings are loaded.
     */
    private void ensureWebContentKeyBindings() {
        if (sBindings.size() > 0) {
            return;
        }

        WebViewCore.ModifySelectionData data = new WebViewCore.ModifySelectionData();
        data.mAlter = alter;
        data.mDirection = direction;
        data.mGranularity = granularity;
        webViewCore.sendMessage(EventHub.MODIFY_SELECTION, data);
        String webContentKeyBindingsString  = Settings.Secure.getString(
                mWebView.getContext().getContentResolver(),
                Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS);

        SimpleStringSplitter semiColonSplitter = new SimpleStringSplitter(';');
        semiColonSplitter.setString(webContentKeyBindingsString);

        ArrayList<AccessibilityWebContentKeyBinding> bindings =
            new ArrayList<AccessibilityWebContentKeyBinding>();

        while (semiColonSplitter.hasNext()) {
            String bindingString = semiColonSplitter.next();
            if (TextUtils.isEmpty(bindingString)) {
                Log.e(LOG_TAG, "Malformed Web content key binding: "
                        + webContentKeyBindingsString);
                continue;
            }
            String[] keyValueArray = bindingString.split("=");
            if (keyValueArray.length != 2) {
                Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " +
                        bindingString);
                continue;
            }
            try {
                SimpleStringSplitter colonSplitter = new SimpleStringSplitter(':');//remove
                int key = Integer.decode(keyValueArray[0].trim());
                String[] actionStrings = keyValueArray[1].split(":");
                int[] actions = new int[actionStrings.length];
                for (int i = 0, count = actions.length; i < count; i++) {
                    actions[i] = Integer.decode(actionStrings[i].trim());
                }

                bindings.add(new AccessibilityWebContentKeyBinding(key, actions));
            } catch (NumberFormatException nfe) {
                Log.e(LOG_TAG, "Disregarding malformed key binding: " + bindingString);
            }
        }

        for (AccessibilityWebContentKeyBinding binding : bindings) {
            sBindings.put(binding.getKey(), binding);
        }
    }

    /**
     * Represents a web content key-binding.
     */
    private class AccessibilityWebContentKeyBinding {

        private static final int OFFSET_META_STATE = 0x00000010;

        private static final int MASK_META_STATE = 0xFFFF0000;

        private static final int OFFSET_KEY_CODE = 0x00000000;

        private static final int MASK_KEY_CODE = 0x0000FFFF;

        private static final int OFFSET_ACTION = 0x00000018;

        private static final int MASK_ACTION = 0xFF000000;

        private static final int OFFSET_FIRST_ARGUMENT = 0x00000010;

        private static final int MASK_FIRST_ARGUMENT = 0x00FF0000;

        private static final int OFFSET_SECOND_ARGUMENT = 0x00000008;

        private static final int MASK_SECOND_ARGUMENT = 0x0000FF00;

        private static final int OFFSET_THIRD_ARGUMENT = 0x00000000;

        private static final int MASK_THIRD_ARGUMENT = 0x000000FF;

        private int mKey;

        private int [] mActionSequence;

        /**
         * @return The binding key with key code and meta state.
         *
         * @see #MASK_KEY_CODE
         * @see #MASK_META_STATE
         * @see #OFFSET_KEY_CODE
         * @see #OFFSET_META_STATE
         */
        public int getKey() {
            return mKey;
        }

        /**
         * @return The key code of the binding key.
         */
        public int getKeyCode() {
            return (mKey & MASK_KEY_CODE) >> OFFSET_KEY_CODE;
        }

        /**
         * @return The meta state of the binding key.
         */
        public int getMetaState() {
            return (mKey & MASK_META_STATE) >> OFFSET_META_STATE;
        }

        /**
         * @return The number of actions in the key binding.
         */
        public int getActionCount() {
            return mActionSequence.length;
        }

        /**
         * @param index The action for a given action <code>index</code>.
         */
        public int getAction(int index) {
            return mActionSequence[index];
        }

        /**
         * @param index The action code for a given action <code>index</code>.
         */
        public int getActionCode(int index) {
            return (mActionSequence[index] & MASK_ACTION) >> OFFSET_ACTION;
        }

        /**
         * @param index The first argument for a given action <code>index</code>.
         */
        public int getFirstArgument(int index) {
            return (mActionSequence[index] & MASK_FIRST_ARGUMENT) >> OFFSET_FIRST_ARGUMENT;
        }

        /**
         * @param index The second argument for a given action <code>index</code>.
         */
        public int getSecondArgument(int index) {
            return (mActionSequence[index] & MASK_SECOND_ARGUMENT) >> OFFSET_SECOND_ARGUMENT;
        }

        /**
         * @param index The third argument for a given action <code>index</code>.
         */
        public int getThirdArgument(int index) {
            return (mActionSequence[index] & MASK_THIRD_ARGUMENT) >> OFFSET_THIRD_ARGUMENT;
        }

        /**
         * Creates a new instance.
         * @param key The key for the binding (key and meta state)
         * @param actionSequence The sequence of action for the binding.
         * @see #getKey()
         */
        public AccessibilityWebContentKeyBinding(int key, int[] actionSequence) {
            mKey = key;
            mActionSequence = actionSequence;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("key: ");
            builder.append(getKey());
            builder.append(", metaState: ");
            builder.append(getMetaState());
            builder.append(", keyCode: ");
            builder.append(getKeyCode());
            builder.append(", actions[");
            for (int i = 0, count = getActionCount(); i < count; i++) {
                builder.append("{actionCode");
                builder.append(i);
                builder.append(": ");
                builder.append(getActionCode(i));
                builder.append(", firstArgument: ");
                builder.append(getFirstArgument(i));
                builder.append(", secondArgument: ");
                builder.append(getSecondArgument(i));
                builder.append(", thirdArgument: ");
                builder.append(getThirdArgument(i));
                builder.append("}");
            }
            builder.append("]");
            return builder.toString();
        }
    }
}
+3 −10
Original line number Diff line number Diff line
@@ -256,17 +256,10 @@ class CallbackProxy extends Handler {
        // 32-bit reads and writes.
        switch (msg.what) {
            case PAGE_STARTED:
                // every time we start a new page, we want to reset the
                // WebView certificate:
                // if the new site is secure, we will reload it and get a
                // new certificate set;
                // if the new site is not secure, the certificate must be
                // null, and that will be the case
                mWebView.setCertificate(null);
                String startedUrl = msg.getData().getString("url");
                mWebView.onPageStarted(startedUrl);
                if (mWebViewClient != null) {
                    mWebViewClient.onPageStarted(mWebView,
                            msg.getData().getString("url"),
                            (Bitmap) msg.obj);
                    mWebViewClient.onPageStarted(mWebView, startedUrl, (Bitmap) msg.obj);
                }
                break;

+128 −10

File changed.

Preview size limit exceeded, changes collapsed.

+4 −18

File changed.

Preview size limit exceeded, changes collapsed.

Loading