1525823a7548d73c9ec9ba3ae84663ab286b20205alanv/*
2525823a7548d73c9ec9ba3ae84663ab286b20205alanv * Copyright (C) 2010 The Android Open Source Project
3525823a7548d73c9ec9ba3ae84663ab286b20205alanv *
4525823a7548d73c9ec9ba3ae84663ab286b20205alanv * Licensed under the Apache License, Version 2.0 (the "License");
5525823a7548d73c9ec9ba3ae84663ab286b20205alanv * you may not use this file except in compliance with the License.
6525823a7548d73c9ec9ba3ae84663ab286b20205alanv * You may obtain a copy of the License at
7525823a7548d73c9ec9ba3ae84663ab286b20205alanv *
8525823a7548d73c9ec9ba3ae84663ab286b20205alanv *      http://www.apache.org/licenses/LICENSE-2.0
9525823a7548d73c9ec9ba3ae84663ab286b20205alanv *
10525823a7548d73c9ec9ba3ae84663ab286b20205alanv * Unless required by applicable law or agreed to in writing, software
11525823a7548d73c9ec9ba3ae84663ab286b20205alanv * distributed under the License is distributed on an "AS IS" BASIS,
12525823a7548d73c9ec9ba3ae84663ab286b20205alanv * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13525823a7548d73c9ec9ba3ae84663ab286b20205alanv * See the License for the specific language governing permissions and
14525823a7548d73c9ec9ba3ae84663ab286b20205alanv * limitations under the License.
15525823a7548d73c9ec9ba3ae84663ab286b20205alanv */
16525823a7548d73c9ec9ba3ae84663ab286b20205alanv
17525823a7548d73c9ec9ba3ae84663ab286b20205alanvpackage android.webkit;
18525823a7548d73c9ec9ba3ae84663ab286b20205alanv
196a62b77dfb95364e863b44662c13d6deec47f7d2alanvimport android.os.Bundle;
20525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.provider.Settings;
21525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.text.TextUtils;
22525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.text.TextUtils.SimpleStringSplitter;
23525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.util.Log;
24525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.view.KeyEvent;
25525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.view.accessibility.AccessibilityEvent;
26525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.view.accessibility.AccessibilityManager;
276a62b77dfb95364e863b44662c13d6deec47f7d2alanvimport android.view.accessibility.AccessibilityNodeInfo;
28525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport android.webkit.WebViewCore.EventHub;
29525823a7548d73c9ec9ba3ae84663ab286b20205alanv
30525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport java.util.ArrayList;
31525823a7548d73c9ec9ba3ae84663ab286b20205alanvimport java.util.Stack;
32525823a7548d73c9ec9ba3ae84663ab286b20205alanv
33525823a7548d73c9ec9ba3ae84663ab286b20205alanv/**
34525823a7548d73c9ec9ba3ae84663ab286b20205alanv * This class injects accessibility into WebViews with disabled JavaScript or
35525823a7548d73c9ec9ba3ae84663ab286b20205alanv * WebViews with enabled JavaScript but for which we have no accessibility
36525823a7548d73c9ec9ba3ae84663ab286b20205alanv * script to inject.
37525823a7548d73c9ec9ba3ae84663ab286b20205alanv * </p>
38525823a7548d73c9ec9ba3ae84663ab286b20205alanv * Note: To avoid changes in the framework upon changing the available
39525823a7548d73c9ec9ba3ae84663ab286b20205alanv *       navigation axis, or reordering the navigation axis, or changing
40525823a7548d73c9ec9ba3ae84663ab286b20205alanv *       the key bindings, or defining sequence of actions to be bound to
41525823a7548d73c9ec9ba3ae84663ab286b20205alanv *       a given key this class is navigation axis agnostic. It is only
42525823a7548d73c9ec9ba3ae84663ab286b20205alanv *       aware of one navigation axis which is in fact the default behavior
43525823a7548d73c9ec9ba3ae84663ab286b20205alanv *       of webViews while using the DPAD/TrackBall.
44525823a7548d73c9ec9ba3ae84663ab286b20205alanv * </p>
45525823a7548d73c9ec9ba3ae84663ab286b20205alanv * In general a key binding is a mapping from modifiers + key code to
46525823a7548d73c9ec9ba3ae84663ab286b20205alanv * a sequence of actions. For more detail how to specify key bindings refer to
47525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link android.provider.Settings.Secure#ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS}.
48525823a7548d73c9ec9ba3ae84663ab286b20205alanv * </p>
49525823a7548d73c9ec9ba3ae84663ab286b20205alanv * The possible actions are invocations to
50525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link #setCurrentAxis(int, boolean, String)}, or
51525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link #traverseCurrentAxis(int, boolean, String)}
52525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link #traverseGivenAxis(int, int, boolean, String)}
536a62b77dfb95364e863b44662c13d6deec47f7d2alanv * {@link #performAxisTransition(int, int, boolean, String)}
54525823a7548d73c9ec9ba3ae84663ab286b20205alanv * referred via the values of:
55525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link #ACTION_SET_CURRENT_AXIS},
56525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link #ACTION_TRAVERSE_CURRENT_AXIS},
57525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link #ACTION_TRAVERSE_GIVEN_AXIS},
58525823a7548d73c9ec9ba3ae84663ab286b20205alanv * {@link #ACTION_PERFORM_AXIS_TRANSITION},
59525823a7548d73c9ec9ba3ae84663ab286b20205alanv * respectively.
60525823a7548d73c9ec9ba3ae84663ab286b20205alanv * The arguments for the action invocation are specified as offset
61525823a7548d73c9ec9ba3ae84663ab286b20205alanv * hexademical pairs. Note the last argument of the invocation
62525823a7548d73c9ec9ba3ae84663ab286b20205alanv * should NOT be specified in the binding as it is provided by
63525823a7548d73c9ec9ba3ae84663ab286b20205alanv * this class. For details about the key binding implementation
64525823a7548d73c9ec9ba3ae84663ab286b20205alanv * refer to {@link AccessibilityWebContentKeyBinding}.
65525823a7548d73c9ec9ba3ae84663ab286b20205alanv */
66525823a7548d73c9ec9ba3ae84663ab286b20205alanvclass AccessibilityInjectorFallback {
67525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final String LOG_TAG = "AccessibilityInjector";
68525823a7548d73c9ec9ba3ae84663ab286b20205alanv
69525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final boolean DEBUG = true;
70525823a7548d73c9ec9ba3ae84663ab286b20205alanv
71525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final int ACTION_SET_CURRENT_AXIS = 0;
72525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final int ACTION_TRAVERSE_CURRENT_AXIS = 1;
73525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final int ACTION_TRAVERSE_GIVEN_AXIS = 2;
74525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final int ACTION_PERFORM_AXIS_TRANSITION = 3;
75525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final int ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS = 4;
76525823a7548d73c9ec9ba3ae84663ab286b20205alanv
776a62b77dfb95364e863b44662c13d6deec47f7d2alanv    // WebView navigation axes from WebViewCore.h, plus an additional axis for
786a62b77dfb95364e863b44662c13d6deec47f7d2alanv    // the default behavior.
796a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_AXIS_CHARACTER = 0;
806a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_AXIS_WORD = 1;
816a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_AXIS_SENTENCE = 2;
826a62b77dfb95364e863b44662c13d6deec47f7d2alanv    @SuppressWarnings("unused")
836a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_AXIS_HEADING = 3;
846a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_AXIS_SIBLING = 5;
856a62b77dfb95364e863b44662c13d6deec47f7d2alanv    @SuppressWarnings("unused")
866a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_AXIS_PARENT_FIRST_CHILD = 5;
876a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_AXIS_DOCUMENT = 6;
88525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final int NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR = 7;
89525823a7548d73c9ec9ba3ae84663ab286b20205alanv
906a62b77dfb95364e863b44662c13d6deec47f7d2alanv    // WebView navigation directions from WebViewCore.h.
916a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_DIRECTION_BACKWARD = 0;
926a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static final int NAVIGATION_DIRECTION_FORWARD = 1;
936a62b77dfb95364e863b44662c13d6deec47f7d2alanv
94525823a7548d73c9ec9ba3ae84663ab286b20205alanv    // these are the same for all instances so make them process wide
95525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static ArrayList<AccessibilityWebContentKeyBinding> sBindings =
96525823a7548d73c9ec9ba3ae84663ab286b20205alanv        new ArrayList<AccessibilityWebContentKeyBinding>();
97525823a7548d73c9ec9ba3ae84663ab286b20205alanv
98525823a7548d73c9ec9ba3ae84663ab286b20205alanv    // handle to the WebViewClassic this injector is associated with.
99525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private final WebViewClassic mWebView;
1006a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private final WebView mWebViewInternal;
101525823a7548d73c9ec9ba3ae84663ab286b20205alanv
102525823a7548d73c9ec9ba3ae84663ab286b20205alanv    // events scheduled for sending as soon as we receive the selected text
103525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private final Stack<AccessibilityEvent> mScheduledEventStack = new Stack<AccessibilityEvent>();
104525823a7548d73c9ec9ba3ae84663ab286b20205alanv
105525823a7548d73c9ec9ba3ae84663ab286b20205alanv    // the current traversal axis
106525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private int mCurrentAxis = 2; // sentence
107525823a7548d73c9ec9ba3ae84663ab286b20205alanv
108525823a7548d73c9ec9ba3ae84663ab286b20205alanv    // we need to consume the up if we have handled the last down
109525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private boolean mLastDownEventHandled;
110525823a7548d73c9ec9ba3ae84663ab286b20205alanv
111525823a7548d73c9ec9ba3ae84663ab286b20205alanv    // getting two empty selection strings in a row we let the WebView handle the event
112525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private boolean mIsLastSelectionStringNull;
113525823a7548d73c9ec9ba3ae84663ab286b20205alanv
114525823a7548d73c9ec9ba3ae84663ab286b20205alanv    // keep track of last direction
115525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private int mLastDirection;
116525823a7548d73c9ec9ba3ae84663ab286b20205alanv
117525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
118525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Creates a new injector associated with a given {@link WebViewClassic}.
119525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *
120525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param webView The associated WebViewClassic.
121525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
122525823a7548d73c9ec9ba3ae84663ab286b20205alanv    public AccessibilityInjectorFallback(WebViewClassic webView) {
123525823a7548d73c9ec9ba3ae84663ab286b20205alanv        mWebView = webView;
1246a62b77dfb95364e863b44662c13d6deec47f7d2alanv        mWebViewInternal = mWebView.getWebView();
125525823a7548d73c9ec9ba3ae84663ab286b20205alanv        ensureWebContentKeyBindings();
126525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
127525823a7548d73c9ec9ba3ae84663ab286b20205alanv
128525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
129525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Processes a key down <code>event</code>.
130525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *
131525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @return True if the event was processed.
132525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
133525823a7548d73c9ec9ba3ae84663ab286b20205alanv    public boolean onKeyEvent(KeyEvent event) {
134525823a7548d73c9ec9ba3ae84663ab286b20205alanv        // We do not handle ENTER in any circumstances.
135525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (isEnterActionKey(event.getKeyCode())) {
136525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return false;
137525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
138525823a7548d73c9ec9ba3ae84663ab286b20205alanv
139525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (event.getAction() == KeyEvent.ACTION_UP) {
140525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return mLastDownEventHandled;
141525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
142525823a7548d73c9ec9ba3ae84663ab286b20205alanv
143525823a7548d73c9ec9ba3ae84663ab286b20205alanv        mLastDownEventHandled = false;
144525823a7548d73c9ec9ba3ae84663ab286b20205alanv
145525823a7548d73c9ec9ba3ae84663ab286b20205alanv        AccessibilityWebContentKeyBinding binding = null;
146525823a7548d73c9ec9ba3ae84663ab286b20205alanv        for (AccessibilityWebContentKeyBinding candidate : sBindings) {
147525823a7548d73c9ec9ba3ae84663ab286b20205alanv            if (event.getKeyCode() == candidate.getKeyCode()
148525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    && event.hasModifiers(candidate.getModifiers())) {
149525823a7548d73c9ec9ba3ae84663ab286b20205alanv                binding = candidate;
150525823a7548d73c9ec9ba3ae84663ab286b20205alanv                break;
151525823a7548d73c9ec9ba3ae84663ab286b20205alanv            }
152525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
153525823a7548d73c9ec9ba3ae84663ab286b20205alanv
154525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (binding == null) {
155525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return false;
156525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
157525823a7548d73c9ec9ba3ae84663ab286b20205alanv
158525823a7548d73c9ec9ba3ae84663ab286b20205alanv        for (int i = 0, count = binding.getActionCount(); i < count; i++) {
159525823a7548d73c9ec9ba3ae84663ab286b20205alanv            int actionCode = binding.getActionCode(i);
160525823a7548d73c9ec9ba3ae84663ab286b20205alanv            String contentDescription = Integer.toHexString(binding.getAction(i));
161525823a7548d73c9ec9ba3ae84663ab286b20205alanv            switch (actionCode) {
162525823a7548d73c9ec9ba3ae84663ab286b20205alanv                case ACTION_SET_CURRENT_AXIS:
163525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    int axis = binding.getFirstArgument(i);
164525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    boolean sendEvent = (binding.getSecondArgument(i) == 1);
165525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    setCurrentAxis(axis, sendEvent, contentDescription);
166525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    mLastDownEventHandled = true;
167525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    break;
168525823a7548d73c9ec9ba3ae84663ab286b20205alanv                case ACTION_TRAVERSE_CURRENT_AXIS:
169525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    int direction = binding.getFirstArgument(i);
170525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    // on second null selection string in same direction - WebView handles the event
171525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    if (direction == mLastDirection && mIsLastSelectionStringNull) {
172525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        mIsLastSelectionStringNull = false;
173525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        return false;
174525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    }
175525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    mLastDirection = direction;
176525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    sendEvent = (binding.getSecondArgument(i) == 1);
177525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    mLastDownEventHandled = traverseCurrentAxis(direction, sendEvent,
178525823a7548d73c9ec9ba3ae84663ab286b20205alanv                            contentDescription);
179525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    break;
180525823a7548d73c9ec9ba3ae84663ab286b20205alanv                case ACTION_TRAVERSE_GIVEN_AXIS:
181525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    direction = binding.getFirstArgument(i);
182525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    // on second null selection string in same direction => WebView handle the event
183525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    if (direction == mLastDirection && mIsLastSelectionStringNull) {
184525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        mIsLastSelectionStringNull = false;
185525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        return false;
186525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    }
187525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    mLastDirection = direction;
188525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    axis =  binding.getSecondArgument(i);
189525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    sendEvent = (binding.getThirdArgument(i) == 1);
190525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    traverseGivenAxis(direction, axis, sendEvent, contentDescription);
191525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    mLastDownEventHandled = true;
192525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    break;
193525823a7548d73c9ec9ba3ae84663ab286b20205alanv                case ACTION_PERFORM_AXIS_TRANSITION:
194525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    int fromAxis = binding.getFirstArgument(i);
195525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    int toAxis = binding.getSecondArgument(i);
196525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    sendEvent = (binding.getThirdArgument(i) == 1);
1976a62b77dfb95364e863b44662c13d6deec47f7d2alanv                    performAxisTransition(fromAxis, toAxis, sendEvent, contentDescription);
198525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    mLastDownEventHandled = true;
199525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    break;
200525823a7548d73c9ec9ba3ae84663ab286b20205alanv                case ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS:
201525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    // This is a special case since we treat the default WebView navigation
202525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    // behavior as one of the possible navigation axis the user can use.
203525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    // If we are not on the default WebView navigation axis this is NOP.
204525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    if (mCurrentAxis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) {
205525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        // While WebVew handles navigation we do not get null selection
206525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        // strings so do not check for that here as the cases above.
207525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        mLastDirection = binding.getFirstArgument(i);
208525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        sendEvent = (binding.getSecondArgument(i) == 1);
209525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        traverseGivenAxis(mLastDirection, NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR,
210525823a7548d73c9ec9ba3ae84663ab286b20205alanv                            sendEvent, contentDescription);
211525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        mLastDownEventHandled = false;
212525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    } else {
213525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        mLastDownEventHandled = true;
214525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    }
215525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    break;
216525823a7548d73c9ec9ba3ae84663ab286b20205alanv                default:
217525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    Log.w(LOG_TAG, "Unknown action code: " + actionCode);
218525823a7548d73c9ec9ba3ae84663ab286b20205alanv            }
219525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
220525823a7548d73c9ec9ba3ae84663ab286b20205alanv
221525823a7548d73c9ec9ba3ae84663ab286b20205alanv        return mLastDownEventHandled;
222525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
223525823a7548d73c9ec9ba3ae84663ab286b20205alanv
224525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
225525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Set the current navigation axis which will be used while
226525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * calling {@link #traverseCurrentAxis(int, boolean, String)}.
227525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *
228525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param axis The axis to set.
229525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param sendEvent Whether to send an accessibility event to
230525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *        announce the change.
231525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
232525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private void setCurrentAxis(int axis, boolean sendEvent, String contentDescription) {
233525823a7548d73c9ec9ba3ae84663ab286b20205alanv        mCurrentAxis = axis;
234525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (sendEvent) {
2356a62b77dfb95364e863b44662c13d6deec47f7d2alanv            final AccessibilityEvent event = getPartialyPopulatedAccessibilityEvent(
2366a62b77dfb95364e863b44662c13d6deec47f7d2alanv                    AccessibilityEvent.TYPE_ANNOUNCEMENT);
237525823a7548d73c9ec9ba3ae84663ab286b20205alanv            event.getText().add(String.valueOf(axis));
238525823a7548d73c9ec9ba3ae84663ab286b20205alanv            event.setContentDescription(contentDescription);
239525823a7548d73c9ec9ba3ae84663ab286b20205alanv            sendAccessibilityEvent(event);
240525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
241525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
242525823a7548d73c9ec9ba3ae84663ab286b20205alanv
243525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
244525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Performs conditional transition one axis to another.
245525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *
246525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param fromAxis The axis which must be the current for the transition to occur.
247525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param toAxis The axis to which to transition.
248525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param sendEvent Flag if to send an event to announce successful transition.
249525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param contentDescription A description of the performed action.
250525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
2516a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private void performAxisTransition(int fromAxis, int toAxis, boolean sendEvent,
252525823a7548d73c9ec9ba3ae84663ab286b20205alanv            String contentDescription) {
253525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (mCurrentAxis == fromAxis) {
254525823a7548d73c9ec9ba3ae84663ab286b20205alanv            setCurrentAxis(toAxis, sendEvent, contentDescription);
255525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
256525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
257525823a7548d73c9ec9ba3ae84663ab286b20205alanv
258525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
259525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Traverse the document along the current navigation axis.
260525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *
261525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param direction The direction of traversal.
262525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param sendEvent Whether to send an accessibility event to
263525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *        announce the change.
264525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param contentDescription A description of the performed action.
265525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @see #setCurrentAxis(int, boolean, String)
266525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
267525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private boolean traverseCurrentAxis(int direction, boolean sendEvent,
268525823a7548d73c9ec9ba3ae84663ab286b20205alanv            String contentDescription) {
269525823a7548d73c9ec9ba3ae84663ab286b20205alanv        return traverseGivenAxis(direction, mCurrentAxis, sendEvent, contentDescription);
270525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
2716a62b77dfb95364e863b44662c13d6deec47f7d2alanv
2726a62b77dfb95364e863b44662c13d6deec47f7d2alanv    boolean performAccessibilityAction(int action, Bundle arguments) {
2736a62b77dfb95364e863b44662c13d6deec47f7d2alanv        switch (action) {
2746a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
2758bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv            case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
2766a62b77dfb95364e863b44662c13d6deec47f7d2alanv                final int direction = getDirectionForAction(action);
2776a62b77dfb95364e863b44662c13d6deec47f7d2alanv                final int axis = getAxisForGranularity(arguments.getInt(
2786a62b77dfb95364e863b44662c13d6deec47f7d2alanv                        AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT));
2796a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return traverseGivenAxis(direction, axis, true, null);
2808bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv            }
2818bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv            case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
2828bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv            case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: {
2838bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv                final int direction = getDirectionForAction(action);
2848bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv                // TODO: Add support for moving by object.
2858bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv                final int axis = NAVIGATION_AXIS_SENTENCE;
2868bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv                return traverseGivenAxis(direction, axis, true, null);
2878bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv            }
2886a62b77dfb95364e863b44662c13d6deec47f7d2alanv            default:
2896a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return false;
2906a62b77dfb95364e863b44662c13d6deec47f7d2alanv        }
2916a62b77dfb95364e863b44662c13d6deec47f7d2alanv    }
2926a62b77dfb95364e863b44662c13d6deec47f7d2alanv
2936a62b77dfb95364e863b44662c13d6deec47f7d2alanv    /**
2946a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * Returns the {@link WebView}-defined direction for the given
2956a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * {@link AccessibilityNodeInfo}-defined action.
2966a62b77dfb95364e863b44662c13d6deec47f7d2alanv     *
2976a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * @param action An accessibility action identifier.
2986a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * @return A web view navigation direction.
2996a62b77dfb95364e863b44662c13d6deec47f7d2alanv     */
3006a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static int getDirectionForAction(int action) {
3016a62b77dfb95364e863b44662c13d6deec47f7d2alanv        switch (action) {
3028bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv            case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
3036a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
3046a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return NAVIGATION_DIRECTION_FORWARD;
3058bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv            case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
3066a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
3076a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return NAVIGATION_DIRECTION_BACKWARD;
3086a62b77dfb95364e863b44662c13d6deec47f7d2alanv            default:
3096a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return -1;
3106a62b77dfb95364e863b44662c13d6deec47f7d2alanv        }
3116a62b77dfb95364e863b44662c13d6deec47f7d2alanv    }
3126a62b77dfb95364e863b44662c13d6deec47f7d2alanv
3136a62b77dfb95364e863b44662c13d6deec47f7d2alanv    /**
3146a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * Returns the {@link WebView}-defined axis for the given
3156a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * {@link AccessibilityNodeInfo}-defined granularity.
3166a62b77dfb95364e863b44662c13d6deec47f7d2alanv     *
3176a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * @param granularity An accessibility granularity identifier.
3186a62b77dfb95364e863b44662c13d6deec47f7d2alanv     * @return A web view navigation axis.
3196a62b77dfb95364e863b44662c13d6deec47f7d2alanv     */
3206a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private static int getAxisForGranularity(int granularity) {
3216a62b77dfb95364e863b44662c13d6deec47f7d2alanv        switch (granularity) {
3226a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
3236a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return NAVIGATION_AXIS_CHARACTER;
3246a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
3256a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return NAVIGATION_AXIS_WORD;
3266a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
3276a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return NAVIGATION_AXIS_SENTENCE;
3286a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH:
3298bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv                // TODO: This should map to object once we implement it.
3308bab6de6e2c3af2d6fac0ebd06bcdb71a5d65b03alanv                return NAVIGATION_AXIS_SENTENCE;
3316a62b77dfb95364e863b44662c13d6deec47f7d2alanv            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE:
3326a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return NAVIGATION_AXIS_DOCUMENT;
3336a62b77dfb95364e863b44662c13d6deec47f7d2alanv            default:
3346a62b77dfb95364e863b44662c13d6deec47f7d2alanv                return -1;
3356a62b77dfb95364e863b44662c13d6deec47f7d2alanv        }
3366a62b77dfb95364e863b44662c13d6deec47f7d2alanv    }
337525823a7548d73c9ec9ba3ae84663ab286b20205alanv
338525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
339525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Traverse the document along the given navigation axis.
340525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *
341525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param direction The direction of traversal.
342525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param axis The axis along which to traverse.
343525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param sendEvent Whether to send an accessibility event to
344525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *        announce the change.
345525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param contentDescription A description of the performed action.
346525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
347525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private boolean traverseGivenAxis(int direction, int axis, boolean sendEvent,
348525823a7548d73c9ec9ba3ae84663ab286b20205alanv            String contentDescription) {
349525823a7548d73c9ec9ba3ae84663ab286b20205alanv        WebViewCore webViewCore = mWebView.getWebViewCore();
350525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (webViewCore == null) {
351525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return false;
352525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
353525823a7548d73c9ec9ba3ae84663ab286b20205alanv
354525823a7548d73c9ec9ba3ae84663ab286b20205alanv        AccessibilityEvent event = null;
355525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (sendEvent) {
3566a62b77dfb95364e863b44662c13d6deec47f7d2alanv            event = getPartialyPopulatedAccessibilityEvent(
3576a62b77dfb95364e863b44662c13d6deec47f7d2alanv                    AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
358525823a7548d73c9ec9ba3ae84663ab286b20205alanv            // the text will be set upon receiving the selection string
359525823a7548d73c9ec9ba3ae84663ab286b20205alanv            event.setContentDescription(contentDescription);
360525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
361525823a7548d73c9ec9ba3ae84663ab286b20205alanv        mScheduledEventStack.push(event);
362525823a7548d73c9ec9ba3ae84663ab286b20205alanv
363525823a7548d73c9ec9ba3ae84663ab286b20205alanv        // if the axis is the default let WebView handle the event which will
364525823a7548d73c9ec9ba3ae84663ab286b20205alanv        // result in cursor ring movement and selection of its content
365525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (axis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) {
366525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return false;
367525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
368525823a7548d73c9ec9ba3ae84663ab286b20205alanv
369525823a7548d73c9ec9ba3ae84663ab286b20205alanv        webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis);
370525823a7548d73c9ec9ba3ae84663ab286b20205alanv        return true;
371525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
372525823a7548d73c9ec9ba3ae84663ab286b20205alanv
373525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
374525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Called when the <code>selectionString</code> has changed.
375525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
376525823a7548d73c9ec9ba3ae84663ab286b20205alanv    public void onSelectionStringChange(String selectionString) {
377525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (DEBUG) {
378525823a7548d73c9ec9ba3ae84663ab286b20205alanv            Log.d(LOG_TAG, "Selection string: " + selectionString);
379525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
380525823a7548d73c9ec9ba3ae84663ab286b20205alanv        mIsLastSelectionStringNull = (selectionString == null);
381525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (mScheduledEventStack.isEmpty()) {
382525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return;
383525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
384525823a7548d73c9ec9ba3ae84663ab286b20205alanv        AccessibilityEvent event = mScheduledEventStack.pop();
3856a62b77dfb95364e863b44662c13d6deec47f7d2alanv        if ((event != null) && (selectionString != null)) {
386525823a7548d73c9ec9ba3ae84663ab286b20205alanv            event.getText().add(selectionString);
3876a62b77dfb95364e863b44662c13d6deec47f7d2alanv            event.setFromIndex(0);
3886a62b77dfb95364e863b44662c13d6deec47f7d2alanv            event.setToIndex(selectionString.length());
389525823a7548d73c9ec9ba3ae84663ab286b20205alanv            sendAccessibilityEvent(event);
390525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
391525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
392525823a7548d73c9ec9ba3ae84663ab286b20205alanv
393525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
394525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Sends an {@link AccessibilityEvent}.
395525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *
396525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @param event The event to send.
397525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
398525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private void sendAccessibilityEvent(AccessibilityEvent event) {
399525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (DEBUG) {
400525823a7548d73c9ec9ba3ae84663ab286b20205alanv            Log.d(LOG_TAG, "Dispatching: " + event);
401525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
402525823a7548d73c9ec9ba3ae84663ab286b20205alanv        // accessibility may be disabled while waiting for the selection string
403525823a7548d73c9ec9ba3ae84663ab286b20205alanv        AccessibilityManager accessibilityManager =
404525823a7548d73c9ec9ba3ae84663ab286b20205alanv            AccessibilityManager.getInstance(mWebView.getContext());
405525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (accessibilityManager.isEnabled()) {
406525823a7548d73c9ec9ba3ae84663ab286b20205alanv            accessibilityManager.sendAccessibilityEvent(event);
407525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
408525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
409525823a7548d73c9ec9ba3ae84663ab286b20205alanv
410525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
411525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * @return An accessibility event whose members are populated except its
412525823a7548d73c9ec9ba3ae84663ab286b20205alanv     *         text and content description.
413525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
4146a62b77dfb95364e863b44662c13d6deec47f7d2alanv    private AccessibilityEvent getPartialyPopulatedAccessibilityEvent(int eventType) {
4156a62b77dfb95364e863b44662c13d6deec47f7d2alanv        AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
4166a62b77dfb95364e863b44662c13d6deec47f7d2alanv        mWebViewInternal.onInitializeAccessibilityEvent(event);
417525823a7548d73c9ec9ba3ae84663ab286b20205alanv        return event;
418525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
419525823a7548d73c9ec9ba3ae84663ab286b20205alanv
420525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
421525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Ensures that the Web content key bindings are loaded.
422525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
423525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private void ensureWebContentKeyBindings() {
424525823a7548d73c9ec9ba3ae84663ab286b20205alanv        if (sBindings.size() > 0) {
425525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return;
426525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
427525823a7548d73c9ec9ba3ae84663ab286b20205alanv
428525823a7548d73c9ec9ba3ae84663ab286b20205alanv        String webContentKeyBindingsString  = Settings.Secure.getString(
429525823a7548d73c9ec9ba3ae84663ab286b20205alanv                mWebView.getContext().getContentResolver(),
430525823a7548d73c9ec9ba3ae84663ab286b20205alanv                Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS);
431525823a7548d73c9ec9ba3ae84663ab286b20205alanv
432525823a7548d73c9ec9ba3ae84663ab286b20205alanv        SimpleStringSplitter semiColonSplitter = new SimpleStringSplitter(';');
433525823a7548d73c9ec9ba3ae84663ab286b20205alanv        semiColonSplitter.setString(webContentKeyBindingsString);
434525823a7548d73c9ec9ba3ae84663ab286b20205alanv
435525823a7548d73c9ec9ba3ae84663ab286b20205alanv        while (semiColonSplitter.hasNext()) {
436525823a7548d73c9ec9ba3ae84663ab286b20205alanv            String bindingString = semiColonSplitter.next();
437525823a7548d73c9ec9ba3ae84663ab286b20205alanv            if (TextUtils.isEmpty(bindingString)) {
438525823a7548d73c9ec9ba3ae84663ab286b20205alanv                Log.e(LOG_TAG, "Disregarding malformed Web content key binding: "
439525823a7548d73c9ec9ba3ae84663ab286b20205alanv                        + webContentKeyBindingsString);
440525823a7548d73c9ec9ba3ae84663ab286b20205alanv                continue;
441525823a7548d73c9ec9ba3ae84663ab286b20205alanv            }
442525823a7548d73c9ec9ba3ae84663ab286b20205alanv            String[] keyValueArray = bindingString.split("=");
443525823a7548d73c9ec9ba3ae84663ab286b20205alanv            if (keyValueArray.length != 2) {
444525823a7548d73c9ec9ba3ae84663ab286b20205alanv                Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " + bindingString);
445525823a7548d73c9ec9ba3ae84663ab286b20205alanv                continue;
446525823a7548d73c9ec9ba3ae84663ab286b20205alanv            }
447525823a7548d73c9ec9ba3ae84663ab286b20205alanv            try {
448525823a7548d73c9ec9ba3ae84663ab286b20205alanv                long keyCodeAndModifiers = Long.decode(keyValueArray[0].trim());
449525823a7548d73c9ec9ba3ae84663ab286b20205alanv                String[] actionStrings = keyValueArray[1].split(":");
450525823a7548d73c9ec9ba3ae84663ab286b20205alanv                int[] actions = new int[actionStrings.length];
451525823a7548d73c9ec9ba3ae84663ab286b20205alanv                for (int i = 0, count = actions.length; i < count; i++) {
452525823a7548d73c9ec9ba3ae84663ab286b20205alanv                    actions[i] = Integer.decode(actionStrings[i].trim());
453525823a7548d73c9ec9ba3ae84663ab286b20205alanv                }
454525823a7548d73c9ec9ba3ae84663ab286b20205alanv                sBindings.add(new AccessibilityWebContentKeyBinding(keyCodeAndModifiers, actions));
455525823a7548d73c9ec9ba3ae84663ab286b20205alanv            } catch (NumberFormatException nfe) {
456525823a7548d73c9ec9ba3ae84663ab286b20205alanv                Log.e(LOG_TAG, "Disregarding malformed key binding: " + bindingString);
457525823a7548d73c9ec9ba3ae84663ab286b20205alanv            }
458525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
459525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
460525823a7548d73c9ec9ba3ae84663ab286b20205alanv
461525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private boolean isEnterActionKey(int keyCode) {
462525823a7548d73c9ec9ba3ae84663ab286b20205alanv        return keyCode == KeyEvent.KEYCODE_DPAD_CENTER
463525823a7548d73c9ec9ba3ae84663ab286b20205alanv                || keyCode == KeyEvent.KEYCODE_ENTER
464525823a7548d73c9ec9ba3ae84663ab286b20205alanv                || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER;
465525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
466525823a7548d73c9ec9ba3ae84663ab286b20205alanv
467525823a7548d73c9ec9ba3ae84663ab286b20205alanv    /**
468525823a7548d73c9ec9ba3ae84663ab286b20205alanv     * Represents a web content key-binding.
469525823a7548d73c9ec9ba3ae84663ab286b20205alanv     */
470525823a7548d73c9ec9ba3ae84663ab286b20205alanv    private static final class AccessibilityWebContentKeyBinding {
471525823a7548d73c9ec9ba3ae84663ab286b20205alanv
472525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int MODIFIERS_OFFSET = 32;
473525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final long MODIFIERS_MASK = 0xFFFFFFF00000000L;
474525823a7548d73c9ec9ba3ae84663ab286b20205alanv
475525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int KEY_CODE_OFFSET = 0;
476525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final long KEY_CODE_MASK = 0x00000000FFFFFFFFL;
477525823a7548d73c9ec9ba3ae84663ab286b20205alanv
478525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int ACTION_OFFSET = 24;
479525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int ACTION_MASK = 0xFF000000;
480525823a7548d73c9ec9ba3ae84663ab286b20205alanv
481525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int FIRST_ARGUMENT_OFFSET = 16;
482525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int FIRST_ARGUMENT_MASK = 0x00FF0000;
483525823a7548d73c9ec9ba3ae84663ab286b20205alanv
484525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int SECOND_ARGUMENT_OFFSET = 8;
485525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int SECOND_ARGUMENT_MASK = 0x0000FF00;
486525823a7548d73c9ec9ba3ae84663ab286b20205alanv
487525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int THIRD_ARGUMENT_OFFSET = 0;
488525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private static final int THIRD_ARGUMENT_MASK = 0x000000FF;
489525823a7548d73c9ec9ba3ae84663ab286b20205alanv
490525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private final long mKeyCodeAndModifiers;
491525823a7548d73c9ec9ba3ae84663ab286b20205alanv
492525823a7548d73c9ec9ba3ae84663ab286b20205alanv        private final int [] mActionSequence;
493525823a7548d73c9ec9ba3ae84663ab286b20205alanv
494525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
495525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @return The key code of the binding key.
496525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
497525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getKeyCode() {
498525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return (int) ((mKeyCodeAndModifiers & KEY_CODE_MASK) >> KEY_CODE_OFFSET);
499525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
500525823a7548d73c9ec9ba3ae84663ab286b20205alanv
501525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
502525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @return The meta state of the binding key.
503525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
504525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getModifiers() {
505525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return (int) ((mKeyCodeAndModifiers & MODIFIERS_MASK) >> MODIFIERS_OFFSET);
506525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
507525823a7548d73c9ec9ba3ae84663ab286b20205alanv
508525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
509525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @return The number of actions in the key binding.
510525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
511525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getActionCount() {
512525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return mActionSequence.length;
513525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
514525823a7548d73c9ec9ba3ae84663ab286b20205alanv
515525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
516525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @param index The action for a given action <code>index</code>.
517525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
518525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getAction(int index) {
519525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return mActionSequence[index];
520525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
521525823a7548d73c9ec9ba3ae84663ab286b20205alanv
522525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
523525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @param index The action code for a given action <code>index</code>.
524525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
525525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getActionCode(int index) {
526525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return (mActionSequence[index] & ACTION_MASK) >> ACTION_OFFSET;
527525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
528525823a7548d73c9ec9ba3ae84663ab286b20205alanv
529525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
530525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @param index The first argument for a given action <code>index</code>.
531525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
532525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getFirstArgument(int index) {
533525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return (mActionSequence[index] & FIRST_ARGUMENT_MASK) >> FIRST_ARGUMENT_OFFSET;
534525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
535525823a7548d73c9ec9ba3ae84663ab286b20205alanv
536525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
537525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @param index The second argument for a given action <code>index</code>.
538525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
539525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getSecondArgument(int index) {
540525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return (mActionSequence[index] & SECOND_ARGUMENT_MASK) >> SECOND_ARGUMENT_OFFSET;
541525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
542525823a7548d73c9ec9ba3ae84663ab286b20205alanv
543525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
544525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @param index The third argument for a given action <code>index</code>.
545525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
546525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public int getThirdArgument(int index) {
547525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return (mActionSequence[index] & THIRD_ARGUMENT_MASK) >> THIRD_ARGUMENT_OFFSET;
548525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
549525823a7548d73c9ec9ba3ae84663ab286b20205alanv
550525823a7548d73c9ec9ba3ae84663ab286b20205alanv        /**
551525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * Creates a new instance.
552525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @param keyCodeAndModifiers The key for the binding (key and modifiers).
553525823a7548d73c9ec9ba3ae84663ab286b20205alanv         * @param actionSequence The sequence of action for the binding.
554525823a7548d73c9ec9ba3ae84663ab286b20205alanv         */
555525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public AccessibilityWebContentKeyBinding(long keyCodeAndModifiers, int[] actionSequence) {
556525823a7548d73c9ec9ba3ae84663ab286b20205alanv            mKeyCodeAndModifiers = keyCodeAndModifiers;
557525823a7548d73c9ec9ba3ae84663ab286b20205alanv            mActionSequence = actionSequence;
558525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
559525823a7548d73c9ec9ba3ae84663ab286b20205alanv
560525823a7548d73c9ec9ba3ae84663ab286b20205alanv        @Override
561525823a7548d73c9ec9ba3ae84663ab286b20205alanv        public String toString() {
562525823a7548d73c9ec9ba3ae84663ab286b20205alanv            StringBuilder builder = new StringBuilder();
563525823a7548d73c9ec9ba3ae84663ab286b20205alanv            builder.append("modifiers: ");
564525823a7548d73c9ec9ba3ae84663ab286b20205alanv            builder.append(getModifiers());
565525823a7548d73c9ec9ba3ae84663ab286b20205alanv            builder.append(", keyCode: ");
566525823a7548d73c9ec9ba3ae84663ab286b20205alanv            builder.append(getKeyCode());
567525823a7548d73c9ec9ba3ae84663ab286b20205alanv            builder.append(", actions[");
568525823a7548d73c9ec9ba3ae84663ab286b20205alanv            for (int i = 0, count = getActionCount(); i < count; i++) {
569525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append("{actionCode");
570525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(i);
571525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(": ");
572525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(getActionCode(i));
573525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(", firstArgument: ");
574525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(getFirstArgument(i));
575525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(", secondArgument: ");
576525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(getSecondArgument(i));
577525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(", thirdArgument: ");
578525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append(getThirdArgument(i));
579525823a7548d73c9ec9ba3ae84663ab286b20205alanv                builder.append("}");
580525823a7548d73c9ec9ba3ae84663ab286b20205alanv            }
581525823a7548d73c9ec9ba3ae84663ab286b20205alanv            builder.append("]");
582525823a7548d73c9ec9ba3ae84663ab286b20205alanv            return builder.toString();
583525823a7548d73c9ec9ba3ae84663ab286b20205alanv        }
584525823a7548d73c9ec9ba3ae84663ab286b20205alanv    }
585525823a7548d73c9ec9ba3ae84663ab286b20205alanv}
586