diff options
| -rw-r--r-- | core/java/android/provider/Settings.java | 60 | ||||
| -rw-r--r-- | core/java/android/webkit/AccessibilityInjector.java | 443 | ||||
| -rw-r--r-- | core/java/android/webkit/CallbackProxy.java | 13 | ||||
| -rw-r--r-- | core/java/android/webkit/WebView.java | 138 | ||||
| -rw-r--r-- | core/java/android/webkit/WebViewCore.java | 22 | ||||
| -rw-r--r-- | core/tests/coretests/AndroidManifest.xml | 8 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/webkit/AccessibilityInjectorTest.java | 945 | ||||
| -rw-r--r-- | packages/SettingsProvider/res/values/defaults.xml | 28 | ||||
| -rw-r--r-- | packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java | 41 |
9 files changed, 1623 insertions, 75 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 46bde373b9b3..e1c84effa255 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -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; @@ -2527,6 +2530,60 @@ public final class Settings { "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. * 1 = override 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, diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java index 49ddc19be178..ba16c8a553d0 100644 --- a/core/java/android/webkit/AccessibilityInjector.java +++ b/core/java/android/webkit/AccessibilityInjector.java @@ -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; - // Handle to the WebView this injector is associated with. + // 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 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; + } + + mLastDownEventHandled = false; - // as a proof of concept let us do the simplest example + int key = event.getMetaState() << AccessibilityWebContentKeyBinding.OFFSET_META_STATE | + event.getKeyCode() << AccessibilityWebContentKeyBinding.OFFSET_KEY_CODE; - if (event.getAction() != KeyEvent.ACTION_UP) { + AccessibilityWebContentKeyBinding binding = sBindings.get(key); + if (binding == null) { return false; } - int keyCode = event.getKeyCode(); + 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; + } - switch (keyCode) { - case KeyEvent.KEYCODE_N: - modifySelection("extend", "forward", "sentence"); - break; - case KeyEvent.KEYCODE_P: - modifySelection("extend", "backward", "sentence"); - break; + /** + * 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); } + } - return false; + /** + * 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); - event.getText().add(selectionString); - mWebView.sendAccessibilityEventUnchecked(event); + mIsLastSelectionStringNull = (selectionString == null); + AccessibilityEvent event = mScheduledEventStack.pop(); + if (event != null) { + event.getText().add(selectionString); + 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); + } - if (webViewCore == null) { + /** + * @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; } - 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(); + } } } diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 1b5651b324c8..b00f88cbf85e 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -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; diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index a3a4c43cd961..d4acff892211 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -49,6 +49,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.SystemClock; +import android.provider.Settings; import android.speech.tts.TextToSpeech; import android.text.Selection; import android.text.Spannable; @@ -101,6 +102,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * <p>A View that displays web pages. This class is the basis upon which you @@ -535,6 +538,10 @@ public class WebView extends AbsoluteLayout // JavaScript or ones for which no accessibility script exists private AccessibilityInjector mAccessibilityInjector; + // flag indicating if accessibility script is injected so we + // know to handle Shift and arrows natively first + private boolean mAccessibilityScriptInjected; + // the color used to highlight the touch rectangles private static final int mHightlightColor = 0x33000000; // the round corner for the highlight path @@ -694,6 +701,11 @@ public class WebView extends AbsoluteLayout 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"; @@ -707,6 +719,14 @@ public class WebView extends AbsoluteLayout " 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))"; + + // variable to cache the above pattern in case accessibility is enabled. + private Pattern mMatchAxsUrlParameterPattern; + // Used to match key downs and key ups private boolean mGotKeyDown; @@ -2947,6 +2967,23 @@ public class WebView extends AbsoluteLayout } /** + * Called by CallbackProxy when the page starts loading. + * @param url The URL of the page which has started loading. + */ + /* package */ void onPageStarted(String url) { + // 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 + setCertificate(null); + + // reset the flag since we set to true in if need after + // loading is see onPageFinished(Url) + mAccessibilityScriptInjected = false; + } + + /** * Called by CallbackProxy when the page finishes loading. * @param url The URL of the page which has finished loading. */ @@ -2971,23 +3008,93 @@ public class WebView extends AbsoluteLayout * 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 (AccessibilityManager.getInstance(mContext).isEnabled()) { - if (getSettings().getJavaScriptEnabled()) { + 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 loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); - } else if (mAccessibilityInjector == null) { - mAccessibilityInjector = new AccessibilityInjector(this); + // 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 + loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT); + } 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 && mAccessibilityInjector == null) { + mAccessibilityInjector = new AccessibilityInjector(this); } else { - // it is possible that accessibility was turned off between reloads mAccessibilityInjector = null; } } /** + * 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; + } + + /** * The URL of a page that sent a message to scroll the title bar off screen. * * Many mobile sites tell the page to scroll to (0,1) in order to scroll the @@ -3964,7 +4071,7 @@ public class WebView extends AbsoluteLayout if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - if (nativePageShouldHandleShiftAndArrows()) { + if (pageShouldHandleShiftAndArrows()) { mShiftIsPressed = true; } else if (!nativeCursorWantsKeyEvents() && !mSelectingText) { setUpSelect(); @@ -3984,7 +4091,7 @@ public class WebView extends AbsoluteLayout if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { switchOutDrawHistory(); - if (nativePageShouldHandleShiftAndArrows()) { + if (pageShouldHandleShiftAndArrows()) { letPageHandleNavKey(keyCode, event.getEventTime(), true); return true; } @@ -4118,7 +4225,7 @@ public class WebView extends AbsoluteLayout if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - if (nativePageShouldHandleShiftAndArrows()) { + if (pageShouldHandleShiftAndArrows()) { mShiftIsPressed = false; } else if (copySelection()) { selectionDone(); @@ -4128,7 +4235,7 @@ public class WebView extends AbsoluteLayout if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { - if (nativePageShouldHandleShiftAndArrows()) { + if (pageShouldHandleShiftAndArrows()) { letPageHandleNavKey(keyCode, event.getEventTime(), false); return true; } @@ -5550,7 +5657,8 @@ public class WebView extends AbsoluteLayout } return false; // let common code in onKeyUp at it } - if (mMapTrackballToArrowKeys && mShiftIsPressed == false) { + if ((mMapTrackballToArrowKeys && mShiftIsPressed == false) || + (mAccessibilityInjector != null || mAccessibilityScriptInjected)) { if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent gmail quit"); return false; } @@ -7288,6 +7396,16 @@ public class WebView extends AbsoluteLayout } /** + * @return If the page should receive Shift and arrows. + */ + private boolean pageShouldHandleShiftAndArrows() { + // 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 (nativePageShouldHandleShiftAndArrows() || mAccessibilityScriptInjected); + } + + /** * Set the background color. It's white by default. Pass * zero to make the view transparent. * @param color the ARGB color described by Color.java diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 60ddf0808feb..56d62962f313 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -572,13 +572,12 @@ final class WebViewCore { /** * Modifies the current selection. * - * @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. * * @return The selection string. */ - private native String nativeModifySelection(String alter, String direction, String granularity); + private native String nativeModifySelection(int direction, int granularity); // EventHub for processing messages private final EventHub mEventHub; @@ -724,12 +723,6 @@ final class WebViewCore { boolean mRemember; } - static class ModifySelectionData { - String mAlter; - String mDirection; - String mGranularity; - } - static final String[] HandlerDebugString = { "REVEAL_SELECTION", // 96 "REQUEST_LABEL", // 97 @@ -1270,16 +1263,9 @@ final class WebViewCore { break; case MODIFY_SELECTION: - ModifySelectionData modifySelectionData = - (ModifySelectionData) msg.obj; - String selectionString = nativeModifySelection( - modifySelectionData.mAlter, - modifySelectionData.mDirection, - modifySelectionData.mGranularity); - - mWebView.mPrivateHandler.obtainMessage( - WebView.SELECTION_STRING_CHANGED, selectionString) - .sendToTarget(); + String selectionString = nativeModifySelection(msg.arg1, msg.arg2); + mWebView.mPrivateHandler.obtainMessage(WebView.SELECTION_STRING_CHANGED, + selectionString).sendToTarget(); break; case LISTBOX_CHOICES: diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index f09421bca74f..e449ddb9160a 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1022,7 +1022,13 @@ <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> </service> - + + <service android:name="android.webkit.AccessibilityInjectorTest$MockAccessibilityService"> + <intent-filter> + <action android:name="android.accessibilityservice.AccessibilityService" /> + </intent-filter> + </service> + <activity android:name="android.widget.RadioGroupActivity" android:label="RadioGroupActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> diff --git a/core/tests/coretests/src/android/webkit/AccessibilityInjectorTest.java b/core/tests/coretests/src/android/webkit/AccessibilityInjectorTest.java new file mode 100644 index 000000000000..9c9d9fef752c --- /dev/null +++ b/core/tests/coretests/src/android/webkit/AccessibilityInjectorTest.java @@ -0,0 +1,945 @@ +/* + * 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.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.provider.Settings; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +/** + * This is a test for the behavior of the {@link AccessibilityInjector} + * which is used by {@link WebView} to provide basic accessibility support + * in case JavaScript is disabled. + * </p> + * Note: This test works against the generated {@link AccessibilityEvent}s + * to so it also checks if the test for announcing navigation axis and + * status messages as appropriate. + */ +public class AccessibilityInjectorTest extends AndroidTestCase { + + /** The timeout to wait for the expected selection. */ + private static final long TIMEOUT_WAIT_FOR_SELECTION_STRING = 1000; + + /** The timeout to wait for accessibility and the mock service to be enabled. */ + private static final long TIMEOUT_ENABLE_ACCESSIBILITY_AND_MOCK_SERVICE = 500; + + /** The count of tests to detect when to shut down the service. */ + private static final int TEST_CASE_COUNT = 8; + + /** The meta state for pressed left ALT. */ + private static final int META_STATE_ALT_LEFT_ON = KeyEvent.META_ALT_ON + | KeyEvent.META_ALT_LEFT_ON; + + /** The value for not specified selection string since null is a valid value. */ + private static final String SELECTION_STRING_UNKNOWN = "Unknown"; + + /** Lock for locking the test. */ + private static final Object sTestLock = new Object(); + + /** Handle to the test for use by the mock service. */ + private static AccessibilityInjectorTest sInstance; + + /** Flag indicating if the accessibility service is ready to receive events. */ + private static boolean sIsAccessibilityServiceReady; + + /** The count of executed tests to detect when to toggle accessibility and the service. */ + private static int sExecutedTestCount; + + /** Worker thread with a handler to perform non test thread processing. */ + private Worker mWorker; + + /** Handle to the {@link WebView} to load data in. */ + private WebView mWebView; + + /** The received selection string for assertion checking. */ + private static String sReceivedSelectionString = SELECTION_STRING_UNKNOWN; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mWorker = new Worker(); + sInstance = this; + if (sExecutedTestCount == 0) { + // until JUnit4 comes to play with @BeforeTest + disableAccessibilityAndMockAccessibilityService(); + enableAccessibilityAndMockAccessibilityService(); + } + } + + @Override + protected void tearDown() throws Exception { + if (mWorker != null) { + mWorker.stop(); + } + if (sExecutedTestCount == TEST_CASE_COUNT) { + // until JUnit4 comes to play with @AfterTest + disableAccessibilityAndMockAccessibilityService(); + } + super.tearDown(); + } + + /** + * Tests navigation by character. + */ + @LargeTest + public void testNavigationByCharacter() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<p>" + + "a <b>b</b> c" + + "</p>" + + "<p>" + + "d" + + "<input>e</input>" + + "</p>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // change navigation axis to word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, META_STATE_ALT_LEFT_ON); + assertSelectionString("1"); // expect the word navigation axis + + // change navigation axis to character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, META_STATE_ALT_LEFT_ON); + assertSelectionString("0"); // expect the character navigation axis + + // go to the first character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("a"); + + // go to the second character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<b>b</b>"); + + // go to the third character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("c"); + + // go to the fourth character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("d"); + + // go to the fifth character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("e"); + + // try to go past the last character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString(null); + + // go to the fourth character (reverse) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("d"); + + // go to the third character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("c"); + + // go to the second character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<b>b</b>"); + + // go to the first character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("a"); + + // try to go before the first character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString(null); + + // go to the second character (reverse again) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<b>b</b>"); + } + + /** + * Tests navigation by word. + */ + @LargeTest + public void testNavigationByWord() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<p>" + + "This is <b>a</b> sentence" + + "</p>" + + "<p>" + + " scattered " + + "<input>all</input>" + + " over " + + "</p>" + + "<div>" + + "<button>the place.</button>" + + "</div>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // change navigation axis to word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, META_STATE_ALT_LEFT_ON); + assertSelectionString("1"); // expect the word navigation axis + + // go to the first word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("This"); + + // go to the second word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("is"); + + // go to the third word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<b>a</b>"); + + // go to the fourth word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("sentence"); + + // go to the fifth word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("scattered"); + + // go to the sixth word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("all"); + + // go to the seventh word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("over"); + + // go to the eight word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("the"); + + // go to the ninth word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("place"); + + // NOTE: WebKit selection returns the dot as a word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("."); + + // try to go past the last word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString(null); + + // go to the last word (reverse) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("place"); + + // go to the eight word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("the"); + + // go to the seventh word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("over"); + + // go to the sixth word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("all"); + + // go to the fifth word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("scattered"); + + // go to the fourth word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("sentence"); + + // go to the third word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<b>a</b>"); + + // go to the second word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("is"); + + // go to the first word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("This"); + + // try to go before the first word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString(null); + + // go to the second word (reverse again) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("is"); + } + + /** + * Tests navigation by sentence. + */ + @LargeTest + public void testNavigationBySentence() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<div>" + + "<p>" + + "This is the first sentence of the first paragraph and has an <b>inline bold tag</b>." + + "This is the second sentence of the first paragraph." + + "</p>" + + "<h1>This is a heading</h1>" + + "<p>" + + "This is the first sentence of the second paragraph." + + "This is the second sentence of the second paragraph." + + "</p>" + + "</div>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // Sentence axis is the default + + // go to the first sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("This is the first sentence of the first paragraph and has an " + + "<b>inline bold tag</b>."); + + // go to the second sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("This is the second sentence of the first paragraph."); + + // go to the third sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("This is a heading"); + + // go to the fourth sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("This is the first sentence of the second paragraph."); + + // go to the fifth sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("This is the second sentence of the second paragraph."); + + // try to go past the last sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString(null); + + // go to the fourth sentence (reverse) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("This is the first sentence of the second paragraph."); + + // go to the third sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("This is a heading"); + + // go to the second sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("This is the second sentence of the first paragraph."); + + // go to the first sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("This is the first sentence of the first paragraph and has an " + + "<b>inline bold tag</b>."); + + // try to go before the first sentence + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString(null); + + // go to the second sentence (reverse again) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("This is the second sentence of the first paragraph."); + } + + /** + * Tests navigation by heading. + */ + @LargeTest + public void testNavigationByHeading() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<!DOCTYPE html>" + + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<h1>Heading one</h1>" + + "<p>" + + "This is some text" + + "</p>" + + "<h2>Heading two</h2>" + + "<p>" + + "This is some text" + + "</p>" + + "<h3>Heading three</h3>" + + "<p>" + + "This is some text" + + "</p>" + + "<h4>Heading four</h4>" + + "<p>" + + "This is some text" + + "</p>" + + "<h5>Heading five</h5>" + + "<p>" + + "This is some text" + + "</p>" + + "<h6>Heading six</h6>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // change navigation axis to heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_RIGHT, META_STATE_ALT_LEFT_ON); + assertSelectionString("3"); // expect the heading navigation axis + + // go to the first heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h1>Heading one</h1>"); + + // go to the second heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h2>Heading two</h2>"); + + // go to the third heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h3>Heading three</h3>"); + + // go to the fourth heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h4>Heading four</h4>"); + + // go to the fifth heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h5>Heading five</h5>"); + + // go to the sixth heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h6>Heading six</h6>"); + + // try to go past the last heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString(null); + + // go to the fifth heading (reverse) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<h5>Heading five</h5>"); + + // go to the fourth heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<h4>Heading four</h4>"); + + // go to the third heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<h3>Heading three</h3>"); + + // go to the second heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<h2>Heading two</h2>"); + + // go to the first heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<h1>Heading one</h1>"); + + // try to go before the first heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString(null); + + // go to the second heading (reverse again) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h2>Heading two</h2>"); + } + + /** + * Tests navigation by sibling. + */ + @LargeTest + public void testNavigationBySibing() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<!DOCTYPE html>" + + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<h1>Heading one</h1>" + + "<p>" + + "This is some text" + + "</p>" + + "<div>" + + "<button>Input</button>" + + "</div>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // change navigation axis to heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_RIGHT, META_STATE_ALT_LEFT_ON); + assertSelectionString("3"); // expect the heading navigation axis + + // change navigation axis to sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_RIGHT, META_STATE_ALT_LEFT_ON); + assertSelectionString("4"); // expect the sibling navigation axis + + // change navigation axis to parent/first child + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_RIGHT, META_STATE_ALT_LEFT_ON); + assertSelectionString("5"); // expect the parent/first child navigation axis + + // go to the first child of the body + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<h1>Heading one</h1>"); + + // change navigation axis to sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_LEFT, META_STATE_ALT_LEFT_ON); + assertSelectionString("4"); // expect the sibling navigation axis + + // go to the next sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<p>This is some text</p>"); + + // go to the next sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<div><button>Input</button></div>"); + + // try to go past the last sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString(null); + + // go to the previous sibling (reverse) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<p>This is some text</p>"); + + // go to the previous sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<h1>Heading one</h1>"); + + // try to go before the previous sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString(null); + + // go to the next sibling (reverse again) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<p>This is some text</p>"); + } + + /** + * Tests navigation by parent/first child. + */ + @LargeTest + public void testNavigationByParentFirstChild() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<!DOCTYPE html>" + + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<div>" + + "<button>Input</button>" + + "</div>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // change navigation axis to document + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_LEFT, META_STATE_ALT_LEFT_ON); + assertSelectionString("6"); // expect the document navigation axis + + // change navigation axis to parent/first child + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_LEFT, META_STATE_ALT_LEFT_ON); + assertSelectionString("5"); // expect the parent/first child navigation axis + + // go to the first child + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<div><button>Input</button></div>"); + + // go to the first child + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<button>Input</button>"); + + // try to go to the first child of a leaf element + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString(null); + + // go to the parent (reverse) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<div><button>Input</button></div>"); + + // go to the parent + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<body><div><button>Input</button></div></body>"); + + // try to go to the body parent + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString(null); + + // go to the first child (reverse again) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<div><button>Input</button></div>"); + } + + /** + * Tests navigation by document. + */ + @LargeTest + public void testNavigationByDocument() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<!DOCTYPE html>" + + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<button>Click</button>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // change navigation axis to document + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_LEFT, META_STATE_ALT_LEFT_ON); + assertSelectionString("6"); // expect the document navigation axis + + // go to the bottom of the document + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("Click"); + + // go to the top of the document (reverse) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, 0); + assertSelectionString("<body><button>Click</button></body>"); + + // go to the bottom of the document (reverse again) + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("Click"); + } + + /** + * Tests the sync between the text navigation and navigation by DOM elements. + */ + @LargeTest + public void testSyncBetweenTextAndDomNodeNavigation() throws Exception { + // a bit ugly but helps detect beginning and end of all tests so accessibility + // and the mock service are not toggled on every test (expensive) + sExecutedTestCount++; + + String html = + "<!DOCTYPE html>" + + "<html>" + + "<head>" + + "</head>" + + "<body>" + + "<p>" + + "First" + + "</p>" + + "<button>Second</button>" + + "<p>" + + "Third" + + "</p>" + + "</body>" + + "</html>"; + + WebView webView = createWebVewWithHtml(html); + + // change navigation axis to word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, META_STATE_ALT_LEFT_ON); + assertSelectionString("1"); // expect the word navigation axis + + // go to the first word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("First"); + + // change navigation axis to heading + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_RIGHT, META_STATE_ALT_LEFT_ON); + assertSelectionString("3"); // expect the heading navigation axis + + // change navigation axis to sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_RIGHT, META_STATE_ALT_LEFT_ON); + assertSelectionString("4"); // expect the sibling navigation axis + + // go to the next sibling + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("<button>Second</button>"); + + // change navigation axis to character + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, META_STATE_ALT_LEFT_ON); + assertSelectionString("0"); // expect the character navigation axis + + // change navigation axis to word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_UP, META_STATE_ALT_LEFT_ON); + assertSelectionString("1"); // expect the word navigation axis + + // go to the next word + sendKeyEvent(webView, KeyEvent.KEYCODE_DPAD_DOWN, 0); + assertSelectionString("Third"); + } + + /** + * Enable accessibility and the mock accessibility service. + */ + private void enableAccessibilityAndMockAccessibilityService() { + // make sure the manager is instantiated so the system initializes it + AccessibilityManager.getInstance(getContext()); + + // enable accessibility and the mock accessibility service + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 1); + String enabledServices = new ComponentName(getContext().getPackageName(), + MockAccessibilityService.class.getName()).flattenToShortString(); + Settings.Secure.putString(getContext().getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, enabledServices); + + // poll within a timeout and let be interrupted in case of success + long incrementStep = TIMEOUT_ENABLE_ACCESSIBILITY_AND_MOCK_SERVICE / 5; + long start = SystemClock.uptimeMillis(); + while (SystemClock.uptimeMillis() - start < TIMEOUT_ENABLE_ACCESSIBILITY_AND_MOCK_SERVICE && + !sIsAccessibilityServiceReady) { + synchronized (sTestLock) { + try { + sTestLock.wait(incrementStep); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } + + if (!sIsAccessibilityServiceReady) { + throw new IllegalStateException("MockAccessibilityService not ready. Did you add " + + "tests and forgot to update AccessibilityInjectorTest#TEST_CASE_COUNT?"); + } + } + + @Override + protected void scrubClass(Class<?> testCaseClass) { + /* do nothing - avoid superclass behavior */ + } + + /** + * Disables accessibility and the mock accessibility service. + */ + private void disableAccessibilityAndMockAccessibilityService() { + // disable accessibility and the mock accessibility service + Settings.Secure.putInt(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED, 0); + Settings.Secure.putString(getContext().getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, ""); + } + + /** + * Asserts the next <code>expectedSelectionString</code> to be received. + */ + private void assertSelectionString(String expectedSelectionString) { + assertTrue("MockAccessibilityService not ready", sIsAccessibilityServiceReady); + + long incrementStep = TIMEOUT_WAIT_FOR_SELECTION_STRING / 5; + long start = SystemClock.uptimeMillis(); + while (SystemClock.uptimeMillis() - start < TIMEOUT_WAIT_FOR_SELECTION_STRING && + sReceivedSelectionString == SELECTION_STRING_UNKNOWN) { + synchronized (sTestLock) { + try { + sTestLock.wait(incrementStep); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } + try { + if (sReceivedSelectionString == SELECTION_STRING_UNKNOWN) { + fail("No selection string received. Expected: " + expectedSelectionString); + } + assertEquals(expectedSelectionString, sReceivedSelectionString); + } finally { + sReceivedSelectionString = SELECTION_STRING_UNKNOWN; + } + } + + /** + * Sends a {@link KeyEvent} (up and down) to the {@link WebView}. + * + * @param keyCode The event key code. + */ + private void sendKeyEvent(WebView webView, int keyCode, int metaState) { + webView.onKeyDown(keyCode, new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 1, metaState)); + webView.onKeyUp(keyCode, new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 1, metaState)); + } + + /** + * Creates a {@link WebView} with with a given HTML content. + * + * @param html The HTML content; + * @return The created view. + */ + private WebView createWebVewWithHtml(final String html) { + mWorker.getHandler().post(new Runnable() { + public void run() { + mWebView = new WebView(getContext()); + mWebView.loadData(html, "text/html", "utf-8"); + mWebView.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(WebView view, String url) { + mWorker.getHandler().post(new Runnable() { + public void run() { + synchronized (sTestLock) { + sTestLock.notifyAll(); + } + } + }); + } + }); + } + }); + synchronized (sTestLock) { + try { + sTestLock.wait(); + } catch (InterruptedException ie) { + /* ignore */ + } + } + return mWebView; + } + + /** + * This is a worker thread responsible for creating the {@link WebView}. + */ + private class Worker implements Runnable { + private final Object mWorkerLock = new Object(); + private Handler mHandler; + + public Worker() { + new Thread(this).start(); + synchronized (mWorkerLock) { + while (mHandler == null) { + try { + mWorkerLock.wait(); + } catch (InterruptedException ex) { + /* ignore */ + } + } + } + } + + public void run() { + synchronized (mWorkerLock) { + Looper.prepare(); + mHandler = new Handler(); + mWorkerLock.notifyAll(); + } + Looper.loop(); + } + + public Handler getHandler() { + return mHandler; + } + + public void stop() { + mHandler.getLooper().quit(); + } + } + + /** + * Mock accessibility service to receive the accessibility events + * with the current {@link WebView} selection. + */ + public static class MockAccessibilityService extends AccessibilityService { + private boolean mIsServiceInfoSet; + + @Override + protected void onServiceConnected() { + if (mIsServiceInfoSet) { + return; + } + AccessibilityServiceInfo info = new AccessibilityServiceInfo(); + info.eventTypes = AccessibilityEvent.TYPE_VIEW_SELECTED; + info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC; + setServiceInfo(info); + mIsServiceInfoSet = true; + + sIsAccessibilityServiceReady = true; + + if (sInstance == null) { + return; + } + synchronized (sTestLock) { + sTestLock.notifyAll(); + } + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + if (sInstance == null) { + return; + } + if (!event.getText().isEmpty()) { + CharSequence text = event.getText().get(0); + sReceivedSelectionString = (text != null) ? text.toString() : null; + } + synchronized (sTestLock) { + sTestLock.notifyAll(); + } + } + + @Override + public void onInterrupt() { + /* do nothing */ + } + + @Override + public boolean onUnbind(Intent intent) { + sIsAccessibilityServiceReady = false; + return false; + } + } +} diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 2ae34e33ae05..43bb26ab37a4 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -74,4 +74,32 @@ <bool name="def_vibrate_in_silent">true</bool> <bool name="def_use_ptp_interface">false</bool> + + <!-- Default for Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION --> + <bool name="def_accessibility_script_injection">false</bool> + + <!-- Default for Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS --> + <string name="def_accessibility_web_content_key_bindings"> + <!-- DPAD/Trackball UP maps to traverse previous on current axis and send an event. --> + 0x13=0x01000100; + <!-- DPAD/Trackball DOWN maps to traverse next on current axis and send an event. --> + 0x14=0x01010100; + <!-- DPAD/Trackball LEFT maps to action in non-android default navigation axis. --> + 0x15=0x04000000; + <!-- DPAD/Trackball RIGHT maps to no action in non-android default navigation axis. --> + 0x16=0x04000000; + <!-- Left Alt+DPAD/Trackball UP transitions from an axis to another and sends an event. --> + <!-- Axis transitions: 2 -> 7; 1 -> 2; 0 -> 1; 3 -> 0; 4 -> 0; 5 -> 0; 6 -> 0; --> + 0x120013=0x03020701:0x03010201:0x03000101:0x03030001:0x03040001:0x03050001:0x03060001; + <!-- Left Alt+DPAD/Trackball DOWN transitions from an axis to another and sends an event. --> + <!-- Axis transitions: 1 -> 0; 2 -> 1; 7 -> 2; 3 -> 7; 4 -> 7; 5 -> 7; 6 -> 7; --> + 0x120014=0x03010001:0x03020101:0x03070201:0x03030701:0x03040701:0x03050701:0x03060701; + <!-- Left Alt+DPAD/Trackball LEFT transitions from an axis to another and sends an event. --> + <!-- Axis transitions: 4 -> 3; 5 -> 4; 6 -> 5; 0 -> 6; 1 -> 6; 2 -> 6; 7 -> 6; --> + 0x120015=0x03040301:0x03050401:0x03060501:0x03000601:0x03010601:0x03020601:0x03070601; + <!-- Left Alt+DPAD/Trackball RIGHT transitions from an axis to another and sends an event. --> + <!-- Axis transitions: 5 -> 6; 4 -> 5; 3 -> 4; 2 -> 3; 7 -> 3; 1 -> 3; 0 -> 3; --> + 0x120016=0x03050601:0x03040501:0x03030401:0x03020301:0x03070301:0x03010301:0x03000301; + </string> + </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java index c1ad1ca10dbe..8eb3fe69bf98 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java @@ -21,7 +21,6 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.XmlResourceParser; import android.database.Cursor; @@ -35,10 +34,7 @@ import android.os.SystemProperties; import android.provider.Settings; import android.provider.Settings.Secure; import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Config; import android.util.Log; -import android.util.Xml; import com.android.internal.content.PackageHelper; import com.android.internal.telephony.RILConstants; @@ -64,7 +60,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { // database gets upgraded properly. At a minimum, please confirm that 'upgradeVersion' // is properly propagated through your change. Not doing so will result in a loss of user // settings. - private static final int DATABASE_VERSION = 57; + private static final int DATABASE_VERSION = 58; private Context mContext; @@ -734,6 +730,33 @@ public class DatabaseHelper extends SQLiteOpenHelper { } upgradeVersion = 57; } + + if (upgradeVersion == 57) { + /* + * New settings to: + * 1. Enable injection of accessibility scripts in WebViews. + * 2. Define the key bindings for traversing web content in WebViews. + */ + db.beginTransaction(); + SQLiteStatement stmt = null; + try { + stmt = db.compileStatement("INSERT INTO secure(name,value)" + + " VALUES(?,?);"); + loadBooleanSetting(stmt, Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + R.bool.def_accessibility_script_injection); + stmt.close(); + stmt = db.compileStatement("INSERT INTO secure(name,value)" + + " VALUES(?,?);"); + loadStringSetting(stmt, Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS, + R.string.def_accessibility_web_content_key_bindings); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + if (stmt != null) stmt.close(); + } + upgradeVersion = 58; + } + // *** Remember to update DATABASE_VERSION above! if (upgradeVersion != currentVersion) { @@ -876,7 +899,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { String cls = parser.getAttributeValue(null, "class"); String shortcutStr = parser.getAttributeValue(null, "shortcut"); - int shortcutValue = (int) shortcutStr.charAt(0); + int shortcutValue = shortcutStr.charAt(0); if (TextUtils.isEmpty(shortcutStr)) { Log.w(TAG, "Unable to get shortcut for: " + pkg + "/" + cls); } @@ -1187,6 +1210,12 @@ public class DatabaseHelper extends SQLiteOpenHelper { loadBooleanSetting(stmt, Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED, R.bool.def_mount_ums_notify_enabled); + + loadBooleanSetting(stmt, Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, + R.bool.def_accessibility_script_injection); + + loadStringSetting(stmt, Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS, + R.string.def_accessibility_web_content_key_bindings); } finally { if (stmt != null) stmt.close(); } |