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

Commit ba15633a authored by Shimeng (Simon) Wang's avatar Shimeng (Simon) Wang Committed by Android (Google) Code Review
Browse files

Merge "Accessibility support for WebViews"

parents 8e0835c0 585f13f8
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