Merge "Refactor WebView accessibility code into a separate class." into jb-dev

This commit is contained in:
Svetoslav Ganov
2012-05-17 10:32:27 -07:00
committed by Android (Google) Code Review
3 changed files with 806 additions and 658 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2010 The Android Open Source Project
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,484 +16,335 @@
package android.webkit;
import android.content.Context;
import android.os.Vibrator;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.util.Log;
import android.speech.tts.TextToSpeech;
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;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
/**
* 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 modifiers + 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}.
* Handles injecting accessibility JavaScript and related JavaScript -> Java
* APIs.
*/
class AccessibilityInjector {
private static final String LOG_TAG = "AccessibilityInjector";
// The WebViewClassic this injector is responsible for managing.
private final WebViewClassic mWebViewClassic;
private static final boolean DEBUG = true;
// Cached reference to mWebViewClassic.getContext(), for convenience.
private final Context mContext;
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;
private static final int ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS = 4;
// Cached reference to mWebViewClassic.getWebView(), for convenience.
private final WebView mWebView;
// the default WebView behavior abstracted as a navigation axis
private static final int NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR = 7;
// The Java objects that are exposed to JavaScript.
private TextToSpeech mTextToSpeech;
// these are the same for all instances so make them process wide
private static ArrayList<AccessibilityWebContentKeyBinding> sBindings =
new ArrayList<AccessibilityWebContentKeyBinding>();
// Lazily loaded helper objects.
private AccessibilityManager mAccessibilityManager;
private AccessibilityInjectorFallback mAccessibilityInjector;
// handle to the WebViewClassic this injector is associated with.
private final WebViewClassic mWebView;
// Whether the accessibility script has been injected into the current page.
private boolean mAccessibilityScriptInjected;
// events scheduled for sending as soon as we receive the selected text
private final Stack<AccessibilityEvent> mScheduledEventStack = new Stack<AccessibilityEvent>();
// Constants for determining script injection strategy.
private static final int ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED = -1;
private static final int ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT = 0;
@SuppressWarnings("unused")
private static final int ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED = 1;
// the current traversal axis
private int mCurrentAxis = 2; // sentence
// Aliases for Java objects exposed to JavaScript.
private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility";
// 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;
// Template for JavaScript that injects a screen-reader.
private static final String ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE =
"javascript:(function() {" +
" var chooser = document.createElement('script');" +
" chooser.type = 'text/javascript';" +
" chooser.src = '%1s';" +
" document.getElementsByTagName('head')[0].appendChild(chooser);" +
" })();";
/**
* Creates a new injector associated with a given {@link WebViewClassic}.
* Creates an instance of the AccessibilityInjector based on
* {@code webViewClassic}.
*
* @param webView The associated WebViewClassic.
* @param webViewClassic The WebViewClassic that this AccessibilityInjector
* manages.
*/
public AccessibilityInjector(WebViewClassic webView) {
mWebView = webView;
ensureWebContentKeyBindings();
public AccessibilityInjector(WebViewClassic webViewClassic) {
mWebViewClassic = webViewClassic;
mWebView = webViewClassic.getWebView();
mContext = webViewClassic.getContext();
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
}
/**
* Processes a key down <code>event</code>.
*
* @return True if the event was processed.
* Attempts to load scripting interfaces for accessibility.
* <p>
* This should be called when the window is attached.
* </p>
*/
public boolean onKeyEvent(KeyEvent event) {
// We do not handle ENTER in any circumstances.
if (isEnterActionKey(event.getKeyCode())) {
public void addAccessibilityApisIfNecessary() {
if (!isAccessibilityEnabled() || !isJavaScriptEnabled()) {
return;
}
addTtsApis();
}
/**
* Attempts to unload scripting interfaces for accessibility.
* <p>
* This should be called when the window is detached.
* </p>
*/
public void removeAccessibilityApisIfNecessary() {
removeTtsApis();
}
/**
* Attempts to handle key events when accessibility is turned on.
*
* @param event The key event to handle.
* @return {@code true} if the event was handled.
*/
public boolean handleKeyEventIfNecessary(KeyEvent event) {
if (!isAccessibilityEnabled()) {
mAccessibilityScriptInjected = false;
toggleFallbackAccessibilityInjector(false);
return false;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
return mLastDownEventHandled;
}
mLastDownEventHandled = false;
AccessibilityWebContentKeyBinding binding = null;
for (AccessibilityWebContentKeyBinding candidate : sBindings) {
if (event.getKeyCode() == candidate.getKeyCode()
&& event.hasModifiers(candidate.getModifiers())) {
binding = candidate;
break;
if (mAccessibilityScriptInjected) {
// if an accessibility script is injected we delegate to it the key
// handling. this script is a screen reader which is a fully fledged
// solution for blind users to navigate in and interact with web
// pages.
if (event.getAction() == KeyEvent.ACTION_UP) {
mWebViewClassic.sendBatchableInputMessage(EventHub.KEY_UP, 0, 0, event);
} else if (event.getAction() == KeyEvent.ACTION_DOWN) {
mWebViewClassic.sendBatchableInputMessage(EventHub.KEY_DOWN, 0, 0, event);
} else {
return false;
}
return true;
}
if (binding == null) {
if (mAccessibilityInjector != null) {
// if an accessibility injector is present (no JavaScript enabled or
// the site opts out injecting our JavaScript screen reader) we let
// it decide whether to act on and consume the event.
return mAccessibilityInjector.onKeyEvent(event);
}
return false;
}
/**
* Attempts to handle selection change events when accessibility is using a
* non-JavaScript method.
*
* @param selectionString The selection string.
*/
public void handleSelectionChangedIfNecessary(String selectionString) {
if (mAccessibilityInjector != null) {
mAccessibilityInjector.onSelectionStringChange(selectionString);
}
}
/**
* Prepares for injecting accessibility scripts into a new page.
*
* @param url The URL that will be loaded.
*/
public void onPageStarted(String url) {
mAccessibilityScriptInjected = false;
}
/**
* Attempts to inject the accessibility script using a {@code <script>} tag.
* <p>
* This should be called after a page has finished loading.
* </p>
*
* @param url The URL that just finished loading.
*/
public void onPageFinished(String url) {
if (!isAccessibilityEnabled()) {
mAccessibilityScriptInjected = false;
toggleFallbackAccessibilityInjector(false);
return;
}
if (!shouldInjectJavaScript(url)) {
toggleFallbackAccessibilityInjector(true);
return;
}
toggleFallbackAccessibilityInjector(false);
final String injectionUrl = getScreenReaderInjectionUrl();
mWebView.loadUrl(injectionUrl);
mAccessibilityScriptInjected = true;
}
/**
* Toggles the non-JavaScript method for handling accessibility.
*
* @param enabled {@code true} to enable the non-JavaScript method, or
* {@code false} to disable it.
*/
private void toggleFallbackAccessibilityInjector(boolean enabled) {
if (enabled && (mAccessibilityInjector == null)) {
mAccessibilityInjector = new AccessibilityInjectorFallback(mWebViewClassic);
} else {
mAccessibilityInjector = null;
}
}
/**
* Determines whether it's okay to inject JavaScript into a given URL.
*
* @param url The URL to check.
* @return {@code true} if JavaScript should be injected, {@code false} if a
* non-JavaScript method should be used.
*/
private boolean shouldInjectJavaScript(String url) {
// Respect the WebView's JavaScript setting.
if (!isJavaScriptEnabled()) {
return false;
}
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 ACTION_TRAVERSE_CURRENT_AXIS:
int direction = binding.getFirstArgument(i);
// on second null selection string in same direction - WebView handles the event
if (direction == mLastDirection && mIsLastSelectionStringNull) {
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) {
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;
case ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS:
// This is a special case since we treat the default WebView navigation
// behavior as one of the possible navigation axis the user can use.
// If we are not on the default WebView navigation axis this is NOP.
if (mCurrentAxis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) {
// While WebVew handles navigation we do not get null selection
// strings so do not check for that here as the cases above.
mLastDirection = binding.getFirstArgument(i);
sendEvent = (binding.getSecondArgument(i) == 1);
traverseGivenAxis(mLastDirection, NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR,
sendEvent, contentDescription);
mLastDownEventHandled = false;
} else {
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) {
WebViewCore webViewCore = mWebView.getWebViewCore();
if (webViewCore == null) {
// Allow the page to opt out of Accessibility script injection.
if (getAxsUrlParameterValue(url) == ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT) {
return false;
}
AccessibilityEvent event = null;
if (sendEvent) {
event = getPartialyPopulatedAccessibilityEvent();
// the text will be set upon receiving the selection string
event.setContentDescription(contentDescription);
}
mScheduledEventStack.push(event);
// if the axis is the default let WebView handle the event which will
// result in cursor ring movement and selection of its content
if (axis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) {
// The user must explicitly enable Accessibility script injection.
if (!isScriptInjectionEnabled()) {
return false;
}
webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis);
return true;
}
/**
* Called when the <code>selectionString</code> has changed.
* @return {@code true} if the user has explicitly enabled Accessibility
* script injection.
*/
public void onSelectionStringChange(String selectionString) {
if (DEBUG) {
Log.d(LOG_TAG, "Selection string: " + selectionString);
}
mIsLastSelectionStringNull = (selectionString == null);
if (mScheduledEventStack.isEmpty()) {
return;
}
AccessibilityEvent event = mScheduledEventStack.pop();
if (event != null) {
event.getText().add(selectionString);
sendAccessibilityEvent(event);
}
private boolean isScriptInjectionEnabled() {
final int injectionSetting = Settings.Secure.getInt(
mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0);
return (injectionSetting == 1);
}
/**
* Sends an {@link AccessibilityEvent}.
* Attempts to initialize and add interfaces for TTS, if that hasn't already
* been done.
*/
private void addTtsApis() {
if (mTextToSpeech != null) {
return;
}
final String pkgName = mContext.getPackageName();
mTextToSpeech = new TextToSpeech(mContext, null, null, pkgName + ".**webview**", true);
mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_ACCESSIBILITY_JS_INTERFACE);
}
/**
* Attempts to shutdown and remove interfaces for TTS, if that hasn't
* already been done.
*/
private void removeTtsApis() {
if (mTextToSpeech == null) {
return;
}
mWebView.removeJavascriptInterface(ALIAS_ACCESSIBILITY_JS_INTERFACE);
mTextToSpeech.stop();
mTextToSpeech.shutdown();
mTextToSpeech = null;
}
/**
* Returns the script injection preference requested by the URL, or
* {@link #ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED} if the page has no
* preference.
*
* @param event The event to send.
* @param url The URL to check.
* @return A script injection preference.
*/
private void sendAccessibilityEvent(AccessibilityEvent event) {
if (DEBUG) {
Log.d(LOG_TAG, "Dispatching: " + event);
}
// accessibility may be disabled while waiting for the selection string
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mWebView.getContext());
if (accessibilityManager.isEnabled()) {
accessibilityManager.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.getWebView().isEnabled());
return event;
}
/**
* Ensures that the Web content key bindings are loaded.
*/
private void ensureWebContentKeyBindings() {
if (sBindings.size() > 0) {
return;
private int getAxsUrlParameterValue(String url) {
if (url == null) {
return ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED;
}
String webContentKeyBindingsString = Settings.Secure.getString(
mWebView.getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS);
try {
final List<NameValuePair> params = URLEncodedUtils.parse(new URI(url), null);
SimpleStringSplitter semiColonSplitter = new SimpleStringSplitter(';');
semiColonSplitter.setString(webContentKeyBindingsString);
while (semiColonSplitter.hasNext()) {
String bindingString = semiColonSplitter.next();
if (TextUtils.isEmpty(bindingString)) {
Log.e(LOG_TAG, "Disregarding 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 {
long keyCodeAndModifiers = Long.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());
for (NameValuePair param : params) {
if ("axs".equals(param.getName())) {
return verifyInjectionValue(param.getValue());
}
sBindings.add(new AccessibilityWebContentKeyBinding(keyCodeAndModifiers, actions));
} catch (NumberFormatException nfe) {
Log.e(LOG_TAG, "Disregarding malformed key binding: " + bindingString);
}
} catch (URISyntaxException e) {
// Do nothing.
}
return ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED;
}
private boolean isEnterActionKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|| keyCode == KeyEvent.KEYCODE_ENTER
|| keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER;
private int verifyInjectionValue(String value) {
try {
final int parsed = Integer.parseInt(value);
switch (parsed) {
case ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT:
return ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT;
case ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED:
return ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED;
}
} catch (NumberFormatException e) {
// Do nothing.
}
return ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED;
}
/**
* Represents a web content key-binding.
* @return The URL for injecting the screen reader.
*/
private static final class AccessibilityWebContentKeyBinding {
private String getScreenReaderInjectionUrl() {
final String screenReaderUrl = Settings.Secure.getString(
mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL);
return String.format(ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE, screenReaderUrl);
}
private static final int MODIFIERS_OFFSET = 32;
private static final long MODIFIERS_MASK = 0xFFFFFFF00000000L;
/**
* @return {@code true} if JavaScript is enabled in the {@link WebView}
* settings.
*/
private boolean isJavaScriptEnabled() {
return mWebView.getSettings().getJavaScriptEnabled();
}
private static final int KEY_CODE_OFFSET = 0;
private static final long KEY_CODE_MASK = 0x00000000FFFFFFFFL;
private static final int ACTION_OFFSET = 24;
private static final int ACTION_MASK = 0xFF000000;
private static final int FIRST_ARGUMENT_OFFSET = 16;
private static final int FIRST_ARGUMENT_MASK = 0x00FF0000;
private static final int SECOND_ARGUMENT_OFFSET = 8;
private static final int SECOND_ARGUMENT_MASK = 0x0000FF00;
private static final int THIRD_ARGUMENT_OFFSET = 0;
private static final int THIRD_ARGUMENT_MASK = 0x000000FF;
private final long mKeyCodeAndModifiers;
private final int [] mActionSequence;
/**
* @return The key code of the binding key.
*/
public int getKeyCode() {
return (int) ((mKeyCodeAndModifiers & KEY_CODE_MASK) >> KEY_CODE_OFFSET);
}
/**
* @return The meta state of the binding key.
*/
public int getModifiers() {
return (int) ((mKeyCodeAndModifiers & MODIFIERS_MASK) >> MODIFIERS_OFFSET);
}
/**
* @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] & ACTION_MASK) >> ACTION_OFFSET;
}
/**
* @param index The first argument for a given action <code>index</code>.
*/
public int getFirstArgument(int index) {
return (mActionSequence[index] & FIRST_ARGUMENT_MASK) >> FIRST_ARGUMENT_OFFSET;
}
/**
* @param index The second argument for a given action <code>index</code>.
*/
public int getSecondArgument(int index) {
return (mActionSequence[index] & SECOND_ARGUMENT_MASK) >> SECOND_ARGUMENT_OFFSET;
}
/**
* @param index The third argument for a given action <code>index</code>.
*/
public int getThirdArgument(int index) {
return (mActionSequence[index] & THIRD_ARGUMENT_MASK) >> THIRD_ARGUMENT_OFFSET;
}
/**
* Creates a new instance.
* @param keyCodeAndModifiers The key for the binding (key and modifiers).
* @param actionSequence The sequence of action for the binding.
*/
public AccessibilityWebContentKeyBinding(long keyCodeAndModifiers, int[] actionSequence) {
mKeyCodeAndModifiers = keyCodeAndModifiers;
mActionSequence = actionSequence;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("modifiers: ");
builder.append(getModifiers());
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();
}
/**
* @return {@code true} if accessibility is enabled.
*/
private boolean isAccessibilityEnabled() {
return mAccessibilityManager.isEnabled();
}
}

View File

@@ -0,0 +1,499 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.webkit;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.util.Log;
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 modifiers + 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 AccessibilityInjectorFallback {
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;
private static final int ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS = 4;
// 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 ArrayList<AccessibilityWebContentKeyBinding> sBindings =
new ArrayList<AccessibilityWebContentKeyBinding>();
// handle to the WebViewClassic this injector is associated with.
private final WebViewClassic 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 {@link WebViewClassic}.
*
* @param webView The associated WebViewClassic.
*/
public AccessibilityInjectorFallback(WebViewClassic webView) {
mWebView = webView;
ensureWebContentKeyBindings();
}
/**
* Processes a key down <code>event</code>.
*
* @return True if the event was processed.
*/
public boolean onKeyEvent(KeyEvent event) {
// We do not handle ENTER in any circumstances.
if (isEnterActionKey(event.getKeyCode())) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_UP) {
return mLastDownEventHandled;
}
mLastDownEventHandled = false;
AccessibilityWebContentKeyBinding binding = null;
for (AccessibilityWebContentKeyBinding candidate : sBindings) {
if (event.getKeyCode() == candidate.getKeyCode()
&& event.hasModifiers(candidate.getModifiers())) {
binding = candidate;
break;
}
}
if (binding == null) {
return false;
}
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 ACTION_TRAVERSE_CURRENT_AXIS:
int direction = binding.getFirstArgument(i);
// on second null selection string in same direction - WebView handles the event
if (direction == mLastDirection && mIsLastSelectionStringNull) {
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) {
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;
case ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS:
// This is a special case since we treat the default WebView navigation
// behavior as one of the possible navigation axis the user can use.
// If we are not on the default WebView navigation axis this is NOP.
if (mCurrentAxis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) {
// While WebVew handles navigation we do not get null selection
// strings so do not check for that here as the cases above.
mLastDirection = binding.getFirstArgument(i);
sendEvent = (binding.getSecondArgument(i) == 1);
traverseGivenAxis(mLastDirection, NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR,
sendEvent, contentDescription);
mLastDownEventHandled = false;
} else {
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) {
WebViewCore webViewCore = mWebView.getWebViewCore();
if (webViewCore == null) {
return false;
}
AccessibilityEvent event = null;
if (sendEvent) {
event = getPartialyPopulatedAccessibilityEvent();
// the text will be set upon receiving the selection string
event.setContentDescription(contentDescription);
}
mScheduledEventStack.push(event);
// if the axis is the default let WebView handle the event which will
// result in cursor ring movement and selection of its content
if (axis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) {
return false;
}
webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis);
return true;
}
/**
* Called when the <code>selectionString</code> has changed.
*/
public void onSelectionStringChange(String selectionString) {
if (DEBUG) {
Log.d(LOG_TAG, "Selection string: " + selectionString);
}
mIsLastSelectionStringNull = (selectionString == null);
if (mScheduledEventStack.isEmpty()) {
return;
}
AccessibilityEvent event = mScheduledEventStack.pop();
if (event != null) {
event.getText().add(selectionString);
sendAccessibilityEvent(event);
}
}
/**
* Sends an {@link AccessibilityEvent}.
*
* @param event The event to send.
*/
private void sendAccessibilityEvent(AccessibilityEvent event) {
if (DEBUG) {
Log.d(LOG_TAG, "Dispatching: " + event);
}
// accessibility may be disabled while waiting for the selection string
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mWebView.getContext());
if (accessibilityManager.isEnabled()) {
accessibilityManager.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.getWebView().isEnabled());
return event;
}
/**
* Ensures that the Web content key bindings are loaded.
*/
private void ensureWebContentKeyBindings() {
if (sBindings.size() > 0) {
return;
}
String webContentKeyBindingsString = Settings.Secure.getString(
mWebView.getContext().getContentResolver(),
Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS);
SimpleStringSplitter semiColonSplitter = new SimpleStringSplitter(';');
semiColonSplitter.setString(webContentKeyBindingsString);
while (semiColonSplitter.hasNext()) {
String bindingString = semiColonSplitter.next();
if (TextUtils.isEmpty(bindingString)) {
Log.e(LOG_TAG, "Disregarding 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 {
long keyCodeAndModifiers = Long.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());
}
sBindings.add(new AccessibilityWebContentKeyBinding(keyCodeAndModifiers, actions));
} catch (NumberFormatException nfe) {
Log.e(LOG_TAG, "Disregarding malformed key binding: " + bindingString);
}
}
}
private boolean isEnterActionKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|| keyCode == KeyEvent.KEYCODE_ENTER
|| keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER;
}
/**
* Represents a web content key-binding.
*/
private static final class AccessibilityWebContentKeyBinding {
private static final int MODIFIERS_OFFSET = 32;
private static final long MODIFIERS_MASK = 0xFFFFFFF00000000L;
private static final int KEY_CODE_OFFSET = 0;
private static final long KEY_CODE_MASK = 0x00000000FFFFFFFFL;
private static final int ACTION_OFFSET = 24;
private static final int ACTION_MASK = 0xFF000000;
private static final int FIRST_ARGUMENT_OFFSET = 16;
private static final int FIRST_ARGUMENT_MASK = 0x00FF0000;
private static final int SECOND_ARGUMENT_OFFSET = 8;
private static final int SECOND_ARGUMENT_MASK = 0x0000FF00;
private static final int THIRD_ARGUMENT_OFFSET = 0;
private static final int THIRD_ARGUMENT_MASK = 0x000000FF;
private final long mKeyCodeAndModifiers;
private final int [] mActionSequence;
/**
* @return The key code of the binding key.
*/
public int getKeyCode() {
return (int) ((mKeyCodeAndModifiers & KEY_CODE_MASK) >> KEY_CODE_OFFSET);
}
/**
* @return The meta state of the binding key.
*/
public int getModifiers() {
return (int) ((mKeyCodeAndModifiers & MODIFIERS_MASK) >> MODIFIERS_OFFSET);
}
/**
* @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] & ACTION_MASK) >> ACTION_OFFSET;
}
/**
* @param index The first argument for a given action <code>index</code>.
*/
public int getFirstArgument(int index) {
return (mActionSequence[index] & FIRST_ARGUMENT_MASK) >> FIRST_ARGUMENT_OFFSET;
}
/**
* @param index The second argument for a given action <code>index</code>.
*/
public int getSecondArgument(int index) {
return (mActionSequence[index] & SECOND_ARGUMENT_MASK) >> SECOND_ARGUMENT_OFFSET;
}
/**
* @param index The third argument for a given action <code>index</code>.
*/
public int getThirdArgument(int index) {
return (mActionSequence[index] & THIRD_ARGUMENT_MASK) >> THIRD_ARGUMENT_OFFSET;
}
/**
* Creates a new instance.
* @param keyCodeAndModifiers The key for the binding (key and modifiers).
* @param actionSequence The sequence of action for the binding.
*/
public AccessibilityWebContentKeyBinding(long keyCodeAndModifiers, int[] actionSequence) {
mKeyCodeAndModifiers = keyCodeAndModifiers;
mActionSequence = actionSequence;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("modifiers: ");
builder.append(getModifiers());
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();
}
}
}

View File

@@ -60,9 +60,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings;
import android.security.KeyChain;
import android.speech.tts.TextToSpeech;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
@@ -871,15 +869,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
private static final int MOTIONLESS_IGNORE = 3;
private int mHeldMotionless;
// An instance for injecting accessibility in WebViews with disabled
// JavaScript or ones for which no accessibility script exists
// Lazily-instantiated instance for injecting accessibility.
private AccessibilityInjector mAccessibilityInjector;
// flag indicating if accessibility script is injected so we
// know to handle Shift and arrows natively first
private boolean mAccessibilityScriptInjected;
/**
* How long the caret handle will last without being touched.
*/
@@ -1088,34 +1080,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
private int mHorizontalScrollBarMode = SCROLLBAR_AUTO;
private int mVerticalScrollBarMode = SCROLLBAR_AUTO;
// constants for determining script injection strategy
private static final int ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED = -1;
private static final int ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT = 0;
private static final int ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED = 1;
// the alias via which accessibility JavaScript interface is exposed
private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility";
// Template for JavaScript that injects a screen-reader.
private static final String ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE =
"javascript:(function() {" +
" var chooser = document.createElement('script');" +
" chooser.type = 'text/javascript';" +
" chooser.src = '%1s';" +
" document.getElementsByTagName('head')[0].appendChild(chooser);" +
" })();";
// Regular expression that matches the "axs" URL parameter.
// The value of 0 means the accessibility script is opted out
// The value of 1 means the accessibility script is already injected
private static final String PATTERN_MATCH_AXS_URL_PARAMETER = "(\\?axs=(0|1))|(&axs=(0|1))";
// TextToSpeech instance exposed to JavaScript to the injected screenreader.
private TextToSpeech mTextToSpeech;
// variable to cache the above pattern in case accessibility is enabled.
private Pattern mMatchAxsUrlParameterPattern;
/**
* Max distance to overscroll by in pixels.
* This how far content can be pulled beyond its normal bounds by the user.
@@ -1634,40 +1598,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
return true;
}
/**
* Adds accessibility APIs to JavaScript.
*
* Note: This method is responsible to performing the necessary
* check if the accessibility APIs should be exposed.
*/
private void addAccessibilityApisToJavaScript() {
if (AccessibilityManager.getInstance(mContext).isEnabled()
&& getSettings().getJavaScriptEnabled()) {
// exposing the TTS for now ...
final Context ctx = mContext;
if (ctx != null) {
final String packageName = ctx.getPackageName();
if (packageName != null) {
mTextToSpeech = new TextToSpeech(ctx, null, null,
packageName + ".**webview**", true);
addJavascriptInterface(mTextToSpeech, ALIAS_ACCESSIBILITY_JS_INTERFACE);
}
}
}
}
/**
* Removes accessibility APIs from JavaScript.
*/
private void removeAccessibilityApisFromJavaScript() {
// exposing the TTS for now ...
if (mTextToSpeech != null) {
removeJavascriptInterface(ALIAS_ACCESSIBILITY_JS_INTERFACE);
mTextToSpeech.shutdown();
mTextToSpeech = null;
}
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
info.setScrollable(isScrollableForAccessibility());
@@ -1688,6 +1618,17 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
event.setMaxScrollY(Math.max(convertedContentHeight - adjustedViewHeight, 0));
}
private boolean isAccessibilityEnabled() {
return AccessibilityManager.getInstance(mContext).isEnabled();
}
private AccessibilityInjector getAccessibilityInjector() {
if (mAccessibilityInjector == null) {
mAccessibilityInjector = new AccessibilityInjector(this);
}
return mAccessibilityInjector;
}
private boolean isScrollableForAccessibility() {
return (contentToViewX(getContentWidth()) > getWidth() - mWebView.getPaddingLeft()
- mWebView.getPaddingRight()
@@ -3823,7 +3764,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
// reset the flag since we set to true in if need after
// loading is see onPageFinished(Url)
mAccessibilityScriptInjected = false;
if (isAccessibilityEnabled()) {
getAccessibilityInjector().onPageStarted(url);
}
// Don't start out editing.
mIsEditingText = false;
@@ -3835,114 +3778,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
*/
/* package */ void onPageFinished(String url) {
mZoomManager.onPageFinished(url);
injectAccessibilityForUrl(url);
}
/**
* This method injects accessibility in the loaded document if accessibility
* is enabled. If JavaScript is enabled we try to inject a URL specific script.
* If no URL specific script is found or JavaScript is disabled we fallback to
* the default {@link AccessibilityInjector} implementation.
* </p>
* If the URL has the "axs" paramter set to 1 it has already done the
* script injection so we do nothing. If the parameter is set to 0
* the URL opts out accessibility script injection so we fall back to
* the default {@link AccessibilityInjector}.
* </p>
* Note: If the user has not opted-in the accessibility script injection no scripts
* are injected rather the default {@link AccessibilityInjector} implementation
* is used.
*
* @param url The URL loaded by this {@link WebView}.
*/
private void injectAccessibilityForUrl(String url) {
if (mWebViewCore == null) {
return;
if (isAccessibilityEnabled()) {
getAccessibilityInjector().onPageFinished(url);
}
AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mContext);
if (!accessibilityManager.isEnabled()) {
// it is possible that accessibility was turned off between reloads
ensureAccessibilityScriptInjectorInstance(false);
return;
}
if (!getSettings().getJavaScriptEnabled()) {
// no JS so we fallback to the basic buil-in support
ensureAccessibilityScriptInjectorInstance(true);
return;
}
// check the URL "axs" parameter to choose appropriate action
int axsParameterValue = getAxsUrlParameterValue(url);
if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED) {
boolean onDeviceScriptInjectionEnabled = (Settings.Secure.getInt(mContext
.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0) == 1);
if (onDeviceScriptInjectionEnabled) {
ensureAccessibilityScriptInjectorInstance(false);
// neither script injected nor script injection opted out => we inject
mWebView.loadUrl(getScreenReaderInjectingJs());
// TODO: Set this flag after successfull script injection. Maybe upon injection
// the chooser should update the meta tag and we check it to declare success
mAccessibilityScriptInjected = true;
} else {
// injection disabled so we fallback to the basic built-in support
ensureAccessibilityScriptInjectorInstance(true);
}
} else if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT) {
// injection opted out so we fallback to the basic buil-in support
ensureAccessibilityScriptInjectorInstance(true);
} else if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED) {
ensureAccessibilityScriptInjectorInstance(false);
// the URL provides accessibility but we still need to add our generic script
mWebView.loadUrl(getScreenReaderInjectingJs());
} else {
Log.e(LOGTAG, "Unknown URL value for the \"axs\" URL parameter: " + axsParameterValue);
}
}
/**
* Ensures the instance of the {@link AccessibilityInjector} to be present ot not.
*
* @param present True to ensure an insance, false to ensure no instance.
*/
private void ensureAccessibilityScriptInjectorInstance(boolean present) {
if (present) {
if (mAccessibilityInjector == null) {
mAccessibilityInjector = new AccessibilityInjector(this);
}
} else {
mAccessibilityInjector = null;
}
}
/**
* Gets JavaScript that injects a screen-reader.
*
* @return The JavaScript snippet.
*/
private String getScreenReaderInjectingJs() {
String screenReaderUrl = Settings.Secure.getString(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL);
return String.format(ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE, screenReaderUrl);
}
/**
* Gets the "axs" URL parameter value.
*
* @param url A url to fetch the paramter from.
* @return The parameter value if such, -1 otherwise.
*/
private int getAxsUrlParameterValue(String url) {
if (mMatchAxsUrlParameterPattern == null) {
mMatchAxsUrlParameterPattern = Pattern.compile(PATTERN_MATCH_AXS_URL_PARAMETER);
}
Matcher matcher = mMatchAxsUrlParameterPattern.matcher(url);
if (matcher.find()) {
String keyValuePair = url.substring(matcher.start(), matcher.end());
return Integer.parseInt(keyValuePair.split("=")[1]);
}
return -1;
}
// scale from content to view coordinates, and pin
@@ -4900,30 +4739,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
return false;
}
// accessibility support
if (accessibilityScriptInjected()) {
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
// if an accessibility script is injected we delegate to it the key handling.
// this script is a screen reader which is a fully fledged solution for blind
// users to navigate in and interact with web pages.
sendBatchableInputMessage(EventHub.KEY_DOWN, 0, 0, event);
return true;
} else {
// Clean up if accessibility was disabled after loading the current URL.
mAccessibilityScriptInjected = false;
}
} else if (mAccessibilityInjector != null) {
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityInjector.onKeyEvent(event)) {
// if an accessibility injector is present (no JavaScript enabled or the site
// opts out injecting our JavaScript screen reader) we let it decide whether
// to act on and consume the event.
return true;
}
} else {
// Clean up if accessibility was disabled after loading the current URL.
mAccessibilityInjector = null;
}
// See if the accessibility injector needs to handle this event.
if (isAccessibilityEnabled()
&& getAccessibilityInjector().handleKeyEventIfNecessary(event)) {
return true;
}
if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
@@ -5027,30 +4846,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
return false;
}
// accessibility support
if (accessibilityScriptInjected()) {
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
// if an accessibility script is injected we delegate to it the key handling.
// this script is a screen reader which is a fully fledged solution for blind
// users to navigate in and interact with web pages.
sendBatchableInputMessage(EventHub.KEY_UP, 0, 0, event);
return true;
} else {
// Clean up if accessibility was disabled after loading the current URL.
mAccessibilityScriptInjected = false;
}
} else if (mAccessibilityInjector != null) {
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mAccessibilityInjector.onKeyEvent(event)) {
// if an accessibility injector is present (no JavaScript enabled or the site
// opts out injecting our JavaScript screen reader) we let it decide whether to
// act on and consume the event.
return true;
}
} else {
// Clean up if accessibility was disabled after loading the current URL.
mAccessibilityInjector = null;
}
// See if the accessibility injector needs to handle this event.
if (isAccessibilityEnabled()
&& getAccessibilityInjector().handleKeyEventIfNecessary(event)) {
return true;
}
if (isEnterActionKey(keyCode)) {
@@ -5351,7 +5150,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
public void onAttachedToWindow() {
if (mWebView.hasWindowFocus()) setActive(true);
addAccessibilityApisToJavaScript();
if (isAccessibilityEnabled()) {
getAccessibilityInjector().addAccessibilityApisIfNecessary();
}
updateHwAccelerated();
}
@@ -5362,7 +5163,14 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
mZoomManager.dismissZoomPicker();
if (mWebView.hasWindowFocus()) setActive(false);
removeAccessibilityApisFromJavaScript();
if (isAccessibilityEnabled()) {
getAccessibilityInjector().removeAccessibilityApisIfNecessary();
} else {
// Ensure the injector is cleared if we're detaching from the window
// and accessibility is disabled.
mAccessibilityInjector = null;
}
updateHwAccelerated();
if (mWebView.isHardwareAccelerated()) {
@@ -7414,9 +7222,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
break;
case SELECTION_STRING_CHANGED:
if (mAccessibilityInjector != null) {
String selectionString = (String) msg.obj;
mAccessibilityInjector.onSelectionStringChange(selectionString);
if (isAccessibilityEnabled()) {
getAccessibilityInjector()
.handleSelectionChangedIfNecessary((String) msg.obj);
}
break;
@@ -7975,7 +7783,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
mIsBatchingTextChanges = false;
}
private void sendBatchableInputMessage(int what, int arg1, int arg2,
void sendBatchableInputMessage(int what, int arg1, int arg2,
Object obj) {
if (mWebViewCore == null) {
return;
@@ -8395,16 +8203,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc
sendBatchableInputMessage(eventHubAction, direction, 0, event);
}
/**
* @return Whether accessibility script has been injected.
*/
private boolean accessibilityScriptInjected() {
// TODO: Maybe the injected script should announce its presence in
// the page meta-tag so the nativePageShouldHandleShiftAndArrows
// will check that as one of the conditions it looks for
return mAccessibilityScriptInjected;
}
/**
* See {@link WebView#setBackgroundColor(int)}
*/