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