1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.webkit; 18 19import android.os.Bundle; 20import android.provider.Settings; 21import android.text.TextUtils; 22import android.text.TextUtils.SimpleStringSplitter; 23import android.util.Log; 24import android.view.KeyEvent; 25import android.view.accessibility.AccessibilityEvent; 26import android.view.accessibility.AccessibilityManager; 27import android.view.accessibility.AccessibilityNodeInfo; 28import android.webkit.WebViewCore.EventHub; 29 30import java.util.ArrayList; 31import java.util.Stack; 32 33/** 34 * This class injects accessibility into WebViews with disabled JavaScript or 35 * WebViews with enabled JavaScript but for which we have no accessibility 36 * script to inject. 37 * </p> 38 * Note: To avoid changes in the framework upon changing the available 39 * navigation axis, or reordering the navigation axis, or changing 40 * the key bindings, or defining sequence of actions to be bound to 41 * a given key this class is navigation axis agnostic. It is only 42 * aware of one navigation axis which is in fact the default behavior 43 * of webViews while using the DPAD/TrackBall. 44 * </p> 45 * In general a key binding is a mapping from modifiers + key code to 46 * a sequence of actions. For more detail how to specify key bindings refer to 47 * {@link android.provider.Settings.Secure#ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS}. 48 * </p> 49 * The possible actions are invocations to 50 * {@link #setCurrentAxis(int, boolean, String)}, or 51 * {@link #traverseCurrentAxis(int, boolean, String)} 52 * {@link #traverseGivenAxis(int, int, boolean, String)} 53 * {@link #performAxisTransition(int, int, boolean, String)} 54 * referred via the values of: 55 * {@link #ACTION_SET_CURRENT_AXIS}, 56 * {@link #ACTION_TRAVERSE_CURRENT_AXIS}, 57 * {@link #ACTION_TRAVERSE_GIVEN_AXIS}, 58 * {@link #ACTION_PERFORM_AXIS_TRANSITION}, 59 * respectively. 60 * The arguments for the action invocation are specified as offset 61 * hexademical pairs. Note the last argument of the invocation 62 * should NOT be specified in the binding as it is provided by 63 * this class. For details about the key binding implementation 64 * refer to {@link AccessibilityWebContentKeyBinding}. 65 */ 66class AccessibilityInjectorFallback { 67 private static final String LOG_TAG = "AccessibilityInjector"; 68 69 private static final boolean DEBUG = true; 70 71 private static final int ACTION_SET_CURRENT_AXIS = 0; 72 private static final int ACTION_TRAVERSE_CURRENT_AXIS = 1; 73 private static final int ACTION_TRAVERSE_GIVEN_AXIS = 2; 74 private static final int ACTION_PERFORM_AXIS_TRANSITION = 3; 75 private static final int ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS = 4; 76 77 // WebView navigation axes from WebViewCore.h, plus an additional axis for 78 // the default behavior. 79 private static final int NAVIGATION_AXIS_CHARACTER = 0; 80 private static final int NAVIGATION_AXIS_WORD = 1; 81 private static final int NAVIGATION_AXIS_SENTENCE = 2; 82 @SuppressWarnings("unused") 83 private static final int NAVIGATION_AXIS_HEADING = 3; 84 private static final int NAVIGATION_AXIS_SIBLING = 5; 85 @SuppressWarnings("unused") 86 private static final int NAVIGATION_AXIS_PARENT_FIRST_CHILD = 5; 87 private static final int NAVIGATION_AXIS_DOCUMENT = 6; 88 private static final int NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR = 7; 89 90 // WebView navigation directions from WebViewCore.h. 91 private static final int NAVIGATION_DIRECTION_BACKWARD = 0; 92 private static final int NAVIGATION_DIRECTION_FORWARD = 1; 93 94 // these are the same for all instances so make them process wide 95 private static ArrayList<AccessibilityWebContentKeyBinding> sBindings = 96 new ArrayList<AccessibilityWebContentKeyBinding>(); 97 98 // handle to the WebViewClassic this injector is associated with. 99 private final WebViewClassic mWebView; 100 private final WebView mWebViewInternal; 101 102 // events scheduled for sending as soon as we receive the selected text 103 private final Stack<AccessibilityEvent> mScheduledEventStack = new Stack<AccessibilityEvent>(); 104 105 // the current traversal axis 106 private int mCurrentAxis = 2; // sentence 107 108 // we need to consume the up if we have handled the last down 109 private boolean mLastDownEventHandled; 110 111 // getting two empty selection strings in a row we let the WebView handle the event 112 private boolean mIsLastSelectionStringNull; 113 114 // keep track of last direction 115 private int mLastDirection; 116 117 /** 118 * Creates a new injector associated with a given {@link WebViewClassic}. 119 * 120 * @param webView The associated WebViewClassic. 121 */ 122 public AccessibilityInjectorFallback(WebViewClassic webView) { 123 mWebView = webView; 124 mWebViewInternal = mWebView.getWebView(); 125 ensureWebContentKeyBindings(); 126 } 127 128 /** 129 * Processes a key down <code>event</code>. 130 * 131 * @return True if the event was processed. 132 */ 133 public boolean onKeyEvent(KeyEvent event) { 134 // We do not handle ENTER in any circumstances. 135 if (isEnterActionKey(event.getKeyCode())) { 136 return false; 137 } 138 139 if (event.getAction() == KeyEvent.ACTION_UP) { 140 return mLastDownEventHandled; 141 } 142 143 mLastDownEventHandled = false; 144 145 AccessibilityWebContentKeyBinding binding = null; 146 for (AccessibilityWebContentKeyBinding candidate : sBindings) { 147 if (event.getKeyCode() == candidate.getKeyCode() 148 && event.hasModifiers(candidate.getModifiers())) { 149 binding = candidate; 150 break; 151 } 152 } 153 154 if (binding == null) { 155 return false; 156 } 157 158 for (int i = 0, count = binding.getActionCount(); i < count; i++) { 159 int actionCode = binding.getActionCode(i); 160 String contentDescription = Integer.toHexString(binding.getAction(i)); 161 switch (actionCode) { 162 case ACTION_SET_CURRENT_AXIS: 163 int axis = binding.getFirstArgument(i); 164 boolean sendEvent = (binding.getSecondArgument(i) == 1); 165 setCurrentAxis(axis, sendEvent, contentDescription); 166 mLastDownEventHandled = true; 167 break; 168 case ACTION_TRAVERSE_CURRENT_AXIS: 169 int direction = binding.getFirstArgument(i); 170 // on second null selection string in same direction - WebView handles the event 171 if (direction == mLastDirection && mIsLastSelectionStringNull) { 172 mIsLastSelectionStringNull = false; 173 return false; 174 } 175 mLastDirection = direction; 176 sendEvent = (binding.getSecondArgument(i) == 1); 177 mLastDownEventHandled = traverseCurrentAxis(direction, sendEvent, 178 contentDescription); 179 break; 180 case ACTION_TRAVERSE_GIVEN_AXIS: 181 direction = binding.getFirstArgument(i); 182 // on second null selection string in same direction => WebView handle the event 183 if (direction == mLastDirection && mIsLastSelectionStringNull) { 184 mIsLastSelectionStringNull = false; 185 return false; 186 } 187 mLastDirection = direction; 188 axis = binding.getSecondArgument(i); 189 sendEvent = (binding.getThirdArgument(i) == 1); 190 traverseGivenAxis(direction, axis, sendEvent, contentDescription); 191 mLastDownEventHandled = true; 192 break; 193 case ACTION_PERFORM_AXIS_TRANSITION: 194 int fromAxis = binding.getFirstArgument(i); 195 int toAxis = binding.getSecondArgument(i); 196 sendEvent = (binding.getThirdArgument(i) == 1); 197 performAxisTransition(fromAxis, toAxis, sendEvent, contentDescription); 198 mLastDownEventHandled = true; 199 break; 200 case ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS: 201 // This is a special case since we treat the default WebView navigation 202 // behavior as one of the possible navigation axis the user can use. 203 // If we are not on the default WebView navigation axis this is NOP. 204 if (mCurrentAxis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) { 205 // While WebVew handles navigation we do not get null selection 206 // strings so do not check for that here as the cases above. 207 mLastDirection = binding.getFirstArgument(i); 208 sendEvent = (binding.getSecondArgument(i) == 1); 209 traverseGivenAxis(mLastDirection, NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR, 210 sendEvent, contentDescription); 211 mLastDownEventHandled = false; 212 } else { 213 mLastDownEventHandled = true; 214 } 215 break; 216 default: 217 Log.w(LOG_TAG, "Unknown action code: " + actionCode); 218 } 219 } 220 221 return mLastDownEventHandled; 222 } 223 224 /** 225 * Set the current navigation axis which will be used while 226 * calling {@link #traverseCurrentAxis(int, boolean, String)}. 227 * 228 * @param axis The axis to set. 229 * @param sendEvent Whether to send an accessibility event to 230 * announce the change. 231 */ 232 private void setCurrentAxis(int axis, boolean sendEvent, String contentDescription) { 233 mCurrentAxis = axis; 234 if (sendEvent) { 235 final AccessibilityEvent event = getPartialyPopulatedAccessibilityEvent( 236 AccessibilityEvent.TYPE_ANNOUNCEMENT); 237 event.getText().add(String.valueOf(axis)); 238 event.setContentDescription(contentDescription); 239 sendAccessibilityEvent(event); 240 } 241 } 242 243 /** 244 * Performs conditional transition one axis to another. 245 * 246 * @param fromAxis The axis which must be the current for the transition to occur. 247 * @param toAxis The axis to which to transition. 248 * @param sendEvent Flag if to send an event to announce successful transition. 249 * @param contentDescription A description of the performed action. 250 */ 251 private void performAxisTransition(int fromAxis, int toAxis, boolean sendEvent, 252 String contentDescription) { 253 if (mCurrentAxis == fromAxis) { 254 setCurrentAxis(toAxis, sendEvent, contentDescription); 255 } 256 } 257 258 /** 259 * Traverse the document along the current navigation axis. 260 * 261 * @param direction The direction of traversal. 262 * @param sendEvent Whether to send an accessibility event to 263 * announce the change. 264 * @param contentDescription A description of the performed action. 265 * @see #setCurrentAxis(int, boolean, String) 266 */ 267 private boolean traverseCurrentAxis(int direction, boolean sendEvent, 268 String contentDescription) { 269 return traverseGivenAxis(direction, mCurrentAxis, sendEvent, contentDescription); 270 } 271 272 boolean performAccessibilityAction(int action, Bundle arguments) { 273 switch (action) { 274 case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: 275 case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: { 276 final int direction = getDirectionForAction(action); 277 final int axis = getAxisForGranularity(arguments.getInt( 278 AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT)); 279 return traverseGivenAxis(direction, axis, true, null); 280 } 281 case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: 282 case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: { 283 final int direction = getDirectionForAction(action); 284 // TODO: Add support for moving by object. 285 final int axis = NAVIGATION_AXIS_SENTENCE; 286 return traverseGivenAxis(direction, axis, true, null); 287 } 288 default: 289 return false; 290 } 291 } 292 293 /** 294 * Returns the {@link WebView}-defined direction for the given 295 * {@link AccessibilityNodeInfo}-defined action. 296 * 297 * @param action An accessibility action identifier. 298 * @return A web view navigation direction. 299 */ 300 private static int getDirectionForAction(int action) { 301 switch (action) { 302 case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: 303 case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: 304 return NAVIGATION_DIRECTION_FORWARD; 305 case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: 306 case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: 307 return NAVIGATION_DIRECTION_BACKWARD; 308 default: 309 return -1; 310 } 311 } 312 313 /** 314 * Returns the {@link WebView}-defined axis for the given 315 * {@link AccessibilityNodeInfo}-defined granularity. 316 * 317 * @param granularity An accessibility granularity identifier. 318 * @return A web view navigation axis. 319 */ 320 private static int getAxisForGranularity(int granularity) { 321 switch (granularity) { 322 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: 323 return NAVIGATION_AXIS_CHARACTER; 324 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD: 325 return NAVIGATION_AXIS_WORD; 326 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE: 327 return NAVIGATION_AXIS_SENTENCE; 328 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH: 329 // TODO: This should map to object once we implement it. 330 return NAVIGATION_AXIS_SENTENCE; 331 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE: 332 return NAVIGATION_AXIS_DOCUMENT; 333 default: 334 return -1; 335 } 336 } 337 338 /** 339 * Traverse the document along the given navigation axis. 340 * 341 * @param direction The direction of traversal. 342 * @param axis The axis along which to traverse. 343 * @param sendEvent Whether to send an accessibility event to 344 * announce the change. 345 * @param contentDescription A description of the performed action. 346 */ 347 private boolean traverseGivenAxis(int direction, int axis, boolean sendEvent, 348 String contentDescription) { 349 WebViewCore webViewCore = mWebView.getWebViewCore(); 350 if (webViewCore == null) { 351 return false; 352 } 353 354 AccessibilityEvent event = null; 355 if (sendEvent) { 356 event = getPartialyPopulatedAccessibilityEvent( 357 AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); 358 // the text will be set upon receiving the selection string 359 event.setContentDescription(contentDescription); 360 } 361 mScheduledEventStack.push(event); 362 363 // if the axis is the default let WebView handle the event which will 364 // result in cursor ring movement and selection of its content 365 if (axis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) { 366 return false; 367 } 368 369 webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis); 370 return true; 371 } 372 373 /** 374 * Called when the <code>selectionString</code> has changed. 375 */ 376 public void onSelectionStringChange(String selectionString) { 377 if (DEBUG) { 378 Log.d(LOG_TAG, "Selection string: " + selectionString); 379 } 380 mIsLastSelectionStringNull = (selectionString == null); 381 if (mScheduledEventStack.isEmpty()) { 382 return; 383 } 384 AccessibilityEvent event = mScheduledEventStack.pop(); 385 if ((event != null) && (selectionString != null)) { 386 event.getText().add(selectionString); 387 event.setFromIndex(0); 388 event.setToIndex(selectionString.length()); 389 sendAccessibilityEvent(event); 390 } 391 } 392 393 /** 394 * Sends an {@link AccessibilityEvent}. 395 * 396 * @param event The event to send. 397 */ 398 private void sendAccessibilityEvent(AccessibilityEvent event) { 399 if (DEBUG) { 400 Log.d(LOG_TAG, "Dispatching: " + event); 401 } 402 // accessibility may be disabled while waiting for the selection string 403 AccessibilityManager accessibilityManager = 404 AccessibilityManager.getInstance(mWebView.getContext()); 405 if (accessibilityManager.isEnabled()) { 406 accessibilityManager.sendAccessibilityEvent(event); 407 } 408 } 409 410 /** 411 * @return An accessibility event whose members are populated except its 412 * text and content description. 413 */ 414 private AccessibilityEvent getPartialyPopulatedAccessibilityEvent(int eventType) { 415 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 416 mWebViewInternal.onInitializeAccessibilityEvent(event); 417 return event; 418 } 419 420 /** 421 * Ensures that the Web content key bindings are loaded. 422 */ 423 private void ensureWebContentKeyBindings() { 424 if (sBindings.size() > 0) { 425 return; 426 } 427 428 String webContentKeyBindingsString = Settings.Secure.getString( 429 mWebView.getContext().getContentResolver(), 430 Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS); 431 432 SimpleStringSplitter semiColonSplitter = new SimpleStringSplitter(';'); 433 semiColonSplitter.setString(webContentKeyBindingsString); 434 435 while (semiColonSplitter.hasNext()) { 436 String bindingString = semiColonSplitter.next(); 437 if (TextUtils.isEmpty(bindingString)) { 438 Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " 439 + webContentKeyBindingsString); 440 continue; 441 } 442 String[] keyValueArray = bindingString.split("="); 443 if (keyValueArray.length != 2) { 444 Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " + bindingString); 445 continue; 446 } 447 try { 448 long keyCodeAndModifiers = Long.decode(keyValueArray[0].trim()); 449 String[] actionStrings = keyValueArray[1].split(":"); 450 int[] actions = new int[actionStrings.length]; 451 for (int i = 0, count = actions.length; i < count; i++) { 452 actions[i] = Integer.decode(actionStrings[i].trim()); 453 } 454 sBindings.add(new AccessibilityWebContentKeyBinding(keyCodeAndModifiers, actions)); 455 } catch (NumberFormatException nfe) { 456 Log.e(LOG_TAG, "Disregarding malformed key binding: " + bindingString); 457 } 458 } 459 } 460 461 private boolean isEnterActionKey(int keyCode) { 462 return keyCode == KeyEvent.KEYCODE_DPAD_CENTER 463 || keyCode == KeyEvent.KEYCODE_ENTER 464 || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER; 465 } 466 467 /** 468 * Represents a web content key-binding. 469 */ 470 private static final class AccessibilityWebContentKeyBinding { 471 472 private static final int MODIFIERS_OFFSET = 32; 473 private static final long MODIFIERS_MASK = 0xFFFFFFF00000000L; 474 475 private static final int KEY_CODE_OFFSET = 0; 476 private static final long KEY_CODE_MASK = 0x00000000FFFFFFFFL; 477 478 private static final int ACTION_OFFSET = 24; 479 private static final int ACTION_MASK = 0xFF000000; 480 481 private static final int FIRST_ARGUMENT_OFFSET = 16; 482 private static final int FIRST_ARGUMENT_MASK = 0x00FF0000; 483 484 private static final int SECOND_ARGUMENT_OFFSET = 8; 485 private static final int SECOND_ARGUMENT_MASK = 0x0000FF00; 486 487 private static final int THIRD_ARGUMENT_OFFSET = 0; 488 private static final int THIRD_ARGUMENT_MASK = 0x000000FF; 489 490 private final long mKeyCodeAndModifiers; 491 492 private final int [] mActionSequence; 493 494 /** 495 * @return The key code of the binding key. 496 */ 497 public int getKeyCode() { 498 return (int) ((mKeyCodeAndModifiers & KEY_CODE_MASK) >> KEY_CODE_OFFSET); 499 } 500 501 /** 502 * @return The meta state of the binding key. 503 */ 504 public int getModifiers() { 505 return (int) ((mKeyCodeAndModifiers & MODIFIERS_MASK) >> MODIFIERS_OFFSET); 506 } 507 508 /** 509 * @return The number of actions in the key binding. 510 */ 511 public int getActionCount() { 512 return mActionSequence.length; 513 } 514 515 /** 516 * @param index The action for a given action <code>index</code>. 517 */ 518 public int getAction(int index) { 519 return mActionSequence[index]; 520 } 521 522 /** 523 * @param index The action code for a given action <code>index</code>. 524 */ 525 public int getActionCode(int index) { 526 return (mActionSequence[index] & ACTION_MASK) >> ACTION_OFFSET; 527 } 528 529 /** 530 * @param index The first argument for a given action <code>index</code>. 531 */ 532 public int getFirstArgument(int index) { 533 return (mActionSequence[index] & FIRST_ARGUMENT_MASK) >> FIRST_ARGUMENT_OFFSET; 534 } 535 536 /** 537 * @param index The second argument for a given action <code>index</code>. 538 */ 539 public int getSecondArgument(int index) { 540 return (mActionSequence[index] & SECOND_ARGUMENT_MASK) >> SECOND_ARGUMENT_OFFSET; 541 } 542 543 /** 544 * @param index The third argument for a given action <code>index</code>. 545 */ 546 public int getThirdArgument(int index) { 547 return (mActionSequence[index] & THIRD_ARGUMENT_MASK) >> THIRD_ARGUMENT_OFFSET; 548 } 549 550 /** 551 * Creates a new instance. 552 * @param keyCodeAndModifiers The key for the binding (key and modifiers). 553 * @param actionSequence The sequence of action for the binding. 554 */ 555 public AccessibilityWebContentKeyBinding(long keyCodeAndModifiers, int[] actionSequence) { 556 mKeyCodeAndModifiers = keyCodeAndModifiers; 557 mActionSequence = actionSequence; 558 } 559 560 @Override 561 public String toString() { 562 StringBuilder builder = new StringBuilder(); 563 builder.append("modifiers: "); 564 builder.append(getModifiers()); 565 builder.append(", keyCode: "); 566 builder.append(getKeyCode()); 567 builder.append(", actions["); 568 for (int i = 0, count = getActionCount(); i < count; i++) { 569 builder.append("{actionCode"); 570 builder.append(i); 571 builder.append(": "); 572 builder.append(getActionCode(i)); 573 builder.append(", firstArgument: "); 574 builder.append(getFirstArgument(i)); 575 builder.append(", secondArgument: "); 576 builder.append(getSecondArgument(i)); 577 builder.append(", thirdArgument: "); 578 builder.append(getThirdArgument(i)); 579 builder.append("}"); 580 } 581 builder.append("]"); 582 return builder.toString(); 583 } 584 } 585} 586