blob: ba16c8a553d02b0dc6d26c247caa4746fe326337 [file] [log] [blame]
/*
* 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.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.
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 {@link WebView}.
*
* @param webView The associated WebView.
*/
public AccessibilityInjector(WebView webView) {
mWebView = webView;
ensureWebContentKeyBindings();
}
/**
* Processes a key down <code>event</code>.
*
* @return True if the event was processed.
*/
public boolean onKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_UP) {
return mLastDownEventHandled;
}
mLastDownEventHandled = false;
int key = event.getMetaState() << AccessibilityWebContentKeyBinding.OFFSET_META_STATE |
event.getKeyCode() << AccessibilityWebContentKeyBinding.OFFSET_KEY_CODE;
AccessibilityWebContentKeyBinding binding = sBindings.get(key);
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 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) {
mIsLastSelectionStringNull = (selectionString == null);
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);
}
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;
}
/**
* 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);
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();
}
}
}